Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions tests/core/pyspec/eth2spec/test/helpers/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from eth2spec.test.helpers.deposits import mock_deposit
from eth2spec.test.helpers.forks import (
is_post_altair,
is_post_capella,
is_post_electra,
)
from eth2spec.test.helpers.state import next_epoch
Expand Down Expand Up @@ -296,17 +297,99 @@ def set_some_pending_consolidations(spec, state, rng):
return consolidation_pairs


def set_electra_churn_fields(spec, state, rng):
"""Set Electra churn-related fields to realistic non-default values if post-Electra."""
current_epoch = spec.get_current_epoch(state)

state.earliest_exit_epoch = current_epoch + rng.randint(0, 4)
state.earliest_consolidation_epoch = current_epoch + rng.randint(0, 3)

# Set realistic churn balances
max_churn = spec.EFFECTIVE_BALANCE_INCREMENT * 10
state.deposit_balance_to_consume = rng.randint(0, max_churn)
state.exit_balance_to_consume = rng.randint(0, max_churn)
state.consolidation_balance_to_consume = rng.randint(0, max_churn)


def set_withdrawal_fields(spec, state, rng):
"""Set withdrawal-related fields to realistic non-default values if post-Capella."""
state.next_withdrawal_index = rng.randint(1, 1000)
# Ensure next_withdrawal_validator_index is non-zero and valid
if len(state.validators) > 1:
state.next_withdrawal_validator_index = rng.randint(1, len(state.validators) - 1)
else:
state.next_withdrawal_validator_index = 0


def set_deposit_request_fields(spec, state, rng):
"""Set deposit request fields to realistic non-default values if post-Electra."""
if state.deposit_requests_start_index == spec.FAR_FUTURE_EPOCH:
state.deposit_requests_start_index = rng.randint(100, 10000)


def set_finality_fields(spec, state, rng):
"""Set finality fields to realistic non-default values safely."""
current_epoch = spec.get_current_epoch(state)

# Only modify if they're still at genesis defaults
if (
state.finalized_checkpoint.epoch == 0
and state.current_justified_checkpoint.epoch == 0
and state.previous_justified_checkpoint.epoch == 0
):
# Set realistic but valid finality progression
if current_epoch > 3:
finalized_epoch = max(0, current_epoch - 3)
prev_justified_epoch = max(finalized_epoch, current_epoch - 2)
curr_justified_epoch = max(prev_justified_epoch, current_epoch - 1)

# Create realistic checkpoint progression
state.finalized_checkpoint = spec.Checkpoint(
epoch=finalized_epoch,
root=spec.get_block_root(state, finalized_epoch)
if finalized_epoch > 0
else b"\x01" * 32,
)
state.previous_justified_checkpoint = spec.Checkpoint(
epoch=prev_justified_epoch,
root=spec.get_block_root(state, prev_justified_epoch)
if prev_justified_epoch > 0
else b"\x02" * 32,
)
state.current_justified_checkpoint = spec.Checkpoint(
epoch=curr_justified_epoch,
root=spec.get_block_root(state, curr_justified_epoch)
if curr_justified_epoch > 0
else b"\x03" * 32,
)


def randomize_state(spec, state, rng=None, exit_fraction=0.5, slash_fraction=0.5):
"""
Core randomization function that provides common field randomization across all forks.
Fork-specific randomization is handled by the fork-specific variants in randomized_block_tests.py
which call this function as their foundation.
"""
if rng is None:
rng = Random(8020)

# Core validator lifecycle randomization (applies to all forks)
set_some_new_deposits(spec, state, rng)
exit_random_validators(spec, state, rng, fraction=exit_fraction)
slash_random_validators(spec, state, rng, fraction=slash_fraction)
randomize_attestation_participation(spec, state, rng)

