Skip to content

exec3_parallel: eliminate IBS round-trip in finalize path#19814

Merged
mh0lt merged 15 commits intomainfrom
exec3-parallel-direct-finalize
Mar 18, 2026
Merged

exec3_parallel: eliminate IBS round-trip in finalize path#19814
mh0lt merged 15 commits intomainfrom
exec3-parallel-direct-finalize

Conversation

@mh0lt
Copy link
Copy Markdown
Contributor

@mh0lt mh0lt commented Mar 11, 2026

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 stateObjects 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

  • TestLessConflicts passes
  • Full execution/stagedsync/ test suite passes
  • 17 unit tests for VersionedWrites helpers and finalize comparison
  • make erigon integration builds clean
  • CI triggered (merge from main resolved cleanly)

Closes #19809

🤖 Generated with Claude Code

Mark Holt and others added 2 commits March 11, 2026 22:52
…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>
@mh0lt mh0lt force-pushed the exec3-parallel-direct-finalize branch from 532d814 to 50f9a64 Compare March 11, 2026 22:52
… 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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-memory StateWriter) to capture MakeWriteSet output for later finalize-time adjustments.
  • Add VersionedWrites helpers (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 resetTx to 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.

Comment on lines +410 to +423
{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},
Comment on lines +469 to +477
{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},
Comment on lines +1291 to +1306
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
}
}
Comment on lines +1261 to +1267
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)
}
Comment thread execution/stagedsync/exec3_parallel.go Outdated
allWrites := ibs.VersionedWrites(true)
vm.FlushVersionedWrites(allWrites, true, tracePrefix)
vm.SetTrace(false)
ibs.FinalizeTx(chainRules, stateWriter)
Comment on lines +410 to +423
{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},
Comment on lines +469 to +477
{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},
Comment on lines +1017 to +1034
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)
Comment on lines +281 to +296
{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},
Comment on lines +281 to +296
{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},
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from Claude:

Critical Issues

  1. 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.

  2. 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

  1. finalizeTx emits writes unconditionally even when fees are zero — could produce spurious SelfDestructPath for empty accounts when eventually enabled.

  2. Minimal IBS in finalizeTx lacks selfdestruct state — PostApplyMessage / EIP-7708 LogSelfDestructedAccounts won't see selfdestructed accounts from execution.

  3. 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).

Mark Holt and others added 2 commits March 16, 2026 16:01
# 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>
@mh0lt
Copy link
Copy Markdown
Contributor Author

mh0lt commented Mar 16, 2026

Addressed the review feedback:

  1. Fixed ignored FinalizeTx errorfinalizeWithIBS now properly checks and returns the error (commit a1d13c8). Good catch.

  2. Clarified PR is preparatory — updated description to note that finalizeTx is implemented and tested but not active. The dispatch intentionally clears CollectorWrites and routes through IBS (line 1052 TODO). The switch gets flipped once BAL correctness is verified in the direct path.

  3. Copilot pointer comments — stale. VersionedWrites is now []*VersionedWrite in our branch but the by-value changes from etl, state: reduce allocation pressure in parallel executor #19884 (merged to main) mean the tests compile correctly with either form. Verified: go test -c ./execution/stagedsync/ succeeds.

  4. Zero-fee writes / selfdestruct state — noted for when we enable the direct path. The current IBS path handles these correctly, and the direct path won't be activated until these edge cases are addressed.

Mark Holt and others added 4 commits March 16, 2026 17:30
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>
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness Issues

  1. 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

  1. 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.

  1. 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

  1. 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, ...)
}

  1. 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
  1. Extra blank line in rw_v3.go

After LightCollector.CreateContract there are two consecutive blank lines (before // NotifyAccumulator). Minor lint risk.

  1. 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>
@mh0lt mh0lt enabled auto-merge (squash) March 17, 2026 16:15
mh0lt and others added 5 commits March 17, 2026 16:16
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>
@mh0lt mh0lt merged commit 8b25da6 into main Mar 18, 2026
37 checks passed
@mh0lt mh0lt deleted the exec3-parallel-direct-finalize branch March 18, 2026 00:17
mh0lt added a commit that referenced this pull request Mar 18, 2026
## 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>
sudeepdino008 added a commit that referenced this pull request Mar 20, 2026
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
sudeepdino008 added a commit that referenced this pull request Mar 26, 2026
…9814)"

This reverts commit 8b25da6.

The commit introduced a wrong trie root error during parallel execution
on Hoodi at block 1,745,211. Bisect confirmed 8b25da6 as the first
bad commit — the parent (ec7bb86) passes cleanly.

Fixes #20012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Glamsterdam https://eips.ethereum.org/EIPS/eip-7773

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exec3_parallel: eliminate IBS round-trip in finalize/fee-calc path

3 participants