Skip to content

Commit 14832ac

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

File tree

4 files changed

+287
-2
lines changed

4 files changed

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