From b71aa3fb5623589868f9d11a71b664ac46e74405 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 27 Apr 2021 17:24:15 -0700 Subject: [PATCH 1/8] add `transition` spec test format --- .../test/altair/transition/__init__.py | 0 .../test/altair/transition/test_transition.py | 51 +++++++++++++ tests/core/pyspec/eth2spec/test/context.py | 37 +++++++++- tests/core/pyspec/eth2spec/test/utils.py | 48 ++++++++++++- tests/formats/transition/README.md | 72 +++++++++++++++++++ tests/generators/transition/main.py | 42 +++++++++++ tests/generators/transition/requirements.txt | 2 + 7 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/altair/transition/__init__.py create mode 100644 tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py create mode 100644 tests/formats/transition/README.md create mode 100644 tests/generators/transition/main.py create mode 100644 tests/generators/transition/requirements.txt diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/__init__.py b/tests/core/pyspec/eth2spec/test/altair/transition/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py new file mode 100644 index 000000000..a6e06b1d2 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -0,0 +1,51 @@ +from eth2spec.test.context import ( + fork_transition_test, + single_phase, + with_custom_state, + default_activation_threshold, + low_balances, +) +from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.state import state_transition_and_sign_block +from eth2spec.test.helpers.block import build_empty_block_for_next_slot + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + yield "pre", state + + blocks = [] + for slot in range(state.slot, fork_epoch * spec.SLOTS_PER_EPOCH): + block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, block) + blocks.append(pre_tag(block)) + + state = post_spec.upgrade_to_altair(state) + + assert state.fork.epoch == fork_epoch + assert state.fork.previous_version == post_spec.GENESIS_FORK_VERSION + assert state.fork.current_version == post_spec.ALTAIR_FORK_VERSION + + block = build_empty_block_for_next_slot(post_spec, state) + state_transition_and_sign_block(post_spec, state, block) + blocks.append(post_tag(block)) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR) +def test_normal_transition_with_manual_fork_epoch(state, spec, post_spec, pre_tag, post_tag): + fork_epoch = 2 + yield "fork_epoch", "meta", fork_epoch + + # run test with computed fork_epoch... + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +@with_custom_state(low_balances, default_activation_threshold) +@single_phase +def test_normal_transition_with_low_balances(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + yield "pre", state + + # run test with custom state... diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 7a2e61c22..57071169f 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -11,7 +11,7 @@ from .helpers.constants import ( ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_MERGE, ) from .helpers.genesis import create_genesis_state -from .utils import vector_test, with_meta_tags +from .utils import vector_test, with_meta_tags, build_transition_test from random import Random from typing import Any, Callable, Sequence, TypedDict, Protocol @@ -383,3 +383,38 @@ def is_post_merge(spec): with_altair_and_later = with_phases([ALTAIR]) # TODO: include Merge, but not until Merge work is rebased. with_merge_and_later = with_phases([MERGE]) + + +def fork_transition_test(pre_fork_name, post_fork_name, fork_epoch=None): + """ + A decorator to construct a "transition" test from one fork of the eth2 spec + to another. + + Decorator assumes a transition from the `pre_fork_name` fork to the + `post_fork_name` fork. The user can supply a `fork_epoch` at which the + fork occurs or they must compute one (yielding to the generator) during the test + if more custom behavior is desired. + + A test using this decorator should expect to receive as parameters: + `state`: the default state constructed for the `pre_fork_name` fork + according to the `with_state` decorator. + `fork_epoch`: the `fork_epoch` provided to this decorator, if given. + `spec`: the version of the eth2 spec corresponding to `pre_fork_name`. + `post_spec`: the version of the eth2 spec corresponding to `post_fork_name`. + `pre_tag`: a function to tag data as belonging to `pre_fork_name` fork. + Used to discriminate data during consumption of the generated spec tests. + `post_tag`: a function to tag data as belonging to `post_fork_name` fork. + Used to discriminate data during consumption of the generated spec tests. + """ + def _wrapper(fn): + @with_phases([pre_fork_name], other_phases=[post_fork_name]) + @spec_test + @with_state + def _adapter(*args, **kwargs): + wrapped = build_transition_test(fn, + pre_fork_name, + post_fork_name, + fork_epoch=fork_epoch) + return wrapped(*args, **kwargs) + return _adapter + return _wrapper diff --git a/tests/core/pyspec/eth2spec/test/utils.py b/tests/core/pyspec/eth2spec/test/utils.py index bad6c867b..d94aeb5aa 100644 --- a/tests/core/pyspec/eth2spec/test/utils.py +++ b/tests/core/pyspec/eth2spec/test/utils.py @@ -1,5 +1,6 @@ +import inspect from typing import Dict, Any -from eth2spec.utils.ssz.ssz_typing import View +from eth2spec.utils.ssz.ssz_typing import View, boolean, Container from eth2spec.utils.ssz.ssz_impl import serialize @@ -93,3 +94,48 @@ def with_meta_tags(tags: Dict[str, Any]): yield k, 'meta', v return entry return runner + + +class FlaggedContainer(Container): + flag: boolean + obj: Container + + +def build_transition_test(fn, pre_fork_name, post_fork_name, fork_epoch=None): + """ + Handles the inner plumbing to generate `transition_test`s. + See that decorator in `context.py` for more information. + """ + def _adapter(*args, **kwargs): + post_spec = kwargs["phases"][post_fork_name] + + def pre_tag(obj): + return FlaggedContainer(flag=False, obj=obj) + + def post_tag(obj): + return FlaggedContainer(flag=True, obj=obj) + + yield "post_fork", "meta", post_fork_name + + has_fork_epoch = False + if fork_epoch: + kwargs["fork_epoch"] = fork_epoch + has_fork_epoch = True + yield "fork_epoch", "meta", fork_epoch + + # massage args to handle an optional custom state using + # `with_custom_state` decorator + expected_args = inspect.getfullargspec(fn) + if "phases" not in expected_args.kwonlyargs: + kwargs.pop("phases", None) + + for part in fn(*args, + post_spec=post_spec, + pre_tag=pre_tag, + post_tag=post_tag, + **kwargs): + if part[0] == "fork_epoch": + has_fork_epoch = True + yield part + assert has_fork_epoch + return _adapter diff --git a/tests/formats/transition/README.md b/tests/formats/transition/README.md new file mode 100644 index 000000000..bd5985e50 --- /dev/null +++ b/tests/formats/transition/README.md @@ -0,0 +1,72 @@ +# Transition testing + +Transition tests to cover processing the chain across a fork boundary. + +Each test case contains a `post_fork` key in the `meta.yaml` that indicates the target fork which also fixes the fork the test begins in. + +Clients should assume forks happen sequentially in the following manner: + +0. `phase0` +1. `altair` + +For example, if a test case has `post_fork` of `altair`, the test consumer should assume the test begins in `phase0` and use that specification to process the initial state and any blocks up until the fork epoch. After the fork happens, the test consumer should use the specification according to the `altair` fork to process the remaining data. + +## Encoding notes + +This test type contains objects that span fork boundaries. +In general, it may not be clear which objects belong to which fork so each +object is prefixed with a SSZ `boolean` to indicate if the object belongs to the post fork or if it belongs to the initial fork. +This "flagged" data should be used to select the appropriate version of the spec when interpreting the enclosed object. + +```python +class FlaggedContainer(Container): + flag: boolean + obj: Container +``` + +If `flag` is `False`, then the `obj` belongs to the **initial** fork. +If `flag` is `True`, then the `obj` belongs to the **post** fork. + +Unless stated otherwise, all references to spec types below refer to SSZ-snappy +encoded data `obj` with the relevant `flag` set: +`FlaggedContainer(flag=flag, obj=obj)`. + +For example, when testing the fork from Phase 0 to Altair, an Altair block is given +as the encoding of `FlaggedContainer(flag=True, obj=SignedBeaconBlock())` where +`SignedBeaconBlock` is the type defined in the Altair spec. + +## Test case format + +### `meta.yaml` + +```yaml +post_fork: string -- String name of the spec after the fork. +fork_epoch: int -- The epoch at which the fork takes place. +blocks_count: int -- The number of blocks processed in this test. +``` + +*Note*: There may be a fork transition function to run at the `fork_epoch`. Refer to the specs for the relevant fork for further details. + +### `pre.ssz_snappy` + +A SSZ-snappy encoded `BeaconState` according to the specification of the initial fork, the state before running the block transitions. + +*NOTE*: This object is _not_ "flagged" as it is assumed to always belong to the post fork. + +### `blocks_.ssz_snappy` + +A series of files, with `` in range `[0, blocks_count)`. +Blocks must be processed in order, following the main transition function +(i.e. process slot and epoch transitions in between blocks as normal). + +Blocks are encoded as `SignedBeaconBlock`s from the relevant spec version indicated by flag data as described in the `Encoding notes`. + +### `post.ssz_snappy` + +A SSZ-snappy encoded `BeaconState` according to the specification of the post fork, the state after running the block transitions. + +*NOTE*: This object is _not_ "flagged" as it is assumed to always belong to the post fork. + +## Condition + +The resulting state should match the expected `post` state. diff --git a/tests/generators/transition/main.py b/tests/generators/transition/main.py new file mode 100644 index 000000000..b7fd7b0a8 --- /dev/null +++ b/tests/generators/transition/main.py @@ -0,0 +1,42 @@ +from importlib import reload +from typing import Iterable + +from eth2spec.test.helpers.constants import ALTAIR, MINIMAL, MAINNET, PHASE0 +from eth2spec.config import config_util +from eth2spec.test.altair.transition import test_transition as test_altair_transition +from eth2spec.phase0 import spec as spec_phase0 +from eth2spec.altair import spec as spec_altair + +from eth2spec.gen_helpers.gen_base import gen_runner, gen_typing +from eth2spec.gen_helpers.gen_from_tests.gen import generate_from_tests + + +def create_provider(tests_src, config_name: str, pre_fork_name: str, post_fork_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_altair) + return config_name + + def cases_fn() -> Iterable[gen_typing.TestCase]: + return generate_from_tests( + runner_name='transition', + handler_name='core', + src=tests_src, + fork_name=post_fork_name, + phase=pre_fork_name, + ) + + return gen_typing.TestProvider(prepare=prepare_fn, make_cases=cases_fn) + + +TRANSITION_TESTS = ((PHASE0, ALTAIR, test_altair_transition),) + + +if __name__ == "__main__": + for pre_fork, post_fork, transition_test_module in TRANSITION_TESTS: + gen_runner.run_generator("transition", [ + create_provider(transition_test_module, MINIMAL, pre_fork, post_fork), + create_provider(transition_test_module, MAINNET, pre_fork, post_fork), + ]) diff --git a/tests/generators/transition/requirements.txt b/tests/generators/transition/requirements.txt new file mode 100644 index 000000000..735f863fa --- /dev/null +++ b/tests/generators/transition/requirements.txt @@ -0,0 +1,2 @@ +pytest>=4.4 +../../../[generator] \ No newline at end of file From 0cc6e15b44f075f9258e4bf9524517e0fd4471c1 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 30 Apr 2021 09:57:03 -0700 Subject: [PATCH 2/8] Update tests/formats/transition/README.md Co-authored-by: Adrian Sutton --- tests/formats/transition/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/formats/transition/README.md b/tests/formats/transition/README.md index bd5985e50..fb8b3b5ea 100644 --- a/tests/formats/transition/README.md +++ b/tests/formats/transition/README.md @@ -51,7 +51,7 @@ blocks_count: int -- The number of blocks processed in this test. A SSZ-snappy encoded `BeaconState` according to the specification of the initial fork, the state before running the block transitions. -*NOTE*: This object is _not_ "flagged" as it is assumed to always belong to the post fork. +*NOTE*: This object is _not_ "flagged" as it is assumed to always belong to the pre fork. ### `blocks_.ssz_snappy` From d34b2a08d5b19e499190673d6385d47802ce3f17 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 30 Apr 2021 11:35:18 -0700 Subject: [PATCH 3/8] Use `fork_block` index in lieu of fork flag --- tests/core/pyspec/eth2spec/test/utils.py | 18 ++++--- tests/formats/transition/README.md | 67 ++++++++++++------------ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/utils.py b/tests/core/pyspec/eth2spec/test/utils.py index d94aeb5aa..61fc75040 100644 --- a/tests/core/pyspec/eth2spec/test/utils.py +++ b/tests/core/pyspec/eth2spec/test/utils.py @@ -1,6 +1,6 @@ import inspect from typing import Dict, Any -from eth2spec.utils.ssz.ssz_typing import View, boolean, Container +from eth2spec.utils.ssz.ssz_typing import View from eth2spec.utils.ssz.ssz_impl import serialize @@ -96,11 +96,6 @@ def with_meta_tags(tags: Dict[str, Any]): return runner -class FlaggedContainer(Container): - flag: boolean - obj: Container - - def build_transition_test(fn, pre_fork_name, post_fork_name, fork_epoch=None): """ Handles the inner plumbing to generate `transition_test`s. @@ -109,11 +104,15 @@ def build_transition_test(fn, pre_fork_name, post_fork_name, fork_epoch=None): def _adapter(*args, **kwargs): post_spec = kwargs["phases"][post_fork_name] + pre_fork_counter = 0 + def pre_tag(obj): - return FlaggedContainer(flag=False, obj=obj) + nonlocal pre_fork_counter + pre_fork_counter += 1 + return obj def post_tag(obj): - return FlaggedContainer(flag=True, obj=obj) + return obj yield "post_fork", "meta", post_fork_name @@ -138,4 +137,7 @@ def build_transition_test(fn, pre_fork_name, post_fork_name, fork_epoch=None): has_fork_epoch = True yield part assert has_fork_epoch + + if pre_fork_counter > 0: + yield "fork_block", "meta", pre_fork_counter - 1 return _adapter diff --git a/tests/formats/transition/README.md b/tests/formats/transition/README.md index fb8b3b5ea..832f38ca2 100644 --- a/tests/formats/transition/README.md +++ b/tests/formats/transition/README.md @@ -11,30 +11,6 @@ Clients should assume forks happen sequentially in the following manner: For example, if a test case has `post_fork` of `altair`, the test consumer should assume the test begins in `phase0` and use that specification to process the initial state and any blocks up until the fork epoch. After the fork happens, the test consumer should use the specification according to the `altair` fork to process the remaining data. -## Encoding notes - -This test type contains objects that span fork boundaries. -In general, it may not be clear which objects belong to which fork so each -object is prefixed with a SSZ `boolean` to indicate if the object belongs to the post fork or if it belongs to the initial fork. -This "flagged" data should be used to select the appropriate version of the spec when interpreting the enclosed object. - -```python -class FlaggedContainer(Container): - flag: boolean - obj: Container -``` - -If `flag` is `False`, then the `obj` belongs to the **initial** fork. -If `flag` is `True`, then the `obj` belongs to the **post** fork. - -Unless stated otherwise, all references to spec types below refer to SSZ-snappy -encoded data `obj` with the relevant `flag` set: -`FlaggedContainer(flag=flag, obj=obj)`. - -For example, when testing the fork from Phase 0 to Altair, an Altair block is given -as the encoding of `FlaggedContainer(flag=True, obj=SignedBeaconBlock())` where -`SignedBeaconBlock` is the type defined in the Altair spec. - ## Test case format ### `meta.yaml` @@ -42,16 +18,17 @@ as the encoding of `FlaggedContainer(flag=True, obj=SignedBeaconBlock())` where ```yaml post_fork: string -- String name of the spec after the fork. fork_epoch: int -- The epoch at which the fork takes place. -blocks_count: int -- The number of blocks processed in this test. +fork_block: int -- Optional. The `` of the last block on the initial fork. +blocks_count: int -- Optional. The number of blocks processed in this test. ``` -*Note*: There may be a fork transition function to run at the `fork_epoch`. Refer to the specs for the relevant fork for further details. +*Note*: There may be a fork transition function to run at the `fork_epoch`. +Refer to the specs for the relevant fork for further details. ### `pre.ssz_snappy` -A SSZ-snappy encoded `BeaconState` according to the specification of the initial fork, the state before running the block transitions. - -*NOTE*: This object is _not_ "flagged" as it is assumed to always belong to the pre fork. +A SSZ-snappy encoded `BeaconState` according to the specification of +the initial fork, the state before running the block transitions. ### `blocks_.ssz_snappy` @@ -59,13 +36,37 @@ A series of files, with `` in range `[0, blocks_count)`. Blocks must be processed in order, following the main transition function (i.e. process slot and epoch transitions in between blocks as normal). -Blocks are encoded as `SignedBeaconBlock`s from the relevant spec version indicated by flag data as described in the `Encoding notes`. +*Note*: `blocks_count` will be missing if there are no blocks in this test. + +Blocks are encoded as `SignedBeaconBlock`s from the relevant spec version +as indicated by the `post_fork` and `fork_block` data in the `meta.yaml`. + +As blocks span fork boundaires, a `fork_block` number is given in +the `meta.yaml` to help resolve which blocks belong to which fork. + +The `fork_block` is the index in the test data of the **last** block +of the **initial** fork. + +To demonstrate, the following diagram shows slots with `_` and blocks +in those slots as `x`. The fork happens at the epoch delineated by the `|`. + +``` +x x x x +_ _ _ _ | _ _ _ _ +``` + +The `blocks_count` value in the `meta.yaml` in this case is `4` where the +`fork_block` value in the `meta.yaml` is `1`. If this particular example were +testing the fork from Phase 0 to Altair, blocks with indices `0, 1` represent +`SignedBeaconBlock`s defined in the Phase 0 spec and blocks with indices `2, 3` +represent `SignedBeaconBlock`s defined in the Altair spec. + +*Note*: `fork_block` will be missing if `blocks_count` is also missing. ### `post.ssz_snappy` -A SSZ-snappy encoded `BeaconState` according to the specification of the post fork, the state after running the block transitions. - -*NOTE*: This object is _not_ "flagged" as it is assumed to always belong to the post fork. +A SSZ-snappy encoded `BeaconState` according to the specification of +the post fork, the state after running the block transitions. ## Condition From 0e71496eb5e54ae60e06514853d74d01e53ffc65 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 30 Apr 2021 11:46:46 -0700 Subject: [PATCH 4/8] add "normal" transition test --- .../test/altair/transition/test_transition.py | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index a6e06b1d2..b9b67d55a 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -1,24 +1,44 @@ -from eth2spec.test.context import ( - fork_transition_test, - single_phase, - with_custom_state, - default_activation_threshold, - low_balances, -) +from eth2spec.test.context import fork_transition_test from eth2spec.test.helpers.constants import PHASE0, ALTAIR from eth2spec.test.helpers.state import state_transition_and_sign_block -from eth2spec.test.helpers.block import build_empty_block_for_next_slot +from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block + + +def _state_transition_and_sign_block_at_slot(spec, state): + """ + Cribbed from `transition_unsigned_block` helper + where the early parts of the state transition have already + been applied to `state`. + + Used to produce a block during an irregular state transition. + """ + block = build_empty_block(spec, state) + + assert state.latest_block_header.slot < block.slot + assert state.slot == block.slot + spec.process_block(state, block) + block.state_root = state.hash_tree_root() + return sign_block(spec, state, block) @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag): yield "pre", state + assert spec.get_current_epoch(state) < fork_epoch + blocks = [] - for slot in range(state.slot, fork_epoch * spec.SLOTS_PER_EPOCH): + # regular state transition until fork: + for _ in range(state.slot, fork_epoch * spec.SLOTS_PER_EPOCH - 1): block = build_empty_block_for_next_slot(spec, state) - state_transition_and_sign_block(spec, state, block) - blocks.append(pre_tag(block)) + signed_block = state_transition_and_sign_block(spec, state, block) + blocks.append(pre_tag(signed_block)) + + # irregular state transition to handle fork: + spec.process_slots(state, state.slot + 1) + + assert state.slot % spec.SLOTS_PER_EPOCH == 0 + assert spec.compute_epoch_at_slot(state.slot) == fork_epoch state = post_spec.upgrade_to_altair(state) @@ -26,26 +46,21 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag assert state.fork.previous_version == post_spec.GENESIS_FORK_VERSION assert state.fork.current_version == post_spec.ALTAIR_FORK_VERSION - block = build_empty_block_for_next_slot(post_spec, state) - state_transition_and_sign_block(post_spec, state, block) - blocks.append(post_tag(block)) + signed_block = _state_transition_and_sign_block_at_slot(post_spec, state) + blocks.append(post_tag(signed_block)) + + # continue regular state transition with new spec into next epoch + for _ in range(post_spec.SLOTS_PER_EPOCH): + block = build_empty_block_for_next_slot(post_spec, state) + signed_block = state_transition_and_sign_block(post_spec, state, block) + blocks.append(post_tag(signed_block)) + + assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 + assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + + slots_with_blocks = [block.message.slot for block in blocks] + assert len(set(slots_with_blocks)) == len(slots_with_blocks) + assert set(range(1, state.slot + 1)) == set(slots_with_blocks) yield "blocks", blocks yield "post", state - - -@fork_transition_test(PHASE0, ALTAIR) -def test_normal_transition_with_manual_fork_epoch(state, spec, post_spec, pre_tag, post_tag): - fork_epoch = 2 - yield "fork_epoch", "meta", fork_epoch - - # run test with computed fork_epoch... - - -@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) -@with_custom_state(low_balances, default_activation_threshold) -@single_phase -def test_normal_transition_with_low_balances(state, fork_epoch, spec, post_spec, pre_tag, post_tag): - yield "pre", state - - # run test with custom state... From 3f3aa4fb105298d4126980bc235ec8ef7ad674e4 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 30 Apr 2021 16:21:46 -0700 Subject: [PATCH 5/8] add some altair tests --- .../test/altair/transition/test_transition.py | 158 ++++++++++++++++-- 1 file changed, 140 insertions(+), 18 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index b9b67d55a..b522c1f94 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -1,6 +1,6 @@ from eth2spec.test.context import fork_transition_test from eth2spec.test.helpers.constants import PHASE0, ALTAIR -from eth2spec.test.helpers.state import state_transition_and_sign_block +from eth2spec.test.helpers.state import state_transition_and_sign_block, next_slot from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block @@ -21,20 +21,32 @@ def _state_transition_and_sign_block_at_slot(spec, state): return sign_block(spec, state, block) -@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) -def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag): - yield "pre", state +def _all_blocks(_): + return True - assert spec.get_current_epoch(state) < fork_epoch - blocks = [] - # regular state transition until fork: - for _ in range(state.slot, fork_epoch * spec.SLOTS_PER_EPOCH - 1): - block = build_empty_block_for_next_slot(spec, state) - signed_block = state_transition_and_sign_block(spec, state, block) - blocks.append(pre_tag(signed_block)) +def _skip_slots(*slots): + """ + Skip making a block if its slot is + passed as an argument to this filter + """ + def f(state_at_prior_slot): + return state_at_prior_slot.slot + 1 not in slots + return f - # irregular state transition to handle fork: + +def _state_transition_across_slots(spec, state, slot_count, block_filter=_all_blocks): + for _ in range(slot_count): + should_make_block = block_filter(state) + if should_make_block: + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield signed_block + else: + next_slot(spec, state) + + +def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True): spec.process_slots(state, state.slot + 1) assert state.slot % spec.SLOTS_PER_EPOCH == 0 @@ -46,14 +58,40 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag assert state.fork.previous_version == post_spec.GENESIS_FORK_VERSION assert state.fork.current_version == post_spec.ALTAIR_FORK_VERSION - signed_block = _state_transition_and_sign_block_at_slot(post_spec, state) - blocks.append(post_tag(signed_block)) + if with_block: + return state, _state_transition_and_sign_block_at_slot(post_spec, state) + else: + return state, None + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial `state` to the epoch after the `fork_epoch`, + producing blocks for every slot along the way. + """ + yield "pre", state + + assert spec.get_current_epoch(state) < fork_epoch + + # regular state transition until fork: + slot_count = fork_epoch * spec.SLOTS_PER_EPOCH - 1 - state.slot + blocks = [] + blocks.extend([ + pre_tag(block) for block in + _state_transition_across_slots(spec, state, slot_count) + ]) + + # irregular state transition to handle fork: + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) # continue regular state transition with new spec into next epoch - for _ in range(post_spec.SLOTS_PER_EPOCH): - block = build_empty_block_for_next_slot(post_spec, state) - signed_block = state_transition_and_sign_block(post_spec, state, block) - blocks.append(post_tag(signed_block)) + slot_count = post_spec.SLOTS_PER_EPOCH + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, slot_count) + ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 @@ -64,3 +102,87 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag yield "blocks", blocks yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial `state` to the epoch after the `fork_epoch`, + producing blocks for every slot along the way except for the first block + of the new fork. + """ + yield "pre", state + + assert spec.get_current_epoch(state) < fork_epoch + + # regular state transition until fork: + slot_count = fork_epoch * spec.SLOTS_PER_EPOCH - 1 - state.slot + blocks = [] + blocks.extend([ + pre_tag(block) for block in + _state_transition_across_slots(spec, state, slot_count) + ]) + + # irregular state transition to handle fork: + state, _ = _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) + + # continue regular state transition with new spec into next epoch + slot_count = post_spec.SLOTS_PER_EPOCH + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, slot_count) + ]) + + assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 + assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + + slots_with_blocks = [block.message.slot for block in blocks] + assert len(set(slots_with_blocks)) == len(slots_with_blocks) + expected_slots = set(range(1, state.slot + 1)).difference(set([fork_epoch * spec.SLOTS_PER_EPOCH])) + assert expected_slots == set(slots_with_blocks) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial `state` to the epoch after the `fork_epoch`, + producing blocks for every slot along the way except for the first block + of the new fork. + """ + yield "pre", state + + assert spec.get_current_epoch(state) < fork_epoch + + # regular state transition until fork: + last_slot_of_pre_fork = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + slot_count = last_slot_of_pre_fork - state.slot + blocks = [] + blocks.extend([ + pre_tag(block) for block in + _state_transition_across_slots(spec, state, slot_count, block_filter=_skip_slots(last_slot_of_pre_fork)) + ]) + + # irregular state transition to handle fork: + state, block = _do_altair_fork(state, spec, post_spec, fork_epoch) + blocks.append(post_tag(block)) + + # continue regular state transition with new spec into next epoch + slot_count = post_spec.SLOTS_PER_EPOCH + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, slot_count) + ]) + + assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 + assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + + slots_with_blocks = [block.message.slot for block in blocks] + assert len(set(slots_with_blocks)) == len(slots_with_blocks) + expected_slots = set(range(1, state.slot + 1)).difference(set([last_slot_of_pre_fork])) + assert expected_slots == set(slots_with_blocks) + + yield "blocks", blocks + yield "post", state From c08fb7714c4ba2e2a5d3c65ba84936493e8a88ce Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 30 Apr 2021 16:34:19 -0700 Subject: [PATCH 6/8] More altair fork tests with varied block conditions --- .../test/altair/transition/test_transition.py | 55 +++++++++++++++++++ tests/formats/transition/README.md | 7 +-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index b522c1f94..401a781e1 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -35,6 +35,19 @@ def _skip_slots(*slots): return f +def _no_blocks(_): + return False + + +def _only_at(slot): + """ + Only produce a block if its slot is `slot`. + """ + def f(state_at_prior_slot): + return state_at_prior_slot.slot + 1 == slot + return f + + def _state_transition_across_slots(spec, state, slot_count, block_filter=_all_blocks): for _ in range(slot_count): should_make_block = block_filter(state) @@ -186,3 +199,45 @@ def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_t yield "blocks", blocks yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial `state` to the epoch after the `fork_epoch`, + producing blocks for every slot along the way except for the first block + of the new fork. + """ + yield "pre", state + + assert spec.get_current_epoch(state) < fork_epoch + + # regular state transition until fork: + last_slot_of_pre_fork = fork_epoch * spec.SLOTS_PER_EPOCH - 1 + slot_count = last_slot_of_pre_fork - state.slot + blocks = [] + blocks.extend([ + pre_tag(block) for block in + _state_transition_across_slots(spec, state, slot_count, block_filter=_no_blocks) + ]) + + # irregular state transition to handle fork: + state, _ = _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) + + # continue regular state transition with new spec into next epoch + slot_count = post_spec.SLOTS_PER_EPOCH + last_slot = (fork_epoch + 1) * post_spec.SLOTS_PER_EPOCH + blocks.extend([ + post_tag(block) for block in + _state_transition_across_slots(post_spec, state, slot_count, block_filter=_only_at(last_slot)) + ]) + + assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 + assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + + slots_with_blocks = [block.message.slot for block in blocks] + assert len(slots_with_blocks) == 1 + assert slots_with_blocks[0] == last_slot + + yield "blocks", blocks + yield "post", state diff --git a/tests/formats/transition/README.md b/tests/formats/transition/README.md index 832f38ca2..37df65539 100644 --- a/tests/formats/transition/README.md +++ b/tests/formats/transition/README.md @@ -19,7 +19,7 @@ For example, if a test case has `post_fork` of `altair`, the test consumer shoul post_fork: string -- String name of the spec after the fork. fork_epoch: int -- The epoch at which the fork takes place. fork_block: int -- Optional. The `` of the last block on the initial fork. -blocks_count: int -- Optional. The number of blocks processed in this test. +blocks_count: int -- The number of blocks processed in this test. ``` *Note*: There may be a fork transition function to run at the `fork_epoch`. @@ -36,8 +36,6 @@ A series of files, with `` in range `[0, blocks_count)`. Blocks must be processed in order, following the main transition function (i.e. process slot and epoch transitions in between blocks as normal). -*Note*: `blocks_count` will be missing if there are no blocks in this test. - Blocks are encoded as `SignedBeaconBlock`s from the relevant spec version as indicated by the `post_fork` and `fork_block` data in the `meta.yaml`. @@ -61,7 +59,8 @@ testing the fork from Phase 0 to Altair, blocks with indices `0, 1` represent `SignedBeaconBlock`s defined in the Phase 0 spec and blocks with indices `2, 3` represent `SignedBeaconBlock`s defined in the Altair spec. -*Note*: `fork_block` will be missing if `blocks_count` is also missing. +*Note*: If `fork_block` is missing, then all block data should be +interpreted as belonging to the post fork. ### `post.ssz_snappy` From d7448255833fbd0880501cbe070d5c2b43d48ca1 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 4 May 2021 14:36:58 -0700 Subject: [PATCH 7/8] update docs --- .../eth2spec/test/altair/transition/test_transition.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index 401a781e1..48fdd64c8 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -162,8 +162,8 @@ def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ Transition from the initial `state` to the epoch after the `fork_epoch`, - producing blocks for every slot along the way except for the first block - of the new fork. + producing blocks for every slot along the way except for the last block + of the old fork. """ yield "pre", state @@ -205,8 +205,8 @@ def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_t def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ Transition from the initial `state` to the epoch after the `fork_epoch`, - producing blocks for every slot along the way except for the first block - of the new fork. + skipping blocks for every slot along the way except for the first block + in the ending epoch. """ yield "pre", state From e2aa595d5fc5f241ea271f8260a8d433bb7a11a9 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 11 May 2021 10:06:18 -0700 Subject: [PATCH 8/8] PR feedback --- .../test/altair/transition/test_transition.py | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index 48fdd64c8..c3b03d663 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -6,9 +6,9 @@ from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_e def _state_transition_and_sign_block_at_slot(spec, state): """ - Cribbed from `transition_unsigned_block` helper + Cribbed from ``transition_unsigned_block`` helper where the early parts of the state transition have already - been applied to `state`. + been applied to ``state``. Used to produce a block during an irregular state transition. """ @@ -41,15 +41,16 @@ def _no_blocks(_): def _only_at(slot): """ - Only produce a block if its slot is `slot`. + Only produce a block if its slot is ``slot``. """ def f(state_at_prior_slot): return state_at_prior_slot.slot + 1 == slot return f -def _state_transition_across_slots(spec, state, slot_count, block_filter=_all_blocks): - for _ in range(slot_count): +def _state_transition_across_slots(spec, state, to_slot, block_filter=_all_blocks): + assert state.slot < to_slot + while state.slot < to_slot: should_make_block = block_filter(state) if should_make_block: block = build_empty_block_for_next_slot(spec, state) @@ -80,7 +81,7 @@ def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True): @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ - Transition from the initial `state` to the epoch after the `fork_epoch`, + Transition from the initial ``state`` to the epoch after the ``fork_epoch``, producing blocks for every slot along the way. """ yield "pre", state @@ -88,11 +89,11 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag assert spec.get_current_epoch(state) < fork_epoch # regular state transition until fork: - slot_count = fork_epoch * spec.SLOTS_PER_EPOCH - 1 - state.slot + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, slot_count) + _state_transition_across_slots(spec, state, to_slot) ]) # irregular state transition to handle fork: @@ -100,10 +101,10 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag blocks.append(post_tag(block)) # continue regular state transition with new spec into next epoch - slot_count = post_spec.SLOTS_PER_EPOCH + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot blocks.extend([ post_tag(block) for block in - _state_transition_across_slots(post_spec, state, slot_count) + _state_transition_across_slots(post_spec, state, to_slot) ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 @@ -120,7 +121,7 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ - Transition from the initial `state` to the epoch after the `fork_epoch`, + Transition from the initial ``state`` to the epoch after the ``fork_epoch``, producing blocks for every slot along the way except for the first block of the new fork. """ @@ -129,21 +130,21 @@ def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, assert spec.get_current_epoch(state) < fork_epoch # regular state transition until fork: - slot_count = fork_epoch * spec.SLOTS_PER_EPOCH - 1 - state.slot + to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1 blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, slot_count) + _state_transition_across_slots(spec, state, to_slot) ]) # irregular state transition to handle fork: state, _ = _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) # continue regular state transition with new spec into next epoch - slot_count = post_spec.SLOTS_PER_EPOCH + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot blocks.extend([ post_tag(block) for block in - _state_transition_across_slots(post_spec, state, slot_count) + _state_transition_across_slots(post_spec, state, to_slot) ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 @@ -159,9 +160,9 @@ def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) -def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_tag, post_tag): +def test_transition_missing_last_pre_fork_block(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ - Transition from the initial `state` to the epoch after the `fork_epoch`, + Transition from the initial ``state`` to the epoch after the ``fork_epoch``, producing blocks for every slot along the way except for the last block of the old fork. """ @@ -171,11 +172,11 @@ def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_t # regular state transition until fork: last_slot_of_pre_fork = fork_epoch * spec.SLOTS_PER_EPOCH - 1 - slot_count = last_slot_of_pre_fork - state.slot + to_slot = last_slot_of_pre_fork blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, slot_count, block_filter=_skip_slots(last_slot_of_pre_fork)) + _state_transition_across_slots(spec, state, to_slot, block_filter=_skip_slots(last_slot_of_pre_fork)) ]) # irregular state transition to handle fork: @@ -183,10 +184,10 @@ def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_t blocks.append(post_tag(block)) # continue regular state transition with new spec into next epoch - slot_count = post_spec.SLOTS_PER_EPOCH + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot blocks.extend([ post_tag(block) for block in - _state_transition_across_slots(post_spec, state, slot_count) + _state_transition_across_slots(post_spec, state, to_slot) ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 @@ -204,7 +205,7 @@ def test_transition_missing_fork_block(state, fork_epoch, spec, post_spec, pre_t @fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): """ - Transition from the initial `state` to the epoch after the `fork_epoch`, + Transition from the initial ``state`` to the epoch after the ``fork_epoch``, skipping blocks for every slot along the way except for the first block in the ending epoch. """ @@ -214,22 +215,22 @@ def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pr # regular state transition until fork: last_slot_of_pre_fork = fork_epoch * spec.SLOTS_PER_EPOCH - 1 - slot_count = last_slot_of_pre_fork - state.slot + to_slot = last_slot_of_pre_fork blocks = [] blocks.extend([ pre_tag(block) for block in - _state_transition_across_slots(spec, state, slot_count, block_filter=_no_blocks) + _state_transition_across_slots(spec, state, to_slot, block_filter=_no_blocks) ]) # irregular state transition to handle fork: state, _ = _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=False) # continue regular state transition with new spec into next epoch - slot_count = post_spec.SLOTS_PER_EPOCH + to_slot = post_spec.SLOTS_PER_EPOCH + state.slot last_slot = (fork_epoch + 1) * post_spec.SLOTS_PER_EPOCH blocks.extend([ post_tag(block) for block in - _state_transition_across_slots(post_spec, state, slot_count, block_filter=_only_at(last_slot)) + _state_transition_across_slots(post_spec, state, to_slot, block_filter=_only_at(last_slot)) ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0