ethereum.forks.amsterdam.block_access_lists

Block access lists (BALs), originally defined in EIP-7928, record all accounts and storage locations accessed during block execution along with their post-execution values.

BALs enable parallel disk reads, parallel transaction validation, parallel state root computation, and applying state updates without executing bytecode.

See BlockAccessList for more detail.

StorageChange

In a SlotChanges, represents a single change in an Account's storage slot.

31
@slotted_freezable
32
@dataclass
class StorageChange:

block_access_index

Position within the set of all changes in a Block.

42
    block_access_index: BlockAccessIndex

new_value

Value of an Account's storage slot after this change has been applied.

49
    new_value: U256

BalanceChange

In a BlockAccessList, represents a change in an Account's balance.

57
@slotted_freezable
58
@dataclass
class BalanceChange:

block_access_index

Position within the set of all changes in a Block.

68
    block_access_index: BlockAccessIndex

post_balance

Balance of an Account after this change has been applied.

75
    post_balance: U256

NonceChange

In a BlockAccessList, represents a change in an Account's nonce.

83
@slotted_freezable
84
@dataclass
class NonceChange:

block_access_index

Position within the set of all changes in a Block.

94
    block_access_index: BlockAccessIndex

new_nonce

Nonce of an Account after this change has been applied.

101
    new_nonce: U64

CodeChange

In a BlockAccessList, represents a change in an Account's code.

109
@slotted_freezable
110
@dataclass
class CodeChange:

block_access_index

Position within the set of all changes in a Block.

120
    block_access_index: BlockAccessIndex

new_code

Code of an Account after this change has been applied.

127
    new_code: Bytes

SlotChanges

In a BlockAccessList, represents a change in an Account's storage.

135
@slotted_freezable
136
@dataclass
class SlotChanges:

slot

Location within an Account's storage that has been modified.

146
    slot: U256

changes

Sequence of changes that have been made to one particular storage slot.

153
    changes: Tuple[StorageChange, ...]

AccountChanges

All changes for a single Account, grouped by field type.

159
@slotted_freezable
160
@dataclass
class AccountChanges:

address

Address of the account containing these changes.

168
    address: Address

storage_changes

Writes to the storage of the associated Account.

173
    storage_changes: Tuple[SlotChanges, ...]

storage_reads

Storage slots of the associated Account that have been read but not changed.

180
    storage_reads: Tuple[U256, ...]

balance_changes

Writes to the balance of the associated Account.

188
    balance_changes: Tuple[BalanceChange, ...]

nonce_changes

Writes to the nonce of the associated Account.

195
    nonce_changes: Tuple[NonceChange, ...]

code_changes

Writes to the code of the associated Account.

202
    code_changes: Tuple[CodeChange, ...]

BlockAccessList

List of state changes recorded across a Block.

The hash of a block's access list is included in its Header, though the access list itself is not included in the block body.

A BlockAccessList includes, for example, the targets of:

210
BlockAccessList: TypeAlias = List[AccountChanges]

AccountData

Account data stored in the builder during block execution.

This dataclass tracks all changes made to a single account throughout the execution of a block, organized by the type of change and the transaction index where it occurred.

236
@dataclass
class AccountData:

storage_changes

Mapping from storage slot to list of changes made to that slot. Each change includes the transaction index and new value.

246
    storage_changes: Dict[U256, List[StorageChange]] = field(
247
        default_factory=dict
248
    )

storage_reads

Set of storage slots that were read but not modified.

254
    storage_reads: Set[U256] = field(default_factory=set)

balance_changes

List of balance changes for this account, ordered by transaction index.

259
    balance_changes: List[BalanceChange] = field(default_factory=list)

nonce_changes

List of nonce changes for this account, ordered by transaction index.

264
    nonce_changes: List[NonceChange] = field(default_factory=list)

code_changes

List of code changes (contract deployments) for this account, ordered by transaction index.

269
    code_changes: List[CodeChange] = field(default_factory=list)

BlockAccessListBuilder

Builder for constructing 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 transaction index to enable efficient reconstruction of state changes.

The builder follows a two-phase approach:

  1. Collection Phase: During transaction execution, all state accesses are recorded via the tracking functions.

  2. Build Phase: After block execution, the accumulated data is sorted and encoded into the final deterministic format.

276
@dataclass
class BlockAccessListBuilder:

block_access_index

Current block access index. Set by the caller before each incorporate_tx_into_block call (0 for system txs, i+1 for the i-th user tx, N+1 for post-execution operations).

