diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index f6b007894..48248089b 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -74,17 +74,20 @@ def on_tick_and_append_step(spec, store, time, test_steps): def run_on_block(spec, store, signed_block, test_steps, valid=True): + yield get_block_file_name(signed_block), signed_block if not valid: try: spec.on_block(store, signed_block) - except AssertionError: + test_steps.append({ + 'block': get_block_file_name(signed_block), + 'valid': True, + }) return else: assert False spec.on_block(store, signed_block) - yield get_block_file_name(signed_block), signed_block test_steps.append({'block': get_block_file_name(signed_block)}) # An on_block step implies receiving block's attestations diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py new file mode 100644 index 000000000..e33c32e58 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py @@ -0,0 +1,339 @@ +from eth2spec.utils.ssz.ssz_impl import hash_tree_root + +from eth2spec.test.context import MINIMAL, spec_state_test, with_all_phases, with_presets +from eth2spec.test.helpers.attestations import next_epoch_with_attestations +from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block +from eth2spec.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + run_on_block, + tick_and_run_on_block, +) +from eth2spec.test.helpers.state import next_epoch, state_transition_and_sign_block + + +def apply_next_epoch_with_attestations(spec, state, store, test_steps=None): + if test_steps is None: + test_steps = [] + + _, new_signed_blocks, post_state = next_epoch_with_attestations(spec, state, True, False) + for signed_block in new_signed_blocks: + block = signed_block.message + block_root = hash_tree_root(block) + store.blocks[block_root] = block + store.block_states[block_root] = post_state + yield from tick_and_run_on_block(spec, store, signed_block, test_steps) + last_signed_block = signed_block + + return post_state, store, last_signed_block + + +@with_all_phases +@spec_state_test +def test_basic(spec, state): + # Initialization + test_steps = [] + 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_run_on_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + + # On receiving a block of next epoch + store.time = current_time + spec.config.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + block = build_empty_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH) + signed_block = state_transition_and_sign_block(spec, state, block) + yield from tick_and_run_on_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + + yield 'steps', test_steps + + # TODO: add tests for justified_root and finalized_root + + +@with_all_phases +@with_presets([MINIMAL], reason="too slow") +@spec_state_test +def test_on_block_checkpoints(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 + + # Run for 1 epoch with full attestations + next_epoch(spec, state) + on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) + state, store, last_signed_block = yield from apply_next_epoch_with_attestations(spec, state, store, test_steps) + last_block_root = hash_tree_root(last_signed_block.message) + assert spec.get_head(store) == last_block_root + + # Forward 1 epoch + next_epoch(spec, state) + on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) + + # Mock the finalized_checkpoint and build a block on it + fin_state = store.block_states[last_block_root] + fin_state.finalized_checkpoint = ( + store.block_states[last_block_root].current_justified_checkpoint + ) + + block = build_empty_block_for_next_slot(spec, fin_state) + signed_block = state_transition_and_sign_block(spec, fin_state.copy(), block) + yield from tick_and_run_on_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_on_block_future_block(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 + + # do not tick time + + # Fail receiving 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) + run_on_block(spec, store, signed_block, test_steps, valid=False) + + yield 'steps', test_steps + + +# @with_all_phases +# @spec_state_test +# def test_on_block_bad_parent_root(spec, state): +# test_steps = [] +# # Initialization +# store = get_genesis_forkchoice_store(spec, state) +# time = 100 +# on_tick_and_append_step(spec, store, time, test_steps) + +# # Fail receiving block of `GENESIS_SLOT + 1` slot +# block = build_empty_block_for_next_slot(spec, state) +# transition_unsigned_block(spec, state, block) +# block.state_root = state.hash_tree_root() + +# block.parent_root = b'\x45' * 32 + +# signed_block = sign_block(spec, state, block) + +# run_on_block(spec, store, signed_block, test_steps, valid=False) + + +# @with_all_phases +# @spec_state_test +# def test_on_block_before_finalized(spec, state): +# test_steps = [] +# # Initialization +# store = get_genesis_forkchoice_store(spec, state) +# time = 100 +# on_tick_and_append_step(spec, store, time, test_steps) + +# store.finalized_checkpoint = spec.Checkpoint( +# epoch=store.finalized_checkpoint.epoch + 2, +# root=store.finalized_checkpoint.root +# ) + +# # Fail receiving 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) +# run_on_block(spec, store, signed_block, test_steps, valid=False) + + +# @with_all_phases +# @spec_state_test +# def test_on_block_finalized_skip_slots(spec, state): +# test_steps = [] +# # Initialization +# store = get_genesis_forkchoice_store(spec, state) +# time = 100 +# on_tick_and_append_step(spec, store, time, test_steps) + +# store.finalized_checkpoint = spec.Checkpoint( +# epoch=store.finalized_checkpoint.epoch + 2, +# root=store.finalized_checkpoint.root +# ) + +# # Build block that includes the skipped slots up to finality in chain +# block = build_empty_block(spec, state, spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + 2) +# signed_block = state_transition_and_sign_block(spec, state, block) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# run_on_block(spec, store, signed_block, test_steps) + + +# @with_all_phases +# @spec_state_test +# def test_on_block_finalized_skip_slots_not_in_skip_chain(spec, state): +# test_steps = [] +# # Initialization +# transition_to(spec, state, state.slot + spec.SLOTS_PER_EPOCH - 1) +# block = build_empty_block_for_next_slot(spec, state) +# transition_unsigned_block(spec, state, block) +# block.state_root = state.hash_tree_root() +# store = spec.get_forkchoice_store(state, block) +# store.finalized_checkpoint = spec.Checkpoint( +# epoch=store.finalized_checkpoint.epoch + 2, +# root=store.finalized_checkpoint.root +# ) + +# # First transition through the epoch to ensure no skipped slots +# state, store, _ = apply_next_epoch_with_attestations(spec, state, store) + +# # Now build a block at later slot than finalized epoch +# # Includes finalized block in chain, but not at appropriate skip slot +# block = build_empty_block(spec, state, spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + 2) +# signed_block = state_transition_and_sign_block(spec, state, block) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# run_on_block(spec, store, signed_block, test_steps, valid=False) + + +# @with_all_phases +# @spec_state_test +# def test_on_block_update_justified_checkpoint_within_safe_slots(spec, state): +# test_steps = [] +# # Initialization +# store = get_genesis_forkchoice_store(spec, state) +# time = 0 +# on_tick_and_append_step(spec, store, time, test_steps) + +# next_epoch(spec, state) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# state, store, last_signed_block = apply_next_epoch_with_attestations(spec, state, store) +# next_epoch(spec, state) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# last_block_root = hash_tree_root(last_signed_block.message) + +# # Mock the justified checkpoint +# just_state = store.block_states[last_block_root] +# new_justified = spec.Checkpoint( +# epoch=just_state.current_justified_checkpoint.epoch + 1, +# root=b'\x77' * 32, +# ) +# just_state.current_justified_checkpoint = new_justified + +# block = build_empty_block_for_next_slot(spec, just_state) +# signed_block = state_transition_and_sign_block(spec, deepcopy(just_state), block) +# assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH < spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED +# run_on_block(spec, store, signed_block, test_steps) + +# assert store.justified_checkpoint == new_justified + + +# @with_all_phases +# @spec_state_test +# def test_on_block_outside_safe_slots_and_multiple_better_justified(spec, state): +# test_steps = [] +# # Initialization +# store = get_genesis_forkchoice_store(spec, state) +# time = 0 +# on_tick_and_append_step(spec, store, time, test_steps) + +# next_epoch(spec, state) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# state, store, last_signed_block = apply_next_epoch_with_attestations(spec, state, store) +# next_epoch(spec, state) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# last_block_root = hash_tree_root(last_signed_block.message) + +# # Mock justified block in store +# just_block = build_empty_block_for_next_slot(spec, state) +# # Slot is same as justified checkpoint so does not trigger an override in the store +# just_block.slot = spec.compute_start_slot_at_epoch(store.justified_checkpoint.epoch) +# store.blocks[just_block.hash_tree_root()] = just_block + +# # Step time past safe slots +# time = store.time + spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED * spec.config.SECONDS_PER_SLOT +# on_tick_and_append_step(spec, store, time, test_steps) +# assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED + +# previously_justified = store.justified_checkpoint + +# # Add a series of new blocks with "better" justifications +# best_justified_checkpoint = spec.Checkpoint(epoch=0) +# for i in range(3, 0, -1): +# just_state = store.block_states[last_block_root] +# new_justified = spec.Checkpoint( +# epoch=previously_justified.epoch + i, +# root=just_block.hash_tree_root(), +# ) +# if new_justified.epoch > best_justified_checkpoint.epoch: +# best_justified_checkpoint = new_justified + +# just_state.current_justified_checkpoint = new_justified + +# block = build_empty_block_for_next_slot(spec, just_state) +# signed_block = state_transition_and_sign_block(spec, deepcopy(just_state), block) + +# run_on_block(spec, store, signed_block, test_steps) + +# assert store.justified_checkpoint == previously_justified +# # ensure the best from the series was stored +# assert store.best_justified_checkpoint == best_justified_checkpoint + + +# @with_all_phases +# @spec_state_test +# def test_on_block_outside_safe_slots_but_finality(spec, state): +# test_steps = [] +# # Initialization +# store = get_genesis_forkchoice_store(spec, state) +# time = 100 +# on_tick_and_append_step(spec, store, time, test_steps) + +# next_epoch(spec, state) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# state, store, last_signed_block = apply_next_epoch_with_attestations(spec, state, store) +# next_epoch(spec, state) +# on_tick_and_append_step(spec, store, store.time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) +# last_block_root = hash_tree_root(last_signed_block.message) + +# # Mock justified block in store +# just_block = build_empty_block_for_next_slot(spec, state) +# # Slot is same as justified checkpoint so does not trigger an override in the store +# just_block.slot = spec.compute_start_slot_at_epoch(store.justified_checkpoint.epoch) +# store.blocks[just_block.hash_tree_root()] = just_block + +# # Step time past safe slots +# time = store.time + spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED * spec.config.SECONDS_PER_SLOT +# on_tick_and_append_step(spec, store, time, test_steps) +# assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED + +# # Mock justified and finalized update in state +# just_fin_state = store.block_states[last_block_root] +# new_justified = spec.Checkpoint( +# epoch=store.justified_checkpoint.epoch + 1, +# root=just_block.hash_tree_root(), +# ) +# new_finalized = spec.Checkpoint( +# epoch=store.finalized_checkpoint.epoch + 1, +# root=just_block.parent_root, +# ) +# just_fin_state.current_justified_checkpoint = new_justified +# just_fin_state.finalized_checkpoint = new_finalized + +# # Build and add block that includes the new justified/finalized info +# block = build_empty_block_for_next_slot(spec, just_fin_state) +# signed_block = state_transition_and_sign_block(spec, deepcopy(just_fin_state), block) + +# run_on_block(spec, store, signed_block, test_steps) + +# assert store.finalized_checkpoint == new_finalized +# assert store.justified_checkpoint == new_justified diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 832ce9dd1..199b93784 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -28,7 +28,11 @@ The steps to execute in sequence. There may be multiple items of the following t The parameter that is required for executing `on_tick(store, time)`. ```yaml -{ tick: int } -- to execute `on_tick(store, time)` +{ + tick: int -- to execute `on_tick(store, time)`. + valid: bool -- optional, default to `True`. + If it's `False`, this execution step is expected to be invalid. +} ``` After this step, the `store` object may have been updated. @@ -38,7 +42,12 @@ After this step, the `store` object may have been updated. The parameter that is required for executing `on_attestation(store, attestation)`. ```yaml -{ attestation: string } -- the name of the `attestation_<32-byte-root>.ssz_snappy` file. To execute `on_attestation(store, attestation)` with the given attestation. +{ + attestation: string -- the name of the `attestation_<32-byte-root>.ssz_snappy` file. + To execute `on_attestation(store, attestation)` with the given attestation. + valid: bool -- optional, default to `True`. + If it's `False`, this execution step is expected to be invalid. +} ``` The file is located in the same folder (see below). @@ -49,7 +58,12 @@ After this step, the `store` object may have been updated. The parameter that is required for executing `on_block(store, block)`. ```yaml -{ block: string } -- the name of the `block_<32-byte-root>.ssz_snappy` file. To execute `on_block(store, block)` with the given attestation. +{ + block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. + To execute `on_block(store, block)` with the given attestation. + valid: bool -- optional, default to `True`. + If it's `False`, this execution step is expected to be invalid. +} ``` The file is located in the same folder (see below). diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index f162d9564..9b6325ccf 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -4,7 +4,8 @@ from eth2spec.test.helpers.constants import PHASE0, ALTAIR, MERGE if __name__ == "__main__": phase_0_mods = {key: 'eth2spec.test.phase0.fork_choice.test_' + key for key in [ - 'get_head', + # 'get_head', + 'on_block', ]} # No additional Altair specific finality tests, yet. altair_mods = phase_0_mods