Skip to content

execution/state: don't seed initial BAL balance from post-write reads#20843

Merged
mh0lt merged 4 commits intomainfrom
yperbasis/bal-finalize-init-balance
Apr 28, 2026
Merged

execution/state: don't seed initial BAL balance from post-write reads#20843
mh0lt merged 4 commits intomainfrom
yperbasis/bal-finalize-init-balance

Conversation

@yperbasis
Copy link
Copy Markdown
Member

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

Reproduction (bal-devnet-3, block 91648)

The lighthouse-erigon-super-1 node looped on:

BAL mismatch: computed   block=91648 hash=0x973750b403f7df11... headerHash=0xff40dbc3b03daf0b...
BAL mismatch: stored     block=91648 hash=0xff40dbc3b03daf0b...
invalid block, block=91648 ...: block access list mismatch

Diff between the two BALs was a single missing entry on erigon's side:

[0] addr=0x0000000000000000000000000000000000000000
  computed:  balanceChanges=[41:0x16ebce16]
  stored:    balanceChanges=[1:0x16eaeb76 41:0x16ebce16]

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.

History

This fix is a slightly retitled, comment-expanded version of the updateRead tightening originally landed in 0428bb818b on the yperbasis/dbBALBytes_parexec branch. 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 issues
  • make erigon — builds clean
  • Apply on the stuck lighthouse-erigon-super-1 node and verify it advances past block 91648
  • Add a regression test exercising the chain-tip FCU validation path with BAL pre-population (follow-up)

🤖 Generated with Claude Code

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>
@yperbasis yperbasis added the Glamsterdam https://eips.ethereum.org/EIPS/eip-7773 label Apr 27, 2026
yperbasis and others added 2 commits April 27, 2026 16:21
…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>
@yperbasis yperbasis marked this pull request as ready for review April 28, 2026 07:04
@yperbasis yperbasis requested a review from mh0lt as a code owner April 28, 2026 07:04
@yperbasis yperbasis requested review from Copilot and taratorio April 28, 2026 07:04
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

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.updateRead so initialBalanceValue is 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 mh0lt added this pull request to the merge queue Apr 28, 2026
mh0lt added a commit that referenced this pull request Apr 28, 2026
…ost-write reads (#20864)

Cherry-pick of #20843 to bal-devnet-3.

---------

Co-authored-by: yperbasis <andrey.ashikhmin@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged via the queue into main with commit aa2b21c Apr 28, 2026
42 checks passed
@mh0lt mh0lt deleted the yperbasis/bal-finalize-init-balance branch April 28, 2026 09:14
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.

4 participants