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)

index_start_accounts

Account as it was when a block access index began, captured the first time that index writes it. Several system calls share one index, so their writes are netted against this rather than against each other.

319
    index_start_accounts: Dict[
320
        Tuple[BlockAccessIndex, Address], Optional[Account]
321
    ] = field(default_factory=dict)

index_start_storage

Storage value as it was when a block access index began, captured the first time that index writes the slot.

328
    index_start_storage: Dict[
329
        Tuple[BlockAccessIndex, Address, Bytes32], U256
330
    ] = 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:
338
    <snip>
347
    if address not in builder.accounts:
348
        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:
358
    <snip>
365
    ensure_account(builder, address)
366
367
    if slot not in builder.accounts[address].storage_changes:
368
        builder.accounts[address].storage_changes[slot] = []
369
370
    # Check if there's already an entry with the same block_access_index
371
    # If so, update it with the new value, keeping only the final write
372
    changes = builder.accounts[address].storage_changes[slot]
373
    for i, existing_change in enumerate(changes):
374
        if existing_change.block_access_index == block_access_index:
375
            # Update the existing entry with the new value
376
            changes[i] = StorageChange(
377
                block_access_index=block_access_index, new_value=new_value
378
            )
379
            return
380
381
    # No existing entry found, append new change
382
    change = StorageChange(
383
        block_access_index=block_access_index, new_value=new_value
384
    )
385
    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:
391
    <snip>
398
    ensure_account(builder, address)
399
    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:
408
    <snip>
415
    ensure_account(builder, address)
416
417
    # Balance value is already U256
418
    balance_value = post_balance
419
420
    # Check if we already have a balance change for this tx_index and update it
421
    # This ensures we only track the final balance per transaction
422
    existing_changes = builder.accounts[address].balance_changes
423
    for i, existing in enumerate(existing_changes):
424
        if existing.block_access_index == block_access_index:
425
            # Update the existing balance change with the new balance
426
            existing_changes[i] = BalanceChange(
427
                block_access_index=block_access_index,
428
                post_balance=balance_value,
429
            )
430
            return
431
432
    # No existing change for this tx_index, add a new one
433
    change = BalanceChange(
434
        block_access_index=block_access_index, post_balance=balance_value
435
    )
436
    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:
445
    <snip>
455
    ensure_account(builder, address)
456
457
    # Check if we already have a nonce change for this tx_index and update it
458
    # This ensures we only track the final (highest) nonce per transaction
459
    existing_changes = builder.accounts[address].nonce_changes
460
    for i, existing in enumerate(existing_changes):
461
        if existing.block_access_index == block_access_index:
462
            # Keep the highest nonce value
463
            if new_nonce > existing.new_nonce:
464
                existing_changes[i] = NonceChange(
465
                    block_access_index=block_access_index, new_nonce=new_nonce
466
                )
467
            return
468
469
    # No existing change for this tx_index, add a new one
470
    change = NonceChange(
471
        block_access_index=block_access_index, new_nonce=new_nonce
472
    )
473
    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:
482
    <snip>
493
    ensure_account(builder, address)
494
495
    # Check if we already have a code change for this block_access_index
496
    # This handles the case of in-transaction selfdestructs where code is
497
    # first deployed and then cleared in the same transaction
498
    existing_changes = builder.accounts[address].code_changes
499
    for i, existing in enumerate(existing_changes):
500
        if existing.block_access_index == block_access_index:
501
            # Replace the existing code change with the new one
502
            # For selfdestructs, this ensures we only record the final
503
            # state (empty code)
504
            existing_changes[i] = CodeChange(
505
                block_access_index=block_access_index, new_code=new_code
506
            )
507
            return
508
509
    # No existing change for this block_access_index, add a new one
510
    change = CodeChange(
511
        block_access_index=block_access_index, new_code=new_code
512
    )
513
    builder.accounts[address].code_changes.append(change)

remove_storage_write

Drop the storage change recorded for a slot at a block access index.

Called when a later write at the same index restores the value the slot held when the index began: nothing changed over the index as a whole, so the slot is left to surface as a read.

def remove_storage_write(​builder: BlockAccessListBuilder, ​​address: Address, ​​slot: U256, ​​block_access_index: BlockAccessIndex​) -> None:
522
    <snip>
529
    if address not in builder.accounts:
530
        return
531
    storage_changes = builder.accounts[address].storage_changes
