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.

StorageKey

Slot within an Account's storage.

31
StorageKey: TypeAlias = U256

StorageValue

Value associated with a StorageKey within an Account's storage.

36
StorageValue: TypeAlias = U256

CodeData

Bytecode associated with an Account.

44
CodeData: TypeAlias = Bytes

BlockAccessIndex

Position within the set of all changes in a Block.

49
BlockAccessIndex: TypeAlias = U16

Balance

Balance associated with an Account, in wei.

56
Balance: TypeAlias = U256

Nonce

Nonce associated with an Account.

63
Nonce: TypeAlias = U64

StorageChange

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

69
@slotted_freezable
70
@dataclass
class StorageChange:

block_access_index

Position within the set of all changes in a Block.

80
    block_access_index: BlockAccessIndex

new_value

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

87
    new_value: StorageValue

BalanceChange

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

95
@slotted_freezable
96
@dataclass
class BalanceChange:

block_access_index

Position within the set of all changes in a Block.

106
    block_access_index: BlockAccessIndex

post_balance

Balance of an Account after this change has been applied.

113
    post_balance: Balance

NonceChange

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

121
@slotted_freezable
122
@dataclass
class NonceChange:

block_access_index

Position within the set of all changes in a Block.

132
    block_access_index: BlockAccessIndex

new_nonce

Nonce of an Account after this change has been applied.

139
    new_nonce: Nonce

CodeChange

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

147
@slotted_freezable
148
@dataclass
class CodeChange:

block_access_index

Position within the set of all changes in a Block.

158
    block_access_index: BlockAccessIndex

new_code

Code of an Account after this change has been applied.

165
    new_code: CodeData

SlotChanges

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

173
@slotted_freezable
174
@dataclass
class SlotChanges:

slot

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

184
    slot: StorageKey

changes

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

191
    changes: Tuple[StorageChange, ...]

AccountChanges

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

197
@slotted_freezable
198
@dataclass
class AccountChanges:

address

Address of the account containing these changes.

206
    address: Address

storage_changes

Writes to the storage of the associated Account.

211
    storage_changes: Tuple[SlotChanges, ...]

storage_reads

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

218
    storage_reads: Tuple[StorageKey, ...]

balance_changes

Writes to the balance of the associated Account.

226
    balance_changes: Tuple[BalanceChange, ...]

nonce_changes

Writes to the nonce of the associated Account.

233
    nonce_changes: Tuple[NonceChange, ...]

code_changes

Writes to the code of the associated Account.

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

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

274
@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.

284
    storage_changes: Dict[U256, List[StorageChange]] = field(
285
        default_factory=dict
286
    )

storage_reads

Set of storage slots that were read but not modified.

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

balance_changes

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

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

nonce_changes

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

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

code_changes

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

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

314
@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).

335
    block_access_index: BlockAccessIndex = BlockAccessIndex(0)

accounts

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

344
    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:
351
    """
352
    Ensure an account exists in the builder's tracking structure.
353
354
    Creates an empty [`AccountData`][ad] entry for the given address if it
355
    doesn't already exist. This function is idempotent and safe to call
356
    multiple times for the same address.
357
358
    [ad]: ref:ethereum.forks.amsterdam.block_access_lists.AccountData
359
    """
360
    if address not in builder.accounts:
361
        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:
371
    """
372
    Add a storage write operation to the block access list.
373
374
    Records a storage slot modification for a given address at a specific
375
    transaction index. If multiple writes occur to the same slot within the
376
    same transaction (same `block_access_index`), only the final value is kept.
377
    """
378
    ensure_account(builder, address)
379
380
    if slot not in builder.accounts[address].storage_changes:
381
        builder.accounts[address].storage_changes[slot] = []
382
383
    # Check if there's already an entry with the same block_access_index
384
    # If so, update it with the new value, keeping only the final write
385
    changes = builder.accounts[address].storage_changes[slot]
386
    for i, existing_change in enumerate(changes):
387
        if existing_change.block_access_index == block_access_index:
388
            # Update the existing entry with the new value
389
            changes[i] = StorageChange(
390
                block_access_index=block_access_index, new_value=new_value
391
            )
392
            return
393
394
    # No existing entry found, append new change
395
    change = StorageChange(
396
        block_access_index=block_access_index, new_value=new_value
397
    )
398
    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:
404
    """
405
    Add a storage read operation to the block access list.
406
407
    Records that a storage slot was read during execution. Storage slots
408
    that are both read and written will only appear in the storage changes
409
    list, not in the storage reads list, as per [EIP-7928].
410
    """
411
    ensure_account(builder, address)
412
    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:
421
    """
422
    Add a balance change to the block access list.
423
424
    Records the post-transaction balance for an account after it has been
425
    modified. This includes changes from transfers, gas fees, block rewards,
426
    and any other balance-affecting operations.
427
    """
428
    ensure_account(builder, address)
429
430
    # Balance value is already U256
431
    balance_value = post_balance
432
433
    # Check if we already have a balance change for this tx_index and update it
