Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fork transition spec tests #2363

Merged
merged 8 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
244 changes: 244 additions & 0 deletions tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
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, 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)


def _all_blocks(_):
return True


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


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, 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)
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
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
assert spec.compute_epoch_at_slot(state.slot) == fork_epoch

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

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:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
blocks = []
blocks.extend([
pre_tag(block) for block in
_state_transition_across_slots(spec, state, to_slot)
])

# 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
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_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(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, 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:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
blocks = []
blocks.extend([
pre_tag(block) for block in
_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
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_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(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_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``,
producing blocks for every slot along the way except for the last block
of the old 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
to_slot = last_slot_of_pre_fork
blocks = []
blocks.extend([
pre_tag(block) for block in
_state_transition_across_slots(spec, state, to_slot, 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
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_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(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


@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``,
skipping blocks for every slot along the way except for the first block
in the ending epoch.
"""
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
to_slot = last_slot_of_pre_fork
blocks = []
blocks.extend([
pre_tag(block) for block in
_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
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, to_slot, 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
37 changes: 36 additions & 1 deletion tests/core/pyspec/eth2spec/test/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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
Expand Down Expand Up @@ -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
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
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
48 changes: 48 additions & 0 deletions tests/core/pyspec/eth2spec/test/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
from typing import Dict, Any
from eth2spec.utils.ssz.ssz_typing import View
from eth2spec.utils.ssz.ssz_impl import serialize
Expand Down Expand Up @@ -93,3 +94,50 @@ def entry(*args, **kw):
yield k, 'meta', v
return entry
return runner


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]

pre_fork_counter = 0

def pre_tag(obj):
nonlocal pre_fork_counter
pre_fork_counter += 1
return obj

def post_tag(obj):
return 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

if pre_fork_counter > 0:
yield "fork_block", "meta", pre_fork_counter - 1
return _adapter
Loading