Skip to content

Commit 91fcb07

Browse files
mh0ltclaudeMark Holtshohamc1
authored
bal-devnet-2 working update: fix BAL system address filter and gas_table tracking (#19894)
## Summary Cherry-pick of BAL fixes from bal-devnet-2 that resolved all BAL hash mismatches on the devnet. Erigon synced from block 110k to tip (221k+) with zero BAL errors after these changes. **Two fixes:** - **gas_table.go**: Add unconditional `MarkAddressAccess(address, false)` after all `Empty()`/`Exist()` calls in `statefulGasCall` and `gasSelfdestruct` (4 sites). Previously only recorded in the narrow `transfersValue && empty` case, but geth tracks ALL state reads via `OnAccountRead` hooks. - **versionedio.go**: Change system address filter from requiring non-revertable user access (`!opts.revertable`) to including on ANY user tx access. Rename `nonRevertableUserAccess` → `userAccess`. Geth includes the system address whenever any user tx reads it, even via internal `versionRead` (revertable=true). ## Test plan - [x] All `TestAsBlockAccessList_*` tests pass - [x] Verified on bal-devnet-2: erigon synced 110k blocks (110000→221000+) with zero BAL hash mismatches - [ ] CI passes on main - [ ] `make lint` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Mark Holt <erigon@dev-bm-e3-ethmainnet-n4.erigon.io> Co-authored-by: Shoham Chakraborty <shhmchk@gmail.com>
1 parent 624598a commit 91fcb07

4 files changed

Lines changed: 285 additions & 56 deletions

File tree

execution/stagedsync/bal_create_test.go

Lines changed: 229 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package stagedsync
22

33
import (
4+
"fmt"
45
"math/big"
56
"testing"
67

@@ -96,6 +97,10 @@ func TestCreateBALOrdering(t *testing.T) {
9697
}
9798

9899
func addStorageRead(readSets map[int]state.ReadSet, txIdx int, addr accounts.Address, slot accounts.StorageKey) {
100+
addStorageReadVal(readSets, txIdx, addr, slot, *uint256.NewInt(0))
101+
}
102+
103+
func addStorageReadVal(readSets map[int]state.ReadSet, txIdx int, addr accounts.Address, slot accounts.StorageKey, val uint256.Int) {
99104
rs := readSets[txIdx]
100105
if rs == nil {
101106
rs = state.ReadSet{}
@@ -105,6 +110,7 @@ func addStorageRead(readSets map[int]state.ReadSet, txIdx int, addr accounts.Add
105110
Address: addr,
106111
Path: state.StoragePath,
107112
Key: slot,
113+
Val: val,
108114
})
109115
}
110116

@@ -117,7 +123,7 @@ func addBalanceRead(readSets map[int]state.ReadSet, txIdx int, addr accounts.Add
117123
rs.Set(state.VersionedRead{
118124
Address: addr,
119125
Path: state.BalancePath,
120-
Val: value,
126+
Val: *uint256.NewInt(value),
121127
})
122128
}
123129

@@ -148,3 +154,225 @@ func recordAll(io *state.VersionedIO, reads map[int]state.ReadSet, writes map[in
148154
io.RecordWrites(state.Version{TxIndex: txIdx}, ws)
149155
}
150156
}
157+
158+
// TestBALBlock943Direct constructs the BAL for bal-devnet-2 block 943
159+
// (first Amsterdam block, 0 user txs) directly and verifies the hash
160+
// matches the known-good value from the devnet header.
161+
//
162+
// Block 943 system calls:
163+
// - txIndex=-1: EIP-4788 beacon root (2 storage writes), EIP-2935 block hash (1 storage write)
164+
// - txIndex=0 (finalize): EIP-7002 withdrawal request dequeue (4 SLOADs + 4 SSTOREs, all net-zero),
165+
// EIP-7251 consolidation request dequeue (4 SLOADs + 4 SSTOREs, all net-zero)
166+
//
167+
// The system address (0xff..fe) is filtered out per EIP-7928.
168+
func TestBALBlock943Direct(t *testing.T) {
169+
expectedHash := common.HexToHash("0x0e9aff2d3f1c6d5083afd44ef786e54858d50004845d5ae2f0437dd61b83d00d")
170+
171+
// System contract addresses (sorted lexicographically)
172+
eip7002Addr := accounts.InternAddress(common.HexToAddress("0x00000961Ef480Eb55e80D19ad83579A64c007002")) // EIP-7002 Withdrawal Requests
173+
eip7251Addr := accounts.InternAddress(common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251")) // EIP-7251 Consolidation Requests
174+
eip2935Addr := accounts.InternAddress(common.HexToAddress("0x0000F90827F1C53a10cb7A02335B175320002935")) // EIP-2935 Block Hash Store
175+
eip4788Addr := accounts.InternAddress(common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02")) // EIP-4788 Beacon Roots
176+
177+
// Storage slots and values for block 943 (from RPC)
178+
slot4788Timestamp := accounts.InternKey(common.BigToHash(big.NewInt(0x1747))) // timestamp % 8191
179+
slot4788Root := accounts.InternKey(common.BigToHash(big.NewInt(0x3746))) // (timestamp % 8191) + 8191
180+
slot2935 := accounts.InternKey(common.BigToHash(big.NewInt(0x3ae))) // (blockNum-1) % 8191
181+
182+
val4788Timestamp := uint256.NewInt(0x69862afc)
183+
val4788Root := new(uint256.Int)
184+
val4788Root.SetBytes(common.Hex2Bytes("6d29857d40bc9c49248f83435de3c0f4df64b701e77c8550a40b4981802ccc9c"))
185+
val2935 := new(uint256.Int)
186+
val2935.SetBytes(common.Hex2Bytes("52b76c49b7b2892ef00b543e27480f8e57ca399e8ddf4a2aba127b263988885c"))
187+
188+
// EIP-7002 and EIP-7251 dequeue contracts read slots 0-3 (excess, count, head, tail)
189+
// when queue is empty. All SSTOREs write 0 back to zero-valued slots (net-zero → reads only).
190+
slot0 := accounts.InternKey(common.BigToHash(big.NewInt(0)))
191+
slot1 := accounts.InternKey(common.BigToHash(big.NewInt(1)))
192+
slot2 := accounts.InternKey(common.BigToHash(big.NewInt(2)))
193+
slot3 := accounts.InternKey(common.BigToHash(big.NewInt(3)))
194+
195+
// Construct BAL directly. Addresses must be sorted lexicographically:
196+
// 0x00000961... < 0x0000BBdD... < 0x0000F908... < 0x000F3df6...
197+
bal := types.BlockAccessList{
198+
// EIP-7002: only storage reads (empty queue, all writes are net-zero)
199+
{
200+
Address: eip7002Addr,
201+
StorageReads: []accounts.StorageKey{slot0, slot1, slot2, slot3},
202+
},
203+
// EIP-7251: only storage reads (empty queue, all writes are net-zero)
204+
{
205+
Address: eip7251Addr,
206+
StorageReads: []accounts.StorageKey{slot0, slot1, slot2, slot3},
207+
},
208+
// EIP-2935: 1 storage change at accessIndex 0 (system call txIndex=-1)
209+
{
210+
Address: eip2935Addr,
211+
StorageChanges: []*types.SlotChanges{
212+
{
213+
Slot: slot2935,
214+
Changes: []*types.StorageChange{{Index: 0, Value: *val2935}},
215+
},
216+
},
217+
},
218+
// EIP-4788: 2 storage changes at accessIndex 0 (system call txIndex=-1)
219+
{
220+
Address: eip4788Addr,
221+
StorageChanges: []*types.SlotChanges{
222+
{
223+
Slot: slot4788Timestamp,
224+
Changes: []*types.StorageChange{{Index: 0, Value: *val4788Timestamp}},
225+
},
226+
{
227+
Slot: slot4788Root,
228+
Changes: []*types.StorageChange{{Index: 0, Value: *val4788Root}},
229+
},
230+
},
231+
},
232+
}
233+
234+
if err := bal.Validate(); err != nil {
235+
t.Fatalf("BAL validation failed: %v", err)
236+
}
237+
238+
got := bal.Hash()
239+
t.Logf("BAL hash: %s", got.Hex())
240+
t.Logf("BAL debug: %s", bal.DebugString())
241+
if got != expectedHash {
242+
t.Fatalf("BAL hash mismatch:\n got: %s\n expected: %s", got.Hex(), expectedHash.Hex())
243+
}
244+
}
245+
246+
// TestBALBlock943ViaVersionedIO constructs the BAL through VersionedIO
247+
// (the same path used during block execution) and verifies the hash.
248+
func TestBALBlock943ViaVersionedIO(t *testing.T) {
249+
expectedHash := common.HexToHash("0x0e9aff2d3f1c6d5083afd44ef786e54858d50004845d5ae2f0437dd61b83d00d")
250+
251+
// System address, system contracts
252+
systemAddr := accounts.InternAddress(common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe"))
253+
eip7002Addr := accounts.InternAddress(common.HexToAddress("0x00000961Ef480Eb55e80D19ad83579A64c007002"))
254+
eip7251Addr := accounts.InternAddress(common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251"))
255+
eip2935Addr := accounts.InternAddress(common.HexToAddress("0x0000F90827F1C53a10cb7A02335B175320002935"))
256+
eip4788Addr := accounts.InternAddress(common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02"))
257+
258+
// Storage slots and values
259+
slot4788Timestamp := accounts.InternKey(common.BigToHash(big.NewInt(0x1747)))
260+
slot4788Root := accounts.InternKey(common.BigToHash(big.NewInt(0x3746)))
261+
slot2935 := accounts.InternKey(common.BigToHash(big.NewInt(0x3ae)))
262+
slot0 := accounts.InternKey(common.BigToHash(big.NewInt(0)))
263+
slot1 := accounts.InternKey(common.BigToHash(big.NewInt(1)))
264+
slot2 := accounts.InternKey(common.BigToHash(big.NewInt(2)))
265+
slot3 := accounts.InternKey(common.BigToHash(big.NewInt(3)))
266+
267+
val4788Timestamp := uint256.NewInt(0x69862afc)
268+
val4788Root := new(uint256.Int)
269+
val4788Root.SetBytes(common.Hex2Bytes("6d29857d40bc9c49248f83435de3c0f4df64b701e77c8550a40b4981802ccc9c"))
270+
val2935 := new(uint256.Int)
271+
val2935.SetBytes(common.Hex2Bytes("52b76c49b7b2892ef00b543e27480f8e57ca399e8ddf4a2aba127b263988885c"))
272+
273+
// Block 943 has 0 user txs. Tasks: txIndex=-1 (Initialize), txIndex=0 (Finalize).
274+
// NewVersionedIO(numTx) where numTx is the count of user transactions (0).
275+
// However, the Finalize task at txIndex=0 needs to be accommodated.
276+
vio := state.NewVersionedIO(0)
277+
278+
readSets := map[int]state.ReadSet{}
279+
writeSets := map[int]state.VersionedWrites{}
280+
281+
// === txIndex=-1: Initialize system calls (EIP-4788, EIP-2935) ===
282+
283+
// System address balance reads/writes (from SubBalance with Gnosis exception)
284+
// Balance is 0x24ac0a — read and write same value (no-op write filtered)
285+
systemBalance := uint256.NewInt(0x24ac0a)
286+
addBalanceRead(readSets, -1, systemAddr, systemBalance.Uint64())
287+
addBalanceWrite(writeSets, -1, systemAddr, systemBalance.Uint64())
288+
289+
// EIP-4788 contract: balance read+write (no-op), 2 storage writes
290+
addBalanceRead(readSets, -1, eip4788Addr, 0)
291+
addBalanceWrite(writeSets, -1, eip4788Addr, 0)
292+
addStorageWrite(writeSets, -1, eip4788Addr, slot4788Timestamp, val4788Timestamp.Uint64())
293+
writeSets[-1] = append(writeSets[-1], &state.VersionedWrite{
294+
Address: eip4788Addr,
295+
Path: state.StoragePath,
296+
Key: slot4788Root,
297+
Version: state.Version{TxIndex: -1},
298+
Val: *val4788Root,
299+
})
300+
301+
// EIP-2935 contract: balance read+write (no-op), 1 storage write
302+
addBalanceRead(readSets, -1, eip2935Addr, 0)
303+
addBalanceWrite(writeSets, -1, eip2935Addr, 0)
304+
writeSets[-1] = append(writeSets[-1], &state.VersionedWrite{
305+
Address: eip2935Addr,
306+
Path: state.StoragePath,
307+
Key: slot2935,
308+
Version: state.Version{TxIndex: -1},
309+
Val: *val2935,
310+
})
311+
312+
// === txIndex=0: Finalize system calls (EIP-7002, EIP-7251 dequeue) ===
313+
// Empty queue: read slots 0-3, write 0 back to each (net-zero → reads only)
314+
315+
// EIP-7002: balance read+write (no-op), storage reads + net-zero writes
316+
addBalanceRead(readSets, 0, eip7002Addr, 0)
317+
addBalanceWrite(writeSets, 0, eip7002Addr, 0)
318+
addStorageRead(readSets, 0, eip7002Addr, slot0)
319+
addStorageRead(readSets, 0, eip7002Addr, slot1)
320+
addStorageRead(readSets, 0, eip7002Addr, slot2)
321+
addStorageRead(readSets, 0, eip7002Addr, slot3)
322+
addStorageWrite(writeSets, 0, eip7002Addr, slot0, 0) // net-zero: write 0 to 0-valued slot
323+
addStorageWrite(writeSets, 0, eip7002Addr, slot1, 0)
324+
addStorageWrite(writeSets, 0, eip7002Addr, slot2, 0)
325+
addStorageWrite(writeSets, 0, eip7002Addr, slot3, 0)
326+
327+
// EIP-7251: balance read+write (no-op), storage reads + net-zero writes
328+
addBalanceRead(readSets, 0, eip7251Addr, 0)
329+
addBalanceWrite(writeSets, 0, eip7251Addr, 0)
330+
addStorageRead(readSets, 0, eip7251Addr, slot0)
331+
addStorageRead(readSets, 0, eip7251Addr, slot1)
332+
addStorageRead(readSets, 0, eip7251Addr, slot2)
333+
addStorageRead(readSets, 0, eip7251Addr, slot3)
334+
addStorageWrite(writeSets, 0, eip7251Addr, slot0, 0)
335+
addStorageWrite(writeSets, 0, eip7251Addr, slot1, 0)
336+
addStorageWrite(writeSets, 0, eip7251Addr, slot2, 0)
337+
addStorageWrite(writeSets, 0, eip7251Addr, slot3, 0)
338+
339+
// System address balance from Finalize Transfer calls (no-op)
340+
addBalanceRead(readSets, 0, systemAddr, systemBalance.Uint64())
341+
addBalanceWrite(writeSets, 0, systemAddr, systemBalance.Uint64())
342+
343+
recordAll(vio, readSets, writeSets)
344+
345+
bal := vio.AsBlockAccessList()
346+
347+
t.Logf("BAL accounts: %d", len(bal))
348+
for i, ac := range bal {
349+
t.Logf(" [%d] %s: storage_changes=%d storage_reads=%d balance_changes=%d nonce_changes=%d code_changes=%d",
350+
i, ac.Address.Value().Hex(),
351+
len(ac.StorageChanges), len(ac.StorageReads),
352+
len(ac.BalanceChanges), len(ac.NonceChanges), len(ac.CodeChanges))
353+
for _, sc := range ac.StorageChanges {
354+
for _, ch := range sc.Changes {
355+
t.Logf(" slot %s [%d] -> %s", sc.Slot.Value().Hex(), ch.Index, ch.Value.Hex())
356+
}
357+
}
358+
for _, sr := range ac.StorageReads {
359+
t.Logf(" read slot %s", sr.Value().Hex())
360+
}
361+
}
362+
363+
got := bal.Hash()
364+
t.Logf("BAL hash: %s", got.Hex())
365+
if got != expectedHash {
366+
t.Logf("BAL debug: %s", bal.DebugString())
367+
t.Fatalf("BAL hash mismatch:\n got: %s\n expected: %s", got.Hex(), expectedHash.Hex())
368+
}
369+
370+
// Verify system address was filtered out
371+
for _, ac := range bal {
372+
if ac.Address == systemAddr {
373+
t.Fatal("system address should have been filtered from BAL")
374+
}
375+
}
376+
377+
_ = fmt.Sprintf // suppress unused import
378+
}

execution/state/versionedio.go

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,14 +1085,8 @@ func (io *VersionedIO) AsBlockAccessList() types.BlockAccessList {
10851085
}
10861086

10871087
account := ensureAccountState(ac, addr)
1088-
// A non-revertable access means the address was the target of
1089-
// an actual EVM operation (evm.Call, evm.Create, SELFDESTRUCT
1090-
// with non-zero balance, BALANCE, EXTCODESIZE, etc.) — not just
1091-
// a gas-calculation read. This is used to distinguish real state
1092-
// access from incidental reads (e.g. Empty() in gas calc) for
1093-
// the system address filter.
1094-
if isUserTx && opts != nil && !opts.revertable {
1095-
account.nonRevertableUserAccess = true
1088+
if isUserTx && opts != nil {
1089+
account.userAccess = true
10961090
}
10971091
}
10981092
}
@@ -1102,14 +1096,9 @@ func (io *VersionedIO) AsBlockAccessList() types.BlockAccessList {
11021096
account.finalize()
11031097
account.changes.Normalize()
11041098
// The system address (0xff...fe) is touched during every block's system
1105-
// call (EIP-4788 beacon root) because it is msg.sender. Per EIP-7928,
1106-
// "SYSTEM_ADDRESS MUST NOT be included unless it experiences state access
1107-
// itself." We use the non-revertable access flag from MarkAddressAccess
1108-
// to distinguish real state access (evm.Call target, SELFDESTRUCT
1109-
// beneficiary, BALANCE opcode, etc.) from incidental gas-calculation
1110-
// reads (Empty() in statefulGasCall). Keep it when it has actual state
1111-
// changes or when a user tx performed a non-revertable access to it.
1112-
if account.changes.Address == params.SystemAddress && !hasAccountChanges(account.changes) && !account.nonRevertableUserAccess {
1099+
// call (EIP-4788 beacon root) because it is msg.sender. Filter it out
1100+
// unless it has actual state changes or a user tx accessed it.
1101+
if account.changes.Address == params.SystemAddress && !hasAccountChanges(account.changes) && !account.userAccess {
11131102
continue
11141103
}
11151104
bal = append(bal, account.changes)
@@ -1122,22 +1111,30 @@ func (io *VersionedIO) AsBlockAccessList() types.BlockAccessList {
11221111
return bal
11231112
}
11241113

1114+
// hasAccountChanges returns true if the account has any state changes
1115+
// (storage, balance, nonce, or code) that belong in the BAL.
1116+
func hasAccountChanges(ac *types.AccountChanges) bool {
1117+
return len(ac.StorageChanges) > 0 || len(ac.StorageReads) > 0 ||
1118+
len(ac.BalanceChanges) > 0 || len(ac.NonceChanges) > 0 ||
1119+
len(ac.CodeChanges) > 0
1120+
}
1121+
11251122
type accountState struct {
1126-
changes *types.AccountChanges
1127-
balance *fieldTracker[uint256.Int]
1128-
nonce *fieldTracker[uint64]
1129-
code *fieldTracker[[]byte]
1130-
balanceValue *uint256.Int // tracks latest seen balance
1131-
initialBalanceValue *uint256.Int // tracks pre-block balance for net-zero detection
1132-
selfDestructed bool //
1133-
selfDestructedAt uint16 // access index of the selfdestruct
1134-
storageReadValues map[accounts.StorageKey]uint256.Int // original read values for net-zero detection
1135-
nonRevertableUserAccess bool // true if a user tx (txIndex >= 0) has non-revertable access
1123+
changes *types.AccountChanges
1124+
balance *fieldTracker[uint256.Int]
1125+
nonce *fieldTracker[uint64]
1126+
code *fieldTracker[[]byte]
1127+
balanceValue *uint256.Int // tracks latest seen balance
1128+
initialBalanceValue *uint256.Int // tracks pre-block balance for net-zero detection
1129+
selfDestructed bool
1130+
selfDestructedAt uint16 // access index of the selfdestruct
1131+
storageReadValues map[accounts.StorageKey]uint256.Int // original read values for net-zero detection
1132+
userAccess bool // true if a user tx (txIndex >= 0) accessed this account
11361133
}
11371134

11381135
// check pre- and post-values, add to BAL if different
11391136
func (a *accountState) finalize() {
1140-
applyToBalance(a.balance, a.changes)
1137+
applyToBalance(a.balance, a.changes, a.initialBalanceValue)
11411138
applyToNonce(a.nonce, a.changes)
11421139
applyToCode(a.code, a.changes)
11431140
}
@@ -1154,8 +1151,20 @@ func newBalanceTracker() *fieldTracker[uint256.Int] {
11541151
return &fieldTracker[uint256.Int]{}
11551152
}
11561153

1157-
func applyToBalance(bt *fieldTracker[uint256.Int], ac *types.AccountChanges) {
1154+
func applyToBalance(bt *fieldTracker[uint256.Int], ac *types.AccountChanges, initialBalance *uint256.Int) {
1155+
// Get the sorted indices to identify the first (lowest-index) entry.
1156+
// If the first entry equals the pre-block balance, it's a net-zero
1157+
// change from a reverted tx (e.g. CALL with value then revert) and
1158+
// must be excluded. Geth's scope-based BAL builder handles this via
1159+
// ExitScope which converts reverted writes to reads.
1160+
firstFiltered := false
11581161
bt.changes.apply(func(idx uint16, value uint256.Int) {
1162+
if !firstFiltered {
1163+
firstFiltered = true
1164+
if initialBalance != nil && value.Eq(initialBalance) {
1165+
return
1166+
}
1167+
}
11591168
ac.BalanceChanges = append(ac.BalanceChanges, &types.BalanceChange{
11601169
Index: idx,
11611170
Value: value,
@@ -1403,11 +1412,6 @@ func removeStorageRead(ac *types.AccountChanges, slot accounts.StorageKey) {
14031412
}
14041413
}
14051414

1406-
func hasAccountChanges(ac *types.AccountChanges) bool {
1407-
return len(ac.BalanceChanges) > 0 || len(ac.NonceChanges) > 0 ||
1408-
len(ac.CodeChanges) > 0 || len(ac.StorageChanges) > 0
1409-
}
1410-
14111415
type versionedReadSet struct {
14121416
incarnation int
14131417
readSet ReadSet

0 commit comments

Comments
 (0)