Skip to content

Commit 5c00326

Browse files
committed
feat: improve randomize_state field coverage for fork transition testing
* Add Electra churn field randomization * Add withdrawal field randomization for Capella+ forks * Add finality checkpoint randomization with safety checks * Add pending operations randomization * Randomize fields for all forks transitioning from phase0 to Electra
1 parent c62556b commit 5c00326

File tree

2 files changed

+248
-8
lines changed

2 files changed

+248
-8
lines changed

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from eth2spec.test.helpers.deposits import mock_deposit
55
from eth2spec.test.helpers.forks import (
66
is_post_altair,
7+
is_post_capella,
78
is_post_electra,
89
)
910
from eth2spec.test.helpers.state import next_epoch
@@ -296,17 +297,111 @@ def set_some_pending_consolidations(spec, state, rng):
296297
return consolidation_pairs
297298

298299

300+
def set_electra_churn_fields(spec, state, rng):
301+
"""Set Electra churn-related fields to realistic non-default values if post-Electra."""
302+
current_epoch = spec.get_current_epoch(state)
303+
304+
state.earliest_exit_epoch = current_epoch + rng.randint(0, 4)
305+
state.earliest_consolidation_epoch = current_epoch + rng.randint(0, 3)
306+
307+
# Set realistic churn balances
308+
max_churn = spec.EFFECTIVE_BALANCE_INCREMENT * 10
309+
state.deposit_balance_to_consume = rng.randint(0, max_churn)
310+
state.exit_balance_to_consume = rng.randint(0, max_churn)
311+
state.consolidation_balance_to_consume = rng.randint(0, max_churn)
312+
313+
314+
def set_withdrawal_fields(spec, state, rng):
315+
"""Set withdrawal-related fields to realistic non-default values if post-Capella."""
316+
state.next_withdrawal_index = rng.randint(1, 1000)
317+
# Ensure next_withdrawal_validator_index is non-zero and valid
318+
if len(state.validators) > 1:
319+
state.next_withdrawal_validator_index = rng.randint(1, len(state.validators) - 1)
320+
else:
321+
state.next_withdrawal_validator_index = 0
322+
323+
324+
def set_deposit_request_fields(spec, state, rng):
325+
"""Set deposit request fields to realistic non-default values if post-Electra."""
326+
if state.deposit_requests_start_index == spec.FAR_FUTURE_EPOCH:
327+
state.deposit_requests_start_index = rng.randint(100, 10000)
328+
329+
330+
def set_finality_fields(spec, state, rng):
331+
"""Set finality fields to realistic non-default values safely."""
332+
current_epoch = spec.get_current_epoch(state)
333+
334+
# Only modify if they're still at genesis defaults
335+
if (
336+
state.finalized_checkpoint.epoch == 0
337+
and state.current_justified_checkpoint.epoch == 0
338+
and state.previous_justified_checkpoint.epoch == 0
339+
):
340+
# Set realistic but valid finality progression
341+
if current_epoch > 3:
342+
finalized_epoch = max(0, current_epoch - 3)
343+
prev_justified_epoch = max(finalized_epoch, current_epoch - 2)
344+
curr_justified_epoch = max(prev_justified_epoch, current_epoch - 1)
345+
346+
# Create realistic checkpoint progression
347+
state.finalized_checkpoint = spec.Checkpoint(
348+
epoch=finalized_epoch,
349+
root=spec.get_block_root(state, finalized_epoch)
350+
if finalized_epoch > 0
351+
else b"\x01" * 32,
352+
)
353+
state.previous_justified_checkpoint = spec.Checkpoint(
354+
epoch=prev_justified_epoch,
355+
root=spec.get_block_root(state, prev_justified_epoch)
356+
if prev_justified_epoch > 0
357+
else b"\x02" * 32,
358+
)
359+
state.current_justified_checkpoint = spec.Checkpoint(
360+
epoch=curr_justified_epoch,
361+
root=spec.get_block_root(state, curr_justified_epoch)
362+
if curr_justified_epoch > 0
363+
else b"\x03" * 32,
364+
)
365+
366+
367+
# Phase0 field randomization has been moved to _randomize_phase0_fields()
368+
# in randomized_block_tests.py for better fork-specific organization
369+
370+
371+
# Fork-specific field randomization functions have been moved to
372+
# randomized_block_tests.py for better organization and separation of concerns.
373+
374+
299375
def randomize_state(spec, state, rng=None, exit_fraction=0.5, slash_fraction=0.5):
376+
"""
377+
Core randomization function that provides common field randomization across all forks.
378+
Fork-specific randomization is handled by the fork-specific variants in randomized_block_tests.py
379+
which call this function as their foundation.
380+
"""
300381
if rng is None:
301382
rng = Random(8020)
383+
384+
# Core validator lifecycle randomization (applies to all forks)
302385
set_some_new_deposits(spec, state, rng)
303386
exit_random_validators(spec, state, rng, fraction=exit_fraction)
304387
slash_random_validators(spec, state, rng, fraction=slash_fraction)
305388
randomize_attestation_participation(spec, state, rng)
389+
390+
# Fork-specific field randomization
306391
if is_post_electra(spec):
307392
set_some_pending_deposits(spec, state, rng)
308393
set_some_pending_partial_withdrawals(spec, state, rng)
309394
set_some_pending_consolidations(spec, state, rng)
395+
set_electra_churn_fields(spec, state, rng)
396+
set_deposit_request_fields(spec, state, rng)
397+
398+
# Apply withdrawal fields for Capella+
399+
if is_post_capella(spec):
400+
set_withdrawal_fields(spec, state, rng)
401+
402+
# Note: Fork-specific fields (Phase0, Altair, Bellatrix, Capella, Deneb) are now
403+
# handled by the corresponding randomize_state_[fork]() functions in randomized_block_tests.py
404+
# This provides better separation of concerns and cleaner incremental enhancement.
310405

