Skip to content

Commit e755ca3

Browse files
committed
fix(spec-specs): Use proper frames for system transactions
1 parent 5224504 commit e755ca3

File tree

3 files changed

+273
-8
lines changed

3 files changed

+273
-8
lines changed

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -484,19 +484,14 @@ def build_block_access_list(
484484
for address, slot in state_changes.storage_reads:
485485
add_storage_read(builder, address, slot)
486486

487-
# Add all storage writes, filtering net-zero changes
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.
488490
for (
489491
address,
490492
slot,
491493
block_access_index,
492494
), value in state_changes.storage_writes.items():
493-
# Check if this is a net-zero change by comparing with pre-state
494-
if (address, slot) in state_changes.pre_storage:
495-
if state_changes.pre_storage[(address, slot)] == value:
496-
# Net-zero change - convert to read only
497-
add_storage_read(builder, address, slot)
498-
continue
499-
500495
# Convert U256 to Bytes32 for storage
501496
value_bytes = Bytes32(value.to_bytes(U256(32), "big"))
502497
add_storage_write(

src/ethereum/forks/amsterdam/fork.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
get_block_access_index,
7272
handle_in_transaction_selfdestruct,
7373
increment_block_access_index,
74+
merge_on_success,
7475
normalize_balance_changes_for_transaction,
7576
track_address,
7677
track_balance_change,
@@ -631,6 +632,10 @@ def process_system_transaction(
631632
Output of processing the system transaction.
632633
633634
"""
635+
# EIP-7928: Create a child frame for system transaction
636+
# This allows proper pre-state capture for net-zero filtering
637+
system_tx_state_changes = create_child_frame(block_env.block_state_changes)
638+
634639
tx_env = vm.TransactionEnvironment(
635640
origin=SYSTEM_ADDRESS,
636641
gas_price=block_env.base_fee_per_gas,
@@ -662,10 +667,15 @@ def process_system_transaction(
662667
accessed_storage_keys=set(),
663668
disable_precompiles=False,
664669
parent_evm=None,
670+
transaction_state_changes=system_tx_state_changes,
665671
)
666672

667673
system_tx_output = process_message_call(system_tx_message)
668674

675+
# Merge system transaction changes back to block frame
676+
# System transactions always succeed (or block is invalid)
677+
merge_on_success(system_tx_state_changes)
678+
669679
return system_tx_output
670680

671681

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""
2+
Tests for EIP-7928 BAL cross-index tracking.
3+
4+
Tests that state changes are correctly tracked across different block indices:
5+
- Index 1..N: Regular transactions
6+
- Index N+1: Post-execution system operations
7+
8+
Includes tests for system contracts (withdrawal/consolidation) cross-index
9+
tracking and NOOP filtering behavior.
10+
"""
11+
12+
import pytest
13+
from execution_testing import (
14+
Account,
15+
Address,
16+
Alloc,
17+
BalAccountExpectation,
18+
BalStorageChange,
19+
BalStorageSlot,
20+
Block,
21+
BlockAccessListExpectation,
22+
BlockchainTestFiller,
23+
Bytecode,
24+
Op,
25+
Transaction,
26+
)
27+
28+
from .spec import ref_spec_7928
29+
30+
REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path
31+
REFERENCE_SPEC_VERSION = ref_spec_7928.version
32+
33+
pytestmark = pytest.mark.valid_from("Amsterdam")
34+
35+
WITHDRAWAL_REQUEST_ADDRESS = Address(
36+
0x00000961EF480EB55E80D19AD83579A64C007002
37+
)
38+
CONSOLIDATION_REQUEST_ADDRESS = Address(
39+
0x0000BBDDC7CE488642FB579F8B00F3A590007251
40+
)
41+
42+
43+
def test_bal_withdrawal_contract_cross_index(
44+
pre: Alloc,
45+
blockchain_test: BlockchainTestFiller,
46+
) -> None:
47+
"""
48+
Test that the withdrawal system contract shows storage changes at both
49+
index 1 (during transaction) and index 2 (during post-execution).
50+
51+
This verifies that slots 0x01 and 0x03 are:
52+
1. Incremented during the transaction (index 1)
53+
2. Reset during post-execution (index 2)
54+
"""
55+
sender = pre.fund_eoa()
56+
57+
withdrawal_calldata = (
58+
(b"\x01" + b"\x00" * 47) # validator pubkey
59+
+ (b"\x00" * 8) # amount
60+
)
61+
62+
tx = Transaction(
63+
sender=sender,
64+
to=WITHDRAWAL_REQUEST_ADDRESS,
65+
value=1,
66+
data=withdrawal_calldata,
67+
gas_limit=1_000_000,
68+
)
69+
70+
blockchain_test(
71+
pre=pre,
72+
blocks=[
73+
Block(
74+
txs=[tx],
75+
expected_block_access_list=BlockAccessListExpectation(
76+
account_expectations={
77+
WITHDRAWAL_REQUEST_ADDRESS: BalAccountExpectation(
78+
# slots 0x01 and 0x03 change at BOTH indices
79+
storage_changes=[
80+
BalStorageSlot(
81+
slot=0x01, # Request count
82+
slot_changes=[
83+
BalStorageChange(
84+
# Incremented during tx
85+
tx_index=1,
86+
post_value=1,
87+
),
88+
BalStorageChange(
89+
# Reset during post-exec
90+
tx_index=2,
91+
post_value=0,
92+
),
93+
],
94+
),
95+
BalStorageSlot(
96+
slot=0x03, # Target count
97+
slot_changes=[
98+
BalStorageChange(
99+
# Incremented during tx
100+
tx_index=1,
101+
post_value=1,
102+
),
103+
BalStorageChange(
104+
# Reset during post-exec
105+
tx_index=2,
106+
post_value=0,
107+
),
108+
],
109+
),
110+
],
111+
),
112+
}
113+
),
114+
)
115+
],
116+
post={},
117+
)
118+
119+
120+
def test_bal_consolidation_contract_cross_index(
121+
pre: Alloc,
122+
blockchain_test: BlockchainTestFiller,
123+
) -> None:
124+
"""
125+
Test that the consolidation system contract shows storage changes at both
126+
index 1 (during transaction) and index 2 (during post-execution).
127+
"""
128+
sender = pre.fund_eoa()
129+
130+
consolidation_calldata = (
131+
(b"\x01" + b"\x00" * 47) # source pubkey
132+
+ (b"\x02" + b"\x00" * 47) # target pubkey
133+
)
134+
135+
tx = Transaction(
136+
sender=sender,
137+
to=CONSOLIDATION_REQUEST_ADDRESS,
138+
value=1,
139+
data=consolidation_calldata,
140+
gas_limit=1_000_000,
141+
)
142+
143+
blockchain_test(
144+
pre=pre,
145+
blocks=[
146+
Block(
147+
txs=[tx],
148+
expected_block_access_list=BlockAccessListExpectation(
149+
account_expectations={
150+
CONSOLIDATION_REQUEST_ADDRESS: BalAccountExpectation(
151+
storage_changes=[
152+
BalStorageSlot(
153+
slot=0x01,
154+
slot_changes=[
155+
BalStorageChange(
156+
# Incremented during tx
157+
tx_index=1,
158+
post_value=1,
159+
),
160+
BalStorageChange(
161+
# Reset during post-exec
162+
tx_index=2,
163+
post_value=0,
164+
),
165+
],
166+
),
167+
BalStorageSlot(
168+
slot=0x03,
169+
slot_changes=[
170+
BalStorageChange(
171+
# Incremented during tx
172+
tx_index=1,
173+
post_value=1,
174+
),
175+
BalStorageChange(
176+
# Reset during post-exec
177+
tx_index=2,
178+
post_value=0,
179+
),
180+
],
181+
),
182+
],
183+
),
184+
}
185+
),
186+
)
187+
],
188+
post={},
189+
)
190+
191+
192+
def test_bal_noop_write_filtering(
193+
pre: Alloc,
194+
blockchain_test: BlockchainTestFiller,
195+
) -> None:
196+
"""
197+
Test that NOOP writes (writing same value or 0 to empty) are filtered.
198+
199+
This verifies that:
200+
1. Writing 0 to an uninitialized slot doesn't appear in BAL
201+
2. Writing the same value to a slot doesn't appear in BAL
202+
3. Only actual changes are tracked
203+
"""
204+
test_code = Bytecode(
205+
# Write 0 to uninitialized slot 1 (noop)
206+
Op.SSTORE(1, 0)
207+
# Write 42 to slot 2
208+
+ Op.SSTORE(2, 42)
209+
# Write 100 to slot 3 (will be same as pre-state, should be filtered)
210+
+ Op.SSTORE(3, 100)
211+
# Write 200 to slot 4 (different from pre-state 150, should appear)
212+
+ Op.SSTORE(4, 200)
213+
)
214+
215+
sender = pre.fund_eoa()
216+
test_address = pre.deploy_contract(
217+
code=test_code,
218+
storage={3: 100, 4: 150},
219+
)
220+
221+
tx = Transaction(
222+
sender=sender,
223+
to=test_address,
224+
gas_limit=100_000,
225+
)
226+
227+
# Expected BAL should only show actual changes
228+
expected_block_access_list = BlockAccessListExpectation(
229+
account_expectations={
230+
test_address: BalAccountExpectation(
231+
storage_changes=[
232+
BalStorageSlot(
233+
slot=2,
234+
slot_changes=[
235+
BalStorageChange(tx_index=1, post_value=42),
236+
],
237+
),
238+
BalStorageSlot(
239+
slot=4,
240+
slot_changes=[
241+
BalStorageChange(tx_index=1, post_value=200),
242+
],
243+
),
244+
],
245+
),
246+
}
247+
)
248+
249+
block = Block(
250+
txs=[tx],
251+
expected_block_access_list=expected_block_access_list,
252+
)
253+
254+
blockchain_test(
255+
pre=pre,
256+
blocks=[block],
257+
post={
258+
test_address: Account(storage={2: 42, 3: 100, 4: 200}),
259+
},
260+
)

0 commit comments

Comments
 (0)