ethereum.forks.amsterdam.state_tracker
EIP-7928 Block Access Lists: Hierarchical State Change Tracking.
Frame hierarchy mirrors EVM execution: Block -> Transaction -> Call frames. Each frame tracks state accesses and merges to parent on completion.
On success, changes merge upward with net-zero filtering (pre-state vs final). On failure, only reads merge (writes discarded). Pre-state captures use first-write-wins semantics and are stored at the transaction frame level.
StateChanges
Tracks state changes within a single execution frame.
Frames form a hierarchy (Block -> Transaction -> Call) linked by parent references. The block_access_index is stored at the root frame. Pre-state captures (pre_balances, etc.) are only populated at the transaction level.
| 24 | @dataclass |
|---|
class StateChanges:
parent
| 34 | parent: Optional["StateChanges"] = None |
|---|
block_access_index
| 35 | block_access_index: BlockAccessIndex = BlockAccessIndex(0) |
|---|
touched_addresses
| 37 | touched_addresses: Set[Address] = field(default_factory=set) |
|---|
storage_reads
| 38 | storage_reads: Set[Tuple[Address, Bytes32]] = field(default_factory=set) |
|---|
storage_writes
| 39 | storage_writes: Dict[Tuple[Address, Bytes32, BlockAccessIndex], U256] = ( |
|---|---|
| 40 | field(default_factory=dict) |
| 41 | ) |
balance_changes
| 43 | balance_changes: Dict[Tuple[Address, BlockAccessIndex], U256] = field( |
|---|---|
| 44 | default_factory=dict |
| 45 | ) |
nonce_changes
| 46 | nonce_changes: Set[Tuple[Address, BlockAccessIndex, U64]] = field( |
|---|---|
| 47 | default_factory=set |
| 48 | ) |
code_changes
| 49 | code_changes: Dict[Tuple[Address, BlockAccessIndex], Bytes] = field( |
|---|---|
| 50 | default_factory=dict |
| 51 | ) |
pre_balances
| 54 | pre_balances: Dict[Address, U256] = field(default_factory=dict) |
|---|
pre_storage
| 55 | pre_storage: Dict[Tuple[Address, Bytes32], U256] = field( |
|---|---|
| 56 | default_factory=dict |
| 57 | ) |
pre_code
| 58 | pre_code: Dict[Address, Bytes] = field(default_factory=dict) |
|---|
get_block_frame
Walk to the root (block-level) frame.
Parameters
state_changes : Any frame in the hierarchy.
Returns
block_frame : StateChanges The root block-level frame.
def get_block_frame(state_changes: StateChanges) -> StateChanges:
| 62 | """ |
|---|---|
| 63 | Walk to the root (block-level) frame. |
| 64 | |
| 65 | Parameters |
| 66 | ---------- |
| 67 | state_changes : |
| 68 | Any frame in the hierarchy. |
| 69 | |
| 70 | Returns |
| 71 | ------- |
| 72 | block_frame : StateChanges |
| 73 | The root block-level frame. |
| 74 | |
| 75 | """ |
| 76 | block_frame = state_changes |
| 77 | while block_frame.parent is not None: |
| 78 | block_frame = block_frame.parent |
| 79 | return block_frame |
increment_block_access_index
Increment the block access index in the root frame.
Parameters
root_frame : The root block-level frame.
def increment_block_access_index(root_frame: StateChanges) -> None:
| 83 | """ |
|---|---|
| 84 | Increment the block access index in the root frame. |
| 85 | |
| 86 | Parameters |
| 87 | ---------- |
| 88 | root_frame : |
| 89 | The root block-level frame. |
| 90 | |
| 91 | """ |
| 92 | root_frame.block_access_index = BlockAccessIndex( |
| 93 | root_frame.block_access_index + Uint(1) |
| 94 | ) |
get_transaction_frame
Walk to the transaction-level frame (child of block frame).
Parameters
state_changes : Any frame in the hierarchy.
Returns
tx_frame : StateChanges The transaction-level frame.
def get_transaction_frame(state_changes: StateChanges) -> StateChanges:
| 98 | """ |
|---|---|
| 99 | Walk to the transaction-level frame (child of block frame). |
| 100 | |
| 101 | Parameters |
| 102 | ---------- |
| 103 | state_changes : |
| 104 | Any frame in the hierarchy. |
| 105 | |
| 106 | Returns |
| 107 | ------- |
| 108 | tx_frame : StateChanges |
| 109 | The transaction-level frame. |
| 110 | |
| 111 | """ |
| 112 | tx_frame = state_changes |
| 113 | while tx_frame.parent is not None and tx_frame.parent.parent is not None: |
| 114 | tx_frame = tx_frame.parent |
| 115 | return tx_frame |
capture_pre_balance
Capture pre-balance if not already captured (first-write-wins).
Parameters
tx_frame : The transaction-level frame. address : The address whose balance to capture. balance : The current balance value.
def capture_pre_balance(tx_frame: StateChanges, address: Address, balance: U256) -> None:
| 121 | """ |
|---|---|
| 122 | Capture pre-balance if not already captured (first-write-wins). |
| 123 | |
| 124 | Parameters |
| 125 | ---------- |
| 126 | tx_frame : |
| 127 | The transaction-level frame. |
| 128 | address : |
| 129 | The address whose balance to capture. |
| 130 | balance : |
| 131 | The current balance value. |
| 132 | |
| 133 | """ |
| 134 | # Only capture pre-values in a transaction level |
| 135 | # or block level frame |
| 136 | assert tx_frame.parent is None or tx_frame.parent.parent is None |
| 137 | if address not in tx_frame.pre_balances: |
| 138 | tx_frame.pre_balances[address] = balance |
capture_pre_storage
Capture pre-storage value if not already captured (first-write-wins).
Parameters
tx_frame : The transaction-level frame. address : The address whose storage to capture. key : The storage key. value : The current storage value.
def capture_pre_storage(tx_frame: StateChanges, address: Address, key: Bytes32, value: U256) -> None:
| 144 | """ |
|---|---|
| 145 | Capture pre-storage value if not already captured (first-write-wins). |
| 146 | |
| 147 | Parameters |
| 148 | ---------- |
| 149 | tx_frame : |
| 150 | The transaction-level frame. |
| 151 | address : |
| 152 | The address whose storage to capture. |
| 153 | key : |
| 154 | The storage key. |
| 155 | value : |
| 156 | The current storage value. |
| 157 | |
| 158 | """ |
| 159 | # Only capture pre-values in a transaction level |
| 160 | # or block level frame |
| 161 | assert tx_frame.parent is None or tx_frame.parent.parent is None |
| 162 | slot = (address, key) |
| 163 | if slot not in tx_frame.pre_storage: |
| 164 | tx_frame.pre_storage[slot] = value |
capture_pre_code
Capture pre-code if not already captured (first-write-wins).
Parameters
tx_frame : The transaction-level frame. address : The address whose code to capture. code : The current code value.
def capture_pre_code(tx_frame: StateChanges, address: Address, code: Bytes) -> None:
| 170 | """ |
|---|---|
| 171 | Capture pre-code if not already captured (first-write-wins). |
| 172 | |
| 173 | Parameters |
| 174 | ---------- |
| 175 | tx_frame : |
| 176 | The transaction-level frame. |
| 177 | address : |
| 178 | The address whose code to capture. |
| 179 | code : |
| 180 | The current code value. |
| 181 | |
| 182 | """ |
| 183 | # Only capture pre-values in a transaction level |
| 184 | # or block level frame |
| 185 | assert tx_frame.parent is None or tx_frame.parent.parent is None |
| 186 | if address not in tx_frame.pre_code: |
| 187 | tx_frame.pre_code[address] = code |
track_address
Record that an address was accessed.
Parameters
state_changes : The state changes frame. address : The address that was accessed.
def track_address(state_changes: StateChanges, address: Address) -> None:
| 191 | """ |
|---|---|
| 192 | Record that an address was accessed. |
| 193 | |
| 194 | Parameters |
| 195 | ---------- |
| 196 | state_changes : |
| 197 | The state changes frame. |
| 198 | address : |
| 199 | The address that was accessed. |
| 200 | |
| 201 | """ |
| 202 | state_changes.touched_addresses.add(address) |
track_storage_read
Record a storage read operation.
Parameters
state_changes : The state changes frame. address : The address whose storage was read. key : The storage key that was read.
def track_storage_read(state_changes: StateChanges, address: Address, key: Bytes32) -> None:
| 208 | """ |
|---|---|
| 209 | Record a storage read operation. |
| 210 | |
| 211 | Parameters |
| 212 | ---------- |
| 213 | state_changes : |
| 214 | The state changes frame. |
| 215 | address : |
| 216 | The address whose storage was read. |
| 217 | key : |
| 218 | The storage key that was read. |
| 219 | |
| 220 | """ |
| 221 | state_changes.storage_reads.add((address, key)) |
track_storage_write
Record a storage write keyed by (address, key, block_access_index).
Parameters
state_changes : The state changes frame. address : The address whose storage was written. key : The storage key that was written. value : The new storage value.
def track_storage_write(state_changes: StateChanges, address: Address, key: Bytes32, value: U256) -> None:
| 230 | """ |
|---|---|
| 231 | Record a storage write keyed by (address, key, block_access_index). |
| 232 | |
| 233 | Parameters |
| 234 | ---------- |
| 235 | state_changes : |
| 236 | The state changes frame. |
| 237 | address : |
| 238 | The address whose storage was written. |
| 239 | key : |
| 240 | The storage key that was written. |
| 241 | value : |
| 242 | The new storage value. |
| 243 | |
| 244 | """ |
| 245 | idx = state_changes.block_access_index |
| 246 | state_changes.storage_writes[(address, key, idx)] = value |
track_balance_change
Record a balance change keyed by (address, block_access_index).
Parameters
state_changes : The state changes frame. address : The address whose balance changed. new_balance : The new balance value.
def track_balance_change(state_changes: StateChanges, address: Address, new_balance: U256) -> None:
| 254 | """ |
|---|---|
| 255 | Record a balance change keyed by (address, block_access_index). |
| 256 | |
| 257 | Parameters |
| 258 | ---------- |
| 259 | state_changes : |
| 260 | The state changes frame. |
| 261 | address : |
| 262 | The address whose balance changed. |
| 263 | new_balance : |
| 264 | The new balance value. |
| 265 | |
| 266 | """ |
| 267 | idx = state_changes.block_access_index |
| 268 | state_changes.balance_changes[(address, idx)] = new_balance |
track_nonce_change
Record a nonce change as (address, block_access_index, new_nonce).
Parameters
state_changes : The state changes frame. address : The address whose nonce changed. new_nonce : The new nonce value.
def track_nonce_change(state_changes: StateChanges, address: Address, new_nonce: U64) -> None:
| 276 | """ |
|---|---|
| 277 | Record a nonce change as (address, block_access_index, new_nonce). |
| 278 | |
| 279 | Parameters |
| 280 | ---------- |
| 281 | state_changes : |
| 282 | The state changes frame. |
| 283 | address : |
| 284 | The address whose nonce changed. |
| 285 | new_nonce : |
| 286 | The new nonce value. |
| 287 | |
| 288 | """ |
| 289 | idx = state_changes.block_access_index |
| 290 | state_changes.nonce_changes.add((address, idx, new_nonce)) |
track_code_change
Record a code change keyed by (address, block_access_index).
Parameters
state_changes : The state changes frame. address : The address whose code changed. new_code : The new code value.
def track_code_change(state_changes: StateChanges, address: Address, new_code: Bytes) -> None:
| 298 | """ |
|---|---|
| 299 | Record a code change keyed by (address, block_access_index). |
| 300 | |
| 301 | Parameters |
| 302 | ---------- |
| 303 | state_changes : |
| 304 | The state changes frame. |
| 305 | address : |
| 306 | The address whose code changed. |
| 307 | new_code : |
| 308 | The new code value. |
| 309 | |
| 310 | """ |
| 311 | idx = state_changes.block_access_index |
| 312 | state_changes.code_changes[(address, idx)] = new_code |
track_selfdestruct
Handle selfdestruct of account created in same transaction.
Per EIP-7928/EIP-6780: removes nonce/code changes, converts storage writes to reads. Balance changes handled by net-zero filtering.
Parameters
tx_frame : The state changes tracker. Should be a transaction frame. address : The address that self-destructed.
def track_selfdestruct(tx_frame: StateChanges, address: Address) -> None:
| 319 | """ |
|---|---|
| 320 | Handle selfdestruct of account created in same transaction. |
| 321 | |
| 322 | Per EIP-7928/EIP-6780: removes nonce/code changes, converts storage |
| 323 | writes to reads. Balance changes handled by net-zero filtering. |
| 324 | |
| 325 | Parameters |
| 326 | ---------- |
| 327 | tx_frame : |
| 328 | The state changes tracker. Should be a transaction frame. |
| 329 | address : |
| 330 | The address that self-destructed. |
| 331 | |
| 332 | """ |
| 333 | # Has to be a transaction frame |
| 334 | assert tx_frame.parent is not None and tx_frame.parent.parent is None |
| 335 | |
| 336 | idx = tx_frame.block_access_index |
| 337 | |
| 338 | # Remove nonce changes from current transaction |
| 339 | tx_frame.nonce_changes = { |
| 340 | (addr, i, nonce) |
| 341 | for addr, i, nonce in tx_frame.nonce_changes |
| 342 | if not (addr == address and i == idx) |
| 343 | } |
| 344 | |
| 345 | # Remove balance changes from current transaction |
| 346 | if (address, idx) in tx_frame.balance_changes: |
| 347 | pre_balance = tx_frame.pre_balances[address] |
| 348 | if pre_balance == U256(0): |
| 349 | # Post balance will be U256(0) after deletion. |
| 350 | # So no change and hence bal does not need to |
| 351 | # capture anything. |
| 352 | del tx_frame.balance_changes[(address, idx)] |
| 353 | |
| 354 | # Remove code changes from current transaction |
| 355 | if (address, idx) in tx_frame.code_changes: |
| 356 | del tx_frame.code_changes[(address, idx)] |
| 357 | |
| 358 | # Convert storage writes from current transaction to reads |
| 359 | for addr, key, i in list(tx_frame.storage_writes.keys()): |
| 360 | if addr == address and i == idx: |
| 361 | del tx_frame.storage_writes[(addr, key, i)] |
| 362 | tx_frame.storage_reads.add((addr, key)) |
merge_on_success
Merge child frame into parent on success.
Child values overwrite parent values (most recent wins). No net-zero filtering here - that happens once at transaction commit via normalize_transaction().
Parameters
child_frame : The child frame being merged.
def merge_on_success(child_frame: StateChanges) -> None:
| 366 | """ |
|---|---|
| 367 | Merge child frame into parent on success. |
| 368 | |
| 369 | Child values overwrite parent values (most recent wins). No net-zero |
| 370 | filtering here - that happens once at transaction commit via |
| 371 | normalize_transaction(). |
| 372 | |
| 373 | Parameters |
| 374 | ---------- |
| 375 | child_frame : |
| 376 | The child frame being merged. |
| 377 | |
| 378 | """ |
| 379 | assert child_frame.parent is not None |
| 380 | parent_frame = child_frame.parent |
| 381 | |
| 382 | # Merge address accesses |
| 383 | parent_frame.touched_addresses.update(child_frame.touched_addresses) |
| 384 | |
| 385 | # Merge storage: reads union, writes overwrite (child supersedes parent) |
| 386 | parent_frame.storage_reads.update(child_frame.storage_reads) |
| 387 | for storage_key, storage_value in child_frame.storage_writes.items(): |
| 388 | parent_frame.storage_writes[storage_key] = storage_value |
| 389 | |
| 390 | # Merge balance changes: child overwrites parent for same key |
| 391 | for balance_key, balance_value in child_frame.balance_changes.items(): |
| 392 | parent_frame.balance_changes[balance_key] = balance_value |
| 393 | |
| 394 | # Merge nonce changes: keep highest nonce per address |
| 395 | address_final_nonces: Dict[Address, Tuple[BlockAccessIndex, U64]] = {} |
| 396 | for addr, idx, nonce in child_frame.nonce_changes: |
| 397 | if ( |
| 398 | addr not in address_final_nonces |
| 399 | or nonce > address_final_nonces[addr][1] |
| 400 | ): |
| 401 | address_final_nonces[addr] = (idx, nonce) |
| 402 | for addr, (idx, final_nonce) in address_final_nonces.items(): |
| 403 | parent_frame.nonce_changes.add((addr, idx, final_nonce)) |
| 404 | |
| 405 | # Merge code changes: child overwrites parent for same key |
| 406 | for code_key, code_value in child_frame.code_changes.items(): |
| 407 | parent_frame.code_changes[code_key] = code_value |
merge_on_failure
Merge child frame into parent on failure/revert.
Only reads merge; writes are discarded (converted to reads).
Parameters
child_frame : The failed child frame.
def merge_on_failure(child_frame: StateChanges) -> None:
| 411 | """ |
|---|---|
| 412 | Merge child frame into parent on failure/revert. |
| 413 | |
| 414 | Only reads merge; writes are discarded (converted to reads). |
| 415 | |
| 416 | Parameters |
| 417 | ---------- |
| 418 | child_frame : |
| 419 | The failed child frame. |
| 420 | |
| 421 | """ |
| 422 | assert child_frame.parent is not None |
| 423 | parent_frame = child_frame.parent |
| 424 | # Only merge reads and address accesses on failure |
| 425 | parent_frame.touched_addresses.update(child_frame.touched_addresses) |
| 426 | parent_frame.storage_reads.update(child_frame.storage_reads) |
| 427 | |
| 428 | # Convert writes to reads (failed writes still accessed the slots) |
| 429 | for address, key, _idx in child_frame.storage_writes.keys(): |
| 430 | parent_frame.storage_reads.add((address, key)) |
commit_transaction_frame
Commit transaction frame to block frame.
Filters net-zero changes before merging to ensure only actual state modifications are recorded in the block access list.
Parameters
tx_frame : The transaction frame to commit.
def commit_transaction_frame(tx_frame: StateChanges) -> None:
| 437 | """ |
|---|---|
| 438 | Commit transaction frame to block frame. |
| 439 | |
| 440 | Filters net-zero changes before merging to ensure only actual state |
| 441 | modifications are recorded in the block access list. |
| 442 | |
| 443 | Parameters |
| 444 | ---------- |
| 445 | tx_frame : |
| 446 | The transaction frame to commit. |
| 447 | |
| 448 | """ |
| 449 | assert tx_frame.parent is not None |
| 450 | block_frame = tx_frame.parent |
| 451 | |
| 452 | # Filter net-zero changes before committing |
| 453 | filter_net_zero_frame_changes(tx_frame) |
| 454 | |
| 455 | # Merge address accesses |
| 456 | block_frame.touched_addresses.update(tx_frame.touched_addresses) |
| 457 | |
| 458 | # Merge storage operations |
| 459 | block_frame.storage_reads.update(tx_frame.storage_reads) |
| 460 | for (addr, key, idx), value in tx_frame.storage_writes.items(): |
| 461 | block_frame.storage_writes[(addr, key, idx)] = value |
| 462 | |
| 463 | # Merge balance changes |
| 464 | for (addr, idx), final_balance in tx_frame.balance_changes.items(): |
| 465 | block_frame.balance_changes[(addr, idx)] = final_balance |
| 466 | |
| 467 | # Merge nonce changes |
| 468 | for addr, idx, nonce in tx_frame.nonce_changes: |
| 469 | block_frame.nonce_changes.add((addr, idx, nonce)) |
| 470 | |
| 471 | # Merge code changes |
| 472 | for (addr, idx), final_code in tx_frame.code_changes.items(): |
| 473 | block_frame.code_changes[(addr, idx)] = final_code |
create_child_frame
Create a child frame linked to the given parent.
Inherits block_access_index from parent so track functions can access it directly without walking up the frame hierarchy.
Parameters
parent : The parent frame.
Returns
child : StateChanges A new child frame with parent reference and inherited block_access_index.
def create_child_frame(parent: StateChanges) -> StateChanges:
| 477 | """ |
|---|---|
| 478 | Create a child frame linked to the given parent. |
| 479 | |
| 480 | Inherits block_access_index from parent so track functions can |
| 481 | access it directly without walking up the frame hierarchy. |
| 482 | |
| 483 | Parameters |
| 484 | ---------- |
| 485 | parent : |
| 486 | The parent frame. |
| 487 | |
| 488 | Returns |
| 489 | ------- |
| 490 | child : StateChanges |
| 491 | A new child frame with parent reference and inherited |
| 492 | block_access_index. |
| 493 | |
| 494 | """ |
| 495 | return StateChanges( |
| 496 | parent=parent, |
| 497 | block_access_index=parent.block_access_index, |
| 498 | ) |
filter_net_zero_frame_changes
Filter net-zero changes from transaction frame before commit.
Compares final values against pre-tx state for storage, balance, and code. Net-zero storage writes are converted to reads. Net-zero balance/code changes are removed entirely. Nonces are not filtered (only increment).
Parameters
tx_frame : The transaction-level state changes frame.
def filter_net_zero_frame_changes(tx_frame: StateChanges) -> None:
| 502 | """ |
|---|---|
| 503 | Filter net-zero changes from transaction frame before commit. |
| 504 | |
| 505 | Compares final values against pre-tx state for storage, balance, and code. |
| 506 | Net-zero storage writes are converted to reads. Net-zero balance/code |
| 507 | changes are removed entirely. Nonces are not filtered (only increment). |
| 508 | |
| 509 | Parameters |
| 510 | ---------- |
| 511 | tx_frame : |
| 512 | The transaction-level state changes frame. |
| 513 | |
| 514 | """ |
| 515 | idx = tx_frame.block_access_index |
| 516 | |
| 517 | # Filter storage: compare against pre_storage, convert net-zero to reads |
| 518 | addresses_to_check_storage = [ |
| 519 | (addr, key) |
| 520 | for (addr, key, i) in tx_frame.storage_writes.keys() |
| 521 | if i == idx |
| 522 | ] |
| 523 | for addr, key in addresses_to_check_storage: |
| 524 | # For any (address, key) whose balance has changed, its |
| 525 | # pre-value should have been captured |
| 526 | assert (addr, key) in tx_frame.pre_storage |
| 527 | pre_value = tx_frame.pre_storage[(addr, key)] |
| 528 | post_value = tx_frame.storage_writes[(addr, key, idx)] |
| 529 | if pre_value == post_value: |
| 530 | # Net-zero write - convert to read |
| 531 | del tx_frame.storage_writes[(addr, key, idx)] |
| 532 | tx_frame.storage_reads.add((addr, key)) |
| 533 | |
| 534 | # Filter balance: compare pre vs post, remove if equal |
| 535 | addresses_to_check_balance = [ |
| 536 | addr for (addr, i) in tx_frame.balance_changes.keys() if i == idx |
| 537 | ] |
| 538 | for addr in addresses_to_check_balance: |
| 539 | # For any account whose balance has changed, its |
| 540 | # pre-balance should have been captured |
| 541 | assert addr in tx_frame.pre_balances |
| 542 | pre_balance = tx_frame.pre_balances[addr] |
| 543 | post_balance = tx_frame.balance_changes[(addr, idx)] |
| 544 | if pre_balance == post_balance: |
| 545 | del tx_frame.balance_changes[(addr, idx)] |
| 546 | |
| 547 | # Filter code: compare pre vs post, remove if equal |
| 548 | addresses_to_check_code = [ |
| 549 | addr for (addr, i) in tx_frame.code_changes.keys() if i == idx |
| 550 | ] |
| 551 | for addr in addresses_to_check_code: |
| 552 | assert addr in tx_frame.pre_code |
| 553 | pre_code = tx_frame.pre_code[addr] |
| 554 | post_code = tx_frame.code_changes[(addr, idx)] |
| 555 | if pre_code == post_code: |
| 556 | del tx_frame.code_changes[(addr, idx)] |