From ea6c0429185a2db2b34d8a38ad159cd93344f33b Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 6 May 2021 10:55:10 +1000 Subject: [PATCH 1/3] Altair: carry-over prev epoch participation --- specs/altair/beacon-chain.md | 60 +++++++++++++++++++++++------------- specs/altair/fork.md | 17 ++++++++++ 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/specs/altair/beacon-chain.md b/specs/altair/beacon-chain.md index 7bf0c2d99..374bac5cf 100644 --- a/specs/altair/beacon-chain.md +++ b/specs/altair/beacon-chain.md @@ -37,6 +37,7 @@ - [`get_base_reward_per_increment`](#get_base_reward_per_increment) - [`get_base_reward`](#get_base_reward) - [`get_unslashed_participating_indices`](#get_unslashed_participating_indices) + - [`get_attestation_participation_flag_indices`](#get_attestation_participation_flag_indices) - [`get_flag_index_deltas`](#get_flag_index_deltas) - [Modified `get_inactivity_penalty_deltas`](#modified-get_inactivity_penalty_deltas) - [Beacon state mutators](#beacon-state-mutators) @@ -352,6 +353,37 @@ def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epo return set(filter(lambda index: not state.validators[index].slashed, participating_indices)) ``` +#### `get_attestation_participation_flag_indices` + +```python +def get_attestation_participation_flag_indices(state: BeaconState, + data: AttestationData, + inclusion_delay: uint64) -> Sequence[int]: + """ + Return the flag indices that are satisfied by an attestation. + """ + if data.target.epoch == get_current_epoch(state): + justified_checkpoint = state.current_justified_checkpoint + else: + justified_checkpoint = state.previous_justified_checkpoint + + # Matching roots + is_matching_source = data.source == justified_checkpoint + is_matching_target = is_matching_source and data.target.root == get_block_root(state, data.target.epoch) + is_matching_head = is_matching_target and data.beacon_block_root == get_block_root_at_slot(state, data.slot) + assert is_matching_source + + participation_flag_indices = [] + if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH): + participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX) + if is_matching_target and inclusion_delay <= SLOTS_PER_EPOCH: + participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX) + if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY: + participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX) + + return participation_flag_indices +``` + #### `get_flag_index_deltas` ```python @@ -463,32 +495,18 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: committee = get_beacon_committee(state, data.slot, data.index) assert len(attestation.aggregation_bits) == len(committee) - if data.target.epoch == get_current_epoch(state): - epoch_participation = state.current_epoch_participation - justified_checkpoint = state.current_justified_checkpoint - else: - epoch_participation = state.previous_epoch_participation - justified_checkpoint = state.previous_justified_checkpoint - - # Matching roots - is_matching_source = data.source == justified_checkpoint - is_matching_target = is_matching_source and data.target.root == get_block_root(state, data.target.epoch) - is_matching_head = is_matching_target and data.beacon_block_root == get_block_root_at_slot(state, data.slot) - assert is_matching_source + # Participation flag indices + participation_flag_indices = get_attestation_participation_flag_indices(state, data, state.slot - data.slot) # Verify signature assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) - # Participation flag indices - participation_flag_indices = [] - if is_matching_source and state.slot <= data.slot + integer_squareroot(SLOTS_PER_EPOCH): - participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX) - if is_matching_target and state.slot <= data.slot + SLOTS_PER_EPOCH: - participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX) - if is_matching_head and state.slot == data.slot + MIN_ATTESTATION_INCLUSION_DELAY: - participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX) - # Update epoch participation flags + if data.target.epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + proposer_reward_numerator = 0 for index in get_attesting_indices(state, data, attestation.aggregation_bits): for flag_index, weight in get_flag_indices_and_weights(): diff --git a/specs/altair/fork.md b/specs/altair/fork.md index be562b82b..d8374d009 100644 --- a/specs/altair/fork.md +++ b/specs/altair/fork.md @@ -41,6 +41,21 @@ Note that for the pure Altair networks, we don't apply `upgrade_to_altair` since After `process_slots` of Phase 0 finishes, if `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == ALTAIR_FORK_EPOCH`, an irregular state change is made to upgrade to Altair. ```python +def translate_participation(state: BeaconState, pending_attestations: Sequence[PendingAttestation]) -> None: + for attestation in pending_attestations: + data = attestation.data + inclusion_delay = attestation.inclusion_delay + # Translate attestation inclusion info to flag indices + participation_flag_indices = get_attestation_participation_flag_indices(state, data, inclusion_delay) + + # Apply flags to all attesting validators + epoch_participation = state.previous_epoch_participation + for index in get_attesting_indices(state, data, attestation.aggregation_bits): + for flag_index, weight in get_flag_indices_and_weights(): + if flag_index in participation_flag_indices and not has_flag(epoch_participation[index], flag_index): + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + + def upgrade_to_altair(pre: phase0.BeaconState) -> BeaconState: epoch = phase0.get_current_epoch(pre) post = BeaconState( @@ -80,6 +95,8 @@ def upgrade_to_altair(pre: phase0.BeaconState) -> BeaconState: # Inactivity inactivity_scores=[uint64(0) for _ in range(len(pre.validators))], ) + # Fill in previous epoch participation from the pre state's pending attestations + translate_participation(post, pre.previous_epoch_attestations) # Fill in sync committees post.current_sync_committee = get_sync_committee(post, get_current_epoch(post)) post.next_sync_committee = get_sync_committee(post, get_current_epoch(post) + EPOCHS_PER_SYNC_COMMITTEE_PERIOD) From a6d5b2e215a5796bfd96713e8fbbb930b353e95b Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Thu, 13 May 2021 08:22:24 -0600 Subject: [PATCH 2/3] pull state randomization functions out of rewards testing --- .../pyspec/eth2spec/test/helpers/rewards.py | 96 +-------------- .../pyspec/eth2spec/test/helpers/state.py | 110 +++++++++++++++++- 2 files changed, 115 insertions(+), 91 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index f563c70a2..47a629ac5 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -4,8 +4,11 @@ from lru import LRU from eth2spec.phase0 import spec as spec_phase0 from eth2spec.test.context import is_post_altair from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations -from eth2spec.test.helpers.deposits import mock_deposit -from eth2spec.test.helpers.state import next_epoch +from eth2spec.test.helpers.state import ( + next_epoch, + set_some_new_deposits, exit_random_validators, slash_random_validators, + randomize_state, +) from eth2spec.utils.ssz.ssz_typing import Container, uint64, List @@ -285,49 +288,6 @@ def leaking(epochs=None): return deco -def set_some_new_deposits(spec, state, rng): - num_validators = len(state.validators) - # Set ~1/10 to just recently deposited - for index in range(num_validators): - # If not already active, skip - if not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)): - 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]): - state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) - - -def exit_random_validators(spec, state, rng): - 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]): - continue - - validator = state.validators[index] - validator.exit_epoch = rng.choice([current_epoch - 1, current_epoch - 2, current_epoch - 3]) - # ~1/2 are withdrawable - if rng.choice([True, False]): - validator.withdrawable_epoch = current_epoch - else: - validator.withdrawable_epoch = current_epoch + 1 - - -def slash_random_validators(spec, state, rng): - # Slash ~1/2 of validators - for index in range(len(state.validators)): - # slash at least one validator - if index == 0 or rng.choice([True, False]): - spec.slash_validator(state, index) - - def run_test_empty(spec, state): # Do not add any attestations to state @@ -531,49 +491,5 @@ def run_test_all_balances_too_low_for_reward(spec, state): def run_test_full_random(spec, state, rng=Random(8020)): - set_some_new_deposits(spec, state, rng) - exit_random_validators(spec, state, rng) - slash_random_validators(spec, state, rng) - - cached_prepare_state_with_attestations(spec, state) - - if not is_post_altair(spec): - for pending_attestation in state.previous_epoch_attestations: - # ~1/3 have bad target - if rng.randint(0, 2) == 0: - pending_attestation.data.target.root = b'\x55' * 32 - # ~1/3 have bad head - if rng.randint(0, 2) == 0: - pending_attestation.data.beacon_block_root = b'\x66' * 32 - # ~50% participation - pending_attestation.aggregation_bits = [rng.choice([True, False]) - for _ in pending_attestation.aggregation_bits] - # Random inclusion delay - pending_attestation.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH) - else: - for index in range(len(state.validators)): - # ~1/3 have bad head or bad target or not timely enough - is_timely_correct_head = rng.randint(0, 2) != 0 - flags = state.previous_epoch_participation[index] - - def set_flag(index, value): - nonlocal flags - flag = spec.ParticipationFlags(2**index) - if value: - flags |= flag - else: - flags &= 0xff ^ flag - - set_flag(spec.TIMELY_HEAD_FLAG_INDEX, is_timely_correct_head) - if is_timely_correct_head: - # If timely head, then must be timely target - set_flag(spec.TIMELY_TARGET_FLAG_INDEX, True) - # If timely head, then must be timely source - set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, True) - else: - # ~50% of remaining have bad target or not timely enough - set_flag(spec.TIMELY_TARGET_FLAG_INDEX, rng.choice([True, False])) - # ~50% of remaining have bad source or not timely enough - set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, rng.choice([True, False])) - state.previous_epoch_participation[index] = flags + randomize_state(spec, state, rng) yield from run_deltas(spec, state) diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index d61df7610..610f4871e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -1,5 +1,9 @@ -from eth2spec.test.context import expect_assertion_error +from random import Random + +from eth2spec.test.context import expect_assertion_error, is_post_altair from eth2spec.test.helpers.block import apply_empty_block, sign_block, transition_unsigned_block +from eth2spec.test.helpers.deposits import mock_deposit +from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations def get_balance(state, index): @@ -84,3 +88,107 @@ def state_transition_and_sign_block(spec, state, block, expect_fail=False): transition_unsigned_block(spec, state, block) block.state_root = state.hash_tree_root() return sign_block(spec, state, block) + + +def set_some_new_deposits(spec, state, rng): + num_validators = len(state.validators) + # Set ~1/10 to just recently deposited + for index in range(num_validators): + # If not already active, skip + if not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)): + 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]): + state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) + + +def exit_random_validators(spec, state, rng): + 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]): + continue + + validator = state.validators[index] + validator.exit_epoch = rng.choice([current_epoch - 1, current_epoch - 2, current_epoch - 3]) + # ~1/2 are withdrawable + if rng.choice([True, False]): + validator.withdrawable_epoch = current_epoch + else: + validator.withdrawable_epoch = current_epoch + 1 + + +def slash_random_validators(spec, state, rng): + # Slash ~1/2 of validators + for index in range(len(state.validators)): + # slash at least one validator + if index == 0 or rng.choice([True, False]): + spec.slash_validator(state, index) + + +def randomize_epoch_participation(spec, state, epoch, rng): + assert epoch in (spec.get_current_epoch(state), spec.get_previous_epoch(state)) + if not is_post_altair(spec): + if epoch == spec.get_current_epoch(state): + pending_attestations = state.current_epoch_attestations + else: + pending_attestations = state.previous_epoch_attestations + for pending_attestation in pending_attestations: + # ~1/3 have bad target + if rng.randint(0, 2) == 0: + pending_attestation.data.target.root = b'\x55' * 32 + # ~1/3 have bad head + if rng.randint(0, 2) == 0: + pending_attestation.data.beacon_block_root = b'\x66' * 32 + # ~50% participation + pending_attestation.aggregation_bits = [rng.choice([True, False]) + for _ in pending_attestation.aggregation_bits] + # Random inclusion delay + pending_attestation.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH) + else: + if epoch == spec.get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + for index in range(len(state.validators)): + # ~1/3 have bad head or bad target or not timely enough + is_timely_correct_head = rng.randint(0, 2) != 0 + flags = epoch_participation[index] + + def set_flag(index, value): + nonlocal flags + flag = spec.ParticipationFlags(2**index) + if value: + flags |= flag + else: + flags &= 0xff ^ flag + + set_flag(spec.TIMELY_HEAD_FLAG_INDEX, is_timely_correct_head) + if is_timely_correct_head: + # If timely head, then must be timely target + set_flag(spec.TIMELY_TARGET_FLAG_INDEX, True) + # If timely head, then must be timely source + set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, True) + else: + # ~50% of remaining have bad target or not timely enough + set_flag(spec.TIMELY_TARGET_FLAG_INDEX, rng.choice([True, False])) + # ~50% of remaining have bad source or not timely enough + set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, rng.choice([True, False])) + epoch_participation[index] = flags + + +def randomize_state(spec, state, rng=Random(8020)): + set_some_new_deposits(spec, state, rng) + exit_random_validators(spec, state, rng) + slash_random_validators(spec, state, rng) + + cached_prepare_state_with_attestations(spec, state) + randomize_epoch_participation(spec, state, spec.get_previous_epoch(state), rng) + randomize_epoch_participation(spec, state, spec.get_current_epoch(state), rng) From a52565aa8141fed2ae4424436cd236130ff529c6 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Thu, 13 May 2021 13:21:20 -0600 Subject: [PATCH 3/3] add random altair fork tests for better translation coverage + pr review --- specs/altair/fork.md | 7 +- ...test_fork.py => test_altair_fork_basic.py} | 45 +------ .../altair/fork/test_altair_fork_random.py | 120 ++++++++++++++++++ .../eth2spec/test/helpers/altair/fork.py | 42 ++++++ .../pyspec/eth2spec/test/helpers/random.py | 113 +++++++++++++++++ .../pyspec/eth2spec/test/helpers/rewards.py | 6 +- .../pyspec/eth2spec/test/helpers/state.py | 110 +--------------- 7 files changed, 288 insertions(+), 155 deletions(-) rename tests/core/pyspec/eth2spec/test/altair/fork/{test_fork.py => test_altair_fork_basic.py} (66%) create mode 100644 tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_random.py create mode 100644 tests/core/pyspec/eth2spec/test/helpers/altair/fork.py create mode 100644 tests/core/pyspec/eth2spec/test/helpers/random.py diff --git a/specs/altair/fork.md b/specs/altair/fork.md index d14e8ae8d..b4c4e3a31 100644 --- a/specs/altair/fork.md +++ b/specs/altair/fork.md @@ -45,7 +45,7 @@ Care must be taken when transitioning through the fork boundary as implementatio In particular, the outer `state_transition` function defined in the Phase 0 spec will not expose the precise fork slot to execute the upgrade in the presence of skipped slots at the fork boundary. Instead the logic must be within `process_slots`. ```python -def translate_participation(state: BeaconState, pending_attestations: Sequence[PendingAttestation]) -> None: +def translate_participation(state: BeaconState, pending_attestations: Sequence[phase0.PendingAttestation]) -> None: for attestation in pending_attestations: data = attestation.data inclusion_delay = attestation.inclusion_delay @@ -55,9 +55,8 @@ def translate_participation(state: BeaconState, pending_attestations: Sequence[P # Apply flags to all attesting validators epoch_participation = state.previous_epoch_participation for index in get_attesting_indices(state, data, attestation.aggregation_bits): - for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): - if flag_index in participation_flag_indices and not has_flag(epoch_participation[index], flag_index): - epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + for flag_index in participation_flag_indices: + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) def upgrade_to_altair(pre: phase0.BeaconState) -> BeaconState: diff --git a/tests/core/pyspec/eth2spec/test/altair/fork/test_fork.py b/tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_basic.py similarity index 66% rename from tests/core/pyspec/eth2spec/test/altair/fork/test_fork.py rename to tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_basic.py index 1ad39209c..bc082026e 100644 --- a/tests/core/pyspec/eth2spec/test/altair/fork/test_fork.py +++ b/tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_basic.py @@ -14,47 +14,10 @@ from eth2spec.test.helpers.state import ( next_epoch, next_epoch_via_block, ) - - -ALTAIR_FORK_TEST_META_TAGS = { - 'fork': 'altair', -} - - -def run_fork_test(post_spec, pre_state): - yield 'pre', pre_state - - post_state = post_spec.upgrade_to_altair(pre_state) - - # Stable fields - stable_fields = [ - 'genesis_time', 'genesis_validators_root', 'slot', - # History - 'latest_block_header', 'block_roots', 'state_roots', 'historical_roots', - # Eth1 - 'eth1_data', 'eth1_data_votes', 'eth1_deposit_index', - # Registry - 'validators', 'balances', - # Randomness - 'randao_mixes', - # Slashings - 'slashings', - # Finality - 'justification_bits', 'previous_justified_checkpoint', 'current_justified_checkpoint', 'finalized_checkpoint', - ] - for field in stable_fields: - assert getattr(pre_state, field) == getattr(post_state, field) - - # Modified fields - modified_fields = ['fork'] - for field in modified_fields: - assert getattr(pre_state, field) != getattr(post_state, field) - - assert pre_state.fork.current_version == post_state.fork.previous_version - assert post_state.fork.current_version == post_spec.ALTAIR_FORK_VERSION - assert post_state.fork.epoch == post_spec.get_current_epoch(post_state) - - yield 'post', post_state +from eth2spec.test.helpers.altair.fork import ( + ALTAIR_FORK_TEST_META_TAGS, + run_fork_test, +) @with_phases(phases=[PHASE0], other_phases=[ALTAIR]) diff --git a/tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_random.py b/tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_random.py new file mode 100644 index 000000000..ba350bd68 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/fork/test_altair_fork_random.py @@ -0,0 +1,120 @@ +from random import Random + +from eth2spec.test.context import ( + with_phases, + with_custom_state, + with_configs, + spec_test, with_state, + low_balances, misc_balances, large_validator_set, +) +from eth2spec.test.utils import with_meta_tags +from eth2spec.test.helpers.constants import ( + PHASE0, ALTAIR, + MINIMAL, +) +from eth2spec.test.helpers.altair.fork import ( + ALTAIR_FORK_TEST_META_TAGS, + run_fork_test, +) +from eth2spec.test.helpers.random import ( + randomize_state, + randomize_attestation_participation, +) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_state +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_0(spec, phases, state): + randomize_state(spec, state, rng=Random(1010)) + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_state +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_1(spec, phases, state): + randomize_state(spec, state, rng=Random(2020)) + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_state +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_2(spec, phases, state): + randomize_state(spec, state, rng=Random(3030)) + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_state +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_3(spec, phases, state): + randomize_state(spec, state, rng=Random(4040)) + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_state +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_duplicate_attestations(spec, phases, state): + randomize_state(spec, state, rng=Random(1111)) + # Note: `run_fork_test` empties `current_epoch_attestations` + state.previous_epoch_attestations = state.previous_epoch_attestations + state.previous_epoch_attestations + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_state +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_mismatched_attestations(spec, phases, state): + # Create a random state + randomize_state(spec, state, rng=Random(2222)) + + # Now make two copies + state_0 = state.copy() + state_1 = state.copy() + + # Randomize attestation participation of both + randomize_attestation_participation(spec, state_0, rng=Random(3333)) + randomize_attestation_participation(spec, state_1, rng=Random(4444)) + + # Note: `run_fork_test` empties `current_epoch_attestations` + # Use pending attestations from both random states in a single state for testing + state_0.previous_epoch_attestations = state_0.previous_epoch_attestations + state_1.previous_epoch_attestations + yield from run_fork_test(phases[ALTAIR], state_0) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_low_balances(spec, phases, state): + randomize_state(spec, state, rng=Random(5050)) + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@spec_test +@with_custom_state(balances_fn=misc_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_misc_balances(spec, phases, state): + randomize_state(spec, state, rng=Random(6060)) + yield from run_fork_test(phases[ALTAIR], state) + + +@with_phases(phases=[PHASE0], other_phases=[ALTAIR]) +@with_configs([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=large_validator_set, threshold_fn=lambda spec: spec.EJECTION_BALANCE) +@with_meta_tags(ALTAIR_FORK_TEST_META_TAGS) +def test_altair_fork_random_large_validator_set(spec, phases, state): + randomize_state(spec, state, rng=Random(7070)) + yield from run_fork_test(phases[ALTAIR], state) diff --git a/tests/core/pyspec/eth2spec/test/helpers/altair/fork.py b/tests/core/pyspec/eth2spec/test/helpers/altair/fork.py new file mode 100644 index 000000000..b1074c881 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/altair/fork.py @@ -0,0 +1,42 @@ +ALTAIR_FORK_TEST_META_TAGS = { + 'fork': 'altair', +} + + +def run_fork_test(post_spec, pre_state): + # Clean up state to be more realistic + pre_state.current_epoch_attestations = [] + + yield 'pre', pre_state + + post_state = post_spec.upgrade_to_altair(pre_state) + + # Stable fields + stable_fields = [ + 'genesis_time', 'genesis_validators_root', 'slot', + # History + 'latest_block_header', 'block_roots', 'state_roots', 'historical_roots', + # Eth1 + 'eth1_data', 'eth1_data_votes', 'eth1_deposit_index', + # Registry + 'validators', 'balances', + # Randomness + 'randao_mixes', + # Slashings + 'slashings', + # Finality + 'justification_bits', 'previous_justified_checkpoint', 'current_justified_checkpoint', 'finalized_checkpoint', + ] + for field in stable_fields: + assert getattr(pre_state, field) == getattr(post_state, field) + + # Modified fields + modified_fields = ['fork'] + for field in modified_fields: + assert getattr(pre_state, field) != getattr(post_state, field) + + assert pre_state.fork.current_version == post_state.fork.previous_version + assert post_state.fork.current_version == post_spec.ALTAIR_FORK_VERSION + assert post_state.fork.epoch == post_spec.get_current_epoch(post_state) + + yield 'post', post_state diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py new file mode 100644 index 000000000..5b5e419ba --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -0,0 +1,113 @@ +from random import Random + +from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations +from eth2spec.test.context import is_post_altair +from eth2spec.test.helpers.deposits import mock_deposit +from eth2spec.test.helpers.state import next_epoch + + +def set_some_new_deposits(spec, state, rng): + num_validators = len(state.validators) + # Set ~1/10 to just recently deposited + for index in range(num_validators): + # If not already active, skip + if not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)): + 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]): + state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) + + +def exit_random_validators(spec, state, rng): + 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]): + continue + + validator = state.validators[index] + validator.exit_epoch = rng.choice([current_epoch - 1, current_epoch - 2, current_epoch - 3]) + # ~1/2 are withdrawable + if rng.choice([True, False]): + validator.withdrawable_epoch = current_epoch + else: + validator.withdrawable_epoch = current_epoch + 1 + + +def slash_random_validators(spec, state, rng): + # Slash ~1/2 of validators + for index in range(len(state.validators)): + # slash at least one validator + if index == 0 or rng.choice([True, False]): + spec.slash_validator(state, index) + + +def randomize_epoch_participation(spec, state, epoch, rng): + assert epoch in (spec.get_current_epoch(state), spec.get_previous_epoch(state)) + if not is_post_altair(spec): + if epoch == spec.get_current_epoch(state): + pending_attestations = state.current_epoch_attestations + else: + pending_attestations = state.previous_epoch_attestations + for pending_attestation in pending_attestations: + # ~1/3 have bad target + if rng.randint(0, 2) == 0: + pending_attestation.data.target.root = b'\x55' * 32 + # ~1/3 have bad head + if rng.randint(0, 2) == 0: + pending_attestation.data.beacon_block_root = b'\x66' * 32 + # ~50% participation + pending_attestation.aggregation_bits = [rng.choice([True, False]) + for _ in pending_attestation.aggregation_bits] + # Random inclusion delay + pending_attestation.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH) + else: + if epoch == spec.get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + for index in range(len(state.validators)): + # ~1/3 have bad head or bad target or not timely enough + is_timely_correct_head = rng.randint(0, 2) != 0 + flags = epoch_participation[index] + + def set_flag(index, value): + nonlocal flags + flag = spec.ParticipationFlags(2**index) + if value: + flags |= flag + else: + flags &= 0xff ^ flag + + set_flag(spec.TIMELY_HEAD_FLAG_INDEX, is_timely_correct_head) + if is_timely_correct_head: + # If timely head, then must be timely target + set_flag(spec.TIMELY_TARGET_FLAG_INDEX, True) + # If timely head, then must be timely source + set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, True) + else: + # ~50% of remaining have bad target or not timely enough + set_flag(spec.TIMELY_TARGET_FLAG_INDEX, rng.choice([True, False])) + # ~50% of remaining have bad source or not timely enough + set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, rng.choice([True, False])) + epoch_participation[index] = flags + + +def randomize_attestation_participation(spec, state, rng=Random(8020)): + cached_prepare_state_with_attestations(spec, state) + randomize_epoch_participation(spec, state, spec.get_previous_epoch(state), rng) + randomize_epoch_participation(spec, state, spec.get_current_epoch(state), rng) + + +def randomize_state(spec, state, rng=Random(8020)): + set_some_new_deposits(spec, state, rng) + exit_random_validators(spec, state, rng) + slash_random_validators(spec, state, rng) + randomize_attestation_participation(spec, state, rng) diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index 47a629ac5..6bf922750 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -3,12 +3,16 @@ from lru import LRU from eth2spec.phase0 import spec as spec_phase0 from eth2spec.test.context import is_post_altair -from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations from eth2spec.test.helpers.state import ( next_epoch, +) +from eth2spec.test.helpers.random import ( set_some_new_deposits, exit_random_validators, slash_random_validators, randomize_state, ) +from eth2spec.test.helpers.attestations import ( + cached_prepare_state_with_attestations, +) from eth2spec.utils.ssz.ssz_typing import Container, uint64, List diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index 610f4871e..d61df7610 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -1,9 +1,5 @@ -from random import Random - -from eth2spec.test.context import expect_assertion_error, is_post_altair +from eth2spec.test.context import expect_assertion_error from eth2spec.test.helpers.block import apply_empty_block, sign_block, transition_unsigned_block -from eth2spec.test.helpers.deposits import mock_deposit -from eth2spec.test.helpers.attestations import cached_prepare_state_with_attestations def get_balance(state, index): @@ -88,107 +84,3 @@ def state_transition_and_sign_block(spec, state, block, expect_fail=False): transition_unsigned_block(spec, state, block) block.state_root = state.hash_tree_root() return sign_block(spec, state, block) - - -def set_some_new_deposits(spec, state, rng): - num_validators = len(state.validators) - # Set ~1/10 to just recently deposited - for index in range(num_validators): - # If not already active, skip - if not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)): - 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]): - state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) - - -def exit_random_validators(spec, state, rng): - 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]): - continue - - validator = state.validators[index] - validator.exit_epoch = rng.choice([current_epoch - 1, current_epoch - 2, current_epoch - 3]) - # ~1/2 are withdrawable - if rng.choice([True, False]): - validator.withdrawable_epoch = current_epoch - else: - validator.withdrawable_epoch = current_epoch + 1 - - -def slash_random_validators(spec, state, rng): - # Slash ~1/2 of validators - for index in range(len(state.validators)): - # slash at least one validator - if index == 0 or rng.choice([True, False]): - spec.slash_validator(state, index) - - -def randomize_epoch_participation(spec, state, epoch, rng): - assert epoch in (spec.get_current_epoch(state), spec.get_previous_epoch(state)) - if not is_post_altair(spec): - if epoch == spec.get_current_epoch(state): - pending_attestations = state.current_epoch_attestations - else: - pending_attestations = state.previous_epoch_attestations - for pending_attestation in pending_attestations: - # ~1/3 have bad target - if rng.randint(0, 2) == 0: - pending_attestation.data.target.root = b'\x55' * 32 - # ~1/3 have bad head - if rng.randint(0, 2) == 0: - pending_attestation.data.beacon_block_root = b'\x66' * 32 - # ~50% participation - pending_attestation.aggregation_bits = [rng.choice([True, False]) - for _ in pending_attestation.aggregation_bits] - # Random inclusion delay - pending_attestation.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH) - else: - if epoch == spec.get_current_epoch(state): - epoch_participation = state.current_epoch_participation - else: - epoch_participation = state.previous_epoch_participation - for index in range(len(state.validators)): - # ~1/3 have bad head or bad target or not timely enough - is_timely_correct_head = rng.randint(0, 2) != 0 - flags = epoch_participation[index] - - def set_flag(index, value): - nonlocal flags - flag = spec.ParticipationFlags(2**index) - if value: - flags |= flag - else: - flags &= 0xff ^ flag - - set_flag(spec.TIMELY_HEAD_FLAG_INDEX, is_timely_correct_head) - if is_timely_correct_head: - # If timely head, then must be timely target - set_flag(spec.TIMELY_TARGET_FLAG_INDEX, True) - # If timely head, then must be timely source - set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, True) - else: - # ~50% of remaining have bad target or not timely enough - set_flag(spec.TIMELY_TARGET_FLAG_INDEX, rng.choice([True, False])) - # ~50% of remaining have bad source or not timely enough - set_flag(spec.TIMELY_SOURCE_FLAG_INDEX, rng.choice([True, False])) - epoch_participation[index] = flags - - -def randomize_state(spec, state, rng=Random(8020)): - set_some_new_deposits(spec, state, rng) - exit_random_validators(spec, state, rng) - slash_random_validators(spec, state, rng) - - cached_prepare_state_with_attestations(spec, state) - randomize_epoch_participation(spec, state, spec.get_previous_epoch(state), rng) - randomize_epoch_participation(spec, state, spec.get_current_epoch(state), rng)