From a1288e5a712d8397d204f2555c05657f5d502e1e Mon Sep 17 00:00:00 2001 From: bhartnett Date: Tue, 21 Oct 2025 15:11:10 +0800 Subject: [PATCH 1/7] Update nim-eth and nim-web3 deps to BALs_EIP-7928 branches. --- vendor/nim-eth | 2 +- vendor/nim-web3 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vendor/nim-eth b/vendor/nim-eth index 7f7aea1035..0f2cd60e57 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit 7f7aea10355660b9770a56a69dce085f3a285af3 +Subproject commit 0f2cd60e5732ce5b141fee71d3b528fe5edb6be6 diff --git a/vendor/nim-web3 b/vendor/nim-web3 index 141907cd95..b777d2e009 160000 --- a/vendor/nim-web3 +++ b/vendor/nim-web3 @@ -1 +1 @@ -Subproject commit 141907cd958d7ee3b554ec94bc9ac7ec692e546b +Subproject commit b777d2e00979161242feeb4820a2862237ce8199 From 266acbcde65c43dc742d4dae16f3295a51a38b13 Mon Sep 17 00:00:00 2001 From: bhartnett Date: Thu, 23 Oct 2025 12:12:00 +0800 Subject: [PATCH 2/7] Update nim-eth. --- vendor/nim-eth | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-eth b/vendor/nim-eth index 0f2cd60e57..0f25815873 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit 0f2cd60e5732ce5b141fee71d3b528fe5edb6be6 +Subproject commit 0f258158732952e06c103e0f274fe1fa760a6057 From 53dce03deb4e0832ca7ced54cd0a0a512d6bd683 Mon Sep 17 00:00:00 2001 From: bhartnett Date: Fri, 24 Oct 2025 00:10:53 +0800 Subject: [PATCH 3/7] Implement BAL validation. --- .../block_access_list_validation.nim | 87 +++++++++++++++++++ vendor/nim-eth | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 execution_chain/block_access_list/block_access_list_validation.nim diff --git a/execution_chain/block_access_list/block_access_list_validation.nim b/execution_chain/block_access_list/block_access_list_validation.nim new file mode 100644 index 0000000000..d14e8a1364 --- /dev/null +++ b/execution_chain/block_access_list/block_access_list_validation.nim @@ -0,0 +1,87 @@ +# Nimbus +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [], gcsafe.} + +import + std/[sets, sequtils, algorithm], + eth/common/[block_access_lists, block_access_lists_rlp, hashes], + stint, + stew/byteutils, + results + +export block_access_lists, hashes, results + +# TODO: Consider setting max values and adding to the validation. +# This is not yet defined in the EIP. +# MAX_TXS = 30_000 +# MAX_SLOTS = 300_000 +# MAX_ACCOUNTS = 300_000 +# MAX_CODE_SIZE = 24_576 +# MAX_CODE_CHANGES = 1 + +func validate*(bal: BlockAccessList, expectedHash: Hash32): Result[void, string] = + ## Validate that a block access list is structurally correct and matches the expected hash. + + # Check that storage changes and reads don't overlap for the same slot. + for accountChanges in bal: + var changedSlots: HashSet[StorageKey] + + for slotChanges in accountChanges.storageChanges: + changedSlots.incl(slotChanges.slot) + + for slot in accountChanges.storageReads: + if changedSlots.contains(slot): + return err("A slot should not be in both changes and reads") + + # Validate ordering (addresses should be sorted lexicographically). + let balAddresses = bal.mapIt(it.address.data.toHex()) + if balAddresses != balAddresses.sorted(): + return err("Addresses should be sorted lexicographically") + + # Validate ordering of fields for each account + for accountChanges in bal: + # Validate storage changes slots are sorted lexicographically + let storageChangesSlots = accountChanges.storageChanges.mapIt( + UInt256.fromBytesBE(it.slot.data)) + if storageChangesSlots != storageChangesSlots.sorted(): + return err("Storage changes slots should be sorted lexicographically") + + # Check storage changes are sorted by blockAccessIndex + for slotChanges in accountChanges.storageChanges: + let indices = slotChanges.changes.mapIt(it.blockAccessIndex) + if indices != indices.sorted(): + return err("Slot changes should be sorted by blockAccessIndex") + + # Validate storage reads are sorted within each account + let storageReadsSlots = accountChanges.storageReads.mapIt( + UInt256.fromBytesBE(it.data)) + if storageReadsSlots != storageReadsSlots.sorted(): + return err("Storage reads should be sorted by blockAccessIndex") + + # Check balance changes are sorted by blockAccessIndex + let balanceIndices = accountChanges.balanceChanges.mapIt(it.blockAccessIndex) + if balanceIndices != balanceIndices.sorted(): + return err("Balance changes should be sorted by blockAccessIndex") + + # Check nonce changes are sorted by blockAccessIndex + let nonceIndices = accountChanges.nonceChanges.mapIt(it.blockAccessIndex) + if nonceIndices != nonceIndices.sorted(): + return err("Nonce changes should be sorted by blockAccessIndex") + + # Check code changes are sorted by blockAccessIndex + let codeIndices = accountChanges.codeChanges.mapIt(it.blockAccessIndex) + if codeIndices != codeIndices.sorted(): + return err("Code changes should be sorted by blockAccessIndex") + + # Check that the block access list matches the expected hash. + if bal.computeBlockAccessListHash() != expectedHash: + return err("Computed block access list hash does not match the expected hash") + + ok() diff --git a/vendor/nim-eth b/vendor/nim-eth index 0f25815873..e4ddfd9eb2 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit 0f258158732952e06c103e0f274fe1fa760a6057 +Subproject commit e4ddfd9eb2722a29fa34667c957d437f549abcb9 From 27f5efd8189d635924505f8b07e5c26ca53f737e Mon Sep 17 00:00:00 2001 From: bhartnett Date: Sat, 25 Oct 2025 00:39:39 +0800 Subject: [PATCH 4/7] Implement block access list builder. --- .../block_access_list_builder.nim | 174 ++++++++++++++++++ .../block_access_list_validation.nim | 10 +- vendor/nim-eth | 2 +- 3 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 execution_chain/block_access_list/block_access_list_builder.nim diff --git a/execution_chain/block_access_list/block_access_list_builder.nim b/execution_chain/block_access_list/block_access_list_builder.nim new file mode 100644 index 0000000000..c2dc3c5ebf --- /dev/null +++ b/execution_chain/block_access_list/block_access_list_builder.nim @@ -0,0 +1,174 @@ +# Nimbus +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [], gcsafe.} + +import + std/[tables, sets, algorithm], + eth/common/[block_access_lists, block_access_lists_rlp], + stint, + stew/byteutils + +type + # Account data stored in the builder during block execution. + # This type tracks all changes made to a single account throughout + # the execution of a block, organized by the type of change and the + # block access list index where it occurred. + AccountData = object + storageChanges: Table[UInt256, Table[int, UInt256]] + ## Maps storage key -> block access list index -> storage value + storageReads: HashSet[UInt256] + ## Set of storage keys + balanceChanges: Table[int, UInt256] + ## Maps block access list index -> balance + nonceChanges: Table[int, AccountNonce] + ## Maps block access list index -> nonce + codeChanges: Table[int, seq[byte]] + ## Maps block access list index -> code + + # Builder for constructing a BlockAccessList efficiently during transaction + # execution. + # The builder accumulates all account and storage accesses during block + # execution and constructs a deterministic access list. Changes are tracked + # by address, field type, and block access list index to enable efficient + # reconstruction of state changes. + BlockAccessListBuilderRef* = ref object + accounts: Table[Address, AccountData] + ## Maps address -> account data + +proc init*(T: type AccountData): T = + AccountData() + +# Disallow copying of AccountData +proc `=copy`(dest: var AccountData; src: AccountData) {.error: "Copying AccountData is forbidden".} = + discard + +proc init*(T: type BlockAccessListBuilderRef): T = + BlockAccessListBuilderRef() + +proc ensureAccount(builder: BlockAccessListBuilderRef, address: Address) = + if address notin builder.accounts: + builder.accounts[address] = AccountData.init() + +proc addStorageWrite*( + builder: BlockAccessListBuilderRef, + address: Address, + slot: UInt256, + blockAccessIndex: int, + newValue: UInt256) = + + builder.ensureAccount(address) + + builder.accounts.withValue(address, accData): + if slot notin accData[].storageChanges: + accData[].storageChanges[slot] = default(Table[int, UInt256]) + accData[].storageChanges.withValue(slot, slotChanges): + slotChanges[][blockAccessIndex] = newValue + +proc addStorageRead*(builder: BlockAccessListBuilderRef, address: Address, slot: UInt256) = + builder.ensureAccount(address) + + builder.accounts.withValue(address, accData): + accData[].storageReads.incl(slot) + +proc addBalanceChange*( + builder: BlockAccessListBuilderRef, + address: Address, + blockAccessIndex: int, + postBalance: UInt256) = + builder.ensureAccount(address) + + builder.accounts.withValue(address, accData): + accData[].balanceChanges[blockAccessIndex] = postBalance + +proc addNonceChange*( + builder: BlockAccessListBuilderRef, + address: Address, + blockAccessIndex: int, + newNonce: AccountNonce) = + builder.ensureAccount(address) + + builder.accounts.withValue(address, accData): + accData[].nonceChanges[blockAccessIndex] = newNonce + +proc addCodeChange*( + builder: BlockAccessListBuilderRef, + address: Address, + blockAccessIndex: int, + newCode: seq[byte]) = + builder.ensureAccount(address) + + builder.accounts.withValue(address, accData): + accData[].codeChanges[blockAccessIndex] = newCode + +proc addTouchedAccount*(builder: BlockAccessListBuilderRef, address: Address) = + ensureAccount(builder, address) + +proc balIndexCmp(x, y: StorageChange | BalanceChange | NonceChange | CodeChange): int = + cmp(x.blockAccessIndex, y.blockAccessIndex) + +proc slotCmp(x, y: SlotChanges): int = + cmp(x.slot, y.slot) + +proc addressCmp(x, y: AccountChanges): int = + cmp(x.address.data.toHex(), y.address.data.toHex()) + +proc buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessList = + var blockAccessList: BlockAccessList + + for address, accData in builder.accounts: + # Collect and sort storageChanges + var storageChanges: seq[SlotChanges] + for slot, changes in accData.storageChanges: + var slotChanges: seq[StorageChange] + + for balIndex, value in changes: + slotChanges.add((BlockAccessIndex(balIndex), StorageValue(value))) + slotChanges.sort(balIndexCmp) + + storageChanges.add((StorageKey(slot), slotChanges)) + storageChanges.sort(slotCmp) + + # Collect and sort storageReads + var storageReads: seq[StorageKey] + for slot in accData.storageReads: + if slot notin accData.storageChanges: + storageReads.add(slot) + storageReads.sort() + + # Collect and sort balanceChanges + var balanceChanges: seq[BalanceChange] + for balIndex, balance in accData.balanceChanges: + balanceChanges.add((BlockAccessIndex(balIndex), Balance(balance))) + balanceChanges.sort(balIndexCmp) + + # Collect and sort nonceChanges + var nonceChanges: seq[NonceChange] + for balIndex, nonce in accData.nonceChanges: + nonceChanges.add((BlockAccessIndex(balIndex), Nonce(nonce))) + nonceChanges.sort(balIndexCmp) + + # Collect and sort codeChanges + var codeChanges: seq[CodeChange] + for balIndex, code in accData.codeChanges: + codeChanges.add((BlockAccessIndex(balIndex), CodeData(code))) + codeChanges.sort(balIndexCmp) + + blockAccessList.add(AccountChanges( + address: address, + storageChanges: storageChanges, + storageReads: storageReads, + balanceChanges: balanceChanges, + nonceChanges: nonceChanges, + codeChanges: codeChanges + )) + + blockAccessList.sort(addressCmp) + + blockAccessList diff --git a/execution_chain/block_access_list/block_access_list_validation.nim b/execution_chain/block_access_list/block_access_list_validation.nim index d14e8a1364..9491341363 100644 --- a/execution_chain/block_access_list/block_access_list_validation.nim +++ b/execution_chain/block_access_list/block_access_list_validation.nim @@ -48,8 +48,7 @@ func validate*(bal: BlockAccessList, expectedHash: Hash32): Result[void, string] # Validate ordering of fields for each account for accountChanges in bal: # Validate storage changes slots are sorted lexicographically - let storageChangesSlots = accountChanges.storageChanges.mapIt( - UInt256.fromBytesBE(it.slot.data)) + let storageChangesSlots = accountChanges.storageChanges.mapIt(it.slot) if storageChangesSlots != storageChangesSlots.sorted(): return err("Storage changes slots should be sorted lexicographically") @@ -59,11 +58,10 @@ func validate*(bal: BlockAccessList, expectedHash: Hash32): Result[void, string] if indices != indices.sorted(): return err("Slot changes should be sorted by blockAccessIndex") - # Validate storage reads are sorted within each account - let storageReadsSlots = accountChanges.storageReads.mapIt( - UInt256.fromBytesBE(it.data)) + # Validate storage reads are sorted lexicographically + let storageReadsSlots = accountChanges.storageReads if storageReadsSlots != storageReadsSlots.sorted(): - return err("Storage reads should be sorted by blockAccessIndex") + return err("Storage reads should be sorted lexicographically") # Check balance changes are sorted by blockAccessIndex let balanceIndices = accountChanges.balanceChanges.mapIt(it.blockAccessIndex) diff --git a/vendor/nim-eth b/vendor/nim-eth index e4ddfd9eb2..2b798775e6 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit e4ddfd9eb2722a29fa34667c957d437f549abcb9 +Subproject commit 2b798775e65a061119b8b55acf2a3c87b4715a33 From 3e407a7157fc346151cd53908e0bd16e1599c703 Mon Sep 17 00:00:00 2001 From: bhartnett Date: Tue, 28 Oct 2025 00:30:00 +0800 Subject: [PATCH 5/7] Undo nim-web3 bump. --- vendor/nim-web3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/nim-web3 b/vendor/nim-web3 index b777d2e009..141907cd95 160000 --- a/vendor/nim-web3 +++ b/vendor/nim-web3 @@ -1 +1 @@ -Subproject commit b777d2e00979161242feeb4820a2862237ce8199 +Subproject commit 141907cd958d7ee3b554ec94bc9ac7ec692e546b From 2faf89cb80768670aae7ef1e941aa71d73364c20 Mon Sep 17 00:00:00 2001 From: bhartnett Date: Tue, 28 Oct 2025 12:14:04 +0800 Subject: [PATCH 6/7] Implement block access list builder tests. --- .../block_access_list_builder.nim | 10 +- tests/all_tests.nim | 1 + tests/test_block_access_list_builder.nim | 197 ++++++++++++++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 tests/test_block_access_list_builder.nim diff --git a/execution_chain/block_access_list/block_access_list_builder.nim b/execution_chain/block_access_list/block_access_list_builder.nim index c2dc3c5ebf..92ea4ff71e 100644 --- a/execution_chain/block_access_list/block_access_list_builder.nim +++ b/execution_chain/block_access_list/block_access_list_builder.nim @@ -15,6 +15,8 @@ import stint, stew/byteutils +export block_access_lists + type # Account data stored in the builder during block execution. # This type tracks all changes made to a single account throughout @@ -56,6 +58,9 @@ proc ensureAccount(builder: BlockAccessListBuilderRef, address: Address) = if address notin builder.accounts: builder.accounts[address] = AccountData.init() +template addTouchedAccount*(builder: BlockAccessListBuilderRef, address: Address) = + ensureAccount(builder, address) + proc addStorageWrite*( builder: BlockAccessListBuilderRef, address: Address, @@ -107,9 +112,6 @@ proc addCodeChange*( builder.accounts.withValue(address, accData): accData[].codeChanges[blockAccessIndex] = newCode -proc addTouchedAccount*(builder: BlockAccessListBuilderRef, address: Address) = - ensureAccount(builder, address) - proc balIndexCmp(x, y: StorageChange | BalanceChange | NonceChange | CodeChange): int = cmp(x.blockAccessIndex, y.blockAccessIndex) @@ -122,7 +124,7 @@ proc addressCmp(x, y: AccountChanges): int = proc buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessList = var blockAccessList: BlockAccessList - for address, accData in builder.accounts: + for address, accData in builder.accounts.mpairs(): # Collect and sort storageChanges var storageChanges: seq[SlotChanges] for slot, changes in accData.storageChanges: diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 65c8b06dbb..b6225e3f27 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -43,6 +43,7 @@ import test_stateless_witness_types, test_stateless_witness_generation, test_stateless_witness_verification, + test_block_access_list_builder, # These two suites are much slower than all the rest, so run them last test_generalstate_json, ] diff --git a/tests/test_block_access_list_builder.nim b/tests/test_block_access_list_builder.nim new file mode 100644 index 0000000000..2e765a9ff9 --- /dev/null +++ b/tests/test_block_access_list_builder.nim @@ -0,0 +1,197 @@ +# Nimbus +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +# http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or +# http://opensource.org/licenses/MIT) +# at your option. This file may not be copied, modified, or +# distributed except according to those terms. + +{.used.} + +import + unittest2, + ../execution_chain/block_access_list/block_access_list_builder + + +suite "Block access list builder": + const + address1 = address"0x10007bc31cedb7bfb8a345f31e668033056b2728" + address2 = address"0x20007bc31cedb7bfb8a345f31e668033056b2728" + address3 = address"0x30007bc31cedb7bfb8a345f31e668033056b2728" + slot1 = 1.u256() + slot2 = 2.u256() + slot3 = 3.u256() + + setup: + let builder = BlockAccessListBuilderRef.init() + + test "Add touched account": + builder.addTouchedAccount(address3) + builder.addTouchedAccount(address2) + builder.addTouchedAccount(address1) + builder.addTouchedAccount(address1) # duplicate + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 3 + bal[0].address == address1 + bal[1].address == address2 + bal[2].address == address3 + + for accChange in bal: + check: + accChange.storageChanges.len() == 0 + accChange.storageReads.len() == 0 + accChange.balanceChanges.len() == 0 + accChange.nonceChanges.len() == 0 + accChange.codeChanges.len() == 0 + + test "Add storage write": + builder.addStorageWrite(address1, slot3, 0, 3.u256) + builder.addStorageWrite(address1, slot2, 2, 2.u256) + builder.addStorageWrite(address1, slot1, 1, 1.u256) + builder.addStorageWrite(address2, slot1, 1, 1.u256) + builder.addStorageWrite(address1, slot3, 3, 4.u256) + builder.addStorageWrite(address1, slot3, 3, 5.u256) # duplicate should overwrite + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 2 + bal[0].address == address1 + bal[0].storageChanges.len() == 3 + bal[0].storageChanges[0] == (slot1, @[(1.BlockAccessIndex, 1.u256)]) + bal[0].storageChanges[1] == (slot2, @[(2.BlockAccessIndex, 2.u256)]) + bal[0].storageChanges[2] == (slot3, @[(0.BlockAccessIndex, 3.u256), (3.BlockAccessIndex, 5.u256)]) + bal[1].address == address2 + bal[1].storageChanges.len() == 1 + bal[1].storageChanges[0] == (slot1, @[(1.BlockAccessIndex, 1.u256)]) + + test "Add storage read": + builder.addStorageRead(address2, slot3) + builder.addStorageRead(address2, slot2) + builder.addStorageRead(address3, slot3) + builder.addStorageRead(address1, slot1) + builder.addStorageRead(address1, slot1) # duplicate + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 3 + bal[0].address == address1 + bal[0].storageReads == @[slot1] + bal[1].address == address2 + bal[1].storageReads == @[slot2, slot3] + bal[2].address == address3 + bal[2].storageReads == @[slot3] + + test "Add balance change": + builder.addBalanceChange(address2, 1, 0.u256) + builder.addBalanceChange(address2, 0, 1.u256) + builder.addBalanceChange(address3, 3, 3.u256) + builder.addBalanceChange(address1, 2, 2.u256) + builder.addBalanceChange(address1, 2, 10.u256) # duplicate should overwrite + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 3 + bal[0].address == address1 + bal[0].balanceChanges == @[(2.BlockAccessIndex, 10.u256)] + bal[1].address == address2 + bal[1].balanceChanges == @[(0.BlockAccessIndex, 1.u256), (1.BlockAccessIndex, 0.u256)] + bal[2].address == address3 + bal[2].balanceChanges == @[(3.BlockAccessIndex, 3.u256)] + + test "Add nonce change": + builder.addNonceChange(address1, 3, 3) + builder.addNonceChange(address2, 2, 2) + builder.addNonceChange(address2, 1, 1) + builder.addNonceChange(address3, 1, 1) + builder.addNonceChange(address3, 1, 10) # duplicate should overwrite + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 3 + bal[0].address == address1 + bal[0].nonceChanges == @[(3.BlockAccessIndex, 3.AccountNonce)] + bal[1].address == address2 + bal[1].nonceChanges == @[(1.BlockAccessIndex, 1.AccountNonce), (2.BlockAccessIndex, 2.AccountNonce)] + bal[2].address == address3 + bal[2].nonceChanges == @[(1.BlockAccessIndex, 10.AccountNonce)] + + test "Add code change": + builder.addCodeChange(address2, 0, @[0x1.byte]) + builder.addCodeChange(address2, 1, @[0x2.byte]) + builder.addCodeChange(address1, 3, @[0x3.byte]) + builder.addCodeChange(address1, 3, @[0x4.byte]) # duplicate should overwrite + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 2 + bal[0].address == address1 + bal[0].codeChanges == @[(3.BlockAccessIndex, @[0x4.byte])] + bal[1].address == address2 + bal[1].codeChanges == @[(0.BlockAccessIndex, @[0x1.byte]), (1.BlockAccessIndex, @[0x2.byte])] + + test "All changes and reads": + builder.addTouchedAccount(address3) + builder.addTouchedAccount(address2) + builder.addTouchedAccount(address1) + builder.addTouchedAccount(address1) # duplicate + + builder.addStorageWrite(address1, slot3, 0, 3.u256) + builder.addStorageWrite(address1, slot2, 2, 2.u256) + builder.addStorageWrite(address1, slot1, 1, 1.u256) + builder.addStorageWrite(address2, slot1, 1, 1.u256) + builder.addStorageWrite(address1, slot3, 3, 4.u256) + builder.addStorageWrite(address1, slot3, 3, 5.u256) # duplicate should overwrite + + builder.addStorageRead(address2, slot3) + builder.addStorageRead(address2, slot2) + builder.addStorageRead(address3, slot3) + builder.addStorageRead(address1, slot1) + builder.addStorageRead(address1, slot1) # duplicate + + builder.addBalanceChange(address2, 1, 0.u256) + builder.addBalanceChange(address2, 0, 1.u256) + builder.addBalanceChange(address3, 3, 3.u256) + builder.addBalanceChange(address1, 2, 2.u256) + builder.addBalanceChange(address1, 2, 10.u256) # duplicate should overwrite + + builder.addNonceChange(address1, 3, 3) + builder.addNonceChange(address2, 2, 2) + builder.addNonceChange(address2, 1, 1) + builder.addNonceChange(address3, 1, 1) + builder.addNonceChange(address3, 1, 10) # duplicate should overwrite + + builder.addCodeChange(address2, 0, @[0x1.byte]) + builder.addCodeChange(address2, 1, @[0x2.byte]) + builder.addCodeChange(address1, 3, @[0x3.byte]) + builder.addCodeChange(address1, 3, @[0x4.byte]) # duplicate should overwrite + + let bal = builder.buildBlockAccessList() + check: + bal.len() == 3 + + bal[0].address == address1 + bal[0].storageChanges.len() == 3 + bal[0].storageChanges[0] == (slot1, @[(1.BlockAccessIndex, 1.u256)]) + bal[0].storageChanges[1] == (slot2, @[(2.BlockAccessIndex, 2.u256)]) + bal[0].storageChanges[2] == (slot3, @[(0.BlockAccessIndex, 3.u256), (3.BlockAccessIndex, 5.u256)]) + bal[0].storageReads.len() == 0 # read removed by storage change with the same slot + bal[0].balanceChanges == @[(2.BlockAccessIndex, 10.u256)] + bal[0].nonceChanges == @[(3.BlockAccessIndex, 3.AccountNonce)] + bal[0].codeChanges == @[(3.BlockAccessIndex, @[0x4.byte])] + + bal[1].address == address2 + bal[1].storageChanges.len() == 1 + bal[1].storageChanges[0] == (slot1, @[(1.BlockAccessIndex, 1.u256)]) + bal[1].storageReads == @[slot2, slot3] + bal[1].balanceChanges == @[(0.BlockAccessIndex, 1.u256), (1.BlockAccessIndex, 0.u256)] + bal[1].nonceChanges == @[(1.BlockAccessIndex, 1.AccountNonce), (2.BlockAccessIndex, 2.AccountNonce)] + bal[1].codeChanges == @[(0.BlockAccessIndex, @[0x1.byte]), (1.BlockAccessIndex, @[0x2.byte])] + + bal[2].address == address3 + bal[2].storageReads == @[slot3] + bal[2].balanceChanges == @[(3.BlockAccessIndex, 3.u256)] + bal[2].nonceChanges == @[(1.BlockAccessIndex, 10.AccountNonce)] From 3b9f83b86267b49ee0f920b380e7fdf36015432d Mon Sep 17 00:00:00 2001 From: bhartnett Date: Tue, 28 Oct 2025 13:14:39 +0800 Subject: [PATCH 7/7] Implement block access list validation tests. --- .../block_access_list_builder.nim | 17 +- .../block_access_list_validation.nim | 13 +- tests/all_tests.nim | 3 +- tests/test_block_access_list_validation.nim | 153 ++++++++++++++++++ 4 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 tests/test_block_access_list_validation.nim diff --git a/execution_chain/block_access_list/block_access_list_builder.nim b/execution_chain/block_access_list/block_access_list_builder.nim index 92ea4ff71e..abc0f77acb 100644 --- a/execution_chain/block_access_list/block_access_list_builder.nim +++ b/execution_chain/block_access_list/block_access_list_builder.nim @@ -18,10 +18,10 @@ import export block_access_lists type - # Account data stored in the builder during block execution. - # This type tracks all changes made to a single account throughout - # the execution of a block, organized by the type of change and the - # block access list index where it occurred. + # Account data stored in the builder during block execution. This type tracks + # all changes made to a single account throughout the execution of a block, + # organized by the type of change and the block access list index where it + # occurred. AccountData = object storageChanges: Table[UInt256, Table[int, UInt256]] ## Maps storage key -> block access list index -> storage value @@ -35,11 +35,10 @@ type ## Maps block access list index -> code # Builder for constructing a BlockAccessList efficiently during transaction - # execution. - # The builder accumulates all account and storage accesses during block - # execution and constructs a deterministic access list. Changes are tracked - # by address, field type, and block access list index to enable efficient - # reconstruction of state changes. + # execution. The builder accumulates all account and storage accesses during + # block execution and constructs a deterministic access list. Changes are + # tracked by address, field type, and block access list index to enable + # efficient reconstruction of state changes. BlockAccessListBuilderRef* = ref object accounts: Table[Address, AccountData] ## Maps address -> account data diff --git a/execution_chain/block_access_list/block_access_list_validation.nim b/execution_chain/block_access_list/block_access_list_validation.nim index 9491341363..e87af9180f 100644 --- a/execution_chain/block_access_list/block_access_list_validation.nim +++ b/execution_chain/block_access_list/block_access_list_validation.nim @@ -18,13 +18,6 @@ import export block_access_lists, hashes, results -# TODO: Consider setting max values and adding to the validation. -# This is not yet defined in the EIP. -# MAX_TXS = 30_000 -# MAX_SLOTS = 300_000 -# MAX_ACCOUNTS = 300_000 -# MAX_CODE_SIZE = 24_576 -# MAX_CODE_CHANGES = 1 func validate*(bal: BlockAccessList, expectedHash: Hash32): Result[void, string] = ## Validate that a block access list is structurally correct and matches the expected hash. @@ -78,8 +71,8 @@ func validate*(bal: BlockAccessList, expectedHash: Hash32): Result[void, string] if codeIndices != codeIndices.sorted(): return err("Code changes should be sorted by blockAccessIndex") - # Check that the block access list matches the expected hash. - if bal.computeBlockAccessListHash() != expectedHash: - return err("Computed block access list hash does not match the expected hash") + # Check that the block access list matches the expected hash. + if bal.computeBlockAccessListHash() != expectedHash: + return err("Computed block access list hash does not match the expected hash") ok() diff --git a/tests/all_tests.nim b/tests/all_tests.nim index b6225e3f27..5625cee813 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -44,6 +44,7 @@ import test_stateless_witness_generation, test_stateless_witness_verification, test_block_access_list_builder, - # These two suites are much slower than all the rest, so run them last + test_block_access_list_validation, + # These suites below are much slower than all the rest, so run them last test_generalstate_json, ] diff --git a/tests/test_block_access_list_validation.nim b/tests/test_block_access_list_validation.nim new file mode 100644 index 0000000000..ef6eb8a34b --- /dev/null +++ b/tests/test_block_access_list_validation.nim @@ -0,0 +1,153 @@ +# Nimbus +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +# http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or +# http://opensource.org/licenses/MIT) +# at your option. This file may not be copied, modified, or +# distributed except according to those terms. + +{.used.} + +import + unittest2, + eth/common/block_access_lists_rlp, + ../execution_chain/block_access_list/[block_access_list_builder, block_access_list_validation] + + +suite "Block access list validation": + const + address1 = address"0x10007bc31cedb7bfb8a345f31e668033056b2728" + address2 = address"0x20007bc31cedb7bfb8a345f31e668033056b2728" + address3 = address"0x30007bc31cedb7bfb8a345f31e668033056b2728" + slot1 = 1.u256() + slot2 = 2.u256() + slot3 = 3.u256() + + setup: + let builder = BlockAccessListBuilderRef.init() + + test "Empty BAL should equal the EMPTY_BLOCK_ACCESS_LIST_HASH": + let emptyBal = builder.buildBlockAccessList() + check: + emptyBal.validate(EMPTY_BLOCK_ACCESS_LIST_HASH).isOk() + emptyBal.validate(default(Hash32)).isErr() + + test "Valid BAL should validate successfully": + builder.addTouchedAccount(address3) + builder.addTouchedAccount(address2) + builder.addTouchedAccount(address1) + builder.addTouchedAccount(address1) # duplicate + + builder.addStorageWrite(address1, slot3, 0, 3.u256) + builder.addStorageWrite(address1, slot2, 2, 2.u256) + builder.addStorageWrite(address1, slot1, 1, 1.u256) + builder.addStorageWrite(address2, slot1, 1, 1.u256) + builder.addStorageWrite(address1, slot3, 3, 4.u256) + builder.addStorageWrite(address1, slot3, 3, 5.u256) # duplicate should overwrite + + builder.addStorageRead(address2, slot3) + builder.addStorageRead(address2, slot2) + builder.addStorageRead(address3, slot3) + builder.addStorageRead(address1, slot1) + builder.addStorageRead(address1, slot1) # duplicate + + builder.addBalanceChange(address2, 1, 0.u256) + builder.addBalanceChange(address2, 0, 1.u256) + builder.addBalanceChange(address3, 3, 3.u256) + builder.addBalanceChange(address1, 2, 2.u256) + builder.addBalanceChange(address1, 2, 10.u256) # duplicate should overwrite + + builder.addNonceChange(address1, 3, 3) + builder.addNonceChange(address2, 2, 2) + builder.addNonceChange(address2, 1, 1) + builder.addNonceChange(address3, 1, 1) + builder.addNonceChange(address3, 1, 10) # duplicate should overwrite + + builder.addCodeChange(address2, 0, @[0x1.byte]) + builder.addCodeChange(address2, 1, @[0x2.byte]) + builder.addCodeChange(address1, 3, @[0x3.byte]) + builder.addCodeChange(address1, 3, @[0x4.byte]) # duplicate should overwrite + + let bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + + test "Storage changes and reads don't overlap for the same slot": + builder.addStorageWrite(address1, slot1, 1, 1.u256) + builder.addStorageWrite(address1, slot2, 2, 2.u256) + builder.addStorageWrite(address1, slot3, 3, 3.u256) + + var bal = builder.buildBlockAccessList() + bal[0].storageReads = @[slot1] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Account changes out of order should fail validation": + builder.addTouchedAccount(address1) + builder.addTouchedAccount(address2) + builder.addTouchedAccount(address3) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0] = bal[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Storage changes out of order should fail validation": + builder.addStorageWrite(address1, slot1, 1, 1.u256) + builder.addStorageWrite(address1, slot2, 2, 2.u256) + builder.addStorageWrite(address1, slot3, 3, 3.u256) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0].storageChanges[0] = bal[0].storageChanges[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Slot changes out of order should fail validation": + builder.addStorageWrite(address1, slot1, 0, 0.u256) + builder.addStorageWrite(address1, slot1, 1, 1.u256) + builder.addStorageWrite(address1, slot1, 2, 2.u256) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0].storageChanges[0].changes[0] = bal[0].storageChanges[0].changes[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Storage reads out of order should fail validation": + builder.addStorageRead(address1, slot1) + builder.addStorageRead(address1, slot2) + builder.addStorageRead(address1, slot3) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0].storageReads[0] = bal[0].storageReads[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Balance changes out of order should fail validation": + builder.addBalanceChange(address1, 1, 1.u256) + builder.addBalanceChange(address1, 2, 2.u256) + builder.addBalanceChange(address1, 3, 3.u256) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0].balanceChanges[0] = bal[0].balanceChanges[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Nonce changes out of order should fail validation": + builder.addNonceChange(address1, 1, 1) + builder.addNonceChange(address1, 2, 2) + builder.addNonceChange(address1, 3, 3) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0].nonceChanges[0] = bal[0].nonceChanges[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr() + + test "Code changes out of order should fail validation": + builder.addCodeChange(address1, 0, @[0x1.byte]) + builder.addCodeChange(address1, 1, @[0x2.byte]) + builder.addCodeChange(address1, 2, @[0x3.byte]) + + var bal = builder.buildBlockAccessList() + check bal.validate(bal.computeBlockAccessListHash()).isOk() + bal[0].codeChanges[0] = bal[0].codeChanges[2] + check bal.validate(bal.computeBlockAccessListHash()).isErr()