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)]