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
0 commit comments