execution/state: don't seed initial BAL balance from post-write reads#20843
Merged
execution/state: don't seed initial BAL balance from post-write reads#20843
Conversation
The parallel executor's block-end finalize creates a fresh IBS (unlike the assembler which reuses the same IBS with cached state objects). This fresh IBS generates BalancePath reads for accounts that were already written during the same block — the initialize-phase system calls (EIP-4788 beacon root, EIP-2935 history storage), the burnt contract (EIP-1559 base-fee burn), or any account whose tx-1 write was pre-populated into the version map from a stored BAL sidecar in the chain-tip FCU validation path. These post-write reads were treated as the "initial" (pre-block) balance by `updateRead`, so `applyToBalance`'s net-zero filter would later drop the legitimate write whose value happened to equal the post-write read. The block's computed BAL hash then disagreed with the header-attested BAL hash, putting the node in an unrecoverable retry loop on the offending payload. Tighten the guard to only set `initialBalanceValue` from a balance read that arrives BEFORE any balance writes have been recorded. Reads arriving after a write reflect post-write state and cannot be used as a pre-block reference. Reproduced on bal-devnet-3 at block 91648 where the node looped on "BAL mismatch: got 0x973750... expected 0xff40db...". The diff was a single missing entry: balanceChanges=[1:0x16eaeb76] for the zero address (the EIP-1559 burnt contract on this devnet). The fresh IBS read at finalize observed the value already pre-populated from the sidecar BAL for tx 1 (0x16eaeb76), set initialBalanceValue to that, and the net-zero filter then dropped the very write whose value the read had borrowed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oning Captures the bal-devnet-3 block-91648 BAL hash mismatch in a single-file test: a balance write at txIndex=1 followed by a later BalancePath read of the same value (the pattern emitted by the parallel executor's fresh-IBS finalize, or by a BAL-prepopulated read of a tx's predicted write). Without the previous commit's `updateRead` guard, the late read seeds `initialBalanceValue` and `applyToBalance`'s net-zero filter then drops the legitimate write — the exact symptom that put `lighthouse-erigon-super-1` into a retry loop on the offending payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adjusts VersionedIO’s BAL construction to avoid treating post-write balance reads as the “initial/pre-block” balance, preventing legitimate balance writes from being incorrectly filtered as net-zero and causing BAL hash mismatches during parallel executor finalization.
Changes:
- Tighten
accountState.updateReadsoinitialBalanceValueis only seeded from a balance read that occurs before any balance write has been observed for that account. - Add a regression test reproducing the bal-devnet-3 block 91648 failure pattern (write, then later read of the same post-write value).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
execution/state/versionedio.go |
Prevents post-write balance reads from seeding initialBalanceValue, avoiding erroneous net-zero filtering in applyToBalance. |
execution/state/versionedio_test.go |
Adds regression coverage ensuring a post-write balance read does not remove the first legitimate balance write from the BAL. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mh0lt
approved these changes
Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The parallel executor's block-end finalize creates a fresh IBS (unlike the assembler which reuses the same IBS with cached state objects). This fresh IBS generates
BalancePathreads for accounts that were already written during the same block — the initialize-phase system calls (EIP-4788 beacon root, EIP-2935 history storage), the burnt contract (EIP-1559 base-fee burn), or any account whose tx-1 write was pre-populated into the version map from a stored BAL sidecar in the chain-tip FCU validation path.These post-write reads were treated as the "initial" (pre-block) balance by
updateRead, soapplyToBalance's net-zero filter would later drop the legitimate write whose value happened to equal the post-write read. The block's computed BAL hash then disagreed with the header-attested BAL hash, putting the node in an unrecoverable retry loop on the offending payload.Tighten the guard to only set
initialBalanceValuefrom a balance read that arrives before any balance writes have been recorded. Reads arriving after a write reflect post-write state and cannot be used as a pre-block reference.Reproduction (bal-devnet-3, block 91648)
The
lighthouse-erigon-super-1node looped on:Diff between the two BALs was a single missing entry on erigon's side:
The fresh IBS read at finalize observed the value already pre-populated from the sidecar BAL for tx 1 (
0x16eaeb76), setinitialBalanceValueto that, and the net-zero filter then dropped the very write whose value the read had borrowed.History
This fix is a slightly retitled, comment-expanded version of the
updateReadtightening originally landed in0428bb818bon theyperbasis/dbBALBytes_parexecbranch. A follow-up there (6b34cf657e) reverted this part with the rationale "no test in the suite depends on it and it adds a post-write / pre-write branch that is not needed"; the bal-devnet-3 chain-tip FCU symptom shows the branch IS needed and is not represented in the existing test suite.Test plan
make lint— 0 issuesmake erigon— builds cleanlighthouse-erigon-super-1node and verify it advances past block 91648🤖 Generated with Claude Code