Skip to content

Commit e599bd6

Browse files
nerolationfselmo
andcommitted
fix zero-value transfer tracking (#6)
* fix zero-value transfer tracking * fix reverted frame tracking * rename variables * fix missing addresses bug * fix: docs run & move imports to top of file --------- Co-authored-by: fselmo <[email protected]>
1 parent 82c0be4 commit e599bd6

File tree

7 files changed

+456
-26
lines changed

7 files changed

+456
-26
lines changed

src/ethereum/forks/amsterdam/block_access_lists/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
)
2020
from .tracker import (
2121
StateChangeTracker,
22-
set_transaction_index,
22+
begin_call_frame,
23+
commit_call_frame,
24+
rollback_call_frame,
25+
set_block_access_index,
2326
track_address_access,
2427
track_balance_change,
2528
track_code_change,
@@ -37,9 +40,12 @@
3740
"add_storage_read",
3841
"add_storage_write",
3942
"add_touched_account",
43+
"begin_call_frame",
4044
"build_block_access_list",
45+
"commit_call_frame",
4146
"compute_block_access_list_hash",
42-
"set_transaction_index",
47+
"rollback_call_frame",
48+
"set_block_access_index",
4349
"rlp_encode_block_access_list",
4450
"track_address_access",
4551
"track_balance_change",

src/ethereum/forks/amsterdam/block_access_lists/tracker.py

Lines changed: 192 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"""
1717

1818
from dataclasses import dataclass, field
19-
from typing import TYPE_CHECKING, Dict
19+
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
2020

2121
from ethereum_types.bytes import Bytes, Bytes32
2222
from ethereum_types.numeric import U64, U256, Uint
@@ -37,6 +37,39 @@
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
4174
class 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

260314
def 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

293354
def 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

324391
def 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()

src/ethereum/forks/amsterdam/fork.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .block_access_lists.builder import build_block_access_list
3434
from .block_access_lists.rlp_utils import compute_block_access_list_hash
3535
from .block_access_lists.tracker import (
36-
set_transaction_index,
36+
set_block_access_index,
3737
track_balance_change,
3838
)
3939
from .blocks import Block, Header, Log, Receipt, Withdrawal, encode_receipt
@@ -764,9 +764,9 @@ def apply_body(
764764
"""
765765
block_output = vm.BlockOutput()
766766

767-
# Set system transaction index for pre-execution system contracts
767+
# Set block access index for pre-execution system contracts
768768
# EIP-7928: System contracts use block_access_index 0
769-
set_transaction_index(block_env.state.change_tracker, Uint(0))
769+
set_block_access_index(block_env.state.change_tracker, Uint(0))
770770

771771
process_unchecked_system_transaction(
772772
block_env=block_env,
@@ -785,7 +785,9 @@ def apply_body(
785785

786786
# EIP-7928: Post-execution uses block_access_index len(transactions) + 1
787787
post_execution_index = ulen(transactions) + Uint(1)
788-
set_transaction_index(block_env.state.change_tracker, post_execution_index)
788+
set_block_access_index(
789+
block_env.state.change_tracker, post_execution_index
790+
)
789791

790792
process_withdrawals(block_env, block_output, withdrawals)
791793

@@ -874,7 +876,8 @@ def process_transaction(
874876
Index of the transaction in the block.
875877
"""
876878
# EIP-7928: Transactions use block_access_index 1 to len(transactions)
877-
set_transaction_index(block_env.state.change_tracker, index + Uint(1))
879+
# Transaction at index i gets block_access_index i+1
880+
set_block_access_index(block_env.state.change_tracker, index + Uint(1))
878881

879882
trie_set(
880883
block_output.transactions_trie,

src/ethereum/forks/amsterdam/vm/interpreter.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
evm_trace,
3131
)
3232

33+
from ..block_access_lists.tracker import (
34+
begin_call_frame,
35+
commit_call_frame,
36+
rollback_call_frame,
37+
track_address_access,
38+
)
3339
from ..blocks import Log
3440
from ..fork_types import Address
3541
from ..state import (
@@ -239,6 +245,11 @@ def process_message(message: Message) -> Evm:
239245
# take snapshot of state before processing the message
240246
begin_transaction(state, transient_storage)
241247

248+
if hasattr(state, 'change_tracker') and state.change_tracker:
249+
begin_call_frame(state.change_tracker)
250+
# Track target address access when processing a message
251+
track_address_access(state.change_tracker, message.current_target)
252+
242253
if message.should_transfer_value and message.value != 0:
243254
move_ether(
244255
state, message.caller, message.current_target, message.value
@@ -249,8 +260,12 @@ def process_message(message: Message) -> Evm:
249260
# revert state to the last saved checkpoint
250261
# since the message call resulted in an error
251262
rollback_transaction(state, transient_storage)
263+
if hasattr(state, 'change_tracker') and state.change_tracker:
264+
rollback_call_frame(state.change_tracker)
252265
else:
253266
commit_transaction(state, transient_storage)
267+
if hasattr(state, 'change_tracker') and state.change_tracker:
268+
commit_call_frame(state.change_tracker)
254269
return evm
255270

256271

src/ethereum_spec_tools/evm_tools/loaders/fork_loader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,10 @@ def compute_block_access_list_hash(self) -> Any:
137137
)
138138

139139
@property
140-
def set_transaction_index(self) -> Any:
141-
"""set_transaction_index function of the fork"""
140+
def set_block_access_index(self) -> Any:
141+
"""set_block_access_index function of the fork"""
142142
return (
143-
self._module("block_access_lists").set_transaction_index
143+
self._module("block_access_lists").set_block_access_index
144144
)
145145

146146
@property

0 commit comments

Comments
 (0)