297
    block_access_index: BlockAccessIndex = BlockAccessIndex(0)

accounts

Mapping from account address to its tracked changes during block execution.

306
    accounts: Dict[Address, AccountData] = field(default_factory=dict)

ensure_account

Ensure an account exists in the builder's tracking structure.

Creates an empty AccountData entry for the given address if it doesn't already exist. This function is idempotent and safe to call multiple times for the same address.

def ensure_account(builder: BlockAccessListBuilder, ​​address: Address) -> None:
313
    <snip>
322
    if address not in builder.accounts:
323
        builder.accounts[address] = AccountData()

add_storage_write

Add a storage write operation to the block access list.

Records a storage slot modification for a given address at a specific transaction index. If multiple writes occur to the same slot within the same transaction (same block_access_index), only the final value is kept.

def add_storage_write(builder: BlockAccessListBuilder, ​​address: Address, ​​slot: U256, ​​block_access_index: BlockAccessIndex, ​​new_value: U256) -> None:
333
    <snip>
340
    ensure_account(builder, address)
341
342
    if slot not in builder.accounts[address].storage_changes:
343
        builder.accounts[address].storage_changes[slot] = []
344
345
    # Check if there's already an entry with the same block_access_index
346
    # If so, update it with the new value, keeping only the final write
347
    changes = builder.accounts[address].storage_changes[slot]
348
    for i, existing_change in enumerate(changes):
349
        if existing_change.block_access_index == block_access_index:
350
            # Update the existing entry with the new value
351
            changes[i] = StorageChange(
352
                block_access_index=block_access_index, new_value=new_value
353
            )
354
            return
355
356
    # No existing entry found, append new change
357
    change = StorageChange(
358
        block_access_index=block_access_index, new_value=new_value
359
    )
360
    builder.accounts[address].storage_changes[slot].append(change)

add_storage_read

Add a storage read operation to the block access list.

Records that a storage slot was read during execution. Storage slots that are both read and written will only appear in the storage changes list, not in the storage reads list, as per [EIP-7928].

def add_storage_read(builder: BlockAccessListBuilder, ​​address: Address, ​​slot: U256) -> None:
366
    <snip>
373
    ensure_account(builder, address)
374
    builder.accounts[address].storage_reads.add(slot)

add_balance_change

Add a balance change to the block access list.

Records the post-transaction balance for an account after it has been modified. This includes changes from transfers, gas fees, block rewards, and any other balance-affecting operations.

def add_balance_change(builder: BlockAccessListBuilder, ​​address: Address, ​​block_access_index: BlockAccessIndex, ​​post_balance: U256) -> None:
383
    <snip>
390
    ensure_account(builder, address)
391
392
    # Balance value is already U256
393
    balance_value = post_balance
394
395
    # Check if we already have a balance change for this tx_index and update it
396
    # This ensures we only track the final balance per transaction
397
    existing_changes = builder.accounts[address].balance_changes
398
    for i, existing in enumerate(existing_changes):
399
        if existing.block_access_index == block_access_index:
400
            # Update the existing balance change with the new balance
401
            existing_changes[i] = BalanceChange(
402
                block_access_index=block_access_index,
403
                post_balance=balance_value,
404
            )
405
            return
406
407
    # No existing change for this tx_index, add a new one
408
    change = BalanceChange(
409
        block_access_index=block_access_index, post_balance=balance_value
410
    )
411
    builder.accounts[address].balance_changes.append(change)

add_nonce_change

Add a nonce change to the block access list.

Records a nonce increment for an account. This occurs when an EOA sends a transaction or when a contract performs CREATE or CREATE2 operations.

def add_nonce_change(builder: BlockAccessListBuilder, ​​address: Address, ​​block_access_index: BlockAccessIndex, ​​new_nonce: U64) -> None:
420
    <snip>
430
    ensure_account(builder, address)
431
432
    # Check if we already have a nonce change for this tx_index and update it
433
    # This ensures we only track the final (highest) nonce per transaction
434
    existing_changes = builder.accounts[address].nonce_changes
435
    for i, existing in enumerate(existing_changes):
436
        if existing.block_access_index == block_access_index:
437
            # Keep the highest nonce value
438
            if new_nonce > existing.new_nonce:
439
                existing_changes[i] = NonceChange(
440
                    block_access_index=block_access_index, new_nonce=new_nonce
441
                )
442
            return
443
444
    # No existing change for this tx_index, add a new one
445
    change = NonceChange(
446
        block_access_index=block_access_index, new_nonce=new_nonce
447
    )
