eth/tracers: align traceBlockParallel with pre-exec system calls

traceBlockParallel rebuilt per-tx prestate by replaying transactions but did
not execute the same pre-exec system calls that traceBlock/traceChain apply
before transaction execution. This created a semantic mismatch under post-
merge forks (EIP-4788/Prague), where tracer-visible state could diverge from
the canonical block execution context.

Root cause
- The parallel fast-replay path initialized EVM and immediately started tx
  replay.
- It skipped:
  - ProcessBeaconBlockRoot (EIP-4788)
  - ProcessParentBlockHash (Prague / EIP-2935)

Fix
- In traceBlockParallel, after constructing EVM on parent state and before tx
  replay, execute the same pre-exec system calls as other tracing paths:
  - ProcessBeaconBlockRoot when block has a beacon root
  - ProcessParentBlockHash when Prague rules are active
- This makes parallel tracing state preparation behaviorally equivalent to
  traceBlock and traceChain.

Regression test
- Add TestTraceBlockParallelPragueParentHashSystemCall.
- Build a post-merge test chain (beacon+ethash faker), seed history storage
  contract in genesis, run traceBlockParallel, and assert that the EIP-2935
  ring-buffer slot for block-1 stores block.ParentHash().
- The test fails without the fix and passes with it.

Validation
- go test ./eth/tracers -run TestTraceBlockParallelPragueParentHashSystemCall
- go test ./eth/tracers -run 'TestTraceBlock|TestTraceCall|TestTraceTransaction'
This commit is contained in:
Daniel Liu 2026-02-14 23:54:00 +08:00
parent 406a852ec8
commit 429d7aca05
2 changed files with 71 additions and 0 deletions

View file

@ -692,6 +692,12 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat
var failed error
blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
evm := vm.NewEVM(blockCtx, statedb, api.backend.ChainConfig(), vm.Config{})
if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
core.ProcessBeaconBlockRoot(*beaconRoot, evm)
}
if api.backend.ChainConfig().IsPrague(block.Number(), block.Time()) {
core.ProcessParentBlockHash(block.ParentHash(), evm)
}
txloop:
for i, tx := range txs {

View file

@ -19,6 +19,7 @@ package tracers
import (
"context"
"crypto/ecdsa"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
@ -284,6 +285,70 @@ func TestStateHooks(t *testing.T) {
}
}
func TestTraceBlockParallelPragueParentHashSystemCall(t *testing.T) {
var (
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
from = crypto.PubkeyToAddress(key.PublicKey)
to = common.HexToAddress("0x00000000000000000000000000000000deadbeef")
cfg = *params.AllDevChainProtocolChanges
nonce = uint64(0)
)
genesis := &core.Genesis{
Config: &cfg,
Alloc: types.GenesisAlloc{
from: {Balance: big.NewInt(params.Ether)},
to: {Balance: big.NewInt(0)},
params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
},
}
backend := newTestMergedBackend(t, 1, genesis, func(i int, b *core.BlockGen) {
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: nonce,
To: &to,
Value: big.NewInt(1),
Gas: params.TxGas,
GasPrice: b.BaseFee(),
}), types.HomesteadSigner{}, key)
b.AddTx(tx)
nonce++
})
defer backend.teardown()
api := NewAPI(backend)
block := backend.chain.GetBlockByNumber(1)
if block == nil {
t.Fatal("failed to retrieve test block")
}
parent := backend.chain.GetBlock(block.ParentHash(), block.NumberU64()-1)
if parent == nil {
t.Fatal("failed to retrieve parent block")
}
statedb, release, err := backend.StateAtBlock(context.Background(), parent, defaultTraceReexec, nil, true, false)
if err != nil {
t.Fatalf("failed to create parent state: %v", err)
}
defer release()
results, err := api.traceBlockParallel(context.Background(), block, statedb, nil)
if err != nil {
t.Fatalf("traceBlockParallel failed: %v", err)
}
if len(results) != 1 {
t.Fatalf("unexpected result length: have %d want 1", len(results))
}
if results[0].Error != "" {
t.Fatalf("unexpected trace error: %v", results[0].Error)
}
var historyKey common.Hash
binary.BigEndian.PutUint64(historyKey[24:], (block.NumberU64()-1)%params.HistoryServeWindow)
have := statedb.GetState(params.HistoryStorageAddress, historyKey)
want := block.ParentHash()
if have != want {
t.Fatalf("unexpected parent hash in storage: have %v want %v", have, want)
}
}
func TestTraceCall(t *testing.T) {
t.Parallel()