diff --git a/eth/tracers/api.go b/eth/tracers/api.go index ab58189a2b..e3fe04b6ec 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -69,6 +69,13 @@ const ( // for tracing. The creation of trace state will be paused if the unused // trace states exceed this limit. maximumPendingTraceStates = 128 + + // Per-request limits for TraceCallMany, to bound the work and memory a single + // request can demand. Each traced call is still individually bounded by the + // per-trace timeout. + maxTraceCallManyBundles = 256 + maxTraceCallManyCallsPerBundle = 1000 + maxTraceCallManyTotalCalls = 10000 ) var errTxNotFound = errors.New("transaction not found") @@ -1154,6 +1161,276 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc return res, err } +// Bundle is a group of calls traced against a shared, evolving state with a +// common block-context override. +type Bundle struct { + Transactions []ethapi.TransactionArgs `json:"transactions"` + BlockOverride *override.BlockOverrides `json:"blockOverride"` +} + +// StateContext selects the base state for TraceCallMany. The state used is the +// one after replaying transactions [0, TransactionIndex) of the referenced +// block. If TransactionIndex is nil, -1, or points past the last transaction, +// the full post-block state is used. Values below -1 are rejected. +type StateContext struct { + BlockNumber rpc.BlockNumberOrHash `json:"blockNumber"` + TransactionIndex *int `json:"transactionIndex"` +} + +// TraceCallMany traces a list of eth_call bundles over a shared, evolving state +// and returns a per-bundle, per-call slice of tracer results. It mirrors +// erigon's eth_callMany semantics: +// +// - simulateContext selects the base state (state after the referenced block's +// transactions [0, transactionIndex)). +// - config.StateOverrides is applied once to that base state. +// - each bundle's BlockOverride customizes the block context for the calls +// within it. +// +// State mutations persist across calls within a bundle and across bundles (no +// rollback), so a transfer in one call is visible to a later call. After each +// bundle the simulated block number and time advance by one. Per-bundle +// BlockOverride is preferred over config.BlockOverrides. +// +// The result is positional: result[i] holds the traces for bundles[i]. A bundle +// with no transactions is allowed (only a request with no transactions at all is +// rejected) and yields an empty slice at its position. +func (api *API) TraceCallMany(ctx context.Context, bundles []Bundle, simulateContext StateContext, config *TraceCallConfig) ([][]interface{}, error) { + if err := validateBundles(bundles); err != nil { + return nil, err + } + + // Try to retrieve the specified block + var ( + err error + block *types.Block + statedb *state.StateDB + release StateReleaseFunc + precompiles vm.PrecompiledContracts + ) + + if hash, ok := simulateContext.BlockNumber.Hash(); ok { + block, err = api.blockByHash(ctx, hash) + } else if number, ok := simulateContext.BlockNumber.Number(); ok { + if number == rpc.PendingBlockNumber { + // We don't have access to the miner here. For tracing 'future' transactions, + // it can be done with block- and state-overrides instead, which offers + // more flexibility and stability than trying to trace on 'pending', since + // the contents of 'pending' is unstable and probably not a true representation + // of what the next actual block is likely to contain. + return nil, errors.New("tracing on top of pending is not supported") + } + + block, err = api.blockByNumber(ctx, number) + } else { + return nil, errors.New("invalid arguments; neither block nor hash specified") + } + + if err != nil { + return nil, err + } + // try to recompute the state + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + + // Default tx index is "-1" which means full block + var txIndex = -1 + if simulateContext.TransactionIndex != nil { + txIndex = *simulateContext.TransactionIndex + } + if txIndex < -1 { + return nil, fmt.Errorf("transaction index %d out of range for block %#x", txIndex, block.Hash()) + } + // Avoid calling `StateAtTransaction` if `txIndex` points past the last transaction as it will + // return an error. Instead use `StateAtBlock` directly to fetch the post-block state. Hence + // we set the txIndex to the default value in such cases to fetch post-block state directly. + if txIndex >= len(block.Transactions()) { + txIndex = -1 + } + + if txIndex == -1 { + statedb, release, err = api.backend.StateAtBlock(ctx, block, reexec, nil, true, false) + } else { + _, _, statedb, release, err = api.backend.StateAtTransaction(ctx, block, txIndex, reexec) + } + if err != nil { + return nil, err + } + + defer release() + + h := block.Header() + chainCtx := api.chainContext(ctx) + blockContext := core.NewEVMBlockContext(h, chainCtx, nil) + chainConfig := api.backend.ChainConfig() + + // advancedGetHash resolves blockhash() once the effective block moves past the + // real head: the header copy is pinned at head+1 with the real head as its parent, + // so real blocks (<= head) resolve via the chain walk while simulated blocks above + // the head resolve to 0. Otherwise a contract could detect simulation by checking + // blockhash(head) == 0x0 (#32175). Built once and reused across bundles so its + // internal block-hash cache stays warm; h is never mutated. + hc := types.CopyHeader(h) + hc.ParentHash = hc.Hash() + hc.Number = new(big.Int).Add(hc.Number, big.NewInt(1)) + advancedGetHash := core.GetHashFn(hc, chainCtx) + + // applyBlockOverride applies o to blockContext. + applyBlockOverride := func(o *override.BlockOverrides, blockContext *vm.BlockContext, applyPrecompiles bool) error { + if err := o.Apply(blockContext); err != nil { + return err + } + // Once the effective block number moves past the real head — whether via an + // explicit Number override or the per-bundle advance — use the head+1 GetHash + // so blockhash(head) resolves to the real head hash instead of 0. + if blockContext.BlockNumber.Cmp(h.Number) > 0 { + blockContext.GetHash = advancedGetHash + } + if applyPrecompiles && config.StateOverrides != nil { + rules := chainConfig.Rules(blockContext.BlockNumber, blockContext.Random != nil, blockContext.Time) + active := vm.ActivePrecompiledContracts(rules) + // State overrides change the precompile set only when an overridden address + // is itself an active precompile — that covers freeing a precompile slot and + // MovePrecompileTo (which requires the source to be a precompile). Detect this + // before Apply mutates the map. + touchesPrecompiles := false + for addr := range *config.StateOverrides { + if _, ok := active[addr]; ok { + touchesPrecompiles = true + break + } + } + if err := config.StateOverrides.Apply(statedb, active); err != nil { + return err + } + // Only pin the mutated set if the override actually changed it; then every + // bundle reuses it. Re-deriving it per bundle isn't possible without + // re-applying the override, which would clobber state mutations carried over + // from earlier bundles — so a bundle whose BlockOverride crosses a fork keeps + // the base-block precompile set. This is a known (now narrow) limitation. + // + // Otherwise leave precompiles nil so each bundle's traceTx derives the active + // set from its own (advanced) block context — correct across fork boundaries. + if touchesPrecompiles { + precompiles = active + } + } + return nil + } + + var traceConfig *TraceConfig + // Apply the customization rules if required. + if config != nil { + if err := applyBlockOverride(config.BlockOverrides, &blockContext, true); err != nil { + return nil, err + } + traceConfig = &config.TraceConfig + } + + results := make([][]interface{}, 0, len(bundles)) + var ( + prevNumber *big.Int + prevTime uint64 + ) + for _, bundle := range bundles { + if err := applyBlockOverride(bundle.BlockOverride, &blockContext, false); err != nil { + return nil, err + } + // The per-bundle advance models consecutive blocks, so an absolute per-bundle + // BlockOverride must not move the number or timestamp backwards (mirrors + // eth_simulateV1). + if prevNumber != nil { + if blockContext.BlockNumber.Cmp(prevNumber) <= 0 { + return nil, fmt.Errorf("block numbers must be in order: %d <= %d", blockContext.BlockNumber, prevNumber) + } + if blockContext.Time <= prevTime { + return nil, fmt.Errorf("block timestamps must be in order: %d <= %d", blockContext.Time, prevTime) + } + } + prevNumber, prevTime = new(big.Int).Set(blockContext.BlockNumber), blockContext.Time + + res, err := api.traceBundle(ctx, bundle.Transactions, blockContext, block, statedb, traceConfig, precompiles) + if err != nil { + return nil, err + } + results = append(results, res) + blockContext.BlockNumber = new(big.Int).Add(blockContext.BlockNumber, big.NewInt(1)) + blockContext.Time++ + } + return results, nil +} + +// validateBundles rejects requests with no transactions to trace and enforces +// the per-request limits on bundle and call counts. +func validateBundles(bundles []Bundle) error { + var errEmptyBundles = errors.New("empty bundles") + if len(bundles) == 0 { + return errEmptyBundles + } + if len(bundles) > maxTraceCallManyBundles { + return fmt.Errorf("too many bundles: %d, maximum allowed is %d", len(bundles), maxTraceCallManyBundles) + } + total := 0 + hasTx := false + for _, b := range bundles { + if len(b.Transactions) > maxTraceCallManyCallsPerBundle { + return fmt.Errorf("too many calls in a single bundle: %d, maximum allowed is %d", len(b.Transactions), maxTraceCallManyCallsPerBundle) + } + if len(b.Transactions) > 0 { + hasTx = true + total += len(b.Transactions) + } + } + if !hasTx { + return errEmptyBundles + } + if total > maxTraceCallManyTotalCalls { + return fmt.Errorf("too many calls across all bundles: %d, maximum allowed is %d", total, maxTraceCallManyTotalCalls) + } + return nil +} + +// traceBundle traces each call in a bundle sequentially against the shared +// state, so each call observes the mutations of the calls before it. +func (api *API) traceBundle(ctx context.Context, txs []ethapi.TransactionArgs, blockCtx vm.BlockContext, block *types.Block, statedb *state.StateDB, traceConfig *TraceConfig, precompiles vm.PrecompiledContracts) ([]interface{}, error) { + results := make([]interface{}, 0, len(txs)) + for i, args := range txs { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := args.CallDefaults(api.backend.RPCGasCap(), blockCtx.BaseFee, api.backend.ChainConfig().ChainID); err != nil { + return nil, err + } + var ( + msg = args.ToMessage(blockCtx.BaseFee, true) + tx = args.ToTransaction(types.LegacyTxType) + callCtx = blockCtx + ) + // Lower the basefee to 0 to avoid breaking EVM invariants (basefee < feecap). + // Mutate a per-call copy so it doesn't leak to the next call. + if msg.GasPrice.Sign() == 0 { + callCtx.BaseFee = new(big.Int) + } + if msg.BlobGasFeeCap != nil && msg.BlobGasFeeCap.BitLen() == 0 { + callCtx.BlobBaseFee = new(big.Int) + } + txctx := &Context{ + BlockHash: block.Hash(), + BlockNumber: callCtx.BlockNumber, + TxIndex: i, + TxHash: tx.Hash(), + } + res, _, err := api.traceTx(ctx, tx, msg, txctx, callCtx, statedb, traceConfig, precompiles) + if err != nil { + return nil, err + } + results = append(results, res) + } + return results, nil +} + // traceTx configures a new tracer according to the provided configuration, and // executes the given message in the provided environment. The return value will // be tracer dependent. For state-sync transactions, it only supports transactions diff --git a/eth/tracers/api_statesync_test.go b/eth/tracers/api_statesync_test.go index 2046a11ead..9b204a95e4 100644 --- a/eth/tracers/api_statesync_test.go +++ b/eth/tracers/api_statesync_test.go @@ -17,6 +17,8 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" @@ -867,6 +869,56 @@ func TestTraceTransaction_StateSync_PrestateTracer(t *testing.T) { } } +// TestTraceCallMany_StateSyncBlock_BaseState verifies that TraceCallMany builds its +// base state from a block whose trailing transaction is a state-sync tx without ever +// applying that synthetic tx as a normal message. Because the state-sync tx is always +// last, an in-range transactionIndex replays only the normal txs, and the full-block +// case uses the committed post-block state — neither path executes the state-sync tx. +func TestTraceCallMany_StateSyncBlock_BaseState(t *testing.T) { + t.Parallel() + + backend, api, stateSyncBlock := newStateSyncTestSetup(t, 3, 2) + defer backend.chain.Stop() + + block, _ := backend.BlockByNumber(context.Background(), rpc.BlockNumber(stateSyncBlock)) + require.NotNil(t, block) + txs := block.Transactions() + require.Equal(t, 2, len(txs), "expected 1 normal tx + 1 trailing state-sync tx") + stateSyncIdx := len(txs) - 1 // index of the trailing state-sync tx + pastEnd := len(txs) + + to := targetAddr + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{{ + From: &address, + To: &to, + Value: (*hexutil.Big)(big.NewInt(1)), + }}}} + + byNumber := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(stateSyncBlock)) + cases := []struct { + name string + sc StateContext + }{ + {"full post-block state by hash", StateContext{BlockNumber: rpc.BlockNumberOrHashWithHash(block.Hash(), false)}}, + {"index at state-sync tx replays normal txs only", StateContext{BlockNumber: byNumber, TransactionIndex: &stateSyncIdx}}, + {"index past all txs falls back to post-block state", StateContext{BlockNumber: byNumber, TransactionIndex: &pastEnd}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res, err := api.TraceCallMany(context.Background(), bundles, tc.sc, nil) + require.NoError(t, err, "base-state setup must not apply the state-sync tx as a normal message") + require.Len(t, res, 1) + require.Len(t, res[0], 1) + + raw, ok := res[0][0].(json.RawMessage) + require.True(t, ok, "expected json.RawMessage, got %T", res[0][0]) + var out logger.ExecutionResult + require.NoError(t, json.Unmarshal(raw, &out)) + require.False(t, out.Failed, "traced call should succeed") + }) + } +} + // prestateTracerConfig builds a TraceConfig for the prestateTracer with the given // JSON config blob (or nil for the default config). func prestateTracerConfig(cfg json.RawMessage) *TraceConfig { diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index aac650ae24..581af41d8a 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -26,6 +26,7 @@ import ( "os" "reflect" "slices" + "strings" "sync/atomic" "testing" "time" @@ -548,6 +549,477 @@ func TestTraceCall(t *testing.T) { } } +func TestTraceCallMany(t *testing.T) { + t.Parallel() + + accounts := newAccounts(3) + genBlocks := 10 // chain head ends up at block number genBlocks + // Tiny contracts whose top-of-stack reflects a block-context field, so the + // active value shows up on the struct logger's stack. + numberAddr := common.HexToAddress("0x000000000000000000000000000000000000aaaa") // NUMBER; STOP + timeAddr := common.HexToAddress("0x000000000000000000000000000000000000bbbb") // TIMESTAMP; STOP + blockhashAddr := common.HexToAddress("0x000000000000000000000000000000000000cccc") // blockhash(number-1); STOP + basefeeAddr := common.HexToAddress("0x000000000000000000000000000000000000eeee") // BASEFEE; STOP + headHashAddr := common.HexToAddress("0x000000000000000000000000000000000000ffff") // blockhash(genBlocks); STOP + // An account with no genesis balance; funded only via a state override. + overrideAddr := common.HexToAddress("0x000000000000000000000000000000000000dddd") + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + numberAddr: {Code: []byte{0x43, 0x00}, Balance: big.NewInt(0)}, + timeAddr: {Code: []byte{0x42, 0x00}, Balance: big.NewInt(0)}, + // NUMBER, PUSH1 1, SWAP1, SUB, BLOCKHASH, STOP -> pushes blockhash(number-1). + blockhashAddr: {Code: []byte{0x43, 0x60, 0x01, 0x90, 0x03, 0x40, 0x00}, Balance: big.NewInt(0)}, + basefeeAddr: {Code: []byte{0x48, 0x00}, Balance: big.NewInt(0)}, // BASEFEE, STOP + // PUSH1 genBlocks, BLOCKHASH, STOP -> pushes blockhash(genBlocks), the real head. + headHashAddr: {Code: []byte{0x60, byte(genBlocks), 0x40, 0x00}, Balance: big.NewInt(0)}, + }, + } + signer := types.HomesteadSigner{} + nonce := uint64(0) + backend := newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + send := func(to common.Address) { + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: nonce, To: &to, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), + }), signer, accounts[0].key) + b.AddTx(tx) + nonce++ + } + send(accounts[1].addr) + if i == genBlocks-2 { + // A 3-tx block: accounts[2] is funded by the tx at index 1, so the + // state before/after that index is observably different. + send(accounts[2].addr) + send(accounts[1].addr) + } + }) + defer backend.teardown() + api := NewAPI(backend) + + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + transfer := func(from, to common.Address, value *big.Int) ethapi.TransactionArgs { + return ethapi.TransactionArgs{From: &from, To: &to, Value: (*hexutil.Big)(value)} + } + mustResult := func(t *testing.T, v interface{}) *logger.ExecutionResult { + t.Helper() + var r *logger.ExecutionResult + if err := json.Unmarshal(v.(json.RawMessage), &r); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + return r + } + // topOfStack returns the top stack word recorded by the struct logger right + // before the final STOP — i.e. the value the tiny contract pushed. + topOfStack := func(t *testing.T, v interface{}) string { + t.Helper() + res := mustResult(t, v) + for i := len(res.StructLogs) - 1; i >= 0; i-- { + var entry struct { + Stack []string `json:"stack"` + } + if err := json.Unmarshal(res.StructLogs[i], &entry); err != nil { + t.Fatalf("failed to unmarshal struct log: %v", err) + } + if len(entry.Stack) > 0 { + return entry.Stack[len(entry.Stack)-1] + } + } + t.Fatalf("no stack entries in trace") + return "" + } + + t.Run("single call", func(t *testing.T) { + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{ + transfer(accounts[0].addr, accounts[1].addr, big.NewInt(1000)), + }}} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res) != 1 || len(res[0]) != 1 { + t.Fatalf("unexpected result shape: %v", res) + } + if got := mustResult(t, res[0][0]); got.Failed || got.Gas != params.TxGas { + t.Fatalf("unexpected trace: failed=%v gas=%d", got.Failed, got.Gas) + } + }) + + t.Run("state persists across calls", func(t *testing.T) { + // Call A funds accounts[0]->accounts[2]; call B then spends more than its + // running balance would allow without A, so B only succeeds if A persisted. + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{ + transfer(accounts[0].addr, accounts[2].addr, big.NewInt(2000)), + transfer(accounts[2].addr, accounts[1].addr, new(big.Int).Add(big.NewInt(params.Ether), big.NewInt(2500))), + }}} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for i, r := range res[0] { + if got := mustResult(t, r); got.Failed { + t.Fatalf("call %d failed; state should persist across calls in a bundle", i) + } + } + }) + + t.Run("block number override and per-bundle advance", func(t *testing.T) { + numberCall := ethapi.TransactionArgs{From: &accounts[0].addr, To: &numberAddr} + bundles := []Bundle{ + { + Transactions: []ethapi.TransactionArgs{numberCall}, + BlockOverride: &override.BlockOverrides{Number: (*hexutil.Big)(big.NewInt(0x1337))}, + }, + {Transactions: []ethapi.TransactionArgs{numberCall}}, + } + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := topOfStack(t, res[0][0]); got != "0x1337" { + t.Fatalf("bundle 0 block number: want 0x1337, got %s", got) + } + // The second bundle inherits the prior bundle's block number, advanced by one. + if got := topOfStack(t, res[1][0]); got != "0x1338" { + t.Fatalf("bundle 1 block number: want 0x1338 (advanced), got %s", got) + } + }) + + t.Run("block time override and per-bundle advance", func(t *testing.T) { + timeCall := ethapi.TransactionArgs{From: &accounts[0].addr, To: &timeAddr} + ts := hexutil.Uint64(0x9999) + bundles := []Bundle{ + { + Transactions: []ethapi.TransactionArgs{timeCall}, + BlockOverride: &override.BlockOverrides{Time: &ts}, + }, + {Transactions: []ethapi.TransactionArgs{timeCall}}, + } + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := topOfStack(t, res[0][0]); got != "0x9999" { + t.Fatalf("bundle 0 timestamp: want 0x9999, got %s", got) + } + if got := topOfStack(t, res[1][0]); got != "0x999a" { + t.Fatalf("bundle 1 timestamp: want 0x999a (advanced), got %s", got) + } + }) + + t.Run("config block override applies as the base for all bundles", func(t *testing.T) { + // config.BlockOverrides is the request-level base override (applied before any + // bundle). Every bundle builds on it, then advances. + numberCall := ethapi.TransactionArgs{From: &accounts[0].addr, To: &numberAddr} + cfg := &TraceCallConfig{BlockOverrides: &override.BlockOverrides{Number: (*hexutil.Big)(big.NewInt(0x4242))}} + bundles := []Bundle{ + {Transactions: []ethapi.TransactionArgs{numberCall}}, + {Transactions: []ethapi.TransactionArgs{numberCall}}, + } + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := topOfStack(t, res[0][0]); got != "0x4242" { + t.Fatalf("bundle 0 did not observe config block number: want 0x4242, got %s", got) + } + if got := topOfStack(t, res[1][0]); got != "0x4243" { + t.Fatalf("bundle 1 block number: want 0x4243 (advanced), got %s", got) + } + }) + + t.Run("blockhash resolves when overriding to head+1", func(t *testing.T) { + // Overriding the block number to head+1 must rewire GetHash so that + // blockhash(head) returns the real head hash instead of zero. + head := backend.chain.CurrentBlock().Number.Uint64() + bundles := []Bundle{{ + Transactions: []ethapi.TransactionArgs{{From: &accounts[0].addr, To: &blockhashAddr}}, + BlockOverride: &override.BlockOverrides{Number: (*hexutil.Big)(new(big.Int).SetUint64(head + 1))}, + }} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := topOfStack(t, res[0][0]); got == "0x0" { + t.Fatalf("blockhash(head) resolved to zero; the head+1 parent-hash fixup did not apply") + } + }) + + t.Run("blockhash of the real head survives sequential number overrides", func(t *testing.T) { + // Regression: the n+1 fixup must not mutate shared state across bundles. + // Bundle 0 overrides to head+1 (fires the fixup); bundle 1 overrides to head+2. + // blockhash(head) — a real historical block — must stay resolvable in both, + // not get clobbered to zero by a synthetic parent hash carried over from bundle 0. + head := backend.chain.CurrentBlock().Number.Uint64() + if head != uint64(genBlocks) { + t.Fatalf("test assumes head == genBlocks (%d), got %d", genBlocks, head) + } + headCall := []ethapi.TransactionArgs{{From: &accounts[0].addr, To: &headHashAddr}} + bundles := []Bundle{ + {Transactions: headCall, BlockOverride: &override.BlockOverrides{Number: (*hexutil.Big)(new(big.Int).SetUint64(head + 1))}}, + {Transactions: headCall, BlockOverride: &override.BlockOverrides{Number: (*hexutil.Big)(new(big.Int).SetUint64(head + 2))}}, + } + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + for i := range bundles { + if got := topOfStack(t, res[i][0]); got == "0x0" { + t.Fatalf("bundle %d: blockhash(head) resolved to zero; shared-header mutation corrupted GetHash", i) + } + } + }) + + t.Run("blockhash of the real head resolves after the natural per-bundle advance", func(t *testing.T) { + // No explicit block override: bundle 0 runs at the head block, so blockhash(head) + // is the current block -> 0; bundles 1 and 2 advance to head+1 and head+2 via the + // per-bundle advance, where blockhash(head) must resolve to the real head hash in + // every advanced bundle, not just the first (#32175). + head := backend.chain.CurrentBlock().Number.Uint64() + if head != uint64(genBlocks) { + t.Fatalf("test assumes head == genBlocks (%d), got %d", genBlocks, head) + } + headCall := []ethapi.TransactionArgs{{From: &accounts[0].addr, To: &headHashAddr}} + bundles := []Bundle{{Transactions: headCall}, {Transactions: headCall}, {Transactions: headCall}} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Bundle 0 is at the head block, so blockhash(head) is the current block -> 0. + if got := topOfStack(t, res[0][0]); got != "0x0" { + t.Fatalf("bundle 0 runs at head; blockhash(head) should be 0 (current block), got %s", got) + } + // Bundle 1 (head+1) must resolve blockhash(head) to the real head hash... + headHash := topOfStack(t, res[1][0]) + if headHash == "0x0" { + t.Fatalf("bundle 1 runs at head+1 via advance; blockhash(head) must resolve to the real head hash, got 0") + } + // ...and bundle 2 (head+2) must return the same real head hash, not drift to 0. + if got := topOfStack(t, res[2][0]); got != headHash { + t.Fatalf("bundle 2 runs at head+2 via advance; blockhash(head) should still be %s, got %s", headHash, got) + } + }) + + t.Run("basefee is zeroed for zero-gas-price calls", func(t *testing.T) { + // A call with no fee fields ends up with gasPrice 0, which lowers the + // block context basefee to 0 so the BASEFEE opcode reads 0. + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{{From: &accounts[0].addr, To: &basefeeAddr}}}} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := topOfStack(t, res[0][0]); got != "0x0" { + t.Fatalf("BASEFEE should read 0 for a zero-gas-price call, got %s", got) + } + }) + + t.Run("transaction index selects mid-block state", func(t *testing.T) { + // In the 3-tx block, accounts[2] is funded by the tx at index 1. A spend + // from accounts[2] above its genesis balance fails before that tx and + // succeeds after it. + blockNr := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(genBlocks - 1)) + spend := transfer(accounts[2].addr, accounts[0].addr, new(big.Int).Add(big.NewInt(params.Ether), big.NewInt(100))) + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{spend}}} + + beforeIdx, afterIdx := 0, 2 + if _, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: blockNr, TransactionIndex: &beforeIdx}, nil); err == nil { + t.Fatalf("tracing before the funding tx should fail with insufficient funds") + } + // Index 0 is a valid (non-negative) selector: a funded sender succeeds there. + okAtZero := []Bundle{{Transactions: []ethapi.TransactionArgs{transfer(accounts[0].addr, accounts[1].addr, big.NewInt(1))}}} + if _, err := api.TraceCallMany(t.Context(), okAtZero, StateContext{BlockNumber: blockNr, TransactionIndex: &beforeIdx}, nil); err != nil { + t.Fatalf("tracing at index 0 with a funded sender should succeed, got: %v", err) + } + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: blockNr, TransactionIndex: &afterIdx}, nil) + if err != nil { + t.Fatalf("tracing after the funding tx should succeed, got: %v", err) + } + if got := mustResult(t, res[0][0]); got.Failed { + t.Fatalf("call after funding tx unexpectedly failed") + } + // transactionIndex -1 is the "full block" sentinel: same as omitting it. + fullBlock := -1 + if _, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: blockNr, TransactionIndex: &fullBlock}, nil); err != nil { + t.Fatalf("tracing with index -1 (full block) should succeed, got: %v", err) + } + }) + + t.Run("state override is applied to base state", func(t *testing.T) { + // overrideAddr has no genesis balance; the override funds it so its spend succeeds. + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{ + transfer(overrideAddr, accounts[0].addr, new(big.Int).Add(big.NewInt(params.Ether), big.NewInt(7))), + }}} + cfg := &TraceCallConfig{StateOverrides: &override.StateOverride{ + overrideAddr: {Balance: (*hexutil.Big)(new(big.Int).Mul(big.NewInt(5), big.NewInt(params.Ether)))}, + }} + if _, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, nil); err == nil { + t.Fatalf("without the override the unfunded sender should fail") + } + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, cfg) + if err != nil { + t.Fatalf("with the balance override the call should succeed, got: %v", err) + } + if got := mustResult(t, res[0][0]); got.Failed { + t.Fatalf("call unexpectedly failed despite balance override") + } + }) + + t.Run("state override relocating a precompile is honored in execution", func(t *testing.T) { + // Move the identity precompile (0x04) to a fresh address; the destination + // must behave as the precompile (echo its input). This only works if the + // override-mutated precompile set reaches traceTx, not a freshly recomputed one. + identity := common.BytesToAddress([]byte{0x04}) + dest := common.HexToAddress("0x0000000000000000000000000000000000009999") + input := hexutil.Bytes{0xde, 0xad, 0xbe, 0xef} + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{{ + From: &accounts[0].addr, To: &dest, Input: &input, + }}}} + cfg := &TraceCallConfig{StateOverrides: &override.StateOverride{ + identity: {MovePrecompileTo: &dest}, + }} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := mustResult(t, res[0][0]) + if got.Failed { + t.Fatalf("call to relocated precompile failed") + } + if got.ReturnValue.String() != input.String() { + t.Fatalf("relocated identity precompile: want return %s, got %s", input.String(), got.ReturnValue.String()) + } + }) + + t.Run("state override on a precompile slot is honored in execution", func(t *testing.T) { + // Overriding a precompile address with code frees the precompile slot, so the + // code runs instead of the precompile. This exercises the precompile-set detection + // for the slot-override case (not just MovePrecompileTo): the mutated set, where + // 0x04 is no longer a precompile, must reach traceTx — otherwise 0x04 keeps + // behaving as the identity precompile and echoes the input. + identity := common.BytesToAddress([]byte{0x04}) + input := hexutil.Bytes{0xde, 0xad, 0xbe, 0xef} + code := hexutil.Bytes{0x00} // STOP -> returns no data (identity would echo the input) + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{{ + From: &accounts[0].addr, To: &identity, Input: &input, + }}}} + cfg := &TraceCallConfig{StateOverrides: &override.StateOverride{ + identity: {Code: &code}, + }} + res, err := api.TraceCallMany(t.Context(), bundles, StateContext{BlockNumber: latest}, cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := mustResult(t, res[0][0]) + if got.Failed { + t.Fatalf("call to overridden precompile slot failed") + } + if got.ReturnValue.String() == input.String() { + t.Fatalf("0x04 still ran as the identity precompile; the slot override was not honored") + } + }) + + t.Run("honors context cancellation", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + bundles := []Bundle{{Transactions: []ethapi.TransactionArgs{ + transfer(accounts[0].addr, accounts[1].addr, big.NewInt(1)), + }}} + if _, err := api.TraceCallMany(ctx, bundles, StateContext{BlockNumber: latest}, nil); !errors.Is(err, context.Canceled) { + t.Fatalf("want context.Canceled, got %v", err) + } + }) + + t.Run("errors", func(t *testing.T) { + send := transfer(accounts[0].addr, accounts[1].addr, big.NewInt(1)) + withTx := []Bundle{{Transactions: []ethapi.TransactionArgs{send}}} + slot := common.Hash{0x1} + + // Over-limit requests: empty TransactionArgs are fine since validateBundles + // runs before any tracing, so nothing is executed. + tooManyBundles := make([]Bundle, maxTraceCallManyBundles+1) + for i := range tooManyBundles { + tooManyBundles[i] = Bundle{Transactions: []ethapi.TransactionArgs{send}} + } + tooManyPerBundle := []Bundle{{Transactions: make([]ethapi.TransactionArgs, maxTraceCallManyCallsPerBundle+1)}} + // Enough bundles, each at the per-bundle cap, to exceed the total cap while + // staying under the bundle-count cap — so the total check is what trips. + tooManyTotal := make([]Bundle, maxTraceCallManyTotalCalls/maxTraceCallManyCallsPerBundle+1) + for i := range tooManyTotal { + tooManyTotal[i] = Bundle{Transactions: make([]ethapi.TransactionArgs, maxTraceCallManyCallsPerBundle)} + } + + tests := []struct { + name string + bundles []Bundle + sc StateContext + config *TraceCallConfig + wantErr string + }{ + {"empty bundles", nil, StateContext{BlockNumber: latest}, nil, "empty bundles"}, + {"bundles without transactions", []Bundle{{}}, StateContext{BlockNumber: latest}, nil, "empty bundles"}, + {"too many bundles", tooManyBundles, StateContext{BlockNumber: latest}, nil, "too many bundles"}, + {"too many calls in a bundle", tooManyPerBundle, StateContext{BlockNumber: latest}, nil, "too many calls in a single bundle"}, + {"too many calls in total", tooManyTotal, StateContext{BlockNumber: latest}, nil, "too many calls across all bundles"}, + {"pending block", withTx, StateContext{BlockNumber: rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)}, nil, "tracing on top of pending is not supported"}, + {"unknown block", withTx, StateContext{BlockNumber: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(genBlocks + 1))}, nil, fmt.Sprintf("block #%d not found", genBlocks+1)}, + {"no block or hash", withTx, StateContext{}, nil, "invalid arguments; neither block nor hash specified"}, + {"transaction index below -1", withTx, StateContext{BlockNumber: latest, TransactionIndex: func() *int { i := -2; return &i }()}, nil, "transaction index -2 out of range"}, + { + "conflicting fee fields", + []Bundle{{Transactions: []ethapi.TransactionArgs{{ + From: &accounts[0].addr, To: &accounts[1].addr, + GasPrice: (*hexutil.Big)(big.NewInt(1)), + MaxFeePerGas: (*hexutil.Big)(big.NewInt(1)), + }}}}, + StateContext{BlockNumber: latest}, nil, + "both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified", + }, + { + "unsupported block override", + []Bundle{{Transactions: []ethapi.TransactionArgs{send}, BlockOverride: &override.BlockOverrides{BeaconRoot: &slot}}}, + StateContext{BlockNumber: latest}, nil, `block override "beaconRoot" is not supported`, + }, + { + "conflicting state override", + withTx, StateContext{BlockNumber: latest}, + &TraceCallConfig{StateOverrides: &override.StateOverride{ + accounts[0].addr: { + State: map[common.Hash]common.Hash{{}: {}}, + StateDiff: map[common.Hash]common.Hash{{}: {}}, + }, + }}, + "has both 'state' and 'stateDiff'", + }, + { + "non-increasing block number across bundles", + []Bundle{ + {Transactions: []ethapi.TransactionArgs{send}, BlockOverride: &override.BlockOverrides{Number: (*hexutil.Big)(big.NewInt(0x500))}}, + {Transactions: []ethapi.TransactionArgs{send}, BlockOverride: &override.BlockOverrides{Number: (*hexutil.Big)(big.NewInt(0x100))}}, + }, + StateContext{BlockNumber: latest}, nil, "block numbers must be in order", + }, + { + "non-increasing block time across bundles", + []Bundle{ + {Transactions: []ethapi.TransactionArgs{send}, BlockOverride: &override.BlockOverrides{Time: func() *hexutil.Uint64 { t := hexutil.Uint64(0x9999); return &t }()}}, + {Transactions: []ethapi.TransactionArgs{send}, BlockOverride: &override.BlockOverrides{Time: func() *hexutil.Uint64 { t := hexutil.Uint64(0x10); return &t }()}}, + }, + StateContext{BlockNumber: latest}, nil, "block timestamps must be in order", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := api.TraceCallMany(t.Context(), tc.bundles, tc.sc, tc.config) + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("want error containing %q, got %v", tc.wantErr, err) + } + }) + } + }) +} + func TestTraceTransaction(t *testing.T) { t.Parallel()