adjust some helper code for randomized environment

1. randomized block helpers assume most of the validator set is not slashed
2. `randomize_state` helper slashes or exits ~1/2 of the validator set

So, adjust helpers to be less aggresive with exits and slashings and
to skip elements as needed if we happen to make something by
a validator who has been slashed.
This commit is contained in:
Alex Stokes 2021-08-21 17:36:50 -07:00
parent 6da2c7a916
commit 86643d805a
No known key found for this signature in database
GPG Key ID: 99B3D88FD6C55A69
2 changed files with 48 additions and 15 deletions

View File

@ -20,16 +20,20 @@ def set_some_new_deposits(spec, state, rng):
state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) 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: if spec.get_current_epoch(state) < 5:
# Move epochs forward to allow for some validators already exited/withdrawable # Move epochs forward to allow for some validators already exited/withdrawable
for _ in range(5): for _ in range(5):
next_epoch(spec, state) next_epoch(spec, state)
current_epoch = spec.get_current_epoch(state) current_epoch = spec.get_current_epoch(state)
# Exit ~1/2 of validators
for index in spec.get_active_validator_indices(state, current_epoch): for index in spec.get_active_validator_indices(state, current_epoch):
if rng.choice([True, False]): sampled = rng.random() < fraction
if not sampled:
continue continue
validator = state.validators[index] validator = state.validators[index]
@ -41,11 +45,15 @@ def exit_random_validators(spec, state, rng):
validator.withdrawable_epoch = current_epoch + 1 validator.withdrawable_epoch = current_epoch + 1
def slash_random_validators(spec, state, rng): def slash_random_validators(spec, state, rng, fraction=None):
# Slash ~1/2 of validators if fraction is None:
# Slash ~1/2 of validators
fraction = 0.5
for index in range(len(state.validators)): for index in range(len(state.validators)):
# slash at least one validator # 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) 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) 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) set_some_new_deposits(spec, state, rng)
exit_random_validators(spec, state, rng) exit_random_validators(spec, state, rng, fraction=exit_fraction)
slash_random_validators(spec, state, rng) slash_random_validators(spec, state, rng, fraction=slash_fraction)
randomize_attestation_participation(spec, state, rng) randomize_attestation_participation(spec, state, rng)

View File

@ -1,7 +1,7 @@
import itertools import itertools
from random import Random from random import Random
from typing import Callable 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 ( from eth2spec.test.helpers.multi_operations import (
build_random_block_from_state, build_random_block_from_state,
) )
@ -24,7 +24,17 @@ from eth2spec.test.context import (
misc_balances, 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 # primitives
## state
def _randomize_state(spec, state):
return randomize_state(spec, state, exit_fraction=0.1, slash_fraction=0.1)
## epochs ## epochs
def _epochs_until_leak(spec): def _epochs_until_leak(spec):
@ -57,8 +67,23 @@ def _no_block(_spec, _pre_state, _signed_blocks):
return None return None
def _random_block_for_next_slot(spec, pre_state, _signed_blocks): def _random_block(spec, state, _signed_blocks):
return build_random_block_from_state(spec, pre_state) """
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 ## validations
@ -105,7 +130,7 @@ def _transition_with_random_block(epochs=None, slots=None):
number of epochs or slots before applying the random block. number of epochs or slots before applying the random block.
""" """
transition = { transition = {
"block_producer": _random_block_for_next_slot, "block_producer": _random_block,
} }
if epochs: if epochs:
transition.update(epochs) transition.update(epochs)
@ -137,7 +162,7 @@ def _randomized_scenario_setup():
# NOTE: the block randomization function assumes at least 1 shard committee period # NOTE: the block randomization function assumes at least 1 shard committee period
# so advance the state before doing anything else. # so advance the state before doing anything else.
(_skip_epochs(_epochs_for_shard_committee_period), _no_op_validation), (_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 @pytest_generate_tests_adapter
@with_all_phases @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 @spec_test
@single_phase @single_phase
@always_bls @always_bls