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
@final
32
@slotted_freezable
33
@dataclass
class StorageChange:

block_access_index

Position within the set of all changes in a Block.

43
    block_access_index: BlockAccessIndex

new_value

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

50
    new_value: U256

BalanceChange

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

58
@final
59
@slotted_freezable
60
@dataclass
class BalanceChange:

block_access_index

Position within the set of all changes in a Block.

70
    block_access_index: BlockAccessIndex

post_balance

Balance of an Account after this change has been applied.

77
    post_balance: U256

NonceChange

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

85
@final
86
@slotted_freezable
87
@dataclass
class NonceChange:

block_access_index

Position within the set of all changes in a Block.

97
    block_access_index: BlockAccessIndex

new_nonce

Nonce of an Account after this change has been applied.

104
    new_nonce: U64

CodeChange

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

112
@final
113
@slotted_freezable
114
@dataclass
class CodeChange:

block_access_index

Position within the set of all changes in a Block.

124
    block_access_index: BlockAccessIndex

new_code

Code of an Account after this change has been applied.

131
    new_code: Bytes

SlotChanges

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

139
@final
140
@slotted_freezable
141
@dataclass
class SlotChanges:

slot

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

151
    slot: U256

changes

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

158
    changes: Tuple[StorageChange, ...]

AccountChanges

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

164
@final
165
@slotted_freezable
166
@dataclass
class AccountChanges:

address

Address of the account containing these changes.

174
    address: Address

storage_changes

Writes to the storage of the associated Account.

179
    storage_changes: Tuple[SlotChanges, ...]

storage_reads

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

186
    storage_reads: Tuple[U256, ...]

balance_changes

Writes to the balance of the associated Account.

194
    balance_changes: Tuple[BalanceChange, ...]

nonce_changes

Writes to the nonce of the associated Account.

201
    nonce_changes: Tuple[NonceChange, ...]

code_changes

Writes to the code of the associated Account.

208
    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:

216
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.

242
@final
243
@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.

253
    storage_changes: Dict[U256, List[StorageChange]] = field(
254
        default_factory=dict
255
    )

storage_reads

Set of storage slots that were read but not modified.

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

balance_changes

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

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

nonce_changes

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

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

code_changes

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

276
    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.

283
@final
284
@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).

305
    block_access_index: BlockAccessIndex = BlockAccessIndex(0)

accounts

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

314
    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:
321
    <snip>
330
    if address not in builder.accounts:
331
        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:
341
    <snip>
348
    ensure_account(builder, address)
349
350
    if slot not in builder.accounts[address].storage_changes:
351
        builder.accounts[address].storage_changes[slot] = []
352
353
    # Check if there's already an entry with the same block_access_index
354
    # If so, update it with the new value, keeping only the final write
355
    changes = builder.accounts[address].storage_changes[slot]
356
    for i, existing_change in enumerate(changes):
357
        if existing_change.block_access_index == block_access_index:
358
            # Update the existing entry with the new value
359
            changes[i] = StorageChange(
360
                block_access_index=block_access_index, new_value=new_value
361
            )
362
            return
363
364
    # No existing entry found, append new change
365
    change = StorageChange(
366
        block_access_index=block_access_index, new_value=new_value
367
    )
368
    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:
374
    <snip>
381
    ensure_account(builder, address)
382
    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:
391
    <snip>
398
    ensure_account(builder, address)
399
400
    # Balance value is already U256
401
    balance_value = post_balance
402
403
    # Check if we already have a balance change for this tx_index and update it
404
    # This ensures we only track the final balance per transaction
405
    existing_changes = builder.accounts[address].balance_changes
406
    for i, existing in enumerate(existing_changes):
407
        if existing.block_access_index == block_access_index:
408
            # Update the existing balance change with the new balance
409
            existing_changes[i] = BalanceChange(
410
                block_access_index=block_access_index,
411
                post_balance=balance_value,
412
            )
413
            return
414
415
    # No existing change for this tx_index, add a new one
416
    change = BalanceChange(
417
        block_access_index=block_access_index, post_balance=balance_value
418
    )
419
    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:
428
    <snip>
438
    ensure_account(builder, address)
439
440
    # Check if we already have a nonce change for this tx_index and update it
441
    # This ensures we only track the final (highest) nonce per transaction
442
    existing_changes = builder.accounts[address].nonce_changes
443
    for i, existing in enumerate(existing_changes):
444
        if existing.block_access_index == block_access_index:
445
            # Keep the highest nonce value
446
            if new_nonce > existing.new_nonce:
447
                existing_changes[i] = NonceChange(
448
                    block_access_index=block_access_index, new_nonce=new_nonce
449
                )
450
            return
451
452
    # No existing change for this tx_index, add a new one
453
    change = NonceChange(
454
        block_access_index=block_access_index, new_nonce=new_nonce
455
    )
456
    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:
465
    <snip>
476
    ensure_account(builder, address)
477
478
    # Check if we already have a code change for this block_access_index
479
    # This handles the case of in-transaction selfdestructs where code is
480
    # first deployed and then cleared in the same transaction
481
    existing_changes = builder.accounts[address].code_changes
482
    for i, existing in enumerate(existing_changes):
483
        if existing.block_access_index == block_access_index:
484
            # Replace the existing code change with the new one
