Skip to content
175 changes: 175 additions & 0 deletions execution_chain/block_access_list/block_access_list_builder.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# 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

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

template addTouchedAccount*(builder: BlockAccessListBuilderRef, address: Address) =
ensureAccount(builder, address)

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 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.mpairs():
# 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
78 changes: 78 additions & 0 deletions execution_chain/block_access_list/block_access_list_validation.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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


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(it.slot)
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 lexicographically
let storageReadsSlots = accountChanges.storageReads
if storageReadsSlots != storageReadsSlots.sorted():
return err("Storage reads should be sorted lexicographically")

# 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()
4 changes: 3 additions & 1 deletion tests/all_tests.nim
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import
test_stateless_witness_types,
test_stateless_witness_generation,
test_stateless_witness_verification,
# These two suites are much slower than all the rest, so run them last
test_block_access_list_builder,
test_block_access_list_validation,
# These suites below are much slower than all the rest, so run them last
test_generalstate_json,
]
Loading