434
    # This ensures we only track the final balance per transaction
435
    existing_changes = builder.accounts[address].balance_changes
436
    for i, existing in enumerate(existing_changes):
437
        if existing.block_access_index == block_access_index:
438
            # Update the existing balance change with the new balance
439
            existing_changes[i] = BalanceChange(
440
                block_access_index=block_access_index,
441
                post_balance=balance_value,
442
            )
443
            return
444
445
    # No existing change for this tx_index, add a new one
446
    change = BalanceChange(
447
        block_access_index=block_access_index, post_balance=balance_value
448
    )
449
    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:
458
    """
459
    Add a nonce change to the block access list.
460
461
    Records a nonce increment for an account. This occurs when an EOA sends
462
    a transaction or when a contract performs [`CREATE`] or [`CREATE2`]
463
    operations.
464
465
    [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create
466
    [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2
467
    """
468
    ensure_account(builder, address)
469
470
    # Check if we already have a nonce change for this tx_index and update it
471
    # This ensures we only track the final (highest) nonce per transaction
472
    existing_changes = builder.accounts[address].nonce_changes
473
    for i, existing in enumerate(existing_changes):
474
        if existing.block_access_index == block_access_index:
475
            # Keep the highest nonce value
476
            if new_nonce > existing.new_nonce:
477
                existing_changes[i] = NonceChange(
478
                    block_access_index=block_access_index, new_nonce=new_nonce
479
                )
480
            return
481
482
    # No existing change for this tx_index, add a new one
483
    change = NonceChange(
484
        block_access_index=block_access_index, new_nonce=new_nonce
485
    )
486
    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:
495
    """
496
    Add a code change to the block access list.
497
498
    Records contract code deployment or modification. This typically occurs
499
    during contract creation via [`CREATE`], [`CREATE2`], or
500
    [`SetCodeTransaction`][sct] operations.
501
502
    [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create
503
    [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2
504
    [sct]: ref:ethereum.forks.amsterdam.transactions.SetCodeTransaction
505
    """
506
    ensure_account(builder, address)
507
508
    # Check if we already have a code change for this block_access_index
509
    # This handles the case of in-transaction selfdestructs where code is
510
    # first deployed and then cleared in the same transaction
511
    existing_changes = builder.accounts[address].code_changes
512
    for i, existing in enumerate(existing_changes):
513
        if existing.block_access_index == block_access_index:
514
            # Replace the existing code change with the new one
515
            # For selfdestructs, this ensures we only record the final
516
            # state (empty code)
517
            existing_changes[i] = CodeChange(
518
                block_access_index=block_access_index, new_code=new_code
519
            )
520
            return
521
522
    # No existing change for this block_access_index, add a new one
523
    change = CodeChange(
524
        block_access_index=block_access_index, new_code=new_code
525
    )
526
    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:
532
    """
533
    Add an account that was accessed but not modified.
534
535
    Records that an account was accessed during execution without any state
536
    changes. This is used for operations like [`EXTCODEHASH`], [`BALANCE`],
537
    [`EXTCODESIZE`], and [`EXTCODECOPY`] that read account data without
538
    modifying it.
539
540
    [`EXTCODEHASH`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodehash
541
    [`BALANCE`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.balance
542
    [`EXTCODESIZE`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodesize
543
    [`EXTCODECOPY`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodecopy
544
    """  # noqa: E501
545
    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:
551
    """
552
    Build the final [`BlockAccessList`] from a builder (internal helper).
553
554
    Constructs a deterministic block access list by sorting all accumulated
555
    changes. The resulting list is ordered by:
556
557
    1. Account addresses (lexicographically)
558
    2. Within each account:
559
       - Storage slots (lexicographically)
560
       - Transaction indices (numerically) for each change type
561
562
    Addresses, storage slots, and block access indices are unique.
563
    Storage reads that also appear in storage changes are excluded.
564
565
    [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.BlockAccessList
566
    """  # noqa: E501
567
    block_access_list: BlockAccessList = []
568
569
    for address, changes in builder.accounts.items():
570
        storage_changes = []
571
        for slot, slot_changes in changes.storage_changes.items():
572
            sorted_changes = tuple(
573
                sorted(slot_changes, key=lambda x: x.block_access_index)
574
            )
575
            storage_changes.append(
576
                SlotChanges(slot=slot, changes=sorted_changes)
577
            )
578
579
        storage_reads = []
580
        for slot in changes.storage_reads:
581
            if slot not in changes.storage_changes:
582
                storage_reads.append(slot)
583
584
        balance_changes = tuple(
585
            sorted(changes.balance_changes, key=lambda x: x.block_access_index)
586
        )
587
        nonce_changes = tuple(
588
            sorted(changes.nonce_changes, key=lambda x: x.block_access_index)
589
        )
590
        code_changes = tuple(
591
            sorted(changes.code_changes, key=lambda x: x.block_access_index)
592
        )
593
594
        storage_changes.sort(key=lambda x: x.slot)
595
        storage_reads.sort()
