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 2a971f4f0..f262bcdd2 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 @@ -1,7 +1,11 @@ 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.inactivity_scores import ( + randomize_inactivity_scores, + zero_inactivity_scores, + slash_some_validators_for_inactivity_scores_test, +) from eth2spec.test.helpers.state import ( next_epoch, next_epoch_via_block, @@ -201,20 +205,6 @@ def test_random_inactivity_scores_full_participation_leaking(spec, state): assert spec.is_in_inactivity_leak(state) -def slash_some_validators_for_inactivity_scores_test(spec, state, rng=Random(40404040)): - # ``run_inactivity_scores_test`` runs at the next epoch from `state`. - # We retrieve the proposer of this future state to avoid - # accidentally slashing that validator - future_state = state.copy() - next_epoch_via_block(spec, future_state) - - proposer_index = spec.get_beacon_proposer_index(future_state) - # Slash ~1/4 of validaors - for validator_index in range(len(state.validators)): - if rng.choice(range(4)) == 0 and validator_index != proposer_index: - spec.slash_validator(state, validator_index) - - @with_altair_and_later @spec_state_test def test_some_slashed_zero_scores_full_participation(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index 62740df4e..9996a1278 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -1,12 +1,24 @@ import random from eth2spec.test.context import fork_transition_test from eth2spec.test.helpers.constants import PHASE0, ALTAIR -from eth2spec.test.helpers.state import state_transition_and_sign_block, next_slot, next_epoch_via_signed_block +from eth2spec.test.helpers.state import ( + next_epoch_via_signed_block, + next_slot, + state_transition_and_sign_block, + transition_to, +) from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block +from eth2spec.test.helpers.deposits import ( + prepare_state_and_deposit, +) from eth2spec.test.helpers.attestations import next_slots_with_attestations +from eth2spec.test.helpers.random import set_some_new_deposits +from eth2spec.test.helpers.inactivity_scores import ( + slash_some_validators_for_inactivity_scores_test, +) -def _state_transition_and_sign_block_at_slot(spec, state): +def _state_transition_and_sign_block_at_slot(spec, state, deposits=None): """ Cribbed from ``transition_unsigned_block`` helper where the early parts of the state transition have already @@ -15,6 +27,9 @@ def _state_transition_and_sign_block_at_slot(spec, state): Used to produce a block during an irregular state transition. """ block = build_empty_block(spec, state) + # FIXME: not just passing `deposits` + if deposits is not None: + block.body.deposits = deposits assert state.latest_block_header.slot < block.slot assert state.slot == block.slot @@ -62,7 +77,29 @@ def _state_transition_across_slots(spec, state, to_slot, block_filter=_all_block next_slot(spec, state) -def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True): +def _state_transition_across_slots_with_ignoring_proposers(spec, state, to_slot, ignoring_proposers): + """ + The slashed validators can't be proposers. Here we ignore the given `ignoring_proposers` + and ensure that the result state was computed with a block with slot >= to_slot. + """ + assert state.slot < to_slot + + found_valid = False + while state.slot < to_slot or not found_valid: + future_state = state.copy() + next_slot(spec, future_state) + proposer_index = spec.get_beacon_proposer_index(future_state) + if proposer_index not in ignoring_proposers: + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield signed_block + if state.slot >= to_slot: + found_valid = True + else: + next_slot(spec, state) + + +def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True, deposits=None): spec.process_slots(state, state.slot + 1) assert state.slot % spec.SLOTS_PER_EPOCH == 0 @@ -75,11 +112,25 @@ def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True): assert state.fork.current_version == post_spec.config.ALTAIR_FORK_VERSION if with_block: - return state, _state_transition_and_sign_block_at_slot(post_spec, state) + return state, _state_transition_and_sign_block_at_slot(post_spec, state, deposits=deposits) else: return state, None +def _set_validators_exit_epoch(spec, state, exit_epoch, rng=random.Random(40404040), fraction=0.25): + """ + Set some valdiators' exit_epoch. + """ + selected_count = int(len(state.validators) * fraction) + selected_indices = rng.sample(range(len(state.validators)), selected_count) + for validator_index in selected_indices: + state.validators[validator_index].exit_epoch = exit_epoch + state.validators[validator_index].withdrawable_epoch = ( + exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ) + return selected_indices + + @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ @@ -434,3 +485,353 @@ def test_transition_with_no_attestations_until_after_fork(state, fork_epoch, spe yield "blocks", blocks yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=1) +def test_transition_with_one_fourth_slashed_active_validators_pre_fork( + state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + 1/4 validators are slashed but still active at the fork transition. + """ + # slash 1/4 validators + selected_indices = slash_some_validators_for_inactivity_scores_test( + spec, state, rng=random.Random(5566), fraction=0.25) + assert len(selected_indices) > 0 + + # check if some validators are slashed but still active + for validator_index in selected_indices: + validator = state.validators[validator_index] + assert validator.slashed + assert spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert not spec.is_in_inactivity_leak(state) + + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + # since the proposer might have been slashed, here we only create blocks with non-slashed proposers + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots_with_ignoring_proposers(post_spec, state, to_slot, selected_indices) + ]) + + # check post state + for validator in state.validators: + assert post_spec.is_active_validator(validator, post_spec.get_current_epoch(state)) + assert not post_spec.is_in_inactivity_leak(state) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_one_fourth_exiting_validators_exit_post_fork( + state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + 1/4 exiting but still active validators at the fork transition. + """ + exited_indices = _set_validators_exit_epoch(spec, state, exit_epoch=10, rng=random.Random(5566), fraction=0.25) + + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + # check pre state + assert len(exited_indices) > 0 + for index in exited_indices: + validator = state.validators[index] + assert not validator.slashed + assert fork_epoch < validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert not spec.is_in_inactivity_leak(state) + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # ensure that some of the current sync committee members are exiting + exited_pubkeys = [state.validators[index].pubkey for index in exited_indices] + assert any(set(exited_pubkeys).intersection(list(state.current_sync_committee.pubkeys))) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + # check state + for index in exited_indices: + validator = state.validators[index] + assert not validator.slashed + assert post_spec.is_active_validator(validator, post_spec.get_current_epoch(state)) + assert not post_spec.is_in_inactivity_leak(state) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_one_fourth_exiting_validators_exit_at_fork( + state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + 1/4 exiting but still active validators at the fork transition. + """ + exited_indices = _set_validators_exit_epoch(spec, state, exit_epoch=2, rng=random.Random(5566), fraction=0.25) + + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + # check pre state + assert len(exited_indices) > 0 + for index in exited_indices: + validator = state.validators[index] + assert not validator.slashed + assert fork_epoch == validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert not spec.is_in_inactivity_leak(state) + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # check post transition state + for index in exited_indices: + validator = state.validators[index] + assert not validator.slashed + assert not post_spec.is_active_validator(validator, post_spec.get_current_epoch(state)) + assert not post_spec.is_in_inactivity_leak(state) + + # ensure that none of the current sync committee members are exited validators + exited_pubkeys = [state.validators[index].pubkey for index in exited_indices] + assert not any(set(exited_pubkeys).intersection(list(state.current_sync_committee.pubkeys))) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=7) +def test_transition_with_leaking_pre_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Leaking starts at epoch 6 (MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2). + The leaking starts before the fork transition in this case. + """ + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + assert spec.is_in_inactivity_leak(state) + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # check post transition state + assert spec.is_in_inactivity_leak(state) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=6) +def test_transition_with_leaking_at_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Leaking starts at epoch 6 (MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2). + The leaking starts at the fork transition in this case. + """ + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + assert not spec.is_in_inactivity_leak(state) + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # check post transition state + assert spec.is_in_inactivity_leak(state) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=5) +def test_transition_with_leaking_post_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Leaking starts at epoch 6 (MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2). + The leaking starts after the fork transition in this case. + """ + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + assert not spec.is_in_inactivity_leak(state) + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # check post transition state + assert not spec.is_in_inactivity_leak(state) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + # check state again + assert spec.is_in_inactivity_leak(state) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=10) +def test_transition_with_non_empty_activation_queue(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create some deposits before the transition + """ + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + _, queuing_indices = set_some_new_deposits(spec, state, rng=random.Random(5566)) + + assert spec.get_current_epoch(state) < fork_epoch + assert len(queuing_indices) > 0 + for validator_index in queuing_indices: + assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + yield "pre", state + + # irregular state transition to handle fork: + blocks = [] + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=10) +def test_transition_with_deposit_at_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create a deposit at the transition + """ + # regular state transition until fork: + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + yield "pre", state + + # create a new deposit + validator_index = len(state.validators) + amount = post_spec.MAX_EFFECTIVE_BALANCE + deposit = prepare_state_and_deposit(post_spec, state, validator_index, amount, signed=True) + + # irregular state transition to handle fork: + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch, deposits=[deposit]) + blocks = [] + blocks.append(post_tag(block)) + + assert not post_spec.is_active_validator(state.validators[validator_index], post_spec.get_current_epoch(state)) + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + # finalize activation_eligibility_epoch + _, blocks_in_epoch, state = next_slots_with_attestations( + post_spec, + state, + spec.SLOTS_PER_EPOCH * 2, + fill_cur_epoch=True, + fill_prev_epoch=True, + ) + blocks.extend([pre_tag(block) for block in blocks_in_epoch]) + assert state.finalized_checkpoint.epoch == state.validators[validator_index].activation_eligibility_epoch + + # continue regular state transition with new spec into next epoch + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + + assert state.validators[validator_index].activation_epoch < post_spec.FAR_FUTURE_EPOCH + + to_slot = state.validators[validator_index].activation_epoch * post_spec.SLOTS_PER_EPOCH + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, to_slot) + ]) + assert post_spec.is_active_validator(state.validators[validator_index], post_spec.get_current_epoch(state)) + + yield "blocks", blocks + yield "post", state diff --git a/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py b/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py index 29f9038a8..ff744c4ed 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py +++ b/tests/core/pyspec/eth2spec/test/helpers/inactivity_scores.py @@ -1,5 +1,9 @@ from random import Random +from eth2spec.test.helpers.state import ( + next_epoch_via_block, +) + def randomize_inactivity_scores(spec, state, minimum=0, maximum=50000, rng=Random(4242)): state.inactivity_scores = [rng.randint(minimum, maximum) for _ in range(len(state.validators))] @@ -7,3 +11,22 @@ def randomize_inactivity_scores(spec, state, minimum=0, maximum=50000, rng=Rando def zero_inactivity_scores(spec, state, rng=None): state.inactivity_scores = [0] * len(state.validators) + + +def slash_some_validators_for_inactivity_scores_test(spec, state, rng=Random(40404040), fraction=0.25): + """ + ``run_inactivity_scores_test`` runs at the next epoch from `state`. + # We retrieve the proposer of this future state to avoid + # accidentally slashing that validator + """ + future_state = state.copy() + next_epoch_via_block(spec, future_state) + proposer_index = spec.get_beacon_proposer_index(future_state) + selected_count = int(len(state.validators) * fraction) + selected_indices = rng.sample(range(len(state.validators)), selected_count) + if proposer_index in selected_indices: + selected_indices.remove(proposer_index) + for validator_index in selected_indices: + spec.slash_validator(state, validator_index) + + return selected_indices diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py index 8448b2424..e8b233e1e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/random.py +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -7,6 +7,7 @@ from eth2spec.test.helpers.state import next_epoch def set_some_new_deposits(spec, state, rng): + eligible_indices = queuing_indices = [] num_validators = len(state.validators) # Set ~1/10 to just recently deposited for index in range(num_validators): @@ -18,6 +19,10 @@ def set_some_new_deposits(spec, state, rng): # Set ~half of selected to eligible for activation if rng.choice([True, False]): state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) + eligible_indices.append(index) + else: + queuing_indices.append(index) + return eligible_indices, queuing_indices def exit_random_validators(spec, state, rng, fraction=None):