448
    builder.accounts[address].nonce_changes.append(change)

add_code_change

Add a code change to the block access list.

Records contract code deployment or modification. This typically occurs during contract creation via CREATE, CREATE2, or SetCodeTransaction operations.

def add_code_change(builder: BlockAccessListBuilder, ​​address: Address, ​​block_access_index: BlockAccessIndex, ​​new_code: Bytes) -> None:
457
    <snip>
468
    ensure_account(builder, address)
469
470
    # Check if we already have a code change for this block_access_index
471
    # This handles the case of in-transaction selfdestructs where code is
472
    # first deployed and then cleared in the same transaction
473
    existing_changes = builder.accounts[address].code_changes
474
    for i, existing in enumerate(existing_changes):
475
        if existing.block_access_index == block_access_index:
476
            # Replace the existing code change with the new one
477
            # For selfdestructs, this ensures we only record the final
478
            # state (empty code)
479
            existing_changes[i] = CodeChange(
480
                block_access_index=block_access_index, new_code=new_code
481
            )
482
            return
483
484
    # No existing change for this block_access_index, add a new one
485
    change = CodeChange(
486
        block_access_index=block_access_index, new_code=new_code
487
    )
488
    builder.accounts[address].code_changes.append(change)

add_touched_account

Add an account that was accessed but not modified.

Records that an account was accessed during execution without any state changes. This is used for operations like EXTCODEHASH, BALANCE, EXTCODESIZE, and EXTCODECOPY that read account data without modifying it.

def add_touched_account(builder: BlockAccessListBuilder, ​​address: Address) -> None:
494
    <snip>  # noqa: E501
507
    ensure_account(builder, address)

_build_from_builder

Build the final BlockAccessList from a builder (internal helper).

Constructs a deterministic block access list by sorting all accumulated changes. The resulting list is ordered by:

  1. Account addresses (lexicographically)

  2. Within each account:

    • Storage slots (lexicographically)

    • Transaction indices (numerically) for each change type

Addresses, storage slots, and block access indices are unique. Storage reads that also appear in storage changes are excluded.

def _build_from_builder(builder: BlockAccessListBuilder) -> BlockAccessList:
513
    <snip>  # noqa: E501
529
    block_access_list: BlockAccessList = []
530
531
    for address, changes in builder.accounts.items():
532
        storage_changes = []
533
        for slot, slot_changes in changes.storage_changes.items():
534
            sorted_changes = tuple(
535
                sorted(slot_changes, key=lambda x: x.block_access_index)
536
            )
537
            storage_changes.append(
538
                SlotChanges(slot=slot, changes=sorted_changes)
539
            )
540
541
        storage_reads = []
542
        for slot in changes.storage_reads:
543
            if slot not in changes.storage_changes:
544
                storage_reads.append(slot)
545
546
        balance_changes = tuple(
547
            sorted(changes.balance_changes, key=lambda x: x.block_access_index)
548
        )
549
        nonce_changes = tuple(
550
            sorted(changes.nonce_changes, key=lambda x: x.block_access_index)
551
        )
552
        code_changes = tuple(
553
            sorted(changes.code_changes, key=lambda x: x.block_access_index)
554
        )
555
556
        storage_changes.sort(key=lambda x: x.slot)
557
        storage_reads.sort()
558
559
        account_change = AccountChanges(
560
            address=address,
561
            storage_changes=tuple(storage_changes),
562
            storage_reads=tuple(storage_reads),
563
            balance_changes=balance_changes,
564
            nonce_changes=nonce_changes,
565
            code_changes=code_changes,
566
        )
567
568
        block_access_list.append(account_change)
569
570
    block_access_list.sort(key=lambda x: x.address)
571
572
    return block_access_list

_get_pre_tx_account

Look up an account in cumulative state, falling back to pre_state.

The cumulative account state (pre_tx_accounts) should contain state up to (but not including) the current transaction.

Returns None if the address does not exist.

def _get_pre_tx_account(pre_tx_accounts: Dict[Address, Optional[Account]], ​​pre_state: PreState, ​​address: Address) -> Optional[Account]:
580
    <snip>
588
    if address in pre_tx_accounts:
589
        return pre_tx_accounts[address]
590
    return pre_state.get_account_optional(address)

_get_pre_tx_storage

Look up a storage value in cumulative state, falling back to pre_state.

Returns 0 if not set.

def _get_pre_tx_storage(pre_tx_storage: Dict[Address, Dict[Bytes32, U256]], ​​pre_state: PreState, ​​address: Address, ​​key: Bytes32) -> U256:
599
    <snip>
