ethereum.forks.amsterdam.block_access_lists.builder

Implements the Block Access List builder that tracks all account and storage accesses during block execution and constructs the final BlockAccessList.

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.

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.

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

47
    storage_changes: Dict[U256, List[StorageChange]] = field(
48
        default_factory=dict
49
    )

storage_reads

Set of storage slots that were read but not modified.

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

balance_changes

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

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

nonce_changes

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

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

code_changes

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

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

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

91
    block_access_index: BlockAccessIndex = BlockAccessIndex(0)

accounts

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

100
    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:
107
    """
108
    Ensure an account exists in the builder's tracking structure.
109
110
    Creates an empty [`AccountData`] entry for the given address if it
111
    doesn't already exist. This function is idempotent and safe to call
112
    multiple times for the same address.
113
114
    [`AccountData`]: ref:ethereum.forks.amsterdam.block_access_lists.builder.AccountData
115
    """  # noqa: E501
116
    if address not in builder.accounts:
117
        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:
127
    """
128
    Add a storage write operation to the block access list.
129
130
    Records a storage slot modification for a given address at a specific
131
    transaction index. If multiple writes occur to the same slot within the
132
    same transaction (same `block_access_index`), only the final value is kept.
133
    """
134
    ensure_account(builder, address)
135
136
    if slot not in builder.accounts[address].storage_changes:
137
        builder.accounts[address].storage_changes[slot] = []
138
139
    # Check if there's already an entry with the same block_access_index
140
    # If so, update it with the new value, keeping only the final write
141
    changes = builder.accounts[address].storage_changes[slot]
142
    for i, existing_change in enumerate(changes):
143
        if existing_change.block_access_index == block_access_index:
144
            # Update the existing entry with the new value
145
            changes[i] = StorageChange(
146
                block_access_index=block_access_index, new_value=new_value
147
            )
148
            return
149
150
    # No existing entry found, append new change
151
    change = StorageChange(
152
        block_access_index=block_access_index, new_value=new_value
153
    )
154
    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:
160
    """
161
    Add a storage read operation to the block access list.
162
163
    Records that a storage slot was read during execution. Storage slots
164
    that are both read and written will only appear in the storage changes
165
    list, not in the storage reads list, as per [EIP-7928].
166
    """
167
    ensure_account(builder, address)
168
    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:
177
    """
178
    Add a balance change to the block access list.
179
180
    Records the post-transaction balance for an account after it has been
181
    modified. This includes changes from transfers, gas fees, block rewards,
182
    and any other balance-affecting operations.
183
    """
184
    ensure_account(builder, address)
185
186
    # Balance value is already U256
187
    balance_value = post_balance
188
189
    # Check if we already have a balance change for this tx_index and update it
190
    # This ensures we only track the final balance per transaction
191
    existing_changes = builder.accounts[address].balance_changes
192
    for i, existing in enumerate(existing_changes):
193
        if existing.block_access_index == block_access_index:
194
            # Update the existing balance change with the new balance
195
            existing_changes[i] = BalanceChange(
196
                block_access_index=block_access_index,
197
                post_balance=balance_value,
198
            )
199
            return
200
201
    # No existing change for this tx_index, add a new one
202
    change = BalanceChange(
203
        block_access_index=block_access_index, post_balance=balance_value
204
    )
205
    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:
214
    """
215
    Add a nonce change to the block access list.
216
217
    Records a nonce increment for an account. This occurs when an EOA sends
218
    a transaction or when a contract performs [`CREATE`] or [`CREATE2`]
219
    operations.
220
221
    [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create
222
    [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2
223
    """
224
    ensure_account(builder, address)
225
226
    # Check if we already have a nonce change for this tx_index and update it
227
    # This ensures we only track the final (highest) nonce per transaction
228
    existing_changes = builder.accounts[address].nonce_changes
229
    for i, existing in enumerate(existing_changes):
230
        if existing.block_access_index == block_access_index:
231
            # Keep the highest nonce value
232
            if new_nonce > existing.new_nonce:
233
                existing_changes[i] = NonceChange(
234
                    block_access_index=block_access_index, new_nonce=new_nonce
235
                )
236
            return
237
238
    # No existing change for this tx_index, add a new one
239
    change = NonceChange(
240
        block_access_index=block_access_index, new_nonce=new_nonce
241
    )
242
    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:
251
    """
252
    Add a code change to the block access list.
253
254
    Records contract code deployment or modification. This typically occurs
255
    during contract creation via [`CREATE`], [`CREATE2`], or
256
    [`SetCodeTransaction`][sct] operations.
257
258
    [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create
259
    [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2
260
    [sct]: ref:ethereum.forks.amsterdam.transactions.SetCodeTransaction
261
    """
262
    ensure_account(builder, address)
263
264
    # Check if we already have a code change for this block_access_index
265
    # This handles the case of in-transaction selfdestructs where code is
266
    # first deployed and then cleared in the same transaction
267
    existing_changes = builder.accounts[address].code_changes
268
    for i, existing in enumerate(existing_changes):
269
        if existing.block_access_index == block_access_index:
270
            # Replace the existing code change with the new one
271
            # For selfdestructs, this ensures we only record the final
272
            # state (empty code)
273
            existing_changes[i] = CodeChange(
274
                block_access_index=block_access_index, new_code=new_code
275
            )
276
            return
277
278
    # No existing change for this block_access_index, add a new one
279
    change = CodeChange(
280
        block_access_index=block_access_index, new_code=new_code
281
    )
282
    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:
