diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py index 70c871a34..8f095aebb 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/random.py +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -20,16 +20,20 @@ def set_some_new_deposits(spec, state, rng): state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) -def exit_random_validators(spec, state, rng): +def exit_random_validators(spec, state, rng, fraction=None): + if fraction is None: + # Exit ~1/2 + fraction = 0.5 + if spec.get_current_epoch(state) < 5: # Move epochs forward to allow for some validators already exited/withdrawable for _ in range(5): next_epoch(spec, state) current_epoch = spec.get_current_epoch(state) - # Exit ~1/2 of validators for index in spec.get_active_validator_indices(state, current_epoch): - if rng.choice([True, False]): + sampled = rng.random() < fraction + if not sampled: continue validator = state.validators[index] @@ -41,11 +45,15 @@ def exit_random_validators(spec, state, rng): validator.withdrawable_epoch = current_epoch + 1 -def slash_random_validators(spec, state, rng): - # Slash ~1/2 of validators +def slash_random_validators(spec, state, rng, fraction=None): + if fraction is None: + # Slash ~1/2 of validators + fraction = 0.5 + for index in range(len(state.validators)): # slash at least one validator - if index == 0 or rng.choice([True, False]): + sampled = rng.random() < fraction + if index == 0 or sampled: spec.slash_validator(state, index) @@ -115,8 +123,8 @@ def randomize_attestation_participation(spec, state, rng=Random(8020)): randomize_epoch_participation(spec, state, spec.get_current_epoch(state), rng) -def randomize_state(spec, state, rng=Random(8020)): +def randomize_state(spec, state, rng=Random(8020), exit_fraction=None, slash_fraction=None): set_some_new_deposits(spec, state, rng) - exit_random_validators(spec, state, rng) - slash_random_validators(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) diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks_random.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks_random.py index f015362ac..d4740d080 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks_random.py @@ -1,7 +1,7 @@ import itertools from random import Random from typing import Callable -from tests.core.pyspec.eth2spec.test.context import default_activation_threshold, misc_balances_in_default_range +from tests.core.pyspec.eth2spec.test.context import misc_balances_in_default_range, zero_activation_threshold from eth2spec.test.helpers.multi_operations import ( build_random_block_from_state, ) @@ -24,7 +24,17 @@ from eth2spec.test.context import ( misc_balances, ) +# May need to make several attempts to find a block that does not correspond to a slashed +# proposer with the randomization helpers... +BLOCK_ATTEMPTS = 32 + # primitives +## state + +def _randomize_state(spec, state): + return randomize_state(spec, state, exit_fraction=0.1, slash_fraction=0.1) + + ## epochs def _epochs_until_leak(spec): @@ -57,8 +67,23 @@ def _no_block(_spec, _pre_state, _signed_blocks): return None -def _random_block_for_next_slot(spec, pre_state, _signed_blocks): - return build_random_block_from_state(spec, pre_state) +def _random_block(spec, state, _signed_blocks): + """ + Produce a random block. + NOTE: this helper may mutate state, as it will attempt + to produce a block over ``BLOCK_ATTEMPTS`` slots in order + to find a valid block in the event that the proposer has already been slashed. + """ + block = build_random_block_from_state(spec, state) + for _ in range(BLOCK_ATTEMPTS): + proposer = state.validators[block.proposer_index] + if proposer.slashed: + next_slot(spec, state) + block = build_random_block_from_state(spec, state) + else: + return block + else: + raise AssertionError("could not find a block with an unslashed proposer, check ``state`` input") ## validations @@ -105,7 +130,7 @@ def _transition_with_random_block(epochs=None, slots=None): number of epochs or slots before applying the random block. """ transition = { - "block_producer": _random_block_for_next_slot, + "block_producer": _random_block, } if epochs: transition.update(epochs) @@ -137,7 +162,7 @@ def _randomized_scenario_setup(): # NOTE: the block randomization function assumes at least 1 shard committee period # so advance the state before doing anything else. (_skip_epochs(_epochs_for_shard_committee_period), _no_op_validation), - (randomize_state, ensure_state_has_validators_across_lifecycle), + (_randomize_state, ensure_state_has_validators_across_lifecycle), ) @@ -272,7 +297,7 @@ def _iter_temporal(spec, callable_or_int): @pytest_generate_tests_adapter @with_all_phases -@with_custom_state(balances_fn=misc_balances_in_default_range, threshold_fn=default_activation_threshold) +@with_custom_state(balances_fn=misc_balances_in_default_range, threshold_fn=zero_activation_threshold) @spec_test @single_phase @always_bls