builder: compute state root directly during block assembly#19707
builder: compute state root directly during block assembly#19707
Conversation
Add 5 new BAL (Block Access List) tests covering diverse transaction types beyond simple transfers: - TestEngineApiBAlContractCreation: deploy Token contract, verify CodeChange - TestEngineApiBAlStorageWrites: mint tokens, verify StorageChange entries - TestEngineApiBAlMultiTxBlock: transfer + mint in same block - TestEngineApiBAlMixedBlock: transfer + deploy + contract call + withdrawals (known failure: BAL mismatch under parallel execution, see #19668) - TestEngineApiBAlSelfDestruct: storage writes + self-destruct in same block Also add WithWithdrawals option to MockCl for testing CL withdrawal processing in block building. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the ExecV3 re-execution pass from SpawnBuilderExecStage. Instead of building a block with NoopWriter and then re-executing via ExecV3 to compute the state root, write state changes directly to SharedDomains during block assembly and call ComputeCommitment at the end. The sd parameter from MiningStep is already backed by a MemoryBatch, so all writes are ephemeral and discarded when MiningStep returns. A separate filterSd is used for filterBadTransactions speculative nonce/balance writes, which must not pollute the commitment computation. This removes ~80 lines of ExecV3 orchestration, the writeBlockForExecution helper, the SendersCfg parameter, and the parallel-tx unwrapping logic.
When the builder called FinalizeTx after each user transaction, it wrote intermediate state to SharedDomains. If a later system call (e.g. EIP-7002 withdrawal request dequeue) reverted storage slots back to their original values, CommitBlock's blockOriginStorage==dirtyStorage optimization skipped the undo write, leaving stale values in sd.mem and producing a wrong state root. Fix: use NoopWriter for FinalizeTx in Initialize and AddTransactions. Only CommitBlock (in AssembleBlock) uses the real stateWriter. IBS dirty state persists across FinalizeTx calls, so CommitBlock correctly writes all final state without stale intermediate values. Also: - Forward execution requests in MockCl.InsertNewPayload - Isolate filterSd with its own MemoryBatch - Add TestEngineApiBuiltBlockWithWithdrawalRequest
There was a problem hiding this comment.
Pull request overview
Refactors the block builder execution stage to compute the state root directly from domain writes produced during block assembly (removing the ExecV3 re-execution pass), and expands BAL (Block Access List) engine API test coverage to include more transaction varieties and scenarios.
Changes:
- Remove ExecV3 replay from the builder exec stage and compute commitment/state root directly from
SharedDomains. - Extend
BlockAssemblerto optionally accept a realStateWriter(and txNum advancing hook) for domain writes during assembly/finalization. - Add/extend Engine API tests for BAL coverage and builder validation scenarios (including withdrawals / execution requests forwarding).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| node/eth/backend.go | Removes StageSendersCfg from builder stage wiring. |
| execution/execmodule/execmoduletester/exec_module_tester.go | Updates exec-module tester stage wiring to match removed senders stage. |
| execution/exec/block_assembler.go | Adds optional state-writer plumbing and txNum-advancing hook for assembly/finalization paths. |
| execution/builder/builderstages/stages.go | Updates SpawnBuilderExecStage call signature after removing senders dependency. |
| execution/builder/builderstages/exec.go | Implements the “compute commitment directly” builder flow; introduces isolated filterSd for filtering speculation. |
| execution/engineapi/engineapitester/mock_cl.go | Adds withdrawals option and forwards ExecutionRequests from GetPayload to NewPayload. |
| execution/engineapi/engine_api_builder_test.go | Adds a builder integration test exercising withdrawal-request tx inclusion and validation. |
| execution/engineapi/engine_api_bal_test.go | Adds additional BAL tests for contract creation, storage writes, multi-tx, mixed blocks, and self-destruct coverage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| txNum, _, err := sd.SeekCommitment(ctx, tx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| sd.SetTxNum(txNum) | ||
| sd.GetCommitmentContext().SetDeferBranchUpdates(false) | ||
|
|
||
| stateWriter := state.NewWriter(sd.AsPutDel(tx), nil, txNum) | ||
| stateReader := state.NewReaderV3(sd.AsGetter(tx)) |
There was a problem hiding this comment.
txNum is taken from sd.SeekCommitment() and then reused unchanged for both the stateWriter and ComputeCommitment(). If txNum is expected to advance across block boundaries / tx boundaries (as in other execution paths), this will compute the commitment at the wrong txNum and may diverge from validation. Consider maintaining a local txNum counter (starting from the sought value) and incrementing it for block init / each included tx / finalization, calling stateWriter.SetTxNum(...) (and sd.SetTxNum(...) if needed), and pass the final value to ComputeCommitment().
There was a problem hiding this comment.
This is not necessary because: All writes at the same txNum work correctly as last-write-wins in the batch, and ComputeCommitment sees the final state correctly. TestEngineApiBuiltBlockStateMatchesValidation confirms the root hash matches.
No correctness issue. Adding a local txNum here will cause confusion to anyone reading the code as they will believe that it is necessary for correctness where in practise this is not the case as the test shows.
| // Known issue: currently fails with BAL mismatch under parallel execution (#19668). | ||
| func TestEngineApiBAlMixedBlock(t *testing.T) { |
There was a problem hiding this comment.
This test includes a comment saying it is a known failing case under parallel execution (#19668), but the test is still enabled (only gated by dbg.Exec3Parallel). If the issue is still present, this will make the suite fail/flaky. Either remove/update the comment if the issue is resolved, or explicitly t.Skip/gate the test on the known-broken condition until #19668 is fixed.
| func TestEngineApiBAlContractCreation(t *testing.T) { | ||
| if !dbg.Exec3Parallel { | ||
| t.Skip("requires parallel exec") | ||
| } | ||
| eat := engineapitester.DefaultEngineApiTester(t) | ||
| eat.Run(t, func(ctx context.Context, t *testing.T, eat engineapitester.EngineApiTester) { |
There was a problem hiding this comment.
Test function names use BAl (lowercase l) rather than BAL. For consistency with the acronym used throughout the codebase and the file (BlockAccessList/BAL), consider renaming these new tests (and related helpers if any) to use BAL.
yperbasis
left a comment
There was a problem hiding this comment.
advanceTxNum is always nil — the TxNumAdvancer type and all the if ba.advanceTxNum != nil checks are dead code in this PR. Either remove the mechanism or wire it up. As-is, it's confusing infrastructure that
suggests txNum tracking is needed but isn't actually happening.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
advanceTxNum was always passed as nil in SetStateWriter — the TxNumAdvancer type and all three if ba.advanceTxNum != nil call sites were dead code. Remove the TxNumAdvancer type, the advanceTxNum field on BlockAssembler, the three call sites in Initialize/AddTransactions/AssembleBlock, and simplify SetStateWriter to a single-parameter method. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixed by: cf74ee0 |
Rename TestEngineApiBAlContractCreation, TestEngineApiBAlStorageWrites, TestEngineApiBAlMultiTxBlock, TestEngineApiBAlMixedBlock, and TestEngineApiBAlSelfDestruct to use the correct BAL acronym (uppercase L), consistent with the rest of the codebase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…allel block exec Three related fixes for parallel execution (EXEC3_PARALLEL=true): 1. exec3_parallel.go: encode txTask.Txs directly for accumulator.StartChange instead of reading via RawTransactions(applyTx). The execLoop's applyTx is a TemporalRo snapshot opened before the staged-sync RwTx commits kv.HeaderCanonical for the block, so RawTransactions returned empty bytes. This caused the txpool to never learn which transactions were mined, leaving stale nonces 0-6 in pending and preventing block 3 from including the remaining txns after a gas-limit overflow in block 2. 2. exec/block_assembler.go: set block-end tx context (ibs.SetTxContext) before FinalizeBlockExecution so that withdrawal/system-call writes are recorded at TxIndex=len(txns), matching the validator path in exec3_parallel.go. 3. engineapitester/engine_api_tester.go: add EIP-4788 beacon root contract to genesis alloc so that block-init storage writes appear in the BAL, enabling TestEngineApiBALMixedBlock to run without a skip. engine_api_bal_test.go: remove now-stale "Known issue" comment and skip guard from TestEngineApiBALMixedBlock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b52b02f to
1657b81
Compare
## Summary Now that #19707 has merged, ExecV3 is no longer used for block production (the builder uses dedicated `BuilderCreateBlock`/`BuilderExecution`/`BuilderFinish` stages via `BlockAssembler`). This means `isBlockProduction` was always `false` inside ExecV3, making all its guarded code paths dead. - Remove `stages.ModeBlockProduction` constant; the two builder-pipeline callers now use `ModeApplyingBlocks` (the mode label is irrelevant to ExecV3's logic) - Remove `isBlockProduction bool` field from `txExecutor` and all construction sites - Always call `engine.Finalize` in both serial and parallel executors (never `FinalizeAndAssemble`) - Always validate state root and receipt hash — no more mining bypass - Remove `blockProduction bool` param from `ProcessBAL`; drop the BAL hash-assignment branch that was only reachable during block production - Remove `isMining bool` from `BlockPostValidation` and `BlockPostExecutionValidator.Process`; drop the receipt-hash patching branch - Remove unused `isMining bool` param from `NewWorkersPool` - Remove `isBlockProduction bool` param from `computeAndCheckCommitmentV3`; drop the `header.Root` assignment branch Note: `FinalizeBlockExecution` in `block_exec.go` retains its `isMining bool` parameter — it is still legitimately called with `isMining=true` from `BlockAssembler` and `ExecuteBlockEphemerally`. ## Test plan - [x] `make test-all` passes locally 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
This PR contains two sets of changes:
1. Builder refactor: compute state root directly during block assembly (#19318)
Remove the ExecV3 re-execution pass from
SpawnBuilderExecStage. Instead of building a block withNoopWriterand then re-executing via ExecV3 to compute the state root, write state changes directly toSharedDomainsduring block assembly and callComputeCommitmentat the end.Key changes:
block_assembler.go: AddedSetStateWriter(w, advanceTxNum)method. When set,ba.writer()returns the real writer instead ofNoopWriter.Initialize,AddTransactions, andAssembleBlockall useba.writer().exec.go: Usessd(the parameter fromMiningStep, already backed by aMemoryBatch) for execution writes andComputeCommitment. A separatefilterSdhandlesfilterBadTransactionsspeculative nonce/balance writes, which must not pollute the commitment.Removed: ~80 lines of ExecV3 orchestration,
writeBlockForExecutionhelper,SendersCfgparameter, parallel-tx unwrapping logic.stages.go,backend.go,exec_module_tester.go: RemovedSendersCfgfromBuilderStagescall sites.Why filterSd is needed:
filterBadTransactionsmakes speculative writes (nonce++, balance -= cost) to check transaction validity. If a tx passes the filter but fails during EVM execution (e.g., gas limit exceeded),FinalizeTxis never called, so the speculative write remains. These stale writes would corruptComputeCommitmentif they went tosd. The separatefilterSdisolates this speculation.2. Varied BAL tests for Phase 1d/1e (#19632, #19633)
Extend the BAL (Block Access List, EIP-7928) test coverage beyond simple ETH transfers to exercise diverse transaction types and multi-transaction blocks:
New Tests
TestEngineApiBAlContractCreationTestEngineApiBAlStorageWritesTestEngineApiBAlMultiTxBlockTestEngineApiBAlMixedBlockTestEngineApiBAlSelfDestructBuilder Tests
TestEngineApiBuiltBlockStateMatchesValidationTestEngineApiBuiltBlockWithContractDeployAndCallTestEngineApiBuiltBlockReorgRecoveryTest plan
go test ./execution/engineapi/...— all 24 tests passmake test-short— passmake test-all— passmake test-all-race— passCloses: #19318, #19632, #19633