diff --git a/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py b/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py index f7d2fa9c8..9bc0f4841 100644 --- a/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py +++ b/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_inactivity_updates.py @@ -3,10 +3,15 @@ from random import Random from eth2spec.test.context import spec_state_test, with_altair_and_later from eth2spec.test.helpers.inactivity_scores import randomize_inactivity_scores, zero_inactivity_scores from eth2spec.test.helpers.state import ( + next_epoch, next_epoch_via_block, set_full_participation, set_empty_participation, ) +from eth2spec.test.helpers.voluntary_exits import ( + exit_validators, + get_exited_validators +) from eth2spec.test.helpers.epoch_processing import ( run_epoch_processing_with ) @@ -266,3 +271,53 @@ def test_some_slashed_full_random_leaking(spec, state): # Check still in leak assert spec.is_in_inactivity_leak(state) + + +@with_altair_and_later +@spec_state_test +@leaking() +def test_some_exited_full_random_leaking(spec, state): + rng = Random(1102233) + + exit_count = 3 + + # randomize ahead of time to check exited validators do not have + # mutations applied to their inactivity scores + randomize_inactivity_scores(spec, state, rng=rng) + + assert not any(get_exited_validators(spec, state)) + exited_indices = exit_validators(spec, state, exit_count, rng=rng) + assert not any(get_exited_validators(spec, state)) + + # advance the state to effect the exits + target_epoch = max(state.validators[index].exit_epoch for index in exited_indices) + # validators that have exited in the previous epoch or earlier will not + # have their inactivity scores modified, the test advances the state past this point + # to confirm this invariant: + previous_epoch = spec.get_previous_epoch(state) + for _ in range(target_epoch - previous_epoch): + next_epoch(spec, state) + assert len(get_exited_validators(spec, state)) == exit_count + + previous_scores = state.inactivity_scores.copy() + + yield from run_inactivity_scores_test( + spec, state, + randomize_previous_epoch_participation, rng=rng, + ) + + # ensure exited validators have their score "frozen" at exit + # but otherwise there was a change + some_changed = False + for index in range(len(state.validators)): + if index in exited_indices: + assert previous_scores[index] == state.inactivity_scores[index] + else: + previous_score = previous_scores[index] + current_score = state.inactivity_scores[index] + if previous_score != current_score: + some_changed = True + assert some_changed + + # Check still in leak + assert spec.is_in_inactivity_leak(state) diff --git a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py index 28232cc23..73d4598b3 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py +++ b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py @@ -1,3 +1,4 @@ +from random import Random from eth2spec.utils import bls from eth2spec.test.helpers.keys import privkeys @@ -23,3 +24,21 @@ def sign_voluntary_exit(spec, state, voluntary_exit, privkey): message=voluntary_exit, signature=bls.Sign(privkey, signing_root) ) + + +# +# Helpers for applying effects of a voluntary exit +# +def get_exited_validators(spec, state): + current_epoch = spec.get_current_epoch(state) + return [index for (index, validator) in enumerate(state.validators) if validator.exit_epoch <= current_epoch] + + +def exit_validators(spec, state, validator_count, rng=None): + if rng is None: + rng = Random(1337) + + indices = rng.sample(range(len(state.validators)), validator_count) + for index in indices: + spec.initiate_validator_exit(state, index) + return indices