diff --git a/specs/core/0_fork-choice.md b/specs/core/0_fork-choice.md index 051c6ef8d..13d451c3e 100644 --- a/specs/core/0_fork-choice.md +++ b/specs/core/0_fork-choice.md @@ -238,6 +238,12 @@ def on_block(store: Store, block: BeaconBlock) -> None: ```python def on_attestation(store: Store, attestation: Attestation) -> None: + """ + Run ``on_attestation`` upon receiving a new ``attestation`` from either within a block or directly on the wire. + + An ``attestation`` that is asserted as invalid may be valid at a later time, + consider scheduling it for later processing in such case. + """ target = attestation.data.target # Attestations must be from the current or previous epoch @@ -248,10 +254,17 @@ def on_attestation(store: Store, attestation: Attestation) -> None: # Cannot calculate the current shuffling if have not seen the target assert target.root in store.blocks + # Attestations target be for a known block. If target block is unknown, delay consideration until the block is found + assert target.root in store.blocks # Attestations cannot be from future epochs. If they are, delay consideration until the epoch arrives base_state = store.block_states[target.root].copy() assert store.time >= base_state.genesis_time + compute_start_slot_at_epoch(target.epoch) * SECONDS_PER_SLOT + # Attestations must be for a known block. If block is unknown, delay consideration until the block is found + assert attestation.data.beacon_block_root in store.blocks + # Attestations must not be for blocks in the future. If not, the attestation should not be considered + assert store.blocks[attestation.data.beacon_block_root].slot <= attestation.data.slot + # Store target checkpoint state if not yet seen if target not in store.checkpoint_states: process_slots(base_state, compute_start_slot_at_epoch(target.epoch)) diff --git a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py index 8db55cce8..50e3fc070 100644 --- a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py +++ b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_attestation.py @@ -1,8 +1,7 @@ - from eth2spec.test.context import with_all_phases, spec_state_test from eth2spec.test.helpers.block import build_empty_block_for_next_slot -from eth2spec.test.helpers.attestations import get_valid_attestation -from eth2spec.test.helpers.state import state_transition_and_sign_block +from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation +from eth2spec.test.helpers.state import transition_to, state_transition_and_sign_block def run_on_attestation(spec, state, store, attestation, valid=True): @@ -89,18 +88,48 @@ def test_on_attestation_past_epoch(spec, state): @spec_state_test def test_on_attestation_target_not_in_store(spec, state): store = spec.get_genesis_store(state) - time = 100 + time = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH spec.on_tick(store, time) - # move to next epoch to make block new target - state.slot += spec.SLOTS_PER_EPOCH + # move to immediately before next epoch to make block new target + transition_to(spec, state, state.slot + spec.SLOTS_PER_EPOCH - 1) - block = build_empty_block_for_next_slot(spec, state) - state_transition_and_sign_block(spec, state, block) + target_block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, target_block) - # do not add block to store + # do not add target block to store + + attestation = get_valid_attestation(spec, state, slot=target_block.slot) + assert attestation.data.target.root == target_block.signing_root() + + run_on_attestation(spec, state, store, attestation, False) + + +@with_all_phases +@spec_state_test +def test_on_attestation_beacon_block_not_in_store(spec, state): + store = spec.get_genesis_store(state) + time = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + spec.on_tick(store, time) + + # move to immediately before next epoch to make block new target + transition_to(spec, state, state.slot + spec.SLOTS_PER_EPOCH - 1) + + target_block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, target_block) + + # store target in store + spec.on_block(store, target_block) + + head_block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, head_block) + + # do not add head block to store + + attestation = get_valid_attestation(spec, state, slot=head_block.slot) + assert attestation.data.target.root == target_block.signing_root() + assert attestation.data.beacon_block_root == head_block.signing_root() - attestation = get_valid_attestation(spec, state, slot=block.slot) run_on_attestation(spec, state, store, attestation, False) @@ -124,6 +153,26 @@ def test_on_attestation_future_epoch(spec, state): run_on_attestation(spec, state, store, attestation, False) +@with_all_phases +@spec_state_test +def test_on_attestation_future_block(spec, state): + store = spec.get_genesis_store(state) + time = spec.SECONDS_PER_SLOT * 5 + spec.on_tick(store, time) + + block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, block) + + spec.on_block(store, block) + + # attestation for slot immediately prior to the block being attested to + attestation = get_valid_attestation(spec, state, slot=block.slot - 1, signed=False) + attestation.data.beacon_block_root = block.signing_root() + sign_attestation(spec, state, attestation) + + run_on_attestation(spec, state, store, attestation, False) + + @with_all_phases @spec_state_test def test_on_attestation_same_slot(spec, state): diff --git a/test_libs/pyspec/eth2spec/test/helpers/state.py b/test_libs/pyspec/eth2spec/test/helpers/state.py index 27e946cbb..a26621c84 100644 --- a/test_libs/pyspec/eth2spec/test/helpers/state.py +++ b/test_libs/pyspec/eth2spec/test/helpers/state.py @@ -14,6 +14,16 @@ def next_slot(spec, state): spec.process_slots(state, state.slot + 1) +def transition_to(spec, state, slot): + """ + Transition to ``slot``. + """ + assert state.slot <= slot + for _ in range(slot - state.slot): + next_slot(spec, state) + assert state.slot == slot + + def next_epoch(spec, state): """ Transition to the start slot of the next epoch