311406

312407
def patch_state_to_non_leaking(spec, state):

tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py

Lines changed: 153 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
from collections.abc import Callable
88
from random import Random
99

10-
from eth2spec.test.helpers.blob import (
11-
get_sample_blob_tx,
12-
)
10+
from eth2spec.test.helpers.blob import get_sample_blob_tx
1311
from eth2spec.test.helpers.execution_payload import (
1412
build_randomized_execution_payload,
1513
compute_el_block_hash_for_block,
1614
)
17-
from eth2spec.test.helpers.inactivity_scores import (
18-
randomize_inactivity_scores,
15+
from eth2spec.test.helpers.forks import (
16+
is_post_altair,
17+
is_post_bellatrix,
18+
is_post_capella,
19+
is_post_deneb,
1920
)
21+
from eth2spec.test.helpers.inactivity_scores import randomize_inactivity_scores
2022
from eth2spec.test.helpers.multi_operations import (
2123
build_random_block_from_state_for_next_slot,
2224
get_random_bls_to_execution_changes,
@@ -60,6 +62,148 @@ def _randomize_deposit_state(spec, state, stats):
6062
}
6163

6264

65+
def _randomize_phase0_fields(spec, state):
66+
"""Set Phase0-specific fields to realistic non-default values."""
67+
if is_post_altair(spec):
68+
return
69+
70+
rng = Random(8020) # same seed as other randomization functions
71+
current_epoch = spec.get_current_epoch(state)
72+
73+
# Randomize ETH1 data votes (simulate realistic ETH1 voting)
74+
if len(state.eth1_data_votes) == 0:
75+
num_votes = rng.randint(1, min(10, spec.EPOCHS_PER_ETH1_VOTING_PERIOD))
76+
for i in range(num_votes):
77+
eth1_data = spec.Eth1Data(
78+
deposit_root=rng.randbytes(32),
79+
deposit_count=rng.randint(1, 1000),
80+
block_hash=rng.randbytes(32),
81+
)
82+
state.eth1_data_votes.append(eth1_data)
83+
84+
# Randomize historical roots
85+
if current_epoch > 0 and len(state.historical_roots) == 0:
86+
num_historical = rng.randint(0, min(3, current_epoch))
87+
for i in range(num_historical):
88+
state.historical_roots.append(rng.randbytes(32))
89+
90+
# Randomize RANDAO mixes
91+
for i in range(min(len(state.randao_mixes), spec.EPOCHS_PER_HISTORICAL_VECTOR)):
92+
if state.randao_mixes[i] == b"\x00" * 32: # Only modify empty ones
93+
state.randao_mixes[i] = rng.randbytes(32)
94+
95+
# Add some slashing penalties
96+
current_epoch_index = current_epoch % spec.EPOCHS_PER_SLASHINGS_VECTOR
97+
if state.slashings[current_epoch_index] == 0:
98+
penalty = spec.EFFECTIVE_BALANCE_INCREMENT * rng.randint(0, 10)
99+
state.slashings[current_epoch_index] = penalty
100+
101+
102+
def _randomize_altair_fields(spec, state):
103+
"""Set Altair-specific fields to realistic non-default values."""
104+
if not is_post_altair(spec):
105+
return
106+
107+
rng = Random(4242) # consistent seed with inactivity scores
108+
109+
# Simulate sync committee rotation to catch transition bugs
110+
if hasattr(state, "current_sync_committee") and hasattr(state, "next_sync_committee"):
111+
current_epoch = spec.get_current_epoch(state)
112+
active_validators = spec.get_active_validator_indices(state, current_epoch)
113+
114+
if len(active_validators) >= spec.SYNC_COMMITTEE_SIZE:
115+
shuffled_validators = list(active_validators)
116+
rng.shuffle(shuffled_validators)
117+
next_committee_indices = shuffled_validators[: spec.SYNC_COMMITTEE_SIZE]
118+
next_pubkeys = [state.validators[i].pubkey for i in next_committee_indices]
119+
state.next_sync_committee.pubkeys = next_pubkeys
120+
121+
if next_pubkeys:
122+
state.next_sync_committee.aggregate_pubkey = rng.randbytes(48)
123+
124+
125+
def _randomize_bellatrix_fields(spec, state):
126+
"""Set Bellatrix-specific fields to realistic non-default values."""
127+
if not is_post_bellatrix(spec):
128+
return
129+
130+
rng = Random(3456) # Use consistent seed with block randomization
131+
132+
if hasattr(state, "latest_execution_payload_header"):
133+
empty_header = spec.ExecutionPayloadHeader()
134+
if state.latest_execution_payload_header == empty_header:
135+
state.latest_execution_payload_header = spec.ExecutionPayloadHeader(
136+
parent_hash=rng.randbytes(32),
137+
fee_recipient=rng.randbytes(20),
138+
state_root=rng.randbytes(32),
139+
receipts_root=rng.randbytes(32),
140+
logs_bloom=rng.randbytes(spec.BYTES_PER_LOGS_BLOOM),
141+
prev_randao=rng.randbytes(32),
142+
block_number=rng.randint(1, 1000000),
143+
gas_limit=rng.randint(8000000, 30000000),
144+
gas_used=rng.randint(100000, 15000000),
145+
timestamp=rng.randint(1609459200, 2000000000),
146+
extra_data=rng.randbytes(rng.randint(0, 32)),
147+
base_fee_per_gas=rng.randint(
148+
1, 100000000000
149+
),
150+
block_hash=rng.randbytes(32),
151+
transactions_root=rng.randbytes(32),
152+
)
153+
154+
155+
def _randomize_capella_fields(spec, state):
156+
"""Set Capella-specific fields to realistic non-default values."""
157+
if not is_post_capella(spec):
158+
return
159+
160+
rng = Random(7890) # Use consistent seed
161+
162+
# Randomize withdrawal credentials to simulate realistic validator states
163+
if hasattr(state, "validators"):
164+
num_validators = len(state.validators)
165+
166+
# Set some validators to have ETH1 withdrawal credentials (0x01 prefix)
167+
# to simulate realistic pre-Capella state where some validators haven't
168+
# updated their credentials yet
169+
for i in range(min(num_validators, 20)):
170+
validator = state.validators[i]
171+
172+
# ~30% chance to set ETH1 withdrawal credentials
173+
if rng.random() < 0.3:
174+
eth1_address = rng.randbytes(20)
175+
validator.withdrawal_credentials = b"\x01" + b"\x00" * 11 + eth1_address
176+
177+
178+
def _randomize_deneb_fields(spec, state):
179+
"""Set Deneb-specific fields to realistic non-default values."""
180+
if not is_post_deneb(spec):
181+
return
182+
183+
rng = Random(9999) # Use consistent seed
184+
185+
if hasattr(state, "historical_summaries") and len(state.historical_summaries) == 0:
186+
current_epoch = spec.get_current_epoch(state)
187+
num_summaries = rng.randint(0, min(3, current_epoch // 100))
188+
189+
for i in range(num_summaries):
190+
historical_summary = spec.HistoricalSummary(
191+
block_summary_root=rng.randbytes(32),
192+
state_summary_root=rng.randbytes(32),
193+
)
194+
state.historical_summaries.append(historical_summary)
195+
196+
197+
def randomize_state_phase0(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
198+
scenario_state = randomize_state(
199+
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
200+
)
201+
202+
# Initialize Phase0-specific randomization
203+
_randomize_phase0_fields(spec, state)
204+
return scenario_state
205+
206+
63207
def randomize_state(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
64208
randomize_state_helper(spec, state, exit_fraction=exit_fraction, slash_fraction=slash_fraction)
65209
scenario_state = _randomize_deposit_state(spec, state, stats)
@@ -71,30 +215,31 @@ def randomize_state_altair(spec, state, stats, exit_fraction=0.1, slash_fraction
71215
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
72216
)
73217
randomize_inactivity_scores(spec, state)
218+
_randomize_altair_fields(spec, state)
74219
return scenario_state
75220

76221

77222
def randomize_state_bellatrix(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
78223
scenario_state = randomize_state_altair(
79224
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
80225
)
81-
# TODO: randomize execution payload, merge status, etc.
226+
_randomize_bellatrix_fields(spec, state)
82227
return scenario_state
83228

84229

85230
def randomize_state_capella(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
86231
scenario_state = randomize_state_bellatrix(
87232
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
88233
)
89-
# TODO: randomize withdrawals
234+
_randomize_capella_fields(spec, state)
90235
return scenario_state
91236

92237

93238
def randomize_state_deneb(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
94239
scenario_state = randomize_state_capella(
95240
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
96241
)
97-
# TODO: randomize execution payload
242+
_randomize_deneb_fields(spec, state)
98243
return scenario_state
99244

100245

0 commit comments

Comments
 (0)