532
    if slot not in storage_changes:
533
        return
534
    storage_changes[slot] = [
535
        change
536
        for change in storage_changes[slot]
537
        if change.block_access_index != block_access_index
538
    ]
539
    if not storage_changes[slot]:
540
        del storage_changes[slot]

remove_balance_change

Drop the balance change recorded for an account at a block access index.

Called when a later write at the same index restores the balance the account held when the index began.

def remove_balance_change(​builder: BlockAccessListBuilder, ​​address: Address, ​​block_access_index: BlockAccessIndex​) -> None:
548
    <snip>
554
    if address not in builder.accounts:
555
        return
556
    account = builder.accounts[address]
557
    account.balance_changes = [
558
        change
559
        for change in account.balance_changes
560
        if change.block_access_index != block_access_index
561
    ]

remove_nonce_change

Drop the nonce change recorded for an account at a block access index.

Called when a later write at the same index leaves the nonce where the index found it.

def remove_nonce_change(​builder: BlockAccessListBuilder, ​​address: Address, ​​block_access_index: BlockAccessIndex​) -> None:
569
    <snip>
575
    if address not in builder.accounts:
576
        return
577
    account = builder.accounts[address]
578
    account.nonce_changes = [
579
        change
580
        for change in account.nonce_changes
581
        if change.block_access_index != block_access_index
582
    ]

remove_code_change

Drop the code change recorded for an account at a block access index.

Called when a later write at the same index restores the code the account held when the index began.

def remove_code_change(​builder: BlockAccessListBuilder, ​​address: Address, ​​block_access_index: BlockAccessIndex​) -> None:
590
    <snip>
596
    if address not in builder.accounts:
597
        return
598
    account = builder.accounts[address]
599
    account.code_changes = [
600
        change
601
        for change in account.code_changes
602
        if change.block_access_index != block_access_index
603
    ]

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:
609
    <snip>  # noqa: E501
622
    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:
628
    <snip>  # noqa: E501
644
    block_access_list: BlockAccessList = []
645
646
    for address, changes in builder.accounts.items():
647
        storage_changes = []
648
        for slot, slot_changes in changes.storage_changes.items():
649
            sorted_changes = tuple(
650
                sorted(slot_changes, key=lambda x: x.block_access_index)
651
            )
652
            storage_changes.append(
653
                SlotChanges(slot=slot, changes=sorted_changes)
654
            )
655
656
        storage_reads = []
657
        for slot in changes.storage_reads:
658
            if slot not in changes.storage_changes:
659
                storage_reads.append(slot)
660
661
        balance_changes = tuple(
662
            sorted(changes.balance_changes, key=lambda x: x.block_access_index)
663
        )
664
        nonce_changes = tuple(
665
            sorted(changes.nonce_changes, key=lambda x: x.block_access_index)
666
        )
667
        code_changes = tuple(
668
            sorted(changes.code_changes, key=lambda x: x.block_access_index)
669
        )
670
671
        storage_changes.sort(key=lambda x: x.slot)
672
        storage_reads.sort()
673
674
        account_change = AccountChanges(
675
            address=address,
676
            storage_changes=tuple(storage_changes),
677
            storage_reads=tuple(storage_reads),
678
            balance_changes=balance_changes,
679
            nonce_changes=nonce_changes,
680
            code_changes=code_changes,
681
        )
682
683
        block_access_list.append(account_change)
684
685
    block_access_list.sort(key=lambda x: x.address)
686
687
    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]:
695
    <snip>
703
    if address in pre_tx_accounts:
704
        return pre_tx_accounts[address]
705
    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, or if the storage at address was wiped earlier in the block.

def _get_pre_tx_storage(​block_state: BlockState, ​​address: Address, ​​key: Bytes32​) -> U256:
713
    <snip>
719
    if address in block_state.storage_writes:
720
        if key in block_state.storage_writes[address]:
721
            return block_state.storage_writes[address][key]
722
    if address in block_state.storage_clears:
723
        return U256(0)
724
    return block_state.pre_state.get_storage(address, key)

_index_start_account

Return the account as it was when the current block access index began.

The cumulative block state holds that value until the first write at this index is merged, so it is captured on first use and reused by later writes at the same index.

def _index_start_account(​builder: BlockAccessListBuilder, ​​block_state: BlockState, ​​address: Address​) -> Optional[Account]:
732
    <snip>
739
    key = (builder.block_access_index, address)
