Skip to content

Commit b6f3115

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 b6f3115

File tree

2 files changed

+233
-9
lines changed

2 files changed

+233
-9
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: 150 additions & 9 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,
@@ -34,6 +36,7 @@
3436
next_slot,
3537
state_transition_and_sign_block,
3638
)
39+
from eth2spec.utils import bls
3740

3841
# primitives:
3942
# state
@@ -60,41 +63,179 @@ def _randomize_deposit_state(spec, state, stats):
6063
}
6164

6265

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

68208

69209
def randomize_state_altair(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
70-
scenario_state = randomize_state(
210+
scenario_state = randomize_state_phase0(
71211
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
72212
)
73213
randomize_inactivity_scores(spec, state)
214+
_randomize_altair_fields(spec, state)
74215
return scenario_state
75216

76217

77218
def randomize_state_bellatrix(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
78219
scenario_state = randomize_state_altair(
79220
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
80221
)
81-
# TODO: randomize execution payload, merge status, etc.
222+
_randomize_bellatrix_fields(spec, state)
82223
return scenario_state
83224

84225

85226
def randomize_state_capella(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
86227
scenario_state = randomize_state_bellatrix(
87228
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
88229
)
89-
# TODO: randomize withdrawals
230+
_randomize_capella_fields(spec, state)
90231
return scenario_state
91232

92233

93234
def randomize_state_deneb(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
94235
scenario_state = randomize_state_capella(
95236
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
96237
)
97-
# TODO: randomize execution payload
238+
_randomize_deneb_fields(spec, state)
98239
return scenario_state
99240

100241

0 commit comments

Comments
 (0)