604
    if address in pre_tx_storage and key in pre_tx_storage[address]:
605
        return pre_tx_storage[address][key]
606
    return pre_state.get_storage(address, key)

update_builder_from_tx

Update the BAL builder with changes from a single transaction.

Compare the transaction's writes against the block's cumulative state (falling back to pre_state) to extract balance, nonce, code, and storage changes. Net-zero filtering is automatic: if the pre-tx value equals the post-tx value, no change is recorded.

Must be called before the transaction's writes are merged into the block state.

def update_builder_from_tx(builder: BlockAccessListBuilder, ​​tx_state: TransactionState) -> None:
613
    <snip>
624
    block_state = tx_state.parent
625
    pre_state = block_state.pre_state
626
    idx = builder.block_access_index
627
628
    # Compare account writes against block cumulative state
629
    for address, post_account in tx_state.account_writes.items():
630
        pre_account = _get_pre_tx_account(
631
            block_state.account_writes, pre_state, address
632
        )
633
634
        pre_balance = pre_account.balance if pre_account else U256(0)
635
        post_balance = post_account.balance if post_account else U256(0)
636
        if pre_balance != post_balance:
637
            add_balance_change(builder, address, idx, post_balance)
638
639
        pre_nonce = pre_account.nonce if pre_account else Uint(0)
640
        post_nonce = post_account.nonce if post_account else Uint(0)
641
        if pre_nonce != post_nonce:
642
            add_nonce_change(builder, address, idx, U64(post_nonce))
643
644
        pre_code_hash = (
645
            pre_account.code_hash if pre_account else EMPTY_CODE_HASH
646
        )
647
        post_code_hash = (
648
            post_account.code_hash if post_account else EMPTY_CODE_HASH
649
        )
650
        if pre_code_hash != post_code_hash:
651
            post_code = get_code(tx_state, post_code_hash)
652
            add_code_change(builder, address, idx, post_code)
653
654
    # Compare storage writes against block cumulative state
655
    for address, slots in tx_state.storage_writes.items():
656
        for key, post_value in slots.items():
657
            pre_value = _get_pre_tx_storage(
658
                block_state.storage_writes, pre_state, address, key
659
            )
660
            if pre_value != post_value:
661
                # Convert slot from internal Bytes32 format to U256 for BAL.
662
                # EIP-7928 uses U256 as it's more space-efficient in RLP.
663
                u256_slot = U256.from_be_bytes(key)
664
                add_storage_write(builder, address, u256_slot, idx, post_value)

build_block_access_list

Build a BlockAccessList from the builder and block state.

Feed accumulated reads from the block state into the builder, then produce the final sorted and encoded block access list.

def build_block_access_list(builder: BlockAccessListBuilder, ​​block_state: BlockState) -> BlockAccessList:
671
    <snip>  # noqa: E501
679
    # Add storage reads (convert Bytes32 to U256 for BAL encoding)
680
    for address, slot in block_state.storage_reads:
681
        add_storage_read(builder, address, U256.from_be_bytes(slot))
682
683
    # Add touched addresses
684
    for address in block_state.account_reads:
685
        add_touched_account(builder, address)
686
687
    return _build_from_builder(builder)

hash_block_access_list

Compute the hash of a Block Access List.

def hash_block_access_list(block_access_list: BlockAccessList) -> Hash32:
693
    <snip>
696
    return keccak256(rlp.encode(block_access_list))

validate_block_access_list_gas_limit

Validate that the block access list does not exceed the gas limit.

The total number of items (addresses + unique storage keys) must not exceed block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM.

def validate_block_access_list_gas_limit(block_access_list: BlockAccessList, ​​block_gas_limit: Uint) -> None:
703
    <snip>
709
    from .vm.gas import GasCosts
710
711
    bal_items = Uint(0)
712
    for account in block_access_list:
713
        # Count each address as one item
714
        bal_items += Uint(1)
715
716
        # Collect unique storage keys across both
717
        # reads and writes
718
        unique_slots: Set[U256] = set()
719
        for slot_change in account.storage_changes:
720
            unique_slots.add(slot_change.slot)
721
        for slot in account.storage_reads:
722
            unique_slots.add(slot)
723
724
        # Count each unique storage key as one item
725
        bal_items += ulen(unique_slots)
726
727
    if bal_items > block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM:
728
        raise BlockAccessListGasLimitExceededError(
729
            f"Block access list exceeds gas limit, {bal_items} items "
730
            f"exceeds limit of "
731
            f"{block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM}."
732
        )