diff --git a/scripts/build_spec.py b/scripts/build_spec.py index c38d0bd65..cca5a1bf9 100644 --- a/scripts/build_spec.py +++ b/scripts/build_spec.py @@ -82,7 +82,7 @@ def get_eth1_data(distance: uint64) -> Bytes32: return hash(distance) -def hash(x: bytes) -> Bytes32: +def hash(x: bytes) -> Bytes32: # type: ignore if x not in hash_cache: hash_cache[x] = Bytes32(_hash(x)) return hash_cache[x] diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 1c4a4eae9..22522b3bd 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -593,6 +593,34 @@ def is_active_validator(validator: Validator, epoch: Epoch) -> bool: return validator.activation_epoch <= epoch < validator.exit_epoch ``` +#### `is_eligible_for_activation_queue` + +```python +def is_eligible_for_activation_queue(validator: Validator) -> bool: + """ + Check if ``validator`` is eligible to be placed into the activation queue. + """ + return ( + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + and validator.effective_balance == MAX_EFFECTIVE_BALANCE + ) +``` + +#### `is_eligible_for_activation` + +```python +def is_eligible_for_activation(state: BeaconState, validator: Validator) -> bool: + """ + Check if ``validator`` is eligible for activation. + """ + return ( + # Placement in queue is finalized + validator.activation_eligibility_epoch <= state.finalized_checkpoint.epoch + # Has not yet been activated + and validator.activation_epoch == FAR_FUTURE_EPOCH + ) +``` + #### `is_slashable_validator` ```python @@ -630,8 +658,8 @@ def is_valid_indexed_attestation(state: BeaconState, indexed_attestation: Indexe # Verify max number of indices if not len(indices) <= MAX_VALIDATORS_PER_COMMITTEE: return False - # Verify indices are sorted - if not indices == sorted(indices): + # Verify indices are sorted and unique + if not indices == sorted(set(indices)): return False # Verify aggregate signature if not bls_verify( @@ -1304,26 +1332,22 @@ def process_rewards_and_penalties(state: BeaconState) -> None: def process_registry_updates(state: BeaconState) -> None: # Process activation eligibility and ejections for index, validator in enumerate(state.validators): - if ( - validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH - and validator.effective_balance == MAX_EFFECTIVE_BALANCE - ): - validator.activation_eligibility_epoch = get_current_epoch(state) + if is_eligible_for_activation_queue(validator): + validator.activation_eligibility_epoch = get_current_epoch(state) + 1 if is_active_validator(validator, get_current_epoch(state)) and validator.effective_balance <= EJECTION_BALANCE: initiate_validator_exit(state, ValidatorIndex(index)) - # Queue validators eligible for activation and not dequeued for activation prior to finalized epoch + # Queue validators eligible for activation and not yet dequeued for activation activation_queue = sorted([ index for index, validator in enumerate(state.validators) - if validator.activation_eligibility_epoch != FAR_FUTURE_EPOCH - and validator.activation_epoch >= compute_activation_exit_epoch(state.finalized_checkpoint.epoch) - ], key=lambda index: state.validators[index].activation_eligibility_epoch) - # Dequeued validators for activation up to churn limit (without resetting activation epoch) + if is_eligible_for_activation(state, validator) + # Order by the sequence of activation_eligibility_epoch setting and then index + ], key=lambda index: (state.validators[index].activation_eligibility_epoch, index)) + # Dequeued validators for activation up to churn limit for index in activation_queue[:get_validator_churn_limit(state)]: validator = state.validators[index] - if validator.activation_epoch == FAR_FUTURE_EPOCH: - validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) ``` #### Slashings @@ -1484,6 +1508,7 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: data = attestation.data assert data.index < get_committee_count_at_slot(state, data.slot) assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot <= data.slot + SLOTS_PER_EPOCH committee = get_beacon_committee(state, data.slot, data.index) diff --git a/specs/core/0_fork-choice.md b/specs/core/0_fork-choice.md index ae150de00..2191c24a2 100644 --- a/specs/core/0_fork-choice.md +++ b/specs/core/0_fork-choice.md @@ -104,11 +104,18 @@ def get_genesis_store(genesis_state: BeaconState) -> Store: ) ``` +#### `get_slots_since_genesis` + +```python +def get_slots_since_genesis(store: Store) -> int: + return (store.time - store.genesis_time) // SECONDS_PER_SLOT +``` + #### `get_current_slot` ```python def get_current_slot(store: Store) -> Slot: - return Slot((store.time - store.genesis_time) // SECONDS_PER_SLOT) + return Slot(GENESIS_SLOT + get_slots_since_genesis(store)) ``` #### `compute_slots_since_epoch_start` @@ -272,7 +279,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: assert block.parent_root in store.block_states pre_state = store.block_states[block.parent_root].copy() # Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past. - assert store.time >= pre_state.genesis_time + block.slot * SECONDS_PER_SLOT + assert get_current_slot(store) >= block.slot # Add new block to the store store.blocks[hash_tree_root(block)] = block # Check block is a descendant of the finalized block @@ -289,7 +296,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Update justified checkpoint if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - store.best_justified_checkpoint = state.current_justified_checkpoint + if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch: + store.best_justified_checkpoint = state.current_justified_checkpoint if should_update_justified_checkpoint(store, state.current_justified_checkpoint): store.justified_checkpoint = state.current_justified_checkpoint @@ -315,12 +323,13 @@ def on_attestation(store: Store, attestation: Attestation) -> None: # Use GENESIS_EPOCH for previous when genesis to avoid underflow previous_epoch = current_epoch - 1 if current_epoch > GENESIS_EPOCH else GENESIS_EPOCH assert target.epoch in [current_epoch, previous_epoch] + assert target.epoch == compute_epoch_at_slot(attestation.data.slot) # Attestations target be for a known block. If target block is unknown, delay consideration until the block is found assert target.root in store.blocks # Attestations cannot be from future epochs. If they are, delay consideration until the epoch arrives base_state = store.block_states[target.root].copy() - assert store.time >= base_state.genesis_time + compute_start_slot_at_epoch(target.epoch) * SECONDS_PER_SLOT + assert get_current_slot(store) >= compute_start_slot_at_epoch(target.epoch) # Attestations must be for a known block. If block is unknown, delay consideration until the block is found assert attestation.data.beacon_block_root in store.blocks @@ -335,7 +344,7 @@ def on_attestation(store: Store, attestation: Attestation) -> None: # Attestations can only affect the fork choice of subsequent slots. # Delay consideration in the fork choice until their slot is in the past. - assert store.time >= (attestation.data.slot + 1) * SECONDS_PER_SLOT + assert get_current_slot(store) >= attestation.data.slot + 1 # Get state at the `target` to validate attestation and calculate the committees indexed_attestation = get_indexed_attestation(target_state, attestation) diff --git a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py index 8f1d6f74f..d7fbc4777 100644 --- a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py +++ b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py @@ -84,6 +84,29 @@ def test_on_attestation_past_epoch(spec, state): run_on_attestation(spec, state, store, attestation, False) +@with_all_phases +@spec_state_test +def test_on_attestation_mismatched_target_and_slot(spec, state): + store = spec.get_genesis_store(state) + spec.on_tick(store, store.time + spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH) + + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + + # store block in store + spec.on_block(store, signed_block) + + attestation = get_valid_attestation(spec, state, slot=block.slot) + attestation.data.target.epoch += 1 + sign_attestation(spec, state, attestation) + + assert attestation.data.target.epoch == spec.GENESIS_EPOCH + 1 + assert spec.compute_epoch_at_slot(attestation.data.slot) == spec.GENESIS_EPOCH + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == spec.GENESIS_EPOCH + 1 + + run_on_attestation(spec, state, store, attestation, False) + + @with_all_phases @spec_state_test def test_on_attestation_target_not_in_store(spec, state): diff --git a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_block.py b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_block.py index 4fda49664..10d1c0011 100644 --- a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_block.py +++ b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_block.py @@ -168,7 +168,7 @@ def test_on_block_update_justified_checkpoint_within_safe_slots(spec, state): @with_all_phases @spec_state_test -def test_on_block_outside_safe_slots_and_old_block(spec, state): +def test_on_block_outside_safe_slots_and_multiple_better_justified(spec, state): # Initialization store = spec.get_genesis_store(state) time = 100 @@ -187,20 +187,30 @@ def test_on_block_outside_safe_slots_and_old_block(spec, state): just_block.slot = spec.compute_start_slot_at_epoch(store.justified_checkpoint.epoch) store.blocks[just_block.hash_tree_root()] = just_block - # Mock the justified checkpoint - just_state = store.block_states[last_block_root] - new_justified = spec.Checkpoint( - epoch=just_state.current_justified_checkpoint.epoch + 1, - root=just_block.hash_tree_root(), - ) - just_state.current_justified_checkpoint = new_justified - - block = build_empty_block_for_next_slot(spec, just_state) - signed_block = state_transition_and_sign_block(spec, deepcopy(just_state), block) - + # Step time past safe slots spec.on_tick(store, store.time + spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED * spec.SECONDS_PER_SLOT) assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED - run_on_block(spec, store, signed_block) - assert store.justified_checkpoint != new_justified - assert store.best_justified_checkpoint == new_justified + previously_justified = store.justified_checkpoint + + # Add a series of new blocks with "better" justifications + best_justified_checkpoint = spec.Checkpoint(epoch=0) + for i in range(3, 0, -1): + just_state = store.block_states[last_block_root] + new_justified = spec.Checkpoint( + epoch=previously_justified.epoch + i, + root=just_block.hash_tree_root(), + ) + if new_justified.epoch > best_justified_checkpoint.epoch: + best_justified_checkpoint = new_justified + + just_state.current_justified_checkpoint = new_justified + + block = build_empty_block_for_next_slot(spec, just_state) + signed_block = state_transition_and_sign_block(spec, deepcopy(just_state), block) + + run_on_block(spec, store, signed_block) + + assert store.justified_checkpoint == previously_justified + # ensure the best from the series was stored + assert store.best_justified_checkpoint == best_justified_checkpoint diff --git a/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py b/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py index f19bc66d3..d48386fd4 100644 --- a/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py +++ b/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py @@ -177,6 +177,20 @@ def test_invalid_index(spec, state): yield from run_attestation_processing(spec, state, attestation, False) +@with_all_phases +@spec_state_test +def test_mismatched_target_and_slot(spec, state): + next_epoch(spec, state) + next_epoch(spec, state) + + attestation = get_valid_attestation(spec, state) + attestation.data.slot = attestation.data.slot - spec.SLOTS_PER_EPOCH + + sign_attestation(spec, state, attestation) + + yield from run_attestation_processing(spec, state, attestation, False) + + @with_all_phases @spec_state_test def test_old_target_epoch(spec, state): diff --git a/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py b/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py index 85e807ec0..98a6e25e5 100644 --- a/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py +++ b/test_libs/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py @@ -252,6 +252,76 @@ def test_att2_bad_replaced_index(spec, state): yield from run_attester_slashing_processing(spec, state, attester_slashing, False) +@with_all_phases +@spec_state_test +@always_bls +def test_att1_duplicate_index_normal_signed(spec, state): + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=False, signed_2=True) + + indices = attester_slashing.attestation_1.attesting_indices + indices.pop(1) # remove an index, make room for the additional duplicate index. + attester_slashing.attestation_1.attesting_indices = sorted(indices) + + # sign it, the signature will be valid for a single occurence. If the transition accidentally ignores the duplicate. + sign_indexed_attestation(spec, state, attester_slashing.attestation_1) + + indices.append(indices[0]) # add one of the indices a second time + attester_slashing.attestation_1.attesting_indices = sorted(indices) + + # it will just appear normal, unless the double index is spotted + yield from run_attester_slashing_processing(spec, state, attester_slashing, False) + + +@with_all_phases +@spec_state_test +@always_bls +def test_att2_duplicate_index_normal_signed(spec, state): + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=False) + + indices = attester_slashing.attestation_2.attesting_indices + indices.pop(2) # remove an index, make room for the additional duplicate index. + attester_slashing.attestation_2.attesting_indices = sorted(indices) + + # sign it, the signature will be valid for a single occurence. If the transition accidentally ignores the duplicate. + sign_indexed_attestation(spec, state, attester_slashing.attestation_2) + + indices.append(indices[1]) # add one of the indices a second time + attester_slashing.attestation_2.attesting_indices = sorted(indices) + + # it will just appear normal, unless the double index is spotted + yield from run_attester_slashing_processing(spec, state, attester_slashing, False) + + +@with_all_phases +@spec_state_test +@always_bls +def test_att1_duplicate_index_double_signed(spec, state): + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=False, signed_2=True) + + indices = attester_slashing.attestation_1.attesting_indices + indices.pop(1) # remove an index, make room for the additional duplicate index. + indices.append(indices[2]) # add one of the indices a second time + attester_slashing.attestation_1.attesting_indices = sorted(indices) + sign_indexed_attestation(spec, state, attester_slashing.attestation_1) # will have one attester signing it double + + yield from run_attester_slashing_processing(spec, state, attester_slashing, False) + + +@with_all_phases +@spec_state_test +@always_bls +def test_att2_duplicate_index_double_signed(spec, state): + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=False) + + indices = attester_slashing.attestation_2.attesting_indices + indices.pop(1) # remove an index, make room for the additional duplicate index. + indices.append(indices[2]) # add one of the indices a second time + attester_slashing.attestation_2.attesting_indices = sorted(indices) + sign_indexed_attestation(spec, state, attester_slashing.attestation_2) # will have one attester signing it double + + yield from run_attester_slashing_processing(spec, state, attester_slashing, False) + + @with_all_phases @spec_state_test def test_unsorted_att_1(spec, state): diff --git a/test_libs/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py b/test_libs/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py index bfd992ffa..526aba277 100644 --- a/test_libs/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py +++ b/test_libs/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py @@ -17,24 +17,80 @@ def mock_deposit(spec, state, index): @with_all_phases @spec_state_test -def test_activation(spec, state): +def test_add_to_activation_queue(spec, state): + # move past first two irregular epochs wrt finality + next_epoch(spec, state) + next_epoch(spec, state) + index = 0 mock_deposit(spec, state, index) - for _ in range(spec.MAX_SEED_LOOKAHEAD + 1): - next_epoch(spec, state) + yield from run_process_registry_updates(spec, state) + + # validator moved into queue + assert state.validators[index].activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert state.validators[index].activation_epoch == spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)) + + +@with_all_phases +@spec_state_test +def test_activation_queue_to_activated_if_finalized(spec, state): + # move past first two irregular epochs wrt finality + next_epoch(spec, state) + next_epoch(spec, state) + + index = 0 + mock_deposit(spec, state, index) + + # mock validator as having been in queue since latest finalized + state.finalized_checkpoint.epoch = spec.get_current_epoch(state) - 1 + state.validators[index].activation_eligibility_epoch = state.finalized_checkpoint.epoch + + assert not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)) yield from run_process_registry_updates(spec, state) + # validator activated for future epoch assert state.validators[index].activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH assert state.validators[index].activation_epoch != spec.FAR_FUTURE_EPOCH - assert spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)) + assert not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)) + assert spec.is_active_validator( + state.validators[index], + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + ) + + +@with_all_phases +@spec_state_test +def test_activation_queue_no_activation_no_finality(spec, state): + # move past first two irregular epochs wrt finality + next_epoch(spec, state) + next_epoch(spec, state) + + index = 0 + mock_deposit(spec, state, index) + + # mock validator as having been in queue only after latest finalized + state.finalized_checkpoint.epoch = spec.get_current_epoch(state) - 1 + state.validators[index].activation_eligibility_epoch = state.finalized_checkpoint.epoch + 1 + + assert not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)) + + yield from run_process_registry_updates(spec, state) + + # validator not activated + assert state.validators[index].activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert state.validators[index].activation_epoch == spec.FAR_FUTURE_EPOCH @with_all_phases @spec_state_test def test_activation_queue_sorting(spec, state): - mock_activations = 10 + churn_limit = spec.get_validator_churn_limit(state) + + # try to activate more than the per-epoch churn linmit + mock_activations = churn_limit * 2 epoch = spec.get_current_epoch(state) for i in range(mock_activations): @@ -44,9 +100,9 @@ def test_activation_queue_sorting(spec, state): # give the last priority over the others state.validators[mock_activations - 1].activation_eligibility_epoch = epoch - # make sure we are hitting the churn - churn_limit = spec.get_validator_churn_limit(state) - assert mock_activations > churn_limit + # move state forward and finalize to allow for activations + state.slot += spec.SLOTS_PER_EPOCH * 3 + state.finalized_checkpoint.epoch = epoch + 1 yield from run_process_registry_updates(spec, state) @@ -63,6 +119,38 @@ def test_activation_queue_sorting(spec, state): assert state.validators[churn_limit - 2].activation_epoch != spec.FAR_FUTURE_EPOCH +@with_all_phases +@spec_state_test +def test_activation_queue_efficiency(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + mock_activations = churn_limit * 2 + + epoch = spec.get_current_epoch(state) + for i in range(mock_activations): + mock_deposit(spec, state, i) + state.validators[i].activation_eligibility_epoch = epoch + 1 + + # move state forward and finalize to allow for activations + state.slot += spec.SLOTS_PER_EPOCH * 3 + state.finalized_checkpoint.epoch = epoch + 1 + + # Run first registry update. Do not yield test vectors + for _ in run_process_registry_updates(spec, state): + pass + + # Half should churn in first run of registry update + for i in range(mock_activations): + if i < mock_activations // 2: + assert state.validators[i].activation_epoch < spec.FAR_FUTURE_EPOCH + else: + assert state.validators[i].activation_epoch == spec.FAR_FUTURE_EPOCH + + # Second half should churn in second run of registry update + yield from run_process_registry_updates(spec, state) + for i in range(mock_activations): + assert state.validators[i].activation_epoch < spec.FAR_FUTURE_EPOCH + + @with_all_phases @spec_state_test def test_ejection(spec, state): @@ -73,13 +161,87 @@ def test_ejection(spec, state): # Mock an ejection state.validators[index].effective_balance = spec.EJECTION_BALANCE - for _ in range(spec.MAX_SEED_LOOKAHEAD + 1): - next_epoch(spec, state) - yield from run_process_registry_updates(spec, state) assert state.validators[index].exit_epoch != spec.FAR_FUTURE_EPOCH + assert spec.is_active_validator(state.validators[index], spec.get_current_epoch(state)) assert not spec.is_active_validator( state.validators[index], - spec.get_current_epoch(state), + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + ) + + +@with_all_phases +@spec_state_test +def test_ejection_past_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + + # try to eject more than per-epoch churn limit + mock_ejections = churn_limit * 3 + + for i in range(mock_ejections): + state.validators[i].effective_balance = spec.EJECTION_BALANCE + + expected_ejection_epoch = spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + + yield from run_process_registry_updates(spec, state) + + for i in range(mock_ejections): + # first third ejected in normal speed + if i < mock_ejections // 3: + assert state.validators[i].exit_epoch == expected_ejection_epoch + # second thirdgets delayed by 1 epoch + elif mock_ejections // 3 <= i < mock_ejections * 2 // 3: + assert state.validators[i].exit_epoch == expected_ejection_epoch + 1 + # second thirdgets delayed by 2 epochs + else: + assert state.validators[i].exit_epoch == expected_ejection_epoch + 2 + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection(spec, state): + # move past first two irregular epochs wrt finality + next_epoch(spec, state) + next_epoch(spec, state) + + # ready for entrance into activation queue + activation_queue_index = 0 + mock_deposit(spec, state, activation_queue_index) + + # ready for activation + activation_index = 1 + mock_deposit(spec, state, activation_index) + state.finalized_checkpoint.epoch = spec.get_current_epoch(state) - 1 + state.validators[activation_index].activation_eligibility_epoch = state.finalized_checkpoint.epoch + + # ready for ejection + ejection_index = 2 + state.validators[ejection_index].effective_balance = spec.EJECTION_BALANCE + + yield from run_process_registry_updates(spec, state) + + # validator moved into activation queue + validator = state.validators[activation_queue_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + # validator activated for future epoch + validator = state.validators[activation_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch != spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert spec.is_active_validator( + validator, + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + ) + + # validator ejected for future epoch + validator = state.validators[ejection_index] + assert validator.exit_epoch != spec.FAR_FUTURE_EPOCH + assert spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert not spec.is_active_validator( + validator, + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) ) diff --git a/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py b/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py index 41316e92d..c2f980ba0 100644 --- a/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py +++ b/test_libs/pyspec/eth2spec/test/sanity/test_blocks.py @@ -3,7 +3,7 @@ from copy import deepcopy from eth2spec.utils.ssz.ssz_impl import hash_tree_root from eth2spec.utils.bls import bls_sign -from eth2spec.test.helpers.state import get_balance, state_transition_and_sign_block +from eth2spec.test.helpers.state import get_balance, state_transition_and_sign_block, next_slot from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block, \ transition_unsigned_block from eth2spec.test.helpers.keys import privkeys, pubkeys @@ -253,6 +253,58 @@ def test_attester_slashing(spec, state): ) +@with_all_phases +@spec_state_test +def test_proposer_after_inactive_index(spec, state): + # disable some low validator index to check after for + inactive_index = 10 + state.validators[inactive_index].exit_epoch = spec.get_current_epoch(state) + + # skip forward, get brand new proposers + state.slot = spec.SLOTS_PER_EPOCH * 2 + block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, block) + + while True: + next_slot(spec, state) + proposer_index = spec.get_beacon_proposer_index(state) + if proposer_index > inactive_index: + # found a proposer that has a higher index than a disabled validator + yield 'pre', state + # test if the proposer can be recognized correctly after the inactive validator + signed_block = state_transition_and_sign_block(spec, state, build_empty_block(spec, state)) + yield 'blocks', [signed_block] + yield 'post', state + break + + +@with_all_phases +@spec_state_test +def test_high_proposer_index(spec, state): + # disable a good amount of validators to make the active count lower, for a faster test + current_epoch = spec.get_current_epoch(state) + for i in range(len(state.validators) // 3): + state.validators[i].exit_epoch = current_epoch + + # skip forward, get brand new proposers + state.slot = spec.SLOTS_PER_EPOCH * 2 + block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, block) + + active_count = len(spec.get_active_validator_indices(state, current_epoch)) + while True: + next_slot(spec, state) + proposer_index = spec.get_beacon_proposer_index(state) + if proposer_index >= active_count: + # found a proposer that has a higher index than the active validator count + yield 'pre', state + # test if the proposer can be recognized correctly, even while it has a high index. + signed_block = state_transition_and_sign_block(spec, state, build_empty_block(spec, state)) + yield 'blocks', [signed_block] + yield 'post', state + break + + @with_all_phases @spec_state_test def test_expected_deposit_in_block(spec, state): diff --git a/test_libs/pyspec/requirements-testing.txt b/test_libs/pyspec/requirements-testing.txt index b5229ae20..e8ecd12a6 100644 --- a/test_libs/pyspec/requirements-testing.txt +++ b/test_libs/pyspec/requirements-testing.txt @@ -2,6 +2,6 @@ pytest>=4.4 ../config_helpers flake8==3.7.7 -mypy==0.701 +mypy==0.750 pytest-cov pytest-xdist