288
    """
289
    Add an account that was accessed but not modified.
290
291
    Records that an account was accessed during execution without any state
292
    changes. This is used for operations like [`EXTCODEHASH`], [`BALANCE`],
293
    [`EXTCODESIZE`], and [`EXTCODECOPY`] that read account data without
294
    modifying it.
295
296
    [`EXTCODEHASH`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodehash
297
    [`BALANCE`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.balance
298
    [`EXTCODESIZE`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodesize
299
    [`EXTCODECOPY`]: ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodecopy
300
    """  # noqa: E501
301
    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

def _build_from_builder(builder: BlockAccessListBuilder) -> BlockAccessList:
307
    """
308
    Build the final [`BlockAccessList`] from a builder (internal helper).
309
310
    Constructs a deterministic block access list by sorting all accumulated
311
    changes. The resulting list is ordered by:
312
313
    1. Account addresses (lexicographically)
314
    2. Within each account:
315
       - Storage slots (lexicographically)
316
       - Transaction indices (numerically) for each change type
317
318
    [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList
319
    """  # noqa: E501
320
    block_access_list: BlockAccessList = []
321
322
    for address, changes in builder.accounts.items():
323
        storage_changes = []
324
        for slot, slot_changes in changes.storage_changes.items():
325
            sorted_changes = tuple(
326
                sorted(slot_changes, key=lambda x: x.block_access_index)
327
            )
328
            storage_changes.append(
329
                SlotChanges(slot=slot, changes=sorted_changes)
330
            )
331
332
        storage_reads = []
333
        for slot in changes.storage_reads:
334
            if slot not in changes.storage_changes:
335
                storage_reads.append(slot)
336
337
        balance_changes = tuple(
338
            sorted(changes.balance_changes, key=lambda x: x.block_access_index)
339
        )
340
        nonce_changes = tuple(
341
            sorted(changes.nonce_changes, key=lambda x: x.block_access_index)
342
        )
343
        code_changes = tuple(
344
            sorted(changes.code_changes, key=lambda x: x.block_access_index)
345
        )
346
347
        storage_changes.sort(key=lambda x: x.slot)
348
        storage_reads.sort()
349
350
        account_change = AccountChanges(
351
            address=address,
352
            storage_changes=tuple(storage_changes),
353
            storage_reads=tuple(storage_reads),
354
            balance_changes=balance_changes,
355
            nonce_changes=nonce_changes,
356
            code_changes=code_changes,
357
        )
358
359
        block_access_list.append(account_change)
360
361
    block_access_list.sort(key=lambda x: x.address)
362
363
    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]:
371
    """
372
    Look up an account in cumulative state, falling back to `pre_state`.
373
374
    The cumulative account state (`pre_tx_accounts`) should contain state up
375
    to (but not including) the current transaction.
376
377
    Returns `None` if the `address` does not exist.
378
    """
379
    if address in pre_tx_accounts:
380
        return pre_tx_accounts[address]
381
    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:
390
    """
391
    Look up a storage value in cumulative state, falling back to `pre_state`.
392
393
    Returns `0` if not set.
394
    """
395
    if address in pre_tx_storage and key in pre_tx_storage[address]:
396
        return pre_tx_storage[address][key]
397
    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:
404
    """
405
    Update the BAL builder with changes from a single transaction.
406
407
    Compare the transaction's writes against the block's cumulative
408
    state (falling back to `pre_state`) to extract balance, nonce, code, and
409
    storage changes.  Net-zero filtering is automatic: if the pre-tx value
410
    equals the post-tx value, no change is recorded.
411
412
    Must be called **before** the transaction's writes are merged into
413
    the block state.
414
    """
415
    block_state = tx_state.parent
416
    pre_state = block_state.pre_state
417
    idx = builder.block_access_index
418
419
    # Compare account writes against block cumulative state
420
    for address, post_account in tx_state.account_writes.items():
421
        pre_account = _get_pre_tx_account(
422
            block_state.account_writes, pre_state, address
423
        )
424
425
        pre_balance = pre_account.balance if pre_account else U256(0)
426
        post_balance = post_account.balance if post_account else U256(0)
427
        if pre_balance != post_balance:
428
            add_balance_change(builder, address, idx, post_balance)
429
430
        pre_nonce = pre_account.nonce if pre_account else Uint(0)
431
        post_nonce = post_account.nonce if post_account else Uint(0)
432
        if pre_nonce != post_nonce:
433
            add_nonce_change(builder, address, idx, U64(post_nonce))
434
435
        pre_code = pre_account.code if pre_account else b""
436
        post_code = post_account.code if post_account else b""
437
        if pre_code != post_code:
438
            add_code_change(builder, address, idx, post_code)
439
440
    # Compare storage writes against block cumulative state
441
    for address, slots in tx_state.storage_writes.items():
442
        for key, post_value in slots.items():
443
            pre_value = _get_pre_tx_storage(
444
                block_state.storage_writes, pre_state, address, key
445
            )
446
            if pre_value != post_value:
447
                # Convert slot from internal Bytes32 format to U256 for BAL.
448
                # EIP-7928 uses U256 as it's more space-efficient in RLP.
449
                u256_slot = U256.from_be_bytes(key)
450
                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:
457
    """
458
    Build a [`BlockAccessList`] from the builder and block state.
459
460
    Feed accumulated reads from the block state into the builder, then produce
461
    the final sorted and encoded block access list.
462
463
    [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList
464
    """  # noqa: E501
465
    # Add storage reads (convert Bytes32 to U256 for BAL encoding)
466
    for address, slot in block_state.storage_reads:
467
        add_storage_read(builder, address, U256.from_be_bytes(slot))
468
469
    # Add touched addresses
470
    for address in block_state.account_reads:
471
        add_touched_account(builder, address)
472
473
    return _build_from_builder(builder)