# Fork-specific field randomization
if is_post_electra(spec):
set_some_pending_deposits(spec, state, rng)
set_some_pending_partial_withdrawals(spec, state, rng)
set_some_pending_consolidations(spec, state, rng)
set_electra_churn_fields(spec, state, rng)
set_deposit_request_fields(spec, state, rng)

# Apply withdrawal fields for Capella+
if is_post_capella(spec):
set_withdrawal_fields(spec, state, rng)


def patch_state_to_non_leaking(spec, state):
Expand Down
159 changes: 150 additions & 9 deletions tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
from collections.abc import Callable
from random import Random

from eth2spec.test.helpers.blob import (
get_sample_blob_tx,
)
from eth2spec.test.helpers.blob import get_sample_blob_tx
from eth2spec.test.helpers.execution_payload import (
build_randomized_execution_payload,
compute_el_block_hash_for_block,
)
from eth2spec.test.helpers.inactivity_scores import (
randomize_inactivity_scores,
from eth2spec.test.helpers.forks import (
is_post_altair,
is_post_bellatrix,
is_post_capella,
is_post_deneb,
)
from eth2spec.test.helpers.inactivity_scores import randomize_inactivity_scores
from eth2spec.test.helpers.multi_operations import (
build_random_block_from_state_for_next_slot,
get_random_bls_to_execution_changes,
Expand All @@ -34,6 +36,7 @@
next_slot,
state_transition_and_sign_block,
)
from eth2spec.utils import bls

# primitives:
# state
Expand All @@ -60,41 +63,179 @@ def _randomize_deposit_state(spec, state, stats):
}


def _randomize_phase0_fields(spec, state):
"""Set Phase0-specific fields to realistic non-default values."""

rng = Random(8020) # same seed as other randomization functions
current_epoch = spec.get_current_epoch(state)

# Randomize ETH1 data votes (simulate realistic ETH1 voting)
if len(state.eth1_data_votes) == 0:
num_votes = rng.randint(1, min(10, spec.EPOCHS_PER_ETH1_VOTING_PERIOD))
for i in range(num_votes):
eth1_data = spec.Eth1Data(
deposit_root=rng.randbytes(32),
deposit_count=rng.randint(1, 1000),
block_hash=rng.randbytes(32),
)
state.eth1_data_votes.append(eth1_data)

# Randomize historical roots
if current_epoch > 0 and len(state.historical_roots) == 0:
num_historical = rng.randint(0, min(3, current_epoch))
for i in range(num_historical):
state.historical_roots.append(rng.randbytes(32))

# Randomize RANDAO mixes
for i in range(min(len(state.randao_mixes), spec.EPOCHS_PER_HISTORICAL_VECTOR)):
if state.randao_mixes[i] == b"\x00" * 32: # Only modify empty ones
state.randao_mixes[i] = rng.randbytes(32)

# Add some slashing penalties
current_epoch_index = current_epoch % spec.EPOCHS_PER_SLASHINGS_VECTOR
if state.slashings[current_epoch_index] == 0:
penalty = spec.EFFECTIVE_BALANCE_INCREMENT * rng.randint(0, 10)
state.slashings[current_epoch_index] = penalty


def _randomize_altair_fields(spec, state):
"""Set Altair-specific fields to realistic non-default values."""
if not is_post_altair(spec):
return

rng = Random(4242) # consistent seed with inactivity scores

# Simulate sync committee rotation to catch transition bugs
if hasattr(state, "current_sync_committee") and hasattr(state, "next_sync_committee"):
current_epoch = spec.get_current_epoch(state)
active_validators = spec.get_active_validator_indices(state, current_epoch)

if len(active_validators) >= spec.SYNC_COMMITTEE_SIZE:
shuffled_validators = list(active_validators)
rng.shuffle(shuffled_validators)
next_committee_indices = shuffled_validators[: spec.SYNC_COMMITTEE_SIZE]
next_pubkeys = [state.validators[i].pubkey for i in next_committee_indices]
state.next_sync_committee.pubkeys = next_pubkeys