740
    if key not in builder.index_start_accounts:
741
        builder.index_start_accounts[key] = _get_pre_tx_account(
742
            block_state.account_writes, block_state.pre_state, address
743
        )
744
    return builder.index_start_accounts[key]

_index_start_storage

Return a storage value as it was when the current block access index began, captured on first use in the same way as accounts.

def _index_start_storage(​builder: BlockAccessListBuilder, ​​block_state: BlockState, ​​address: Address, ​​key: Bytes32​) -> U256:
753
    <snip>
757
    index_key = (builder.block_access_index, address, key)
758
    if index_key not in builder.index_start_storage:
759
        builder.index_start_storage[index_key] = _get_pre_tx_storage(
760
            block_state, address, key
761
        )
762
    return builder.index_start_storage[index_key]

update_builder_from_tx

Update the BAL builder with changes from a single transaction.

Compare the transaction's writes against the state at the start of the current block access index to extract balance, nonce, code, and storage changes. A write that leaves a value where the index found it records no change, and drops one recorded earlier at the same index by another transaction sharing it, such as a second system call.

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:
769
    <snip>
781
    block_state = tx_state.parent
782
    idx = builder.block_access_index
783
784
    for address, post_account in tx_state.account_writes.items():
785
        pre_account = _index_start_account(builder, block_state, address)
786
787
        pre_balance = pre_account.balance if pre_account else U256(0)
788
        post_balance = post_account.balance if post_account else U256(0)
789
        if pre_balance != post_balance:
790
            add_balance_change(builder, address, idx, post_balance)
791
        else:
792
            remove_balance_change(builder, address, idx)
793
794
        pre_nonce = pre_account.nonce if pre_account else Uint(0)
795
        post_nonce = post_account.nonce if post_account else Uint(0)
796
        if pre_nonce != post_nonce:
797
            add_nonce_change(builder, address, idx, U64(post_nonce))
798
        else:
799
            remove_nonce_change(builder, address, idx)
800
801
        pre_code_hash = (
802
            pre_account.code_hash if pre_account else EMPTY_CODE_HASH
803
        )
804
        post_code_hash = (
805
            post_account.code_hash if post_account else EMPTY_CODE_HASH
806
        )
807
        if pre_code_hash != post_code_hash:
808
            post_code = get_code(tx_state, post_code_hash)
809
            add_code_change(builder, address, idx, post_code)
810
        else:
811
            remove_code_change(builder, address, idx)
812
813
    for address, slots in tx_state.storage_writes.items():
814
        for key, post_value in slots.items():
815
            pre_value = _index_start_storage(
816
                builder, block_state, address, key
817
            )
818
            # Convert slot from internal Bytes32 format to U256 for BAL.
819
            # EIP-7928 uses U256 as it's more space-efficient in RLP.
820
            u256_slot = U256.from_be_bytes(key)
821
            if pre_value != post_value:
822
                add_storage_write(builder, address, u256_slot, idx, post_value)
823
            else:
824
                remove_storage_write(builder, address, u256_slot, idx)

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:
831
    <snip>  # noqa: E501
839
    # Add storage reads (convert Bytes32 to U256 for BAL encoding)
840
    for address, slot in block_state.storage_reads:
841
        add_storage_read(builder, address, U256.from_be_bytes(slot))
842
843
    # Add touched addresses
844
    for address in block_state.account_reads:
845
        add_touched_account(builder, address)
846
847
    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:
853
    <snip>
856
    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:
863
    <snip>
869
    from .vm.gas import GasCosts
870
871
    bal_items = Uint(0)
872
    for account in block_access_list:
873
        # Count each address as one item
874
        bal_items += Uint(1)
875
876
        # Collect unique storage keys across both
877
        # reads and writes
878
        unique_slots: Set[U256] = set()
879
        for slot_change in account.storage_changes:
880
            unique_slots.add(slot_change.slot)
881
        for slot in account.storage_reads:
882
            unique_slots.add(slot)
883
884
        # Count each unique storage key as one item
885
        bal_items += ulen(unique_slots)
886
887
    if bal_items > block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM:
888
        raise BlockAccessListGasLimitExceededError(
889
            f"Block access list exceeds gas limit, {bal_items} items "
890
            f"exceeds limit of "
891
            f"{block_gas_limit // GasCosts.BLOCK_ACCESS_LIST_ITEM}."
892
        )