Skip to content

Commit ff21729

Browse files
committed
eip7732 fork choice tests (part1)
1 parent cecfd12 commit ff21729

File tree

4 files changed

+308
-2
lines changed

4 files changed

+308
-2
lines changed

presets/minimal/eip7732.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
# Execution
44
# ---------------------------------------------------------------
5-
# 2**1(= 2)
6-
PTC_SIZE: 2
5+
# 2**3(= 8)
6+
PTC_SIZE: 8
77
# 2**2 (= 4)
88
MAX_PAYLOAD_ATTESTATIONS: 4
99
# floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK) (= 9 + 1 + 5 = 15)

tests/core/pyspec/eth2spec/test/eip7732/fork_choice/__init__.py

Whitespace-only changes.
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
from eth2spec.test.context import (
2+
spec_state_test,
3+
with_eip7732_and_later,
4+
)
5+
from eth2spec.test.helpers.block import (
6+
build_empty_block_for_next_slot,
7+
)
8+
from eth2spec.test.helpers.execution_payload import (
9+
build_empty_execution_payload,
10+
)
11+
from eth2spec.test.helpers.fork_choice import (
12+
check_head_against_root,
13+
get_anchor_root,
14+
get_genesis_forkchoice_store_and_block,
15+
on_tick_and_append_step,
16+
output_head_check,
17+
tick_and_add_block,
18+
)
19+
from eth2spec.test.helpers.keys import privkeys
20+
from eth2spec.test.helpers.state import (
21+
payload_state_transition,
22+
state_transition_and_sign_block,
23+
)
24+
25+
26+
def run_on_execution_payload(spec, store, signed_envelope, test_steps, valid=True):
27+
"""
28+
Helper to run spec.on_execution_payload() and append test step.
29+
Similar to run_on_block() in fork_choice helpers.
30+
"""
31+
32+
def _append_step(valid=True):
33+
envelope_name = (
34+
f"execution_payload_envelope_{signed_envelope.message.beacon_block_root.hex()[:8]}"
35+
)
36+
test_steps.append(
37+
{
38+
"execution_payload": envelope_name,
39+
"valid": valid,
40+
}
41+
)
42+
43+
if not valid:
44+
try:
45+
spec.on_execution_payload(store, signed_envelope)
46+
except AssertionError:
47+
_append_step(valid=False)
48+
return
49+
else:
50+
assert False
51+
52+
spec.on_execution_payload(store, signed_envelope)
53+
# Verify the envelope was processed
54+
envelope_root = signed_envelope.message.beacon_block_root
55+
assert envelope_root in store.execution_payload_states, "Envelope should be processed in store"
56+
_append_step()
57+
58+
59+
def create_and_yield_execution_payload_envelope(spec, state, block_root, signed_block):
60+
"""
61+
Helper to create and yield an execution payload envelope for testing.
62+
63+
Creates a SignedExecutionPayloadEnvelope with proper EIP7732 fields and yields it
64+
for SSZ serialization in fork choice tests. The builder_index is extracted from
65+
the block's execution payload header to ensure consistency.
66+
67+
Args:
68+
spec: The EIP7732 specification module
69+
state: Current beacon state
70+
block_root: Root of the block this envelope is for
71+
signed_block: The signed beacon block (must contain signed_execution_payload_header)
72+
73+
Returns:
74+
envelope_name: Name of the generated envelope for referencing in test steps
75+
76+
Usage:
77+
# In a fork choice test function:
78+
envelope, envelope_name = yield from create_and_yield_execution_payload_envelope(spec, state, block_root, signed_block)
79+
run_on_execution_payload(spec, store, envelope, test_steps, valid=True)
80+
"""
81+
# Get builder_index from the block's execution payload header
82+
builder_index = signed_block.message.body.signed_execution_payload_header.message.builder_index
83+
84+
# Create a proper execution payload with correct parent_hash for EIP7732
85+
payload = build_empty_execution_payload(spec, state)
86+
# Update parent_hash to match state.latest_block_hash as required by EIP7732
87+
payload.parent_hash = state.latest_block_hash
88+
89+
# Simulate the state changes that will occur during execution payload processing
90+
# to compute the correct state_root for the envelope
91+
temp_state = state.copy()
92+
93+
# Cache latest block header state root (from process_execution_payload)
94+
previous_state_root = temp_state.hash_tree_root()
95+
if temp_state.latest_block_header.state_root == spec.Root():
96+
temp_state.latest_block_header.state_root = previous_state_root
97+
98+
# Apply the key state changes that affect the state root:
99+
# 1. Process execution requests (empty in our test case, but still affects state)
100+
# Note: We don't need to actually process them since we use empty ExecutionRequests()
101+
102+
# 2. Queue the builder payment (this modifies builder_pending_withdrawals and builder_pending_payments)
103+
payment = temp_state.builder_pending_payments[
104+
spec.SLOTS_PER_EPOCH + temp_state.slot % spec.SLOTS_PER_EPOCH
105+
]
106+
exit_queue_epoch = spec.compute_exit_epoch_and_update_churn(
107+
temp_state, payment.withdrawal.amount
108+
)
109+
payment.withdrawal.withdrawable_epoch = spec.Epoch(
110+
exit_queue_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY
111+
)
112+
temp_state.builder_pending_withdrawals.append(payment.withdrawal)
113+
temp_state.builder_pending_payments[
114+
spec.SLOTS_PER_EPOCH + temp_state.slot % spec.SLOTS_PER_EPOCH
115+
] = spec.BuilderPendingPayment()
116+
117+
# 3. Update execution payload availability
118+
temp_state.execution_payload_availability[temp_state.slot % spec.SLOTS_PER_HISTORICAL_ROOT] = (
119+
0b1
120+
)
121+
# 4. Update latest block hash
122+
temp_state.latest_block_hash = payload.block_hash
123+
# 5. Update latest full slot
124+
temp_state.latest_full_slot = temp_state.slot
125+
126+
# Compute the post-processing state root
127+
post_processing_state_root = temp_state.hash_tree_root()
128+
129+
# Create the execution payload envelope message
130+
envelope_message = spec.ExecutionPayloadEnvelope(
131+
beacon_block_root=block_root,
132+
payload=payload,
133+
execution_requests=spec.ExecutionRequests(),
134+
builder_index=builder_index,
135+
slot=signed_block.message.slot,
136+
blob_kzg_commitments=[],
137+
state_root=post_processing_state_root,
138+
)
139+
140+
# Sign the envelope with the builder's private key
141+
builder_privkey = privkeys[envelope_message.builder_index]
142+
signature = spec.get_execution_payload_envelope_signature(
143+
state, envelope_message, builder_privkey
144+
)
145+
146+
# Create the signed envelope
147+
envelope = spec.SignedExecutionPayloadEnvelope(
148+
message=envelope_message,
149+
signature=signature,
150+
)
151+
envelope_name = f"execution_payload_envelope_{block_root.hex()[:8]}"
152+
yield envelope_name, envelope
153+
return envelope, envelope_name
154+
155+
156+
@with_eip7732_and_later
157+
@spec_state_test
158+
def test_genesis(spec, state):
159+
"""Test genesis initialization with EIP7732 fork choice modifications"""
160+
test_steps = []
161+
# Initialization
162+
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
163+
yield "anchor_state", state
164+
yield "anchor_block", anchor_block
165+
166+
anchor_root = get_anchor_root(spec, state)
167+
check_head_against_root(spec, store, anchor_root)
168+
169+
# EIP7732-specific assertions
170+
assert hasattr(store, "execution_payload_states"), (
171+
"Store should have execution_payload_states field"
172+
)
173+
assert hasattr(store, "ptc_vote"), "Store should have ptc_vote field"
174+
assert anchor_root in store.execution_payload_states, (
175+
"Anchor block should be in execution_payload_states"
176+
)
177+
assert anchor_root in store.ptc_vote, "Anchor block should have ptc_vote entry"
178+
179+
# Check PTC vote initialization
180+
ptc_vote = store.ptc_vote[anchor_root]
181+
assert len(ptc_vote) == spec.PTC_SIZE, f"PTC vote should have {spec.PTC_SIZE} entries"
182+
assert all(vote == False for vote in ptc_vote), "All PTC votes should be False initially"
183+
184+
# Verify get_head returns ForkChoiceNode
185+
head = spec.get_head(store)
186+
assert isinstance(head, spec.ForkChoiceNode), "get_head should return ForkChoiceNode in EIP7732"
187+
188+
output_head_check(spec, store, test_steps)
189+
190+
yield "steps", test_steps
191+
192+
193+
@with_eip7732_and_later
194+
@spec_state_test
195+
def test_basic(spec, state):
196+
"""Basic EIP7732 fork choice test with execution payload processing"""
197+
test_steps = []
198+
199+
# Add EIP7732-specific metadata
200+
yield "test_scenario", "meta", "basic_fork_choice_eip7732"
201+
yield "tests_payload_status", "meta", True
202+
yield "tests_execution_payload_states", "meta", True
203+
204+
# Initialization
205+
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
206+
yield "anchor_state", state
207+
yield "anchor_block", anchor_block
208+
209+
# Set initial time and record tick
210+
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
211+
on_tick_and_append_step(spec, store, current_time, test_steps)
212+
213+
# Verify initial EIP7732 state
214+
anchor_root = get_anchor_root(spec, state)
215+
check_head_against_root(spec, store, anchor_root)
216+
217+
# Check initial head - genesis has FULL payload status
218+
head = spec.get_head(store)
219+
assert head.payload_status == spec.PAYLOAD_STATUS_FULL, "Genesis head should have FULL status"
220+
221+
# On receiving a block of `GENESIS_SLOT + 1` slot
222+
block = build_empty_block_for_next_slot(spec, state)
223+
signed_block = state_transition_and_sign_block(spec, state, block)
224+
yield from tick_and_add_block(spec, store, signed_block, test_steps)
225+
226+
# Verify block was added to stores
227+
block_root = signed_block.message.hash_tree_root()
228+
assert block_root in store.blocks, "Block should be in store.blocks"
229+
assert block_root in store.block_states, "Block should have block state"
230+
assert block_root in store.ptc_vote, "Block should have PTC vote entry"
231+
232+
# Head should now be the new block with EMPTY status (no payload revealed yet)
233+
check_head_against_root(spec, store, block_root)
234+
head = spec.get_head(store)
235+
assert head.payload_status == spec.PAYLOAD_STATUS_EMPTY, (
236+
"New head should have EMPTY status (no payload revealed)"
237+
)
238+
239+
# Create and yield execution payload envelope first (builder reveals payload)
240+
envelope, envelope_name = yield from create_and_yield_execution_payload_envelope(
241+
spec, state, block_root, signed_block
242+
)
243+
244+
# Process the execution payload through fork choice on_execution_payload
245+
run_on_execution_payload(spec, store, envelope, test_steps, valid=True)
246+
247+
# Then simulate execution payload processing (process the revealed payload)
248+
payload_state_transition(spec, store, signed_block.message)
249+
250+
# Verify block now has execution payload state after processing
251+
assert block_root in store.execution_payload_states, (
252+
"Block should now have execution payload state"
253+
)
254+
255+
# On receiving a block of next slot
256+
block_2 = build_empty_block_for_next_slot(spec, state)
257+
signed_block_2 = state_transition_and_sign_block(spec, state, block_2)
258+
yield from tick_and_add_block(spec, store, signed_block_2, test_steps)
259+
260+
# Process second block
261+
block_2_root = signed_block_2.message.hash_tree_root()
262+
check_head_against_root(spec, store, block_2_root)
263+
264+
# Create and yield second execution payload envelope first (builder reveals payload)
265+
envelope_2, envelope_2_name = yield from create_and_yield_execution_payload_envelope(
266+
spec, state, block_2_root, signed_block_2
267+
)
268+
269+
# Process the second execution payload through fork choice on_execution_payload
270+
run_on_execution_payload(spec, store, envelope_2, test_steps, valid=True)
271+
272+
# Then simulate execution payload processing for second block
273+
payload_state_transition(spec, store, signed_block_2.message)
274+
275+
# Add EIP7732-specific checks to test steps
276+
test_steps.append(
277+
{
278+
"checks": {
279+
"execution_payload_states_count": len(store.execution_payload_states),
280+
"blocks_with_ptc_votes": len(store.ptc_vote),
281+
"head_payload_status": int(spec.get_head(store).payload_status),
282+
}
283+
}
284+
)
285+
286+
yield "steps", test_steps

tests/core/pyspec/eth2spec/test/helpers/fork_choice.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,23 @@ def get_pow_block_file_name(pow_block):
552552
def add_pow_block(spec, store, pow_block, test_steps):
553553
yield get_pow_block_file_name(pow_block), pow_block
554554
test_steps.append({"pow_block": get_pow_block_file_name(pow_block)})
555+
556+
557+
# EIP7732 Fork Choice Helpers
558+
559+
560+
def create_payload_attestation_message(
561+
spec, validator_index, beacon_block_root, slot, payload_present=True
562+
):
563+
"""Create PayloadAttestationMessage for PTC voting"""
564+
data = spec.PayloadAttestationData(
565+
beacon_block_root=beacon_block_root,
566+
slot=slot,
567+
payload_present=payload_present,
568+
)
569+
570+
return spec.PayloadAttestationMessage(
571+
validator_index=validator_index,
572+
data=data,
573+
signature=spec.BLSSignature(), # Empty signature for testing
574+
)

0 commit comments

Comments
 (0)