if next_pubkeys:
state.next_sync_committee.aggregate_pubkey = bls.AggregatePKs(next_pubkeys)


def _randomize_bellatrix_fields(spec, state):
"""Set Bellatrix-specific fields to realistic non-default values."""
if not is_post_bellatrix(spec):
return

rng = Random(3456) # consistent seed with block randomization

if hasattr(state, "latest_execution_payload_header"):
empty_header = spec.ExecutionPayloadHeader()
if state.latest_execution_payload_header == empty_header:
state.latest_execution_payload_header = spec.ExecutionPayloadHeader(
parent_hash=rng.randbytes(32),
fee_recipient=rng.randbytes(20),
state_root=rng.randbytes(32),
receipts_root=rng.randbytes(32),
logs_bloom=rng.randbytes(spec.BYTES_PER_LOGS_BLOOM),
prev_randao=rng.randbytes(32),
block_number=rng.randint(1, 1000000),
gas_limit=rng.randint(8000000, 30000000),
gas_used=rng.randint(100000, 15000000),
timestamp=rng.randint(1609459200, 2000000000),
extra_data=rng.randbytes(rng.randint(0, 32)),
base_fee_per_gas=rng.randint(1, 100000000000),
block_hash=rng.randbytes(32),
transactions_root=rng.randbytes(32),
)


def _randomize_capella_fields(spec, state):
"""Set Capella-specific fields to realistic non-default values."""
if not is_post_capella(spec):
return

rng = Random(7890)

# Randomize withdrawal credentials to simulate realistic validator states
if hasattr(state, "validators"):
num_validators = len(state.validators)

# Set some validators to have ETH1 withdrawal credentials (0x01 prefix)
# to simulate realistic pre-Capella state where some validators haven't
# updated their credentials yet
for i in range(min(num_validators, 20)):
validator = state.validators[i]

# ~30% chance to set ETH1 withdrawal credentials
if rng.random() < 0.3:
eth1_address = rng.randbytes(20)
validator.withdrawal_credentials = b"\x01" + b"\x00" * 11 + eth1_address


def _randomize_deneb_fields(spec, state):
"""Set Deneb-specific fields to realistic non-default values."""
if not is_post_deneb(spec):
return

rng = Random(9999)

if hasattr(state, "historical_summaries") and len(state.historical_summaries) == 0:
current_epoch = spec.get_current_epoch(state)
num_summaries = rng.randint(0, min(3, current_epoch // 100))

for i in range(num_summaries):
historical_summary = spec.HistoricalSummary(
block_summary_root=rng.randbytes(32),
state_summary_root=rng.randbytes(32),
)
state.historical_summaries.append(historical_summary)


def randomize_state_phase0(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
scenario_state = randomize_state(
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
)

_randomize_phase0_fields(spec, state)
return scenario_state


def randomize_state(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
randomize_state_helper(spec, state, exit_fraction=exit_fraction, slash_fraction=slash_fraction)
scenario_state = _randomize_deposit_state(spec, state, stats)
return scenario_state


def randomize_state_altair(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
scenario_state = randomize_state(
scenario_state = randomize_state_phase0(
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
)
randomize_inactivity_scores(spec, state)
_randomize_altair_fields(spec, state)
return scenario_state


def randomize_state_bellatrix(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
scenario_state = randomize_state_altair(
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
)
# TODO: randomize execution payload, merge status, etc.
_randomize_bellatrix_fields(spec, state)
return scenario_state


def randomize_state_capella(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
scenario_state = randomize_state_bellatrix(
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
)
# TODO: randomize withdrawals
_randomize_capella_fields(spec, state)
return scenario_state


def randomize_state_deneb(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1):
scenario_state = randomize_state_capella(
spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction
)
# TODO: randomize execution payload
_randomize_deneb_fields(spec, state)
return scenario_state


Expand Down