diff --git a/core/state_processor.go b/core/state_processor.go
index 2844693a9e..c74a33c378 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -147,9 +147,14 @@ func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPo
}
*usedGas += result.UsedGas
+ return MakeReceipt(evm, result, statedb, blockNumber, blockHash, tx, *usedGas, root), nil
+}
+
+// MakeReceipt generates the receipt object for a transaction given its execution result.
+func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas uint64, root []byte) *types.Receipt {
// Create a new receipt for the transaction, storing the intermediate root and gas used
// by the tx.
- receipt = &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas}
+ receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: usedGas}
if result.Failed() {
receipt.Status = types.ReceiptStatusFailed
} else {
@@ -164,7 +169,7 @@ func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPo
}
// If the transaction created a contract, store the creation address in the receipt.
- if msg.To == nil {
+ if tx.To() == nil {
receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce())
}
@@ -180,7 +185,7 @@ func ApplyTransactionWithEVM(msg *Message, config *params.ChainConfig, gp *GasPo
receipt.BlockHash = blockHash
receipt.BlockNumber = blockNumber
receipt.TransactionIndex = uint(statedb.TxIndex())
- return receipt, err
+ return receipt
}
// ApplyTransaction attempts to apply a transaction to the given state database
diff --git a/core/state_transition.go b/core/state_transition.go
index 37807d3d2b..d285d03fe2 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -142,27 +142,31 @@ type Message struct {
BlobGasFeeCap *big.Int
BlobHashes []common.Hash
- // When SkipAccountChecks is true, the message nonce is not checked against the
- // account nonce in state. It also disables checking that the sender is an EOA.
+ // When SkipNonceChecks is true, the message nonce is not checked against the
+ // account nonce in state.
// This field will be set to true for operations like RPC eth_call.
- SkipAccountChecks bool
+ SkipNonceChecks bool
+
+ // When SkipFromEOACheck is true, the message sender is not checked to be an EOA.
+ SkipFromEOACheck bool
}
// TransactionToMessage converts a transaction into a Message.
func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) {
msg := &Message{
- Nonce: tx.Nonce(),
- GasLimit: tx.Gas(),
- GasPrice: new(big.Int).Set(tx.GasPrice()),
- GasFeeCap: new(big.Int).Set(tx.GasFeeCap()),
- GasTipCap: new(big.Int).Set(tx.GasTipCap()),
- To: tx.To(),
- Value: tx.Value(),
- Data: tx.Data(),
- AccessList: tx.AccessList(),
- SkipAccountChecks: false,
- BlobHashes: tx.BlobHashes(),
- BlobGasFeeCap: tx.BlobGasFeeCap(),
+ Nonce: tx.Nonce(),
+ GasLimit: tx.Gas(),
+ GasPrice: new(big.Int).Set(tx.GasPrice()),
+ GasFeeCap: new(big.Int).Set(tx.GasFeeCap()),
+ GasTipCap: new(big.Int).Set(tx.GasTipCap()),
+ To: tx.To(),
+ Value: tx.Value(),
+ Data: tx.Data(),
+ AccessList: tx.AccessList(),
+ SkipNonceChecks: false,
+ SkipFromEOACheck: false,
+ BlobHashes: tx.BlobHashes(),
+ BlobGasFeeCap: tx.BlobGasFeeCap(),
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
@@ -280,7 +284,7 @@ func (st *StateTransition) buyGas() error {
func (st *StateTransition) preCheck() error {
// Only check transactions that are not fake
msg := st.msg
- if !msg.SkipAccountChecks {
+ if !msg.SkipNonceChecks {
// Make sure this transaction's nonce is correct.
stNonce := st.state.GetNonce(msg.From)
if msgNonce := msg.Nonce; stNonce < msgNonce {
@@ -293,6 +297,8 @@ func (st *StateTransition) preCheck() error {
return fmt.Errorf("%w: address %v, nonce: %d", ErrNonceMax,
msg.From.Hex(), stNonce)
}
+ }
+ if !msg.SkipFromEOACheck {
// Make sure the sender is an EOA
codeHash := st.state.GetCodeHash(msg.From)
if codeHash != (common.Hash{}) && codeHash != types.EmptyCodeHash {
diff --git a/core/vm/contracts.go b/core/vm/contracts.go
index dd71a9729f..104d2ba814 100644
--- a/core/vm/contracts.go
+++ b/core/vm/contracts.go
@@ -21,6 +21,7 @@ import (
"encoding/binary"
"errors"
"fmt"
+ "maps"
"math/big"
"github.com/consensys/gnark-crypto/ecc"
@@ -46,9 +47,12 @@ type PrecompiledContract interface {
Run(input []byte) ([]byte, error) // Run runs the precompiled contract
}
+// PrecompiledContracts contains the precompiled contracts supported at the given fork.
+type PrecompiledContracts map[common.Address]PrecompiledContract
+
// PrecompiledContractsHomestead contains the default set of pre-compiled Ethereum
// contracts used in the Frontier and Homestead releases.
-var PrecompiledContractsHomestead = map[common.Address]PrecompiledContract{
+var PrecompiledContractsHomestead = PrecompiledContracts{
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
@@ -57,7 +61,7 @@ var PrecompiledContractsHomestead = map[common.Address]PrecompiledContract{
// PrecompiledContractsByzantium contains the default set of pre-compiled Ethereum
// contracts used in the Byzantium release.
-var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{
+var PrecompiledContractsByzantium = PrecompiledContracts{
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
@@ -70,7 +74,7 @@ var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{
// PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum
// contracts used in the Istanbul release.
-var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{
+var PrecompiledContractsIstanbul = PrecompiledContracts{
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
@@ -84,7 +88,7 @@ var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{
// PrecompiledContractsBerlin contains the default set of pre-compiled Ethereum
// contracts used in the Berlin release.
-var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{
+var PrecompiledContractsBerlin = PrecompiledContracts{
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
@@ -98,7 +102,7 @@ var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{
// PrecompiledContractsCancun contains the default set of pre-compiled Ethereum
// contracts used in the Cancun release.
-var PrecompiledContractsCancun = map[common.Address]PrecompiledContract{
+var PrecompiledContractsCancun = PrecompiledContracts{
common.BytesToAddress([]byte{0x1}): &ecrecover{},
common.BytesToAddress([]byte{0x2}): &sha256hash{},
common.BytesToAddress([]byte{0x3}): &ripemd160hash{},
@@ -113,7 +117,7 @@ var PrecompiledContractsCancun = map[common.Address]PrecompiledContract{
// PrecompiledContractsPrague contains the set of pre-compiled Ethereum
// contracts used in the Prague release.
-var PrecompiledContractsPrague = map[common.Address]PrecompiledContract{
+var PrecompiledContractsPrague = PrecompiledContracts{
common.BytesToAddress([]byte{0x01}): &ecrecover{},
common.BytesToAddress([]byte{0x02}): &sha256hash{},
common.BytesToAddress([]byte{0x03}): &ripemd160hash{},
@@ -169,7 +173,31 @@ func init() {
}
}
-// ActivePrecompiles returns the precompiles enabled with the current configuration.
+func activePrecompiledContracts(rules params.Rules) PrecompiledContracts {
+ switch {
+ case rules.IsVerkle:
+ return PrecompiledContractsVerkle
+ case rules.IsPrague:
+ return PrecompiledContractsPrague
+ case rules.IsCancun:
+ return PrecompiledContractsCancun
+ case rules.IsBerlin:
+ return PrecompiledContractsBerlin
+ case rules.IsIstanbul:
+ return PrecompiledContractsIstanbul
+ case rules.IsByzantium:
+ return PrecompiledContractsByzantium
+ default:
+ return PrecompiledContractsHomestead
+ }
+}
+
+// ActivePrecompiledContracts returns a copy of precompiled contracts enabled with the current configuration.
+func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts {
+ return maps.Clone(activePrecompiledContracts(rules))
+}
+
+// ActivePrecompiles returns the precompile addresses enabled with the current configuration.
func ActivePrecompiles(rules params.Rules) []common.Address {
switch {
case rules.IsPrague:
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 7617d843c7..a9c4ae9fe5 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -41,24 +41,7 @@ type (
)
func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) {
- var precompiles map[common.Address]PrecompiledContract
- switch {
- case evm.chainRules.IsVerkle:
- precompiles = PrecompiledContractsVerkle
- case evm.chainRules.IsPrague:
- precompiles = PrecompiledContractsPrague
- case evm.chainRules.IsCancun:
- precompiles = PrecompiledContractsCancun
- case evm.chainRules.IsBerlin:
- precompiles = PrecompiledContractsBerlin
- case evm.chainRules.IsIstanbul:
- precompiles = PrecompiledContractsIstanbul
- case evm.chainRules.IsByzantium:
- precompiles = PrecompiledContractsByzantium
- default:
- precompiles = PrecompiledContractsHomestead
- }
- p, ok := precompiles[addr]
+ p, ok := evm.precompiles[addr]
return p, ok
}
@@ -129,22 +112,13 @@ type EVM struct {
// available gas is calculated in gasCall* according to the 63/64 rule and later
// applied in opCall*.
callGasTemp uint64
+ // precompiles holds the precompiled contracts for the current epoch
+ precompiles map[common.Address]PrecompiledContract
}
// NewEVM returns a new EVM. The returned EVM is not thread safe and should
// only ever be used *once*.
func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM {
- // If basefee tracking is disabled (eth_call, eth_estimateGas, etc), and no
- // gas prices were specified, lower the basefee to 0 to avoid breaking EVM
- // invariants (basefee < feecap)
- if config.NoBaseFee {
- if txCtx.GasPrice.BitLen() == 0 {
- blockCtx.BaseFee = new(big.Int)
- }
- if txCtx.BlobFeeCap != nil && txCtx.BlobFeeCap.BitLen() == 0 {
- blockCtx.BlobBaseFee = new(big.Int)
- }
- }
evm := &EVM{
Context: blockCtx,
TxContext: txCtx,
@@ -153,10 +127,18 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
chainConfig: chainConfig,
chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
}
+ evm.precompiles = activePrecompiledContracts(evm.chainRules)
evm.interpreter = NewEVMInterpreter(evm)
return evm
}
+// SetPrecompiles sets the precompiled contracts for the EVM.
+// This method is only used through RPC calls.
+// It is not thread-safe.
+func (evm *EVM) SetPrecompiles(precompiles PrecompiledContracts) {
+ evm.precompiles = precompiles
+}
+
// Reset resets the EVM with a new transaction context.Reset
// This is not threadsafe and should only be done very cautiously.
func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) {
diff --git a/eth/gasestimator/gasestimator.go b/eth/gasestimator/gasestimator.go
index ac3b59e97e..d43057dda2 100644
--- a/eth/gasestimator/gasestimator.go
+++ b/eth/gasestimator/gasestimator.go
@@ -221,8 +221,16 @@ func run(ctx context.Context, call *core.Message, opts *Options) (*core.Executio
evmContext = core.NewEVMBlockContext(opts.Header, opts.Chain, nil)
dirtyState = opts.State.Copy()
- evm = vm.NewEVM(evmContext, msgContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true})
)
+ // Lower the basefee to 0 to avoid breaking EVM
+ // invariants (basefee < feecap).
+ if msgContext.GasPrice.Sign() == 0 {
+ evmContext.BaseFee = new(big.Int)
+ }
+ if msgContext.BlobFeeCap != nil && msgContext.BlobFeeCap.BitLen() == 0 {
+ evmContext.BlobBaseFee = new(big.Int)
+ }
+ evm := vm.NewEVM(evmContext, msgContext, dirtyState, opts.Config, vm.Config{NoBaseFee: true})
// Monitor the outer context and interrupt the EVM upon cancellation. To avoid
// a dangling goroutine until the outer estimation finishes, create an internal
// context for the lifetime of this method call.
diff --git a/eth/tracers/api.go b/eth/tracers/api.go
index 9ee108d0f1..a828951206 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -22,6 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "math/big"
"os"
"runtime"
"sync"
@@ -955,20 +956,31 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
// Apply the customization rules if required.
if config != nil {
- if err := config.StateOverrides.Apply(statedb); err != nil {
+ config.BlockOverrides.Apply(&vmctx)
+ rules := api.backend.ChainConfig().Rules(vmctx.BlockNumber, vmctx.Random != nil, vmctx.Time)
+
+ precompiles := vm.ActivePrecompiledContracts(rules)
+ if err := config.StateOverrides.Apply(statedb, precompiles); err != nil {
return nil, err
}
- config.BlockOverrides.Apply(&vmctx)
}
// Execute the trace
if err := args.CallDefaults(api.backend.RPCGasCap(), vmctx.BaseFee, api.backend.ChainConfig().ChainID); err != nil {
return nil, err
}
var (
- msg = args.ToMessage(vmctx.BaseFee)
- tx = args.ToTransaction()
+ msg = args.ToMessage(vmctx.BaseFee, true, true)
+ tx = args.ToTransaction(types.LegacyTxType)
traceConfig *TraceConfig
)
+ // Lower the basefee to 0 to avoid breaking EVM
+ // invariants (basefee < feecap).
+ if msg.GasPrice.Sign() == 0 {
+ vmctx.BaseFee = new(big.Int)
+ }
+ if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
+ vmctx.BlobBaseFee = new(big.Int)
+ }
if config != nil {
traceConfig = &config.TraceConfig
}
diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go
index 2f3229cedc..1b7e26fb74 100644
--- a/ethclient/ethclient_test.go
+++ b/ethclient/ethclient_test.go
@@ -219,7 +219,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
t.Fatalf("can't create new node: %v", err)
}
// Create Ethereum Service
- config := ðconfig.Config{Genesis: genesis}
+ config := ðconfig.Config{Genesis: genesis, RPCGasCap: 1000000}
ethservice, err := eth.New(n, config)
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)
diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go
index b1678b6766..02b2598b37 100644
--- a/ethclient/gethclient/gethclient.go
+++ b/ethclient/gethclient/gethclient.go
@@ -328,9 +328,9 @@ func (o BlockOverrides) MarshalJSON() ([]byte, error) {
Difficulty *hexutil.Big `json:"difficulty,omitempty"`
Time hexutil.Uint64 `json:"time,omitempty"`
GasLimit hexutil.Uint64 `json:"gasLimit,omitempty"`
- Coinbase *common.Address `json:"coinbase,omitempty"`
- Random *common.Hash `json:"random,omitempty"`
- BaseFee *hexutil.Big `json:"baseFee,omitempty"`
+ Coinbase *common.Address `json:"feeRecipient,omitempty"`
+ Random *common.Hash `json:"prevRandao,omitempty"`
+ BaseFee *hexutil.Big `json:"baseFeePerGas,omitempty"`
}
output := override{
diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go
index 59ad370146..36ea290a85 100644
--- a/ethclient/gethclient/gethclient_test.go
+++ b/ethclient/gethclient/gethclient_test.go
@@ -57,7 +57,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) {
t.Fatalf("can't create new node: %v", err)
}
// Create Ethereum Service
- config := ðconfig.Config{Genesis: genesis}
+ config := ðconfig.Config{Genesis: genesis, RPCGasCap: 1000000}
ethservice, err := eth.New(n, config)
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)
@@ -510,7 +510,7 @@ func TestBlockOverridesMarshal(t *testing.T) {
bo: BlockOverrides{
Coinbase: common.HexToAddress("0x1111111111111111111111111111111111111111"),
},
- want: `{"coinbase":"0x1111111111111111111111111111111111111111"}`,
+ want: `{"feeRecipient":"0x1111111111111111111111111111111111111111"}`,
},
{
bo: BlockOverrides{
@@ -520,7 +520,7 @@ func TestBlockOverridesMarshal(t *testing.T) {
GasLimit: 4,
BaseFee: big.NewInt(5),
},
- want: `{"number":"0x1","difficulty":"0x2","time":"0x3","gasLimit":"0x4","baseFee":"0x5"}`,
+ want: `{"number":"0x1","difficulty":"0x2","time":"0x3","gasLimit":"0x4","baseFeePerGas":"0x5"}`,
},
} {
marshalled, err := json.Marshal(&tt.bo)
diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go
index f3f9d1778a..ae3a58a983 100644
--- a/graphql/graphql_test.go
+++ b/graphql/graphql_test.go
@@ -453,6 +453,7 @@ func newGQLService(t *testing.T, stack *node.Node, shanghai bool, gspec *core.Ge
TrieDirtyCache: 5,
TrieTimeout: 60 * time.Minute,
SnapshotCache: 5,
+ RPCGasCap: 1000000,
StateScheme: rawdb.HashScheme,
}
var engine consensus.Engine = ethash.NewFaker()
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index aeb3e8adc2..79f94892c5 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -21,6 +21,7 @@ import (
"encoding/hex"
"errors"
"fmt"
+ "maps"
"math/big"
"strings"
"time"
@@ -474,7 +475,7 @@ func (api *PersonalAccountAPI) signTransaction(ctx context.Context, args *Transa
return nil, err
}
// Assemble the transaction and sign with the wallet
- tx := args.ToTransaction()
+ tx := args.ToTransaction(types.LegacyTxType)
return wallet.SignTxWithPassphrase(account, passwd, tx, api.b.ChainConfig().ChainID)
}
@@ -523,7 +524,7 @@ func (api *PersonalAccountAPI) SignTransaction(ctx context.Context, args Transac
return nil, errors.New("nonce not specified")
}
// Before actually signing the transaction, ensure the transaction fee is reasonable.
- tx := args.ToTransaction()
+ tx := args.ToTransaction(types.LegacyTxType)
if err := checkTxFee(tx.GasPrice(), tx.Gas(), api.b.RPCTxFeeCap()); err != nil {
return nil, err
}
@@ -968,22 +969,54 @@ func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rp
// if stateDiff is set, all diff will be applied first and then execute the call
// message.
type OverrideAccount struct {
- Nonce *hexutil.Uint64 `json:"nonce"`
- Code *hexutil.Bytes `json:"code"`
- Balance *hexutil.Big `json:"balance"`
- State map[common.Hash]common.Hash `json:"state"`
- StateDiff map[common.Hash]common.Hash `json:"stateDiff"`
+ Nonce *hexutil.Uint64 `json:"nonce"`
+ Code *hexutil.Bytes `json:"code"`
+ Balance *hexutil.Big `json:"balance"`
+ State map[common.Hash]common.Hash `json:"state"`
+ StateDiff map[common.Hash]common.Hash `json:"stateDiff"`
+ MovePrecompileTo *common.Address `json:"movePrecompileToAddress"`
}
// StateOverride is the collection of overridden accounts.
type StateOverride map[common.Address]OverrideAccount
+func (diff *StateOverride) has(address common.Address) bool {
+ _, ok := (*diff)[address]
+ return ok
+}
+
// Apply overrides the fields of specified accounts into the given state.
-func (diff *StateOverride) Apply(statedb *state.StateDB) error {
+func (diff *StateOverride) Apply(statedb *state.StateDB, precompiles vm.PrecompiledContracts) error {
if diff == nil {
return nil
}
+ // Tracks destinations of precompiles that were moved.
+ dirtyAddrs := make(map[common.Address]struct{})
for addr, account := range *diff {
+ // If a precompile was moved to this address already, it can't be overridden.
+ if _, ok := dirtyAddrs[addr]; ok {
+ return fmt.Errorf("account %s has already been overridden by a precompile", addr.Hex())
+ }
+ p, isPrecompile := precompiles[addr]
+ // The MoveTo feature makes it possible to move a precompile
+ // code to another address. If the target address is another precompile
+ // the code for the latter is lost for this session.
+ // Note the destination account is not cleared upon move.
+ if account.MovePrecompileTo != nil {
+ if !isPrecompile {
+ return fmt.Errorf("account %s is not a precompile", addr.Hex())
+ }
+ // Refuse to move a precompile to an address that has been
+ // or will be overridden.
+ if diff.has(*account.MovePrecompileTo) {
+ return fmt.Errorf("account %s is already overridden", account.MovePrecompileTo.Hex())
+ }
+ precompiles[*account.MovePrecompileTo] = p
+ dirtyAddrs[*account.MovePrecompileTo] = struct{}{}
+ }
+ if isPrecompile {
+ delete(precompiles, addr)
+ }
// Override account nonce.
if account.Nonce != nil {
statedb.SetNonce(addr, uint64(*account.Nonce))
@@ -1020,47 +1053,80 @@ func (diff *StateOverride) Apply(statedb *state.StateDB) error {
// BlockOverrides is a set of header fields to override.
type BlockOverrides struct {
- Number *hexutil.Big
- Difficulty *hexutil.Big
- Time *hexutil.Uint64
- GasLimit *hexutil.Uint64
- Coinbase *common.Address
- Random *common.Hash
- BaseFee *hexutil.Big
- BlobBaseFee *hexutil.Big
+ Number *hexutil.Big
+ Difficulty *hexutil.Big // No-op if we're simulating post-merge calls.
+ Time *hexutil.Uint64
+ GasLimit *hexutil.Uint64
+ FeeRecipient *common.Address
+ PrevRandao *common.Hash
+ BaseFeePerGas *hexutil.Big
+ BlobBaseFee *hexutil.Big
}
// Apply overrides the given header fields into the given block context.
-func (diff *BlockOverrides) Apply(blockCtx *vm.BlockContext) {
- if diff == nil {
+func (o *BlockOverrides) Apply(blockCtx *vm.BlockContext) {
+ if o == nil {
return
}
- if diff.Number != nil {
- blockCtx.BlockNumber = diff.Number.ToInt()
+ if o.Number != nil {
+ blockCtx.BlockNumber = o.Number.ToInt()
}
- if diff.Difficulty != nil {
- blockCtx.Difficulty = diff.Difficulty.ToInt()
+ if o.Difficulty != nil {
+ blockCtx.Difficulty = o.Difficulty.ToInt()
}
- if diff.Time != nil {
- blockCtx.Time = uint64(*diff.Time)
+ if o.Time != nil {
+ blockCtx.Time = uint64(*o.Time)
}
- if diff.GasLimit != nil {
- blockCtx.GasLimit = uint64(*diff.GasLimit)
+ if o.GasLimit != nil {
+ blockCtx.GasLimit = uint64(*o.GasLimit)
}
- if diff.Coinbase != nil {
- blockCtx.Coinbase = *diff.Coinbase
+ if o.FeeRecipient != nil {
+ blockCtx.Coinbase = *o.FeeRecipient
}
- if diff.Random != nil {
- blockCtx.Random = diff.Random
+ if o.PrevRandao != nil {
+ blockCtx.Random = o.PrevRandao
}
- if diff.BaseFee != nil {
- blockCtx.BaseFee = diff.BaseFee.ToInt()
+ if o.BaseFeePerGas != nil {
+ blockCtx.BaseFee = o.BaseFeePerGas.ToInt()
}
- if diff.BlobBaseFee != nil {
- blockCtx.BlobBaseFee = diff.BlobBaseFee.ToInt()
+ if o.BlobBaseFee != nil {
+ blockCtx.BlobBaseFee = o.BlobBaseFee.ToInt()
}
}
+// MakeHeader returns a new header object with the overridden
+// fields.
+// Note: MakeHeader ignores BlobBaseFee if set. That's because
+// header has no such field.
+func (o *BlockOverrides) MakeHeader(header *types.Header) *types.Header {
+ if o == nil {
+ return header
+ }
+ h := types.CopyHeader(header)
+ if o.Number != nil {
+ h.Number = o.Number.ToInt()
+ }
+ if o.Difficulty != nil {
+ h.Difficulty = o.Difficulty.ToInt()
+ }
+ if o.Time != nil {
+ h.Time = uint64(*o.Time)
+ }
+ if o.GasLimit != nil {
+ h.GasLimit = uint64(*o.GasLimit)
+ }
+ if o.FeeRecipient != nil {
+ h.Coinbase = *o.FeeRecipient
+ }
+ if o.PrevRandao != nil {
+ h.MixDigest = *o.PrevRandao
+ }
+ if o.BaseFeePerGas != nil {
+ h.BaseFee = o.BaseFeePerGas.ToInt()
+ }
+ return h
+}
+
// ChainContextBackend provides methods required to implement ChainContext.
type ChainContextBackend interface {
Engine() consensus.Engine
@@ -1094,9 +1160,16 @@ func (context *ChainContext) GetHeader(hash common.Hash, number uint64) *types.H
}
func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *StateOverride, blockOverrides *BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
- if err := overrides.Apply(state); err != nil {
+ blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
+ if blockOverrides != nil {
+ blockOverrides.Apply(&blockCtx)
+ }
+ rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time)
+ precompiles := maps.Clone(vm.ActivePrecompiledContracts(rules))
+ if err := overrides.Apply(state, precompiles); err != nil {
return nil, err
}
+
// Setup context so it may be cancelled the call has completed
// or, in case of unmetered gas, setup a context with a timeout.
var cancel context.CancelFunc
@@ -1108,18 +1181,32 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S
// Make sure the context is cancelled when the call has completed
// this makes sure resources are cleaned up.
defer cancel()
+ return applyMessage(ctx, b, args, state, header, timeout, new(core.GasPool).AddGas(globalGasCap), &blockCtx, &vm.Config{NoBaseFee: true}, precompiles, true)
+}
+func applyMessage(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, timeout time.Duration, gp *core.GasPool, blockContext *vm.BlockContext, vmConfig *vm.Config, precompiles vm.PrecompiledContracts, skipChecks bool) (*core.ExecutionResult, error) {
// Get a new instance of the EVM.
- blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
- if blockOverrides != nil {
- blockOverrides.Apply(&blockCtx)
- }
- if err := args.CallDefaults(globalGasCap, blockCtx.BaseFee, b.ChainConfig().ChainID); err != nil {
+ if err := args.CallDefaults(gp.Gas(), blockContext.BaseFee, b.ChainConfig().ChainID); err != nil {
return nil, err
}
- msg := args.ToMessage(blockCtx.BaseFee)
- evm := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, &blockCtx)
+ msg := args.ToMessage(header.BaseFee, skipChecks, skipChecks)
+ // Lower the basefee to 0 to avoid breaking EVM
+ // invariants (basefee < feecap).
+ if msg.GasPrice.Sign() == 0 {
+ blockContext.BaseFee = new(big.Int)
+ }
+ if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
+ blockContext.BlobBaseFee = new(big.Int)
+ }
+ evm := b.GetEVM(ctx, msg, state, header, vmConfig, blockContext)
+ if precompiles != nil {
+ evm.SetPrecompiles(precompiles)
+ }
+ return applyMessageWithEVM(ctx, evm, msg, state, timeout, gp)
+}
+
+func applyMessageWithEVM(ctx context.Context, evm *vm.EVM, msg *core.Message, state *state.StateDB, timeout time.Duration, gp *core.GasPool) (*core.ExecutionResult, error) {
// Wait for the context to be done and cancel the evm. Even if the
// EVM has finished, cancelling may be done (repeatedly)
go func() {
@@ -1128,7 +1215,6 @@ func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.S
}()
// Execute the message.
- gp := new(core.GasPool).AddGas(math.MaxUint64)
result, err := core.ApplyMessage(evm, msg, gp)
if err := state.Error(); err != nil {
return nil, err
@@ -1151,7 +1237,6 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash
if state == nil || err != nil {
return nil, err
}
-
return doCall(ctx, b, args, state, header, overrides, blockOverrides, timeout, globalGasCap)
}
@@ -1177,6 +1262,41 @@ func (api *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockN
return result.Return(), result.Err
}
+// SimulateV1 executes series of transactions on top of a base state.
+// The transactions are packed into blocks. For each block, block header
+// fields can be overridden. The state can also be overridden prior to
+// execution of each block.
+//
+// Note, this function doesn't make any changes in the state/blockchain and is
+// useful to execute and retrieve values.
+func (api *BlockChainAPI) SimulateV1(ctx context.Context, opts simOpts, blockNrOrHash *rpc.BlockNumberOrHash) ([]map[string]interface{}, error) {
+ if len(opts.BlockStateCalls) == 0 {
+ return nil, &invalidParamsError{message: "empty input"}
+ } else if len(opts.BlockStateCalls) > maxSimulateBlocks {
+ return nil, &clientLimitExceededError{message: "too many blocks"}
+ }
+ if blockNrOrHash == nil {
+ n := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ blockNrOrHash = &n
+ }
+ state, base, err := api.b.StateAndHeaderByNumberOrHash(ctx, *blockNrOrHash)
+ if state == nil || err != nil {
+ return nil, err
+ }
+ sim := &simulator{
+ b: api.b,
+ state: state,
+ base: base,
+ chainConfig: api.b.ChainConfig(),
+ // Each tx and all the series of txes shouldn't consume more gas than cap
+ gp: new(core.GasPool).AddGas(api.b.RPCGasCap()),
+ traceTransfers: opts.TraceTransfers,
+ validate: opts.Validation,
+ fullTx: opts.ReturnFullTransactions,
+ }
+ return sim.execute(ctx, opts.BlockStateCalls)
+}
+
// DoEstimateGas returns the lowest possible gas limit that allows the transaction to run
// successfully at block `blockNrOrHash`. It returns error if the transaction would revert, or if
// there are unexpected failures. The gas limit is capped by both `args.Gas` (if non-nil &
@@ -1187,7 +1307,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
if state == nil || err != nil {
return 0, err
}
- if err = overrides.Apply(state); err != nil {
+ if err := overrides.Apply(state, nil); err != nil {
return 0, err
}
// Construct the gas estimator option from the user input
@@ -1206,7 +1326,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
if err := args.CallDefaults(gasCap, header.BaseFee, b.ChainConfig().ChainID); err != nil {
return 0, err
}
- call := args.ToMessage(header.BaseFee)
+ call := args.ToMessage(header.BaseFee, true, true)
// Run the gas estimation and wrap any revertals into a custom return
estimate, revert, err := gasestimator.Estimate(ctx, call, opts, gasCap)
@@ -1546,15 +1666,23 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
statedb := db.Copy()
// Set the accesslist to the last al
args.AccessList = &accessList
- msg := args.ToMessage(header.BaseFee)
+ msg := args.ToMessage(header.BaseFee, true, true)
// Apply the transaction with the access list tracer
tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles)
config := vm.Config{Tracer: tracer.Hooks(), NoBaseFee: true}
vmenv := b.GetEVM(ctx, msg, statedb, header, &config, nil)
+ // Lower the basefee to 0 to avoid breaking EVM
+ // invariants (basefee < feecap).
+ if msg.GasPrice.Sign() == 0 {
+ vmenv.Context.BaseFee = new(big.Int)
+ }
+ if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 {
+ vmenv.Context.BlobBaseFee = new(big.Int)
+ }
res, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit))
if err != nil {
- return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.ToTransaction().Hash(), err)
+ return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.ToTransaction(types.LegacyTxType).Hash(), err)
}
if tracer.Equal(prevTracer) {
return accessList, res.UsedGas, res.Err, nil
@@ -1823,7 +1951,7 @@ func (api *TransactionAPI) SendTransaction(ctx context.Context, args Transaction
return common.Hash{}, err
}
// Assemble the transaction and sign with the wallet
- tx := args.ToTransaction()
+ tx := args.ToTransaction(types.LegacyTxType)
signed, err := wallet.SignTx(account, tx, api.b.ChainConfig().ChainID)
if err != nil {
@@ -1843,7 +1971,7 @@ func (api *TransactionAPI) FillTransaction(ctx context.Context, args Transaction
return nil, err
}
// Assemble the transaction and obtain rlp
- tx := args.ToTransaction()
+ tx := args.ToTransaction(types.LegacyTxType)
data, err := tx.MarshalBinary()
if err != nil {
return nil, err
@@ -1911,7 +2039,7 @@ func (api *TransactionAPI) SignTransaction(ctx context.Context, args Transaction
return nil, err
}
// Before actually sign the transaction, ensure the transaction fee is reasonable.
- tx := args.ToTransaction()
+ tx := args.ToTransaction(types.LegacyTxType)
if err := checkTxFee(tx.GasPrice(), tx.Gas(), api.b.RPCTxFeeCap()); err != nil {
return nil, err
}
@@ -1969,7 +2097,7 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs,
if err := sendArgs.setDefaults(ctx, api.b, false); err != nil {
return common.Hash{}, err
}
- matchTx := sendArgs.ToTransaction()
+ matchTx := sendArgs.ToTransaction(types.LegacyTxType)
// Before replacing the old transaction, ensure the _new_ transaction fee is reasonable.
var price = matchTx.GasPrice()
@@ -1999,7 +2127,7 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs,
if gasLimit != nil && *gasLimit != 0 {
sendArgs.Gas = gasLimit
}
- signedTx, err := api.sign(sendArgs.from(), sendArgs.ToTransaction())
+ signedTx, err := api.sign(sendArgs.from(), sendArgs.ToTransaction(types.LegacyTxType))
if err != nil {
return common.Hash{}, err
}
diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go
index 3c5da840d3..f2dd93e4ac 100644
--- a/internal/ethapi/api_test.go
+++ b/internal/ethapi/api_test.go
@@ -24,17 +24,16 @@ import (
"encoding/json"
"errors"
"fmt"
+ "maps"
"math/big"
"os"
"path/filepath"
"reflect"
"slices"
+ "strings"
"testing"
"time"
- "github.com/holiman/uint256"
- "github.com/stretchr/testify/require"
-
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
@@ -56,6 +55,9 @@ import (
"github.com/ethereum/go-ethereum/internal/blocktest"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
+ "github.com/ethereum/go-ethereum/triedb"
+ "github.com/holiman/uint256"
+ "github.com/stretchr/testify/require"
)
func testTransactionMarshal(t *testing.T, tests []txData, config *params.ChainConfig) {
@@ -502,7 +504,8 @@ func (b testBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc
}
panic("unknown type rpc.BlockNumberOrHash")
}
-func (b testBackend) CurrentHeader() *types.Header { return b.chain.CurrentBlock() }
+
+func (b testBackend) CurrentHeader() *types.Header { return b.chain.CurrentHeader() }
func (b testBackend) CurrentBlock() *types.Header { return b.chain.CurrentBlock() }
func (b testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
if number == rpc.LatestBlockNumber {
@@ -814,6 +817,7 @@ func TestCall(t *testing.T) {
}))
randomAccounts := newAccounts(3)
var testSuite = []struct {
+ name string
blockNumber rpc.BlockNumber
overrides StateOverride
call TransactionArgs
@@ -823,6 +827,7 @@ func TestCall(t *testing.T) {
}{
// transfer on genesis
{
+ name: "transfer-on-genesis",
blockNumber: rpc.BlockNumber(0),
call: TransactionArgs{
From: &accounts[0].addr,
@@ -834,6 +839,7 @@ func TestCall(t *testing.T) {
},
// transfer on the head
{
+ name: "transfer-on-the-head",
blockNumber: rpc.BlockNumber(genBlocks),
call: TransactionArgs{
From: &accounts[0].addr,
@@ -845,6 +851,7 @@ func TestCall(t *testing.T) {
},
// transfer on a non-existent block, error expects
{
+ name: "transfer-non-existent-block",
blockNumber: rpc.BlockNumber(genBlocks + 1),
call: TransactionArgs{
From: &accounts[0].addr,
@@ -855,6 +862,7 @@ func TestCall(t *testing.T) {
},
// transfer on the latest block
{
+ name: "transfer-latest-block",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &accounts[0].addr,
@@ -866,6 +874,7 @@ func TestCall(t *testing.T) {
},
// Call which can only succeed if state is state overridden
{
+ name: "state-override-success",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &randomAccounts[0].addr,
@@ -879,6 +888,7 @@ func TestCall(t *testing.T) {
},
// Invalid call without state overriding
{
+ name: "insufficient-funds-simple",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &randomAccounts[0].addr,
@@ -904,6 +914,7 @@ func TestCall(t *testing.T) {
// }
// }
{
+ name: "simple-contract-call",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &randomAccounts[0].addr,
@@ -920,6 +931,7 @@ func TestCall(t *testing.T) {
},
// Block overrides should work
{
+ name: "block-override",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &accounts[1].addr,
@@ -932,8 +944,36 @@ func TestCall(t *testing.T) {
blockOverrides: BlockOverrides{Number: (*hexutil.Big)(big.NewInt(11))},
want: "0x000000000000000000000000000000000000000000000000000000000000000b",
},
+ // Clear storage trie
+ {
+ name: "clear-storage-trie",
+ blockNumber: rpc.LatestBlockNumber,
+ call: TransactionArgs{
+ From: &accounts[1].addr,
+ // Yul:
+ // object "Test" {
+ // code {
+ // let dad := 0x0000000000000000000000000000000000000dad
+ // if eq(balance(dad), 0) {
+ // revert(0, 0)
+ // }
+ // let slot := sload(0)
+ // mstore(0, slot)
+ // return(0, 32)
+ // }
+ // }
+ Input: hex2Bytes("610dad6000813103600f57600080fd5b6000548060005260206000f3"),
+ },
+ overrides: StateOverride{
+ dad: OverrideAccount{
+ State: map[common.Hash]common.Hash{},
+ },
+ },
+ want: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ },
// Invalid blob tx
{
+ name: "invalid-blob-tx",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &accounts[1].addr,
@@ -944,6 +984,7 @@ func TestCall(t *testing.T) {
},
// BLOBHASH opcode
{
+ name: "blobhash-opcode",
blockNumber: rpc.LatestBlockNumber,
call: TransactionArgs{
From: &accounts[1].addr,
@@ -985,31 +1026,1178 @@ func TestCall(t *testing.T) {
want: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
}
- for i, tc := range testSuite {
+ for _, tc := range testSuite {
result, err := api.Call(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides)
if tc.expectErr != nil {
if err == nil {
- t.Errorf("test %d: want error %v, have nothing", i, tc.expectErr)
+ t.Errorf("test %s: want error %v, have nothing", tc.name, tc.expectErr)
continue
}
if !errors.Is(err, tc.expectErr) {
// Second try
if !reflect.DeepEqual(err, tc.expectErr) {
- t.Errorf("test %d: error mismatch, want %v, have %v", i, tc.expectErr, err)
+ t.Errorf("test %s: error mismatch, want %v, have %v", tc.name, tc.expectErr, err)
}
}
continue
}
if err != nil {
- t.Errorf("test %d: want no error, have %v", i, err)
+ t.Errorf("test %s: want no error, have %v", tc.name, err)
continue
}
if !reflect.DeepEqual(result.String(), tc.want) {
- t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, result.String(), tc.want)
+ t.Errorf("test %s, result mismatch, have\n%v\n, want\n%v\n", tc.name, result.String(), tc.want)
}
}
}
+func TestSimulateV1(t *testing.T) {
+ t.Parallel()
+ // Initialize test accounts
+ var (
+ accounts = newAccounts(3)
+ fixedAccount = newTestAccount()
+ genBlocks = 10
+ signer = types.HomesteadSigner{}
+ cac = common.HexToAddress("0x0000000000000000000000000000000000000cac")
+ bab = common.HexToAddress("0x0000000000000000000000000000000000000bab")
+ coinbase = "0x000000000000000000000000000000000000ffff"
+ genesis = &core.Genesis{
+ Config: params.TestChainConfig,
+ Alloc: types.GenesisAlloc{
+ accounts[0].addr: {Balance: big.NewInt(params.Ether)},
+ accounts[1].addr: {Balance: big.NewInt(params.Ether)},
+ accounts[2].addr: {Balance: big.NewInt(params.Ether)},
+ // Yul:
+ // object "Test" {
+ // code {
+ // let dad := 0x0000000000000000000000000000000000000dad
+ // selfdestruct(dad)
+ // }
+ // }
+ cac: {Balance: big.NewInt(params.Ether), Code: common.Hex2Bytes("610dad80ff")},
+ bab: {
+ Balance: big.NewInt(1),
+ // object "Test" {
+ // code {
+ // let value1 := sload(1)
+ // let value2 := sload(2)
+ //
+ // // Shift value1 by 128 bits to the left by multiplying it with 2^128
+ // value1 := mul(value1, 0x100000000000000000000000000000000)
+ //
+ // // Concatenate value1 and value2
+ // let concatenatedValue := add(value1, value2)
+ //
+ // // Store the result in memory and return it
+ // mstore(0, concatenatedValue)
+ // return(0, 0x20)
+ // }
+ // }
+ Code: common.FromHex("0x600154600254700100000000000000000000000000000000820291508082018060005260206000f3"),
+ Storage: map[common.Hash]common.Hash{
+ common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(10)),
+ common.BigToHash(big.NewInt(2)): common.BigToHash(big.NewInt(12)),
+ },
+ },
+ },
+ }
+ sha256Address = common.BytesToAddress([]byte{0x02})
+ )
+ api := NewBlockChainAPI(newTestBackend(t, genBlocks, genesis, ethash.NewFaker(), func(i int, b *core.BlockGen) {
+ b.SetCoinbase(common.HexToAddress(coinbase))
+ // Transfer from account[0] to account[1]
+ // value: 1000 wei
+ // fee: 0 wei
+ tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
+ Nonce: uint64(i),
+ To: &accounts[1].addr,
+ Value: big.NewInt(1000),
+ Gas: params.TxGas,
+ GasPrice: b.BaseFee(),
+ Data: nil,
+ }), signer, accounts[0].key)
+ b.AddTx(tx)
+ }))
+ var (
+ randomAccounts = newAccounts(4)
+ latest = rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ includeTransfers = true
+ validation = true
+ )
+ type log struct {
+ Address common.Address `json:"address"`
+ Topics []common.Hash `json:"topics"`
+ Data hexutil.Bytes `json:"data"`
+ BlockNumber hexutil.Uint64 `json:"blockNumber"`
+ // Skip txHash
+ //TxHash common.Hash `json:"transactionHash" gencodec:"required"`
+ TxIndex hexutil.Uint `json:"transactionIndex"`
+ //BlockHash common.Hash `json:"blockHash"`
+ Index hexutil.Uint `json:"logIndex"`
+ }
+ type callErr struct {
+ Message string
+ Code int
+ }
+ type callRes struct {
+ ReturnValue string `json:"returnData"`
+ Error callErr
+ Logs []log
+ GasUsed string
+ Status string
+ }
+ type blockRes struct {
+ Number string
+ //Hash string
+ // Ignore timestamp
+ GasLimit string
+ GasUsed string
+ Miner string
+ BaseFeePerGas string
+ Calls []callRes
+ }
+ var testSuite = []struct {
+ name string
+ blocks []simBlock
+ tag rpc.BlockNumberOrHash
+ includeTransfers *bool
+ validation *bool
+ expectErr error
+ want []blockRes
+ }{
+ // State build-up over calls:
+ // First value transfer OK after state override.
+ // Second one should succeed because of first transfer.
+ {
+ name: "simple",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[0].addr: OverrideAccount{Balance: newRPCBalance(big.NewInt(1000))},
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[1].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ }, {
+ From: &randomAccounts[1].addr,
+ To: &randomAccounts[2].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ }, {
+ To: &randomAccounts[3].addr,
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xf618",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ }, {
+ // State build-up over blocks.
+ name: "simple-multi-block",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[0].addr: OverrideAccount{Balance: newRPCBalance(big.NewInt(2000))},
+ },
+ Calls: []TransactionArgs{
+ {
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[1].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ }, {
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[3].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ },
+ },
+ }, {
+ StateOverrides: &StateOverride{
+ randomAccounts[3].addr: OverrideAccount{Balance: newRPCBalance(big.NewInt(0))},
+ },
+ Calls: []TransactionArgs{
+ {
+ From: &randomAccounts[1].addr,
+ To: &randomAccounts[2].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ },
+ },
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xa410",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xc",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x5208",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ }, {
+ // insufficient funds
+ name: "insufficient-funds",
+ tag: latest,
+ blocks: []simBlock{{
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[1].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ }},
+ }},
+ want: nil,
+ expectErr: &invalidTxError{Message: fmt.Sprintf("err: insufficient funds for gas * price + value: address %s have 0 want 1000 (supplied gas 4712388)", randomAccounts[0].addr.String()), Code: errCodeInsufficientFunds},
+ }, {
+ // EVM error
+ name: "evm-error",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: OverrideAccount{Code: hex2Bytes("f3")},
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[2].addr,
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x47e7c4",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ Error: callErr{Message: "stack underflow (0 <=> 2)", Code: errCodeVMError},
+ GasUsed: "0x47e7c4",
+ Logs: []log{},
+ Status: "0x0",
+ }},
+ }},
+ }, {
+ // Block overrides should work, each call is simulated on a different block number
+ name: "block-overrides",
+ tag: latest,
+ blocks: []simBlock{{
+ BlockOverrides: &BlockOverrides{
+ Number: (*hexutil.Big)(big.NewInt(11)),
+ FeeRecipient: &cac,
+ },
+ Calls: []TransactionArgs{
+ {
+ From: &accounts[0].addr,
+ Input: &hexutil.Bytes{
+ 0x43, // NUMBER
+ 0x60, 0x00, 0x52, // MSTORE offset 0
+ 0x60, 0x20, 0x60, 0x00, 0xf3, // RETURN
+ },
+ },
+ },
+ }, {
+ BlockOverrides: &BlockOverrides{
+ Number: (*hexutil.Big)(big.NewInt(12)),
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[1].addr,
+ Input: &hexutil.Bytes{
+ 0x43, // NUMBER
+ 0x60, 0x00, 0x52, // MSTORE offset 0
+ 0x60, 0x20, 0x60, 0x00, 0xf3,
+ },
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xe891",
+ Miner: strings.ToLower(cac.String()),
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x000000000000000000000000000000000000000000000000000000000000000b",
+ GasUsed: "0xe891",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xc",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xe891",
+ Miner: strings.ToLower(cac.String()),
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x000000000000000000000000000000000000000000000000000000000000000c",
+ GasUsed: "0xe891",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Block numbers must be in order.
+ {
+ name: "block-number-order",
+ tag: latest,
+ blocks: []simBlock{{
+ BlockOverrides: &BlockOverrides{
+ Number: (*hexutil.Big)(big.NewInt(12)),
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[1].addr,
+ Input: &hexutil.Bytes{
+ 0x43, // NUMBER
+ 0x60, 0x00, 0x52, // MSTORE offset 0
+ 0x60, 0x20, 0x60, 0x00, 0xf3, // RETURN
+ },
+ }},
+ }, {
+ BlockOverrides: &BlockOverrides{
+ Number: (*hexutil.Big)(big.NewInt(11)),
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ Input: &hexutil.Bytes{
+ 0x43, // NUMBER
+ 0x60, 0x00, 0x52, // MSTORE offset 0
+ 0x60, 0x20, 0x60, 0x00, 0xf3, // RETURN
+ },
+ }},
+ }},
+ want: []blockRes{},
+ expectErr: &invalidBlockNumberError{message: "block numbers must be in order: 11 <= 12"},
+ },
+ // Test on solidity storage example. Set value in one call, read in next.
+ {
+ name: "storage-contract",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: OverrideAccount{
+ Code: hex2Bytes("608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea2646970667358221220404e37f487a89a932dca5e77faaf6ca2de3b991f93d230604b1b8daaef64766264736f6c63430008070033"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ // Set value to 5
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[2].addr,
+ Input: hex2Bytes("6057361d0000000000000000000000000000000000000000000000000000000000000005"),
+ }, {
+ // Read value
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[2].addr,
+ Input: hex2Bytes("2e64cec1"),
+ },
+ },
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x10683",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0xaacc",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x0000000000000000000000000000000000000000000000000000000000000005",
+ GasUsed: "0x5bb7",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Test logs output.
+ {
+ name: "logs",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: OverrideAccount{
+ // Yul code:
+ // object "Test" {
+ // code {
+ // let hash:u256 := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ // log1(0, 0, hash)
+ // return (0, 0)
+ // }
+ // }
+ Code: hex2Bytes("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80600080a1600080f3"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[2].addr,
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x5508",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ Logs: []log{{
+ Address: randomAccounts[2].addr,
+ Topics: []common.Hash{common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")},
+ BlockNumber: hexutil.Uint64(11),
+ Data: hexutil.Bytes{},
+ }},
+ GasUsed: "0x5508",
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Test ecrecover override
+ {
+ name: "ecrecover-override",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: OverrideAccount{
+ // Yul code that returns ecrecover(0, 0, 0, 0).
+ // object "Test" {
+ // code {
+ // // Free memory pointer
+ // let free_ptr := mload(0x40)
+ //
+ // // Initialize inputs with zeros
+ // mstore(free_ptr, 0) // Hash
+ // mstore(add(free_ptr, 0x20), 0) // v
+ // mstore(add(free_ptr, 0x40), 0) // r
+ // mstore(add(free_ptr, 0x60), 0) // s
+ //
+ // // Call ecrecover precompile (at address 1) with all 0 inputs
+ // let success := staticcall(gas(), 1, free_ptr, 0x80, free_ptr, 0x20)
+ //
+ // // Check if the call was successful
+ // if eq(success, 0) {
+ // revert(0, 0)
+ // }
+ //
+ // // Return the recovered address
+ // return(free_ptr, 0x14)
+ // }
+ // }
+ Code: hex2Bytes("6040516000815260006020820152600060408201526000606082015260208160808360015afa60008103603157600080fd5b601482f3"),
+ },
+ common.BytesToAddress([]byte{0x01}): OverrideAccount{
+ // Yul code that returns the address of the caller.
+ // object "Test" {
+ // code {
+ // let c := caller()
+ // mstore(0, c)
+ // return(0xc, 0x14)
+ // }
+ // }
+ Code: hex2Bytes("33806000526014600cf3"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[2].addr,
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x52f6",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ // Caller is in this case the contract that invokes ecrecover.
+ ReturnValue: strings.ToLower(randomAccounts[2].addr.String()),
+ GasUsed: "0x52f6",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Test moving the sha256 precompile.
+ {
+ name: "precompile-move",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ sha256Address: OverrideAccount{
+ // Yul code that returns the calldata.
+ // object "Test" {
+ // code {
+ // let size := calldatasize() // Get the size of the calldata
+ //
+ // // Allocate memory to store the calldata
+ // let memPtr := msize()
+ //
+ // // Copy calldata to memory
+ // calldatacopy(memPtr, 0, size)
+ //
+ // // Return the calldata from memory
+ // return(memPtr, size)
+ // }
+ // }
+ Code: hex2Bytes("365981600082378181f3"),
+ MovePrecompileTo: &randomAccounts[2].addr,
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[2].addr,
+ Input: hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001"),
+ }, {
+ From: &randomAccounts[0].addr,
+ To: &sha256Address,
+ Input: hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001"),
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xa58c",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0xec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5",
+ GasUsed: "0x52dc",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x0000000000000000000000000000000000000000000000000000000000000001",
+ GasUsed: "0x52b0",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Test ether transfers.
+ {
+ name: "transfer-logs",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[0].addr: OverrideAccount{
+ Balance: newRPCBalance(big.NewInt(100)),
+ // Yul code that transfers 100 wei to address passed in calldata:
+ // object "Test" {
+ // code {
+ // let recipient := shr(96, calldataload(0))
+ // let value := 100
+ // let success := call(gas(), recipient, value, 0, 0, 0, 0)
+ // if eq(success, 0) {
+ // revert(0, 0)
+ // }
+ // }
+ // }
+ Code: hex2Bytes("60003560601c606460008060008084865af160008103601d57600080fd5b505050"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[0].addr,
+ Value: (*hexutil.Big)(big.NewInt(50)),
+ Input: hex2Bytes(strings.TrimPrefix(fixedAccount.addr.String(), "0x")),
+ }},
+ }},
+ includeTransfers: &includeTransfers,
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x77dc",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x77dc",
+ Logs: []log{{
+ Address: transferAddress,
+ Topics: []common.Hash{
+ transferTopic,
+ addressToHash(accounts[0].addr),
+ addressToHash(randomAccounts[0].addr),
+ },
+ Data: hexutil.Bytes(common.BigToHash(big.NewInt(50)).Bytes()),
+ BlockNumber: hexutil.Uint64(11),
+ }, {
+ Address: transferAddress,
+ Topics: []common.Hash{
+ transferTopic,
+ addressToHash(randomAccounts[0].addr),
+ addressToHash(fixedAccount.addr),
+ },
+ Data: hexutil.Bytes(common.BigToHash(big.NewInt(100)).Bytes()),
+ BlockNumber: hexutil.Uint64(11),
+ Index: hexutil.Uint(1),
+ }},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Tests selfdestructed contract.
+ {
+ name: "selfdestruct",
+ tag: latest,
+ blocks: []simBlock{{
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &cac,
+ }, {
+ From: &accounts[0].addr,
+ // Check that cac is selfdestructed and balance transferred to dad.
+ // object "Test" {
+ // code {
+ // let cac := 0x0000000000000000000000000000000000000cac
+ // let dad := 0x0000000000000000000000000000000000000dad
+ // if gt(balance(cac), 0) {
+ // revert(0, 0)
+ // }
+ // if gt(extcodesize(cac), 0) {
+ // revert(0, 0)
+ // }
+ // if eq(balance(dad), 0) {
+ // revert(0, 0)
+ // }
+ // }
+ // }
+ Input: hex2Bytes("610cac610dad600082311115601357600080fd5b6000823b1115602157600080fd5b6000813103602e57600080fd5b5050"),
+ }},
+ }, {
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ Input: hex2Bytes("610cac610dad600082311115601357600080fd5b6000823b1115602157600080fd5b6000813103602e57600080fd5b5050"),
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x1b83f",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0xd166",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x",
+ GasUsed: "0xe6d9",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xc",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xe6d9",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0xe6d9",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Enable validation checks.
+ {
+ name: "validation-checks",
+ tag: latest,
+ blocks: []simBlock{{
+ Calls: []TransactionArgs{{
+ From: &accounts[2].addr,
+ To: &cac,
+ Nonce: newUint64(2),
+ }},
+ }},
+ validation: &validation,
+ want: nil,
+ expectErr: &invalidTxError{Message: fmt.Sprintf("err: nonce too high: address %s, tx: 2 state: 0 (supplied gas 4712388)", accounts[2].addr), Code: errCodeNonceTooHigh},
+ },
+ // Contract sends tx in validation mode.
+ {
+ name: "validation-checks-from-contract",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: OverrideAccount{
+ Balance: newRPCBalance(big.NewInt(2098640803896784)),
+ Code: hex2Bytes("00"),
+ Nonce: newUint64(1),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[2].addr,
+ To: &cac,
+ Nonce: newUint64(1),
+ MaxFeePerGas: newInt(233138868),
+ MaxPriorityFeePerGas: newInt(1),
+ }},
+ }},
+ validation: &validation,
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xd166",
+ Miner: coinbase,
+ BaseFeePerGas: "0xde56ab3",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0xd166",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Successful validation
+ {
+ name: "validation-checks-success",
+ tag: latest,
+ blocks: []simBlock{{
+ BlockOverrides: &BlockOverrides{
+ BaseFeePerGas: (*hexutil.Big)(big.NewInt(1)),
+ },
+ StateOverrides: &StateOverride{
+ randomAccounts[0].addr: OverrideAccount{Balance: newRPCBalance(big.NewInt(10000000))},
+ },
+ Calls: []TransactionArgs{{
+ From: &randomAccounts[0].addr,
+ To: &randomAccounts[1].addr,
+ Value: (*hexutil.Big)(big.NewInt(1000)),
+ MaxFeePerGas: (*hexutil.Big)(big.NewInt(2)),
+ }},
+ }},
+ validation: &validation,
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x5208",
+ Miner: coinbase,
+ BaseFeePerGas: "0x1",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x5208",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ // Clear storage.
+ {
+ name: "clear-storage",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: {
+ Code: newBytes(genesis.Alloc[bab].Code),
+ StateDiff: map[common.Hash]common.Hash{
+ common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(2)),
+ common.BigToHash(big.NewInt(2)): common.BigToHash(big.NewInt(3)),
+ },
+ },
+ bab: {
+ State: map[common.Hash]common.Hash{
+ common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(1)),
+ },
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ }, {
+ From: &accounts[0].addr,
+ To: &bab,
+ }},
+ }, {
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: {
+ State: map[common.Hash]common.Hash{
+ common.BigToHash(big.NewInt(1)): common.BigToHash(big.NewInt(5)),
+ },
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xc542",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x0000000000000000000000000000000200000000000000000000000000000003",
+ GasUsed: "0x62a1",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x0000000000000000000000000000000100000000000000000000000000000000",
+ GasUsed: "0x62a1",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xc",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x62a1",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x0000000000000000000000000000000500000000000000000000000000000000",
+ GasUsed: "0x62a1",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ {
+ name: "blockhash-opcode",
+ tag: latest,
+ blocks: []simBlock{{
+ BlockOverrides: &BlockOverrides{
+ Number: (*hexutil.Big)(big.NewInt(12)),
+ },
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: {
+ Code: hex2Bytes("600035804060008103601057600080fd5b5050"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // Phantom block after base.
+ Input: uint256ToBytes(uint256.NewInt(11)),
+ }, {
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // Canonical block.
+ Input: uint256ToBytes(uint256.NewInt(8)),
+ }, {
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // base block.
+ Input: uint256ToBytes(uint256.NewInt(10)),
+ }},
+ }, {
+ BlockOverrides: &BlockOverrides{
+ Number: (*hexutil.Big)(big.NewInt(16)),
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // blocks[0]
+ Input: uint256ToBytes(uint256.NewInt(12)),
+ }, {
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // Phantom after blocks[0]
+ Input: uint256ToBytes(uint256.NewInt(13)),
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x0",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{},
+ }, {
+ Number: "0xc",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xf864",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x52cc",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x",
+ GasUsed: "0x52cc",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+
+ ReturnValue: "0x",
+ GasUsed: "0x52cc",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xd",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x0",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{},
+ }, {
+ Number: "0xe",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x0",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{},
+ }, {
+ Number: "0xf",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x0",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{},
+ }, {
+ Number: "0x10",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xa598",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x",
+ GasUsed: "0x52cc",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+
+ ReturnValue: "0x",
+ GasUsed: "0x52cc",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ {
+ name: "basefee-non-validation",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: {
+ // Yul code:
+ // object "Test" {
+ // code {
+ // // Get the gas price from the transaction
+ // let gasPrice := gasprice()
+ //
+ // // Get the base fee from the block
+ // let baseFee := basefee()
+ //
+ // // Store gasPrice and baseFee in memory
+ // mstore(0x0, gasPrice)
+ // mstore(0x20, baseFee)
+ //
+ // // Return the data
+ // return(0x0, 0x40)
+ // }
+ // }
+ Code: hex2Bytes("3a489060005260205260406000f3"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // 0 gas price
+ }, {
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // non-zero gas price
+ MaxPriorityFeePerGas: newInt(1),
+ MaxFeePerGas: newInt(2),
+ },
+ },
+ }, {
+ BlockOverrides: &BlockOverrides{
+ BaseFeePerGas: (*hexutil.Big)(big.NewInt(1)),
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // 0 gas price
+ }, {
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ // non-zero gas price
+ MaxPriorityFeePerGas: newInt(1),
+ MaxFeePerGas: newInt(2),
+ },
+ },
+ }, {
+ // Base fee should be 0 to zero even if it was set in previous block.
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ }},
+ }},
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xa44e",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ GasUsed: "0x5227",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000",
+ GasUsed: "0x5227",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xc",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0xa44e",
+ Miner: coinbase,
+ BaseFeePerGas: "0x1",
+ Calls: []callRes{{
+ ReturnValue: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
+ GasUsed: "0x5227",
+ Logs: []log{},
+ Status: "0x1",
+ }, {
+ ReturnValue: "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001",
+ GasUsed: "0x5227",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }, {
+ Number: "0xd",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x5227",
+ Miner: coinbase,
+ BaseFeePerGas: "0x0",
+ Calls: []callRes{{
+ ReturnValue: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ GasUsed: "0x5227",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ }, {
+ name: "basefee-validation-mode",
+ tag: latest,
+ blocks: []simBlock{{
+ StateOverrides: &StateOverride{
+ randomAccounts[2].addr: {
+ // Yul code:
+ // object "Test" {
+ // code {
+ // // Get the gas price from the transaction
+ // let gasPrice := gasprice()
+ //
+ // // Get the base fee from the block
+ // let baseFee := basefee()
+ //
+ // // Store gasPrice and baseFee in memory
+ // mstore(0x0, gasPrice)
+ // mstore(0x20, baseFee)
+ //
+ // // Return the data
+ // return(0x0, 0x40)
+ // }
+ // }
+ Code: hex2Bytes("3a489060005260205260406000f3"),
+ },
+ },
+ Calls: []TransactionArgs{{
+ From: &accounts[0].addr,
+ To: &randomAccounts[2].addr,
+ MaxFeePerGas: newInt(233138868),
+ MaxPriorityFeePerGas: newInt(1),
+ }},
+ }},
+ validation: &validation,
+ want: []blockRes{{
+ Number: "0xb",
+ GasLimit: "0x47e7c4",
+ GasUsed: "0x5227",
+ Miner: coinbase,
+ BaseFeePerGas: "0xde56ab3",
+ Calls: []callRes{{
+ ReturnValue: "0x000000000000000000000000000000000000000000000000000000000de56ab4000000000000000000000000000000000000000000000000000000000de56ab3",
+ GasUsed: "0x5227",
+ Logs: []log{},
+ Status: "0x1",
+ }},
+ }},
+ },
+ }
+
+ for _, tc := range testSuite {
+ t.Run(tc.name, func(t *testing.T) {
+ opts := simOpts{BlockStateCalls: tc.blocks}
+ if tc.includeTransfers != nil && *tc.includeTransfers {
+ opts.TraceTransfers = true
+ }
+ if tc.validation != nil && *tc.validation {
+ opts.Validation = true
+ }
+ result, err := api.SimulateV1(context.Background(), opts, &tc.tag)
+ if tc.expectErr != nil {
+ if err == nil {
+ t.Fatalf("test %s: want error %v, have nothing", tc.name, tc.expectErr)
+ }
+ if !errors.Is(err, tc.expectErr) {
+ // Second try
+ if !reflect.DeepEqual(err, tc.expectErr) {
+ t.Errorf("test %s: error mismatch, want %v, have %v", tc.name, tc.expectErr, err)
+ }
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("test %s: want no error, have %v", tc.name, err)
+ }
+ // Turn result into res-struct
+ var have []blockRes
+ resBytes, _ := json.Marshal(result)
+ if err := json.Unmarshal(resBytes, &have); err != nil {
+ t.Fatalf("failed to unmarshal result: %v", err)
+ }
+ if !reflect.DeepEqual(have, tc.want) {
+ t.Errorf("test %s, result mismatch, have\n%v\n, want\n%v\n", tc.name, have, tc.want)
+ }
+ })
+ }
+}
+
func TestSignTransaction(t *testing.T) {
t.Parallel()
// Initialize test accounts
@@ -1343,16 +2531,40 @@ func newAccounts(n int) (accounts []account) {
return accounts
}
+func newTestAccount() account {
+ // testKey is a private key to use for funding a tester account.
+ key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ // testAddr is the Ethereum address of the tester account.
+ addr := crypto.PubkeyToAddress(key.PublicKey)
+ return account{key: key, addr: addr}
+}
+
func newRPCBalance(balance *big.Int) *hexutil.Big {
rpcBalance := (*hexutil.Big)(balance)
return rpcBalance
}
func hex2Bytes(str string) *hexutil.Bytes {
- rpcBytes := hexutil.Bytes(common.Hex2Bytes(str))
+ rpcBytes := hexutil.Bytes(common.FromHex(str))
return &rpcBytes
}
+func newUint64(v uint64) *hexutil.Uint64 {
+ rpcUint64 := hexutil.Uint64(v)
+ return &rpcUint64
+}
+
+func newBytes(b []byte) *hexutil.Bytes {
+ rpcBytes := hexutil.Bytes(b)
+ return &rpcBytes
+}
+
+func uint256ToBytes(v *uint256.Int) *hexutil.Bytes {
+ b := v.Bytes32()
+ r := hexutil.Bytes(b[:])
+ return &r
+}
+
func TestRPCMarshalBlock(t *testing.T) {
t.Parallel()
var (
@@ -2072,6 +3284,97 @@ func TestRPCGetBlockReceipts(t *testing.T) {
}
}
+type precompileContract struct{}
+
+func (p *precompileContract) RequiredGas(input []byte) uint64 { return 0 }
+
+func (p *precompileContract) Run(input []byte) ([]byte, error) { return nil, nil }
+
+func TestStateOverrideMovePrecompile(t *testing.T) {
+ db := state.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil)
+ statedb, err := state.New(common.Hash{}, db)
+ if err != nil {
+ t.Fatalf("failed to create statedb: %v", err)
+ }
+ precompiles := map[common.Address]vm.PrecompiledContract{
+ common.BytesToAddress([]byte{0x1}): &precompileContract{},
+ common.BytesToAddress([]byte{0x2}): &precompileContract{},
+ }
+ bytes2Addr := func(b []byte) *common.Address {
+ a := common.BytesToAddress(b)
+ return &a
+ }
+ var testSuite = []struct {
+ overrides StateOverride
+ expectedPrecompiles map[common.Address]struct{}
+ fail bool
+ }{
+ {
+ overrides: StateOverride{
+ common.BytesToAddress([]byte{0x1}): {
+ Code: hex2Bytes("0xff"),
+ MovePrecompileTo: bytes2Addr([]byte{0x2}),
+ },
+ common.BytesToAddress([]byte{0x2}): {
+ Code: hex2Bytes("0x00"),
+ },
+ },
+ // 0x2 has already been touched by the moveTo.
+ fail: true,
+ }, {
+ overrides: StateOverride{
+ common.BytesToAddress([]byte{0x1}): {
+ Code: hex2Bytes("0xff"),
+ MovePrecompileTo: bytes2Addr([]byte{0xff}),
+ },
+ common.BytesToAddress([]byte{0x3}): {
+ Code: hex2Bytes("0x00"),
+ MovePrecompileTo: bytes2Addr([]byte{0xfe}),
+ },
+ },
+ // 0x3 is not a precompile.
+ fail: true,
+ }, {
+ overrides: StateOverride{
+ common.BytesToAddress([]byte{0x1}): {
+ Code: hex2Bytes("0xff"),
+ MovePrecompileTo: bytes2Addr([]byte{0xff}),
+ },
+ common.BytesToAddress([]byte{0x2}): {
+ Code: hex2Bytes("0x00"),
+ MovePrecompileTo: bytes2Addr([]byte{0xfe}),
+ },
+ },
+ expectedPrecompiles: map[common.Address]struct{}{common.BytesToAddress([]byte{0xfe}): {}, common.BytesToAddress([]byte{0xff}): {}},
+ },
+ }
+
+ for i, tt := range testSuite {
+ cpy := maps.Clone(precompiles)
+ // Apply overrides
+ err := tt.overrides.Apply(statedb, cpy)
+ if tt.fail {
+ if err == nil {
+ t.Errorf("test %d: want error, have nothing", i)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("test %d: want no error, have %v", i, err)
+ continue
+ }
+ // Precompile keys
+ if len(cpy) != len(tt.expectedPrecompiles) {
+ t.Errorf("test %d: precompile mismatch, want %d, have %d", i, len(tt.expectedPrecompiles), len(cpy))
+ }
+ for k := range tt.expectedPrecompiles {
+ if _, ok := cpy[k]; !ok {
+ t.Errorf("test %d: precompile not found: %s", i, k.String())
+ }
+ }
+ }
+}
+
func testRPCResponseWithFile(t *testing.T, testid int, result interface{}, rpc string, file string) {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
@@ -2088,3 +3391,7 @@ func testRPCResponseWithFile(t *testing.T, testid int, result interface{}, rpc s
}
require.JSONEqf(t, string(want), string(data), "test %d: json not match, want: %s, have: %s", testid, string(want), string(data))
}
+
+func addressToHash(a common.Address) common.Hash {
+ return common.BytesToHash(a.Bytes())
+}
diff --git a/internal/ethapi/errors.go b/internal/ethapi/errors.go
index b5e668a805..ae38061234 100644
--- a/internal/ethapi/errors.go
+++ b/internal/ethapi/errors.go
@@ -17,10 +17,12 @@
package ethapi
import (
+ "errors"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/vm"
)
@@ -76,3 +78,93 @@ func (e *TxIndexingError) ErrorCode() int {
// ErrorData returns the hex encoded revert reason.
func (e *TxIndexingError) ErrorData() interface{} { return "transaction indexing is in progress" }
+
+type callError struct {
+ Message string `json:"message"`
+ Code int `json:"code"`
+ Data string `json:"data,omitempty"`
+}
+
+type invalidTxError struct {
+ Message string `json:"message"`
+ Code int `json:"code"`
+}
+
+func (e *invalidTxError) Error() string { return e.Message }
+func (e *invalidTxError) ErrorCode() int { return e.Code }
+
+const (
+ errCodeNonceTooHigh = -38011
+ errCodeNonceTooLow = -38010
+ errCodeIntrinsicGas = -38013
+ errCodeInsufficientFunds = -38014
+ errCodeBlockGasLimitReached = -38015
+ errCodeBlockNumberInvalid = -38020
+ errCodeBlockTimestampInvalid = -38021
+ errCodeSenderIsNotEOA = -38024
+ errCodeMaxInitCodeSizeExceeded = -38025
+ errCodeClientLimitExceeded = -38026
+ errCodeInternalError = -32603
+ errCodeInvalidParams = -32602
+ errCodeReverted = -32000
+ errCodeVMError = -32015
+)
+
+func txValidationError(err error) *invalidTxError {
+ if err == nil {
+ return nil
+ }
+ switch {
+ case errors.Is(err, core.ErrNonceTooHigh):
+ return &invalidTxError{Message: err.Error(), Code: errCodeNonceTooHigh}
+ case errors.Is(err, core.ErrNonceTooLow):
+ return &invalidTxError{Message: err.Error(), Code: errCodeNonceTooLow}
+ case errors.Is(err, core.ErrSenderNoEOA):
+ return &invalidTxError{Message: err.Error(), Code: errCodeSenderIsNotEOA}
+ case errors.Is(err, core.ErrFeeCapVeryHigh):
+ return &invalidTxError{Message: err.Error(), Code: errCodeInvalidParams}
+ case errors.Is(err, core.ErrTipVeryHigh):
+ return &invalidTxError{Message: err.Error(), Code: errCodeInvalidParams}
+ case errors.Is(err, core.ErrTipAboveFeeCap):
+ return &invalidTxError{Message: err.Error(), Code: errCodeInvalidParams}
+ case errors.Is(err, core.ErrFeeCapTooLow):
+ return &invalidTxError{Message: err.Error(), Code: errCodeInvalidParams}
+ case errors.Is(err, core.ErrInsufficientFunds):
+ return &invalidTxError{Message: err.Error(), Code: errCodeInsufficientFunds}
+ case errors.Is(err, core.ErrIntrinsicGas):
+ return &invalidTxError{Message: err.Error(), Code: errCodeIntrinsicGas}
+ case errors.Is(err, core.ErrInsufficientFundsForTransfer):
+ return &invalidTxError{Message: err.Error(), Code: errCodeInsufficientFunds}
+ case errors.Is(err, core.ErrMaxInitCodeSizeExceeded):
+ return &invalidTxError{Message: err.Error(), Code: errCodeMaxInitCodeSizeExceeded}
+ }
+ return &invalidTxError{
+ Message: err.Error(),
+ Code: errCodeInternalError,
+ }
+}
+
+type invalidParamsError struct{ message string }
+
+func (e *invalidParamsError) Error() string { return e.message }
+func (e *invalidParamsError) ErrorCode() int { return errCodeInvalidParams }
+
+type clientLimitExceededError struct{ message string }
+
+func (e *clientLimitExceededError) Error() string { return e.message }
+func (e *clientLimitExceededError) ErrorCode() int { return errCodeClientLimitExceeded }
+
+type invalidBlockNumberError struct{ message string }
+
+func (e *invalidBlockNumberError) Error() string { return e.message }
+func (e *invalidBlockNumberError) ErrorCode() int { return errCodeBlockNumberInvalid }
+
+type invalidBlockTimestampError struct{ message string }
+
+func (e *invalidBlockTimestampError) Error() string { return e.message }
+func (e *invalidBlockTimestampError) ErrorCode() int { return errCodeBlockTimestampInvalid }
+
+type blockGasLimitReachedError struct{ message string }
+
+func (e *blockGasLimitReachedError) Error() string { return e.message }
+func (e *blockGasLimitReachedError) ErrorCode() int { return errCodeBlockGasLimitReached }
diff --git a/internal/ethapi/logtracer.go b/internal/ethapi/logtracer.go
new file mode 100644
index 0000000000..456aa93736
--- /dev/null
+++ b/internal/ethapi/logtracer.go
@@ -0,0 +1,151 @@
+// Copyright 2023 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package ethapi
+
+import (
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+)
+
+var (
+ // keccak256("Transfer(address,address,uint256)")
+ transferTopic = common.HexToHash("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
+ // ERC-7528
+ transferAddress = common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE")
+)
+
+// tracer is a simple tracer that records all logs and
+// ether transfers. Transfers are recorded as if they
+// were logs. Transfer events include:
+// - tx value
+// - call value
+// - self destructs
+//
+// The log format for a transfer is:
+// - address: 0x0000000000000000000000000000000000000000
+// - data: Value
+// - topics:
+// - Transfer(address,address,uint256)
+// - Sender address
+// - Recipient address
+type tracer struct {
+ // logs keeps logs for all open call frames.
+ // This lets us clear logs for failed calls.
+ logs [][]*types.Log
+ count int
+ traceTransfers bool
+ blockNumber uint64
+ blockHash common.Hash
+ txHash common.Hash
+ txIdx uint
+}
+
+func newTracer(traceTransfers bool, blockNumber uint64, blockHash, txHash common.Hash, txIndex uint) *tracer {
+ return &tracer{
+ traceTransfers: traceTransfers,
+ blockNumber: blockNumber,
+ blockHash: blockHash,
+ txHash: txHash,
+ txIdx: txIndex,
+ }
+}
+
+func (t *tracer) Hooks() *tracing.Hooks {
+ return &tracing.Hooks{
+ OnEnter: t.onEnter,
+ OnExit: t.onExit,
+ OnLog: t.onLog,
+ }
+}
+
+func (t *tracer) onEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
+ t.logs = append(t.logs, make([]*types.Log, 0))
+ if vm.OpCode(typ) != vm.DELEGATECALL && value != nil && value.Cmp(common.Big0) > 0 {
+ t.captureTransfer(from, to, value)
+ }
+}
+
+func (t *tracer) onExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) {
+ if depth == 0 {
+ t.onEnd(reverted)
+ return
+ }
+ size := len(t.logs)
+ if size <= 1 {
+ return
+ }
+ // pop call
+ call := t.logs[size-1]
+ t.logs = t.logs[:size-1]
+ size--
+
+ // Clear logs if call failed.
+ if !reverted {
+ t.logs[size-1] = append(t.logs[size-1], call...)
+ }
+}
+
+func (t *tracer) onEnd(reverted bool) {
+ if reverted {
+ t.logs[0] = nil
+ }
+}
+
+func (t *tracer) onLog(log *types.Log) {
+ t.captureLog(log.Address, log.Topics, log.Data)
+}
+
+func (t *tracer) captureLog(address common.Address, topics []common.Hash, data []byte) {
+ t.logs[len(t.logs)-1] = append(t.logs[len(t.logs)-1], &types.Log{
+ Address: address,
+ Topics: topics,
+ Data: data,
+ BlockNumber: t.blockNumber,
+ BlockHash: t.blockHash,
+ TxHash: t.txHash,
+ TxIndex: t.txIdx,
+ Index: uint(t.count),
+ })
+ t.count++
+}
+
+func (t *tracer) captureTransfer(from, to common.Address, value *big.Int) {
+ if !t.traceTransfers {
+ return
+ }
+ topics := []common.Hash{
+ transferTopic,
+ common.BytesToHash(from.Bytes()),
+ common.BytesToHash(to.Bytes()),
+ }
+ t.captureLog(transferAddress, topics, common.BigToHash(value).Bytes())
+}
+
+// reset prepares the tracer for the next transaction.
+func (t *tracer) reset(txHash common.Hash, txIdx uint) {
+ t.logs = nil
+ t.txHash = txHash
+ t.txIdx = txIdx
+}
+
+func (t *tracer) Logs() []*types.Log {
+ return t.logs[0]
+}
diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go
new file mode 100644
index 0000000000..bccf87f8ef
--- /dev/null
+++ b/internal/ethapi/simulate.go
@@ -0,0 +1,418 @@
+// Copyright 2023 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package ethapi
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "maps"
+ "math/big"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/consensus"
+ "github.com/ethereum/go-ethereum/consensus/misc/eip1559"
+ "github.com/ethereum/go-ethereum/consensus/misc/eip4844"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/ethereum/go-ethereum/trie"
+)
+
+const (
+ // maxSimulateBlocks is the maximum number of blocks that can be simulated
+ // in a single request.
+ maxSimulateBlocks = 256
+
+ // timestampIncrement is the default increment between block timestamps.
+ timestampIncrement = 1
+)
+
+// simBlock is a batch of calls to be simulated sequentially.
+type simBlock struct {
+ BlockOverrides *BlockOverrides
+ StateOverrides *StateOverride
+ Calls []TransactionArgs
+}
+
+// simCallResult is the result of a simulated call.
+type simCallResult struct {
+ ReturnValue hexutil.Bytes `json:"returnData"`
+ Logs []*types.Log `json:"logs"`
+ GasUsed hexutil.Uint64 `json:"gasUsed"`
+ Status hexutil.Uint64 `json:"status"`
+ Error *callError `json:"error,omitempty"`
+}
+
+func (r *simCallResult) MarshalJSON() ([]byte, error) {
+ type callResultAlias simCallResult
+ // Marshal logs to be an empty array instead of nil when empty
+ if r.Logs == nil {
+ r.Logs = []*types.Log{}
+ }
+ return json.Marshal((*callResultAlias)(r))
+}
+
+// simOpts are the inputs to eth_simulateV1.
+type simOpts struct {
+ BlockStateCalls []simBlock
+ TraceTransfers bool
+ Validation bool
+ ReturnFullTransactions bool
+}
+
+// simulator is a stateful object that simulates a series of blocks.
+// it is not safe for concurrent use.
+type simulator struct {
+ b Backend
+ state *state.StateDB
+ base *types.Header
+ chainConfig *params.ChainConfig
+ gp *core.GasPool
+ traceTransfers bool
+ validate bool
+ fullTx bool
+}
+
+// execute runs the simulation of a series of blocks.
+func (sim *simulator) execute(ctx context.Context, blocks []simBlock) ([]map[string]interface{}, error) {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ var (
+ cancel context.CancelFunc
+ timeout = sim.b.RPCEVMTimeout()
+ )
+ if timeout > 0 {
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ } else {
+ ctx, cancel = context.WithCancel(ctx)
+ }
+ // Make sure the context is cancelled when the call has completed
+ // this makes sure resources are cleaned up.
+ defer cancel()
+
+ var err error
+ blocks, err = sim.sanitizeChain(blocks)
+ if err != nil {
+ return nil, err
+ }
+ // Prepare block headers with preliminary fields for the response.
+ headers, err := sim.makeHeaders(blocks)
+ if err != nil {
+ return nil, err
+ }
+ var (
+ results = make([]map[string]interface{}, len(blocks))
+ parent = sim.base
+ // Assume same total difficulty for all simulated blocks.
+ td = sim.b.GetTd(ctx, sim.base.Hash())
+ )
+ for bi, block := range blocks {
+ result, callResults, err := sim.processBlock(ctx, &block, headers[bi], parent, headers[:bi], timeout)
+ if err != nil {
+ return nil, err
+ }
+ enc := RPCMarshalBlock(result, true, sim.fullTx, sim.chainConfig)
+ enc["totalDifficulty"] = (*hexutil.Big)(td)
+ enc["calls"] = callResults
+ results[bi] = enc
+
+ parent = headers[bi]
+ }
+ return results, nil
+}
+
+func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, parent *types.Header, headers []*types.Header, timeout time.Duration) (*types.Block, []simCallResult, error) {
+ // Set header fields that depend only on parent block.
+ // Parent hash is needed for evm.GetHashFn to work.
+ header.ParentHash = parent.Hash()
+ if sim.chainConfig.IsLondon(header.Number) {
+ // In non-validation mode base fee is set to 0 if it is not overridden.
+ // This is because it creates an edge case in EVM where gasPrice < baseFee.
+ // Base fee could have been overridden.
+ if header.BaseFee == nil {
+ if sim.validate {
+ header.BaseFee = eip1559.CalcBaseFee(sim.chainConfig, parent)
+ } else {
+ header.BaseFee = big.NewInt(0)
+ }
+ }
+ }
+ if sim.chainConfig.IsCancun(header.Number, header.Time) {
+ var excess uint64
+ if sim.chainConfig.IsCancun(parent.Number, parent.Time) {
+ excess = eip4844.CalcExcessBlobGas(*parent.ExcessBlobGas, *parent.BlobGasUsed)
+ } else {
+ excess = eip4844.CalcExcessBlobGas(0, 0)
+ }
+ header.ExcessBlobGas = &excess
+ }
+ blockContext := core.NewEVMBlockContext(header, sim.newSimulatedChainContext(ctx, headers), nil)
+ if block.BlockOverrides.BlobBaseFee != nil {
+ blockContext.BlobBaseFee = block.BlockOverrides.BlobBaseFee.ToInt()
+ }
+ precompiles := sim.activePrecompiles(sim.base)
+ // State overrides are applied prior to execution of a block
+ if err := block.StateOverrides.Apply(sim.state, precompiles); err != nil {
+ return nil, nil, err
+ }
+ var (
+ gasUsed, blobGasUsed uint64
+ txes = make([]*types.Transaction, len(block.Calls))
+ callResults = make([]simCallResult, len(block.Calls))
+ receipts = make([]*types.Receipt, len(block.Calls))
+ // Block hash will be repaired after execution.
+ tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), common.Hash{}, common.Hash{}, 0)
+ vmConfig = &vm.Config{
+ NoBaseFee: !sim.validate,
+ Tracer: tracer.Hooks(),
+ }
+ evm = vm.NewEVM(blockContext, vm.TxContext{GasPrice: new(big.Int)}, sim.state, sim.chainConfig, *vmConfig)
+ )
+ sim.state.SetLogger(tracer.Hooks())
+ // It is possible to override precompiles with EVM bytecode, or
+ // move them to another address.
+ if precompiles != nil {
+ evm.SetPrecompiles(precompiles)
+ }
+ for i, call := range block.Calls {
+ if err := ctx.Err(); err != nil {
+ return nil, nil, err
+ }
+ if err := sim.sanitizeCall(&call, sim.state, header, blockContext, &gasUsed); err != nil {
+ return nil, nil, err
+ }
+ tx := call.ToTransaction(types.DynamicFeeTxType)
+ txes[i] = tx
+ tracer.reset(tx.Hash(), uint(i))
+ // EoA check is always skipped, even in validation mode.
+ msg := call.ToMessage(header.BaseFee, !sim.validate, true)
+ evm.Reset(core.NewEVMTxContext(msg), sim.state)
+ result, err := applyMessageWithEVM(ctx, evm, msg, sim.state, timeout, sim.gp)
+ if err != nil {
+ txErr := txValidationError(err)
+ return nil, nil, txErr
+ }
+ // Update the state with pending changes.
+ var root []byte
+ if sim.chainConfig.IsByzantium(blockContext.BlockNumber) {
+ sim.state.Finalise(true)
+ } else {
+ root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes()
+ }
+ gasUsed += result.UsedGas
+ receipts[i] = core.MakeReceipt(evm, result, sim.state, blockContext.BlockNumber, common.Hash{}, tx, gasUsed, root)
+ blobGasUsed += receipts[i].BlobGasUsed
+ logs := tracer.Logs()
+ callRes := simCallResult{ReturnValue: result.Return(), Logs: logs, GasUsed: hexutil.Uint64(result.UsedGas)}
+ if result.Failed() {
+ callRes.Status = hexutil.Uint64(types.ReceiptStatusFailed)
+ if errors.Is(result.Err, vm.ErrExecutionReverted) {
+ // If the result contains a revert reason, try to unpack it.
+ revertErr := newRevertError(result.Revert())
+ callRes.Error = &callError{Message: revertErr.Error(), Code: errCodeReverted, Data: revertErr.ErrorData().(string)}
+ } else {
+ callRes.Error = &callError{Message: result.Err.Error(), Code: errCodeVMError}
+ }
+ } else {
+ callRes.Status = hexutil.Uint64(types.ReceiptStatusSuccessful)
+ }
+ callResults[i] = callRes
+ }
+ header.Root = sim.state.IntermediateRoot(true)
+ header.GasUsed = gasUsed
+ if sim.chainConfig.IsCancun(header.Number, header.Time) {
+ header.BlobGasUsed = &blobGasUsed
+ }
+ var withdrawals types.Withdrawals
+ if sim.chainConfig.IsShanghai(header.Number, header.Time) {
+ withdrawals = make([]*types.Withdrawal, 0)
+ }
+ b := types.NewBlock(header, &types.Body{Transactions: txes, Withdrawals: withdrawals}, receipts, trie.NewStackTrie(nil))
+ repairLogs(callResults, b.Hash())
+ return b, callResults, nil
+}
+
+// repairLogs updates the block hash in the logs present in the result of
+// a simulated block. This is needed as during execution when logs are collected
+// the block hash is not known.
+func repairLogs(calls []simCallResult, hash common.Hash) {
+ for i := range calls {
+ for j := range calls[i].Logs {
+ calls[i].Logs[j].BlockHash = hash
+ }
+ }
+}
+
+func (sim *simulator) sanitizeCall(call *TransactionArgs, state *state.StateDB, header *types.Header, blockContext vm.BlockContext, gasUsed *uint64) error {
+ if call.Nonce == nil {
+ nonce := state.GetNonce(call.from())
+ call.Nonce = (*hexutil.Uint64)(&nonce)
+ }
+ // Let the call run wild unless explicitly specified.
+ if call.Gas == nil {
+ remaining := blockContext.GasLimit - *gasUsed
+ call.Gas = (*hexutil.Uint64)(&remaining)
+ }
+ if *gasUsed+uint64(*call.Gas) > blockContext.GasLimit {
+ return &blockGasLimitReachedError{fmt.Sprintf("block gas limit reached: %d >= %d", gasUsed, blockContext.GasLimit)}
+ }
+ if err := call.CallDefaults(sim.gp.Gas(), header.BaseFee, sim.chainConfig.ChainID); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (sim *simulator) activePrecompiles(base *types.Header) vm.PrecompiledContracts {
+ var (
+ isMerge = (base.Difficulty.Sign() == 0)
+ rules = sim.chainConfig.Rules(base.Number, isMerge, base.Time)
+ )
+ return maps.Clone(vm.ActivePrecompiledContracts(rules))
+}
+
+// sanitizeChain checks the chain integrity. Specifically it checks that
+// block numbers and timestamp are strictly increasing, setting default values
+// when necessary. Gaps in block numbers are filled with empty blocks.
+// Note: It modifies the block's override object.
+func (sim *simulator) sanitizeChain(blocks []simBlock) ([]simBlock, error) {
+ var (
+ res = make([]simBlock, 0, len(blocks))
+ base = sim.base
+ prevNumber = base.Number
+ prevTimestamp = base.Time
+ )
+ for _, block := range blocks {
+ if block.BlockOverrides == nil {
+ block.BlockOverrides = new(BlockOverrides)
+ }
+ if block.BlockOverrides.Number == nil {
+ n := new(big.Int).Add(prevNumber, big.NewInt(1))
+ block.BlockOverrides.Number = (*hexutil.Big)(n)
+ }
+ diff := new(big.Int).Sub(block.BlockOverrides.Number.ToInt(), prevNumber)
+ if diff.Cmp(common.Big0) <= 0 {
+ return nil, &invalidBlockNumberError{fmt.Sprintf("block numbers must be in order: %d <= %d", block.BlockOverrides.Number.ToInt().Uint64(), prevNumber)}
+ }
+ if total := new(big.Int).Sub(block.BlockOverrides.Number.ToInt(), base.Number); total.Cmp(big.NewInt(maxSimulateBlocks)) > 0 {
+ return nil, &clientLimitExceededError{message: "too many blocks"}
+ }
+ if diff.Cmp(big.NewInt(1)) > 0 {
+ // Fill the gap with empty blocks.
+ gap := new(big.Int).Sub(diff, big.NewInt(1))
+ // Assign block number to the empty blocks.
+ for i := uint64(0); i < gap.Uint64(); i++ {
+ n := new(big.Int).Add(prevNumber, big.NewInt(int64(i+1)))
+ t := prevTimestamp + timestampIncrement
+ b := simBlock{BlockOverrides: &BlockOverrides{Number: (*hexutil.Big)(n), Time: (*hexutil.Uint64)(&t)}}
+ prevTimestamp = t
+ res = append(res, b)
+ }
+ }
+ // Only append block after filling a potential gap.
+ prevNumber = block.BlockOverrides.Number.ToInt()
+ var t uint64
+ if block.BlockOverrides.Time == nil {
+ t = prevTimestamp + timestampIncrement
+ block.BlockOverrides.Time = (*hexutil.Uint64)(&t)
+ } else {
+ t = uint64(*block.BlockOverrides.Time)
+ if t <= prevTimestamp {
+ return nil, &invalidBlockTimestampError{fmt.Sprintf("block timestamps must be in order: %d <= %d", t, prevTimestamp)}
+ }
+ }
+ prevTimestamp = t
+ res = append(res, block)
+ }
+ return res, nil
+}
+
+// makeHeaders makes header object with preliminary fields based on a simulated block.
+// Some fields have to be filled post-execution.
+// It assumes blocks are in order and numbers have been validated.
+func (sim *simulator) makeHeaders(blocks []simBlock) ([]*types.Header, error) {
+ var (
+ res = make([]*types.Header, len(blocks))
+ base = sim.base
+ header = base
+ )
+ for bi, block := range blocks {
+ if block.BlockOverrides == nil || block.BlockOverrides.Number == nil {
+ return nil, errors.New("empty block number")
+ }
+ overrides := block.BlockOverrides
+
+ var withdrawalsHash *common.Hash
+ if sim.chainConfig.IsShanghai(overrides.Number.ToInt(), (uint64)(*overrides.Time)) {
+ withdrawalsHash = &types.EmptyWithdrawalsHash
+ }
+ var parentBeaconRoot *common.Hash
+ if sim.chainConfig.IsCancun(overrides.Number.ToInt(), (uint64)(*overrides.Time)) {
+ parentBeaconRoot = &common.Hash{}
+ }
+ header = overrides.MakeHeader(&types.Header{
+ UncleHash: types.EmptyUncleHash,
+ ReceiptHash: types.EmptyReceiptsHash,
+ TxHash: types.EmptyTxsHash,
+ Coinbase: header.Coinbase,
+ Difficulty: header.Difficulty,
+ GasLimit: header.GasLimit,
+ WithdrawalsHash: withdrawalsHash,
+ ParentBeaconRoot: parentBeaconRoot,
+ })
+ res[bi] = header
+ }
+ return res, nil
+}
+
+func (sim *simulator) newSimulatedChainContext(ctx context.Context, headers []*types.Header) *ChainContext {
+ return NewChainContext(ctx, &simBackend{base: sim.base, b: sim.b, headers: headers})
+}
+
+type simBackend struct {
+ b ChainContextBackend
+ base *types.Header
+ headers []*types.Header
+}
+
+func (b *simBackend) Engine() consensus.Engine {
+ return b.b.Engine()
+}
+
+func (b *simBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
+ if uint64(number) == b.base.Number.Uint64() {
+ return b.base, nil
+ }
+ if uint64(number) < b.base.Number.Uint64() {
+ // Resolve canonical header.
+ return b.b.HeaderByNumber(ctx, number)
+ }
+ // Simulated block.
+ for _, header := range b.headers {
+ if header.Number.Uint64() == uint64(number) {
+ return header, nil
+ }
+ }
+ return nil, errors.New("header not found")
+}
diff --git a/internal/ethapi/simulate_test.go b/internal/ethapi/simulate_test.go
new file mode 100644
index 0000000000..37883924ac
--- /dev/null
+++ b/internal/ethapi/simulate_test.go
@@ -0,0 +1,120 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package ethapi
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+func TestSimulateSanitizeBlockOrder(t *testing.T) {
+ type result struct {
+ number uint64
+ timestamp uint64
+ }
+ for i, tc := range []struct {
+ baseNumber int
+ baseTimestamp uint64
+ blocks []simBlock
+ expected []result
+ err string
+ }{
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{}, {}, {}},
+ expected: []result{{number: 11, timestamp: 51}, {number: 12, timestamp: 52}, {number: 13, timestamp: 53}},
+ },
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{BlockOverrides: &BlockOverrides{Number: newInt(13), Time: newUint64(70)}}, {}},
+ expected: []result{{number: 11, timestamp: 51}, {number: 12, timestamp: 52}, {number: 13, timestamp: 70}, {number: 14, timestamp: 71}},
+ },
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{BlockOverrides: &BlockOverrides{Number: newInt(11)}}, {BlockOverrides: &BlockOverrides{Number: newInt(14)}}, {}},
+ expected: []result{{number: 11, timestamp: 51}, {number: 12, timestamp: 52}, {number: 13, timestamp: 53}, {number: 14, timestamp: 54}, {number: 15, timestamp: 55}},
+ },
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{BlockOverrides: &BlockOverrides{Number: newInt(13)}}, {BlockOverrides: &BlockOverrides{Number: newInt(12)}}},
+ err: "block numbers must be in order: 12 <= 13",
+ },
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{BlockOverrides: &BlockOverrides{Number: newInt(13), Time: newUint64(52)}}},
+ err: "block timestamps must be in order: 52 <= 52",
+ },
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{BlockOverrides: &BlockOverrides{Number: newInt(11), Time: newUint64(60)}}, {BlockOverrides: &BlockOverrides{Number: newInt(12), Time: newUint64(55)}}},
+ err: "block timestamps must be in order: 55 <= 60",
+ },
+ {
+ baseNumber: 10,
+ baseTimestamp: 50,
+ blocks: []simBlock{{BlockOverrides: &BlockOverrides{Number: newInt(11), Time: newUint64(60)}}, {BlockOverrides: &BlockOverrides{Number: newInt(13), Time: newUint64(61)}}},
+ err: "block timestamps must be in order: 61 <= 61",
+ },
+ } {
+ sim := &simulator{base: &types.Header{Number: big.NewInt(int64(tc.baseNumber)), Time: tc.baseTimestamp}}
+ res, err := sim.sanitizeChain(tc.blocks)
+ if err != nil {
+ if err.Error() == tc.err {
+ continue
+ } else {
+ t.Fatalf("testcase %d: error mismatch. Want '%s', have '%s'", i, tc.err, err.Error())
+ }
+ }
+ if err == nil && tc.err != "" {
+ t.Fatalf("testcase %d: expected err", i)
+ }
+ if len(res) != len(tc.expected) {
+ t.Errorf("testcase %d: mismatch number of blocks. Want %d, have %d", i, len(tc.expected), len(res))
+ }
+ for bi, b := range res {
+ if b.BlockOverrides == nil {
+ t.Fatalf("testcase %d: block overrides nil", i)
+ }
+ if b.BlockOverrides.Number == nil {
+ t.Fatalf("testcase %d: block number not set", i)
+ }
+ if b.BlockOverrides.Time == nil {
+ t.Fatalf("testcase %d: block time not set", i)
+ }
+ if uint64(*b.BlockOverrides.Time) != tc.expected[bi].timestamp {
+ t.Errorf("testcase %d: block timestamp mismatch. Want %d, have %d", i, tc.expected[bi].timestamp, uint64(*b.BlockOverrides.Time))
+ }
+ have := b.BlockOverrides.Number.ToInt().Uint64()
+ if have != tc.expected[bi].number {
+ t.Errorf("testcase %d: block number mismatch. Want %d, have %d", i, tc.expected[bi].number, have)
+ }
+ }
+ }
+}
+
+func newInt(n int64) *hexutil.Big {
+ return (*hexutil.Big)(big.NewInt(n))
+}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index f199f9d912..f9835a96da 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -421,7 +421,7 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int,
// core evm. This method is used in calls and traces that do not require a real
// live transaction.
// Assumes that fields are not nil, i.e. setDefaults or CallDefaults has been called.
-func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
+func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck, skipEoACheck bool) *core.Message {
var (
gasPrice *big.Int
gasFeeCap *big.Int
@@ -452,27 +452,42 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
accessList = *args.AccessList
}
return &core.Message{
- From: args.from(),
- To: args.To,
- Value: (*big.Int)(args.Value),
- GasLimit: uint64(*args.Gas),
- GasPrice: gasPrice,
- GasFeeCap: gasFeeCap,
- GasTipCap: gasTipCap,
- Data: args.data(),
- AccessList: accessList,
- BlobGasFeeCap: (*big.Int)(args.BlobFeeCap),
- BlobHashes: args.BlobHashes,
- SkipAccountChecks: true,
+ From: args.from(),
+ To: args.To,
+ Value: (*big.Int)(args.Value),
+ Nonce: uint64(*args.Nonce),
+ GasLimit: uint64(*args.Gas),
+ GasPrice: gasPrice,
+ GasFeeCap: gasFeeCap,
+ GasTipCap: gasTipCap,
+ Data: args.data(),
+ AccessList: accessList,
+ BlobGasFeeCap: (*big.Int)(args.BlobFeeCap),
+ BlobHashes: args.BlobHashes,
+ SkipNonceChecks: skipNonceCheck,
+ SkipFromEOACheck: skipEoACheck,
}
}
// ToTransaction converts the arguments to a transaction.
// This assumes that setDefaults has been called.
-func (args *TransactionArgs) ToTransaction() *types.Transaction {
- var data types.TxData
+func (args *TransactionArgs) ToTransaction(defaultType int) *types.Transaction {
+ usedType := types.LegacyTxType
switch {
- case args.BlobHashes != nil:
+ case args.BlobHashes != nil || defaultType == types.BlobTxType:
+ usedType = types.BlobTxType
+ case args.MaxFeePerGas != nil || defaultType == types.DynamicFeeTxType:
+ usedType = types.DynamicFeeTxType
+ case args.AccessList != nil || defaultType == types.AccessListTxType:
+ usedType = types.AccessListTxType
+ }
+ // Make it possible to default to newer tx, but use legacy if gasprice is provided
+ if args.GasPrice != nil {
+ usedType = types.LegacyTxType
+ }
+ var data types.TxData
+ switch usedType {
+ case types.BlobTxType:
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
@@ -498,7 +513,7 @@ func (args *TransactionArgs) ToTransaction() *types.Transaction {
}
}
- case args.MaxFeePerGas != nil:
+ case types.DynamicFeeTxType:
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
@@ -515,7 +530,7 @@ func (args *TransactionArgs) ToTransaction() *types.Transaction {
AccessList: al,
}
- case args.AccessList != nil:
+ case types.AccessListTxType:
data = &types.AccessListTx{
To: args.To,
ChainID: (*big.Int)(args.ChainID),
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index 4a1a37d722..4a53e2c829 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -616,6 +616,12 @@ web3._extend({
params: 4,
inputFormatter: [web3._extend.formatters.inputCallFormatter, web3._extend.formatters.inputDefaultBlockNumberFormatter, null, null],
}),
+ new web3._extend.Method({
+ name: 'simulateV1',
+ call: 'eth_simulateV1',
+ params: 2,
+ inputFormatter: [null, web3._extend.formatters.inputDefaultBlockNumberFormatter],
+ }),
new web3._extend.Method({
name: 'getBlockReceipts',
call: 'eth_getBlockReceipts',