485
            # For selfdestructs, this ensures we only record the final
486
            # state (empty code)
487
            existing_changes[i] = CodeChange(
488
                block_access_index=block_access_index, new_code=new_code
489
            )
490
            return
491
492
    # No existing change for this block_access_index, add a new one
493
    change = CodeChange(
494
        block_access_index=block_access_index, new_code=new_code
495
    )
496
    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:
502
    <snip>  # noqa: E501
515
    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:
521
    <snip>  # noqa: E501
537
    block_access_list: BlockAccessList = []
538
539
    for address, changes in builder.accounts.items():
540
        storage_changes = []
541
        for slot, slot_changes in changes.storage_changes.items():
542
            sorted_changes = tuple(
543
                sorted(slot_changes, key=lambda x: x.block_access_index)
544
            )
545
            storage_changes.append(
546
                SlotChanges(slot=slot, changes=sorted_changes)
547
            )
548
549
        storage_reads = []
550
        for slot in changes.storage_reads:
551
            if slot not in changes.storage_changes:
552
                storage_reads.append(slot)
553
554
        balance_changes = tuple(
555
            sorted(changes.balance_changes, key=lambda x: x.block_access_index)
556
        )
557
        nonce_changes = tuple(
558
            sorted(changes.nonce_changes, key=lambda x: x.block_access_index)
559
        )
560
        code_changes = tuple(
561
            sorted(changes.code_changes, key=lambda x: x.block_access_index)
562
        )
563
564
        storage_changes.sort(key=lambda x: x.slot)
565
        storage_reads.sort()
566
567
        account_change = AccountChanges(
568
            address=address,
569
            storage_changes=tuple(storage_changes),
570
            storage_reads=tuple(storage_reads),
571
            balance_changes=balance_changes,
572
            nonce_changes=nonce_changes,
573
            code_changes=code_changes,
574
        )
575
576
        block_access_list.append(account_change)
577
578
    block_access_list.sort(key=lambda x: x.address)
579
580
    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]:
588
    <snip>
596
    if address in pre_tx_accounts:
597
        return pre_tx_accounts[address]
598
    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:
607
    <snip>
612
    if address in pre_tx_storage and key in pre_tx_storage[address]:
613
        return pre_tx_storage[address][key]
614
    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:
621
    <snip>
632
    block_state = tx_state.parent
633
    pre_state = block_state.pre_state
634
    idx = builder.block_access_index
635
636
    # Compare account writes against block cumulative state
637
    for address, post_account in tx_state.account_writes.items():
638
        pre_account = _get_pre_tx_account(
639
            block_state.account_writes, pre_state, address
640
        )
641
642
        pre_balance = pre_account.balance if pre_account else U256(0)
643
        post_balance = post_account.balance if post_account else U256(0)
644
        if pre_balance != post_balance:
645
            add_balance_change(builder, address, idx, post_balance)
646
647
        pre_nonce = pre_account.nonce if pre_account else Uint(0)
648
        post_nonce = post_account.nonce if post_account else Uint(0)
649
        if pre_nonce != post_nonce:
650
            add_nonce_change(builder, address, idx, U64(post_nonce))
651
652
        pre_code_hash = (
653
            pre_account.code_hash if pre_account else EMPTY_CODE_HASH
654
        )
655
        post_code_hash = (
656
            post_account.code_hash if post_account else EMPTY_CODE_HASH
657
        )
658
        if pre_code_hash != post_code_hash:
659
            post_code = get_code(tx_state, post_code_hash)
660
            add_code_change(builder, address, idx, post_code)
661
662
    # Compare storage writes against block cumulative state
663
    for address, slots in tx_state.storage_writes.items():
664
        for key, post_value in slots.items():
665
            pre_value = _get_pre_tx_storage(
666
                block_state.storage_writes, pre_state, address, key
667
            )
668
            if pre_value != post_value:
669
                # Convert slot from internal Bytes32 format to U256 for BAL.
670
                # EIP-7928 uses U256 as it's more space-efficient in RLP.
671
                u256_slot = U256.from_be_bytes(key)
672
                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:
679
    <snip>  # noqa: E501
687
    # Add storage reads (convert Bytes32 to U256 for BAL encoding)
688
    for address, slot in block_state.storage_reads:
689
        add_storage_read(builder, address, U256.from_be_bytes(slot))
690
691
    # Add touched addresses
692
    for address in block_state.account_reads:
693
        add_touched_account(builder, address)
694
695
    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:
701
    <snip>
704
    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:
711
    <snip>
717
    from .vm.gas import GasCosts
718
719
    bal_items = Uint(0)
720
    for account in block_access_list:
721
        # Count each address as one item
722
        bal_items += Uint(1)
723
724
        # Collect unique storage keys across both
725
        # reads and writes
726
        unique_slots: Set[U256] = set()
727
        for slot_change in account.storage_changes:
728
            unique_slots.add(slot_change.slot)
729
        for slot in account.storage_reads:
730
            unique_slots.add(slot)
731
732
        # Count each unique storage key as one item
733
        bal_items += ulen(unique_slots)
734
735
    if bal_items > block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM:
736
        raise BlockAccessListGasLimitExceededError(
737
            f"Block access list exceeds gas limit, {bal_items} items "
738
            f"exceeds limit of "
739
            f"{block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM}."
740
        )