Skip to content

Commit

Permalink
internal/ethapi: eth_multicall (#27720)
Browse files Browse the repository at this point in the history
This is a successor PR to #25743. This PR is based on a new iteration of
the spec: ethereum/execution-apis#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>
  • Loading branch information
3 people authored Sep 6, 2024
1 parent 83775b1 commit 8f4fac7
Show file tree
Hide file tree
Showing 18 changed files with 2,427 additions and 148 deletions.
11 changes: 8 additions & 3 deletions core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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())
}

Expand All @@ -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
Expand Down
38 changes: 22 additions & 16 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
42 changes: 35 additions & 7 deletions core/vm/contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"maps"
"math/big"

"github.com/consensys/gnark-crypto/ecc"
Expand All @@ -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{},
Expand All @@ -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{},
Expand All @@ -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{},
Expand All @@ -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{},
Expand All @@ -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{},
Expand All @@ -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{},
Expand Down Expand Up @@ -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:
Expand Down
40 changes: 11 additions & 29 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion eth/gasestimator/gasestimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 16 additions & 4 deletions eth/tracers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"runtime"
"sync"
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion ethclient/ethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions ethclient/gethclient/gethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
6 changes: 3 additions & 3 deletions ethclient/gethclient/gethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -510,7 +510,7 @@ func TestBlockOverridesMarshal(t *testing.T) {
bo: BlockOverrides{
Coinbase: common.HexToAddress("0x1111111111111111111111111111111111111111"),
},
want: `{"coinbase":"0x1111111111111111111111111111111111111111"}`,
want: `{"feeRecipient":"0x1111111111111111111111111111111111111111"}`,
},
{
bo: BlockOverrides{
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions graphql/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 8f4fac7

Please sign in to comment.