Skip to content

Commit 7d71cfe

Browse files
authored
EIP-7928: Implement BlockAccessListBuilder and BAL validation (#3798)
* Implement BAL validation. * Implement block access list builder. * Implement block access list builder tests. * Implement block access list validation tests.
1 parent ca58ca6 commit 7d71cfe

File tree

5 files changed

+606
-1
lines changed

5 files changed

+606
-1
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Nimbus
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms.
9+
10+
{.push raises: [], gcsafe.}
11+
12+
import
13+
std/[tables, sets, algorithm],
14+
eth/common/[block_access_lists, block_access_lists_rlp],
15+
stint,
16+
stew/byteutils
17+
18+
export block_access_lists
19+
20+
type
21+
# Account data stored in the builder during block execution. This type tracks
22+
# all changes made to a single account throughout the execution of a block,
23+
# organized by the type of change and the block access list index where it
24+
# occurred.
25+
AccountData = object
26+
storageChanges: Table[UInt256, Table[int, UInt256]]
27+
## Maps storage key -> block access list index -> storage value
28+
storageReads: HashSet[UInt256]
29+
## Set of storage keys
30+
balanceChanges: Table[int, UInt256]
31+
## Maps block access list index -> balance
32+
nonceChanges: Table[int, AccountNonce]
33+
## Maps block access list index -> nonce
34+
codeChanges: Table[int, seq[byte]]
35+
## Maps block access list index -> code
36+
37+
# Builder for constructing a BlockAccessList efficiently during transaction
38+
# execution. The builder accumulates all account and storage accesses during
39+
# block execution and constructs a deterministic access list. Changes are
40+
# tracked by address, field type, and block access list index to enable
41+
# efficient reconstruction of state changes.
42+
BlockAccessListBuilderRef* = ref object
43+
accounts: Table[Address, AccountData]
44+
## Maps address -> account data
45+
46+
proc init*(T: type AccountData): T =
47+
AccountData()
48+
49+
# Disallow copying of AccountData
50+
proc `=copy`(dest: var AccountData; src: AccountData) {.error: "Copying AccountData is forbidden".} =
51+
discard
52+
53+
proc init*(T: type BlockAccessListBuilderRef): T =
54+
BlockAccessListBuilderRef()
55+
56+
proc ensureAccount(builder: BlockAccessListBuilderRef, address: Address) =
57+
if address notin builder.accounts:
58+
builder.accounts[address] = AccountData.init()
59+
60+
template addTouchedAccount*(builder: BlockAccessListBuilderRef, address: Address) =
61+
ensureAccount(builder, address)
62+
63+
proc addStorageWrite*(
64+
builder: BlockAccessListBuilderRef,
65+
address: Address,
66+
slot: UInt256,
67+
blockAccessIndex: int,
68+
newValue: UInt256) =
69+
70+
builder.ensureAccount(address)
71+
72+
builder.accounts.withValue(address, accData):
73+
if slot notin accData[].storageChanges:
74+
accData[].storageChanges[slot] = default(Table[int, UInt256])
75+
accData[].storageChanges.withValue(slot, slotChanges):
76+
slotChanges[][blockAccessIndex] = newValue
77+
78+
proc addStorageRead*(builder: BlockAccessListBuilderRef, address: Address, slot: UInt256) =
79+
builder.ensureAccount(address)
80+
81+
builder.accounts.withValue(address, accData):
82+
accData[].storageReads.incl(slot)
83+
84+
proc addBalanceChange*(
85+
builder: BlockAccessListBuilderRef,
86+
address: Address,
87+
blockAccessIndex: int,
88+
postBalance: UInt256) =
89+
builder.ensureAccount(address)
90+
91+
builder.accounts.withValue(address, accData):
92+
accData[].balanceChanges[blockAccessIndex] = postBalance
93+
94+
proc addNonceChange*(
95+
builder: BlockAccessListBuilderRef,
96+
address: Address,
97+
blockAccessIndex: int,
98+
newNonce: AccountNonce) =
99+
builder.ensureAccount(address)
100+
101+
builder.accounts.withValue(address, accData):
102+
accData[].nonceChanges[blockAccessIndex] = newNonce
103+
104+
proc addCodeChange*(
105+
builder: BlockAccessListBuilderRef,
106+
address: Address,
107+
blockAccessIndex: int,
108+
newCode: seq[byte]) =
109+
builder.ensureAccount(address)
110+
111+
builder.accounts.withValue(address, accData):
112+
accData[].codeChanges[blockAccessIndex] = newCode
113+
114+
proc balIndexCmp(x, y: StorageChange | BalanceChange | NonceChange | CodeChange): int =
115+
cmp(x.blockAccessIndex, y.blockAccessIndex)
116+
117+
proc slotCmp(x, y: SlotChanges): int =
118+
cmp(x.slot, y.slot)
119+
120+
proc addressCmp(x, y: AccountChanges): int =
121+
cmp(x.address.data.toHex(), y.address.data.toHex())
122+
123+
proc buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessList =
124+
var blockAccessList: BlockAccessList
125+
126+
for address, accData in builder.accounts.mpairs():
127+
# Collect and sort storageChanges
128+
var storageChanges: seq[SlotChanges]
129+
for slot, changes in accData.storageChanges:
130+
var slotChanges: seq[StorageChange]
131+
132+
for balIndex, value in changes:
133+
slotChanges.add((BlockAccessIndex(balIndex), StorageValue(value)))
134+
slotChanges.sort(balIndexCmp)
135+
136+
storageChanges.add((StorageKey(slot), slotChanges))
137+
storageChanges.sort(slotCmp)
138+
139+
# Collect and sort storageReads
140+
var storageReads: seq[StorageKey]
141+
for slot in accData.storageReads:
142+
if slot notin accData.storageChanges:
143+
storageReads.add(slot)
144+
storageReads.sort()
145+
146+
# Collect and sort balanceChanges
147+
var balanceChanges: seq[BalanceChange]
148+
for balIndex, balance in accData.balanceChanges:
149+
balanceChanges.add((BlockAccessIndex(balIndex), Balance(balance)))
150+
balanceChanges.sort(balIndexCmp)
151+
152+
# Collect and sort nonceChanges
153+
var nonceChanges: seq[NonceChange]
154+
for balIndex, nonce in accData.nonceChanges:
155+
nonceChanges.add((BlockAccessIndex(balIndex), Nonce(nonce)))
156+
nonceChanges.sort(balIndexCmp)
157+
158+
# Collect and sort codeChanges
159+
var codeChanges: seq[CodeChange]
160+
for balIndex, code in accData.codeChanges:
161+
codeChanges.add((BlockAccessIndex(balIndex), CodeData(code)))
162+
codeChanges.sort(balIndexCmp)
163+
164+
blockAccessList.add(AccountChanges(
165+
address: address,
166+
storageChanges: storageChanges,
167+
storageReads: storageReads,
168+
balanceChanges: balanceChanges,
169+
nonceChanges: nonceChanges,
170+
codeChanges: codeChanges
171+
))
172+
173+
blockAccessList.sort(addressCmp)
174+
175+
blockAccessList
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Nimbus
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms.
9+
10+
{.push raises: [], gcsafe.}
11+
12+
import
13+
std/[sets, sequtils, algorithm],
14+
eth/common/[block_access_lists, block_access_lists_rlp, hashes],
15+
stint,
16+
stew/byteutils,
17+
results
18+
19+
export block_access_lists, hashes, results
20+
21+
22+
func validate*(bal: BlockAccessList, expectedHash: Hash32): Result[void, string] =
23+
## Validate that a block access list is structurally correct and matches the expected hash.
24+
25+
# Check that storage changes and reads don't overlap for the same slot.
26+
for accountChanges in bal:
27+
var changedSlots: HashSet[StorageKey]
28+
29+
for slotChanges in accountChanges.storageChanges:
30+
changedSlots.incl(slotChanges.slot)
31+
32+
for slot in accountChanges.storageReads:
33+
if changedSlots.contains(slot):
34+
return err("A slot should not be in both changes and reads")
35+
36+
# Validate ordering (addresses should be sorted lexicographically).
37+
let balAddresses = bal.mapIt(it.address.data.toHex())
38+
if balAddresses != balAddresses.sorted():
39+
return err("Addresses should be sorted lexicographically")
40+
41+
# Validate ordering of fields for each account
42+
for accountChanges in bal:
43+
# Validate storage changes slots are sorted lexicographically
44+
let storageChangesSlots = accountChanges.storageChanges.mapIt(it.slot)
45+
if storageChangesSlots != storageChangesSlots.sorted():
46+
return err("Storage changes slots should be sorted lexicographically")
47+
48+
# Check storage changes are sorted by blockAccessIndex
49+
for slotChanges in accountChanges.storageChanges:
50+
let indices = slotChanges.changes.mapIt(it.blockAccessIndex)
51+
if indices != indices.sorted():
52+
return err("Slot changes should be sorted by blockAccessIndex")
53+
54+
# Validate storage reads are sorted lexicographically
55+
let storageReadsSlots = accountChanges.storageReads
56+
if storageReadsSlots != storageReadsSlots.sorted():
57+
return err("Storage reads should be sorted lexicographically")
58+
59+
# Check balance changes are sorted by blockAccessIndex
60+
let balanceIndices = accountChanges.balanceChanges.mapIt(it.blockAccessIndex)
61+
if balanceIndices != balanceIndices.sorted():
62+
return err("Balance changes should be sorted by blockAccessIndex")
63+
64+
# Check nonce changes are sorted by blockAccessIndex
65+
let nonceIndices = accountChanges.nonceChanges.mapIt(it.blockAccessIndex)
66+
if nonceIndices != nonceIndices.sorted():
67+
return err("Nonce changes should be sorted by blockAccessIndex")
68+
69+
# Check code changes are sorted by blockAccessIndex
70+
let codeIndices = accountChanges.codeChanges.mapIt(it.blockAccessIndex)
71+
if codeIndices != codeIndices.sorted():
72+
return err("Code changes should be sorted by blockAccessIndex")
73+
74+
# Check that the block access list matches the expected hash.
75+
if bal.computeBlockAccessListHash() != expectedHash:
76+
return err("Computed block access list hash does not match the expected hash")
77+
78+
ok()

tests/all_tests.nim

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import
4343
test_stateless_witness_types,
4444
test_stateless_witness_generation,
4545
test_stateless_witness_verification,
46-
# These two suites are much slower than all the rest, so run them last
46+
test_block_access_list_builder,
47+
test_block_access_list_validation,
48+
# These suites below are much slower than all the rest, so run them last
4749
test_generalstate_json,
4850
]

0 commit comments

Comments
 (0)