From 27507fb3e220f41e640eba19fd2e152a23af2986 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 19 Feb 2021 12:34:12 +0800 Subject: [PATCH] Add `get_head` test vectors --- .../eth2spec/test/helpers/fork_choice.py | 31 +- .../test/phase0/fork_choice/__init__.py | 0 .../test/phase0/fork_choice/test_get_head.py | 288 ++++++++++++++++++ tests/generators/fork_choice/README.md | 3 + tests/generators/fork_choice/main.py | 40 +++ tests/generators/fork_choice/requirements.txt | 2 + 6 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/phase0/fork_choice/__init__.py create mode 100644 tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py create mode 100644 tests/generators/fork_choice/README.md create mode 100644 tests/generators/fork_choice/main.py create mode 100644 tests/generators/fork_choice/requirements.txt diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index 85437e98a..040bee975 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -1,3 +1,5 @@ +from eth_utils import encode_hex + from eth2spec.phase0 import spec as phase0_spec @@ -8,17 +10,25 @@ def get_anchor_root(spec, state): return spec.hash_tree_root(anchor_block_header) -def add_block_to_store(spec, store, signed_block): +def add_block_to_store(spec, store, signed_block, test_steps=None): + if test_steps is None: + test_steps = [] + pre_state = store.block_states[signed_block.message.parent_root] block_time = pre_state.genesis_time + signed_block.message.slot * spec.SECONDS_PER_SLOT if store.time < block_time: spec.on_tick(store, block_time) + test_steps.append({'tick': int(block_time)}) spec.on_block(store, signed_block) + test_steps.append({'block': get_block_file_name(signed_block)}) -def add_attestation_to_store(spec, store, attestation): +def add_attestation_to_store(spec, store, attestation, test_steps=None): + if test_steps is None: + test_steps = [] + parent_block = store.blocks[attestation.data.beacon_block_root] pre_state = store.block_states[spec.hash_tree_root(parent_block)] block_time = pre_state.genesis_time + parent_block.slot * spec.SECONDS_PER_SLOT @@ -26,12 +36,27 @@ def add_attestation_to_store(spec, store, attestation): if store.time < next_epoch_time: spec.on_tick(store, next_epoch_time) + test_steps.append({'tick': int(next_epoch_time)}) spec.on_attestation(store, attestation) + test_steps.append({'attestation': get_attestation_file_name(attestation)}) def get_genesis_forkchoice_store(spec, genesis_state): + store, _ = get_genesis_forkchoice_store_and_block(spec, genesis_state) + return store + + +def get_genesis_forkchoice_store_and_block(spec, genesis_state): assert genesis_state.slot == spec.GENESIS_SLOT # The genesis block must be a Phase 0 `BeaconBlock` genesis_block = phase0_spec.BeaconBlock(state_root=genesis_state.hash_tree_root()) - return spec.get_forkchoice_store(genesis_state, genesis_block) + return spec.get_forkchoice_store(genesis_state, genesis_block), genesis_block + + +def get_block_file_name(block): + return f"block_{encode_hex(block.hash_tree_root())}" + + +def get_attestation_file_name(attestation): + return f"attestation_{encode_hex(attestation.hash_tree_root())}" diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/__init__.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py new file mode 100644 index 000000000..6648000fa --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -0,0 +1,288 @@ +from eth_utils import encode_hex + +from eth2spec.test.context import MINIMAL, with_all_phases, with_configs, spec_state_test +from eth2spec.test.helpers.attestations import get_valid_attestation, next_epoch_with_attestations +from eth2spec.test.helpers.block import build_empty_block_for_next_slot +from eth2spec.test.helpers.fork_choice import ( + add_attestation_to_store, + add_block_to_store, get_anchor_root, + get_genesis_forkchoice_store_and_block, + get_attestation_file_name, + get_block_file_name, +) +from eth2spec.test.helpers.state import ( + next_epoch, + state_transition_and_sign_block, +) + + +@with_all_phases +@with_configs([MINIMAL]) +@spec_state_test +def test_genesis(spec, state): + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + + anchor_root = get_anchor_root(spec, state) + head = spec.get_head(store) + assert head == anchor_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + yield 'steps', test_steps + + +@with_all_phases +@with_configs([MINIMAL]) +@spec_state_test +def test_chain_no_attestations(spec, state): + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + + anchor_root = get_anchor_root(spec, state) + head = spec.get_head(store) + assert head == anchor_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + # On receiving a block of `GENESIS_SLOT + 1` slot + block_1 = build_empty_block_for_next_slot(spec, state) + signed_block_1 = state_transition_and_sign_block(spec, state, block_1) + add_block_to_store(spec, store, signed_block_1, test_steps) + yield get_block_file_name(signed_block_1), signed_block_1 + + # On receiving a block of next epoch + block_2 = build_empty_block_for_next_slot(spec, state) + signed_block_2 = state_transition_and_sign_block(spec, state, block_2) + add_block_to_store(spec, store, signed_block_2, test_steps) + yield get_block_file_name(signed_block_2), signed_block_2 + + head = spec.get_head(store) + assert head == spec.hash_tree_root(block_2) + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + yield 'steps', test_steps + + +@with_all_phases +@with_configs([MINIMAL]) +@spec_state_test +def test_split_tie_breaker_no_attestations(spec, state): + test_steps = [] + genesis_state = state.copy() + + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + anchor_root = get_anchor_root(spec, state) + head = spec.get_head(store) + assert head == anchor_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + # block at slot 1 + block_1_state = genesis_state.copy() + block_1 = build_empty_block_for_next_slot(spec, block_1_state) + signed_block_1 = state_transition_and_sign_block(spec, block_1_state, block_1) + add_block_to_store(spec, store, signed_block_1, test_steps) + yield get_block_file_name(signed_block_1), signed_block_1 + + # additional block at slot 1 + block_2_state = genesis_state.copy() + block_2 = build_empty_block_for_next_slot(spec, block_2_state) + block_2.body.graffiti = b'\x42' * 32 + signed_block_2 = state_transition_and_sign_block(spec, block_2_state, block_2) + add_block_to_store(spec, store, signed_block_2, test_steps) + yield get_block_file_name(signed_block_2), signed_block_2 + + highest_root = max(spec.hash_tree_root(block_1), spec.hash_tree_root(block_2)) + head = spec.get_head(store) + assert head == highest_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_shorter_chain_but_heavier_weight(spec, state): + test_steps = [] + genesis_state = state.copy() + + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + anchor_root = get_anchor_root(spec, state) + head = spec.get_head(store) + assert head == anchor_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + # build longer tree + long_state = genesis_state.copy() + for _ in range(3): + long_block = build_empty_block_for_next_slot(spec, long_state) + signed_long_block = state_transition_and_sign_block(spec, long_state, long_block) + add_block_to_store(spec, store, signed_long_block, test_steps) + yield get_block_file_name(signed_long_block), signed_long_block + + # build short tree + short_state = genesis_state.copy() + short_block = build_empty_block_for_next_slot(spec, short_state) + short_block.body.graffiti = b'\x42' * 32 + signed_short_block = state_transition_and_sign_block(spec, short_state, short_block) + add_block_to_store(spec, store, signed_short_block, test_steps) + yield get_block_file_name(signed_short_block), signed_short_block + + short_attestation = get_valid_attestation(spec, short_state, short_block.slot, signed=True) + add_attestation_to_store(spec, store, short_attestation, test_steps) + + head = spec.get_head(store) + assert head == spec.hash_tree_root(short_block) + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_filtered_block_tree(spec, state): + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + + anchor_root = get_anchor_root(spec, state) + + # transition state past initial couple of epochs + next_epoch(spec, state) + next_epoch(spec, state) + + head = spec.get_head(store) + assert head == anchor_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + # fill in attestations for entire epoch, justifying the recent epoch + prev_state, signed_blocks, state = next_epoch_with_attestations(spec, state, True, False) + attestations = [ + attestation for signed_block in signed_blocks + for attestation in signed_block.message.body.attestations + ] + assert state.current_justified_checkpoint.epoch > prev_state.current_justified_checkpoint.epoch + + # tick time forward and add blocks and attestations to store + current_time = state.slot * spec.SECONDS_PER_SLOT + store.genesis_time + spec.on_tick(store, current_time) + for signed_block in signed_blocks: + spec.on_block(store, signed_block) + test_steps.append({'block': get_block_file_name(signed_block)}) + yield get_block_file_name(signed_block), signed_block + + for attestation in attestations: + spec.on_attestation(store, attestation) + test_steps.append({'attestation': get_attestation_file_name(attestation)}) + yield get_attestation_file_name(attestation), attestation + + assert store.justified_checkpoint == state.current_justified_checkpoint + + # the last block in the branch should be the head + expected_head_root = spec.hash_tree_root(signed_blocks[-1].message) + head = spec.get_head(store) + assert head == expected_head_root + + test_steps.append({ + 'checks': { + 'justified_checkpoint_root': encode_hex(store.justified_checkpoint.hash_tree_root()), + 'head': encode_hex(head), + } + }) + + # + # create branch containing the justified block but not containing enough on + # chain votes to justify that block + # + + # build a chain without attestations off of previous justified block + non_viable_state = store.block_states[store.justified_checkpoint.root].copy() + + # ensure that next wave of votes are for future epoch + next_epoch(spec, non_viable_state) + next_epoch(spec, non_viable_state) + next_epoch(spec, non_viable_state) + assert spec.get_current_epoch(non_viable_state) > store.justified_checkpoint.epoch + + # create rogue block that will be attested to in this non-viable branch + rogue_block = build_empty_block_for_next_slot(spec, non_viable_state) + signed_rogue_block = state_transition_and_sign_block(spec, non_viable_state, rogue_block) + + # create an epoch's worth of attestations for the rogue block + next_epoch(spec, non_viable_state) + attestations = [] + for i in range(spec.SLOTS_PER_EPOCH): + slot = rogue_block.slot + i + for index in range(spec.get_committee_count_per_slot(non_viable_state, spec.compute_epoch_at_slot(slot))): + 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 + current_time = (attestations[-1].data.slot + 1) * spec.SECONDS_PER_SLOT + store.genesis_time + spec.on_tick(store, current_time) + test_steps.append({'tick': int(current_time)}) + + # include rogue block and associated attestations in the store + spec.on_block(store, signed_rogue_block) + test_steps.append({'block': get_block_file_name(signed_rogue_block)}) + yield get_block_file_name(signed_rogue_block), signed_rogue_block + + for attestation in attestations: + spec.on_attestation(store, attestation) + test_steps.append({'attestation': get_attestation_file_name(attestation)}) + yield get_attestation_file_name(attestation), attestation + + # ensure that get_head still returns the head from the previous branch + head = spec.get_head(store) + assert head == expected_head_root + test_steps.append({ + 'checks': { + 'head': encode_hex(head) + } + }) + + yield 'steps', test_steps diff --git a/tests/generators/fork_choice/README.md b/tests/generators/fork_choice/README.md new file mode 100644 index 000000000..e450ca92f --- /dev/null +++ b/tests/generators/fork_choice/README.md @@ -0,0 +1,3 @@ +# Fork choice tests + +TODO diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py new file mode 100644 index 000000000..f684399eb --- /dev/null +++ b/tests/generators/fork_choice/main.py @@ -0,0 +1,40 @@ +from typing import Iterable + +from gen_base import gen_runner, gen_typing +from gen_from_tests.gen import generate_from_tests +from importlib import reload, import_module +from eth2spec.config import config_util +from eth2spec.phase0 import spec as spec_phase0 +from eth2spec.test.context import PHASE0 +from eth2spec.utils import bls + + +def create_provider(fork_name: str, handler_name: str, + tests_src_mod_name: str, config_name: str) -> gen_typing.TestProvider: + def prepare_fn(configs_path: str) -> str: + config_util.prepare_config(configs_path, config_name) + reload(spec_phase0) + bls.use_milagro() + return config_name + + def cases_fn() -> Iterable[gen_typing.TestCase]: + tests_src = import_module(tests_src_mod_name) + return generate_from_tests( + runner_name='fork_choice', + handler_name=handler_name, + src=tests_src, + fork_name=fork_name, + ) + + return gen_typing.TestProvider(prepare=prepare_fn, make_cases=cases_fn) + + +if __name__ == "__main__": + phase_0_mods = {key: 'eth2spec.test.phase0.fork_choice.test_' + key for key in [ + 'get_head', + ]} + + # TODO: add other configs and forks + gen_runner.run_generator(f"fork_choice", [ + create_provider(PHASE0, key, mod_name, 'minimal') for key, mod_name in phase_0_mods.items() + ]) diff --git a/tests/generators/fork_choice/requirements.txt b/tests/generators/fork_choice/requirements.txt new file mode 100644 index 000000000..b82314298 --- /dev/null +++ b/tests/generators/fork_choice/requirements.txt @@ -0,0 +1,2 @@ +../../core/gen_helpers +../../../ \ No newline at end of file