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:
Collection Phase: During transaction execution, all state accesses are recorded via the tracking functions.
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
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:
Account addresses (lexicographically)
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) |