596
597
        account_change = AccountChanges(
598
            address=address,
599
            storage_changes=tuple(storage_changes),
600
            storage_reads=tuple(storage_reads),
601
            balance_changes=balance_changes,
602
            nonce_changes=nonce_changes,
603
            code_changes=code_changes,
604
        )
605
606
        block_access_list.append(account_change)
607
608
    block_access_list.sort(key=lambda x: x.address)
609
610
    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]:
618
    """
619
    Look up an account in cumulative state, falling back to `pre_state`.
620
621
    The cumulative account state (`pre_tx_accounts`) should contain state up
622
    to (but not including) the current transaction.
623
624
    Returns `None` if the `address` does not exist.
625
    """
626
    if address in pre_tx_accounts:
627
        return pre_tx_accounts[address]
628
    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:
637
    """
638
    Look up a storage value in cumulative state, falling back to `pre_state`.
639
640
    Returns `0` if not set.
641
    """
642
    if address in pre_tx_storage and key in pre_tx_storage[address]:
643
        return pre_tx_storage[address][key]
644
    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:
651
    """
652
    Update the BAL builder with changes from a single transaction.
653
654
    Compare the transaction's writes against the block's cumulative
655
    state (falling back to `pre_state`) to extract balance, nonce, code, and
656
    storage changes.  Net-zero filtering is automatic: if the pre-tx value
657
    equals the post-tx value, no change is recorded.
658
659
    Must be called **before** the transaction's writes are merged into
660
    the block state.
661
    """
662
    block_state = tx_state.parent
663
    pre_state = block_state.pre_state
664
    idx = builder.block_access_index
665
666
    # Compare account writes against block cumulative state
667
    for address, post_account in tx_state.account_writes.items():
668
        pre_account = _get_pre_tx_account(
669
            block_state.account_writes, pre_state, address
670
        )
671
672
        pre_balance = pre_account.balance if pre_account else U256(0)
673
        post_balance = post_account.balance if post_account else U256(0)
674
        if pre_balance != post_balance:
675
            add_balance_change(builder, address, idx, post_balance)
676
677
        pre_nonce = pre_account.nonce if pre_account else Uint(0)
678
        post_nonce = post_account.nonce if post_account else Uint(0)
679
        if pre_nonce != post_nonce:
680
            add_nonce_change(builder, address, idx, U64(post_nonce))
681
682
        pre_code_hash = (
683
            pre_account.code_hash if pre_account else EMPTY_CODE_HASH
684
        )
685
        post_code_hash = (
686
            post_account.code_hash if post_account else EMPTY_CODE_HASH
687
        )
688
        if pre_code_hash != post_code_hash:
689
            post_code = get_code(tx_state, post_code_hash)
690
            add_code_change(builder, address, idx, post_code)
691
692
    # Compare storage writes against block cumulative state
693
    for address, slots in tx_state.storage_writes.items():
694
        for key, post_value in slots.items():
695
            pre_value = _get_pre_tx_storage(
696
                block_state.storage_writes, pre_state, address, key
697
            )
698
            if pre_value != post_value:
699
                # Convert slot from internal Bytes32 format to U256 for BAL.
700
                # EIP-7928 uses U256 as it's more space-efficient in RLP.
701
                u256_slot = U256.from_be_bytes(key)
702
                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:
709
    """
710
    Build a [`BlockAccessList`] from the builder and block state.
711
712
    Feed accumulated reads from the block state into the builder, then produce
713
    the final sorted and encoded block access list.
714
715
    [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.BlockAccessList
716
    """  # noqa: E501
717
    # Add storage reads (convert Bytes32 to U256 for BAL encoding)
718
    for address, slot in block_state.storage_reads:
719
        add_storage_read(builder, address, U256.from_be_bytes(slot))
720
721
    # Add touched addresses
722
    for address in block_state.account_reads:
723
        add_touched_account(builder, address)
724
725
    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:
731
    """
732
    Compute the hash of a Block Access List.
733
    """
734
    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:
741
    """
742
    Validate that the block access list does not exceed the gas limit.
743
744
    The total number of items (addresses + unique storage keys) must not
745
    exceed ``block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM``.
746
    """
747
    from .vm.gas import GAS_BLOCK_ACCESS_LIST_ITEM
748
749
    bal_items = Uint(0)
750
    for account in block_access_list:
751
        # Count each address as one item
752
        bal_items += Uint(1)
753
754
        # Collect unique storage keys across both
755
        # reads and writes
756
        unique_slots: Set[U256] = set()
757
        for slot_change in account.storage_changes:
758
            unique_slots.add(slot_change.slot)
759
        for slot in account.storage_reads:
760
            unique_slots.add(slot)
761
762
        # Count each unique storage key as one item
763
        bal_items += Uint(len(unique_slots))
764
765
    if bal_items > block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM:
766
        raise BlockAccessListGasLimitExceededError(
767
            f"Block access list exceeds gas limit, {bal_items} items "
768
            f"exceeds limit of "
769
            f"{block_gas_limit // GAS_BLOCK_ACCESS_LIST_ITEM}."
770
        )