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..1ac622dd9 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,10 @@ 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, +) from eth2spec.test.helpers.state import ( next_epoch, next_epoch_via_block, diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_activations_and_exits.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_activations_and_exits.py new file mode 100644 index 000000000..12aa815ad --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_activations_and_exits.py @@ -0,0 +1,206 @@ +import random +from eth2spec.test.context import ( + MINIMAL, + fork_transition_test, + with_presets, +) +from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.fork_transition import ( + do_altair_fork, + transition_until_fork, + transition_to_next_epoch_and_append_blocks, +) +from eth2spec.test.helpers.random import ( + exit_random_validators, + set_some_activations, + set_some_new_deposits, +) + + +# +# Exit +# + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +@with_presets([MINIMAL], + reason="only test with enough validators such that at least one exited index is not in sync committee") +def test_transition_with_one_fourth_exiting_validators_exit_post_fork(state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag): + """ + 1/4 validators initiated voluntary exit before the fork, + and are exiting but still active *after* the fork transition. + """ + exited_indices = exit_random_validators( + spec, + state, + rng=random.Random(5566), + fraction=0.25, + exit_epoch=10, + from_epoch=spec.get_current_epoch(state), + ) + + transition_until_fork(spec, state, fork_epoch) + + # 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))) + assert any(set(exited_pubkeys).difference(list(state.current_sync_committee.pubkeys))) + + # continue regular state transition with new spec into next epoch + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + # 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 validators initiated voluntary exit before the fork, + and being exited and inactive *right after* the fork transition. + """ + exited_indices = exit_random_validators( + spec, + state, + rng=random.Random(5566), + fraction=0.25, + exit_epoch=fork_epoch, + from_epoch=spec.get_current_epoch(state), + ) + + transition_until_fork(spec, state, fork_epoch) + + # 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 + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + yield "blocks", blocks + yield "post", state + + +# +# Activation +# + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_non_empty_activation_queue(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create some deposits before the transition + """ + transition_until_fork(spec, state, fork_epoch) + + deposited_indices = set_some_new_deposits(spec, state, rng=random.Random(5566)) + + assert spec.get_current_epoch(state) < fork_epoch + assert len(deposited_indices) > 0 + for validator_index in deposited_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 + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_activation_at_fork_epoch(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create some deposits before the transition + """ + transition_until_fork(spec, state, fork_epoch) + + selected_indices = set_some_activations(spec, state, rng=random.Random(5566), activation_epoch=fork_epoch) + + assert spec.get_current_epoch(state) < fork_epoch + assert len(selected_indices) > 0 + for validator_index in selected_indices: + validator = state.validators[validator_index] + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert validator.activation_epoch == 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 + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + # now they are active + for validator_index in selected_indices: + validator = state.validators[validator_index] + assert post_spec.is_active_validator(validator, post_spec.get_current_epoch(state)) + + yield "blocks", blocks + yield "post", state diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_leaking.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_leaking.py new file mode 100644 index 000000000..6cdac1661 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_leaking.py @@ -0,0 +1,63 @@ +from eth2spec.test.context import fork_transition_test +from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.fork_transition import ( + do_altair_fork, + transition_until_fork, + transition_to_next_epoch_and_append_blocks, +) + + +@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. + """ + transition_until_fork(spec, state, fork_epoch) + + 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 + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + 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. + """ + transition_until_fork(spec, state, fork_epoch) + + 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 + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + yield "blocks", blocks + yield "post", state diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_operations.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_operations.py new file mode 100644 index 000000000..e19c57fb1 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_operations.py @@ -0,0 +1,176 @@ +from eth2spec.test.context import ( + always_bls, + fork_transition_test, +) +from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.fork_transition import ( + OperationType, + run_transition_with_operation, +) + + +# +# PROPOSER_SLASHING +# + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +@always_bls +def test_transition_with_proposer_slashing_right_after_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create an attester slashing right *after* the transition + """ + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.PROPOSER_SLASHING, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH, + ) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +@always_bls +def test_transition_with_proposer_slashing_right_before_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create an attester slashing right *before* the transition + """ + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.PROPOSER_SLASHING, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH - 1, + ) + + +# +# ATTESTER_SLASHING +# + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +@always_bls +def test_transition_with_attester_slashing_right_after_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create an attester slashing right *after* the transition + """ + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.ATTESTER_SLASHING, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH, + ) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +@always_bls +def test_transition_with_attester_slashing_right_before_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create an attester slashing right *after* the transition + """ + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.ATTESTER_SLASHING, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH - 1, + ) + + +# +# DEPOSIT +# + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_deposit_right_after_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create a deposit right *after* the transition + """ + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.DEPOSIT, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH, + ) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_deposit_right_before_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create a deposit right *before* the transition + """ + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.DEPOSIT, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH - 1, + ) + + +# +# VOLUNTARY_EXIT +# + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=260) +def test_transition_with_voluntary_exit_right_after_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create a voluntary exit right *after* the transition. + fork_epoch=260 because mainnet `SHARD_COMMITTEE_PERIOD` is 256 epochs. + """ + # Fast forward to the future epoch so that validator can do voluntary exit + state.slot = spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.VOLUNTARY_EXIT, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH, + ) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=260) +def test_transition_with_voluntary_exit_right_before_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Create a voluntary exit right *before* the transition. + fork_epoch=260 because mainnet `SHARD_COMMITTEE_PERIOD` is 256 epochs. + """ + # Fast forward to the future epoch so that validator can do voluntary exit + state.slot = spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + yield from run_transition_with_operation( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type=OperationType.VOLUNTARY_EXIT, + operation_at_slot=fork_epoch * spec.SLOTS_PER_EPOCH - 1, + ) diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_slashing.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_slashing.py new file mode 100644 index 000000000..211a4fbfe --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_slashing.py @@ -0,0 +1,73 @@ +import random +from eth2spec.test.context import ( + MINIMAL, + fork_transition_test, + with_presets, +) +from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.fork_transition import ( + do_altair_fork, + transition_to_next_epoch_and_append_blocks, + transition_until_fork, +) +from eth2spec.test.helpers.random import ( + slash_random_validators, +) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=1) +@with_presets([MINIMAL], + reason="only test with enough validators such that at least one exited index is not in sync committee") +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 + slashed_indices = slash_random_validators(spec, state, rng=random.Random(5566), fraction=0.25) + assert len(slashed_indices) > 0 + + # check if some validators are slashed but still active + for validator_index in slashed_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) + + transition_until_fork(spec, state, fork_epoch) + + assert spec.get_current_epoch(state) < fork_epoch + + yield "pre", state + + # irregular state transition to handle fork: + state, _ = do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) + + # ensure that some of the current sync committee members are slashed + slashed_pubkeys = [state.validators[index].pubkey for index in slashed_indices] + assert any(set(slashed_pubkeys).intersection(list(state.current_sync_committee.pubkeys))) + assert any(set(slashed_pubkeys).difference(list(state.current_sync_committee.pubkeys))) + + # continue regular state transition with new spec into next epoch + # since the proposer might have been slashed, here we only create blocks with non-slashed proposers + blocks = [] + transition_to_next_epoch_and_append_blocks( + post_spec, + state, + post_tag, + blocks, + only_last_block=True, + ignoring_proposers=slashed_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 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..fe6248c5f 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -1,83 +1,18 @@ 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.block import build_empty_block_for_next_slot, build_empty_block, sign_block +from eth2spec.test.helpers.state import ( + next_epoch_via_signed_block, +) from eth2spec.test.helpers.attestations import next_slots_with_attestations - - -def _state_transition_and_sign_block_at_slot(spec, state): - """ - Cribbed from ``transition_unsigned_block`` helper - where the early parts of the state transition have already - been applied to ``state``. - - Used to produce a block during an irregular state transition. - """ - block = build_empty_block(spec, state) - - assert state.latest_block_header.slot < block.slot - assert state.slot == block.slot - spec.process_block(state, block) - block.state_root = state.hash_tree_root() - return sign_block(spec, state, block) - - -def _all_blocks(_): - return True - - -def _skip_slots(*slots): - """ - Skip making a block if its slot is - passed as an argument to this filter - """ - def f(state_at_prior_slot): - return state_at_prior_slot.slot + 1 not in slots - return f - - -def _no_blocks(_): - return False - - -def _only_at(slot): - """ - Only produce a block if its slot is ``slot``. - """ - def f(state_at_prior_slot): - return state_at_prior_slot.slot + 1 == slot - return f - - -def _state_transition_across_slots(spec, state, to_slot, block_filter=_all_blocks): - assert state.slot < to_slot - while state.slot < to_slot: - should_make_block = block_filter(state) - if should_make_block: - block = build_empty_block_for_next_slot(spec, state) - signed_block = state_transition_and_sign_block(spec, state, block) - yield signed_block - else: - next_slot(spec, state) - - -def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True): - spec.process_slots(state, state.slot + 1) - - assert state.slot % spec.SLOTS_PER_EPOCH == 0 - assert spec.get_current_epoch(state) == fork_epoch - - state = post_spec.upgrade_to_altair(state) - - assert state.fork.epoch == fork_epoch - assert state.fork.previous_version == post_spec.config.GENESIS_FORK_VERSION - 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) - else: - return state, None +from eth2spec.test.helpers.fork_transition import ( + do_altair_fork, + no_blocks, + only_at, + skip_slots, + state_transition_across_slots, + transition_to_next_epoch_and_append_blocks, +) @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) @@ -95,19 +30,15 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, to_slot) + state_transition_across_slots(spec, state, to_slot) ]) # irregular state transition to handle fork: - state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + 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) - ]) + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 assert post_spec.get_current_epoch(state) == fork_epoch + 1 @@ -136,18 +67,14 @@ def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, to_slot) + state_transition_across_slots(spec, state, to_slot) ]) # irregular state transition to handle fork: - state, _ = _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) + state, _ = do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) # 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) - ]) + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 assert post_spec.get_current_epoch(state) == fork_epoch + 1 @@ -178,19 +105,15 @@ def test_transition_missing_last_pre_fork_block(state, fork_epoch, spec, post_sp blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, to_slot, block_filter=_skip_slots(last_slot_of_pre_fork)) + state_transition_across_slots(spec, state, to_slot, block_filter=skip_slots(last_slot_of_pre_fork)) ]) # irregular state transition to handle fork: - state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + 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) - ]) + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 assert post_spec.get_current_epoch(state) == fork_epoch + 1 @@ -221,18 +144,18 @@ def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pr blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, to_slot, block_filter=_no_blocks) + state_transition_across_slots(spec, state, to_slot, block_filter=no_blocks) ]) # irregular state transition to handle fork: - state, _ = _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) + state, _ = do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) # continue regular state transition with new spec into next epoch to_slot = post_spec.SLOTS_PER_EPOCH + state.slot last_slot = (fork_epoch + 1) * post_spec.SLOTS_PER_EPOCH blocks.extend([ post_tag(block) for block in - _state_transition_across_slots(post_spec, state, to_slot, block_filter=_only_at(last_slot)) + state_transition_across_slots(post_spec, state, to_slot, block_filter=only_at(last_slot)) ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 @@ -292,7 +215,7 @@ def _run_transition_test_with_attestations(state, assert (state.slot + 1) % spec.SLOTS_PER_EPOCH == 0 # irregular state transition to handle fork: - state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + 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 @@ -405,11 +328,11 @@ def test_transition_with_no_attestations_until_after_fork(state, fork_epoch, spe blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, to_slot) + state_transition_across_slots(spec, state, to_slot) ]) # irregular state transition to handle fork: - state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + state, block = do_altair_fork(state, spec, post_spec, fork_epoch) blocks.append(post_tag(block)) # continue regular state transition but add attestations diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py b/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py new file mode 100644 index 000000000..947954b80 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py @@ -0,0 +1,335 @@ +from enum import Enum, auto + +from eth2spec.test.helpers.attester_slashings import ( + get_valid_attester_slashing_by_indices, +) +from eth2spec.test.helpers.attestations import next_slots_with_attestations +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.proposer_slashings import ( + get_valid_proposer_slashing, +) +from eth2spec.test.helpers.state import ( + next_slot, + state_transition_and_sign_block, + transition_to, +) +from eth2spec.test.helpers.voluntary_exits import ( + prepare_signed_exits, +) + + +class OperationType(Enum): + PROPOSER_SLASHING = auto() + ATTESTER_SLASHING = auto() + DEPOSIT = auto() + VOLUNTARY_EXIT = auto() + + +def _set_operations_by_dict(block, operation_dict): + for key, value in operation_dict.items(): + setattr(block.body, key, value) + + +def _state_transition_and_sign_block_at_slot(spec, + state, + operation_dict=None): + """ + Cribbed from ``transition_unsigned_block`` helper + where the early parts of the state transition have already + been applied to ``state``. + + Used to produce a block during an irregular state transition. + + The optional `operation_dict` is a dict of {'': }. + This is used for assigning the block operations. + p.s. we can't just pass `body` and assign it because randao_reveal and eth1_data was set in `build_empty_block` + Thus use dict to pass operations. + """ + block = build_empty_block(spec, state) + + if operation_dict: + _set_operations_by_dict(block, operation_dict) + + assert state.latest_block_header.slot < block.slot + assert state.slot == block.slot + spec.process_block(state, block) + block.state_root = state.hash_tree_root() + return sign_block(spec, state, block) + + +def _all_blocks(_): + return True + + +def skip_slots(*slots): + """ + Skip making a block if its slot is + passed as an argument to this filter + """ + def f(state_at_prior_slot): + return state_at_prior_slot.slot + 1 not in slots + return f + + +def no_blocks(_): + return False + + +def only_at(slot): + """ + Only produce a block if its slot is ``slot``. + """ + def f(state_at_prior_slot): + return state_at_prior_slot.slot + 1 == slot + return f + + +def state_transition_across_slots(spec, state, to_slot, block_filter=_all_blocks): + assert state.slot < to_slot + while state.slot < to_slot: + should_make_block = block_filter(state) + if should_make_block: + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield signed_block + else: + next_slot(spec, state) + + +def state_transition_across_slots_with_ignoring_proposers(spec, + state, + to_slot, + ignoring_proposers, + only_last_block=False): + """ + 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: + if state.slot + 1 < to_slot and only_last_block: + next_slot(spec, state) + continue + + 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, operation_dict=None): + spec.process_slots(state, state.slot + 1) + + assert state.slot % spec.SLOTS_PER_EPOCH == 0 + assert spec.get_current_epoch(state) == fork_epoch + + state = post_spec.upgrade_to_altair(state) + + assert state.fork.epoch == fork_epoch + assert state.fork.previous_version == post_spec.config.GENESIS_FORK_VERSION + 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, operation_dict=operation_dict) + else: + return state, None + + +def transition_until_fork(spec, state, fork_epoch): + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, to_slot) + + +def _transition_until_fork_minus_one(spec, state, fork_epoch): + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 2 + transition_to(spec, state, to_slot) + + +def transition_to_next_epoch_and_append_blocks(spec, + state, + post_tag, + blocks, + only_last_block=False, + ignoring_proposers=None): + to_slot = spec.SLOTS_PER_EPOCH + state.slot + + if only_last_block: + block_filter = only_at(to_slot) + else: + block_filter = _all_blocks + + if ignoring_proposers is None: + result_blocks = state_transition_across_slots(spec, state, to_slot, block_filter=block_filter) + else: + result_blocks = state_transition_across_slots_with_ignoring_proposers( + spec, + state, + to_slot, + ignoring_proposers, + only_last_block=only_last_block, + ) + + blocks.extend([ + post_tag(block) for block in + result_blocks + ]) + + +def run_transition_with_operation(state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + operation_type, + operation_at_slot): + """ + Generate `operation_type` operation with the spec before fork. + The operation would be included into the block at `operation_at_slot`. + """ + is_at_fork = operation_at_slot == fork_epoch * spec.SLOTS_PER_EPOCH + is_right_before_fork = operation_at_slot == fork_epoch * spec.SLOTS_PER_EPOCH - 1 + assert is_at_fork or is_right_before_fork + + if is_at_fork: + transition_until_fork(spec, state, fork_epoch) + elif is_right_before_fork: + _transition_until_fork_minus_one(spec, state, fork_epoch) + + is_slashing_operation = operation_type in (OperationType.PROPOSER_SLASHING, OperationType.ATTESTER_SLASHING) + # prepare operation + selected_validator_index = None + if is_slashing_operation: + # avoid slashing the next proposer + future_state = state.copy() + next_slot(spec, future_state) + proposer_index = spec.get_beacon_proposer_index(future_state) + selected_validator_index = (proposer_index + 1) % len(state.validators) + if operation_type == OperationType.PROPOSER_SLASHING: + proposer_slashing = get_valid_proposer_slashing( + spec, state, slashed_index=selected_validator_index, signed_1=True, signed_2=True) + operation_dict = {'proposer_slashings': [proposer_slashing]} + else: + # operation_type == OperationType.ATTESTER_SLASHING: + attester_slashing = get_valid_attester_slashing_by_indices( + spec, state, + [selected_validator_index], + signed_1=True, signed_2=True, + ) + operation_dict = {'attester_slashings': [attester_slashing]} + elif operation_type == OperationType.DEPOSIT: + # create a new deposit + selected_validator_index = len(state.validators) + amount = spec.MAX_EFFECTIVE_BALANCE + deposit = prepare_state_and_deposit(spec, state, selected_validator_index, amount, signed=True) + operation_dict = {'deposits': [deposit]} + elif operation_type == OperationType.VOLUNTARY_EXIT: + selected_validator_index = 0 + signed_exits = prepare_signed_exits(spec, state, [selected_validator_index]) + operation_dict = {'voluntary_exits': signed_exits} + + blocks = [] + + if is_right_before_fork: + # add a block with operation. + block = build_empty_block_for_next_slot(spec, state) + _set_operations_by_dict(block, operation_dict) + signed_block = state_transition_and_sign_block(spec, state, block) + blocks.append(pre_tag(signed_block)) + + def _check_state(): + if operation_type == OperationType.PROPOSER_SLASHING: + slashed_proposer = state.validators[proposer_slashing.signed_header_1.message.proposer_index] + assert slashed_proposer.slashed + elif operation_type == OperationType.ATTESTER_SLASHING: + indices = set(attester_slashing.attestation_1.attesting_indices).intersection( + attester_slashing.attestation_2.attesting_indices + ) + assert selected_validator_index in indices + assert len(indices) > 0 + for validator_index in indices: + assert state.validators[validator_index].slashed + elif operation_type == OperationType.DEPOSIT: + assert not post_spec.is_active_validator( + state.validators[selected_validator_index], + post_spec.get_current_epoch(state) + ) + elif operation_type == OperationType.VOLUNTARY_EXIT: + validator = state.validators[selected_validator_index] + assert validator.exit_epoch < post_spec.FAR_FUTURE_EPOCH + + if is_right_before_fork: + _check_state() + + yield "pre", state + + # irregular state transition to handle fork: + _operation_at_slot = operation_dict if is_at_fork else None + state, block = do_altair_fork(state, spec, post_spec, fork_epoch, operation_dict=_operation_at_slot) + blocks.append(post_tag(block)) + + if is_at_fork: + _check_state() + + # after the fork + if operation_type == OperationType.DEPOSIT: + _transition_until_active(post_spec, state, post_tag, blocks, selected_validator_index) + else: + # avoid using the slashed validators as block proposers + ignoring_proposers = [selected_validator_index] if is_slashing_operation else None + + # continue regular state transition with new spec into next epoch + transition_to_next_epoch_and_append_blocks( + post_spec, + state, + post_tag, + blocks, + only_last_block=True, + ignoring_proposers=ignoring_proposers, + ) + + yield "blocks", blocks + yield "post", state + + +def _transition_until_active(post_spec, state, post_tag, blocks, validator_index): + # continue regular state transition with new spec into next epoch + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks) + # finalize activation_eligibility_epoch + _, blocks_in_epoch, state = next_slots_with_attestations( + post_spec, + state, + post_spec.SLOTS_PER_EPOCH * 2, + fill_cur_epoch=True, + fill_prev_epoch=True, + ) + blocks.extend([post_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 + transition_to_next_epoch_and_append_blocks(post_spec, state, post_tag, blocks, only_last_block=True) + + 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, block_filter=only_at(to_slot)) + ]) + assert post_spec.is_active_validator(state.validators[validator_index], post_spec.get_current_epoch(state)) diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py index 8448b2424..0dc446d19 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/random.py +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -6,7 +6,28 @@ from eth2spec.test.helpers.deposits import mock_deposit from eth2spec.test.helpers.state import next_epoch +def set_some_activations(spec, state, rng, activation_epoch=None): + if activation_epoch is None: + activation_epoch = spec.get_current_epoch(state) + num_validators = len(state.validators) + selected_indices = [] + for index in range(num_validators): + # If is slashed or exiting, skip + if state.validators[index].slashed or state.validators[index].exit_epoch != spec.FAR_FUTURE_EPOCH: + continue + # Set ~1/10 validators' activation_eligibility_epoch and activation_epoch + if rng.randrange(num_validators) < num_validators // 10: + state.validators[index].activation_eligibility_epoch = max( + int(activation_epoch) - int(spec.MAX_SEED_LOOKAHEAD) - 1, + spec.GENESIS_EPOCH, + ) + state.validators[index].activation_epoch = activation_epoch + selected_indices.append(index) + return selected_indices + + def set_some_new_deposits(spec, state, rng): + deposited_indices = [] num_validators = len(state.validators) # Set ~1/10 to just recently deposited for index in range(num_validators): @@ -15,46 +36,64 @@ def set_some_new_deposits(spec, state, rng): continue if rng.randrange(num_validators) < num_validators // 10: mock_deposit(spec, state, index) - # Set ~half of selected to eligible for activation if rng.choice([True, False]): + # Set ~half of selected to eligible for activation state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) + else: + # The validators that just made a deposit + deposited_indices.append(index) + return deposited_indices -def exit_random_validators(spec, state, rng, fraction=None): - if fraction is None: - # Exit ~1/2 - fraction = 0.5 +def exit_random_validators(spec, state, rng, fraction=0.5, exit_epoch=None, withdrawable_epoch=None, from_epoch=None): + """ + Set some validators' exit_epoch and withdrawable_epoch. - 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) + If exit_epoch is configured, use the given exit_epoch. Otherwise, randomly set exit_epoch and withdrawable_epoch. + """ + if from_epoch is None: + from_epoch = spec.MAX_SEED_LOOKAHEAD + 1 + epoch_diff = int(from_epoch) - int(spec.get_current_epoch(state)) + for _ in range(epoch_diff): + # NOTE: if `epoch_diff` is negative, then this loop body does not execute. + next_epoch(spec, state) current_epoch = spec.get_current_epoch(state) + exited_indices = [] for index in spec.get_active_validator_indices(state, current_epoch): sampled = rng.random() < fraction if not sampled: continue + exited_indices.append(index) validator = state.validators[index] - validator.exit_epoch = rng.choice([current_epoch, current_epoch - 1, current_epoch - 2, current_epoch - 3]) - # ~1/2 are withdrawable (note, unnatural span between exit epoch and withdrawable epoch) - if rng.choice([True, False]): - validator.withdrawable_epoch = current_epoch + if exit_epoch is None: + assert withdrawable_epoch is None + validator.exit_epoch = rng.choice([current_epoch, current_epoch - 1, current_epoch - 2, current_epoch - 3]) + # ~1/2 are withdrawable (note, unnatural span between exit epoch and withdrawable epoch) + if rng.choice([True, False]): + validator.withdrawable_epoch = current_epoch + else: + validator.withdrawable_epoch = current_epoch + 1 else: - validator.withdrawable_epoch = current_epoch + 1 + validator.exit_epoch = exit_epoch + if withdrawable_epoch is None: + validator.withdrawable_epoch = validator.exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + else: + validator.withdrawable_epoch = withdrawable_epoch + + return exited_indices -def slash_random_validators(spec, state, rng, fraction=None): - if fraction is None: - # Slash ~1/2 of validators - fraction = 0.5 - +def slash_random_validators(spec, state, rng, fraction=0.5): + slashed_indices = [] for index in range(len(state.validators)): # slash at least one validator sampled = rng.random() < fraction if index == 0 or sampled: spec.slash_validator(state, index) + slashed_indices.append(index) + return slashed_indices def randomize_epoch_participation(spec, state, epoch, rng): @@ -123,7 +162,7 @@ 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), exit_fraction=None, slash_fraction=None): +def randomize_state(spec, state, rng=Random(8020), exit_fraction=0.5, slash_fraction=0.5): 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) diff --git a/tests/generators/transition/main.py b/tests/generators/transition/main.py index 2ded56a13..a850a7f45 100644 --- a/tests/generators/transition/main.py +++ b/tests/generators/transition/main.py @@ -1,7 +1,13 @@ from typing import Iterable from eth2spec.test.helpers.constants import ALTAIR, MINIMAL, MAINNET, PHASE0 -from eth2spec.test.altair.transition import test_transition as test_altair_transition +from eth2spec.test.altair.transition import ( + test_transition as test_altair_transition, + test_activations_and_exits as test_altair_activations_and_exits, + test_leaking as test_altair_leaking, + test_slashing as test_altair_slashing, + test_operations as test_altair_operations, +) from eth2spec.gen_helpers.gen_base import gen_runner, gen_typing from eth2spec.gen_helpers.gen_from_tests.gen import generate_from_tests @@ -25,7 +31,13 @@ def create_provider(tests_src, preset_name: str, pre_fork_name: str, post_fork_n return gen_typing.TestProvider(prepare=prepare_fn, make_cases=cases_fn) -TRANSITION_TESTS = ((PHASE0, ALTAIR, test_altair_transition),) +TRANSITION_TESTS = ( + (PHASE0, ALTAIR, test_altair_transition), + (PHASE0, ALTAIR, test_altair_activations_and_exits), + (PHASE0, ALTAIR, test_altair_leaking), + (PHASE0, ALTAIR, test_altair_slashing), + (PHASE0, ALTAIR, test_altair_operations), +) if __name__ == "__main__":