mirror of https://github.com/ethereum/go-ethereum
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>pull/30408/head
parent
83775b1dc7
commit
8f4fac7b86
File diff suppressed because it is too large
Load Diff
@ -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] |
||||
} |
@ -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)) |
||||
} |
Loading…
Reference in new issue