Repository: ethereum/consensus-specs

Issue:
Test coverage: attester slashings with duplicated attesting indices
While updating to v0.9.1 I noticed that the spec allows validator indices to be duplicated in `IndexedAttestation`s. In Lighthouse our function to check the sorting of the indices was also (incorrectly) checking for duplicates, which wasn't picked up by any of our tests or the spec tests. I think it would be good to have a test vector with an attester slashing containing duplicated indices, if that is indeed the intended behaviour. For the signature, it's sufficient to add the signatures of the duplicated validators to the aggregate multiple times. Concretely, this attester slashing passes validation (pardon the Rust):

```rust
let sig1 = Signature::new(&data1.tree_hash_root(), domain, &keypairs[0].sk);
let sig2 = Signature::new(&data2.tree_hash_root(), domain, &keypairs[0].sk);

let mut agg1 = AggregateSignature::new();
agg1.add(&sig1);
agg1.add(&sig1);

let mut agg2 = AggregateSignature::new();
agg2.add(&sig2);
agg2.add(&sig2);

let attestation_1 = IndexedAttestation {
    attesting_indices: vec![0, 0].into(),
    data: data1,
    signature: agg1,
};

let attestation_2 = IndexedAttestation {
    attesting_indices: vec![0, 0].into(),
    data: data2,
    signature: agg2,
};

let attester_slashing = AttesterSlashing {
    attestation_1,
    attestation_2,
};
```
Proposer index edge cases to test
So thanks to Prysm testnet, an under-tested proposer property was found: the proposer index and shuffling can both be correct, but if the active validators don't match regular indices there can still be a problem. And it only is off after the first inactive validator index in the system, so some tests could look like they test shuffling well (or indirectly as by rewards or otherwise), but just missed it.

TLDR: todo, add test cases for:
- `proposer index > len(active validators)` (already have proposals, but not this edge case)
- `proposer index > any slashed index` (already have slashings, but not this edge case).

Larger validator numbers move the proposer index to be more random and more likely to be bigger than those other numbers, while the current smaller validator sets in tests are generally only 512 validators. (and increasing that would increase the total test size by approx. the same factor).


Discussion:

Resolving patch:
diff --git a/scripts/build_spec.py b/scripts/build_spec.py
--- 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]


Test patch:
diff --git a/specs/test_formats/ssz_generic/README.md b/specs/test_formats/ssz_generic/README.md
--- a/specs/test_formats/ssz_generic/README.md
+++ b/specs/test_formats/ssz_generic/README.md
@@ -37,7 +37,7 @@ Valid has 3 parts: `meta.yaml`, `serialized.ssz`, `value.yaml`
 
 ### `meta.yaml`
 
