Skip to content

Commit ca9f91e

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 44db4a3 commit ca9f91e

File tree

2 files changed

+231
-8
lines changed

2 files changed

+231
-8
lines changed

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

Lines changed: 83 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,99 @@ 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+
299367
def randomize_state(spec, state, rng=None, exit_fraction=0.5, slash_fraction=0.5):
368+
"""
369+
Core randomization function that provides common field randomization across all forks.
370+
Fork-specific randomization is handled by the fork-specific variants in randomized_block_tests.py
371+
which call this function as their foundation.
372+
"""
300373
if rng is None:
301374
rng = Random(8020)
375+
376+
# Core validator lifecycle randomization (applies to all forks)
302377
set_some_new_deposits(spec, state, rng)
303378
exit_random_validators(spec, state, rng, fraction=exit_fraction)
304379
slash_random_validators(spec, state, rng, fraction=slash_fraction)
305380
randomize_attestation_participation(spec, state, rng)
381+
382+
# Fork-specific field randomization
306383
if is_post_electra(spec):
307384
set_some_pending_deposits(spec, state, rng)
308385
set_some_pending_partial_withdrawals(spec, state, rng)
309386
set_some_pending_consolidations(spec, state, rng)
387+
set_electra_churn_fields(spec, state, rng)
388+
set_deposit_request_fields(spec, state, rng)
389+
390+
# Apply withdrawal fields for Capella+
391+
if is_post_capella(spec):
392+
set_withdrawal_fields(spec, state, rng)
310393

311394

312395
def patch_state_to_non_leaking(spec, state):

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

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

76216

77217
def randomize_state_bellatrix(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
78218
scenario_state = randomize_state_altair(
79219
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
80220
)
81-
# TODO: randomize execution payload, merge status, etc.
221+
_randomize_bellatrix_fields(spec, state)
82222
return scenario_state
83223

84224

85225
def randomize_state_capella(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
86226
scenario_state = randomize_state_bellatrix(
87227
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
88228
)
89-
# TODO: randomize withdrawals
229+
_randomize_capella_fields(spec, state)
90230
return scenario_state
91231

92232

93233
def randomize_state_deneb(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
94234
scenario_state = randomize_state_capella(
95235
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
96236
)
97-
# TODO: randomize execution payload
237+
_randomize_deneb_fields(spec, state)
98238
return scenario_state
99239

100240

0 commit comments

Comments
 (0)