exec3_parallel: eliminate IBS round-trip in finalize path#19814
Conversation
…e path Replace the expensive full-IBS reconstruction during parallel finalize with a direct fee-balance adjustment on pre-computed collector writes. Previously, every TX finalization created an IntraBlockState, called ApplyVersionedWrites (creating stateObjects for ALL TX writes), then applied 2-4 fee-calc balance adjustments. Now parallel workers capture MakeWriteSet output via LightCollector, and finalizeTx adjusts fee balances directly — only reading the 2 fee-calc accounts from the version map. Key changes: - Add LightCollector StateWriter that captures collector-format writes - Split finalize into finalizeTx (hot path) and finalizeSystemTx (block-end/system TXs only) - Fix resetTx to tolerate in-memory-only writers (remove NoopWriter.SetTx shim, replace error with no-op default case) - Add VersionedWrites.SetBalance helper for in-place balance adjustment Closes #19809 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix the direct finalize path (finalizeTx) to generate BalancePath reads for ALL unique addresses in TxOut, not just those with BalancePath writes. The IBS-based path generates these reads via refreshVersionedAccount for every account loaded through GetOrNewStateObject — triggered by any write type (StoragePath, NoncePath, CodePath, etc.). The direct path was only iterating BalancePath writers, missing reads needed for BAL net-zero detection. This fixes 175 BAL hash mismatch failures in the EEST devnet test suite. The remaining 5 failures are pre-existing (EIP-4788/2935 system calls, withdrawals) and fail identically on the base commit. Add 17 unit tests pinning the finalize building blocks: - SetAccountBalanceOrDelete: EIP-161 empty deletion, in-place update, new account field emission, nil account handling - StripBalanceWrite: delta computation (increase/decrease/no-read/nil) - ApplyVersionedWrites reads generation: verifies that BalancePath, StoragePath, and NoncePath writes all produce BalancePath reads for existing accounts, and that newly-created accounts do not Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
532d814 to
50f9a64
Compare
… tests Fix a bug where CollectorWrites from LightCollector (raw execution writes without fee-calc adjustments) were used for MDBX apply even when the IBS-based finalize path computed the correct writes via collector.Writes(). The fix clears CollectorWrites when routing through finalizeWithIBS, so nextResult correctly falls back to the collector's output. All normal TXs now route through the IBS-based finalize path until finalizeTx produces correct BAL reads/writes. The direct finalizeTx path is preserved and tested — a TODO marks where to re-enable it. Add exec3_finalize_test.go with 4 scenarios (simple transfer, London with burnt fees, coinbase-is-recipient, self-transfer) that compare finalizeTx vs finalizeWithIBS output for writes and BalancePath reads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR targets the parallel execution finalize path by capturing collector-format writes during worker execution and introducing helpers to adjust fee-related balances without reconstructing a full IntraBlockState (IBS), plus test coverage to pin the new behavior.
Changes:
- Add
LightCollector(in-memoryStateWriter) to captureMakeWriteSetoutput for later finalize-time adjustments. - Add
VersionedWriteshelpers (SetBalance,SetAccountBalanceOrDelete) and extensive unit tests to lock in behavior needed by the direct finalize path. - Refactor finalize logic by splitting IBS-based handling for system/block-end transactions vs. regular transactions and updating worker
resetTxto tolerate in-memory-only writers.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
execution/state/versionedio.go |
Adds helper APIs to mutate balance/account writes directly in VersionedWrites. |
execution/state/versionedio_test.go |
Adds targeted tests for new VersionedWrites helpers and for balance-read generation behavior relevant to BAL. |
execution/state/rw_v3.go |
Introduces LightCollector to capture collector-format writes without versionedWriteCollector locking. |
execution/state/database.go |
Removes NoopWriter.SetTx to align with updated writer handling. |
execution/stagedsync/exec3_parallel.go |
Refactors finalize flow and adds a new finalizeTx direct path implementation (plus integrates CollectorWrites selection). |
execution/stagedsync/exec3_finalize_test.go |
Adds scenario tests comparing direct finalize vs IBS finalize outputs. |
execution/exec/txtask.go |
Adds CollectorWrites to TxResult for passing worker-captured collector writes into finalize. |
execution/exec/state.go |
Updates writer reset logic and extracts CollectorWrites from LightCollector after MakeWriteSet. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal, Reason: tracing.BalanceDecreaseGasBuy}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: coinbase, Path: state.BalancePath, Val: *newCoinbaseBal, Reason: tracing.BalanceChangeTransfer}, | ||
| } | ||
|
|
||
| collectorWrites := state.VersionedWrites{ | ||
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, | ||
| {Address: coinbase, Path: state.BalancePath, Val: *newCoinbaseBal}, | ||
| {Address: coinbase, Path: state.NoncePath, Val: uint64(0)}, | ||
| {Address: coinbase, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: coinbase, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, |
| {Address: sender, Path: state.BalancePath, Val: *senderBal, Reason: tracing.BalanceChangeTransfer}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| } | ||
|
|
||
| collectorWrites := state.VersionedWrites{ | ||
| {Address: sender, Path: state.BalancePath, Val: *senderBal}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, |
| if engine != nil { | ||
| if postApplyMessageFunc := engine.GetPostApplyMessageFunc(); postApplyMessageFunc != nil { | ||
| minIBS := state.New(vsReader) | ||
| minIBS.SetTxContext(blockNum, txIndex) | ||
| minIBS.SetVersion(txIncarnation) | ||
| minIBS.SetVersionMap(&state.VersionMap{}) | ||
| // Set adjusted balances so GetRemovedAccountsWithBalance | ||
| // can detect selfdestructed fee accounts with residual balance. | ||
| if err := minIBS.SetBalance(result.Coinbase, newCoinbaseBalance, tracing.BalanceIncreaseRewardTransactionFee); err != nil { | ||
| return nil, nil, nil, err | ||
| } | ||
| if hasBurnt { | ||
| if err := minIBS.SetBalance(burntAddr, newBurntBalance, tracing.BalanceDecreaseGasBuy); err != nil { | ||
| return nil, nil, nil, err | ||
| } | ||
| } |
| emptyRemoval := chainRules.IsSpuriousDragon | ||
| result.CollectorWrites = result.CollectorWrites.SetAccountBalanceOrDelete( | ||
| result.Coinbase, coinbaseAcc, newCoinbaseBalance, tracing.BalanceIncreaseRewardTransactionFee, emptyRemoval) | ||
| if hasBurnt { | ||
| result.CollectorWrites = result.CollectorWrites.SetAccountBalanceOrDelete( | ||
| burntAddr, burntAcc, newBurntBalance, tracing.BalanceDecreaseGasBuy, emptyRemoval) | ||
| } |
| allWrites := ibs.VersionedWrites(true) | ||
| vm.FlushVersionedWrites(allWrites, true, tracePrefix) | ||
| vm.SetTrace(false) | ||
| ibs.FinalizeTx(chainRules, stateWriter) |
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal, Reason: tracing.BalanceDecreaseGasBuy}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: coinbase, Path: state.BalancePath, Val: *newCoinbaseBal, Reason: tracing.BalanceChangeTransfer}, | ||
| } | ||
|
|
||
| collectorWrites := state.VersionedWrites{ | ||
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, | ||
| {Address: coinbase, Path: state.BalancePath, Val: *newCoinbaseBal}, | ||
| {Address: coinbase, Path: state.NoncePath, Val: uint64(0)}, | ||
| {Address: coinbase, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: coinbase, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, |
| {Address: sender, Path: state.BalancePath, Val: *senderBal, Reason: tracing.BalanceChangeTransfer}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| } | ||
|
|
||
| collectorWrites := state.VersionedWrites{ | ||
| {Address: sender, Path: state.BalancePath, Val: *senderBal}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, |
| rules := txTask.EvmBlockContext.Rules(txTask.Config) | ||
|
|
||
| if txIndex < 0 || task.IsBlockEnd() { | ||
| return result.finalizeSystemTx(task, txTask, rules, vm, stateReader, stateWriter) | ||
| } | ||
|
|
||
| // Clear CollectorWrites so nextResult uses collector.Writes() from | ||
| // the IBS-based finalize path (which correctly includes fee-calc | ||
| // adjustments and selfdestruct stripping). | ||
| // TODO: once finalizeTx produces correct BAL reads/writes and | ||
| // collector-format writes, gate the direct path on !BAL and | ||
| // remove this workaround. | ||
| result.CollectorWrites = nil | ||
|
|
||
| return result.finalizeWithIBS(task, txTask, prevReceipt, engine, vm, stateReader, stateWriter, | ||
| coinbaseDelta, coinbaseDeltaIncrease, hasCoinbaseDelta, | ||
| burntDelta, burntDeltaIncrease, hasBurntDelta, | ||
| rules, txTrace, tracePrefix) |
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal, Reason: tracing.BalanceDecreaseGasBuy}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: recipient, Path: state.BalancePath, Val: *newRecipientBal, Reason: tracing.BalanceChangeTransfer}, | ||
| } | ||
|
|
||
| // CollectorWrites: LightCollector output from MakeWriteSet. | ||
| // No coinbase (not touched during execution). | ||
| collectorWrites := state.VersionedWrites{ | ||
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, | ||
| {Address: recipient, Path: state.BalancePath, Val: *newRecipientBal}, | ||
| {Address: recipient, Path: state.NoncePath, Val: uint64(0)}, | ||
| {Address: recipient, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: recipient, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, |
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal, Reason: tracing.BalanceDecreaseGasBuy}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: recipient, Path: state.BalancePath, Val: *newRecipientBal, Reason: tracing.BalanceChangeTransfer}, | ||
| } | ||
|
|
||
| // CollectorWrites: LightCollector output from MakeWriteSet. | ||
| // No coinbase (not touched during execution). | ||
| collectorWrites := state.VersionedWrites{ | ||
| {Address: sender, Path: state.BalancePath, Val: *newSenderBal}, | ||
| {Address: sender, Path: state.NoncePath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: sender, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, | ||
| {Address: recipient, Path: state.BalancePath, Val: *newRecipientBal}, | ||
| {Address: recipient, Path: state.NoncePath, Val: uint64(0)}, | ||
| {Address: recipient, Path: state.IncarnationPath, Val: uint64(1)}, | ||
| {Address: recipient, Path: state.CodeHashPath, Val: accounts.EmptyCodeHash}, |
yperbasis
left a comment
There was a problem hiding this comment.
Review from Claude:
Critical Issues
-
finalizeTx is dead code — the optimization is not enabled. The dispatch explicitly clears CollectorWrites and always routes to finalizeWithIBS. The PR title is misleading — this is preparatory
infrastructure, not the elimination itself. -
Ignored error from ibs.FinalizeTx in finalizeWithIBS. The error return is silently discarded:
ibs.FinalizeTx(chainRules, stateWriter)
Meanwhile finalizeSystemTx correctly checks this error. This is a real bug in the refactored code.
Moderate Issues
-
finalizeTx emits writes unconditionally even when fees are zero — could produce spurious SelfDestructPath for empty accounts when eventually enabled.
-
Minimal IBS in finalizeTx lacks selfdestruct state — PostApplyMessage / EIP-7708 LogSelfDestructedAccounts won't see selfdestructed accounts from execution.
-
NoopWriter.SetTx removal — should be safe (compile-time catch) but worth verifying no other callers depend on it.
Minor
- var err error shadowing in the hasBurnt block of finalizeTx (works but code smell)
- LightCollector.UpdateAccountData dereferences original without nil check (unlike versionedWriteCollector)
- Tests are thorough but only exercise the unused finalizeTx path
Positives
- Clean architectural decomposition of the three finalize paths
- Well-designed LightCollector with TakeWrites() preventing accidental reuse
- Correct EIP-161 handling in SetAccountBalanceOrDelete
- Smart incremental delivery — infrastructure first, flip the switch later
- Excellent test coverage (17 unit tests) for the helper functions
- Good commit hygiene
Verdict
The PR is solid infrastructure work with good test coverage, but the ignored FinalizeTx error in finalizeWithIBS should be fixed before merge, and the PR description should clarify this is preparatory (the
optimization isn't active yet).
# Conflicts: # execution/state/rw_v3.go
The error return from ibs.FinalizeTx was silently discarded in the IBS-based finalize path, while finalizeSystemTx correctly checked it. Fix to propagate the error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed the review feedback:
|
Route all regular TX finalization through finalizeTx (the direct path) instead of the IBS-based path. The direct path computes fee-adjusted balances on pre-computed collector writes without reconstructing full IntraBlockState, avoiding expensive ApplyVersionedWrites for all TX writes. Changes: - Remove CollectorWrites=nil workaround that forced IBS path - Add zero-fee write guards: only emit coinbase/burnt balance writes when the balance actually changed - Remove finalizeWithIBS (~120 lines of dead code) - Update tests: convert IBS-path tests to test finalizeTx directly, replace IBS-vs-direct comparison test with multi-scenario correctness test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yperbasis
left a comment
There was a problem hiding this comment.
Correctness Issues
- PostApplyMessage minimal IBS lacks selfdestruct state (moderate)
exec3_parallel.go ~line 1274: The minimal IBS for PostApplyMessage only calls SetBalance on coinbase/burnt. For engines using misc.LogSelfDestructedAccounts (EIP-7708), ibs.GetRemovedAccountsWithBalance() only
reports accounts marked selfdestructed in the IBS journal. If a fee account was selfdestructed during execution, the minimal IBS won't know — residual-balance burn logs will be missed.
Follow up created: Issue #19951
- LightCollector.UpdateAccountData dereferences original without nil guard
rw_v3.go ~line 558:
if original.Incarnation > accountCopy.Incarnation {
This is consistent with versionedWriteCollector which does the same thing, and original comes from &obj.original (a value type, never nil in practice). So not a real bug — but the asymmetry with
SetAccountBalanceOrDelete (which does nil-guard acc) is worth noting.
- finalizeReads skips newly-created accounts correctly — but the comment is load-bearing
exec3_parallel.go ~lines 1322-1344: The loop over result.TxOut reads each address from vsReader and only emits a BalancePath read if the account exists. This correctly mirrors the IBS behavior where
refreshVersionedAccount isn't called for createObject accounts. The extensive comment explaining why this works is essential — if someone removes it and changes the loop to unconditionally emit reads, BAL
net-zero detection will break. Consider adding a test that explicitly verifies no BalancePath read is emitted for newly-created accounts in the finalizeTx path (the existing test in versionedio_test.go covers
the IBS side but not finalizeTx itself).
Code Quality
- Duplicate oldBurntBalance computation
exec3_parallel.go ~lines 1227-1240 and ~lines 1253-1262: The burnt-changed check is computed inline twice (once for CollectorWrites update, once for allWrites append). Extract a burntChanged bool to match the
coinbaseChanged pattern:
oldBurntBalance := uint256.Int{}
if burntAcc != nil {
oldBurntBalance = burntAcc.Balance
}
burntChanged := newBurntBalance != oldBurntBalance
if burntChanged {
result.CollectorWrites = result.CollectorWrites.SetAccountBalanceOrDelete(...)
}
// ...
if burntChanged {
allWrites = append(allWrites, ...)
}
- Dual write-sets (CollectorWrites vs allWrites) — easy to diverge
finalizeTx maintains two parallel write-sets: result.CollectorWrites (for MDBX apply in nextResult) and allWrites (for version map + reads). These must stay in sync. If a future change updates one but not the
other, the version map and MDBX will diverge silently. Consider either:
- Deriving allWrites from CollectorWrites (single source of truth), or
- Adding a comment/invariant that documents the dual-write requirement
- Extra blank line in rw_v3.go
After LightCollector.CreateContract there are two consecutive blank lines (before // NotifyAccumulator). Minor lint risk.
- var err error shadow in hasBurnt block
exec3_parallel.go ~line 1200: Inner var err error shadows outer err. Works correctly (Go scoping), but a burntErr or combined declaration would be cleaner.
Design Observations
Strengths:
- Clean architectural split — system TXs (1/block, ~0.001% of total) keep the safe IBS path; hot-path TXs skip the expensive ApplyVersionedWrites for all writes
- LightCollector.TakeWrites() prevents accidental reuse — good ownership transfer pattern
- SetAccountBalanceOrDelete correctly handles EIP-161 empty deletion with full-field emission for new accounts
- Excellent test coverage: 17 unit tests for helpers + 4 scenario tests for the finalize path
- The resetTx default-case fix is correct — in-memory writers don't need DB transactions
The CollectorWrites vs collector.Writes() dispatch in nextResult:
if txResult.CollectorWrites != nil {
txResult.writes = txResult.CollectorWrites
} else {
txResult.writes = collector.Writes()
}
This is the critical junction. For regular TXs, CollectorWrites comes from LightCollector + fee adjustments from finalizeTx. For system TXs, CollectorWrites is nil (system TXs don't use LightCollector), so it
falls back to the collector populated by finalizeSystemTx → FinalizeTx. This is correct.
Verdict
Solid optimization with good test coverage and clean decomposition. The build must be fixed before merge (type mismatch in findRead). The dual write-set pattern (CollectorWrites / allWrites) is the main
maintenance risk going forward — I'd recommend at minimum a comment documenting the invariant, ideally deriving one from the other.
Blocking: Fix compilation error in versionedio_test.go:852
Non-blocking: Extract burntChanged, document dual write-set invariant, track selfdestruct-in-minimal-IBS for EIP-7708
Pass nil tx to blockReader.Header in getHashFn closure so BLOCKHASH reads go directly to frozen snapshots, bypassing the fcuOverlay. This eliminates the nil-db crash in memory_mutation.go when the overlay is recycled, and removes the getHashMu mutex (snapshots are thread-safe). Also fix test compilation errors from ReadSet value-type change (map values are now VersionedRead, not *VersionedRead) and LondonBlock type change (*big.Int → *uint64). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When BAL (EIP-7928) is active, the direct finalizeTx path produces incorrect read/write sets because it computes fee-adjusted balances arithmetically without going through AddBalance/SubBalance on IntraBlockState. This causes BAL hash mismatches. Restore finalizeWithIBS for BAL-active blocks (experimentalBAL or Amsterdam fork), which reconstructs full IBS state and generates correct BAL-compatible reads/writes. The direct path remains for non-BAL blocks where performance matters and BAL hash isn't computed. Also fix coinbase/burnt reads to always emit BalancePath reads even when the account doesn't exist in pre-state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LogSelfDestructedAccounts was summing execution-time and finalization-time residual balances for selfdestructed accounts. This double-counts because the finalization balance already includes the execution-time residual plus any additions during finalization (e.g. priority fee). Fix: when the finalized balance is available, use it directly instead of adding it to the execution-time value. Cherry-picked from bal-devnet-3 (4a93583). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix two CI failures: 1. gofmt: fix indentation of balActive line in exec3_parallel.go 2. DATA RACE: PriorityQueue.Close() was closing resultCh while Add() could be concurrently sending on it (outside the lock). The race detector flagged chansend vs closechan on the same channel. Fix: never close(resultCh). Close() just sets closed=true. Drain() nils the reference when it detects closed && empty. The channel is GC'd when all references are dropped. Consumers already select on ctx.Done() to unblock when the queue shuts down. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary - Add `LightCollector` (lightweight `StateWriter`) to capture `MakeWriteSet` output for later finalize-time adjustments - Add `VersionedWrites` helpers (`SetBalance`, `SetAccountBalanceOrDelete`) with unit tests - Split finalize into `finalizeTx` (direct path) and `finalizeSystemTx` (block-end/system TXs) - Fix `resetTx` type-switch to tolerate in-memory-only writers - Fix ignored `FinalizeTx` error in `finalizeWithIBS` **Note:** This is preparatory infrastructure. The direct `finalizeTx` path is implemented and tested but not yet active — the dispatch currently clears `CollectorWrites` and routes through the IBS path (see TODO at line 1052). The switch will be flipped once BAL read/write correctness is verified in the direct path. ## Detail Previously every TX finalization created an `IntraBlockState`, called `ApplyVersionedWrites` (creating `stateObject`s for ALL TX writes), then applied 2–4 fee-calc balance adjustments. The new `finalizeTx` adjusts fee balances directly on pre-computed collector writes — only reading the 2 fee-calc accounts (coinbase + burnt) from the version map. ## Test plan - [x] `TestLessConflicts` passes - [x] Full `execution/stagedsync/` test suite passes - [x] 17 unit tests for `VersionedWrites` helpers and finalize comparison - [x] `make erigon integration` builds clean - [x] CI triggered (merge from main resolved cleanly) Closes #19809 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Mark Holt <erigon@dev-bm-e3-ethmainnet-n4.erigon.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The direct finalizeTx path (introduced in #19814) bypasses versionedWriteCollector, which means rs.accounts is never updated for regular TXs. This breaks the cross-block timing hole bridge: block N+1 workers reading via bufferedReader see stale state for accounts modified by block N, because: 1. rs.accounts doesn't have the latest values (LightCollector doesn't update it) 2. SharedDomains may not have been updated yet (async applyResults) Fix: add UpdateBufferedAccounts(writes VersionedWrites) method on StateV3Buffered that reconstructs account/code/storage state from VersionedWrites and updates rs.accounts. Call it from the finalize goroutine when using CollectorWrites (the direct path). This is the root cause of the wrong trie root at block 1,745,211 on Hoodi reported in #20012. Fixes #20012
Summary
LightCollector(lightweightStateWriter) to captureMakeWriteSetoutput for later finalize-time adjustmentsVersionedWriteshelpers (SetBalance,SetAccountBalanceOrDelete) with unit testsfinalizeTx(direct path) andfinalizeSystemTx(block-end/system TXs)resetTxtype-switch to tolerate in-memory-only writersFinalizeTxerror infinalizeWithIBSNote: This is preparatory infrastructure. The direct
finalizeTxpath is implemented and tested but not yet active — the dispatch currently clearsCollectorWritesand routes through the IBS path (see TODO at line 1052). The switch will be flipped once BAL read/write correctness is verified in the direct path.Detail
Previously every TX finalization created an
IntraBlockState, calledApplyVersionedWrites(creatingstateObjects for ALL TX writes), then applied 2–4 fee-calc balance adjustments. The newfinalizeTxadjusts fee balances directly on pre-computed collector writes — only reading the 2 fee-calc accounts (coinbase + burnt) from the version map.Test plan
TestLessConflictspassesexecution/stagedsync/test suite passesVersionedWriteshelpers and finalize comparisonmake erigon integrationbuilds cleanCloses #19809
🤖 Generated with Claude Code