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.
[BlockAccessList]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501
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.
| 38 | @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.
| 48 | storage_changes: Dict[U256, List[StorageChange]] = field( |
|---|---|
| 49 | default_factory=dict |
| 50 | ) |
storage_reads
Set of storage slots that were read but not modified.
| 56 | storage_reads: Set[U256] = field(default_factory=set) |
|---|
balance_changes
List of balance changes for this account, ordered by transaction index.
| 61 | balance_changes: List[BalanceChange] = field(default_factory=list) |
|---|
nonce_changes
List of nonce changes for this account, ordered by transaction index.
| 66 | nonce_changes: List[NonceChange] = field(default_factory=list) |
|---|
code_changes
List of code changes (contract deployments) for this account, ordered by transaction index.
| 71 | 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.
[BlockAccessList]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501
| 78 | @dataclass |
|---|
class BlockAccessListBuilder:
accounts
Mapping from account address to its tracked changes during block execution.
| 92 | 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.
Parameters
builder : The block access list builder instance. address : The account address to ensure exists.
[AccountData] :
ref:ethereum.forks.amsterdam.block_access_lists.builder.AccountData
def ensure_account(builder: BlockAccessListBuilder, address: Address) -> None:
| 99 | """ |
|---|---|
| 100 | Ensure an account exists in the builder's tracking structure. |
| 101 | |
| 102 | Creates an empty [`AccountData`] entry for the given address if it |
| 103 | doesn't already exist. This function is idempotent and safe to call |
| 104 | multiple times for the same address. |
| 105 | |
| 106 | Parameters |
| 107 | ---------- |
| 108 | builder : |
| 109 | The block access list builder instance. |
| 110 | address : |
| 111 | The account address to ensure exists. |
| 112 | |
| 113 | [`AccountData`] : |
| 114 | ref:ethereum.forks.amsterdam.block_access_lists.builder.AccountData |
| 115 | |
| 116 | """ |
| 117 | if address not in builder.accounts: |
| 118 | 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.
Parameters
builder : The block access list builder instance. address : The account address whose storage is being modified. slot : The storage slot being written to. block_access_index : The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). new_value : The new value being written to the storage slot.
def add_storage_write(builder: BlockAccessListBuilder, address: Address, slot: U256, block_access_index: BlockAccessIndex, new_value: U256) -> None:
| 128 | """ |
|---|---|
| 129 | Add a storage write operation to the block access list. |
| 130 | |
| 131 | Records a storage slot modification for a given address at a specific |
| 132 | transaction index. If multiple writes occur to the same slot within the |
| 133 | same transaction (same block_access_index), only the final value is kept. |
| 134 | |
| 135 | Parameters |
| 136 | ---------- |
| 137 | builder : |
| 138 | The block access list builder instance. |
| 139 | address : |
| 140 | The account address whose storage is being modified. |
| 141 | slot : |
| 142 | The storage slot being written to. |
| 143 | block_access_index : |
| 144 | The block access index for this change (0 for pre-execution, |
| 145 | 1..n for transactions, n+1 for post-execution). |
| 146 | new_value : |
| 147 | The new value being written to the storage slot. |
| 148 | |
| 149 | """ |
| 150 | ensure_account(builder, address) |
| 151 | |
| 152 | if slot not in builder.accounts[address].storage_changes: |
| 153 | builder.accounts[address].storage_changes[slot] = [] |
| 154 | |
| 155 | # Check if there's already an entry with the same block_access_index |
| 156 | # If so, update it with the new value, keeping only the final write |
| 157 | changes = builder.accounts[address].storage_changes[slot] |
| 158 | for i, existing_change in enumerate(changes): |
| 159 | if existing_change.block_access_index == block_access_index: |
| 160 | # Update the existing entry with the new value |
| 161 | changes[i] = StorageChange( |
| 162 | block_access_index=block_access_index, new_value=new_value |
| 163 | ) |
| 164 | return |
| 165 | |
| 166 | # No existing entry found, append new change |
| 167 | change = StorageChange( |
| 168 | block_access_index=block_access_index, new_value=new_value |
| 169 | ) |
| 170 | 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.
Parameters
builder : The block access list builder instance. address : The account address whose storage is being read. slot : The storage slot being read.
def add_storage_read(builder: BlockAccessListBuilder, address: Address, slot: U256) -> None:
| 176 | """ |
|---|---|
| 177 | Add a storage read operation to the block access list. |
| 178 | |
| 179 | Records that a storage slot was read during execution. Storage slots |
| 180 | that are both read and written will only appear in the storage changes |
| 181 | list, not in the storage reads list, as per [EIP-7928]. |
| 182 | |
| 183 | Parameters |
| 184 | ---------- |
| 185 | builder : |
| 186 | The block access list builder instance. |
| 187 | address : |
| 188 | The account address whose storage is being read. |
| 189 | slot : |
| 190 | The storage slot being read. |
| 191 | |
| 192 | [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928 |
| 193 | |
| 194 | """ |
| 195 | ensure_account(builder, address) |
| 196 | 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.
Parameters
builder : The block access list builder instance. address : The account address whose balance changed. block_access_index : The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). post_balance : The account balance after the change as U256.
def add_balance_change(builder: BlockAccessListBuilder, address: Address, block_access_index: BlockAccessIndex, post_balance: U256) -> None:
| 205 | """ |
|---|---|
| 206 | Add a balance change to the block access list. |
| 207 | |
| 208 | Records the post-transaction balance for an account after it has been |
| 209 | modified. This includes changes from transfers, gas fees, block rewards, |
| 210 | and any other balance-affecting operations. |
| 211 | |
| 212 | Parameters |
| 213 | ---------- |
| 214 | builder : |
| 215 | The block access list builder instance. |
| 216 | address : |
| 217 | The account address whose balance changed. |
| 218 | block_access_index : |
| 219 | The block access index for this change (0 for pre-execution, |
| 220 | 1..n for transactions, n+1 for post-execution). |
| 221 | post_balance : |
| 222 | The account balance after the change as U256. |
| 223 | |
| 224 | """ |
| 225 | ensure_account(builder, address) |
| 226 | |
| 227 | # Balance value is already U256 |
| 228 | balance_value = post_balance |
| 229 | |
| 230 | # Check if we already have a balance change for this tx_index and update it |
| 231 | # This ensures we only track the final balance per transaction |
| 232 | existing_changes = builder.accounts[address].balance_changes |
| 233 | for i, existing in enumerate(existing_changes): |
| 234 | if existing.block_access_index == block_access_index: |
| 235 | # Update the existing balance change with the new balance |
| 236 | existing_changes[i] = BalanceChange( |
| 237 | block_access_index=block_access_index, |
| 238 | post_balance=balance_value, |
| 239 | ) |
| 240 | return |
| 241 | |
| 242 | # No existing change for this tx_index, add a new one |
| 243 | change = BalanceChange( |
| 244 | block_access_index=block_access_index, post_balance=balance_value |
| 245 | ) |
| 246 | 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.
Parameters
builder : The block access list builder instance. address : The account address whose nonce changed. block_access_index : The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). new_nonce : The new nonce value after the change.
def add_nonce_change(builder: BlockAccessListBuilder, address: Address, block_access_index: BlockAccessIndex, new_nonce: U64) -> None:
| 255 | """ |
|---|---|
| 256 | Add a nonce change to the block access list. |
| 257 | |
| 258 | Records a nonce increment for an account. This occurs when an EOA sends |
| 259 | a transaction or when a contract performs [`CREATE`] or [`CREATE2`] |
| 260 | operations. |
| 261 | |
| 262 | Parameters |
| 263 | ---------- |
| 264 | builder : |
| 265 | The block access list builder instance. |
| 266 | address : |
| 267 | The account address whose nonce changed. |
| 268 | block_access_index : |
| 269 | The block access index for this change (0 for pre-execution, |
| 270 | 1..n for transactions, n+1 for post-execution). |
| 271 | new_nonce : |
| 272 | The new nonce value after the change. |
| 273 | |
| 274 | [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create |
| 275 | [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 |
| 276 | |
| 277 | """ |
| 278 | ensure_account(builder, address) |
| 279 | |
| 280 | # Check if we already have a nonce change for this tx_index and update it |
| 281 | # This ensures we only track the final (highest) nonce per transaction |
| 282 | existing_changes = builder.accounts[address].nonce_changes |
| 283 | for i, existing in enumerate(existing_changes): |
| 284 | if existing.block_access_index == block_access_index: |
| 285 | # Keep the highest nonce value |
| 286 | if new_nonce > existing.new_nonce: |
| 287 | existing_changes[i] = NonceChange( |
| 288 | block_access_index=block_access_index, new_nonce=new_nonce |
| 289 | ) |
| 290 | return |
| 291 | |
| 292 | # No existing change for this tx_index, add a new one |
| 293 | change = NonceChange( |
| 294 | block_access_index=block_access_index, new_nonce=new_nonce |
| 295 | ) |
| 296 | 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 [SETCODE]
operations.
Parameters
builder : The block access list builder instance. address : The account address receiving new code. block_access_index : The block access index for this change (0 for pre-execution, 1..n for transactions, n+1 for post-execution). new_code : The deployed contract bytecode.
def add_code_change(builder: BlockAccessListBuilder, address: Address, block_access_index: BlockAccessIndex, new_code: Bytes) -> None:
| 305 | """ |
|---|---|
| 306 | Add a code change to the block access list. |
| 307 | |
| 308 | Records contract code deployment or modification. This typically occurs |
| 309 | during contract creation via [`CREATE`], [`CREATE2`], or [`SETCODE`] |
| 310 | operations. |
| 311 | |
| 312 | Parameters |
| 313 | ---------- |
| 314 | builder : |
| 315 | The block access list builder instance. |
| 316 | address : |
| 317 | The account address receiving new code. |
| 318 | block_access_index : |
| 319 | The block access index for this change (0 for pre-execution, |
| 320 | 1..n for transactions, n+1 for post-execution). |
| 321 | new_code : |
| 322 | The deployed contract bytecode. |
| 323 | |
| 324 | [`CREATE`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create |
| 325 | [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2 |
| 326 | |
| 327 | """ |
| 328 | ensure_account(builder, address) |
| 329 | |
| 330 | # Check if we already have a code change for this block_access_index |
| 331 | # This handles the case of in-transaction selfdestructs where code is |
| 332 | # first deployed and then cleared in the same transaction |
| 333 | existing_changes = builder.accounts[address].code_changes |
| 334 | for i, existing in enumerate(existing_changes): |
| 335 | if existing.block_access_index == block_access_index: |
| 336 | # Replace the existing code change with the new one |
| 337 | # For selfdestructs, this ensures we only record the final state (empty code) |
| 338 | existing_changes[i] = CodeChange( |
| 339 | block_access_index=block_access_index, new_code=new_code |
| 340 | ) |
| 341 | return |
| 342 | |
| 343 | # No existing change for this block_access_index, add a new one |
| 344 | change = CodeChange( |
| 345 | block_access_index=block_access_index, new_code=new_code |
| 346 | ) |
| 347 | 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.
Parameters
builder : The block access list builder instance. address : The account address that was accessed.
[EXTCODEHASH] :
ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodehash
[BALANCE] :
ref:ethereum.forks.amsterdam.vm.instructions.environment.balance
[EXTCODESIZE] :
ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodesize
[EXTCODECOPY] :
ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodecopy
def add_touched_account(builder: BlockAccessListBuilder, address: Address) -> None:
| 353 | """ |
|---|---|
| 354 | Add an account that was accessed but not modified. |
| 355 | |
| 356 | Records that an account was accessed during execution without any state |
| 357 | changes. This is used for operations like [`EXTCODEHASH`], [`BALANCE`], |
| 358 | [`EXTCODESIZE`], and [`EXTCODECOPY`] that read account data without |
| 359 | modifying it. |
| 360 | |
| 361 | Parameters |
| 362 | ---------- |
| 363 | builder : |
| 364 | The block access list builder instance. |
| 365 | address : |
| 366 | The account address that was accessed. |
| 367 | |
| 368 | [`EXTCODEHASH`] : |
| 369 | ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodehash |
| 370 | [`BALANCE`] : |
| 371 | ref:ethereum.forks.amsterdam.vm.instructions.environment.balance |
| 372 | [`EXTCODESIZE`] : |
| 373 | ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodesize |
| 374 | [`EXTCODECOPY`] : |
| 375 | ref:ethereum.forks.amsterdam.vm.instructions.environment.extcodecopy |
| 376 | |
| 377 | """ |
| 378 | 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
Parameters
builder : The block access list builder containing all tracked changes.
Returns
block_access_list : The final sorted and encoded block access list.
[BlockAccessList]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501
def _build_from_builder(builder: BlockAccessListBuilder) -> BlockAccessList:
| 384 | """ |
|---|---|
| 385 | Build the final [`BlockAccessList`] from a builder (internal helper). |
| 386 | |
| 387 | Constructs a deterministic block access list by sorting all accumulated |
| 388 | changes. The resulting list is ordered by: |
| 389 | |
| 390 | 1. Account addresses (lexicographically) |
| 391 | 2. Within each account: |
| 392 | - Storage slots (lexicographically) |
| 393 | - Transaction indices (numerically) for each change type |
| 394 | |
| 395 | Parameters |
| 396 | ---------- |
| 397 | builder : |
| 398 | The block access list builder containing all tracked changes. |
| 399 | |
| 400 | Returns |
| 401 | ------- |
| 402 | block_access_list : |
| 403 | The final sorted and encoded block access list. |
| 404 | |
| 405 | [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 |
| 406 | |
| 407 | """ |
| 408 | block_access_list: BlockAccessList = [] |
| 409 | |
| 410 | for address, changes in builder.accounts.items(): |
| 411 | storage_changes = [] |
| 412 | for slot, slot_changes in changes.storage_changes.items(): |
| 413 | sorted_changes = tuple( |
| 414 | sorted(slot_changes, key=lambda x: x.block_access_index) |
| 415 | ) |
| 416 | storage_changes.append( |
| 417 | SlotChanges(slot=slot, changes=sorted_changes) |
| 418 | ) |
| 419 | |
| 420 | storage_reads = [] |
| 421 | for slot in changes.storage_reads: |
| 422 | if slot not in changes.storage_changes: |
| 423 | storage_reads.append(slot) |
| 424 | |
| 425 | balance_changes = tuple( |
| 426 | sorted(changes.balance_changes, key=lambda x: x.block_access_index) |
| 427 | ) |
| 428 | nonce_changes = tuple( |
| 429 | sorted(changes.nonce_changes, key=lambda x: x.block_access_index) |
| 430 | ) |
| 431 | code_changes = tuple( |
| 432 | sorted(changes.code_changes, key=lambda x: x.block_access_index) |
| 433 | ) |
| 434 | |
| 435 | storage_changes.sort(key=lambda x: x.slot) |
| 436 | storage_reads.sort() |
| 437 | |
| 438 | account_change = AccountChanges( |
| 439 | address=address, |
| 440 | storage_changes=tuple(storage_changes), |
| 441 | storage_reads=tuple(storage_reads), |
| 442 | balance_changes=balance_changes, |
| 443 | nonce_changes=nonce_changes, |
| 444 | code_changes=code_changes, |
| 445 | ) |
| 446 | |
| 447 | block_access_list.append(account_change) |
| 448 | |
| 449 | block_access_list.sort(key=lambda x: x.address) |
| 450 | |
| 451 | return block_access_list |
build_block_access_list
Build a [BlockAccessList] from a StateChanges frame.
Converts the accumulated state changes from the frame-based architecture into the final deterministic BlockAccessList format.
Parameters
state_changes : The block-level StateChanges frame containing all changes from the block.
Returns
block_access_list : The final sorted and encoded block access list.
[BlockAccessList]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501
[StateChanges]: ref:ethereum.forks.amsterdam.state_tracker.StateChanges
def build_block_access_list(state_changes: "StateChanges") -> BlockAccessList:
| 457 | """ |
|---|---|
| 458 | Build a [`BlockAccessList`] from a StateChanges frame. |
| 459 | |
| 460 | Converts the accumulated state changes from the frame-based architecture |
| 461 | into the final deterministic BlockAccessList format. |
| 462 | |
| 463 | Parameters |
| 464 | ---------- |
| 465 | state_changes : |
| 466 | The block-level StateChanges frame containing all changes from the block. |
| 467 | |
| 468 | Returns |
| 469 | ------- |
| 470 | block_access_list : |
| 471 | The final sorted and encoded block access list. |
| 472 | |
| 473 | [`BlockAccessList`]: ref:ethereum.forks.amsterdam.block_access_lists.rlp_types.BlockAccessList # noqa: E501 |
| 474 | [`StateChanges`]: ref:ethereum.forks.amsterdam.state_tracker.StateChanges |
| 475 | |
| 476 | """ |
| 477 | builder = BlockAccessListBuilder() |
| 478 | |
| 479 | # Add all touched addresses |
| 480 | for address in state_changes.touched_addresses: |
| 481 | add_touched_account(builder, address) |
| 482 | |
| 483 | # Add all storage reads |
| 484 | for address, slot in state_changes.storage_reads: |
| 485 | add_storage_read(builder, address, U256(int.from_bytes(slot))) |
| 486 | |
| 487 | # Add all storage writes |
| 488 | # Net-zero filtering happens at transaction commit time, not here. |
| 489 | # At block level, we track ALL writes at their respective indices. |
| 490 | for ( |
| 491 | address, |
| 492 | slot, |
| 493 | block_access_index, |
| 494 | ), value in state_changes.storage_writes.items(): |
| 495 | u256_slot = U256(int.from_bytes(slot)) |
| 496 | add_storage_write( |
| 497 | builder, address, u256_slot, block_access_index, value |
| 498 | ) |
| 499 | |
| 500 | # Add all balance changes (balance_changes is keyed by (address, index)) |
| 501 | for ( |
| 502 | address, |
| 503 | block_access_index, |
| 504 | ), new_balance in state_changes.balance_changes.items(): |
| 505 | add_balance_change(builder, address, block_access_index, new_balance) |
| 506 | |
| 507 | # Add all nonce changes |
| 508 | for address, block_access_index, new_nonce in state_changes.nonce_changes: |
| 509 | add_nonce_change(builder, address, block_access_index, new_nonce) |
| 510 | |
| 511 | # Add all code changes |
| 512 | # Filtering happens at transaction level in eoa_delegation.py |
| 513 | for ( |
| 514 | address, |
| 515 | block_access_index, |
| 516 | ), new_code in state_changes.code_changes.items(): |
| 517 | add_code_change(builder, address, block_access_index, new_code) |
| 518 | |
| 519 | return _build_from_builder(builder) |