1616"""
1717
1818from dataclasses import dataclass , field
19- from typing import TYPE_CHECKING , Dict
19+ from typing import TYPE_CHECKING , Dict , List , Set , Tuple
2020
2121from ethereum_types .bytes import Bytes , Bytes32
2222from ethereum_types .numeric import U64 , U256 , Uint
3737 from ..state import State # noqa: F401
3838
3939
40+ @dataclass
41+ class CallFrameSnapshot :
42+ """
43+ Snapshot of block access list state for a single call frame.
44+
45+ Used to track changes within a call frame to enable proper handling
46+ of reverts as specified in EIP-7928.
47+ """
48+
49+ touched_addresses : Set [Address ] = field (default_factory = set )
50+ """Addresses touched during this call frame."""
51+
52+ storage_writes : Dict [Tuple [Address , Bytes32 ], U256 ] = field (
53+ default_factory = dict
54+ )
55+ """Storage writes made during this call frame."""
56+
57+ balance_changes : Set [Tuple [Address , BlockAccessIndex , U256 ]] = field (
58+ default_factory = set
59+ )
60+ """Balance changes made during this call frame."""
61+
62+ nonce_changes : Set [Tuple [Address , BlockAccessIndex , U64 ]] = field (
63+ default_factory = set
64+ )
65+ """Nonce changes made during this call frame."""
66+
67+ code_changes : Set [Tuple [Address , BlockAccessIndex , Bytes ]] = field (
68+ default_factory = set
69+ )
70+ """Code changes made during this call frame."""
71+
72+
4073@dataclass
4174class StateChangeTracker :
4275 """
@@ -70,16 +103,25 @@ class StateChangeTracker:
70103 1..n for transactions, n+1 for post-execution).
71104 """
72105
106+ call_frame_snapshots : List [CallFrameSnapshot ] = field (default_factory = list )
107+ """
108+ Stack of snapshots for nested call frames to handle reverts properly.
109+ """
73110
74- def set_transaction_index (
111+
112+ def set_block_access_index (
75113 tracker : StateChangeTracker , block_access_index : Uint
76114) -> None :
77115 """
78116 Set the current block access index for tracking changes.
79117
80118 Must be called before processing each transaction/system contract
81- to ensure changes
82- are associated with the correct block access index.
119+ to ensure changes are associated with the correct block access index.
120+
121+ Note: Block access indices differ from transaction indices:
122+ - 0: Pre-execution (system contracts like beacon roots, block hashes)
123+ - 1..n: Transactions (tx at index i gets block_access_index i+1)
124+ - n+1: Post-execution (withdrawals, requests)
83125
84126 Parameters
85127 ----------
@@ -221,6 +263,10 @@ def track_storage_write(
221263 BlockAccessIndex (tracker .current_block_access_index ),
222264 value_bytes ,
223265 )
266+ # Record in current call frame snapshot if exists
267+ if tracker .call_frame_snapshots :
268+ snapshot = tracker .call_frame_snapshots [- 1 ]
269+ snapshot .storage_writes [(address , key )] = new_value
224270 else :
225271 add_storage_read (tracker .block_access_list_builder , address , key )
226272
@@ -249,13 +295,21 @@ def track_balance_change(
249295 """
250296 track_address_access (tracker , address )
251297
298+ block_access_index = BlockAccessIndex (tracker .current_block_access_index )
252299 add_balance_change (
253300 tracker .block_access_list_builder ,
254301 address ,
255- BlockAccessIndex ( tracker . current_block_access_index ) ,
302+ block_access_index ,
256303 new_balance ,
257304 )
258305
306+ # Record in current call frame snapshot if exists
307+ if tracker .call_frame_snapshots :
308+ snapshot = tracker .call_frame_snapshots [- 1 ]
309+ snapshot .balance_changes .add (
310+ (address , block_access_index , new_balance )
311+ )
312+
259313
260314def track_nonce_change (
261315 tracker : StateChangeTracker , address : Address , new_nonce : Uint
@@ -282,13 +336,20 @@ def track_nonce_change(
282336 [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2
283337 """
284338 track_address_access (tracker , address )
339+ block_access_index = BlockAccessIndex (tracker .current_block_access_index )
340+ nonce_u64 = U64 (new_nonce )
285341 add_nonce_change (
286342 tracker .block_access_list_builder ,
287343 address ,
288- BlockAccessIndex ( tracker . current_block_access_index ) ,
289- U64 ( new_nonce ) ,
344+ block_access_index ,
345+ nonce_u64 ,
290346 )
291347
348+ # Record in current call frame snapshot if exists
349+ if tracker .call_frame_snapshots :
350+ snapshot = tracker .call_frame_snapshots [- 1 ]
351+ snapshot .nonce_changes .add ((address , block_access_index , nonce_u64 ))
352+
292353
293354def track_code_change (
294355 tracker : StateChangeTracker , address : Address , new_code : Bytes
@@ -313,13 +374,19 @@ def track_code_change(
313374 [`CREATE2`]: ref:ethereum.forks.amsterdam.vm.instructions.system.create2
314375 """
315376 track_address_access (tracker , address )
377+ block_access_index = BlockAccessIndex (tracker .current_block_access_index )
316378 add_code_change (
317379 tracker .block_access_list_builder ,
318380 address ,
319- BlockAccessIndex ( tracker . current_block_access_index ) ,
381+ block_access_index ,
320382 new_code ,
321383 )
322384
385+ # Record in current call frame snapshot if exists
386+ if tracker .call_frame_snapshots :
387+ snapshot = tracker .call_frame_snapshots [- 1 ]
388+ snapshot .code_changes .add ((address , block_access_index , new_code ))
389+
323390
324391def finalize_transaction_changes (
325392 tracker : StateChangeTracker , state : "State"
@@ -339,3 +406,120 @@ def finalize_transaction_changes(
339406 The current execution state.
340407 """
341408 pass
409+
410+
411+ def begin_call_frame (tracker : StateChangeTracker ) -> None :
412+ """
413+ Begin a new call frame for tracking reverts.
414+
415+ Creates a new snapshot to track changes within this call frame.
416+ This allows proper handling of reverts as specified in EIP-7928.
417+
418+ Parameters
419+ ----------
420+ tracker :
421+ The state change tracker instance.
422+ """
423+ tracker .call_frame_snapshots .append (CallFrameSnapshot ())
424+
425+
426+ def rollback_call_frame (tracker : StateChangeTracker ) -> None :
427+ """
428+ Rollback changes from the current call frame.
429+
430+ When a call reverts, this function:
431+ - Converts storage writes to reads
432+ - Removes balance, nonce, and code changes
433+ - Preserves touched addresses
434+
435+ This implements EIP-7928 revert handling where reverted writes
436+ become reads and addresses remain in the access list.
437+
438+ Parameters
439+ ----------
440+ tracker :
441+ The state change tracker instance.
442+ """
443+ if not tracker .call_frame_snapshots :
444+ return
445+
446+ snapshot = tracker .call_frame_snapshots .pop ()
447+ builder = tracker .block_access_list_builder
448+
449+ # Convert storage writes to reads
450+ for (address , slot ), _ in snapshot .storage_writes .items ():
451+ # Remove the write from storage_changes
452+ if address in builder .accounts :
453+ account_data = builder .accounts [address ]
454+ if slot in account_data .storage_changes :
455+ # Filter out changes from this call frame
456+ account_data .storage_changes [slot ] = [
457+ change
458+ for change in account_data .storage_changes [slot ]
459+ if change .block_access_index
460+ != tracker .current_block_access_index
461+ ]
462+ if not account_data .storage_changes [slot ]:
463+ del account_data .storage_changes [slot ]
464+ # Add as a read instead
465+ account_data .storage_reads .add (slot )
466+
467+ # Remove balance changes from this call frame
468+ for address , block_access_index , new_balance in snapshot .balance_changes :
469+ if address in builder .accounts :
470+ account_data = builder .accounts [address ]
471+ # Filter out balance changes from this call frame
472+ account_data .balance_changes = [
473+ change
474+ for change in account_data .balance_changes
475+ if not (
476+ change .block_access_index == block_access_index
477+ and change .post_balance == new_balance
478+ )
479+ ]
480+
481+ # Remove nonce changes from this call frame
482+ for address , block_access_index , new_nonce in snapshot .nonce_changes :
483+ if address in builder .accounts :
484+ account_data = builder .accounts [address ]
485+ # Filter out nonce changes from this call frame
486+ account_data .nonce_changes = [
487+ change
488+ for change in account_data .nonce_changes
489+ if not (
490+ change .block_access_index == block_access_index
491+ and change .new_nonce == new_nonce
492+ )
493+ ]
494+
495+ # Remove code changes from this call frame
496+ for address , block_access_index , new_code in snapshot .code_changes :
497+ if address in builder .accounts :
498+ account_data = builder .accounts [address ]
499+ # Filter out code changes from this call frame
500+ account_data .code_changes = [
501+ change
502+ for change in account_data .code_changes
503+ if not (
504+ change .block_access_index == block_access_index
505+ and change .new_code == new_code
506+ )
507+ ]
508+
509+ # All touched addresses remain in the access list (already tracked)
510+
511+
512+ def commit_call_frame (tracker : StateChangeTracker ) -> None :
513+ """
514+ Commit changes from the current call frame.
515+
516+ Removes the current call frame snapshot without rolling back changes.
517+ Called when a call completes successfully.
518+
519+ Parameters
520+ ----------
521+ tracker :
522+ The state change tracker instance.
523+ """
524+ if tracker .call_frame_snapshots :
525+ tracker .call_frame_snapshots .pop ()
0 commit comments