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:
parent
83775b1dc7
commit
8f4fac7b86
@ -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,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 {
|
||||
|
@ -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 := ð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)
|
||||
|
@ -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 := ð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)
|
||||
|
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
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 }
|
||||
|
151
internal/ethapi/logtracer.go
Normal file
151
internal/ethapi/logtracer.go
Normal file
@ -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
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")
|
||||
}
|
120
internal/ethapi/simulate_test.go
Normal file
120
internal/ethapi/simulate_test.go
Normal file
@ -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
|
||||
@ -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),
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user