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