Repository: ethereum/consensus-specs

Issue:
Attestation validation differences between fork choice and process_attestation
An attestation contains three votes: LMD GHOST vote, FFG vote and Crosslink vote.
The beacon chain specs specify how to validate an attestation and its votes.
However, the validation is split in several parts: some checks defined in [fork_choice/on_attestation](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_fork-choice.md#on_attestation) and others - in [process_attestation](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_beacon-chain.md#attestations). They both invoke [is_valid_indexed_attestation](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_beacon-chain.md#is_valid_indexed_attestation).

The `on_attestation` deals mostly with LMD GHOST vote and part of FFG vote (`target` field). So, if there arrives an attestation with an invalid crosslink vote or an invalid `source` fields, then it is still accepted by the fork choice spec and affects the LMD GHOST fork choice.
However, the attestation should not be included in newly proposed blocks, since it won't pass `process_attestation` checks (see [validator spec](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/validator/0_beacon-chain-validator.md#attestations).

While there may be reasons to split validation in two parts, this can lead to inconsistencies in fork choice. Consider a case, when there are two head candidates, both having the same amount of votes and tie break chooses the first one. If a partially correct attestation added, which votes for the second block, then the second block becomes the head, based on off-chain plus on-chain attestation. But since this last vote cannot be incorporated into the chain, from on-chain attestations perspective, the first candidate is the head. I do not fully understand consequences of the discrepancy, but it seems awkward at least.

Another example. If such a partially valid attestations is propagated only to part of the network nodes, it cannot reach other (honest) validators via proposed blocks, because of `process_attestation` assertion failures. It can result in different views on the state of the blockchain, which can be only corrected by propagating such invalid attestations to other nodes.
So this looks like a potential unnecessary source of forks, which can be exploited by an attacker.


Discussion:
There are multiple things to consider here:

There is a related thing with "should we verify twice"; I'm heavily opposed to doing a single-pass verification of the attestation for several reasons:
- relying on your pool of operations is bad. Bitcoin Core had a bug where consensus was partially reliant on the mempool being validated (iirc).
- the fork-choice is different from the state transition, since voting is not the only thing happening with attestation, more on this below. Fork-choice can just verify the format, and the weight, and it is valid enough.

Fork-choice accepting partially invalid attestations is not that bad. If an attestation has weight and is signed, it still helps determine the head. We shouldn't rely too much on full state details being available to process it in a large graph of forks.
And if the attestation is really bad, it can be slashed. It having an invalid crosslink may hurt it for the attesters economical purposes as it won't get included on-chain, but it won't hurt the chain itself.

> Fork-choice accepting partially invalid attestations is not that bad.

We might want to have same inputs for the fork choice disregard the source of attestations. I.e. it should have no matter whether attestation was gossiped or came within one of the blocks. If fork choice doesn't run those checks then we may get different heads depending on attestation source. 

Also, if you've checked signature, don't you already have a state to check crosslink and checkpoits?

There are two cases with partially valid attestations:
* block-wise invalid but fork-choice-wise valid attestation, i.e. which affects fork choice, but cannot be included in a block
* fork-choice-wise invalid but block-wise valid attestation, i.e. which can be included in a block, but shouldn't affect fork choice (of honest validators)

The second case doesn't look like a problem - the block is used as a transport for attestations, and invalid ones will be filtered out in on_attestation method. They could have come from wire as plain messages as well.

The first cases looks like a problem, since attestations affecting fork choice cannot be persisted, i.e. included in the block chain. And will be forgotten eventually. So, if an impact of such partially valid attestations is significant (i.e. leading to a situation where an alternate block becomes the head), then it looks like somewhat severe problem. Since the on-chain information cannot explain the choice made, in some cases.

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -108,7 +108,7 @@ def get_spec(file_name: str) -> SpecObject:
 PHASE1_IMPORTS = '''from eth2spec.phase0 import spec as phase0
 from eth2spec.config.config_util import apply_constants_config
 from typing import (
-    Any, Dict, Set, Sequence, NewType, Tuple, TypeVar, Callable
+    Any, Dict, Set, Sequence, NewType, Tuple, TypeVar, Callable, Optional
 )
 
 from dataclasses import (
@@ -146,8 +146,14 @@ def ceillog2(x: uint64) -> int:
 hash_cache: Dict[bytes, Bytes32] = {}
 
 
-def get_eth1_data(distance: uint64) -> Bytes32:
-    return hash(distance)
+def get_eth1_data(block: Eth1Block) -> Eth1Data:
+    """
+    A stub function return mocking Eth1Data.
+    """
+    return Eth1Data(
+        deposit_root=block.deposit_root,
+        deposit_count=block.deposit_count,
+        block_hash=hash_tree_root(block))
 
 
 def hash(x: bytes) -> Bytes32:  # type: ignore
@@ -373,9 +379,10 @@ def finalize_options(self):
                 self.md_doc_paths = """
                     specs/phase0/beacon-chain.md
                     specs/phase0/fork-choice.md
+                    specs/phase0/validator.md
                     specs/phase1/custody-game.md
                     specs/phase1/beacon-chain.md
-                    specs/phase1/fraud-proofs.md
+                    specs/phase1/shard-transition.md
                     specs/phase1/fork-choice.md
                     specs/phase1/phase1-fork.md
                 """
@@ -478,6 +485,7 @@ def run(self):
     url="https://github.com/ethereum/eth2.0-specs",
     include_package_data=False,
     package_data={'configs': ['*.yaml'],
+                 
                   'specs': ['**/*.md'],
                   'eth2spec': ['VERSION.txt']},
     package_dir={
@@ -497,9 +505,10 @@ def run(self):
         "eth-utils>=1.3.0,<2",
         "eth-typing>=2.1.0,<3.0.0",
         "pycryptodome==3.9.4",
-        "py_ecc==2.0.0",
+        "py_ecc==4.0.0",
+        "milagro_bls_binding==1.3.0",
         "dataclasses==0.6",
-        "remerkleable==0.1.13",
+        "remerkleable==0.1.16",
         "ruamel.yaml==0.16.5",
         "lru-dict==1.1.6"
     ]


Test patch:
diff --git a/tests/core/pyspec/README.md b/tests/core/pyspec/README.md
--- a/tests/core/pyspec/README.md
+++ b/tests/core/pyspec/README.md
@@ -55,6 +55,11 @@ Run the test command from the `tests/core/pyspec` directory:
 pytest --config=minimal eth2spec
 ```
 
+Options:
+- `--config`, to change the config. Defaults to `minimal`, can be set to `mainnet`, or other configs from the configs directory.
+- `--disable-bls`, to disable BLS (only for tests that can run without)
+- `--bls-type`, `milagro` or `py_ecc` (default)
+
 ### How to view code coverage report
 
 Run `make open_cov` from the root of the specs repository after running `make test` to open the html code coverage report.
diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt
--- a/tests/core/pyspec/eth2spec/VERSION.txt
+++ b/tests/core/pyspec/eth2spec/VERSION.txt
@@ -1 +1 @@
-0.11.2
\ No newline at end of file
+0.12.0
\ No newline at end of file
diff --git a/tests/core/pyspec/eth2spec/config/README.md b/tests/core/pyspec/eth2spec/config/README.md
--- a/tests/core/pyspec/eth2spec/config/README.md
+++ b/tests/core/pyspec/eth2spec/config/README.md
@@ -12,7 +12,7 @@ configs_path = 'configs/'
 from eth2spec.config import config_util
 from eth2spec.phase0 import spec
 from importlib import reload
-my_presets = config_util.prepare_config(configs_path, 'mainnet')
+config_util.prepare_config(configs_path, 'mainnet')
 # reload spec to make loaded config effective
 reload(spec)
 ```
diff --git a/tests/core/pyspec/eth2spec/config/config_util.py b/tests/core/pyspec/eth2spec/config/config_util.py
--- a/tests/core/pyspec/eth2spec/config/config_util.py
+++ b/tests/core/pyspec/eth2spec/config/config_util.py
@@ -24,12 +24,12 @@ def apply_constants_config(spec_globals: Dict[str, Any], warn_if_unknown: bool =
 
 # Load presets from a file, and then prepares the global config setting. This does not apply the config.
 # To apply the config, reload the spec module (it will re-initialize with the config taken from here).
-def prepare_config(configs_path, config_name):
+def prepare_config(configs_path: str, config_name: str) -> None:
     global config
     config = load_config_file(configs_path, config_name)
 
 
-def load_config_file(configs_dir, presets_name) -> Dict[str, Any]:
+def load_config_file(configs_dir: str, presets_name: str) -> Dict[str, Any]:
     """
     Loads the given preset
     :param presets_name: The name of the presets. (lowercase snake_case)
@@ -38,7 +38,7 @@ def load_config_file(configs_dir, presets_name) -> Dict[str, Any]:
     path = Path(join(configs_dir, presets_name + '.yaml'))
     yaml = YAML(typ='base')
     loaded = yaml.load(path)
-    out = dict()
+    out: Dict[str, Any] = dict()
     for k, v in loaded.items():
         if isinstance(v, list):
             # Clean up integer values. YAML parser renders lists of ints as list of str
diff --git a/tests/core/pyspec/eth2spec/test/conftest.py b/tests/core/pyspec/eth2spec/test/conftest.py
--- a/tests/core/pyspec/eth2spec/test/conftest.py
+++ b/tests/core/pyspec/eth2spec/test/conftest.py
@@ -1,6 +1,6 @@
 from eth2spec.config import config_util
-from eth2spec.test.context import reload_specs
-
+from eth2spec.test import context
+from eth2spec.utils import bls as bls_utils
 
 # We import pytest only when it's present, i.e. when we are running tests.
 # The test-cases themselves can be generated without installing pytest.
@@ -27,7 +27,16 @@ def ignore():
 
 def pytest_addoption(parser):
     parser.addoption(
-        "--config", action="store", default="minimal", help="config: make the pyspec use the specified configuration"
+        "--config", action="store", type=str, default="minimal",
+        help="config: make the pyspec use the specified configuration"
+    )
+    parser.addoption(
+        "--disable-bls", action="store_true",
+        help="bls-default: make tests that are not dependent on BLS run without BLS"
+    )
+    parser.addoption(
+        "--bls-type", action="store", type=str, default="py_ecc", choices=["py_ecc", "milagro"],
+        help="bls-type: use 'pyecc' or 'milagro' implementation for BLS"
     )
 
 
@@ -36,4 +45,22 @@ def config(request):
     config_name = request.config.getoption("--config")
     config_util.prepare_config('../../../configs/', config_name)
     # now that the presets are loaded, reload the specs to apply them
-    reload_specs()
+    context.reload_specs()
+
+
+@fixture(autouse=True)
+def bls_default(request):
+    disable_bls = request.config.getoption("--disable-bls")
+    if disable_bls:
+        context.DEFAULT_BLS_ACTIVE = False
+
+
+@fixture(autouse=True)
+def bls_type(request):
+    bls_type = request.config.getoption("--bls-type")
+    if bls_type == "py_ecc":
+        bls_utils.bls = bls_utils.py_ecc_bls
+    elif bls_type == "milagro":
+        bls_utils.bls = bls_utils.milagro_bls
+    else:
+        raise Exception(f"unrecognized bls type: {bls_type}")
diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py
--- a/tests/core/pyspec/eth2spec/test/context.py
+++ b/tests/core/pyspec/eth2spec/test/context.py
@@ -9,6 +9,8 @@
 from random import Random
 from typing import Any, Callable, NewType, Sequence, TypedDict, Protocol
 
+from lru import LRU
+
 from importlib import reload
 
 
@@ -48,28 +50,46 @@ class SpecForks(TypedDict, total=False):
     PHASE1: SpecPhase1
 
 
+def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int],
+                   spec: Spec, phases: SpecForks):
+
+    p0 = phases[PHASE0]
+    balances = balances_fn(p0)
+    activation_threshold = threshold_fn(p0)
+
+    state = create_genesis_state(spec=p0, validator_balances=balances,
+                                 activation_threshold=activation_threshold)
+    if spec.fork == PHASE1:
+        # TODO: instead of upgrading a test phase0 genesis state we can also write a phase1 state helper.
+        # Decide based on performance/consistency results later.
+        state = phases[PHASE1].upgrade_to_phase1(state)
+        # Shard state slot must lag behind BeaconState slot by at least 1
+        # Will handle this more elegantly with fork mechanics
+        spec.process_slots(state, state.slot + 1)
+
+    return state
+
+
+_custom_state_cache_dict = LRU(size=10)
+
+
 def with_custom_state(balances_fn: Callable[[Any], Sequence[int]],
                       threshold_fn: Callable[[Any], int]):
     def deco(fn):
+
         def entry(*args, spec: Spec, phases: SpecForks, **kw):
-            try:
-                p0 = phases[PHASE0]
-                balances = balances_fn(p0)
-                activation_threshold = threshold_fn(p0)
-
-                state = create_genesis_state(spec=p0, validator_balances=balances,
-                                             activation_threshold=activation_threshold)
-                if spec.fork == PHASE1:
-                    # TODO: instead of upgrading a test phase0 genesis state we can also write a phase1 state helper.
-                    # Decide based on performance/consistency results later.
-                    state = phases[PHASE1].upgrade_to_phase1(state)
-                    # Shard state slot must lag behind BeaconState slot by at least 1
-                    # Will handle this more elegantly with fork mechanics
-                    spec.process_slots(state, state.slot + 1)
-
-                kw['state'] = state
-            except KeyError:
-                raise TypeError('Spec decorator must come within state decorator to inject spec into state.')
+            # make a key for the state
+            # genesis fork version separates configs during test-generation runtime.
+            key = (spec.fork, spec.GENESIS_FORK_VERSION, spec.__file__, balances_fn, threshold_fn)
+            global _custom_state_cache_dict
+            if key not in _custom_state_cache_dict:
+                state = _prepare_state(balances_fn, threshold_fn, spec, phases)
+                _custom_state_cache_dict[key] = state.get_backing()
+
+            # Take an entry out of the LRU.
+            # No copy is necessary, as we wrap the immutable backing with a new view.
+            state = spec.BeaconState(backing=_custom_state_cache_dict[key])
+            kw['state'] = state
             return fn(*args, spec=spec, phases=phases, **kw)
         return entry
     return deco
@@ -147,14 +167,15 @@ def entry(*args, **kw):
     return entry
 
 
-# BLS is turned off by default *for performance purposes during TESTING*.
+# BLS is turned on by default, it can be disabled in tests by overriding this, or using `--disable-bls`.
+# *This is for performance purposes during TESTING, DO NOT DISABLE IN PRODUCTION*.
 # The runner of the test can indicate the preferred setting (test generators prefer BLS to be ON).
 # - Some tests are marked as BLS-requiring, and ignore this setting.
 #    (tests that express differences caused by BLS, e.g. invalid signatures being rejected)
 # - Some other tests are marked as BLS-ignoring, and ignore this setting.
 #    (tests that are heavily performance impacted / require unsigned state transitions)
 # - Most tests respect the BLS setting.
-DEFAULT_BLS_ACTIVE = False
+DEFAULT_BLS_ACTIVE = True
 
 
 def spec_test(fn):
diff --git a/tests/core/pyspec/eth2spec/test/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/fork_choice/test_get_head.py
--- a/tests/core/pyspec/eth2spec/test/fork_choice/test_get_head.py
+++ b/tests/core/pyspec/eth2spec/test/fork_choice/test_get_head.py
@@ -183,7 +183,7 @@ def test_filtered_block_tree(spec, state):
     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)
+            attestation = get_valid_attestation(spec, non_viable_state, slot, index, signed=True)
             attestations.append(attestation)
 
     # tick time forward to be able to include up to the latest attestation
diff --git a/tests/core/pyspec/eth2spec/test/fork_choice/test_on_attestation.py b/tests/core/pyspec/eth2spec/test/fork_choice/test_on_attestation.py
--- a/tests/core/pyspec/eth2spec/test/fork_choice/test_on_attestation.py
+++ b/tests/core/pyspec/eth2spec/test/fork_choice/test_on_attestation.py
@@ -1,7 +1,7 @@
 from eth2spec.test.context import PHASE0, with_all_phases, spec_state_test
 from eth2spec.test.helpers.block import build_empty_block_for_next_slot
 from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation
-from eth2spec.test.helpers.state import transition_to, state_transition_and_sign_block, next_epoch
+from eth2spec.test.helpers.state import transition_to, state_transition_and_sign_block, next_epoch, next_slot
 
 
 def run_on_attestation(spec, state, store, attestation, valid=True):
@@ -116,6 +116,44 @@ def test_on_attestation_mismatched_target_and_slot(spec, state):
     run_on_attestation(spec, state, store, attestation, False)
 
 
+@with_all_phases
+@spec_state_test
+def test_on_attestation_inconsistent_target_and_head(spec, state):
+    store = spec.get_forkchoice_store(state)
+    spec.on_tick(store, store.time + 2 * spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH)
+
+    # Create chain 1 as empty chain between genesis and start of 1st epoch
+    target_state_1 = state.copy()
+    next_epoch(spec, target_state_1)
+
+    # Create chain 2 with different block in chain from chain 1 from chain 1 from chain 1 from chain 1
+    target_state_2 = state.copy()
+    diff_block = build_empty_block_for_next_slot(spec, target_state_2)
+    signed_diff_block = state_transition_and_sign_block(spec, target_state_2, diff_block)
+    spec.on_block(store, signed_diff_block)
+    next_epoch(spec, target_state_2)
+    next_slot(spec, target_state_2)
+
+    # Create and store block new head block on target state 1
+    head_block = build_empty_block_for_next_slot(spec, target_state_1)
+    signed_head_block = state_transition_and_sign_block(spec, target_state_1, head_block)
+    spec.on_block(store, signed_head_block)
+
+    # Attest to head of chain 1
+    attestation = get_valid_attestation(spec, target_state_1, slot=head_block.slot, signed=False)
+    epoch = spec.compute_epoch_at_slot(attestation.data.slot)
+
+    # Set attestation target to be from chain 2
+    attestation.data.target = spec.Checkpoint(epoch=epoch, root=spec.get_block_root(target_state_2, epoch))
+    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 + 1
+    assert spec.get_block_root(target_state_1, epoch) != attestation.data.target.root
+
+    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/tests/core/pyspec/eth2spec/test/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/fork_choice/test_on_block.py
--- a/tests/core/pyspec/eth2spec/test/fork_choice/test_on_block.py
+++ b/tests/core/pyspec/eth2spec/test/fork_choice/test_on_block.py
@@ -184,7 +184,7 @@ def test_on_block_finalized_skip_slots_not_in_skip_chain(spec, state):
 def test_on_block_update_justified_checkpoint_within_safe_slots(spec, state):
     # Initialization
     store = spec.get_forkchoice_store(state)
-    time = 100
+    time = 0
     spec.on_tick(store, time)
 
     next_epoch(spec, state)
@@ -215,7 +215,7 @@ def test_on_block_update_justified_checkpoint_within_safe_slots(spec, state):
 def test_on_block_outside_safe_slots_and_multiple_better_justified(spec, state):
     # Initialization
     store = spec.get_forkchoice_store(state)
-    time = 100
+    time = 0
     spec.on_tick(store, time)
 
     next_epoch(spec, state)
diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py
--- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py
@@ -1,11 +1,12 @@
 from typing import List
 
-from eth2spec.test.context import expect_assertion_error, PHASE0
-from eth2spec.test.helpers.state import state_transition_and_sign_block
+from eth2spec.test.context import expect_assertion_error, PHASE0, PHASE1
+from eth2spec.test.helpers.state import state_transition_and_sign_block, next_epoch, next_slot, transition_to
 from eth2spec.test.helpers.block import build_empty_block_for_next_slot
 from eth2spec.test.helpers.keys import privkeys
 from eth2spec.utils import bls
 from eth2spec.utils.ssz.ssz_typing import Bitlist
+from lru import LRU
 
 
 def run_attestation_processing(spec, state, attestation, valid=True):
@@ -43,7 +44,7 @@ def run_attestation_processing(spec, state, attestation, valid=True):
     yield 'post', state
 
 
-def build_attestation_data(spec, state, slot, index):
+def build_attestation_data(spec, state, slot, index, shard_transition=None, on_time=True):
     assert state.slot >= slot
 
     if slot == state.slot:
@@ -66,7 +67,7 @@ def build_attestation_data(spec, state, slot, index):
         source_epoch = state.current_justified_checkpoint.epoch
         source_root = state.current_justified_checkpoint.root
 
-    return spec.AttestationData(
+    attestation_data = spec.AttestationData(
         slot=slot,
         index=index,
         beacon_block_root=block_root,
@@ -74,11 +75,34 @@ def build_attestation_data(spec, state, slot, index):
         target=spec.Checkpoint(epoch=spec.compute_epoch_at_slot(slot), root=epoch_boundary_root),
     )
 
+    if spec.fork == PHASE1:
+        if shard_transition is not None:
+            lastest_shard_data_root_index = len(shard_transition.shard_data_roots) - 1
+            attestation_data.head_shard_root = shard_transition.shard_data_roots[lastest_shard_data_root_index]
+            attestation_data.shard_transition_root = shard_transition.hash_tree_root()
+        else:
+            # No shard transition
+            shard = spec.get_shard(state, spec.Attestation(data=attestation_data))
+            if on_time:
+                temp_state = state.copy()
+                next_slot(spec, temp_state)
+                shard_transition = spec.get_shard_transition(temp_state, shard, [])
+                lastest_shard_data_root_index = len(shard_transition.shard_data_roots) - 1
+                attestation_data.head_shard_root = shard_transition.shard_data_roots[lastest_shard_data_root_index]
+                attestation_data.shard_transition_root = shard_transition.hash_tree_root()
+            else:
+                attestation_data.head_shard_root = state.shard_states[shard].transition_digest
+                attestation_data.shard_transition_root = spec.Root()
+    return attestation_data
+
 
 def convert_to_valid_on_time_attestation(spec, state, attestation, signed=False):
     shard = spec.get_shard(state, attestation)
-    offset_slots = spec.compute_offset_slots(spec.get_latest_slot_for_shard(state, shard), state.slot + 1)
-    for offset_slot in offset_slots:
+    offset_slots = spec.compute_offset_slots(
+        spec.get_latest_slot_for_shard(state, shard),
+        attestation.data.slot + spec.MIN_ATTESTATION_INCLUSION_DELAY,
+    )
+    for _ in offset_slots:
         attestation.custody_bits_blocks.append(
             Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE]([0 for _ in attestation.aggregation_bits])
         )
@@ -89,7 +113,7 @@ def convert_to_valid_on_time_attestation(spec, state, attestation, signed=False)
     return attestation
 
 
-def get_valid_on_time_attestation(spec, state, slot=None, index=None, signed=False):
+def get_valid_on_time_attestation(spec, state, slot=None, index=None, shard_transition=None, signed=False):
     '''
     Construct on-time attestation for next slot
     '''
@@ -98,7 +122,15 @@ def get_valid_on_time_attestation(spec, state, slot=None, index=None, signed=Fal
     if index is None:
         index = 0
 
-    return get_valid_attestation(spec, state, slot=slot, index=index, signed=signed, on_time=True)
+    return get_valid_attestation(
+        spec,
+        state,
+        slot=slot,
+        index=index,
+        shard_transition=shard_transition,
+        signed=signed,
+        on_time=True,
+    )
 
 
 def get_valid_late_attestation(spec, state, slot=None, index=None, signed=False):
@@ -113,13 +145,24 @@ def get_valid_late_attestation(spec, state, slot=None, index=None, signed=False)
     return get_valid_attestation(spec, state, slot=slot, index=index, signed=signed, on_time=False)
 
 
-def get_valid_attestation(spec, state, slot=None, index=None, empty=False, signed=False, on_time=True):
+def get_valid_attestation(spec,
+                          state,
+                          slot=None,
+                          index=None,
+                          filter_participant_set=None,
+                          shard_transition=None,
+                          signed=False,
+                          on_time=True):
+    # If filter_participant_set filters everything, the attestation has 0 participants, and cannot be signed.
+    # Thus strictly speaking invalid when no participant is added later.
     if slot is None:
         slot = state.slot
     if index is None:
         index = 0
 
-    attestation_data = build_attestation_data(spec, state, slot, index)
+    attestation_data = build_attestation_data(
+        spec, state, slot=slot, index=index, shard_transition=shard_transition, on_time=on_time
+    )
 
     beacon_committee = spec.get_beacon_committee(
         state,
@@ -133,12 +176,10 @@ def get_valid_attestation(spec, state, slot=None, index=None, empty=False, signe
         aggregation_bits=aggregation_bits,
         data=attestation_data,
     )
-    if not empty:
-        fill_aggregate_attestation(spec, state, attestation)
-    if signed:
-        sign_attestation(spec, state, attestation)
+    # fill the attestation with (optionally filtered) participants, and optionally sign it
+    fill_aggregate_attestation(spec, state, attestation, signed=signed, filter_participant_set=filter_participant_set)
 
-    if spec.fork == 'phase1' and on_time:
+    if spec.fork == PHASE1 and on_time:
         attestation = convert_to_valid_on_time_attestation(spec, state, attestation, signed)
 
     return attestation
@@ -210,7 +251,7 @@ def get_attestation_custody_signature(spec, state, attestation_data, block_index
 
 
 def sign_attestation(spec, state, attestation):
-    if spec.fork == 'phase1' and any(attestation.custody_bits_blocks):
+    if spec.fork == PHASE1 and any(attestation.custody_bits_blocks):
         sign_on_time_attestation(spec, state, attestation)
         return
 
@@ -229,16 +270,25 @@ def get_attestation_signature(spec, state, attestation_data, privkey):
     return bls.Sign(privkey, signing_root)
 
 
-def fill_aggregate_attestation(spec, state, attestation, signed=False):
+def fill_aggregate_attestation(spec, state, attestation, signed=False, filter_participant_set=None):
+    """
+     `signed`: Signing is optional.
+     `filter_participant_set`: Optional, filters the full committee indices set (default) to a subset that participates
+    """
     beacon_committee = spec.get_beacon_committee(
         state,
         attestation.data.slot,
         attestation.data.index,
     )
+    # By default, have everyone participate
+    participants = set(beacon_committee)
+    # But optionally filter the participants to a smaller amount
+    if filter_participant_set is not None:
+        participants = filter_participant_set(participants)
     for i in range(len(beacon_committee)):
-        attestation.aggregation_bits[i] = True
+        attestation.aggregation_bits[i] = beacon_committee[i] in participants
 
-    if signed:
+    if signed and len(participants) > 0:
         sign_attestation(spec, state, attestation)
 
 
@@ -275,7 +325,82 @@ def next_epoch_with_attestations(spec,
                     spec, post_state, slot_to_attest, index=index, signed=True, on_time=False)
                 block.body.attestations.append(prev_attestation)
 
+        if spec.fork == PHASE1:
+            fill_block_shard_transitions_by_attestations(spec, post_state, block)
+
         signed_block = state_transition_and_sign_block(spec, post_state, block)
         signed_blocks.append(signed_block)
 
     return state, signed_blocks, post_state
+
+
+def prepare_state_with_attestations(spec, state, participation_fn=None):
+    """
+    Prepare state with attestations according to the ``participation_fn``.
+    If no ``participation_fn``, default to "full" -- max committee participation at each slot.
+
+    participation_fn: (slot, committee_index, committee_indices_set) -> participants_indices_set
+    """
+    # Go to start of next epoch to ensure can have full participation
+    next_epoch(spec, state)
+
+    start_slot = state.slot
+    start_epoch = spec.get_current_epoch(state)
+    next_epoch_start_slot = spec.compute_start_slot_at_epoch(start_epoch + 1)
+    attestations = []
+    for _ in range(spec.SLOTS_PER_EPOCH + spec.MIN_ATTESTATION_INCLUSION_DELAY):
+        # create an attestation for each index in each slot in epoch
+        if state.slot < next_epoch_start_slot:
+            for committee_index in range(spec.get_committee_count_at_slot(state, state.slot)):
+                def temp_participants_filter(comm):
+                    if participation_fn is None:
+                        return comm
+                    else:
+                        return participation_fn(state.slot, committee_index, comm)
+                attestation = get_valid_attestation(spec, state, index=committee_index,
+                                                    filter_participant_set=temp_participants_filter, signed=True)
+                if any(attestation.aggregation_bits):  # Only if there is at least 1 participant.
+                    attestations.append(attestation)
+        # fill each created slot in state after inclusion delay
+        if state.slot >= start_slot + spec.MIN_ATTESTATION_INCLUSION_DELAY:
+            inclusion_slot = state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY
+            include_attestations = [att for att in attestations if att.data.slot == inclusion_slot]
+            add_attestations_to_state(spec, state, include_attestations, state.slot)
+        next_slot(spec, state)
+
+    assert state.slot == next_epoch_start_slot + spec.MIN_ATTESTATION_INCLUSION_DELAY
+    assert len(state.previous_epoch_attestations) == len(attestations)
+
+    return attestations
+
+
+_prep_state_cache_dict = LRU(size=10)
+
+
+def cached_prepare_state_with_attestations(spec, state):
+    """
+    Cached version of prepare_state_with_attestations,
+    but does not return anything, and does not support a participation fn argument
+    """
+    # If the pre-state is not already known in the LRU, then take it,
+    # prepare it with attestations, and put it in the LRU.
+    # The input state is likely already cached, so the hash-tree-root does not affect speed.
+    key = (spec.fork, state.hash_tree_root())
+    global _prep_state_cache_dict
+    if key not in _prep_state_cache_dict:
+        prepare_state_with_attestations(spec, state)
+        _prep_state_cache_dict[key] = state.get_backing()  # cache the tree structure, not the view wrapping it.
+
+    # Put the LRU cache result into the state view, as if we transitioned the original view
+    state.set_backing(_prep_state_cache_dict[key])
+
+
+def fill_block_shard_transitions_by_attestations(spec, state, block):
+    block.body.shard_transitions = [spec.ShardTransition()] * spec.MAX_SHARDS
+    for attestation in block.body.attestations:
+        shard = spec.get_shard(state, attestation)
+        if attestation.data.slot == state.slot:
+            temp_state = state.copy()
+            transition_to(spec, temp_state, slot=block.slot)
+            shard_transition = spec.get_shard_transition(temp_state, shard, [])
+            block.body.shard_transitions[shard] = shard_transition
diff --git a/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py
--- a/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py
@@ -1,5 +1,5 @@
 from eth2spec.test.context import PHASE1
-from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation
+from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation, sign_indexed_attestation
 
 
 def get_valid_attester_slashing(spec, state, signed_1=False, signed_2=False):
@@ -17,6 +17,26 @@ def get_valid_attester_slashing(spec, state, signed_1=False, signed_2=False):
     )
 
 
+def get_valid_attester_slashing_by_indices(spec, state, indices_1, indices_2=None, signed_1=False, signed_2=False):
+    if indices_2 is None:
+        indices_2 = indices_1
+
+    assert indices_1 == sorted(indices_1)
+    assert indices_2 == sorted(indices_2)
+
+    attester_slashing = get_valid_attester_slashing(spec, state)
+
+    attester_slashing.attestation_1.attesting_indices = indices_1
+    attester_slashing.attestation_2.attesting_indices = indices_2
+
+    if signed_1:
+        sign_indexed_attestation(spec, state, attester_slashing.attestation_1)
+    if signed_2:
+        sign_indexed_attestation(spec, state, attester_slashing.attestation_2)
+
+    return attester_slashing
+
+
 def get_indexed_attestation_participants(spec, indexed_att):
     """
     Wrapper around index-attestation to return the list of participant indices, regardless of spec phase.
diff --git a/tests/core/pyspec/eth2spec/test/helpers/block.py b/tests/core/pyspec/eth2spec/test/helpers/block.py
--- a/tests/core/pyspec/eth2spec/test/helpers/block.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/block.py
@@ -83,17 +83,30 @@ def build_empty_block(spec, state, slot=None):
         state = state.copy()
         spec.process_slots(state, slot)
 
+    state, parent_block_root = get_state_and_beacon_parent_root_at_slot(spec, state, slot)
     empty_block = spec.BeaconBlock()
     empty_block.slot = slot
     empty_block.proposer_index = spec.get_beacon_proposer_index(state)
     empty_block.body.eth1_data.deposit_count = state.eth1_deposit_index
-    previous_block_header = state.latest_block_header.copy()
-    if previous_block_header.state_root == spec.Root():
-        previous_block_header.state_root = hash_tree_root(state)
-    empty_block.parent_root = hash_tree_root(previous_block_header)
+    empty_block.parent_root = parent_block_root
     apply_randao_reveal(spec, state, empty_block)
     return empty_block
 
 
 def build_empty_block_for_next_slot(spec, state):
     return build_empty_block(spec, state, state.slot + 1)
+
+
+def get_state_and_beacon_parent_root_at_slot(spec, state, slot):
+    if slot < state.slot:
+        raise Exception("Cannot build blocks for past slots")
+    if slot > state.slot:
+        # transition forward in copied state to grab relevant data from state
+        state = state.copy()
+        spec.process_slots(state, slot)
+
+    previous_block_header = state.latest_block_header.copy()
+    if previous_block_header.state_root == spec.Root():
+        previous_block_header.state_root = hash_tree_root(state)
+    beacon_parent_root = hash_tree_root(previous_block_header)
+    return state, beacon_parent_root
diff --git a/tests/core/pyspec/eth2spec/test/helpers/crosslinks.py b/tests/core/pyspec/eth2spec/test/helpers/crosslinks.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/helpers/crosslinks.py
@@ -0,0 +1,28 @@
+from eth2spec.test.context import expect_assertion_error
+
+
+def run_crosslinks_processing(spec, state, shard_transitions, attestations, valid=True):
+    """
+    Run ``process_attestation``, yielding:
+      - pre-state ('pre')
+      - shard_transitions ('shard_transitions')
+      - attestations ('attestations')
+      - post-state ('post').
+    If ``valid == False``, run expecting ``AssertionError``
+    """
+    # yield pre-state
+    yield 'pre', state
+    yield 'shard_transitions', shard_transitions
+    yield 'attestations', attestations
+
+    # If the attestation is invalid, processing is aborted, and there is no post-state.
+    if not valid:
+        expect_assertion_error(lambda: spec.process_crosslinks(state, shard_transitions, attestations))
+        yield 'post', None
+        return
+
+    # process crosslinks
+    spec.process_crosslinks(state, shard_transitions, attestations)
+
+    # yield post-state
+    yield 'post', state
diff --git a/tests/core/pyspec/eth2spec/test/helpers/deposits.py b/tests/core/pyspec/eth2spec/test/helpers/deposits.py
--- a/tests/core/pyspec/eth2spec/test/helpers/deposits.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/deposits.py
@@ -5,6 +5,17 @@
 from eth2spec.utils.ssz.ssz_typing import List
 
 
+def mock_deposit(spec, state, index):
+    """
+    Mock validator at ``index`` as having just made a deposit
+    """
+    assert spec.is_active_validator(state.validators[index], spec.get_current_epoch(state))
+    state.validators[index].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH
+    state.validators[index].activation_epoch = spec.FAR_FUTURE_EPOCH
+    state.validators[index].effective_balance = spec.MAX_EFFECTIVE_BALANCE
+    assert not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state))
+
+
 def build_deposit_data(spec, pubkey, privkey, amount, withdrawal_credentials, signed=False):
     deposit_data = spec.DepositData(
         pubkey=pubkey,
diff --git a/tests/core/pyspec/eth2spec/test/helpers/keys.py b/tests/core/pyspec/eth2spec/test/helpers/keys.py
--- a/tests/core/pyspec/eth2spec/test/helpers/keys.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/keys.py
@@ -2,5 +2,5 @@
 from eth2spec.phase0 import spec
 
 privkeys = [i + 1 for i in range(spec.SLOTS_PER_EPOCH * 256)]
-pubkeys = [bls.PrivToPub(privkey) for privkey in privkeys]
+pubkeys = [bls.SkToPk(privkey) for privkey in privkeys]
 pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)}
diff --git a/tests/core/pyspec/eth2spec/test/helpers/phase1/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/phase1/attestations.py
deleted file mode 100644
--- a/tests/core/pyspec/eth2spec/test/helpers/phase1/attestations.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from eth2spec.utils.ssz.ssz_typing import Bitlist
-from eth2spec.utils import bls
-
-from eth2spec.test.helpers.keys import privkeys
-import eth2spec.test.helpers.attestations as phase0_attestations
-
-
-def get_valid_on_time_attestation(spec, state, index=None, signed=False):
-    '''
-    Construct on-time attestation for next slot
-    '''
-    if index is None:
-        index = 0
-
-    attestation = phase0_attestations.get_valid_attestation(spec, state, state.slot, index, False)
-    shard = spec.get_shard(state, attestation)
-    offset_slots = spec.compute_offset_slots(spec.get_latest_slot_for_shard(state, shard), state.slot + 1)
-
-    for _ in offset_slots:
-        attestation.custody_bits_blocks.append(
-            Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE]([0 for _ in attestation.aggregation_bits])
-        )
-
-    if signed:
-        sign_attestation(spec, state, attestation)
-
-    return attestation
-
-
-def sign_attestation(spec, state, attestation):
-    if not any(attestation.custody_bits_blocks):
-        phase0_attestations.sign_attestation(spec, state, attestation)
-        return
-
-    committee = spec.get_beacon_committee(state, attestation.data.slot, attestation.data.index)
-    signatures = []
-    for block_index, custody_bits in enumerate(attestation.custody_bits_blocks):
-        for participant, abit, cbit in zip(committee, attestation.aggregation_bits, custody_bits):
-            if not abit:
-                continue
-            signatures.append(get_attestation_custody_signature(
-                spec,
-                state,
-                attestation.data,
-                block_index,
-                cbit,
-                privkeys[participant]
-            ))
-
-    attestation.signature = bls.Aggregate(signatures)
-
-
-def get_attestation_custody_signature(spec, state, attestation_data, block_index, bit, privkey):
-    domain = spec.get_domain(state, spec.DOMAIN_BEACON_ATTESTER, attestation_data.target.epoch)
-    signing_root = spec.compute_signing_root(
-        spec.AttestationCustodyBitWrapper(
-            attestation_data.hash_tree_root(),
-            block_index,
-            bit,
-        ),
-        domain,
-    )
-    return bls.Sign(privkey, signing_root)
diff --git a/tests/core/pyspec/eth2spec/test/helpers/phase1/shard_block.py b/tests/core/pyspec/eth2spec/test/helpers/phase1/shard_block.py
deleted file mode 100644
--- a/tests/core/pyspec/eth2spec/test/helpers/phase1/shard_block.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from eth2spec.test.helpers.keys import privkeys
-from eth2spec.utils import bls
-from eth2spec.utils.bls import only_with_bls
-from eth2spec.utils.ssz.ssz_impl import (
-    hash_tree_root,
-)
-
-from .attestations import (
-    sign_shard_attestation,
-)
-
-
-@only_with_bls()
-def sign_shard_block(spec, beacon_state, shard_state, block, proposer_index=None):
-    if proposer_index is None:
-        proposer_index = spec.get_shard_proposer_index(beacon_state, shard_state.shard, block.slot)
-
-    privkey = privkeys[proposer_index]
-    domain = spec.get_domain(beacon_state, spec.DOMAIN_SHARD_PROPOSER, spec.compute_epoch_of_shard_slot(block.slot))
-    signing_root = spec.compute_signing_root(block, domain)
-    block.signature = bls.Sign(privkey, signing_root)
-
-
-def build_empty_shard_block(spec,
-                            beacon_state,
-                            shard_state,
-                            slot,
-                            signed=False,
-                            full_attestation=False):
-    if slot is None:
-        slot = shard_state.slot
-
-    previous_beacon_header = beacon_state.latest_block_header.copy()
-    if previous_beacon_header.state_root == spec.Bytes32():
-        previous_beacon_header.state_root = beacon_state.hash_tree_root()
-    beacon_block_root = hash_tree_root(previous_beacon_header)
-
-    previous_block_header = shard_state.latest_block_header.copy()
-    if previous_block_header.state_root == spec.Bytes32():
-        previous_block_header.state_root = shard_state.hash_tree_root()
-    parent_root = hash_tree_root(previous_block_header)
-
-    block = spec.ShardBlock(
-        shard=shard_state.shard,
-        slot=slot,
-        beacon_block_root=beacon_block_root,
-        parent_root=parent_root,
-        block_size_sum=shard_state.block_size_sum + spec.SHARD_HEADER_SIZE,
-    )
-
-    if full_attestation:
-        shard_committee = spec.get_shard_committee(beacon_state, shard_state.shard, block.slot)
-        block.aggregation_bits = list(
-            (True,) * len(shard_committee) +
-            (False,) * (spec.MAX_PERIOD_COMMITTEE_SIZE * 2 - len(shard_committee))
-        )
-    else:
-        shard_committee = []
-
-    block.attestations = sign_shard_attestation(
-        spec,
-        beacon_state,
-        shard_state,
-        block,
-        participants=shard_committee,
-    )
-
-    if signed:
-        sign_shard_block(spec, beacon_state, shard_state, block)
-
-    return block
diff --git a/tests/core/pyspec/eth2spec/test/helpers/phase1/shard_state.py b/tests/core/pyspec/eth2spec/test/helpers/phase1/shard_state.py
deleted file mode 100644
--- a/tests/core/pyspec/eth2spec/test/helpers/phase1/shard_state.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from eth2spec.test.helpers.phase1.shard_block import sign_shard_block
-
-
-def configure_shard_state(spec, beacon_state, shard=0):
-    beacon_state.slot = spec.Slot(spec.SHARD_GENESIS_EPOCH * spec.SLOTS_PER_EPOCH)
-    shard_state = spec.get_genesis_shard_state(spec.Shard(shard))
-    shard_state.slot = spec.ShardSlot(spec.SHARD_GENESIS_EPOCH * spec.SHARD_SLOTS_PER_EPOCH)
-    return beacon_state, shard_state
-
-
-def shard_state_transition_and_sign_block(spec, beacon_state, shard_state, block):
-    """
-    Shard state transition via the provided ``block``
-    then package the block with the state root and signature.
-    """
-    spec.shard_state_transition(beacon_state, shard_state, block)
-    block.state_root = shard_state.hash_tree_root()
-    sign_shard_block(spec, beacon_state, shard_state, block)
diff --git a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py
--- a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py
@@ -1,22 +1,55 @@
 from eth2spec.test.helpers.block_header import sign_block_header
 from eth2spec.test.helpers.keys import pubkey_to_privkey
+from eth2spec.test.helpers.state import get_balance
 
 
-def get_valid_proposer_slashing(spec, state, signed_1=False, signed_2=False):
-    current_epoch = spec.get_current_epoch(state)
-    validator_index = spec.get_active_validator_indices(state, current_epoch)[-1]
-    privkey = pubkey_to_privkey[state.validators[validator_index].pubkey]
+def check_proposer_slashing_effect(spec, pre_state, state, slashed_index):
+    slashed_validator = state.validators[slashed_index]
+    assert slashed_validator.slashed
+    assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
+    assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
+
+    proposer_index = spec.get_beacon_proposer_index(state)
+    slash_penalty = state.validators[slashed_index].effective_balance // spec.MIN_SLASHING_PENALTY_QUOTIENT
+    whistleblower_reward = state.validators[slashed_index].effective_balance // spec.WHISTLEBLOWER_REWARD_QUOTIENT
+    if proposer_index != slashed_index:
+        # slashed validator lost initial slash penalty
+        assert (
+            get_balance(state, slashed_index)
+            == get_balance(pre_state, slashed_index) - slash_penalty
+        )
+        # block proposer gained whistleblower reward
+        # >= because proposer could have reported multiple
+        assert (
+            get_balance(state, proposer_index)
+            >= get_balance(pre_state, proposer_index) + whistleblower_reward
+        )
+    else:
+        # proposer reported themself so get penalty and reward
+        # >= because proposer could have reported multiple
+        assert (
+            get_balance(state, slashed_index)
+            >= get_balance(pre_state, slashed_index) - slash_penalty + whistleblower_reward
+        )
+
+
+def get_valid_proposer_slashing(spec, state, random_root=b'\x99' * 32,
+                                slashed_index=None, signed_1=False, signed_2=False):
+    if slashed_index is None:
+        current_epoch = spec.get_current_epoch(state)
+        slashed_index = spec.get_active_validator_indices(state, current_epoch)[-1]
+    privkey = pubkey_to_privkey[state.validators[slashed_index].pubkey]
     slot = state.slot
 
     header_1 = spec.BeaconBlockHeader(
         slot=slot,
-        proposer_index=validator_index,
+        proposer_index=slashed_index,
         parent_root=b'\x33' * 32,
         state_root=b'\x44' * 32,
         body_root=b'\x55' * 32,
     )
     header_2 = header_1.copy()
-    header_2.parent_root = b'\x99' * 32
+    header_2.parent_root = random_root
 
     if signed_1:
         signed_header_1 = sign_block_header(spec, state, header_1, privkey)
diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py
@@ -0,0 +1,457 @@
+from random import Random
+from lru import LRU
+
+from eth2spec.phase0 import spec as spec_phase0
+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.utils.ssz.ssz_typing import Container, uint64, List
+
+
+class Deltas(Container):
+    rewards: List[uint64, spec_phase0.VALIDATOR_REGISTRY_LIMIT]
+    penalties: List[uint64, spec_phase0.VALIDATOR_REGISTRY_LIMIT]
+
+
+def has_enough_for_reward(spec, state, index):
+    """
+    Check if base_reward will be non-zero.
+
+    At very low balances, it is possible for a validator have a positive effective_balance
+    but a zero base reward.
+    """
+    return (
+        state.validators[index].effective_balance * spec.BASE_REWARD_FACTOR
+        > spec.integer_squareroot(spec.get_total_active_balance(state)) // spec.BASE_REWARDS_PER_EPOCH
+    )
+
+
+def run_deltas(spec, state):
+    """
+    Run all deltas functions yielding:
+      - pre-state ('pre')
+      - source deltas ('source_deltas')
+      - target deltas ('target_deltas')
+      - head deltas ('head_deltas')
+      - inclusion delay deltas ('inclusion_delay_deltas')
+      - inactivity penalty deltas ('inactivity_penalty_deltas')
+    """
+    yield 'pre', state
+    yield from run_attestation_component_deltas(
+        spec,
+        state,
+        spec.get_source_deltas,
+        spec.get_matching_source_attestations,
+        'source_deltas',
+    )
+    yield from run_attestation_component_deltas(
+        spec,
+        state,
+        spec.get_target_deltas,
+        spec.get_matching_target_attestations,
+        'target_deltas',
+    )
+    yield from run_attestation_component_deltas(
+        spec,
+        state,
+        spec.get_head_deltas,
+        spec.get_matching_head_attestations,
+        'head_deltas',
+    )
+    yield from run_get_inclusion_delay_deltas(spec, state)
+    yield from run_get_inactivity_penalty_deltas(spec, state)
+
+
+def run_attestation_component_deltas(spec, state, component_delta_fn, matching_att_fn, deltas_name):
+    """
+    Run ``component_delta_fn``, yielding:
+      - deltas ('{``deltas_name``}')
+    """
+    rewards, penalties = component_delta_fn(state)
+
+    yield deltas_name, Deltas(rewards=rewards, penalties=penalties)
+
+    matching_attestations = matching_att_fn(state, spec.get_previous_epoch(state))
+    matching_indices = spec.get_unslashed_attesting_indices(state, matching_attestations)
+    eligible_indices = spec.get_eligible_validator_indices(state)
+    for index in range(len(state.validators)):
+        if index not in eligible_indices:
+            assert rewards[index] == 0
+            assert penalties[index] == 0
+            continue
+
+        validator = state.validators[index]
+        enough_for_reward = has_enough_for_reward(spec, state, index)
+        if index in matching_indices and not validator.slashed:
+            if enough_for_reward:
+                assert rewards[index] > 0
+            else:
+                assert rewards[index] == 0
+            assert penalties[index] == 0
+        else:
+            assert rewards[index] == 0
+            if enough_for_reward:
+                assert penalties[index] > 0
+            else:
+                assert penalties[index] == 0
+
+
+def run_get_inclusion_delay_deltas(spec, state):
+    """
+    Run ``get_inclusion_delay_deltas``, yielding:
+      - inclusion delay deltas ('inclusion_delay_deltas')
+    """
+    rewards, penalties = spec.get_inclusion_delay_deltas(state)
+
+    yield 'inclusion_delay_deltas', Deltas(rewards=rewards, penalties=penalties)
+
+    eligible_attestations = spec.get_matching_source_attestations(state, spec.get_previous_epoch(state))
+    attesting_indices = spec.get_unslashed_attesting_indices(state, eligible_attestations)
+
+    rewarded_indices = set()
+    rewarded_proposer_indices = set()
+    # Ensure attesters with enough balance are rewarded for attestations
+    # Track those that are rewarded and track proposers that should be rewarded
+    for index in range(len(state.validators)):
+        if index in attesting_indices and has_enough_for_reward(spec, state, index):
+            assert rewards[index] > 0
+            rewarded_indices.add(index)
+
+            # Track proposer of earliest included attestation for the validator defined by index
+            earliest_attestation = min([
+                a for a in eligible_attestations
+                if index in spec.get_attesting_indices(state, a.data, a.aggregation_bits)
+            ], key=lambda a: a.inclusion_delay)
+            rewarded_proposer_indices.add(earliest_attestation.proposer_index)
+
+    # Ensure all expected proposers have been rewarded
+    # Track rewarde indices
+    proposing_indices = [a.proposer_index for a in eligible_attestations]
+    for index in proposing_indices:
+        if index in rewarded_proposer_indices:
+            assert rewards[index] > 0
+            rewarded_indices.add(index)
+
+    # Ensure all expected non-rewarded indices received no reward
+    for index in range(len(state.validators)):
+        assert penalties[index] == 0
+        if index not in rewarded_indices:
+            assert rewards[index] == 0
+
+
+def run_get_inactivity_penalty_deltas(spec, state):
+    """
+    Run ``get_inactivity_penalty_deltas``, yielding:
+      - inactivity penalty deltas ('inactivity_penalty_deltas')
+    """
+    rewards, penalties = spec.get_inactivity_penalty_deltas(state)
+
+    yield 'inactivity_penalty_deltas', Deltas(rewards=rewards, penalties=penalties)
+
+    matching_attestations = spec.get_matching_target_attestations(state, spec.get_previous_epoch(state))
+    matching_attesting_indices = spec.get_unslashed_attesting_indices(state, matching_attestations)
+
+    eligible_indices = spec.get_eligible_validator_indices(state)
+    for index in range(len(state.validators)):
+        assert rewards[index] == 0
+        if index not in eligible_indices:
+            assert penalties[index] == 0
+            continue
+
+        if spec.is_in_inactivity_leak(state):
+            base_reward = spec.get_base_reward(state, index)
+            base_penalty = spec.BASE_REWARDS_PER_EPOCH * base_reward - spec.get_proposer_reward(state, index)
+            if not has_enough_for_reward(spec, state, index):
+                assert penalties[index] == 0
+            elif index in matching_attesting_indices:
+                assert penalties[index] == base_penalty
+            else:
+                assert penalties[index] > base_penalty
+        else:
+            assert penalties[index] == 0
+
+
+def transition_state_to_leak(spec, state, epochs=None):
+    if epochs is None:
+        epochs = spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY
+    assert epochs >= spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY
+
+    for _ in range(epochs):
+        next_epoch(spec, state)
+
+
+_cache_dict = LRU(size=10)
+
+
+def leaking(epochs=None):
+
+    def deco(fn):
+        def entry(*args, spec, state, **kw):
+            # If the pre-state is not already known in the LRU, then take it,
+            # transition it to leak, and put it in the LRU.
+            # The input state is likely already cached, so the hash-tree-root does not affect speed.
+            key = (state.hash_tree_root(), spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY, spec.SLOTS_PER_EPOCH, epochs)
+            global _cache_dict
+            if key not in _cache_dict:
+                transition_state_to_leak(spec, state, epochs=epochs)
+                _cache_dict[key] = state.get_backing()  # cache the tree structure, not the view wrapping it.
+
+            # Take an entry out of the LRU.
+            # No copy is necessary, as we wrap the immutable backing with a new view.
+            state = spec.BeaconState(backing=_cache_dict[key])
+            return fn(*args, spec=spec, state=state, **kw)
+        return entry
+    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
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_full_all_correct(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_full_but_partial_participation(spec, state, rng=Random(5522)):
+    cached_prepare_state_with_attestations(spec, state)
+
+    for a in state.previous_epoch_attestations:
+        a.aggregation_bits = [rng.choice([True, False]) for _ in a.aggregation_bits]
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_partial(spec, state, fraction_filled):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Remove portion of attestations
+    num_attestations = int(len(state.previous_epoch_attestations) * fraction_filled)
+    state.previous_epoch_attestations = state.previous_epoch_attestations[:num_attestations]
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_half_full(spec, state):
+    yield from run_test_partial(spec, state, 0.5)
+
+
+def run_test_one_attestation_one_correct(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Remove all attestations except for the first one
+    state.previous_epoch_attestations = state.previous_epoch_attestations[:1]
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_with_not_yet_activated_validators(spec, state, rng=Random(5555)):
+    set_some_new_deposits(spec, state, rng)
+    cached_prepare_state_with_attestations(spec, state)
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_with_exited_validators(spec, state, rng=Random(1337)):
+    exit_random_validators(spec, state, rng)
+    cached_prepare_state_with_attestations(spec, state)
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_with_slashed_validators(spec, state, rng=Random(3322)):
+    exit_random_validators(spec, state, rng)
+    slash_random_validators(spec, state, rng)
+
+    cached_prepare_state_with_attestations(spec, state)
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_some_very_low_effective_balances_that_attested(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Set some balances to be very low (including 0)
+    assert len(state.validators) >= 5
+    for i, index in enumerate(range(5)):
+        state.validators[index].effective_balance = i
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_some_very_low_effective_balances_that_did_not_attest(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Remove attestation
+    attestation = state.previous_epoch_attestations[0]
+    state.previous_epoch_attestations = state.previous_epoch_attestations[1:]
+    # Set removed indices effective balance to very low amount
+    indices = spec.get_unslashed_attesting_indices(state, [attestation])
+    for i, index in enumerate(indices):
+        state.validators[index].effective_balance = i
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_full_fraction_incorrect(spec, state, correct_target, correct_head, fraction_incorrect):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Make fraction_incorrect of pending attestations have bad target/head as specified
+    num_incorrect = int(fraction_incorrect * len(state.previous_epoch_attestations))
+    for pending_attestation in state.previous_epoch_attestations[:num_incorrect]:
+        if not correct_target:
+            pending_attestation.data.target.root = b'\x55' * 32
+        if not correct_head:
+            pending_attestation.data.beacon_block_root = b'\x66' * 32
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_full_delay_one_slot(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+    for a in state.previous_epoch_attestations:
+        a.inclusion_delay += 1
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_full_delay_max_slots(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+    for a in state.previous_epoch_attestations:
+        a.inclusion_delay += spec.SLOTS_PER_EPOCH
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_full_mixed_delay(spec, state, rng=Random(1234)):
+    cached_prepare_state_with_attestations(spec, state)
+    for a in state.previous_epoch_attestations:
+        a.inclusion_delay = rng.randint(1, spec.SLOTS_PER_EPOCH)
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_proposer_not_in_attestations(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Get an attestation where the proposer is not in the committee
+    non_proposer_attestations = []
+    for a in state.previous_epoch_attestations:
+        if a.proposer_index not in spec.get_unslashed_attesting_indices(state, [a]):
+            non_proposer_attestations.append(a)
+
+    assert any(non_proposer_attestations)
+    state.previous_epoch_attestations = non_proposer_attestations
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_duplicate_attestations_at_later_slots(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    # Remove 2/3 of attestations to make it more interesting
+    num_attestations = int(len(state.previous_epoch_attestations) * 0.33)
+    state.previous_epoch_attestations = state.previous_epoch_attestations[:num_attestations]
+
+    # Get map of the proposer at each slot to make valid-looking duplicate attestations
+    per_slot_proposers = {
+        (a.data.slot + a.inclusion_delay): a.proposer_index
+        for a in state.previous_epoch_attestations
+    }
+    max_slot = max([a.data.slot + a.inclusion_delay for a in state.previous_epoch_attestations])
+    later_attestations = []
+    for a in state.previous_epoch_attestations:
+        # Only have proposers for previous epoch so do not create later
+        # duplicate if slot exceeds the max slot in previous_epoch_attestations
+        if a.data.slot + a.inclusion_delay >= max_slot:
+            continue
+        later_a = a.copy()
+        later_a.inclusion_delay += 1
+        later_a.proposer_index = per_slot_proposers[later_a.data.slot + later_a.inclusion_delay]
+        later_attestations.append(later_a)
+
+    assert any(later_attestations)
+
+    state.previous_epoch_attestations = sorted(
+        state.previous_epoch_attestations + later_attestations,
+        key=lambda a: a.data.slot + a.inclusion_delay
+    )
+
+    yield from run_deltas(spec, state)
+
+
+def run_test_all_balances_too_low_for_reward(spec, state):
+    cached_prepare_state_with_attestations(spec, state)
+
+    for index in range(len(state.validators)):
+        state.validators[index].effective_balance = 10
+
+    yield from run_deltas(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)
+
+    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)
+
+    yield from run_deltas(spec, state)
diff --git a/tests/core/pyspec/eth2spec/test/helpers/shard_block.py b/tests/core/pyspec/eth2spec/test/helpers/shard_block.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/helpers/shard_block.py
@@ -0,0 +1,85 @@
+from eth2spec.test.helpers.attestations import get_valid_on_time_attestation
+from eth2spec.test.helpers.block import get_state_and_beacon_parent_root_at_slot
+from eth2spec.test.helpers.state import transition_to
+from eth2spec.test.helpers.keys import privkeys
+from eth2spec.utils import bls
+from eth2spec.utils.bls import only_with_bls
+
+
+@only_with_bls()
+def sign_shard_block(spec, beacon_state, shard, block, proposer_index=None):
+    slot = block.message.slot
+    if proposer_index is None:
+        proposer_index = spec.get_shard_proposer_index(beacon_state, slot, shard)
+
+    privkey = privkeys[proposer_index]
+    domain = spec.get_domain(beacon_state, spec.DOMAIN_SHARD_PROPOSAL, spec.compute_epoch_at_slot(slot))
+    signing_root = spec.compute_signing_root(block.message, domain)
+    block.signature = bls.Sign(privkey, signing_root)
+
+
+def build_shard_block(spec,
+                      beacon_state,
+                      shard,
+                      slot=None,
+                      body=None,
+                      signed=False):
+    shard_state = beacon_state.shard_states[shard]
+    if slot is None:
+        slot = shard_state.slot + 1
+
+    if body is None:
+        body = b'\x56' * 128
+
+    proposer_index = spec.get_shard_proposer_index(beacon_state, slot, shard)
+    beacon_state, beacon_parent_root = get_state_and_beacon_parent_root_at_slot(spec, beacon_state, slot)
+
+    block = spec.ShardBlock(
+        shard_parent_root=shard_state.latest_block_root,
+        beacon_parent_root=beacon_parent_root,
+        slot=slot,
+        proposer_index=proposer_index,
+        body=body,
+    )
+    signed_block = spec.SignedShardBlock(
+        message=block,
+    )
+
+    if signed:
+        sign_shard_block(spec, beacon_state, shard, signed_block, proposer_index=proposer_index)
+
+    return signed_block
+
+
+def build_shard_transitions_till_slot(spec, state, shard_blocks, on_time_slot):
+    temp_state = state.copy()
+    transition_to(spec, temp_state, on_time_slot)
+    shard_transitions = [spec.ShardTransition()] * spec.MAX_SHARDS
+    for shard, blocks in shard_blocks.items():
+        offset_slots = spec.get_offset_slots(temp_state, shard)
+        len_offset_slots = len(offset_slots)
+        assert len_offset_slots == on_time_slot - state.shard_states[shard].slot - 1
+        shard_transition = spec.get_shard_transition(temp_state, shard, blocks)
+        if len(blocks) > 0:
+            shard_block_root = blocks[-1].message.hash_tree_root()
+            assert shard_transition.shard_states[len_offset_slots - 1].latest_block_root == shard_block_root
+            assert shard_transition.shard_states[len_offset_slots - 1].slot == offset_slots[-1]
+        shard_transitions[shard] = shard_transition
+
+    return shard_transitions
+
+
+def build_attestation_with_shard_transition(spec, state, index, on_time_slot, shard_transition=None):
+    temp_state = state.copy()
+    transition_to(spec, temp_state, on_time_slot - 1)
+    attestation = get_valid_on_time_attestation(
+        spec,
+        temp_state,
+        index=index,
+        shard_transition=shard_transition,
+        signed=True,
+    )
+    assert attestation.data.slot == temp_state.slot
+    if shard_transition is not None:
+        assert attestation.data.shard_transition_root == shard_transition.hash_tree_root()
+    return attestation
diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py
--- a/tests/core/pyspec/eth2spec/test/helpers/state.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/state.py
@@ -40,6 +40,16 @@ def transition_to_slot_via_block(spec, state, slot):
     assert state.slot == slot
 
 
+def transition_to_valid_shard_slot(spec, state):
+    """
+    Transition to slot `spec.PHASE_1_GENESIS_SLOT + 1` and fork at `spec.PHASE_1_GENESIS_SLOT`.
+    """
+    transition_to(spec, state, spec.PHASE_1_GENESIS_SLOT)
+    state = spec.upgrade_to_phase1(state)  # `upgrade_to_phase1` is a pure function
+    next_slot(spec, state)
+    return state
+
+
 def next_epoch(spec, state):
     """
     Transition to the start slot of the next epoch
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py
--- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attestation.py
@@ -13,7 +13,6 @@
     sign_attestation,
 )
 from eth2spec.test.helpers.state import (
-    next_slot,
     next_slots,
     next_epoch_via_block,
     transition_to_slot_via_block,
@@ -61,6 +60,29 @@ def test_invalid_attestation_signature(spec, state):
     yield from run_attestation_processing(spec, state, attestation, False)
 
 
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_empty_participants_zeroes_sig(spec, state):
+    attestation = get_valid_attestation(spec, state, filter_participant_set=lambda comm: [])  # 0 participants
+    attestation.signature = spec.BLSSignature(b'\x00' * 96)
+    next_slots(spec, state, spec.MIN_ATTESTATION_INCLUSION_DELAY)
+
+    yield from run_attestation_processing(spec, state, attestation, False)
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_empty_participants_seemingly_valid_sig(spec, state):
+    attestation = get_valid_attestation(spec, state, filter_participant_set=lambda comm: [])  # 0 participants
+    # Special BLS value, valid for zero pubkeys on some (but not all) BLS implementations.
+    attestation.signature = spec.BLSSignature(b'\xc0' + b'\x00' * 95)
+    next_slots(spec, state, spec.MIN_ATTESTATION_INCLUSION_DELAY)
+
+    yield from run_attestation_processing(spec, state, attestation, False)
+
+
 @with_all_phases
 @spec_state_test
 def test_before_inclusion_delay(spec, state):
@@ -256,19 +278,6 @@ def test_bad_source_root(spec, state):
     yield from run_attestation_processing(spec, state, attestation, False)
 
 
-@with_all_phases
-@spec_state_test
-def test_empty_aggregation_bits(spec, state):
-    next_slot(spec, state)
-    attestation = get_valid_attestation(spec, state, empty=True)
-    next_slots(spec, state, spec.MIN_ATTESTATION_INCLUSION_DELAY)
-
-    assert attestation.aggregation_bits == Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE](
-        *([0b0] * len(attestation.aggregation_bits)))
-
-    yield from run_attestation_processing(spec, state, attestation)
-
-
 @with_all_phases
 @spec_state_test
 def test_too_many_aggregation_bits(spec, state):
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py
--- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_attester_slashing.py
@@ -1,6 +1,6 @@
 from eth2spec.test.context import (
     PHASE0, PHASE1,
-    spec_state_test, expect_assertion_error, always_bls, with_all_phases, with_phases
+    spec_state_test, expect_assertion_error, always_bls, never_bls, with_all_phases, with_phases
 )
 from eth2spec.test.helpers.attestations import sign_indexed_attestation
 from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing, \
@@ -89,6 +89,7 @@ def test_success_double(spec, state):
 
 @with_all_phases
 @spec_state_test
+@never_bls
 def test_success_surround(spec, state):
     next_epoch_via_block(spec, state)
 
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py
--- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py
@@ -1,8 +1,9 @@
 from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls, with_all_phases
+from eth2spec.test.helpers.block import build_empty_block_for_next_slot
 from eth2spec.test.helpers.block_header import sign_block_header
 from eth2spec.test.helpers.keys import privkeys
-from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing
-from eth2spec.test.helpers.state import get_balance, next_epoch
+from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect
+from eth2spec.test.helpers.state import next_epoch
 
 
 def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True):
@@ -14,6 +15,8 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True)
     If ``valid == False``, run expecting ``AssertionError``
     """
 
+    pre_state = state.copy()
+
     yield 'pre', state
     yield 'proposer_slashing', proposer_slashing
 
@@ -22,25 +25,31 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True)
         yield 'post', None
         return
 
-    proposer_index = proposer_slashing.signed_header_1.message.proposer_index
-    pre_proposer_balance = get_balance(state, proposer_index)
-
     spec.process_proposer_slashing(state, proposer_slashing)
     yield 'post', state
 
-    # check if slashed
-    slashed_validator = state.validators[proposer_index]
-    assert slashed_validator.slashed
-    assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
-    assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
-
-    # lost whistleblower reward
-    assert get_balance(state, proposer_index) < pre_proposer_balance
+    slashed_proposer_index = proposer_slashing.signed_header_1.message.proposer_index
+    check_proposer_slashing_effect(spec, pre_state, state, slashed_proposer_index)
 
 
 @with_all_phases
 @spec_state_test
 def test_success(spec, state):
+    # Get proposer for next slot
+    block = build_empty_block_for_next_slot(spec, state)
+    proposer_index = block.proposer_index
+
+    # Create slashing for same proposer
+    proposer_slashing = get_valid_proposer_slashing(spec, state,
+                                                    slashed_index=proposer_index,
+                                                    signed_1=True, signed_2=True)
+
+    yield from run_proposer_slashing_processing(spec, state, proposer_slashing)
+
+
+@with_all_phases
+@spec_state_test
+def test_success_slashed_and_proposer_index_the_same(spec, state):
     proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True)
 
     yield from run_proposer_slashing_processing(spec, state, proposer_slashing)
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_voluntary_exit.py
--- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_voluntary_exit.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_voluntary_exit.py
@@ -34,8 +34,8 @@ def run_voluntary_exit_processing(spec, state, signed_voluntary_exit, valid=True
 @with_all_phases
 @spec_state_test
 def test_success(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
     validator_index = spec.get_active_validator_indices(state, current_epoch)[0]
@@ -53,8 +53,8 @@ def test_success(spec, state):
 @spec_state_test
 @always_bls
 def test_invalid_signature(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
     validator_index = spec.get_active_validator_indices(state, current_epoch)[0]
@@ -71,8 +71,8 @@ def test_invalid_signature(spec, state):
 @with_all_phases
 @spec_state_test
 def test_success_exit_queue(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
 
@@ -115,8 +115,8 @@ def test_success_exit_queue(spec, state):
 @with_all_phases
 @spec_state_test
 def test_default_exit_epoch_subsequent_exit(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
     validator_index = spec.get_active_validator_indices(state, current_epoch)[0]
@@ -137,8 +137,8 @@ def test_default_exit_epoch_subsequent_exit(spec, state):
 @with_all_phases
 @spec_state_test
 def test_validator_exit_in_future(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
     validator_index = spec.get_active_validator_indices(state, current_epoch)[0]
@@ -156,8 +156,8 @@ def test_validator_exit_in_future(spec, state):
 @with_all_phases
 @spec_state_test
 def test_validator_invalid_validator_index(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
     validator_index = spec.get_active_validator_indices(state, current_epoch)[0]
@@ -190,8 +190,8 @@ def test_validator_not_active(spec, state):
 @with_all_phases
 @spec_state_test
 def test_validator_already_exited(spec, state):
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow validator able to exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
     current_epoch = spec.get_current_epoch(state)
     validator_index = spec.get_active_validator_indices(state, current_epoch)[0]
@@ -218,7 +218,7 @@ def test_validator_not_active_long_enough(spec, state):
 
     assert (
         current_epoch - state.validators[validator_index].activation_epoch <
-        spec.PERSISTENT_COMMITTEE_PERIOD
+        spec.SHARD_COMMITTEE_PERIOD
     )
 
     yield from run_voluntary_exit_processing(spec, state, signed_voluntary_exit, False)
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py
--- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_registry_updates.py
@@ -1,3 +1,4 @@
+from eth2spec.test.helpers.deposits import mock_deposit
 from eth2spec.test.helpers.state import next_epoch, next_slots
 from eth2spec.test.context import spec_state_test, with_all_phases
 from eth2spec.test.phase_0.epoch_processing.run_epoch_process_base import run_epoch_processing_with
@@ -7,14 +8,6 @@ def run_process_registry_updates(spec, state):
     yield from run_epoch_processing_with(spec, state, 'process_registry_updates')
 
 
-def mock_deposit(spec, state, index):
-    assert spec.is_active_validator(state.validators[index], spec.get_current_epoch(state))
-    state.validators[index].activation_eligibility_epoch = spec.FAR_FUTURE_EPOCH
-    state.validators[index].activation_epoch = spec.FAR_FUTURE_EPOCH
-    state.validators[index].effective_balance = spec.MAX_EFFECTIVE_BALANCE
-    assert not spec.is_active_validator(state.validators[index], spec.get_current_epoch(state))
-
-
 @with_all_phases
 @spec_state_test
 def test_add_to_activation_queue(spec, state):
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py
--- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py
@@ -12,42 +12,18 @@
 from eth2spec.test.helpers.attestations import (
     add_attestations_to_state,
     get_valid_attestation,
+    prepare_state_with_attestations,
 )
+from eth2spec.test.helpers.rewards import leaking
 from eth2spec.test.helpers.attester_slashings import get_indexed_attestation_participants
 from eth2spec.test.phase_0.epoch_processing.run_epoch_process_base import run_epoch_processing_with
+from random import Random
 
 
 def run_process_rewards_and_penalties(spec, state):
     yield from run_epoch_processing_with(spec, state, 'process_rewards_and_penalties')
 
 
-def prepare_state_with_full_attestations(spec, state, empty=False):
-    # Go to start of next epoch to ensure can have full participation
-    next_epoch(spec, state)
-
-    start_slot = state.slot
-    start_epoch = spec.get_current_epoch(state)
-    next_epoch_start_slot = spec.compute_start_slot_at_epoch(start_epoch + 1)
-    attestations = []
-    for _ in range(spec.SLOTS_PER_EPOCH + spec.MIN_ATTESTATION_INCLUSION_DELAY):
-        # create an attestation for each index in each slot in epoch
-        if state.slot < next_epoch_start_slot:
-            for committee_index in range(spec.get_committee_count_at_slot(state, state.slot)):
-                attestation = get_valid_attestation(spec, state, index=committee_index, empty=empty, signed=True)
-                attestations.append(attestation)
-        # fill each created slot in state after inclusion delay
-        if state.slot >= start_slot + spec.MIN_ATTESTATION_INCLUSION_DELAY:
-            inclusion_slot = state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY
-            include_attestations = [att for att in attestations if att.data.slot == inclusion_slot]
-            add_attestations_to_state(spec, state, include_attestations, state.slot)
-        next_slot(spec, state)
-
-    assert state.slot == next_epoch_start_slot + spec.MIN_ATTESTATION_INCLUSION_DELAY
-    assert len(state.previous_epoch_attestations) == len(attestations)
-
-    return attestations
-
-
 @with_phases(['phase0'])
 @spec_state_test
 def test_genesis_epoch_no_attestations_no_penalties(spec, state):
@@ -87,28 +63,10 @@ def test_genesis_epoch_full_attestations_no_rewards(spec, state):
         assert state.balances[index] == pre_state.balances[index]
 
 
-@with_all_phases
-@spec_state_test
-def test_full_attestations(spec, state):
-    attestations = prepare_state_with_full_attestations(spec, state)
-
-    pre_state = state.copy()
-
-    yield from run_process_rewards_and_penalties(spec, state)
-
-    attesting_indices = spec.get_unslashed_attesting_indices(state, attestations)
-    assert len(attesting_indices) == len(pre_state.validators)
-    for index in range(len(pre_state.validators)):
-        if index in attesting_indices:
-            assert state.balances[index] > pre_state.balances[index]
-        else:
-            assert state.balances[index] < pre_state.balances[index]
-
-
 @with_all_phases
 @spec_state_test
 def test_full_attestations_random_incorrect_fields(spec, state):
-    attestations = prepare_state_with_full_attestations(spec, state)
+    attestations = prepare_state_with_attestations(spec, state)
     for i, attestation in enumerate(state.previous_epoch_attestations):
         if i % 3 == 0:
             # Mess up some head votes
@@ -133,7 +91,7 @@ def test_full_attestations_random_incorrect_fields(spec, state):
 @with_custom_state(balances_fn=misc_balances, threshold_fn=lambda spec: spec.MAX_EFFECTIVE_BALANCE // 2)
 @single_phase
 def test_full_attestations_misc_balances(spec, state):
-    attestations = prepare_state_with_full_attestations(spec, state)
+    attestations = prepare_state_with_attestations(spec, state)
 
     pre_state = state.copy()
 
@@ -165,7 +123,7 @@ def test_full_attestations_misc_balances(spec, state):
 @with_custom_state(balances_fn=low_single_balance, threshold_fn=zero_activation_threshold)
 @single_phase
 def test_full_attestations_one_validaor_one_gwei(spec, state):
-    attestations = prepare_state_with_full_attestations(spec, state)
+    attestations = prepare_state_with_attestations(spec, state)
 
     yield from run_process_rewards_and_penalties(spec, state)
 
@@ -189,20 +147,97 @@ def test_no_attestations_all_penalties(spec, state):
         assert state.balances[index] < pre_state.balances[index]
 
 
-@with_all_phases
-@spec_state_test
-def test_empty_attestations(spec, state):
-    attestations = prepare_state_with_full_attestations(spec, state, empty=True)
+def run_with_participation(spec, state, participation_fn):
+    participated = set()
+
+    def participation_tracker(slot, comm_index, comm):
+        att_participants = participation_fn(slot, comm_index, comm)
+        participated.update(att_participants)
+        return att_participants
+
+    attestations = prepare_state_with_attestations(spec, state, participation_fn=participation_tracker)
+    proposer_indices = [a.proposer_index for a in state.previous_epoch_attestations]
 
     pre_state = state.copy()
 
     yield from run_process_rewards_and_penalties(spec, state)
 
     attesting_indices = spec.get_unslashed_attesting_indices(state, attestations)
-    assert len(attesting_indices) == 0
+    assert len(attesting_indices) == len(participated)
 
     for index in range(len(pre_state.validators)):
-        assert state.balances[index] < pre_state.balances[index]
+        if spec.is_in_inactivity_leak(state):
+            # Proposers can still make money during a leak
+            if index in proposer_indices and index in participated:
+                assert state.balances[index] > pre_state.balances[index]
+            # If not proposer but participated optimally, should have exactly neutral balance
+            elif index in attesting_indices:
+                assert state.balances[index] == pre_state.balances[index]
+            else:
+                assert state.balances[index] < pre_state.balances[index]
+        else:
+            if index in participated:
+                assert state.balances[index] > pre_state.balances[index]
+            else:
+                assert state.balances[index] < pre_state.balances[index]
+
+
+@with_all_phases
+@spec_state_test
+def test_almost_empty_attestations(spec, state):
+    rng = Random(1234)
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1))
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_almost_empty_attestations_with_leak(spec, state):
+    rng = Random(1234)
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1))
+
+
+@with_all_phases
+@spec_state_test
+def test_random_fill_attestations(spec, state):
+    rng = Random(4567)
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3))
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_random_fill_attestations_with_leak(spec, state):
+    rng = Random(4567)
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3))
+
+
+@with_all_phases
+@spec_state_test
+def test_almost_full_attestations(spec, state):
+    rng = Random(8901)
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1))
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_almost_full_attestations_with_leak(spec, state):
+    rng = Random(8901)
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1))
+
+
+@with_all_phases
+@spec_state_test
+def test_full_attestation_participation(spec, state):
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_attestation_participation_with_leak(spec, state):
+    yield from run_with_participation(spec, state, lambda slot, comm_index, comm: comm)
 
 
 @with_all_phases
@@ -247,7 +282,7 @@ def test_duplicate_attestation(spec, state):
 @spec_state_test
 # Case when some eligible attestations are slashed. Modifies attesting_balance and consequently rewards/penalties.
 def test_attestations_some_slashed(spec, state):
-    attestations = prepare_state_with_full_attestations(spec, state)
+    attestations = prepare_state_with_attestations(spec, state)
     attesting_indices_before_slashings = list(spec.get_unslashed_attesting_indices(state, attestations))
 
     # Slash maximum amount of validators allowed per epoch.
diff --git a/tests/core/pyspec/eth2spec/test/helpers/phase1/__init__.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/__init__.py
similarity index 100%
rename from tests/core/pyspec/eth2spec/test/helpers/phase1/__init__.py
rename to tests/core/pyspec/eth2spec/test/phase_0/rewards/__init__.py
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_basic.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_basic.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_basic.py
@@ -0,0 +1,155 @@
+from eth2spec.test.context import with_all_phases, spec_state_test
+import eth2spec.test.helpers.rewards as rewards_helpers
+
+
+@with_all_phases
+@spec_state_test
+def test_empty(spec, state):
+    yield from rewards_helpers.run_test_empty(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_full_all_correct(spec, state):
+    yield from rewards_helpers.run_test_full_all_correct(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_half_full(spec, state):
+    yield from rewards_helpers.run_test_half_full(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_quarter_full(spec, state):
+    yield from rewards_helpers.run_test_partial(spec, state, 0.25)
+
+
+@with_all_phases
+@spec_state_test
+def test_full_but_partial_participation(spec, state):
+    yield from rewards_helpers.run_test_full_but_partial_participation(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_one_attestation_one_correct(spec, state):
+    yield from rewards_helpers.run_test_one_attestation_one_correct(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_with_not_yet_activated_validators(spec, state):
+    yield from rewards_helpers.run_test_with_not_yet_activated_validators(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_with_exited_validators(spec, state):
+    yield from rewards_helpers.run_test_with_exited_validators(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_with_slashed_validators(spec, state):
+    yield from rewards_helpers.run_test_with_slashed_validators(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_some_very_low_effective_balances_that_attested(spec, state):
+    yield from rewards_helpers.run_test_some_very_low_effective_balances_that_attested(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_some_very_low_effective_balances_that_did_not_attest(spec, state):
+    yield from rewards_helpers.run_test_some_very_low_effective_balances_that_did_not_attest(spec, state)
+
+
+#
+# NOTE: No source incorrect tests
+# All PendingAttestations in state have source validated
+# We choose to keep this invariant in these tests to not force clients to test with degenerate states
+#
+
+
+@with_all_phases
+@spec_state_test
+def test_full_half_correct_target_incorrect_head(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=True,
+        correct_head=False,
+        fraction_incorrect=0.5,
+    )
+
+
+@with_all_phases
+@spec_state_test
+def test_full_correct_target_incorrect_head(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=True,
+        correct_head=False,
+        fraction_incorrect=1.0,
+    )
+
+
+@with_all_phases
+@spec_state_test
+def test_full_half_incorrect_target_incorrect_head(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=False,
+        correct_head=False,
+        fraction_incorrect=0.5,
+    )
+
+
+@with_all_phases
+@spec_state_test
+def test_full_half_incorrect_target_correct_head(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=False,
+        correct_head=True,
+        fraction_incorrect=0.5,
+    )
+
+
+@with_all_phases
+@spec_state_test
+def test_full_delay_one_slot(spec, state):
+    yield from rewards_helpers.run_test_full_delay_one_slot(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_full_delay_max_slots(spec, state):
+    yield from rewards_helpers.run_test_full_delay_max_slots(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_full_mixed_delay(spec, state):
+    yield from rewards_helpers.run_test_full_mixed_delay(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_proposer_not_in_attestations(spec, state):
+    yield from rewards_helpers.run_test_proposer_not_in_attestations(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_duplicate_attestations_at_later_slots(spec, state):
+    yield from rewards_helpers.run_test_duplicate_attestations_at_later_slots(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+def test_all_balances_too_low_for_reward(spec, state):
+    yield from rewards_helpers.run_test_all_balances_too_low_for_reward(spec, state)
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_leak.py
@@ -0,0 +1,156 @@
+from eth2spec.test.context import with_all_phases, spec_state_test
+from eth2spec.test.helpers.rewards import leaking
+import eth2spec.test.helpers.rewards as rewards_helpers
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_empty_leak(spec, state):
+    yield from rewards_helpers.run_test_empty(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_leak(spec, state):
+    yield from rewards_helpers.run_test_full_all_correct(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_half_full_leak(spec, state):
+    yield from rewards_helpers.run_test_half_full(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_quarter_full_leak(spec, state):
+    yield from rewards_helpers.run_test_partial(spec, state, 0.25)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_but_partial_participation_leak(spec, state):
+    yield from rewards_helpers.run_test_full_but_partial_participation(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_one_attestation_one_correct_leak(spec, state):
+    yield from rewards_helpers.run_test_one_attestation_one_correct(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_with_not_yet_activated_validators_leak(spec, state):
+    yield from rewards_helpers.run_test_with_not_yet_activated_validators(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_with_exited_validators_leak(spec, state):
+    yield from rewards_helpers.run_test_with_exited_validators(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_with_slashed_validators_leak(spec, state):
+    yield from rewards_helpers.run_test_with_slashed_validators(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_some_very_low_effective_balances_that_attested_leak(spec, state):
+    yield from rewards_helpers.run_test_some_very_low_effective_balances_that_attested(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_some_very_low_effective_balances_that_did_not_attest_leak(spec, state):
+    yield from rewards_helpers.run_test_some_very_low_effective_balances_that_did_not_attest(spec, state)
+
+
+#
+# NOTE: No source incorrect tests
+# All PendingAttestations in state have source validated
+# We choose to keep this invariant in these tests to not force clients to test with degenerate states
+#
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_half_correct_target_incorrect_head_leak(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=True,
+        correct_head=False,
+        fraction_incorrect=0.5,
+    )
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_correct_target_incorrect_head_leak(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=True,
+        correct_head=False,
+        fraction_incorrect=1.0,
+    )
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_half_incorrect_target_incorrect_head_leak(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=False,
+        correct_head=False,
+        fraction_incorrect=0.5,
+    )
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_half_incorrect_target_correct_head_leak(spec, state):
+    yield from rewards_helpers.run_test_full_fraction_incorrect(
+        spec, state,
+        correct_target=False,
+        correct_head=True,
+        fraction_incorrect=0.5,
+    )
+
+
+@with_all_phases
+@spec_state_test
+@leaking()
+def test_full_random_leak(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking(epochs=5)
+def test_full_random_five_epoch_leak(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state)
+
+
+@with_all_phases
+@spec_state_test
+@leaking(epochs=10)
+def test_full_random_ten_epoch_leak(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state)
diff --git a/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_random.py b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_random.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/phase_0/rewards/test_random.py
@@ -0,0 +1,45 @@
+from random import Random
+
+from eth2spec.test.context import (
+    with_all_phases,
+    spec_test,
+    spec_state_test,
+    with_custom_state,
+    single_phase,
+    low_balances, misc_balances,
+)
+import eth2spec.test.helpers.rewards as rewards_helpers
+
+
+@with_all_phases
+@spec_state_test
+def test_full_random_0(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(1010))
+
+
+@with_all_phases
+@spec_state_test
+def test_full_random_1(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(2020))
+
+
+@with_all_phases
+@spec_state_test
+def test_full_random_2(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(3030))
+
+
+@with_all_phases
+@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE)
+@spec_test
+@single_phase
+def test_full_random_low_balances(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state)
+
+
+@with_all_phases
+@with_custom_state(balances_fn=misc_balances, threshold_fn=lambda spec: spec.EJECTION_BALANCE)
+@spec_test
+@single_phase
+def test_full_random_misc_balances(spec, state):
+    yield from rewards_helpers.run_test_full_random(spec, state)
diff --git a/tests/core/pyspec/eth2spec/test/sanity/__init__.py b/tests/core/pyspec/eth2spec/test/phase_0/sanity/__init__.py
similarity index 100%
rename from tests/core/pyspec/eth2spec/test/sanity/__init__.py
rename to tests/core/pyspec/eth2spec/test/phase_0/sanity/__init__.py
diff --git a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py
similarity index 65%
rename from tests/core/pyspec/eth2spec/test/sanity/test_blocks.py
rename to tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py
--- a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py
+++ b/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py
@@ -1,5 +1,3 @@
-from copy import deepcopy
-
 from eth2spec.utils import bls
 
 from eth2spec.test.helpers.state import (
@@ -12,12 +10,19 @@
     transition_unsigned_block,
 )
 from eth2spec.test.helpers.keys import privkeys, pubkeys
-from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing, get_indexed_attestation_participants
-from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing
-from eth2spec.test.helpers.attestations import get_valid_attestation
+from eth2spec.test.helpers.attester_slashings import (
+    get_valid_attester_slashing_by_indices,
+    get_valid_attester_slashing,
+    get_indexed_attestation_participants,
+)
+from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect
+from eth2spec.test.helpers.attestations import get_valid_attestation, fill_block_shard_transitions_by_attestations
 from eth2spec.test.helpers.deposits import prepare_state_and_deposit
 
-from eth2spec.test.context import spec_state_test, with_all_phases, expect_assertion_error, always_bls, with_phases
+from eth2spec.test.context import (
+    spec_state_test, with_all_phases, expect_assertion_error, always_bls, with_phases,
+    PHASE1
+)
 
 
 @with_all_phases
@@ -311,11 +316,11 @@ def test_empty_epoch_transition_not_finalizing(spec, state):
 @spec_state_test
 def test_proposer_slashing(spec, state):
     # copy for later balance lookups.
-    pre_state = deepcopy(state)
+    pre_state = state.copy()
     proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True)
-    validator_index = proposer_slashing.signed_header_1.message.proposer_index
+    slashed_index = proposer_slashing.signed_header_1.message.proposer_index
 
-    assert not state.validators[validator_index].slashed
+    assert not state.validators[slashed_index].slashed
 
     yield 'pre', state
 
@@ -330,25 +335,108 @@ def test_proposer_slashing(spec, state):
     yield 'blocks', [signed_block]
     yield 'post', state
 
-    # check if slashed
-    slashed_validator = state.validators[validator_index]
-    assert slashed_validator.slashed
-    assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
-    assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
-    # lost whistleblower reward
-    assert get_balance(state, validator_index) < get_balance(pre_state, validator_index)
+    check_proposer_slashing_effect(spec, pre_state, state, slashed_index)
+
+
+@with_all_phases
+@spec_state_test
+def test_double_same_proposer_slashings_same_block(spec, state):
+    proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True)
+    slashed_index = proposer_slashing.signed_header_1.message.proposer_index
+    assert not state.validators[slashed_index].slashed
+
+    yield 'pre', state
+
+    block = build_empty_block_for_next_slot(spec, state)
+    block.body.proposer_slashings = [proposer_slashing, proposer_slashing]
+    signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True)
+
+    yield 'blocks', [signed_block]
+    yield 'post', None
+
+
+@with_all_phases
+@spec_state_test
+def test_double_similar_proposer_slashings_same_block(spec, state):
+    slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1]
+
+    # Same validator, but different slashable offences in the same block
+    proposer_slashing_1 = get_valid_proposer_slashing(spec, state, random_root=b'\xaa' * 32,
+                                                      slashed_index=slashed_index,
+                                                      signed_1=True, signed_2=True)
+    proposer_slashing_2 = get_valid_proposer_slashing(spec, state, random_root=b'\xbb' * 32,
+                                                      slashed_index=slashed_index,
+                                                      signed_1=True, signed_2=True)
+    assert not state.validators[slashed_index].slashed
+
+    yield 'pre', state
+
+    block = build_empty_block_for_next_slot(spec, state)
+    block.body.proposer_slashings = [proposer_slashing_1, proposer_slashing_2]
+    signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True)
+
+    yield 'blocks', [signed_block]
+    yield 'post', None
+
+
+@with_all_phases
+@spec_state_test
+def test_multiple_different_proposer_slashings_same_block(spec, state):
+    pre_state = state.copy()
+
+    num_slashings = 3
+    proposer_slashings = []
+    for i in range(num_slashings):
+        slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i]
+        assert not state.validators[slashed_index].slashed
+
+        proposer_slashing = get_valid_proposer_slashing(spec, state,
+                                                        slashed_index=slashed_index,
+                                                        signed_1=True, signed_2=True)
+        proposer_slashings.append(proposer_slashing)
+
+    yield 'pre', state
+
+    #
+    # Add to state via block transition
+    #
+    block = build_empty_block_for_next_slot(spec, state)
+    block.body.proposer_slashings = proposer_slashings
+
+    signed_block = state_transition_and_sign_block(spec, state, block)
+
+    yield 'blocks', [signed_block]
+    yield 'post', state
+
+    for proposer_slashing in proposer_slashings:
+        slashed_index = proposer_slashing.signed_header_1.message.proposer_index
+        check_proposer_slashing_effect(spec, pre_state, state, slashed_index)
+
+
+def check_attester_slashing_effect(spec, pre_state, state, slashed_indices):
+    for slashed_index in slashed_indices:
+        slashed_validator = state.validators[slashed_index]
+        assert slashed_validator.slashed
+        assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
+        assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
+        # lost whistleblower reward
+        assert get_balance(state, slashed_index) < get_balance(pre_state, slashed_index)
+
+    proposer_index = spec.get_beacon_proposer_index(state)
+    # gained whistleblower reward
+    assert get_balance(state, proposer_index) > get_balance(pre_state, proposer_index)
 
 
 @with_all_phases
 @spec_state_test
 def test_attester_slashing(spec, state):
     # copy for later balance lookups.
-    pre_state = deepcopy(state)
+    pre_state = state.copy()
 
     attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True)
-    validator_index = get_indexed_attestation_participants(spec, attester_slashing.attestation_1)[0]
+    slashed_indices = get_indexed_attestation_participants(spec, attester_slashing.attestation_1)
 
-    assert not state.validators[validator_index].slashed
+    assert not any(state.validators[i].slashed for i in slashed_indices)
 
     yield 'pre', state
 
@@ -363,19 +451,118 @@ def test_attester_slashing(spec, state):
     yield 'blocks', [signed_block]
     yield 'post', state
 
-    slashed_validator = state.validators[validator_index]
-    assert slashed_validator.slashed
-    assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH
-    assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH
-    # lost whistleblower reward
-    assert get_balance(state, validator_index) < get_balance(pre_state, validator_index)
+    check_attester_slashing_effect(spec, pre_state, state, slashed_indices)
 
-    proposer_index = spec.get_beacon_proposer_index(state)
-    # gained whistleblower reward
-    assert (
-        get_balance(state, proposer_index) >
-        get_balance(pre_state, proposer_index)
+
+@with_all_phases
+@spec_state_test
+def test_duplicate_attester_slashing(spec, state):
+    # Skip test if config cannot handle multiple AttesterSlashings per block
+    if spec.MAX_ATTESTER_SLASHINGS < 2:
+        return
+
+    attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True)
+    attester_slashings = [attester_slashing, attester_slashing.copy()]
+    slashed_indices = get_indexed_attestation_participants(spec, attester_slashing.attestation_1)
+
+    assert not any(state.validators[i].slashed for i in slashed_indices)
+
+    yield 'pre', state
+
+    #
+    # Add to state via block transition
+    #
+    block = build_empty_block_for_next_slot(spec, state)
+    block.body.attester_slashings = attester_slashings
+
+    signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True)
+
+    yield 'blocks', [signed_block]
+    yield 'post', None
+
+
+# All AttesterSlashing tests should be adopted for Phase 1 but helper support is not yet there
+
+@with_phases(['phase0'])
+@spec_state_test
+def test_multiple_attester_slashings_no_overlap(spec, state):
+    # Skip test if config cannot handle multiple AttesterSlashings per block
+    if spec.MAX_ATTESTER_SLASHINGS < 2:
+        return
+
+    # copy for later balance lookups.
+    pre_state = state.copy()
+
+    full_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[:8]
+    half_length = len(full_indices) // 2
+
+    attester_slashing_1 = get_valid_attester_slashing_by_indices(
+        spec, state,
+        full_indices[:half_length], signed_1=True, signed_2=True,
+    )
+    attester_slashing_2 = get_valid_attester_slashing_by_indices(
+        spec, state,
+        full_indices[half_length:], signed_1=True, signed_2=True,
     )
+    attester_slashings = [attester_slashing_1, attester_slashing_2]
+
+    assert not any(state.validators[i].slashed for i in full_indices)
+
+    yield 'pre', state
+
+    #
+    # Add to state via block transition
+    #
+    block = build_empty_block_for_next_slot(spec, state)
+    block.body.attester_slashings = attester_slashings
+
+    signed_block = state_transition_and_sign_block(spec, state, block)
+
+    yield 'blocks', [signed_block]
+    yield 'post', state
+
+    check_attester_slashing_effect(spec, pre_state, state, full_indices)
+
+
+@with_phases(['phase0'])
+@spec_state_test
+def test_multiple_attester_slashings_partial_overlap(spec, state):
+    # Skip test if config cannot handle multiple AttesterSlashings per block
+    if spec.MAX_ATTESTER_SLASHINGS < 2:
+        return
+
+    # copy for later balance lookups.
+    pre_state = state.copy()
+
+    full_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[:8]
+    one_third_length = len(full_indices) // 3
+
+    attester_slashing_1 = get_valid_attester_slashing_by_indices(
+        spec, state,
+        full_indices[:one_third_length * 2], signed_1=True, signed_2=True,
+    )
+    attester_slashing_2 = get_valid_attester_slashing_by_indices(
+        spec, state,
+        full_indices[one_third_length:], signed_1=True, signed_2=True,
+    )
+    attester_slashings = [attester_slashing_1, attester_slashing_2]
+
+    assert not any(state.validators[i].slashed for i in full_indices)
+
+    yield 'pre', state
+
+    #
+    # Add to state via block transition
+    #
+    block = build_empty_block_for_next_slot(spec, state)
+    block.body.attester_slashings = attester_slashings
+
+    signed_block = state_transition_and_sign_block(spec, state, block)
+
+    yield 'blocks', [signed_block]
+    yield 'post', state
+
+    check_attester_slashing_effect(spec, pre_state, state, full_indices)
 
 
 @with_all_phases
@@ -500,12 +687,14 @@ def test_attestation(spec, state):
 
     yield 'pre', state
 
-    attestation = get_valid_attestation(spec, state, signed=True)
+    attestation = get_valid_attestation(spec, state, signed=True, on_time=True)
 
     # Add to state via block transition
     pre_current_attestations_len = len(state.current_epoch_attestations)
     attestation_block = build_empty_block(spec, state, state.slot + spec.MIN_ATTESTATION_INCLUSION_DELAY)
     attestation_block.body.attestations.append(attestation)
+    if spec.fork == PHASE1:
+        fill_block_shard_transitions_by_attestations(spec, state, attestation_block)
     signed_attestation_block = state_transition_and_sign_block(spec, state, attestation_block)
 
     assert len(state.current_epoch_attestations) == pre_current_attestations_len + 1
@@ -523,35 +712,38 @@ def test_attestation(spec, state):
     assert spec.hash_tree_root(state.previous_epoch_attestations) == pre_current_attestations_root
 
 
-# In phase1 a committee is computed for PERSISTENT_COMMITTEE_PERIOD slots ago,
+def prepare_signed_exits(spec, state, indices):
+    domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT)
+
+    def create_signed_exit(index):
+        exit = spec.VoluntaryExit(
+            epoch=spec.get_current_epoch(state),
+            validator_index=index,
+        )
+        signing_root = spec.compute_signing_root(exit, domain)
+        return spec.SignedVoluntaryExit(message=exit, signature=bls.Sign(privkeys[index], signing_root))
+
+    return [create_signed_exit(index) for index in indices]
+
+
+# In phase1 a committee is computed for SHARD_COMMITTEE_PERIOD slots ago,
 # exceeding the minimal-config randao mixes memory size.
+# Applies to all voluntary-exit sanity block tests.
+
 @with_phases(['phase0'])
 @spec_state_test
 def test_voluntary_exit(spec, state):
-    validator_index = spec.get_active_validator_indices(
-        state,
-        spec.get_current_epoch(state)
-    )[-1]
+    validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1]
 
-    # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit
-    state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
 
+    signed_exits = prepare_signed_exits(spec, state, [validator_index])
     yield 'pre', state
 
-    voluntary_exit = spec.VoluntaryExit(
-        epoch=spec.get_current_epoch(state),
-        validator_index=validator_index,
-    )
-    domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT)
-    signing_root = spec.compute_signing_root(voluntary_exit, domain)
-    signed_voluntary_exit = spec.SignedVoluntaryExit(
-        message=voluntary_exit,
-        signature=bls.Sign(privkeys[validator_index], signing_root)
-    )
-
     # Add to state via block transition
     initiate_exit_block = build_empty_block_for_next_slot(spec, state)
-    initiate_exit_block.body.voluntary_exits.append(signed_voluntary_exit)
+    initiate_exit_block.body.voluntary_exits = signed_exits
     signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block)
 
     assert state.validators[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH
@@ -566,6 +758,59 @@ def test_voluntary_exit(spec, state):
     assert state.validators[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH
 
 
+@with_phases(['phase0'])
+@spec_state_test
+def test_double_validator_exit_same_block(spec, state):
+    validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1]
+
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+
+    # Same index tries to exit twice, but should only be able to do so once.
+    signed_exits = prepare_signed_exits(spec, state, [validator_index, validator_index])
+    yield 'pre', state
+
+    # Add to state via block transition
+    initiate_exit_block = build_empty_block_for_next_slot(spec, state)
+    initiate_exit_block.body.voluntary_exits = signed_exits
+    signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block, expect_fail=True)
+
+    yield 'blocks', [signed_initiate_exit_block]
+    yield 'post', None
+
+
+@with_phases(['phase0'])
+@spec_state_test
+def test_multiple_different_validator_exits_same_block(spec, state):
+    validator_indices = [
+        spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i]
+        for i in range(3)
+    ]
+    # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit
+    state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH
+
+    signed_exits = prepare_signed_exits(spec, state, validator_indices)
+    yield 'pre', state
+
+    # Add to state via block transition
+    initiate_exit_block = build_empty_block_for_next_slot(spec, state)
+    initiate_exit_block.body.voluntary_exits = signed_exits
+    signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block)
+
+    for index in validator_indices:
+        assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH
+
+    # Process within epoch transition
+    exit_block = build_empty_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH)
+    signed_exit_block = state_transition_and_sign_block(spec, state, exit_block)
+
+    yield 'blocks', [signed_initiate_exit_block, signed_exit_block]
+    yield 'post', state
+
+    for index in validator_indices:
+        assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH
+
+
 @with_all_phases
 @spec_state_test
 def test_balance_driven_status_transitions(spec, state):
diff --git a/tests/core/pyspec/eth2spec/test/sanity/test_slots.py b/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_slots.py
similarity index 100%
rename from tests/core/pyspec/eth2spec/test/sanity/test_slots.py
rename to tests/core/pyspec/eth2spec/test/phase_0/sanity/test_slots.py
diff --git a/tests/core/pyspec/eth2spec/test/phase_1/block_processing/test_process_crosslink.py b/tests/core/pyspec/eth2spec/test/phase_1/block_processing/test_process_crosslink.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/phase_1/block_processing/test_process_crosslink.py
@@ -0,0 +1,73 @@
+from eth2spec.test.context import (
+    PHASE0,
+    with_all_phases_except,
+    spec_state_test,
+    always_bls,
+)
+from eth2spec.test.helpers.crosslinks import run_crosslinks_processing
+from eth2spec.test.helpers.shard_block import (
+    build_attestation_with_shard_transition,
+    build_shard_block,
+    build_shard_transitions_till_slot,
+)
+from eth2spec.test.helpers.state import transition_to, transition_to_valid_shard_slot
+
+
+def run_basic_crosslink_tests(spec, state, target_len_offset_slot, valid=True):
+    state = transition_to_valid_shard_slot(spec, state)
+    # At the beginning, let `x = state.slot`, `state.shard_states[shard].slot == x - 1`
+    slot_x = state.slot
+    committee_index = spec.CommitteeIndex(0)
+    shard = spec.compute_shard_from_committee_index(state, committee_index, state.slot)
+    assert state.shard_states[shard].slot == slot_x - 1
+
+    # Create SignedShardBlock
+    body = b'\x56' * spec.MAX_SHARD_BLOCK_SIZE
+    shard_block = build_shard_block(spec, state, shard, body=body, signed=True)
+    shard_blocks = [shard_block]
+    # Create a shard_transitions that would be included at beacon block `state.slot + target_len_offset_slot`
+    shard_transitions = build_shard_transitions_till_slot(
+        spec,
+        state,
+        shard_blocks={shard: shard_blocks},
+        on_time_slot=state.slot + target_len_offset_slot,
+    )
+    shard_transition = shard_transitions[shard]
+    # Create an attestation that would be included at beacon block `state.slot + target_len_offset_slot`
+    attestation = build_attestation_with_shard_transition(
+        spec,
+        state,
+        index=committee_index,
+        on_time_slot=state.slot + target_len_offset_slot,
+        shard_transition=shard_transition,
+    )
+    pre_gasprice = state.shard_states[shard].gasprice
+
+    transition_to(spec, state, state.slot + target_len_offset_slot)
+    pre_shard_state = state.shard_states[shard]
+
+    yield from run_crosslinks_processing(spec, state, shard_transitions, [attestation], valid=valid)
+
+    if valid:
+        # After state transition,
+        assert state.slot == slot_x + target_len_offset_slot
+        shard_state = state.shard_states[shard]
+        assert shard_state != pre_shard_state
+        assert shard_state == shard_transition.shard_states[len(shard_transition.shard_states) - 1]
+
+        if target_len_offset_slot == 1:
+            assert shard_state.gasprice > pre_gasprice
+
+
+@with_all_phases_except([PHASE0])
+@spec_state_test
+@always_bls
+def test_basic_crosslinks(spec, state):
+    yield from run_basic_crosslink_tests(spec, state, target_len_offset_slot=1, valid=True)
+
+
+@with_all_phases_except([PHASE0])
+@spec_state_test
+@always_bls
+def test_multiple_offset_slots(spec, state):
+    yield from run_basic_crosslink_tests(spec, state, target_len_offset_slot=3, valid=True)
diff --git a/tests/core/pyspec/eth2spec/test/phase_1/sanity/__init__.py b/tests/core/pyspec/eth2spec/test/phase_1/sanity/__init__.py
new file mode 100644
diff --git a/tests/core/pyspec/eth2spec/test/phase_1/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase_1/sanity/test_blocks.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/phase_1/sanity/test_blocks.py
@@ -0,0 +1,107 @@
+from typing import Dict, Sequence
+
+from eth2spec.test.context import (
+    PHASE0,
+    with_all_phases_except,
+    spec_state_test,
+    always_bls,
+)
+from eth2spec.test.helpers.block import build_empty_block
+from eth2spec.test.helpers.shard_block import (
+    build_attestation_with_shard_transition,
+    build_shard_block,
+    build_shard_transitions_till_slot,
+)
+from eth2spec.test.helpers.state import state_transition_and_sign_block, transition_to_valid_shard_slot
+
+
+def run_beacon_block_with_shard_blocks(spec, state, shard_blocks, target_len_offset_slot, committee_index, valid=True):
+    shard_transitions = build_shard_transitions_till_slot(
+        spec, state, shard_blocks, on_time_slot=state.slot + target_len_offset_slot
+    )
+    attestations = [
+        build_attestation_with_shard_transition(
+            spec,
+            state,
+            on_time_slot=state.slot + target_len_offset_slot,
+            index=committee_index,
+            shard_transition=shard_transitions[shard],
+        )
+        for shard in shard_blocks.keys()
+    ]
+
+    # Propose beacon block at slot `x + 1`
+    beacon_block = build_empty_block(spec, state, slot=state.slot + target_len_offset_slot)
+    beacon_block.body.attestations = attestations
+    beacon_block.body.shard_transitions = shard_transitions
+
+    pre_shard_states = state.shard_states.copy()
+    yield 'pre', state.copy()
+    yield 'block', beacon_block
+    state_transition_and_sign_block(spec, state, beacon_block)
+    if valid:
+        yield 'post', state
+    else:
+        yield 'post', None
+        return
+
+    for shard in range(spec.get_active_shard_count(state)):
+        post_shard_state = state.shard_states[shard]
+        if shard in shard_blocks:
+            # Shard state has been changed to state_transition result
+            assert post_shard_state == shard_transitions[shard].shard_states[
+                len(shard_transitions[shard].shard_states) - 1
+            ]
+            assert beacon_block.slot == shard_transitions[shard].shard_states[0].slot + target_len_offset_slot
+            assert post_shard_state.slot == state.slot - 1
+            if len(shard_blocks[shard]) == 0:
+                # `latest_block_root` is the same
+                assert post_shard_state.latest_block_root == pre_shard_states[shard].latest_block_root
+
+
+@with_all_phases_except([PHASE0])
+@spec_state_test
+@always_bls
+def test_process_beacon_block_with_normal_shard_transition(spec, state):
+    state = transition_to_valid_shard_slot(spec, state)
+
+    target_len_offset_slot = 1
+    committee_index = spec.CommitteeIndex(0)
+    shard = spec.compute_shard_from_committee_index(state, committee_index, state.slot)
+    assert state.shard_states[shard].slot == state.slot - 1
+
+    pre_gasprice = state.shard_states[shard].gasprice
+
+    # Create SignedShardBlock at slot `shard_state.slot + 1`
+    body = b'\x56' * spec.MAX_SHARD_BLOCK_SIZE
+    shard_block = build_shard_block(spec, state, shard, body=body, signed=True)
+    shard_blocks: Dict[spec.Shard, Sequence[spec.SignedShardBlock]] = {shard: [shard_block]}
+
+    yield from run_beacon_block_with_shard_blocks(spec, state, shard_blocks, target_len_offset_slot, committee_index)
+
+    shard_state = state.shard_states[shard]
+
+    if target_len_offset_slot == 1 and len(shard_blocks) > 0:
+        assert shard_state.gasprice > pre_gasprice
+
+
+@with_all_phases_except([PHASE0])
+@spec_state_test
+@always_bls
+def test_process_beacon_block_with_empty_proposal_transition(spec, state):
+    state = transition_to_valid_shard_slot(spec, state)
+
+    target_len_offset_slot = 1
+    committee_index = spec.CommitteeIndex(0)
+    shard = spec.compute_shard_from_committee_index(state, committee_index, state.slot)
+    assert state.shard_states[shard].slot == state.slot - 1
+
+    # No new shard block
+    shard_blocks = {}
+
+    pre_gasprice = state.shard_states[shard].gasprice
+
+    yield from run_beacon_block_with_shard_blocks(spec, state, shard_blocks, target_len_offset_slot, committee_index)
+
+    if target_len_offset_slot == 1 and len(shard_blocks) > 0:
+        assert state.shard_states[shard].gasprice > pre_gasprice
diff --git a/tests/core/pyspec/eth2spec/test/validator/test_validator_unittest.py b/tests/core/pyspec/eth2spec/test/validator/test_validator_unittest.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/validator/test_validator_unittest.py
@@ -0,0 +1,444 @@
+from eth2spec.test.context import spec_state_test, always_bls, with_all_phases
+from eth2spec.test.helpers.attestations import build_attestation_data
+from eth2spec.test.helpers.block import build_empty_block
+from eth2spec.test.helpers.deposits import prepare_state_and_deposit
+from eth2spec.test.helpers.keys import privkeys, pubkeys
+from eth2spec.test.helpers.state import next_epoch
+from eth2spec.utils import bls
+from eth2spec.utils.ssz.ssz_typing import Bitlist
+
+
+def run_get_signature_test(spec, state, obj, domain, get_signature_fn, privkey, pubkey, signing_ssz_object=None):
+    if signing_ssz_object is None:
+        signing_ssz_object = obj
+    signature = get_signature_fn(state, obj, privkey)
+    signing_root = spec.compute_signing_root(signing_ssz_object, domain)
+    assert bls.Verify(pubkey, signing_root, signature)
+
+
+def run_get_committee_assignment(spec, state, epoch, validator_index, valid=True):
+    try:
+        assignment = spec.get_committee_assignment(state, epoch, validator_index)
+        committee, committee_index, slot = assignment
+        assert spec.compute_epoch_at_slot(slot) == epoch
+        assert committee == spec.get_beacon_committee(state, slot, committee_index)
+        assert committee_index < spec.get_committee_count_at_slot(state, slot)
+        assert validator_index in committee
+        assert valid
+    except AssertionError:
+        assert not valid
+    else:
+        assert valid
+
+
+def run_is_candidate_block(spec, eth1_block, period_start, success=True):
+    assert success == spec.is_candidate_block(eth1_block, period_start)
+
+
+def get_min_new_period_epochs(spec):
+    return int(
+        spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE * 2  # to seconds
+        / spec.SECONDS_PER_SLOT / spec.SLOTS_PER_EPOCH
+    )
+
+
+def get_mock_aggregate(spec):
+    return spec.Attestation(
+        data=spec.AttestationData(
+            slot=10,
+        )
+    )
+
+
+#
+# Becoming a validator
+#
+
+
+@with_all_phases
+@spec_state_test
+def test_check_if_validator_active(spec, state):
+    active_validator_index = len(state.validators) - 1
+    assert spec.check_if_validator_active(state, active_validator_index)
+    new_validator_index = len(state.validators)
+    amount = spec.MAX_EFFECTIVE_BALANCE
+    deposit = prepare_state_and_deposit(spec, state, new_validator_index, amount, signed=True)
+    spec.process_deposit(state, deposit)
+    assert not spec.check_if_validator_active(state, new_validator_index)
+
+
+#
+# Validator assignments
+#
+
+
+@with_all_phases
+@spec_state_test
+def test_get_committee_assignment_current_epoch(spec, state):
+    epoch = spec.get_current_epoch(state)
+    validator_index = len(state.validators) - 1
+    run_get_committee_assignment(spec, state, epoch, validator_index, valid=True)
+
+
+@with_all_phases
+@spec_state_test
+def test_get_committee_assignment_next_epoch(spec, state):
+    epoch = spec.get_current_epoch(state) + 1
+    validator_index = len(state.validators) - 1
+    run_get_committee_assignment(spec, state, epoch, validator_index, valid=True)
+
+
+@with_all_phases
+@spec_state_test
+def test_get_committee_assignment_out_bound_epoch(spec, state):
+    epoch = spec.get_current_epoch(state) + 2
+    validator_index = len(state.validators) - 1
+    run_get_committee_assignment(spec, state, epoch, validator_index, valid=False)
+
+
+@with_all_phases
+@spec_state_test
+def test_is_proposer(spec, state):
+    proposer_index = spec.get_beacon_proposer_index(state)
+    assert spec.is_proposer(state, proposer_index)
+
+    proposer_index = proposer_index + 1 % len(state.validators)
+    assert not spec.is_proposer(state, proposer_index)
+
+
+#
+# Beacon chain responsibilities
+#
+
+
+# Block proposal
+
+
+@with_all_phases
+@spec_state_test
+def test_get_epoch_signature(spec, state):
+    block = spec.BeaconBlock()
+    privkey = privkeys[0]
+    pubkey = pubkeys[0]
+    domain = spec.get_domain(state, spec.DOMAIN_RANDAO, spec.compute_epoch_at_slot(block.slot))
+    run_get_signature_test(
+        spec=spec,
+        state=state,
+        obj=block,
+        domain=domain,
+        get_signature_fn=spec.get_epoch_signature,
+        privkey=privkey,
+        pubkey=pubkey,
+        signing_ssz_object=spec.compute_epoch_at_slot(block.slot),
+    )
+
+
+@with_all_phases
+@spec_state_test
+def test_is_candidate_block(spec, state):
+    period_start = spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE * 2 + 1000
+    run_is_candidate_block(
+        spec,
+        spec.Eth1Block(timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE),
+        period_start,
+        success=True,
+    )
+    run_is_candidate_block(
+        spec,
+        spec.Eth1Block(timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE + 1),
+        period_start,
+        success=False,
+    )
+    run_is_candidate_block(
+        spec,
+        spec.Eth1Block(timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE * 2),
+        period_start,
+        success=True,
+    )
+    run_is_candidate_block(
+        spec,
+        spec.Eth1Block(timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE * 2 - 1),
+        period_start,
+        success=False,
+    )
+
+
+@with_all_phases
+@spec_state_test
+def test_get_eth1_vote_default_vote(spec, state):
+    min_new_period_epochs = get_min_new_period_epochs(spec)
+    for _ in range(min_new_period_epochs):
+        next_epoch(spec, state)
+
+    state.eth1_data_votes = ()
+    eth1_chain = []
+    eth1_data = spec.get_eth1_vote(state, eth1_chain)
+    assert eth1_data == state.eth1_data
+
+
+@with_all_phases
+@spec_state_test
+def test_get_eth1_vote_consensus_vote(spec, state):
+    min_new_period_epochs = get_min_new_period_epochs(spec)
+    for _ in range(min_new_period_epochs + 2):
+        next_epoch(spec, state)
+
+    period_start = spec.voting_period_start_time(state)
+    votes_length = spec.get_current_epoch(state) % spec.EPOCHS_PER_ETH1_VOTING_PERIOD
+    assert votes_length >= 3  # We need to have the majority vote
+    state.eth1_data_votes = ()
+
+    block_1 = spec.Eth1Block(
+        timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE - 1,
+        deposit_count=state.eth1_data.deposit_count,
+        deposit_root=b'\x04' * 32,
+    )
+    block_2 = spec.Eth1Block(
+        timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE,
+        deposit_count=state.eth1_data.deposit_count + 1,
+        deposit_root=b'\x05' * 32,
+    )
+    eth1_chain = [block_1, block_2]
+    eth1_data_votes = []
+
+    # Only the first vote is for block_1
+    eth1_data_votes.append(spec.get_eth1_data(block_1))
+    # Other votes are for block_2
+    for _ in range(votes_length - 1):
+        eth1_data_votes.append(spec.get_eth1_data(block_2))
+
+    state.eth1_data_votes = eth1_data_votes
+    eth1_data = spec.get_eth1_vote(state, eth1_chain)
+    assert eth1_data.block_hash == block_2.hash_tree_root()
+
+
+@with_all_phases
+@spec_state_test
+def test_get_eth1_vote_tie(spec, state):
+    min_new_period_epochs = get_min_new_period_epochs(spec)
+    for _ in range(min_new_period_epochs + 1):
+        next_epoch(spec, state)
+
+    period_start = spec.voting_period_start_time(state)
+    votes_length = spec.get_current_epoch(state) % spec.EPOCHS_PER_ETH1_VOTING_PERIOD
+    assert votes_length > 0 and votes_length % 2 == 0
+
+    state.eth1_data_votes = ()
+    block_1 = spec.Eth1Block(
+        timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE - 1,
+        deposit_count=state.eth1_data.deposit_count,
+        deposit_root=b'\x04' * 32,
+    )
+    block_2 = spec.Eth1Block(
+        timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE,
+        deposit_count=state.eth1_data.deposit_count + 1,
+        deposit_root=b'\x05' * 32,
+    )
+    eth1_chain = [block_1, block_2]
+    eth1_data_votes = []
+    # Half votes are for block_1, another half votes are for block_2
+    for i in range(votes_length):
+        if i % 2 == 0:
+            block = block_1
+        else:
+            block = block_2
+        eth1_data_votes.append(spec.get_eth1_data(block))
+
+    state.eth1_data_votes = eth1_data_votes
+    eth1_data = spec.get_eth1_vote(state, eth1_chain)
+
+    # Tiebreak by smallest distance -> eth1_chain[0]
+    assert eth1_data.block_hash == eth1_chain[0].hash_tree_root()
+
+
+@with_all_phases
+@spec_state_test
+def test_get_eth1_vote_chain_in_past(spec, state):
+    min_new_period_epochs = get_min_new_period_epochs(spec)
+    for _ in range(min_new_period_epochs + 1):
+        next_epoch(spec, state)
+
+    period_start = spec.voting_period_start_time(state)
+    votes_length = spec.get_current_epoch(state) % spec.EPOCHS_PER_ETH1_VOTING_PERIOD
+    assert votes_length > 0 and votes_length % 2 == 0
+
+    state.eth1_data_votes = ()
+    block_1 = spec.Eth1Block(
+        timestamp=period_start - spec.SECONDS_PER_ETH1_BLOCK * spec.ETH1_FOLLOW_DISTANCE,
+        deposit_count=state.eth1_data.deposit_count - 1,  # Chain prior to current eth1data
+        deposit_root=b'\x42' * 32,
+    )
+    eth1_chain = [block_1]
+    eth1_data_votes = []
+
+    state.eth1_data_votes = eth1_data_votes
+    eth1_data = spec.get_eth1_vote(state, eth1_chain)
+
+    # Should be default vote
+    assert eth1_data == state.eth1_data
+
+
+@with_all_phases
+@spec_state_test
+def test_compute_new_state_root(spec, state):
+    pre_state = state.copy()
+    post_state = state.copy()
+    block = build_empty_block(spec, state, state.slot + 1)
+    state_root = spec.compute_new_state_root(state, block)
+
+    assert state_root != pre_state.hash_tree_root()
+    assert state == pre_state
+
+    # dumb verification
+    spec.process_slots(post_state, block.slot)
+    spec.process_block(post_state, block)
+    assert state_root == post_state.hash_tree_root()
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_get_block_signature(spec, state):
+    privkey = privkeys[0]
+    pubkey = pubkeys[0]
+    block = build_empty_block(spec, state)
+    domain = spec.get_domain(state, spec.DOMAIN_BEACON_PROPOSER, spec.compute_epoch_at_slot(block.slot))
+    run_get_signature_test(
+        spec=spec,
+        state=state,
+        obj=block,
+        domain=domain,
+        get_signature_fn=spec.get_block_signature,
+        privkey=privkey,
+        pubkey=pubkey,
+    )
+
+
+# Attesting
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_get_attestation_signature(spec, state):
+    privkey = privkeys[0]
+    pubkey = pubkeys[0]
+    attestation_data = spec.AttestationData(slot=10)
+    domain = spec.get_domain(state, spec.DOMAIN_BEACON_ATTESTER, attestation_data.target.epoch)
+    run_get_signature_test(
+        spec=spec,
+        state=state,
+        obj=attestation_data,
+        domain=domain,
+        get_signature_fn=spec.get_attestation_signature,
+        privkey=privkey,
+        pubkey=pubkey,
+    )
+
+
+# Attestation aggregation
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_get_slot_signature(spec, state):
+    privkey = privkeys[0]
+    pubkey = pubkeys[0]
+    slot = spec.Slot(10)
+    domain = spec.get_domain(state, spec.DOMAIN_SELECTION_PROOF, spec.compute_epoch_at_slot(slot))
+    run_get_signature_test(
+        spec=spec,
+        state=state,
+        obj=slot,
+        domain=domain,
+        get_signature_fn=spec.get_slot_signature,
+        privkey=privkey,
+        pubkey=pubkey,
+    )
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_is_aggregator(spec, state):
+    # TODO: we can test the probabilistic result against `TARGET_AGGREGATORS_PER_COMMITTEE`
+    #  if we have more validators and larger committeee size
+    slot = state.slot
+    committee_index = 0
+    has_aggregator = False
+    beacon_committee = spec.get_beacon_committee(state, slot, committee_index)
+    for validator_index in beacon_committee:
+        privkey = privkeys[validator_index]
+        slot_signature = spec.get_slot_signature(state, slot, privkey)
+        if spec.is_aggregator(state, slot, committee_index, slot_signature):
+            has_aggregator = True
+            break
+    assert has_aggregator
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_get_aggregate_signature(spec, state):
+    attestations = []
+    attesting_pubkeys = []
+    slot = state.slot
+    committee_index = 0
+    attestation_data = build_attestation_data(spec, state, slot=slot, index=committee_index)
+    beacon_committee = spec.get_beacon_committee(
+        state,
+        attestation_data.slot,
+        attestation_data.index,
+    )
+    committee_size = len(beacon_committee)
+    aggregation_bits = Bitlist[spec.MAX_VALIDATORS_PER_COMMITTEE](*([0] * committee_size))
+    for i, validator_index in enumerate(beacon_committee):
+        bits = aggregation_bits.copy()
+        bits[i] = True
+        attestations.append(
+            spec.Attestation(
+                data=attestation_data,
+                aggregation_bits=bits,
+                signature=spec.get_attestation_signature(state, attestation_data, privkeys[validator_index]),
+            )
+        )
+        attesting_pubkeys.append(state.validators[validator_index].pubkey)
+    assert len(attestations) > 0
+    signature = spec.get_aggregate_signature(attestations)
+    domain = spec.get_domain(state, spec.DOMAIN_BEACON_ATTESTER, attestation_data.target.epoch)
+    signing_root = spec.compute_signing_root(attestation_data, domain)
+    assert bls.FastAggregateVerify(attesting_pubkeys, signing_root, signature)
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_get_aggregate_and_proof(spec, state):
+    privkey = privkeys[0]
+    aggregator_index = spec.ValidatorIndex(10)
+    aggregate = get_mock_aggregate(spec)
+    aggregate_and_proof = spec.get_aggregate_and_proof(state, aggregator_index, aggregate, privkey)
+    assert aggregate_and_proof.aggregator_index == aggregator_index
+    assert aggregate_and_proof.aggregate == aggregate
+    assert aggregate_and_proof.selection_proof == spec.get_slot_signature(state, aggregate.data.slot, privkey)
+
+
+@with_all_phases
+@spec_state_test
+@always_bls
+def test_get_aggregate_and_proof_signature(spec, state):
+    privkey = privkeys[0]
+    pubkey = pubkeys[0]
+    aggregate = get_mock_aggregate(spec)
+    aggregate_and_proof = spec.get_aggregate_and_proof(state, spec.ValidatorIndex(1), aggregate, privkey)
+    domain = spec.get_domain(state, spec.DOMAIN_AGGREGATE_AND_PROOF, spec.compute_epoch_at_slot(aggregate.data.slot))
+    run_get_signature_test(
+        spec=spec,
+        state=state,
+        obj=aggregate_and_proof,
+        domain=domain,
+        get_signature_fn=spec.get_aggregate_and_proof_signature,
+        privkey=privkey,
+        pubkey=pubkey,
+    )
diff --git a/tests/core/pyspec/eth2spec/utils/bls.py b/tests/core/pyspec/eth2spec/utils/bls.py
--- a/tests/core/pyspec/eth2spec/utils/bls.py
+++ b/tests/core/pyspec/eth2spec/utils/bls.py
@@ -1,12 +1,17 @@
-from py_ecc.bls import G2ProofOfPossession as bls
+from py_ecc.bls import G2ProofOfPossession as py_ecc_bls
 from py_ecc.bls.g2_primatives import signature_to_G2 as _signature_to_G2
+import milagro_bls_binding as milagro_bls  # noqa: F401 for BLS switching option
 
 # Flag to make BLS active or not. Used for testing, do not ignore BLS in production unless you know what you are doing.
 bls_active = True
 
+# To change bls implementation, default to PyECC for correctness. Milagro is a good faster alternative.
+bls = py_ecc_bls
+
 STUB_SIGNATURE = b'\x11' * 96
 STUB_PUBKEY = b'\x22' * 48
-STUB_COORDINATES = _signature_to_G2(bls.Sign(0, b""))
+Z2_SIGNATURE = b'\xc0' + b'\x00' * 95
+STUB_COORDINATES = _signature_to_G2(Z2_SIGNATURE)
 
 
 def only_with_bls(alt_return=None):
@@ -25,17 +30,32 @@ def entry(*args, **kw):
 
 @only_with_bls(alt_return=True)
 def Verify(PK, message, signature):
-    return bls.Verify(PK, message, signature)
+    try:
+        result = bls.Verify(PK, message, signature)
+    except Exception:
+        result = False
+    finally:
+        return result
 
 
 @only_with_bls(alt_return=True)
-def AggregateVerify(pairs, signature):
-    return bls.AggregateVerify(pairs, signature)
+def AggregateVerify(pubkeys, messages, signature):
+    try:
+        result = bls.AggregateVerify(list(pubkeys), list(messages), signature)
+    except Exception:
+        result = False
+    finally:
+        return result
 
 
 @only_with_bls(alt_return=True)
-def FastAggregateVerify(PKs, message, signature):
-    return bls.FastAggregateVerify(PKs, message, signature)
+def FastAggregateVerify(pubkeys, message, signature):
+    try:
+        result = bls.FastAggregateVerify(list(pubkeys), message, signature)
+    except Exception:
+        result = False
+    finally:
+        return result
 
 
 @only_with_bls(alt_return=STUB_SIGNATURE)
@@ -45,9 +65,22 @@ def Aggregate(signatures):
 
 @only_with_bls(alt_return=STUB_SIGNATURE)
 def Sign(SK, message):
-    return bls.Sign(SK, message)
+    if bls == py_ecc_bls:
+        return bls.Sign(SK, message)
+    else:
+        return bls.Sign(SK.to_bytes(32, 'big'), message)
 
 
 @only_with_bls(alt_return=STUB_COORDINATES)
 def signature_to_G2(signature):
     return _signature_to_G2(signature)
+
+
+@only_with_bls(alt_return=STUB_PUBKEY)
+def AggregatePKs(pubkeys):
+    return bls._AggregatePKs(list(pubkeys))
+
+
+@only_with_bls(alt_return=STUB_SIGNATURE)
+def SkToPk(SK):
+    return bls.SkToPk(SK)
diff --git a/tests/core/pyspec/eth2spec/utils/hash_function.py b/tests/core/pyspec/eth2spec/utils/hash_function.py
--- a/tests/core/pyspec/eth2spec/utils/hash_function.py
+++ b/tests/core/pyspec/eth2spec/utils/hash_function.py
@@ -1,28 +1,17 @@
 from hashlib import sha256
+from typing import Dict, Union
 
 ZERO_BYTES32 = b'\x00' * 32
 
 
-def _hash(x):
+def _hash(x: Union[bytes, bytearray, memoryview]) -> bytes:
     return sha256(x).digest()
 
 
-# Minimal collection of (key, value) pairs, for fast hash-retrieval, to save on repetitive computation cost.
-# Key = the hash input
-# Value = the hash output
-hash_cache = []
+hash_cache: Dict[bytes, bytes] = {}
 
 
-def add_zero_hashes_to_cache():
-    zerohashes = [(None, ZERO_BYTES32)]
-    for layer in range(1, 32):
-        k = zerohashes[layer - 1][1] + zerohashes[layer - 1][1]
-        zerohashes.append((k, _hash(k)))
-    hash_cache.extend(zerohashes[1:])
-
-
-def hash(x):
-    for (k, h) in hash_cache:
-        if x == k:
-            return h
+def hash(x: bytes) -> bytes:
+    if x in hash_cache:
+        return hash_cache[x]
     return _hash(x)
diff --git a/tests/formats/bls/README.md b/tests/formats/bls/README.md
--- a/tests/formats/bls/README.md
+++ b/tests/formats/bls/README.md
@@ -5,11 +5,10 @@ We do not recommend rolling your own crypto or using an untested BLS library.
 
 The BLS test suite runner has the following handlers:
 
-- [`aggregate_pubkeys`](./aggregate_pubkeys.md)
-- [`aggregate_sigs`](./aggregate_sigs.md)
-- [`msg_hash_g2_compressed`](./msg_hash_g2_compressed.md)
-- [`msg_hash_g2_uncompressed`](./msg_hash_g2_uncompressed.md)
-- [`priv_to_pub`](./priv_to_pub.md)
-- [`sign_msg`](./sign_msg.md)
+- [`aggregate_verify`](./aggregate_verify.md)
+- [`aggregate`](./aggregate.md)
+- [`fast_aggregate_verify`](./fast_aggregate_verify.md)
+- [`sign`](./sign.md)
+- [`verify`](./verify.md)
 
 *Note*: Signature-verification and aggregate-verify test cases are not yet supported.
diff --git a/tests/formats/bls/aggregate.md b/tests/formats/bls/aggregate.md
new file mode 100644
--- /dev/null
+++ b/tests/formats/bls/aggregate.md
@@ -0,0 +1,19 @@
+# Test format: BLS signature aggregation
+
+A BLS signature aggregation combines a series of signatures into a single signature.
+
+## Test case format
+
+The test data is declared in a `data.yaml` file:
+
+```yaml
+input: List[BLS Signature] -- list of input BLS signatures
+output: BLS Signature -- expected output, single BLS signature or empty.
+```
+
+- `BLS Signature` here is encoded as a string: hexadecimal encoding of 96 bytes (192 nibbles), prefixed with `0x`.
+- No output value if the input is invalid.
+
+## Condition
+
+The `aggregate` handler should aggregate the signatures in the `input`, and the result should match the expected `output`.
diff --git a/tests/formats/bls/aggregate_pubkeys.md b/tests/formats/bls/aggregate_pubkeys.md
deleted file mode 100644
--- a/tests/formats/bls/aggregate_pubkeys.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Test format: BLS pubkey aggregation
-
-A BLS pubkey aggregation combines a series of pubkeys into a single pubkey.
-
-## Test case format
-
-The test data is declared in a `data.yaml` file:
-
-```yaml
-input: List[BLS Pubkey] -- list of input BLS pubkeys
-output: BLS Pubkey -- expected output, single BLS pubkey
-```
-
-`BLS Pubkey` here is encoded as a string: hexadecimal encoding of 48 bytes (96 nibbles), prefixed with `0x`.
-
-
-## Condition
-
-The `aggregate_pubkeys` handler should aggregate the keys in the `input`, and the result should match the expected `output`.
diff --git a/tests/formats/bls/aggregate_sigs.md b/tests/formats/bls/aggregate_sigs.md
deleted file mode 100644
--- a/tests/formats/bls/aggregate_sigs.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Test format: BLS signature aggregation
-
-A BLS signature aggregation combines a series of signatures into a single signature.
-
-## Test case format
-
-The test data is declared in a `data.yaml` file:
-
-```yaml
-input: List[BLS Signature] -- list of input BLS signatures
-output: BLS Signature -- expected output, single BLS signature
-```
-
-`BLS Signature` here is encoded as a string: hexadecimal encoding of 96 bytes (192 nibbles), prefixed with `0x`.
-
-
-## Condition
-
-The `aggregate_sigs` handler should aggregate the signatures in the `input`, and the result should match the expected `output`.
diff --git a/tests/formats/bls/aggregate_verify.md b/tests/formats/bls/aggregate_verify.md
new file mode 100644
--- /dev/null
+++ b/tests/formats/bls/aggregate_verify.md
@@ -0,0 +1,17 @@
+# Test format: BLS sign message
+
+Verify the signature against the given pubkeys and one messages.
+
+## Test case format
+
+The test data is declared in a `data.yaml` file:
+
+```yaml
+input:
+  pubkeys: List[bytes48] -- the pubkeys
+  messages: List[bytes32] -- the messages
+  signature: bytes96 -- the signature to verify against pubkeys and messages
+output: bool  -- VALID or INVALID
+```
+
+All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
diff --git a/tests/formats/bls/fast_aggregate_verify.md b/tests/formats/bls/fast_aggregate_verify.md
new file mode 100644
--- /dev/null
+++ b/tests/formats/bls/fast_aggregate_verify.md
@@ -0,0 +1,17 @@
+# Test format: BLS sign message
+
+Verify the signature against the given pubkeys and one message.
+
+## Test case format
+
+The test data is declared in a `data.yaml` file:
+
+```yaml
+input:
+  pubkeys: List[bytes48] -- the pubkey
+  message: bytes32 -- the message
+  signature: bytes96 -- the signature to verify against pubkeys and message
+output: bool  -- VALID or INVALID
+```
+
+All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
diff --git a/tests/formats/bls/msg_hash_g2_compressed.md b/tests/formats/bls/msg_hash_g2_compressed.md
deleted file mode 100644
--- a/tests/formats/bls/msg_hash_g2_compressed.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Test format: BLS hash-compressed
-
-A BLS compressed-hash to G2.
-
-## Test case format
-
-The test data is declared in a `data.yaml` file:
-
-```yaml
-input: 
-  message: bytes32
-  domain: bytes8   -- the BLS domain
-output: List[bytes48] -- length of two
-```
-
-All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
-
-
-## Condition
-
-The `msg_hash_g2_compressed` handler should hash the `message`, with the given `domain`, to G2 with compression, and the result should match the expected `output`.
diff --git a/tests/formats/bls/msg_hash_g2_uncompressed.md b/tests/formats/bls/msg_hash_g2_uncompressed.md
deleted file mode 100644
--- a/tests/formats/bls/msg_hash_g2_uncompressed.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Test format: BLS hash-uncompressed
-
-A BLS uncompressed-hash to G2.
-
-## Test case format
-
-The test data is declared in a `data.yaml` file:
-
-```yaml
-input:
-  message: bytes32
-  domain: bytes8   -- the BLS domain
-output: List[List[bytes48]] -- 3 lists, each a length of two
-```
-
-All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
-
-
-## Condition
-
-The `msg_hash_g2_uncompressed` handler should hash the `message`, with the given `domain`, to G2, without compression, and the result should match the expected `output`.
diff --git a/tests/formats/bls/priv_to_pub.md b/tests/formats/bls/priv_to_pub.md
deleted file mode 100644
--- a/tests/formats/bls/priv_to_pub.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Test format: BLS private key to pubkey
-
-A BLS private key to public key conversion.
-
-## Test case format
-
-The test data is declared in a `data.yaml` file:
-
-```yaml
-input: bytes32 -- the private key
-output: bytes48 -- the public key
-```
-
-All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
-
-
-## Condition
-
-The `priv_to_pub` handler should compute the public key for the given private key `input`, and the result should match the expected `output`.
diff --git a/tests/formats/bls/sign_msg.md b/tests/formats/bls/sign.md
similarity index 67%
rename from tests/formats/bls/sign_msg.md
rename to tests/formats/bls/sign.md
--- a/tests/formats/bls/sign_msg.md
+++ b/tests/formats/bls/sign.md
@@ -10,13 +10,7 @@ The test data is declared in a `data.yaml` file:
 input:
   privkey: bytes32 -- the private key used for signing
   message: bytes32 -- input message to sign (a hash)
-  domain: bytes8   -- the BLS domain
 output: bytes96    -- expected signature
 ```
 
 All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
-
-
-## Condition
-
-The `sign_msg` handler should sign the given `message`, with `domain`, using the given `privkey`, and the result should match the expected `output`.
diff --git a/tests/formats/bls/verify.md b/tests/formats/bls/verify.md
new file mode 100644
--- /dev/null
+++ b/tests/formats/bls/verify.md
@@ -0,0 +1,17 @@
+# Test format: BLS sign message
+
+Verify the signature against the given one pubkey and one message.
+
+## Test case format
+
+The test data is declared in a `data.yaml` file:
+
+```yaml
+input:
+  pubkey: bytes48 -- the pubkey
+  message: bytes32 -- the message
+  signature: bytes96 -- the signature to verify against pubkey and message
+output: bool  -- VALID or INVALID
+```
+
+All byte(s) fields are encoded as strings, hexadecimal encoding, prefixed with `0x`.
diff --git a/tests/formats/epoch_processing/README.md b/tests/formats/epoch_processing/README.md
--- a/tests/formats/epoch_processing/README.md
+++ b/tests/formats/epoch_processing/README.md
@@ -38,7 +38,7 @@ The provided pre-state is already transitioned to just before the specific sub-t
 Sub-transitions:
 
 - `justification_and_finalization`
-- *`rewards_and_penalties` - planned testing extension*
+- `rewards_and_penalties` (limited to `minimal` config)
 - `registry_updates`
 - `slashings`
 - `final_updates`
diff --git a/tests/formats/rewards/README.md b/tests/formats/rewards/README.md
new file mode 100644
--- /dev/null
+++ b/tests/formats/rewards/README.md
@@ -0,0 +1,75 @@
+# Rewards tests
+
+All rewards deltas sub-functions are tested for each test case.
+There is no "change" factor, the rewards/penalties outputs are pure functions with just the pre-state as input.
+(See test condition documentation on how to run the tests.)
+
+`Deltas` is defined as:
+```python
+class Deltas(Container):
+    rewards: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
+    penalties: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
+```
+
+## Test case format
+
+### `meta.yaml`
+
+```yaml
+description: string    -- Optional description of test case, purely for debugging purposes.
+                          Tests should use the directory name of the test case as identifier, not the description.
+```
+
+_Note_: No signature verification happens within rewards sub-functions. These
+ tests can safely be run with or without BLS enabled.
+
+### `pre.yaml`
+
+A YAML-encoded `BeaconState`, the state before running the rewards sub-function.
+
+Also available as `pre.ssz`.
+
+### `source_deltas.yaml`
+
+A YAML-encoded `Deltas` representing the rewards and penalties returned by the rewards the `get_source_deltas` function
+
+Also available as `source_deltas.ssz`.
+
+### `target_deltas.yaml`
+
+A YAML-encoded `Deltas` representing the rewards and penalties returned by the rewards the `get_target_deltas` function
+
+Also available as `target_deltas.ssz`.
+
+### `head_deltas.yaml`
+
+A YAML-encoded `Deltas` representing the rewards and penalties returned by the rewards the `get_head_deltas` function
+
+Also available as `head_deltas.ssz`.
+
+### `inclusion_delay_deltas.yaml`
+
+A YAML-encoded `Deltas` representing the rewards and penalties returned by the rewards the `get_inclusion_delay_deltas` function
+
+Also available as `inclusion_delay_deltas.ssz`.
+
+### `inactivity_penalty_deltas.yaml`
+
+A YAML-encoded `Deltas` representing the rewards and penalties returned by the rewards the `get_inactivity_penalty_deltas` function
+
+Also available as `inactivity_penalty_deltas.ssz`.
+
+## Condition
+
+A handler of the `rewards` test-runner should process these cases, 
+ calling the corresponding rewards deltas function for each set of deltas.
+
+The provided pre-state is ready to be input into each rewards deltas function.
+
+The provided `deltas` should match the return values of the
+ deltas function. Specifically the following must hold true for each set of deltas:
+
+```python
+    deltas.rewards == deltas_function(state)[0]
+    deltas.penalties == deltas_function(state)[1]
+```
diff --git a/tests/generators/bls/README.md b/tests/generators/bls/README.md
--- a/tests/generators/bls/README.md
+++ b/tests/generators/bls/README.md
@@ -1,21 +1,11 @@
 # BLS Test Generator
 
-Explanation of BLS12-381 type hierarchy
-The base unit is bytes48 of which only 381 bits are used
+The [BLS Signature APIs](../../../specs/phase0/beacon-chain.md#bls-signatures)
 
-- FQ: uint381 modulo field modulus
-- FQ2: (FQ, FQ)
-- G2: (FQ2, FQ2, FQ2)
+Information on the format of the tests can be found in the [BLS test formats documentation](../../formats/bls/README.md).
 
 ## Resources
 
-- [Eth2 spec](../../../specs/phase0/beacon-chain.md#bls-signatures)
+- [IETF BLS Signature Scheme](https://datatracker.ietf.org/doc/draft-irtf-cfrg-bls-signature/)
 - [Finite Field Arithmetic](http://www.springeronline.com/sgw/cda/pageitems/document/cda_downloaddocument/0,11996,0-0-45-110359-0,00.pdf)
-- Chapter 2 of [Elliptic Curve Cryptography](http://cacr.uwaterloo.ca/ecc/). Darrel Hankerson, Alfred Menezes, and Scott Vanstone 
-- [Zcash BLS parameters](https://github.com/zkcrypto/pairing/tree/master/src/bls12_381)
-- [Trinity implementation](https://github.com/ethereum/trinity/blob/master/eth2/_utils/bls.py)
-
-## Comments
-
-Compared to Zcash, Ethereum specs always requires the compressed form (c_flag / most significant bit always set).
-Also note that pubkeys and privkeys are reversed.
+- Chapter 2 of [Elliptic Curve Cryptography](http://cacr.uwaterloo.ca/ecc/). Darrel Hankerson, Alfred Menezes, and Scott Vanstone
diff --git a/tests/generators/bls/main.py b/tests/generators/bls/main.py
--- a/tests/generators/bls/main.py
+++ b/tests/generators/bls/main.py
@@ -2,28 +2,28 @@
 BLS test vectors generator
 """
 
+from hashlib import sha256
 from typing import Tuple, Iterable, Any, Callable, Dict
 
 from eth_utils import (
     encode_hex,
     int_to_big_endian,
 )
+import milagro_bls_binding as milagro_bls
+
+from eth2spec.utils import bls
+from eth2spec.test.context import PHASE0
 from gen_base import gen_runner, gen_typing
 
-from py_ecc import bls
-from hashlib import sha256
 
-from eth2spec.test.context import PHASE0
+def to_bytes(i):
+    return i.to_bytes(32, "big")
+
 
 def hash(x):
     return sha256(x).digest()
 
 
-F2Q_COEFF_LEN = 48
-G2_COMPRESSED_Z_LEN = 48
-DST = bls.G2ProofOfPossession.DST
-
-
 def int_to_hex(n: int, byte_length: int = None) -> str:
     byte_value = int_to_big_endian(n)
     if byte_length:
@@ -49,11 +49,15 @@ def hex_to_int(x: str) -> int:
     hex_to_int('0x00000000000000000000000000000000328388aff0d4a5b7dc9205abd374e7e98f3cd9f3418edb4eafda5fb16473d216'),
 ]
 
+Z1_PUBKEY = b'\xc0' + b'\x00' * 47
+NO_SIGNATURE = b'\x00' * 96
+Z2_SIGNATURE = b'\xc0' + b'\x00' * 95
+
 
 def case01_sign():
     for privkey in PRIVKEYS:
         for message in MESSAGES:
-            sig = bls.G2ProofOfPossession.Sign(privkey, message)
+            sig = bls.Sign(privkey, message)
             identifier = f'{int_to_hex(privkey)}_{encode_hex(message)}'
             yield f'sign_case_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
                 'input': {
@@ -68,9 +72,17 @@ def case02_verify():
     for i, privkey in enumerate(PRIVKEYS):
         for message in MESSAGES:
             # Valid signature
-            signature = bls.G2ProofOfPossession.Sign(privkey, message)
-            pubkey = bls.G2ProofOfPossession.PrivToPub(privkey)
+            signature = bls.Sign(privkey, message)
+            pubkey = bls.SkToPk(privkey)
+
+            assert milagro_bls.SkToPk(to_bytes(privkey)) == pubkey
+            assert milagro_bls.Sign(to_bytes(privkey), message) == signature
+
             identifier = f'{encode_hex(pubkey)}_{encode_hex(message)}'
+
+            assert bls.Verify(pubkey, message, signature)
+            assert milagro_bls.Verify(pubkey, message, signature)
+
             yield f'verify_valid_case_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
                 'input': {
                     'pubkey': encode_hex(pubkey),
@@ -81,8 +93,10 @@ def case02_verify():
             }
 
             # Invalid signatures -- wrong pubkey
-            wrong_pubkey = bls.G2ProofOfPossession.PrivToPub(PRIVKEYS[(i + 1) % len(PRIVKEYS)])
+            wrong_pubkey = bls.SkToPk(PRIVKEYS[(i + 1) % len(PRIVKEYS)])
             identifier = f'{encode_hex(wrong_pubkey)}_{encode_hex(message)}'
+            assert not bls.Verify(wrong_pubkey, message, signature)
+            assert not milagro_bls.Verify(wrong_pubkey, message, signature)
             yield f'verify_wrong_pubkey_case_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
                 'input': {
                     'pubkey': encode_hex(wrong_pubkey),
@@ -95,6 +109,8 @@ def case02_verify():
             # Invalid signature -- tampered with signature
             tampered_signature = signature[:-4] + b'\xFF\xFF\xFF\xFF'
             identifier = f'{encode_hex(pubkey)}_{encode_hex(message)}'
+            assert not bls.Verify(pubkey, message, tampered_signature)
+            assert not milagro_bls.Verify(pubkey, message, tampered_signature)
             yield f'verify_tampered_signature_case_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
                 'input': {
                     'pubkey': encode_hex(pubkey),
@@ -104,26 +120,53 @@ def case02_verify():
                 'output': False,
             }
 
+        # Valid pubkey and signature with the point at infinity
+        assert bls.Verify(Z1_PUBKEY, message, Z2_SIGNATURE)
+        assert milagro_bls.Verify(Z1_PUBKEY, message, Z2_SIGNATURE)
+        yield f'verify_infinity_pubkey_and_infinity_signature', {
+            'input': {
+                'pubkey': encode_hex(Z1_PUBKEY),
+                'message': encode_hex(message),
+                'signature': encode_hex(Z2_SIGNATURE),
+            },
+            'output': True,
+        }
+
 
 def case03_aggregate():
     for message in MESSAGES:
-        sigs = [bls.G2ProofOfPossession.Sign(privkey, message) for privkey in PRIVKEYS]
+        sigs = [bls.Sign(privkey, message) for privkey in PRIVKEYS]
         yield f'aggregate_{encode_hex(message)}', {
             'input': [encode_hex(sig) for sig in sigs],
-            'output': encode_hex(bls.G2ProofOfPossession.Aggregate(sigs)),
+            'output': encode_hex(bls.Aggregate(sigs)),
         }
 
+    # Invalid pubkeys -- len(pubkeys) == 0
+    try:
+        bls.Aggregate([])
+    except Exception:
+        pass
+    else:
+        raise Exception("Should have been INVALID")
+
+    yield f'aggregate_na_pubkeys', {
+        'input': [],
+        'output': None,
+    }
+
 
 def case04_fast_aggregate_verify():
     for i, message in enumerate(MESSAGES):
         privkeys = PRIVKEYS[:i + 1]
-        sigs = [bls.G2ProofOfPossession.Sign(privkey, message) for privkey in privkeys]
-        aggregate_signature = bls.G2ProofOfPossession.Aggregate(sigs)
-        pubkeys = [bls.G2ProofOfPossession.PrivToPub(privkey) for privkey in privkeys]
+        sigs = [bls.Sign(privkey, message) for privkey in privkeys]
+        aggregate_signature = bls.Aggregate(sigs)
+        pubkeys = [bls.SkToPk(privkey) for privkey in privkeys]
         pubkeys_serial = [encode_hex(pubkey) for pubkey in pubkeys]
 
         # Valid signature
         identifier = f'{pubkeys_serial}_{encode_hex(message)}'
+        assert bls.FastAggregateVerify(pubkeys, message, aggregate_signature)
+        assert milagro_bls.FastAggregateVerify(pubkeys, message, aggregate_signature)
         yield f'fast_aggregate_verify_valid_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
             'input': {
                 'pubkeys': pubkeys_serial,
@@ -134,9 +177,11 @@ def case04_fast_aggregate_verify():
         }
 
         # Invalid signature -- extra pubkey
-        pubkeys_extra = pubkeys + [bls.G2ProofOfPossession.PrivToPub(PRIVKEYS[-1])]
+        pubkeys_extra = pubkeys + [bls.SkToPk(PRIVKEYS[-1])]
         pubkeys_extra_serial = [encode_hex(pubkey) for pubkey in pubkeys_extra]
         identifier = f'{pubkeys_extra_serial}_{encode_hex(message)}'
+        assert not bls.FastAggregateVerify(pubkeys_extra, message, aggregate_signature)
+        assert not milagro_bls.FastAggregateVerify(pubkeys_extra, message, aggregate_signature)
         yield f'fast_aggregate_verify_extra_pubkey_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
             'input': {
                 'pubkeys': pubkeys_extra_serial,
@@ -149,6 +194,8 @@ def case04_fast_aggregate_verify():
         # Invalid signature -- tampered with signature
         tampered_signature = aggregate_signature[:-4] + b'\xff\xff\xff\xff'
         identifier = f'{pubkeys_serial}_{encode_hex(message)}'
+        assert not bls.FastAggregateVerify(pubkeys, message, tampered_signature)
+        assert not milagro_bls.FastAggregateVerify(pubkeys, message, tampered_signature)
         yield f'fast_aggregate_verify_tampered_signature_{(hash(bytes(identifier, "utf-8"))[:8]).hex()}', {
             'input': {
                 'pubkeys': pubkeys_serial,
@@ -158,37 +205,94 @@ def case04_fast_aggregate_verify():
             'output': False,
         }
 
+    # Invalid pubkeys and signature -- len(pubkeys) == 0 and signature == Z1_SIGNATURE
+    assert not bls.FastAggregateVerify([], message, Z2_SIGNATURE)
+    assert not milagro_bls.FastAggregateVerify([], message, Z2_SIGNATURE)
+    yield f'fast_aggregate_verify_na_pubkeys_and_infinity_signature', {
+        'input': {
+            'pubkeys': [],
+            'message': encode_hex(message),
+            'signature': encode_hex(Z2_SIGNATURE),
+        },
+        'output': False,
+    }
+
+    # Invalid pubkeys and signature -- len(pubkeys) == 0 and signature == 0x00...
+    assert not bls.FastAggregateVerify([], message, NO_SIGNATURE)
+    assert not milagro_bls.FastAggregateVerify([], message, NO_SIGNATURE)
+    yield f'fast_aggregate_verify_na_pubkeys_and_na_signature', {
+        'input': {
+            'pubkeys': [],
+            'message': encode_hex(message),
+            'signature': encode_hex(NO_SIGNATURE),
+        },
+        'output': False,
+    }
+
 
 def case05_aggregate_verify():
-    pairs = []
+    pubkeys = []
+    pubkeys_serial = []
+    messages = []
+    messages_serial = []
     sigs = []
     for privkey, message in zip(PRIVKEYS, MESSAGES):
-        sig = bls.G2ProofOfPossession.Sign(privkey, message)
-        pubkey = bls.G2ProofOfPossession.PrivToPub(privkey)
-        pairs.append({
-            'pubkey': encode_hex(pubkey),
-            'message': encode_hex(message),
-        })
+        sig = bls.Sign(privkey, message)
+        pubkey = bls.SkToPk(privkey)
+        pubkeys.append(pubkey)
+        pubkeys_serial.append(encode_hex(pubkey))
+        messages.append(message)
+        messages_serial.append(encode_hex(message))
         sigs.append(sig)
 
-    aggregate_signature = bls.G2ProofOfPossession.Aggregate(sigs)
+    aggregate_signature = bls.Aggregate(sigs)
+    assert bls.AggregateVerify(pubkeys, messages, aggregate_signature)
+    assert milagro_bls.AggregateVerify(pubkeys, messages, aggregate_signature)
     yield f'aggregate_verify_valid', {
         'input': {
-            'pairs': pairs,
+            'pubkeys': pubkeys_serial,
+            'messages': messages_serial,
             'signature': encode_hex(aggregate_signature),
         },
         'output': True,
     }
 
     tampered_signature = aggregate_signature[:4] + b'\xff\xff\xff\xff'
+    assert not bls.AggregateVerify(pubkey, messages, tampered_signature)
+    assert not milagro_bls.AggregateVerify(pubkeys, messages, tampered_signature)
     yield f'aggregate_verify_tampered_signature', {
         'input': {
-            'pairs': pairs,
+            'pubkeys': pubkeys_serial,
+            'messages': messages_serial,
             'signature': encode_hex(tampered_signature),
         },
         'output': False,
     }
 
+    # Invalid pubkeys and signature -- len(pubkeys) == 0 and signature == Z1_SIGNATURE
+    assert not bls.AggregateVerify([], [], Z2_SIGNATURE)
+    assert not milagro_bls.AggregateVerify([], [], Z2_SIGNATURE)
+    yield f'aggregate_verify_na_pubkeys_and_infinity_signature', {
+        'input': {
+            'pubkeys': [],
+            'messages': [],
+            'signature': encode_hex(Z2_SIGNATURE),
+        },
+        'output': False,
+    }
+
+    # Invalid pubkeys and signature -- len(pubkeys) == 0 and signature == 0x00...
+    assert not bls.AggregateVerify([], [], NO_SIGNATURE)
+    assert not milagro_bls.AggregateVerify([], [], NO_SIGNATURE)
+    yield f'aggregate_verify_na_pubkeys_and_na_signature', {
+        'input': {
+            'pubkeys': [],
+            'messages': [],
+            'signature': encode_hex(NO_SIGNATURE),
+        },
+        'output': False,
+    }
+
 
 def create_provider(handler_name: str,
                     test_case_fn: Callable[[], Iterable[Tuple[str, Dict[str, Any]]]]) -> gen_typing.TestProvider:
diff --git a/tests/generators/bls/requirements.txt b/tests/generators/bls/requirements.txt
--- a/tests/generators/bls/requirements.txt
+++ b/tests/generators/bls/requirements.txt
@@ -1,4 +1,4 @@
-py_ecc==2.0.0
+py_ecc==4.0.0
 eth-utils==1.6.0
 ../../core/gen_helpers
 ../../../
diff --git a/tests/generators/rewards/README.md b/tests/generators/rewards/README.md
new file mode 100644
--- /dev/null
+++ b/tests/generators/rewards/README.md
@@ -0,0 +1,8 @@
+# Rewards
+
+Rewards covers the sub-functions of `process_rewards_and_penalties` for granular testing of components of the rewards function.
+
+A rewards test-runner can consume these sub-transition test-suites,
+ and handle different kinds of epoch sub-transitions by processing the cases using the specified test handler.
+
+Information on the format of the tests can be found in the [rewards test formats documentation](../../formats/rewards/README.md).
diff --git a/tests/generators/rewards/main.py b/tests/generators/rewards/main.py
new file mode 100644
--- /dev/null
+++ b/tests/generators/rewards/main.py
@@ -0,0 +1,44 @@
+from typing import Iterable
+
+from eth2spec.phase0 import spec as spec_phase0
+from eth2spec.phase1 import spec as spec_phase1
+from eth2spec.test.phase_0.rewards import (
+    test_basic,
+    test_leak,
+    test_random,
+)
+from gen_base import gen_runner, gen_typing
+from gen_from_tests.gen import generate_from_tests
+from importlib import reload
+from eth2spec.config import config_util
+from eth2spec.test.context import PHASE0
+
+
+def create_provider(tests_src, config_name: str) -> gen_typing.TestProvider:
+
+    def prepare_fn(configs_path: str) -> str:
+        config_util.prepare_config(configs_path, config_name)
+        reload(spec_phase0)
+        reload(spec_phase1)
+        return config_name
+
+    def cases_fn() -> Iterable[gen_typing.TestCase]:
+        return generate_from_tests(
+            runner_name='rewards',
+            handler_name='core',
+            src=tests_src,
+            fork_name=PHASE0,
+        )
+
+    return gen_typing.TestProvider(prepare=prepare_fn, make_cases=cases_fn)
+
+
+if __name__ == "__main__":
+    gen_runner.run_generator("rewards", [
+        create_provider(test_basic, 'minimal'),
+        create_provider(test_basic, 'mainnet'),
+        create_provider(test_leak, 'minimal'),
+        create_provider(test_leak, 'mainnet'),
+        create_provider(test_random, 'minimal'),
+        create_provider(test_random, 'mainnet'),
+    ])
diff --git a/tests/generators/rewards/requirements.txt b/tests/generators/rewards/requirements.txt
new file mode 100644
--- /dev/null
+++ b/tests/generators/rewards/requirements.txt
@@ -0,0 +1,2 @@
+../../core/gen_helpers
+../../../
\ No newline at end of file
diff --git a/tests/generators/sanity/main.py b/tests/generators/sanity/main.py
--- a/tests/generators/sanity/main.py
+++ b/tests/generators/sanity/main.py
@@ -5,7 +5,7 @@
 from gen_from_tests.gen import generate_from_tests
 
 from eth2spec.test.context import PHASE0
-from eth2spec.test.sanity import test_blocks, test_slots
+from eth2spec.test.phase_0.sanity import test_blocks, test_slots
 from eth2spec.config import config_util
 from eth2spec.phase0 import spec as spec_phase0
 from eth2spec.phase1 import spec as spec_phase1