-Valid ssz objects can have a hash-tree-root, and for some types also a signing-root.
+Valid ssz objects can have a hash-tree-root.
 The expected roots are encoded into the metadata yaml:
 
 ```yaml
@@ -61,7 +61,6 @@ The conditions are the same for each type:
 - Encoding: After encoding the given `value` object, the output should match `serialized`.
 - Decoding: After decoding the given `serialized` bytes, it should match the `value` object. 
 - Hash-tree-root: the root should match the root declared in the metadata.
-- Signing-root: if present in metadata, the signing root of the object should match the container.
 
 ## `invalid`
 
diff --git a/test_libs/pyspec/eth2spec/test/fork_choice/test_get_head.py b/test_libs/pyspec/eth2spec/test/fork_choice/test_get_head.py
--- a/test_libs/pyspec/eth2spec/test/fork_choice/test_get_head.py
+++ b/test_libs/pyspec/eth2spec/test/fork_choice/test_get_head.py
@@ -1,7 +1,11 @@
 from eth2spec.test.context import with_all_phases, spec_state_test
 from eth2spec.test.helpers.attestations import get_valid_attestation
 from eth2spec.test.helpers.block import build_empty_block_for_next_slot
-from eth2spec.test.helpers.state import state_transition_and_sign_block
+from eth2spec.test.helpers.state import (
+    next_epoch,
+    next_epoch_with_attestations,
+    state_transition_and_sign_block,
+)
 
 
 def add_block_to_store(spec, store, signed_block):
@@ -112,3 +116,79 @@ def test_shorter_chain_but_heavier_weight(spec, state):
     add_attestation_to_store(spec, store, short_attestation)
 
     assert spec.get_head(store) == spec.hash_tree_root(short_block)
+
+
+@with_all_phases
+@spec_state_test
+def test_filtered_block_tree(spec, state):
+    # Initialization
+    genesis_state_root = state.hash_tree_root()
+    store = spec.get_genesis_store(state)
+    genesis_block = spec.BeaconBlock(state_root=genesis_state_root)
+
+    # transition state past initial couple of epochs
+    next_epoch(spec, state)
+    next_epoch(spec, state)
+
+    assert spec.get_head(store) == spec.hash_tree_root(genesis_block)
+
+    # fill in attestations for entire epoch, justifying the recent epoch
+    prev_state, signed_blocks, state = next_epoch_with_attestations(spec, state, True, False)
+    attestations = [
+        attestation for signed_block in signed_blocks
+        for attestation in signed_block.message.body.attestations
+    ]
+    assert state.current_justified_checkpoint.epoch > prev_state.current_justified_checkpoint.epoch
+
+    # tick time forward and add blocks and attestations to store
+    current_time = state.slot * spec.SECONDS_PER_SLOT + store.genesis_time
+    spec.on_tick(store, current_time)
+    for signed_block in signed_blocks:
+        spec.on_block(store, signed_block)
+    for attestation in attestations:
+        spec.on_attestation(store, attestation)
+
+    assert store.justified_checkpoint == state.current_justified_checkpoint
+
+    # the last block in the branch should be the head
+    expected_head_root = spec.hash_tree_root(signed_blocks[-1].message)
+    assert spec.get_head(store) == expected_head_root
+
+    #
+    # create branch containing the justified block but not containing enough on
+    # chain votes to justify that block
+    #
+
+    # build a chain without attestations off of previous justified block
+    non_viable_state = store.block_states[store.justified_checkpoint.root].copy()
+
+    # ensure that next wave of votes are for future epoch
+    next_epoch(spec, non_viable_state)
+    next_epoch(spec, non_viable_state)
+    next_epoch(spec, non_viable_state)
+    assert spec.get_current_epoch(non_viable_state) > store.justified_checkpoint.epoch
+
+    # create rogue block that will be attested to in this non-viable branch
+    rogue_block = build_empty_block_for_next_slot(spec, non_viable_state)
+    signed_rogue_block = state_transition_and_sign_block(spec, non_viable_state, rogue_block)
+
+    # create an epoch's worth of attestations for the rogue block
+    next_epoch(spec, non_viable_state)
+    attestations = []
+    for i in range(spec.SLOTS_PER_EPOCH):
+        slot = rogue_block.slot + i
+        for index in range(spec.get_committee_count_at_slot(non_viable_state, slot)):
+            attestation = get_valid_attestation(spec, non_viable_state, rogue_block.slot + i, index)
+            attestations.append(attestation)
+
+    # tick time forward to be able to include up to the latest attestation
+    current_time = (attestations[-1].data.slot + 1) * spec.SECONDS_PER_SLOT + store.genesis_time
+    spec.on_tick(store, current_time)
+
+    # include rogue block and associated attestations in the store
+    spec.on_block(store, signed_rogue_block)
+    for attestation in attestations:
+        spec.on_attestation(store, attestation)
+
+    # ensure that get_head still returns the head from the previous branch
+    assert spec.get_head(store) == expected_head_root
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
--- 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
--- 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
--- 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
--- 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
--- 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)
+
+    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)
 
-    for _ in range(spec.MAX_SEED_LOOKAHEAD + 1):
-        next_epoch(spec, state)
+    # 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
--- 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 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
--- 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