internal/ethapi: eth_multicall (#27720)

This is a successor PR to #25743. This PR is based on a new iteration of
the spec: https://github.com/ethereum/execution-apis/pull/484.

`eth_multicall` takes in a list of blocks, each optionally overriding
fields like number, timestamp, etc. of a base block. Each block can
include calls. At each block users can override the state. There are
extra features, such as:

- Include ether transfers as part of the logs
- Overriding precompile codes with evm bytecode
- Redirecting accounts to another address

## Breaking changes

This PR includes the following breaking changes:

- Block override fields of eth_call and debug_traceCall have had the
following fields renamed
  - `coinbase` -> `feeRecipient`
  - `random` -> `prevRandao`
  - `baseFee` -> `baseFeePerGas`

---------

Co-authored-by: Gary Rong <garyrong0905@gmail.com>
Co-authored-by: Martin Holst Swende <martin@swende.se>
This commit is contained in:
Sina M 2024-09-06 11:31:00 +02:00 committed by GitHub
parent 83775b1dc7
commit 8f4fac7b86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2427 additions and 148 deletions

@ -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

@ -142,10 +142,13 @@ 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.
@ -160,7 +163,8 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In
Value: tx.Value(),
Data: tx.Data(),
AccessList: tx.AccessList(),
SkipAccountChecks: false,
SkipNonceChecks: false,
SkipFromEOACheck: false,
BlobHashes: tx.BlobHashes(),
BlobGasFeeCap: tx.BlobGasFeeCap(),
}
@ -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 {

@ -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:

@ -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) {

@ -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.

@ -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
}

@ -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 := &ethconfig.Config{Genesis: genesis}
config := &ethconfig.Config{Genesis: genesis, RPCGasCap: 1000000}
ethservice, err := eth.New(n, config)
if err != nil {
t.Fatalf("can't create new ethereum service: %v", err)

@ -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{

@ -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 := &ethconfig.Config{Genesis: genesis}
config := &ethconfig.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)

@ -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()

@ -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
}
@ -973,17 +974,49 @@ type OverrideAccount struct {
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))
@ -1021,46 +1054,79 @@ 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
Difficulty *hexutil.Big // No-op if we're simulating post-merge calls.
Time *hexutil.Uint64
GasLimit *hexutil.Uint64
Coinbase *common.Address
Random *common.Hash
BaseFee *hexutil.Big
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
}

File diff suppressed because it is too large Load Diff

@ -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 }

@ -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 <http://www.gnu.org/licenses/>.
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]
}

418
internal/ethapi/simulate.go Normal file

@ -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 <http://www.gnu.org/licenses/>.
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")
}

@ -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 <http://www.gnu.org/licenses/>.
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))
}

@ -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
@ -455,6 +455,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
From: args.from(),
To: args.To,
Value: (*big.Int)(args.Value),
Nonce: uint64(*args.Nonce),
GasLimit: uint64(*args.Gas),
GasPrice: gasPrice,
GasFeeCap: gasFeeCap,
@ -463,16 +464,30 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int) *core.Message {
AccessList: accessList,
BlobGasFeeCap: (*big.Int)(args.BlobFeeCap),
BlobHashes: args.BlobHashes,
SkipAccountChecks: true,
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),

@ -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',