Skip to content

Fix BAL validation: make BalancePath cross-check unconditional#19656

Merged
mh0lt merged 1 commit intomainfrom
fix/bal-validate-without-hasbal
Mar 5, 2026
Merged

Fix BAL validation: make BalancePath cross-check unconditional#19656
mh0lt merged 1 commit intomainfrom
fix/bal-validate-without-hasbal

Conversation

@mh0lt
Copy link
Copy Markdown
Contributor

@mh0lt mh0lt commented Mar 5, 2026

Summary

Follow-up to #19628. Two fixes for deterministic BAL computation without a stored BAL body (p2p blocks before eth/71):

  • Remove vm.HasBAL guard from BalancePath cross-check (versionmap.go): The cross-check was only running when a stored BAL body was available for pre-population. Without it, stale "account doesn't exist" reads passed validation when a concurrent worker had already created the account via a BalancePath flush. This caused phantom accounts in the computed BAL, producing hash mismatches.

  • Always validate computed BAL against header (bal_create.go): Remove the dbBALBytes != nil guard so the computed BAL hash is validated even for p2p blocks. With the cross-check fix above, parallel execution is deterministic regardless of whether BAL pre-population was used.

Root Cause

When TX N creates an account and TX M (M > N) reads the account as non-existent, the stale read must be invalidated. The BalancePath cross-check catches this: if BalancePath has an entry at a lower txIndex (from BAL pre-population or worker flush), the AddressPath read is stale.

The bug was that the cross-check was gated behind vm.HasBAL, so it only worked with BAL pre-population — not with worker flushes (the HasBAL=false case).

Test plan

  • New unit test: TestValidateRead_NoHasBAL_AddressPathCrossCheckWithBalancePath
  • All existing TestValidateRead_* tests pass
  • make lint clean
  • Verified on bal-devnet-2: 40K+ p2p blocks with zero BAL mismatches (previously failed at block 8997)

🤖 Generated with Claude Code

…ministic BAL without stored body

The BalancePath cross-check in VersionMap.validateRead was gated behind
vm.HasBAL, meaning it only ran when a stored BAL body was available for
pre-population. Without it (e.g. p2p blocks before eth/71), stale
"account doesn't exist" reads could pass validation when a concurrent
worker had already created the account via a BalancePath flush.

This caused non-deterministic BAL computation — phantom accounts appeared
in the computed BAL that weren't in the reference, producing hash
mismatches at block validation time.

Fix: remove the vm.HasBAL guard so the cross-check runs unconditionally.
Worker flushes create BalancePath entries just like BAL pre-population,
so the check is equally valid in both cases.

Also remove the dbBALBytes != nil guard from ProcessBAL so the computed
BAL hash is always validated against the header, even for p2p blocks.

Verified: 40K+ p2p blocks on bal-devnet-2 with zero mismatches
(previously failed at block 8997).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mh0lt mh0lt requested a review from yperbasis as a code owner March 5, 2026 12:34
@mh0lt mh0lt requested review from shohamc1 and taratorio March 5, 2026 12:35
@mh0lt mh0lt added the Glamsterdam https://eips.ethereum.org/EIPS/eip-7773 label Mar 5, 2026
@mh0lt mh0lt enabled auto-merge (squash) March 5, 2026 16:34
@mh0lt mh0lt merged commit 76f310a into main Mar 5, 2026
30 of 32 checks passed
@mh0lt mh0lt deleted the fix/bal-validate-without-hasbal branch March 5, 2026 18:58
mh0lt added a commit that referenced this pull request Mar 6, 2026
## Summary

Follow-up to #19628. Two fixes for deterministic BAL computation without
a stored BAL body (p2p blocks before eth/71):

- **Remove `vm.HasBAL` guard from BalancePath cross-check**
(`versionmap.go`): The cross-check was only running when a stored BAL
body was available for pre-population. Without it, stale "account
doesn't exist" reads passed validation when a concurrent worker had
already created the account via a BalancePath flush. This caused phantom
accounts in the computed BAL, producing hash mismatches.

- **Always validate computed BAL against header** (`bal_create.go`):
Remove the `dbBALBytes != nil` guard so the computed BAL hash is
validated even for p2p blocks. With the cross-check fix above, parallel
execution is deterministic regardless of whether BAL pre-population was
used.

## Root Cause

When TX N creates an account and TX M (M > N) reads the account as
non-existent, the stale read must be invalidated. The BalancePath
cross-check catches this: if BalancePath has an entry at a lower txIndex
(from BAL pre-population **or worker flush**), the AddressPath read is
stale.

The bug was that the cross-check was gated behind `vm.HasBAL`, so it
only worked with BAL pre-population — not with worker flushes (the
`HasBAL=false` case).

## Test plan

- [x] New unit test:
`TestValidateRead_NoHasBAL_AddressPathCrossCheckWithBalancePath`
- [x] All existing `TestValidateRead_*` tests pass
- [x] `make lint` clean
- [x] Verified on bal-devnet-2: 40K+ p2p blocks with zero BAL mismatches
(previously failed at block 8997)

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

2 participants