From 24b4d469032c73b073d689cfe49a9afcbb7f5cd4 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 23 Oct 2023 16:49:52 +0800 Subject: [PATCH] Add `get_proposer_head` and `should_override_forkchoice_update` tests --- .../test/bellatrix/fork_choice/test_reorg.py | 162 ++++++++++++++++++ .../eth2spec/test/helpers/attestations.py | 24 +-- .../eth2spec/test/helpers/fork_choice.py | 13 +- .../fork_choice/test_get_proposer_head.py | 154 +++++++++++++++++ 4 files changed, 334 insertions(+), 19 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/bellatrix/fork_choice/test_reorg.py create mode 100644 tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_proposer_head.py diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/fork_choice/test_reorg.py b/tests/core/pyspec/eth2spec/test/bellatrix/fork_choice/test_reorg.py new file mode 100644 index 000000000..cb7adac1c --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/bellatrix/fork_choice/test_reorg.py @@ -0,0 +1,162 @@ +from eth2spec.test.context import ( + spec_state_test, + with_bellatrix_and_later, + with_presets, +) +from eth2spec.test.helpers.constants import ( + MINIMAL, +) +from eth2spec.test.helpers.attestations import ( + get_valid_attestation_at_slot, +) +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.fork_choice import ( + apply_next_epoch_with_attestations, + apply_next_slots_with_attestations, + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + tick_and_add_block, + tick_and_run_on_attestation, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, + next_epoch, + next_slot, +) + + +@with_bellatrix_and_later +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_should_override_forkchoice_update__false(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 + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving a block of `GENESIS_SLOT + 1` slot + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + + # Proposer of next slot + head_root = spec.get_head(store) + + # Next slot + next_slot(spec, state) + slot = state.slot + + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + assert not spec.should_override_forkchoice_update(store, head_root) + + yield 'steps', test_steps + + +@with_bellatrix_and_later +@spec_state_test +def test_should_override_forkchoice_update__true(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 + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step(spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) + + # Fill epoch 1 to 3 + for _ in range(3): + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + + # Make an empty block + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + + # Fill a slot (parent) + state, store, signed_parent_block = yield from apply_next_slots_with_attestations( + spec, state, store, 1, True, True, test_steps) + + # Fill a slot with attestations to its parent + block = build_empty_block_for_next_slot(spec, state) + parent_block_slot = block.slot - 1 + block.body.attestations = get_valid_attestation_at_slot( + state, + spec, + parent_block_slot, + ) + signed_block = state_transition_and_sign_block(spec, state, block) + + # Make the head block late + attesting_cutoff = spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + attesting_cutoff + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_current_slot(store) == block.slot + + # Check conditions + head_root = spec.get_head(store) + head_block = store.blocks[head_root] + parent_root = head_block.parent_root + assert parent_root == signed_parent_block.message.hash_tree_root() + parent_block = store.blocks[parent_root] + + # Add attestations to the parent block + temp_state = state.copy() + next_slot(spec, temp_state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + 1 + on_tick_and_append_step(spec, store, current_time, test_steps) + attestations = get_valid_attestation_at_slot( + temp_state, + spec, + slot_to_attest=temp_state.slot - 1, + beacon_block_root=parent_root, + ) + current_slot = spec.get_current_slot(store) + for attestation in attestations: + yield from tick_and_run_on_attestation(spec, store, attestation, test_steps) + + current_slot = spec.get_current_slot(store) + proposal_slot = head_block.slot + 1 + + # The conditions in `get_proposer_head` + assert spec.is_head_late(store, head_root) + assert spec.is_shuffling_stable(proposal_slot) + assert spec.is_ffg_competitive(store, head_root, parent_root) + assert spec.is_finalization_ok(store, proposal_slot) + + # TODO: proposing_reorg_slot + + # Single slot re-org. + parent_slot_ok = parent_block.slot + 1 == head_block.slot + proposing_on_time = spec.is_proposing_on_time(store) + assert proposing_on_time + assert parent_slot_ok and proposal_slot == current_slot and proposing_on_time + + assert spec.is_head_weak(store, head_root) + assert spec.is_parent_strong(store, parent_root) + + assert spec.should_override_forkchoice_update(store, head_root) + + # TODO: export the `should_override_forkchoice_update` result to test vectors? + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index 4899e6224..6cd35c538 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -51,19 +51,21 @@ def run_attestation_processing(spec, state, attestation, valid=True): yield 'post', state -def build_attestation_data(spec, state, slot, index, shard=None): +def build_attestation_data(spec, state, slot, index, beacon_block_root=None, shard=None): assert state.slot >= slot - if slot == state.slot: - block_root = build_empty_block_for_next_slot(spec, state).parent_root + if beacon_block_root is not None: + pass + elif slot == state.slot: + beacon_block_root = build_empty_block_for_next_slot(spec, state).parent_root else: - block_root = spec.get_block_root_at_slot(state, slot) + beacon_block_root = spec.get_block_root_at_slot(state, slot) current_epoch_start_slot = spec.compute_start_slot_at_epoch(spec.get_current_epoch(state)) if slot < current_epoch_start_slot: epoch_boundary_root = spec.get_block_root(state, spec.get_previous_epoch(state)) elif slot == current_epoch_start_slot: - epoch_boundary_root = block_root + epoch_boundary_root = beacon_block_root else: epoch_boundary_root = spec.get_block_root(state, spec.get_current_epoch(state)) @@ -77,7 +79,7 @@ def build_attestation_data(spec, state, slot, index, shard=None): data = spec.AttestationData( slot=slot, index=index, - beacon_block_root=block_root, + beacon_block_root=beacon_block_root, source=spec.Checkpoint(epoch=source_epoch, root=source_root), target=spec.Checkpoint(epoch=spec.compute_epoch_at_slot(slot), root=epoch_boundary_root), ) @@ -91,6 +93,7 @@ def get_valid_attestation(spec, slot=None, index=None, filter_participant_set=None, + beacon_block_root=None, signed=False): # 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. @@ -99,9 +102,7 @@ def get_valid_attestation(spec, if index is None: index = 0 - attestation_data = build_attestation_data( - spec, state, slot=slot, index=index - ) + attestation_data = build_attestation_data(spec, state, slot=slot, index=index, beacon_block_root=beacon_block_root) beacon_committee = spec.get_beacon_committee( state, @@ -195,7 +196,7 @@ 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): +def get_valid_attestation_at_slot(state, spec, slot_to_attest, participation_fn=None, beacon_block_root=None): 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): @@ -210,7 +211,8 @@ def get_valid_attestation_at_slot(state, spec, slot_to_attest, participation_fn= slot_to_attest, index=index, signed=True, - filter_participant_set=participants_filter + filter_participant_set=participants_filter, + beacon_block_root=beacon_block_root, ) diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index e0e354722..ef80a3ab6 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -92,14 +92,11 @@ def add_attestations(spec, store, attestations, test_steps, is_from_block=False) def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_block=False): - 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.config.SECONDS_PER_SLOT - next_epoch_time = block_time + spec.SLOTS_PER_EPOCH * spec.config.SECONDS_PER_SLOT - - if store.time < next_epoch_time: - spec.on_tick(store, next_epoch_time) - test_steps.append({'tick': int(next_epoch_time)}) + # Make get_current_slot(store) >= attestation.data.slot + 1 + min_time_to_include = (attestation.data.slot + 1) * spec.config.SECONDS_PER_SLOT + if store.time < min_time_to_include: + spec.on_tick(store, min_time_to_include) + test_steps.append({'tick': int(min_time_to_include)}) yield from add_attestation(spec, store, attestation, test_steps, is_from_block) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_proposer_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_proposer_head.py new file mode 100644 index 000000000..083b292c0 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_proposer_head.py @@ -0,0 +1,154 @@ +from eth2spec.test.context import ( + spec_state_test, + with_altair_and_later, +) +from eth2spec.test.helpers.attestations import ( + get_valid_attestation_at_slot, +) +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.fork_choice import ( + apply_next_epoch_with_attestations, + apply_next_slots_with_attestations, + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + tick_and_add_block, + tick_and_run_on_attestation, +) +from eth2spec.test.helpers.state import ( + next_epoch, + next_slot, + state_transition_and_sign_block, +) + + +@with_altair_and_later +@spec_state_test +def test_basic_is_head_root(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 + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving a block of `GENESIS_SLOT + 1` slot + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + + # Proposer of next slot + head_root = spec.get_head(store) + + # Proposing next slot + next_slot(spec, state) + slot = state.slot + + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + proposer_head = spec.get_proposer_head(store, head_root, slot) + + assert proposer_head == head_root + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +def test_basic_is_parent_root(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 + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step(spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) + + # Fill epoch 1 to 3 + for _ in range(3): + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + + # Make an empty block + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + + # Fill a slot (parent) + state, store, signed_parent_block = yield from apply_next_slots_with_attestations( + spec, state, store, 1, True, True, test_steps) + + # Fill a slot with attestations to its parent + block = build_empty_block_for_next_slot(spec, state) + parent_block_slot = block.slot - 1 + block.body.attestations = get_valid_attestation_at_slot( + state, + spec, + parent_block_slot, + ) + signed_block = state_transition_and_sign_block(spec, state, block) + + # Make the head block late + attesting_cutoff = spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + attesting_cutoff + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + yield from tick_and_add_block(spec, store, signed_block, test_steps) + + # Check conditions + head_root = spec.get_head(store) + head_block = store.blocks[head_root] + parent_root = head_block.parent_root + assert parent_root == signed_parent_block.message.hash_tree_root() + parent_block = store.blocks[parent_root] + + # Proposing next slot + next_slot(spec, state) + slot = state.slot + + # Add attestations to the parent block + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + attestations = get_valid_attestation_at_slot( + state, + spec, + slot_to_attest=slot - 1, + beacon_block_root=parent_root, + ) + for attestation in attestations: + yield from tick_and_run_on_attestation(spec, store, attestation, test_steps) + + # The conditions in `get_proposer_head` + assert spec.is_head_late(store, head_root) + assert spec.is_shuffling_stable(slot) + assert spec.is_ffg_competitive(store, head_root, parent_root) + assert spec.is_finalization_ok(store, slot) + assert spec.is_proposing_on_time(store) + + parent_slot_ok = parent_block.slot + 1 == head_block.slot + current_time_ok = head_block.slot + 1 == slot + single_slot_reorg = parent_slot_ok and current_time_ok + assert single_slot_reorg + + assert spec.is_head_weak(store, head_root) + assert spec.is_parent_strong(store, parent_root) + + proposer_head = spec.get_proposer_head(store, head_root, state.slot) + assert proposer_head == parent_root + + # TODO: export the `proposer_head` result to test vectors? + + yield 'steps', test_steps