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 aa2f0694b..20e13b9ba 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -1,7 +1,9 @@ +import random 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.state import state_transition_and_sign_block, next_slot, next_epoch_via_block from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block +from eth2spec.test.helpers.attestations import next_slots_with_attestations def _state_transition_and_sign_block_at_slot(spec, state): @@ -64,7 +66,7 @@ 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 - assert spec.compute_epoch_at_slot(state.slot) == fork_epoch + assert spec.get_current_epoch(state) == fork_epoch state = post_spec.upgrade_to_altair(state) @@ -108,7 +110,7 @@ def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 - assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + assert post_spec.get_current_epoch(state) == fork_epoch + 1 slots_with_blocks = [block.message.slot for block in blocks] assert len(set(slots_with_blocks)) == len(slots_with_blocks) @@ -148,7 +150,7 @@ def test_transition_missing_first_post_block(state, fork_epoch, spec, post_spec, ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 - assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + assert post_spec.get_current_epoch(state) == fork_epoch + 1 slots_with_blocks = [block.message.slot for block in blocks] assert len(set(slots_with_blocks)) == len(slots_with_blocks) @@ -191,7 +193,7 @@ def test_transition_missing_last_pre_fork_block(state, fork_epoch, spec, post_sp ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 - assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + assert post_spec.get_current_epoch(state) == fork_epoch + 1 slots_with_blocks = [block.message.slot for block in blocks] assert len(set(slots_with_blocks)) == len(slots_with_blocks) @@ -234,7 +236,7 @@ def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pr ]) assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 - assert post_spec.compute_epoch_at_slot(state.slot) == fork_epoch + 1 + assert post_spec.get_current_epoch(state) == fork_epoch + 1 slots_with_blocks = [block.message.slot for block in blocks] assert len(slots_with_blocks) == 1 @@ -242,3 +244,193 @@ def test_transition_only_blocks_post_fork(state, fork_epoch, spec, post_spec, pr yield "blocks", blocks yield "post", state + + +def _run_transition_test_with_attestations(state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + participation_fn=None, + expect_finality=True): + yield "pre", state + + current_epoch = spec.get_current_epoch(state) + assert current_epoch < fork_epoch + assert current_epoch == spec.GENESIS_EPOCH + + # skip genesis epoch to avoid dealing with some edge cases... + block = next_epoch_via_block(spec, state) + + # regular state transition until fork: + fill_cur_epoch = False + fill_prev_epoch = True + blocks = [pre_tag(sign_block(spec, state, block))] + current_epoch = spec.get_current_epoch(state) + for _ in range(current_epoch, fork_epoch - 1): + _, blocks_in_epoch, state = next_slots_with_attestations( + spec, + state, + spec.SLOTS_PER_EPOCH, + fill_cur_epoch, + fill_prev_epoch, + participation_fn=participation_fn, + ) + blocks.extend([pre_tag(block) for block in blocks_in_epoch]) + + _, blocks_in_epoch, state = next_slots_with_attestations( + spec, + state, + spec.SLOTS_PER_EPOCH - 1, + fill_cur_epoch, + fill_prev_epoch, + participation_fn=participation_fn, + ) + blocks.extend([pre_tag(block) for block in blocks_in_epoch]) + assert spec.get_current_epoch(state) == fork_epoch - 1 + assert (state.slot + 1) % spec.SLOTS_PER_EPOCH == 0 + + # 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(4): + _, blocks_in_epoch, state = next_slots_with_attestations( + post_spec, + state, + post_spec.SLOTS_PER_EPOCH, + fill_cur_epoch, + fill_prev_epoch, + participation_fn=participation_fn, + ) + blocks.extend([post_tag(block) for block in blocks_in_epoch]) + + assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 + assert post_spec.get_current_epoch(state) == fork_epoch + 4 + + if expect_finality: + assert state.current_justified_checkpoint.epoch == fork_epoch + 2 + assert state.finalized_checkpoint.epoch == fork_epoch + else: + assert state.current_justified_checkpoint.epoch == spec.GENESIS_EPOCH + assert state.finalized_checkpoint.epoch == spec.GENESIS_EPOCH + + assert len(blocks) == (fork_epoch + 3) * post_spec.SLOTS_PER_EPOCH + 1 + assert len(blocks) == len(set(blocks)) + + blocks_without_attestations = [block for block in blocks if len(block.message.body.attestations) == 0] + assert len(blocks_without_attestations) == 2 + slots_without_attestations = [b.message.slot for b in blocks_without_attestations] + + assert set(slots_without_attestations) == set([spec.SLOTS_PER_EPOCH, fork_epoch * spec.SLOTS_PER_EPOCH]) + + yield "blocks", blocks + yield "post", state + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=3) +def test_transition_with_finality(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial ``state`` to the epoch after the ``fork_epoch``, + including attestations so as to produce finality through the fork boundary. + """ + yield from _run_transition_test_with_attestations(state, fork_epoch, spec, post_spec, pre_tag, post_tag) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=3) +def test_transition_with_random_three_quarters_participation(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial ``state`` to the epoch after the ``fork_epoch``, + including attestations so as to produce finality through the fork boundary. + """ + rng = random.Random(1337) + + def _drop_random_quarter(_slot, _index, indices): + # still finalize, but drop some attestations + committee_len = len(indices) + assert committee_len >= 4 + filter_len = committee_len // 4 + participant_count = committee_len - filter_len + return rng.sample(indices, participant_count) + + yield from _run_transition_test_with_attestations( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + participation_fn=_drop_random_quarter + ) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=3) +def test_transition_with_random_half_participation(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + rng = random.Random(2020) + + def _drop_random_half(_slot, _index, indices): + # drop enough attestations to not finalize + committee_len = len(indices) + assert committee_len >= 2 + filter_len = committee_len // 2 + participant_count = committee_len - filter_len + return rng.sample(indices, participant_count) + + yield from _run_transition_test_with_attestations( + state, + fork_epoch, + spec, + post_spec, + pre_tag, + post_tag, + participation_fn=_drop_random_half, + expect_finality=False + ) + + +@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2) +def test_transition_with_no_attestations_until_after_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag): + """ + Transition from the initial ``state`` to the ``fork_epoch`` with no attestations, + then transition forward with enough attestations to finalize the fork epoch. + """ + 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 but add attestations + # for enough epochs to finalize the ``fork_epoch`` + block = next_epoch_via_block(post_spec, state) + blocks.append(post_tag(sign_block(post_spec, state, block))) + for _ in range(4): + _, blocks_in_epoch, state = next_slots_with_attestations( + post_spec, + state, + post_spec.SLOTS_PER_EPOCH, + False, + True, + ) + blocks.extend([post_tag(block) for block in blocks_in_epoch]) + + assert state.slot % post_spec.SLOTS_PER_EPOCH == 0 + assert post_spec.get_current_epoch(state) == fork_epoch + 5 + + assert state.current_justified_checkpoint.epoch == fork_epoch + 3 + assert state.finalized_checkpoint.epoch == fork_epoch + 1 + + yield "blocks", blocks + yield "post", state diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index 8e8002041..b55aff9e5 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -219,41 +219,81 @@ def add_attestations_to_state(spec, state, attestations, slot): spec.process_attestation(state, attestation) +def _get_valid_attestation_at_slot(state, spec, slot_to_attest, participation_fn=None, on_time=True): + committees_per_slot = spec.get_committee_count_per_slot(state, spec.compute_epoch_at_slot(slot_to_attest)) + for index in range(committees_per_slot): + def participants_filter(comm): + if participation_fn is None: + return comm + else: + return participation_fn(state.slot, index, comm) + # if spec.fork == SHARDING: TODO: add shard data to attestation, include shard headers in block + yield get_valid_attestation( + spec, + state, + slot_to_attest, + index=index, + signed=True, + on_time=on_time, + filter_participant_set=participants_filter + ) + + +def next_slots_with_attestations(spec, + state, + slot_count, + fill_cur_epoch, + fill_prev_epoch, + participation_fn=None): + """ + participation_fn: (slot, committee_index, committee_indices_set) -> participants_indices_set + """ + post_state = state.copy() + signed_blocks = [] + for _ in range(slot_count): + block = build_empty_block_for_next_slot(spec, post_state) + if fill_cur_epoch and post_state.slot >= spec.MIN_ATTESTATION_INCLUSION_DELAY: + slot_to_attest = post_state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY + 1 + if slot_to_attest >= spec.compute_start_slot_at_epoch(spec.get_current_epoch(post_state)): + attestations = _get_valid_attestation_at_slot( + post_state, + spec, + slot_to_attest, + participation_fn=participation_fn + ) + for attestation in attestations: + block.body.attestations.append(attestation) + if fill_prev_epoch: + slot_to_attest = post_state.slot - spec.SLOTS_PER_EPOCH + 1 + attestations = _get_valid_attestation_at_slot( + post_state, + spec, + slot_to_attest, + on_time=False, + participation_fn=participation_fn + ) + for attestation in attestations: + block.body.attestations.append(attestation) + + signed_block = state_transition_and_sign_block(spec, post_state, block) + signed_blocks.append(signed_block) + + return state, signed_blocks, post_state + + def next_epoch_with_attestations(spec, state, fill_cur_epoch, fill_prev_epoch): assert state.slot % spec.SLOTS_PER_EPOCH == 0 - post_state = state.copy() - signed_blocks = [] - for _ in range(spec.SLOTS_PER_EPOCH): - block = build_empty_block_for_next_slot(spec, post_state) - if fill_cur_epoch and post_state.slot >= spec.MIN_ATTESTATION_INCLUSION_DELAY: - slot_to_attest = post_state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY + 1 - committees_per_slot = spec.get_committee_count_per_slot(state, spec.compute_epoch_at_slot(slot_to_attest)) - if slot_to_attest >= spec.compute_start_slot_at_epoch(spec.get_current_epoch(post_state)): - for index in range(committees_per_slot): - # if spec.fork == SHARDING: TODO: add shard data to attestation, include shard headers in block - - cur_attestation = get_valid_attestation( - spec, post_state, slot_to_attest, - index=index, signed=True, on_time=True - ) - block.body.attestations.append(cur_attestation) - - if fill_prev_epoch: - slot_to_attest = post_state.slot - spec.SLOTS_PER_EPOCH + 1 - committees_per_slot = spec.get_committee_count_per_slot(state, spec.compute_epoch_at_slot(slot_to_attest)) - for index in range(committees_per_slot): - prev_attestation = get_valid_attestation( - spec, post_state, slot_to_attest, index=index, signed=True, on_time=False) - block.body.attestations.append(prev_attestation) - - signed_block = state_transition_and_sign_block(spec, post_state, block) - signed_blocks.append(signed_block) - - return state, signed_blocks, post_state + return next_slots_with_attestations( + spec, + state, + spec.SLOTS_PER_EPOCH, + fill_cur_epoch, + fill_prev_epoch, + ) def prepare_state_with_attestations(spec, state, participation_fn=None): diff --git a/tests/core/pyspec/eth2spec/test/helpers/block.py b/tests/core/pyspec/eth2spec/test/helpers/block.py index 64f406633..0dd9dc98e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/block.py +++ b/tests/core/pyspec/eth2spec/test/helpers/block.py @@ -61,6 +61,7 @@ def transition_unsigned_block(spec, state, block): assert state.latest_block_header.slot < block.slot # There may not already be a block in this slot or past it. assert state.slot == block.slot # The block must be for this slot spec.process_block(state, block) + return block def apply_empty_block(spec, state, slot=None): @@ -68,7 +69,7 @@ def apply_empty_block(spec, state, slot=None): Transition via an empty block (on current slot, assuming no block has been applied yet). """ block = build_empty_block(spec, state, slot) - transition_unsigned_block(spec, state, block) + return transition_unsigned_block(spec, state, block) def build_empty_block(spec, state, slot=None): diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index 74521087d..ef09c6e07 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -62,7 +62,7 @@ def next_epoch_via_block(spec, state): """ Transition to the start slot of the next epoch via a full block transition """ - apply_empty_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH) + return apply_empty_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH) def get_state_root(spec, state, slot) -> bytes: