diff --git a/presets/mainnet/phase0.yaml b/presets/mainnet/phase0.yaml index 89bb97d6a..02bc96c8c 100644 --- a/presets/mainnet/phase0.yaml +++ b/presets/mainnet/phase0.yaml @@ -18,12 +18,6 @@ HYSTERESIS_DOWNWARD_MULTIPLIER: 1 HYSTERESIS_UPWARD_MULTIPLIER: 5 -# Fork Choice -# --------------------------------------------------------------- -# 2**3 (= 8) -SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 8 - - # Gwei values # --------------------------------------------------------------- # 2**0 * 10**9 (= 1,000,000,000) Gwei diff --git a/presets/minimal/phase0.yaml b/presets/minimal/phase0.yaml index 2c6fbb369..e7028f5a4 100644 --- a/presets/minimal/phase0.yaml +++ b/presets/minimal/phase0.yaml @@ -18,12 +18,6 @@ HYSTERESIS_DOWNWARD_MULTIPLIER: 1 HYSTERESIS_UPWARD_MULTIPLIER: 5 -# Fork Choice -# --------------------------------------------------------------- -# 2**1 (= 1) -SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 2 - - # Gwei values # --------------------------------------------------------------- # 2**0 * 10**9 (= 1,000,000,000) Gwei diff --git a/specs/bellatrix/fork-choice.md b/specs/bellatrix/fork-choice.md index 94d068827..ed7d60a93 100644 --- a/specs/bellatrix/fork-choice.md +++ b/specs/bellatrix/fork-choice.md @@ -174,6 +174,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Check the block is valid and compute the post-state state = pre_state.copy() + block_root = hash_tree_root(block) state_transition(state, signed_block, True) # [New in Bellatrix] @@ -181,9 +182,9 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: validate_merge_block(block) # Add new block to the store - store.blocks[hash_tree_root(block)] = block + store.blocks[block_root] = block # Add new state for this block to the store - store.block_states[hash_tree_root(block)] = state + store.block_states[block_root] = state # Add proposer score boost if the block is timely time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT @@ -191,15 +192,9 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_boost_root = hash_tree_root(block) - # Update justified checkpoint - if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch: - store.best_justified_checkpoint = state.current_justified_checkpoint - if should_update_justified_checkpoint(store, state.current_justified_checkpoint): - store.justified_checkpoint = state.current_justified_checkpoint + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) - # Update finalized checkpoint - if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch: - store.finalized_checkpoint = state.finalized_checkpoint - store.justified_checkpoint = state.current_justified_checkpoint + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) ``` diff --git a/specs/deneb/fork-choice.md b/specs/deneb/fork-choice.md index 830c48764..61714cf1a 100644 --- a/specs/deneb/fork-choice.md +++ b/specs/deneb/fork-choice.md @@ -91,6 +91,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Check the block is valid and compute the post-state state = pre_state.copy() + block_root = hash_tree_root(block) state_transition(state, signed_block, True) # Check the merge transition @@ -98,9 +99,9 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: validate_merge_block(block) # Add new block to the store - store.blocks[hash_tree_root(block)] = block + store.blocks[block_root] = block # Add new state for this block to the store - store.block_states[hash_tree_root(block)] = state + store.block_states[block_root] = state # Add proposer score boost if the block is timely time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT @@ -108,15 +109,9 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_boost_root = hash_tree_root(block) - # Update justified checkpoint - if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch: - store.best_justified_checkpoint = state.current_justified_checkpoint - if should_update_justified_checkpoint(store, state.current_justified_checkpoint): - store.justified_checkpoint = state.current_justified_checkpoint + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) - # Update finalized checkpoint - if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch: - store.finalized_checkpoint = state.finalized_checkpoint - store.justified_checkpoint = state.current_justified_checkpoint + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) ``` diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 8681975ca..6e281d5c3 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -8,10 +8,10 @@ - [Introduction](#introduction) - [Fork choice](#fork-choice) - [Constant](#constant) - - [Preset](#preset) - [Configuration](#configuration) - [Helpers](#helpers) - [`LatestMessage`](#latestmessage) + - [`is_previous_epoch_justified`](#is_previous_epoch_justified) - [`Store`](#store) - [`get_forkchoice_store`](#get_forkchoice_store) - [`get_slots_since_genesis`](#get_slots_since_genesis) @@ -19,10 +19,16 @@ - [`compute_slots_since_epoch_start`](#compute_slots_since_epoch_start) - [`get_ancestor`](#get_ancestor) - [`get_weight`](#get_weight) + - [`get_voting_source`](#get_voting_source) - [`filter_block_tree`](#filter_block_tree) - [`get_filtered_block_tree`](#get_filtered_block_tree) - [`get_head`](#get_head) - - [`should_update_justified_checkpoint`](#should_update_justified_checkpoint) + - [`update_checkpoints`](#update_checkpoints) + - [`update_unrealized_checkpoints`](#update_unrealized_checkpoints) + - [Pull-up tip helpers](#pull-up-tip-helpers) + - [`compute_pulled_up_tip`](#compute_pulled_up_tip) + - [`on_tick` helpers](#on_tick-helpers) + - [`on_tick_per_slot`](#on_tick_per_slot) - [`on_attestation` helpers](#on_attestation-helpers) - [`validate_target_epoch_against_current_time`](#validate_target_epoch_against_current_time) - [`validate_on_attestation`](#validate_on_attestation) @@ -67,12 +73,6 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass | -------------------- | ----------- | | `INTERVALS_PER_SLOT` | `uint64(3)` | -### Preset - -| Name | Value | Unit | Duration | -| -------------------------------- | ------------ | :---: | :--------: | -| `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds | - ### Configuration | Name | Value | @@ -92,8 +92,26 @@ class LatestMessage(object): root: Root ``` + +### `is_previous_epoch_justified` + +```python +def is_previous_epoch_justified(store: Store) -> bool: + current_slot = get_current_slot(store) + current_epoch = compute_epoch_at_slot(current_slot) + return store.justified_checkpoint.epoch + 1 == current_epoch +``` + + #### `Store` +The `Store` is responsible for tracking information required for the fork choice algorithm. The important fields being tracked are described below: + +- `justified_checkpoint`: the justified checkpoint used as the starting point for the LMD GHOST fork choice algorithm. +- `finalized_checkpoint`: the highest known finalized checkpoint. The fork choice only considers blocks that are not conflicting with this checkpoint. +- `unrealized_justified_checkpoint` & `unrealized_finalized_checkpoint`: these track the highest justified & finalized checkpoints resp., without regard to whether on-chain ***realization*** has occurred, i.e. FFG processing of new attestations within the state transition function. This is an important distinction from `justified_checkpoint` & `finalized_checkpoint`, because they will only track the checkpoints that are realized on-chain. Note that on-chain processing of FFG information only happens at epoch boundaries. +- `unrealized_justifications`: stores a map of block root to the unrealized justified checkpoint observed in that block. + ```python @dataclass class Store(object): @@ -101,13 +119,15 @@ class Store(object): genesis_time: uint64 justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint - best_justified_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_checkpoint: Checkpoint proposer_boost_root: Root equivocating_indices: Set[ValidatorIndex] blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) + unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) ``` #### `get_forkchoice_store` @@ -130,12 +150,14 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - genesis_time=anchor_state.genesis_time, justified_checkpoint=justified_checkpoint, finalized_checkpoint=finalized_checkpoint, - best_justified_checkpoint=justified_checkpoint, + unrealized_justified_checkpoint=justified_checkpoint, + unrealized_finalized_checkpoint=finalized_checkpoint, proposer_boost_root=proposer_boost_root, equivocating_indices=set(), blocks={anchor_root: copy(anchor_block)}, block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, + unrealized_justifications={anchor_root: justified_checkpoint} ) ``` @@ -167,11 +189,7 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> Root: block = store.blocks[root] if block.slot > slot: return get_ancestor(store, block.parent_root, slot) - elif block.slot == slot: - return root - else: - # root is older than queried slot, thus a skip slot. Return most recent root prior to slot - return root + return root ``` #### `get_weight` @@ -179,9 +197,12 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> Root: ```python def get_weight(store: Store, root: Root) -> Gwei: state = store.checkpoint_states[store.justified_checkpoint] - active_indices = get_active_validator_indices(state, get_current_epoch(state)) + unslashed_and_active_indices = [ + i for i in get_active_validator_indices(state, get_current_epoch(state)) + if not state.validators[i].slashed + ] attestation_score = Gwei(sum( - state.validators[i].effective_balance for i in active_indices + state.validators[i].effective_balance for i in unslashed_and_active_indices if (i in store.latest_messages and i not in store.equivocating_indices and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) @@ -199,8 +220,30 @@ def get_weight(store: Store, root: Root) -> Gwei: return attestation_score + proposer_score ``` +#### `get_voting_source` + +```python +def get_voting_source(store: Store, block_root: Root) -> Checkpoint: + """ + Compute the voting source checkpoint in event that block with root ``block_root`` is the head block + """ + block = store.blocks[block_root] + current_epoch = compute_epoch_at_slot(get_current_slot(store)) + block_epoch = compute_epoch_at_slot(block.slot) + if current_epoch > block_epoch: + # The block is from a prior epoch, the voting source will be pulled-up + return store.unrealized_justifications[block_root] + else: + # The block is not from a prior epoch, therefore the voting source is not pulled up + head_state = store.block_states[block_root] + return head_state.current_justified_checkpoint + +``` + #### `filter_block_tree` +*Note*: External calls to `filter_block_tree` (i.e., any calls that are not made by the recursive logic in this function) MUST set `block_root` to `store.justified_checkpoint`. + ```python def filter_block_tree(store: Store, block_root: Root, blocks: Dict[Root, BeaconBlock]) -> bool: block = store.blocks[block_root] @@ -218,17 +261,29 @@ def filter_block_tree(store: Store, block_root: Root, blocks: Dict[Root, BeaconB return True return False - # If leaf block, check finalized/justified checkpoints as matching latest. - head_state = store.block_states[block_root] + current_epoch = compute_epoch_at_slot(get_current_slot(store)) + voting_source = get_voting_source(store, block_root) + # The voting source should be at the same height as the store's justified checkpoint correct_justified = ( store.justified_checkpoint.epoch == GENESIS_EPOCH - or head_state.current_justified_checkpoint == store.justified_checkpoint + or voting_source.epoch == store.justified_checkpoint.epoch ) + + # If the previous epoch is justified, the block should be pulled-up. In this case, check that unrealized + # justification is higher than the store and that the voting source is not more than two epochs ago + if not correct_justified and is_previous_epoch_justified(store): + correct_justified = ( + store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch and + voting_source.epoch + 2 >= current_epoch + ) + + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) correct_finalized = ( store.finalized_checkpoint.epoch == GENESIS_EPOCH - or head_state.finalized_checkpoint == store.finalized_checkpoint + or store.finalized_checkpoint.root == get_ancestor(store, block_root, finalized_slot) ) + # If expected finalized/justified, add to viable block-tree and signal viability to parent. if correct_justified and correct_finalized: blocks[block_root] = block @@ -272,25 +327,80 @@ def get_head(store: Store) -> Root: head = max(children, key=lambda root: (get_weight(store, root), root)) ``` -#### `should_update_justified_checkpoint` +#### `update_checkpoints` ```python -def should_update_justified_checkpoint(store: Store, new_justified_checkpoint: Checkpoint) -> bool: +def update_checkpoints(store: Store, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint) -> None: """ - To address the bouncing attack, only update conflicting justified - checkpoints in the fork choice if in the early slots of the epoch. - Otherwise, delay incorporation of new justified checkpoint until next epoch boundary. - - See https://ethresear.ch/t/prevention-of-bouncing-attack-on-ffg/6114 for more detailed analysis and discussion. + Update checkpoints in store if necessary """ - if compute_slots_since_epoch_start(get_current_slot(store)) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED: - return True + # Update justified checkpoint + if justified_checkpoint.epoch > store.justified_checkpoint.epoch: + store.justified_checkpoint = justified_checkpoint - justified_slot = compute_start_slot_at_epoch(store.justified_checkpoint.epoch) - if not get_ancestor(store, new_justified_checkpoint.root, justified_slot) == store.justified_checkpoint.root: - return False + # Update finalized checkpoint + if finalized_checkpoint.epoch > store.finalized_checkpoint.epoch: + store.finalized_checkpoint = finalized_checkpoint +``` - return True +#### `update_unrealized_checkpoints` + +```python +def update_unrealized_checkpoints(store: Store, unrealized_justified_checkpoint: Checkpoint, + unrealized_finalized_checkpoint: Checkpoint) -> None: + """ + Update unrealized checkpoints in store if necessary + """ + # Update unrealized justified checkpoint + if unrealized_justified_checkpoint.epoch > store.unrealized_justified_checkpoint.epoch: + store.unrealized_justified_checkpoint = unrealized_justified_checkpoint + + # Update unrealized finalized checkpoint + if unrealized_finalized_checkpoint.epoch > store.unrealized_finalized_checkpoint.epoch: + store.unrealized_finalized_checkpoint = unrealized_finalized_checkpoint +``` + + +#### Pull-up tip helpers + +##### `compute_pulled_up_tip` + +```python +def compute_pulled_up_tip(store: Store, block_root: Root) -> None: + state = store.block_states[block_root].copy() + # Pull up the post-state of the block to the next epoch boundary + process_justification_and_finalization(state) + + store.unrealized_justifications[block_root] = state.current_justified_checkpoint + update_unrealized_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # If the block is from a prior epoch, apply the realized values + block_epoch = compute_epoch_at_slot(store.blocks[block_root].slot) + current_epoch = compute_epoch_at_slot(get_current_slot(store)) + if block_epoch < current_epoch: + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) +``` + +#### `on_tick` helpers + +##### `on_tick_per_slot` + +```python +def on_tick_per_slot(store: Store, time: uint64) -> None: + previous_slot = get_current_slot(store) + + # Update store time + store.time = time + + current_slot = get_current_slot(store) + + # If this is a new slot, reset store.proposer_boost_root + if current_slot > previous_slot: + store.proposer_boost_root = Root() + + # If a new epoch, pull-up justification and finalization from previous epoch + if current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0: + update_checkpoints(store, store.unrealized_justified_checkpoint, store.unrealized_finalized_checkpoint) ``` #### `on_attestation` helpers @@ -323,7 +433,7 @@ def validate_on_attestation(store: Store, attestation: Attestation, is_from_bloc # Check that the epoch number and slot number are matching assert target.epoch == compute_epoch_at_slot(attestation.data.slot) - # Attestations target be for a known block. If target block is unknown, delay consideration until the block is found + # Attestation target must be for a known block. If target block is unknown, delay consideration until block is found assert target.root in store.blocks # Attestations must be for a known block. If block is unknown, delay consideration until the block is found @@ -371,27 +481,13 @@ def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIn ```python def on_tick(store: Store, time: uint64) -> None: - previous_slot = get_current_slot(store) - - # update store time - store.time = time - - current_slot = get_current_slot(store) - - # Reset store.proposer_boost_root if this is a new slot - if current_slot > previous_slot: - store.proposer_boost_root = Root() - - # Not a new epoch, return - if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0): - return - - # Update store.justified_checkpoint if a better checkpoint on the store.finalized_checkpoint chain - if store.best_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) - ancestor_at_finalized_slot = get_ancestor(store, store.best_justified_checkpoint.root, finalized_slot) - if ancestor_at_finalized_slot == store.finalized_checkpoint.root: - store.justified_checkpoint = store.best_justified_checkpoint + # If the ``store.time`` falls behind, while loop catches up slot by slot + # to ensure that every previous slot is processed with ``on_tick_per_slot`` + tick_slot = (time - store.genesis_time) // SECONDS_PER_SLOT + while get_current_slot(store) < tick_slot: + previous_time = store.genesis_time + (get_current_slot(store) + 1) * SECONDS_PER_SLOT + on_tick_per_slot(store, previous_time) + on_tick_per_slot(store, time) ``` #### `on_block` @@ -414,11 +510,12 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Check the block is valid and compute the post-state state = pre_state.copy() + block_root = hash_tree_root(block) state_transition(state, signed_block, True) # Add new block to the store - store.blocks[hash_tree_root(block)] = block + store.blocks[block_root] = block # Add new state for this block to the store - store.block_states[hash_tree_root(block)] = state + store.block_states[block_root] = state # Add proposer score boost if the block is timely time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT @@ -426,17 +523,11 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_boost_root = hash_tree_root(block) - # Update justified checkpoint - if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch: - store.best_justified_checkpoint = state.current_justified_checkpoint - if should_update_justified_checkpoint(store, state.current_justified_checkpoint): - store.justified_checkpoint = state.current_justified_checkpoint + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) - # Update finalized checkpoint - if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch: - store.finalized_checkpoint = state.finalized_checkpoint - store.justified_checkpoint = state.current_justified_checkpoint + # Eagerly compute unrealized justification and finality + compute_pulled_up_tip(store, block_root) ``` #### `on_attestation` diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index c60d047b9..360e194f5 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -187,7 +187,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): 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): @@ -262,7 +262,7 @@ def state_transition_with_full_block(spec, if fill_cur_epoch and state.slot >= spec.MIN_ATTESTATION_INCLUSION_DELAY: slot_to_attest = state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY + 1 if slot_to_attest >= spec.compute_start_slot_at_epoch(spec.get_current_epoch(state)): - attestations = _get_valid_attestation_at_slot( + attestations = get_valid_attestation_at_slot( state, spec, slot_to_attest, @@ -272,7 +272,7 @@ def state_transition_with_full_block(spec, block.body.attestations.append(attestation) if fill_prev_epoch: slot_to_attest = state.slot - spec.SLOTS_PER_EPOCH + 1 - attestations = _get_valid_attestation_at_slot( + attestations = get_valid_attestation_at_slot( state, spec, slot_to_attest, @@ -300,7 +300,7 @@ def state_transition_with_full_attestations_block(spec, state, fill_cur_epoch, f slots = state.slot % spec.SLOTS_PER_EPOCH for slot_offset in range(slots): target_slot = state.slot - slot_offset - attestations += _get_valid_attestation_at_slot( + attestations += get_valid_attestation_at_slot( state, spec, target_slot, @@ -311,7 +311,7 @@ def state_transition_with_full_attestations_block(spec, state, fill_cur_epoch, f slots = spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH for slot_offset in range(1, slots): target_slot = state.slot - (state.slot % spec.SLOTS_PER_EPOCH) - slot_offset - attestations += _get_valid_attestation_at_slot( + attestations += get_valid_attestation_at_slot( state, spec, target_slot, diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index bd8abd95b..af231d87f 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -3,6 +3,7 @@ from eth2spec.test.exceptions import BlockNotFoundException from eth2spec.test.helpers.attestations import ( next_epoch_with_attestations, next_slots_with_attestations, + state_transition_with_full_block, ) @@ -16,12 +17,13 @@ def get_anchor_root(spec, state): def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, merge_block=False, block_not_found=False, is_optimistic=False): pre_state = store.block_states[signed_block.message.parent_root] - block_time = pre_state.genesis_time + signed_block.message.slot * spec.config.SECONDS_PER_SLOT if merge_block: assert spec.is_merge_transition_block(pre_state, signed_block.message.body) - if store.time < block_time: - on_tick_and_append_step(spec, store, block_time, test_steps) + block_time = pre_state.genesis_time + signed_block.message.slot * spec.config.SECONDS_PER_SLOT + while store.time < block_time: + time = pre_state.genesis_time + (spec.get_current_slot(store) + 1) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) post_state = yield from add_block( spec, store, signed_block, test_steps, @@ -39,6 +41,11 @@ def add_attestation(spec, store, attestation, test_steps, is_from_block=False): test_steps.append({'attestation': get_attestation_file_name(attestation)}) +def add_attestations(spec, store, attestations, test_steps, is_from_block=False): + for attestation in attestations: + yield from add_attestation(spec, store, attestation, test_steps, is_from_block=is_from_block) + + 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)] @@ -90,6 +97,7 @@ def get_attester_slashing_file_name(attester_slashing): def on_tick_and_append_step(spec, store, time, test_steps): spec.on_tick(store, time) test_steps.append({'tick': int(time)}) + output_store_checks(spec, store, test_steps) def run_on_block(spec, store, signed_block, valid=True): @@ -153,25 +161,7 @@ def add_block(spec, assert store.blocks[block_root] == signed_block.message assert store.block_states[block_root].hash_tree_root() == signed_block.message.state_root if not is_optimistic: - test_steps.append({ - 'checks': { - 'time': int(store.time), - 'head': get_formatted_head_output(spec, store), - 'justified_checkpoint': { - 'epoch': int(store.justified_checkpoint.epoch), - 'root': encode_hex(store.justified_checkpoint.root), - }, - 'finalized_checkpoint': { - 'epoch': int(store.finalized_checkpoint.epoch), - 'root': encode_hex(store.finalized_checkpoint.root), - }, - 'best_justified_checkpoint': { - 'epoch': int(store.best_justified_checkpoint.epoch), - 'root': encode_hex(store.best_justified_checkpoint.root), - }, - 'proposer_boost_root': encode_hex(store.proposer_boost_root), - } - }) + output_store_checks(spec, store, test_steps) return store.block_states[signed_block.message.hash_tree_root()] @@ -217,6 +207,32 @@ def get_formatted_head_output(spec, store): } +def output_head_check(spec, store, test_steps): + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + +def output_store_checks(spec, store, test_steps): + test_steps.append({ + 'checks': { + 'time': int(store.time), + 'head': get_formatted_head_output(spec, store), + 'justified_checkpoint': { + 'epoch': int(store.justified_checkpoint.epoch), + 'root': encode_hex(store.justified_checkpoint.root), + }, + 'finalized_checkpoint': { + 'epoch': int(store.finalized_checkpoint.epoch), + 'root': encode_hex(store.finalized_checkpoint.root), + }, + 'proposer_boost_root': encode_hex(store.proposer_boost_root), + } + }) + + def apply_next_epoch_with_attestations(spec, state, store, @@ -263,6 +279,39 @@ def apply_next_slots_with_attestations(spec, return post_state, store, last_signed_block +def is_ready_to_justify(spec, state): + """ + Check if the given ``state`` will trigger justification updates at epoch boundary. + """ + temp_state = state.copy() + spec.process_justification_and_finalization(temp_state) + return temp_state.current_justified_checkpoint.epoch > state.current_justified_checkpoint.epoch + + +def find_next_justifying_slot(spec, + state, + fill_cur_epoch, + fill_prev_epoch, + participation_fn=None): + temp_state = state.copy() + + signed_blocks = [] + justifying_slot = None + while justifying_slot is None: + signed_block = state_transition_with_full_block( + spec, + temp_state, + fill_cur_epoch, + fill_prev_epoch, + participation_fn, + ) + signed_blocks.append(signed_block) + if is_ready_to_justify(spec, temp_state): + justifying_slot = temp_state.slot + + return signed_blocks, justifying_slot + + def get_pow_block_file_name(pow_block): return f"pow_block_{encode_hex(pow_block.block_hash)}" diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index 990c42031..2107a470a 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -1,9 +1,9 @@ import random -from eth_utils import encode_hex from eth2spec.test.context import ( spec_state_test, with_all_phases, + with_altair_and_later, with_presets, ) from eth2spec.test.helpers.attestations import get_valid_attestation, next_epoch_with_attestations @@ -22,6 +22,8 @@ from eth2spec.test.helpers.fork_choice import ( add_attestation, tick_and_run_on_attestation, tick_and_add_block, + output_head_check, + apply_next_epoch_with_attestations, ) from eth2spec.test.helpers.forks import ( is_post_altair, @@ -71,11 +73,7 @@ def test_chain_no_attestations(spec, state): anchor_root = get_anchor_root(spec, state) assert spec.get_head(store) == anchor_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) # On receiving a block of `GENESIS_SLOT + 1` slot block_1 = build_empty_block_for_next_slot(spec, state) @@ -88,11 +86,7 @@ def test_chain_no_attestations(spec, state): yield from tick_and_add_block(spec, store, signed_block_2, test_steps) assert spec.get_head(store) == spec.hash_tree_root(block_2) - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) yield 'steps', test_steps @@ -109,11 +103,7 @@ def test_split_tie_breaker_no_attestations(spec, state): yield 'anchor_block', anchor_block anchor_root = get_anchor_root(spec, state) assert spec.get_head(store) == anchor_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) # Create block at slot 1 block_1_state = genesis_state.copy() @@ -135,11 +125,7 @@ def test_split_tie_breaker_no_attestations(spec, state): highest_root = max(spec.hash_tree_root(block_1), spec.hash_tree_root(block_2)) assert spec.get_head(store) == highest_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) yield 'steps', test_steps @@ -156,11 +142,7 @@ def test_shorter_chain_but_heavier_weight(spec, state): yield 'anchor_block', anchor_block anchor_root = get_anchor_root(spec, state) assert spec.get_head(store) == anchor_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) # build longer tree long_state = genesis_state.copy() @@ -183,11 +165,7 @@ def test_shorter_chain_but_heavier_weight(spec, state): yield from tick_and_run_on_attestation(spec, store, short_attestation, test_steps) assert spec.get_head(store) == spec.hash_tree_root(short_block) - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) yield 'steps', test_steps @@ -203,11 +181,7 @@ def test_filtered_block_tree(spec, state): yield 'anchor_block', anchor_block anchor_root = get_anchor_root(spec, state) assert spec.get_head(store) == anchor_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) # transition state past initial couple of epochs next_epoch(spec, state) @@ -227,13 +201,7 @@ def test_filtered_block_tree(spec, state): # the last block in the branch should be the head expected_head_root = spec.hash_tree_root(signed_blocks[-1].message) assert spec.get_head(store) == expected_head_root - - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - 'justified_checkpoint_root': encode_hex(store.justified_checkpoint.root), - } - }) + output_head_check(spec, store, test_steps) # # create branch containing the justified block but not containing enough on @@ -274,11 +242,7 @@ def test_filtered_block_tree(spec, state): # ensure that get_head still returns the head from the previous branch assert spec.get_head(store) == expected_head_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store) - } - }) + output_head_check(spec, store, test_steps) yield 'steps', test_steps @@ -295,11 +259,7 @@ def test_proposer_boost_correct_head(spec, state): yield 'anchor_block', anchor_block anchor_root = get_anchor_root(spec, state) assert spec.get_head(store) == anchor_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) # Build block that serves as head ONLY on timely arrival, and ONLY in that slot state_1 = genesis_state.copy() @@ -337,19 +297,14 @@ def test_proposer_boost_correct_head(spec, state): on_tick_and_append_step(spec, store, time, test_steps) assert store.proposer_boost_root == spec.Root() assert spec.get_head(store) == spec.hash_tree_root(block_2) - - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) yield 'steps', test_steps @with_all_phases @spec_state_test -def test_discard_equivocations(spec, state): +def test_discard_equivocations_on_attester_slashing(spec, state): test_steps = [] genesis_state = state.copy() @@ -359,11 +314,7 @@ def test_discard_equivocations(spec, state): yield 'anchor_block', anchor_block anchor_root = get_anchor_root(spec, state) assert spec.get_head(store) == anchor_root - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) # Build block that serves as head before discarding equivocations state_1 = genesis_state.copy() @@ -418,11 +369,359 @@ def test_discard_equivocations(spec, state): # The head should revert to block_2 yield from add_attester_slashing(spec, store, attester_slashing, test_steps) assert spec.get_head(store) == spec.hash_tree_root(block_2) - - test_steps.append({ - 'checks': { - 'head': get_formatted_head_output(spec, store), - } - }) + output_head_check(spec, store, test_steps) yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_discard_equivocations_slashed_validator_censoring(spec, state): + # Check that the store does not count LMD votes from validators that are slashed in the justified state + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 0 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 0 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 0 + + # We will slash all validators voting at the 2nd slot of epoch 0 + current_slot = spec.get_current_slot(store) + eqv_slot = current_slot + 1 + eqv_epoch = spec.compute_epoch_at_slot(eqv_slot) + assert eqv_slot % spec.SLOTS_PER_EPOCH == 1 + assert eqv_epoch == 0 + slashed_validators = [] + comm_count = spec.get_committee_count_per_slot(state, eqv_epoch) + for comm_index in range(comm_count): + comm = spec.get_beacon_committee(state, eqv_slot, comm_index) + slashed_validators += comm + assert len(slashed_validators) > 0 + + # Slash those validators in the state + for val_index in slashed_validators: + state.validators[val_index].slashed = True + + # Store this state as the anchor state + anchor_state = state.copy() + # Generate an anchor block with correct state root + anchor_block = spec.BeaconBlock(state_root=anchor_state.hash_tree_root()) + yield 'anchor_state', anchor_state + yield 'anchor_block', anchor_block + + # Get a new store with the anchor state & anchor block + store = spec.get_forkchoice_store(anchor_state, anchor_block) + + # Now generate the store checks + current_time = anchor_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 + + # Create two competing blocks at eqv_slot + next_slots(spec, state, eqv_slot - state.slot - 1) + assert state.slot == eqv_slot - 1 + + state_1 = state.copy() + block_1 = build_empty_block_for_next_slot(spec, state_1) + signed_block_1 = state_transition_and_sign_block(spec, state_1, block_1) + + state_2 = state.copy() + block_2 = build_empty_block_for_next_slot(spec, state_2) + block_2.body.graffiti = block_2.body.graffiti = b'\x42' * 32 + signed_block_2 = state_transition_and_sign_block(spec, state_2, block_2) + + assert block_1.slot == block_2.slot == eqv_slot + + # Add both blocks to the store + yield from tick_and_add_block(spec, store, signed_block_1, test_steps) + yield from tick_and_add_block(spec, store, signed_block_2, test_steps) + + # Find out which block will win in tie breaking + if spec.hash_tree_root(block_1) < spec.hash_tree_root(block_2): + block_low_root = block_1.hash_tree_root() + block_low_root_post_state = state_1 + block_high_root = block_2.hash_tree_root() + else: + block_low_root = block_2.hash_tree_root() + block_low_root_post_state = state_2 + block_high_root = block_1.hash_tree_root() + assert block_low_root < block_high_root + + # Tick to next slot so proposer boost does not apply + current_time = store.genesis_time + (block_1.slot + 1) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, current_time, test_steps) + + # Check that block with higher root wins + assert spec.get_head(store) == block_high_root + + # Create attestation for block with lower root + attestation = get_valid_attestation(spec, block_low_root_post_state, slot=eqv_slot, index=0, signed=True) + # Check that all attesting validators were slashed in the anchor state + att_comm = spec.get_beacon_committee(block_low_root_post_state, eqv_slot, 0) + for i in att_comm: + assert anchor_state.validators[i].slashed + # Add attestation to the store + yield from add_attestation(spec, store, attestation, test_steps) + # Check that block with higher root still wins + assert spec.get_head(store) == block_high_root + output_head_check(spec, store, test_steps) + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_voting_source_within_two_epoch(spec, state): + """ + Check that the store allows for a head block that has: + - store.voting_source[block_root].epoch != store.justified_checkpoint.epoch, and + - store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch, and + - store.voting_source[block_root].epoch + 2 >= current_epoch, and + - store.finalized_checkpoint.root == get_ancestor(store, block_root, finalized_slot) + """ + 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 store.finalized_checkpoint.epoch == 2 + + # Copy the state to use later + fork_state = state.copy() + + # Fill epoch 4 + 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)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 4 + assert store.finalized_checkpoint.epoch == 3 + + # Create a fork from the earlier saved state + next_epoch(spec, fork_state) + assert spec.compute_epoch_at_slot(fork_state.slot) == 5 + _, signed_blocks, fork_state = next_epoch_with_attestations(spec, fork_state, True, True) + # Only keep the blocks from epoch 5, so discard the last generated block + signed_blocks = signed_blocks[:-1] + last_fork_block = signed_blocks[-1].message + assert spec.compute_epoch_at_slot(last_fork_block.slot) == 5 + + # Now add the fork to the store + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 4 + assert store.finalized_checkpoint.epoch == 3 + + # Check that the last block from the fork is the head + # LMD votes for the competing branch are overwritten so this fork should win + last_fork_block_root = last_fork_block.hash_tree_root() + # assert store.voting_source[last_fork_block_root].epoch != store.justified_checkpoint.epoch + assert store.unrealized_justifications[last_fork_block_root].epoch >= store.justified_checkpoint.epoch + # assert store.voting_source[last_fork_block_root].epoch + 2 >= \ + # spec.compute_epoch_at_slot(spec.get_current_slot(store)) + finalized_slot = spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert store.finalized_checkpoint.root == spec.get_ancestor(store, last_fork_block_root, finalized_slot) + assert spec.get_head(store) == last_fork_block_root + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_voting_source_beyond_two_epoch(spec, state): + """ + Check that the store doesn't allow for a head block that has: + - store.voting_source[block_root].epoch != store.justified_checkpoint.epoch, and + - store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch, and + - store.voting_source[block_root].epoch + 2 < current_epoch, and + - store.finalized_checkpoint.root == get_ancestor(store, block_root, finalized_slot) + """ + 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 store.finalized_checkpoint.epoch == 2 + + # Copy the state to use later + fork_state = state.copy() + + # Fill epoch 4 and 5 + for _ in range(2): + 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)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 5 + assert store.finalized_checkpoint.epoch == 4 + + # Create a fork from the earlier saved state + for _ in range(2): + next_epoch(spec, fork_state) + assert spec.compute_epoch_at_slot(fork_state.slot) == 6 + assert fork_state.current_justified_checkpoint.epoch == 3 + _, signed_blocks, fork_state = next_epoch_with_attestations(spec, fork_state, True, True) + # Only keep the blocks from epoch 6, so discard the last generated block + signed_blocks = signed_blocks[:-1] + last_fork_block = signed_blocks[-1].message + assert spec.compute_epoch_at_slot(last_fork_block.slot) == 6 + + # Store the head before adding the fork to the store + correct_head = spec.get_head(store) + + # Now add the fork to the store + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 5 + assert store.finalized_checkpoint.epoch == 4 + + last_fork_block_root = last_fork_block.hash_tree_root() + last_fork_block_state = store.block_states[last_fork_block_root] + assert last_fork_block_state.current_justified_checkpoint.epoch == 3 + + # Check that the head is unchanged + # assert store.voting_source[last_fork_block_root].epoch != store.justified_checkpoint.epoch + assert store.unrealized_justifications[last_fork_block_root].epoch >= store.justified_checkpoint.epoch + # assert store.voting_source[last_fork_block_root].epoch + 2 < \ + # spec.compute_epoch_at_slot(spec.get_current_slot(store)) + finalized_slot = spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert store.finalized_checkpoint.root == spec.get_ancestor(store, last_fork_block_root, finalized_slot) + assert spec.get_head(store) == correct_head + + yield 'steps', test_steps + + +""" +Note: +We are unable to generate test vectors that check failure of the correct_finalized condition. +We cannot generate a block that: +- has !correct_finalized, and +- has correct_justified, and +- is a descendant of store.justified_checkpoint.root + +The block being a descendant of store.justified_checkpoint.root is necessary because +filter_block_tree descends the tree starting at store.justified_checkpoint.root + +@with_all_phases +@spec_state_test +def test_incorrect_finalized(spec, state): + # Check that the store doesn't allow for a head block that has: + # - store.voting_source[block_root].epoch == store.justified_checkpoint.epoch, and + # - store.finalized_checkpoint.epoch != GENESIS_EPOCH, and + # - store.finalized_checkpoint.root != get_ancestor(store, block_root, finalized_slot) + 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 4 + for _ in range(4): + 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)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 4 + assert store.finalized_checkpoint.epoch == 3 + + # Identify the fork block as the last block in epoch 4 + fork_block_root = state.latest_block_header.parent_root + fork_block = store.blocks[fork_block_root] + assert spec.compute_epoch_at_slot(fork_block.slot) == 4 + # Copy the state to use later + fork_state = store.block_states[fork_block_root].copy() + assert spec.compute_epoch_at_slot(fork_state.slot) == 4 + assert fork_state.current_justified_checkpoint.epoch == 3 + assert fork_state.finalized_checkpoint.epoch == 2 + + # Create a fork from the earlier saved state + for _ in range(2): + next_epoch(spec, fork_state) + assert spec.compute_epoch_at_slot(fork_state.slot) == 6 + assert fork_state.current_justified_checkpoint.epoch == 4 + assert fork_state.finalized_checkpoint.epoch == 3 + # Fill epoch 6 + signed_blocks = [] + _, signed_blocks_1, fork_state = next_epoch_with_attestations(spec, fork_state, True, False) + signed_blocks += signed_blocks_1 + assert spec.compute_epoch_at_slot(fork_state.slot) == 7 + # Check that epoch 6 is justified in this fork - it will be used as voting source for the tip of this fork + assert fork_state.current_justified_checkpoint.epoch == 6 + assert fork_state.finalized_checkpoint.epoch == 3 + # Create a chain in epoch 7 that has new justification for epoch 7 + _, signed_blocks_2, fork_state = next_epoch_with_attestations(spec, fork_state, True, False) + # Only keep the blocks from epoch 7, so discard the last generated block + signed_blocks_2 = signed_blocks_2[:-1] + signed_blocks += signed_blocks_2 + last_fork_block = signed_blocks[-1].message + assert spec.compute_epoch_at_slot(last_fork_block.slot) == 7 + + # Now add the fork to the store + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 7 + assert store.justified_checkpoint.epoch == 6 + assert store.finalized_checkpoint.epoch == 3 + + # Fill epoch 5 and 6 in the original chain + for _ in range(2): + state, store, signed_head_block = yield from apply_next_epoch_with_attestations( + spec, state, store, True, False, test_steps=test_steps) + + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 7 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 6 + assert store.finalized_checkpoint.epoch == 5 + # Store the expected head + head_root = signed_head_block.message.hash_tree_root() + + # Check that the head is unchanged + last_fork_block_root = last_fork_block.hash_tree_root() + assert store.voting_source[last_fork_block_root].epoch == store.justified_checkpoint.epoch + assert store.finalized_checkpoint.epoch != spec.GENESIS_EPOCH + finalized_slot = spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert store.finalized_checkpoint.root != spec.get_ancestor(store, last_fork_block_root, finalized_slot) + assert spec.get_head(store) != last_fork_block_root + assert spec.get_head(store) == head_root + + yield 'steps', test_steps +""" 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 index 23514b325..eaae825ab 100644 --- 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 @@ -2,12 +2,16 @@ import random from eth_utils import encode_hex 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.context import ( + MINIMAL, + spec_state_test, + with_all_phases, + with_altair_and_later, + with_presets +) from eth2spec.test.helpers.attestations import ( next_epoch_with_attestations, next_slots_with_attestations, - state_transition_with_full_block, - state_transition_with_full_attestations_block, ) from eth2spec.test.helpers.block import ( build_empty_block_for_next_slot, @@ -22,6 +26,8 @@ from eth2spec.test.helpers.fork_choice import ( tick_and_add_block, apply_next_epoch_with_attestations, apply_next_slots_with_attestations, + is_ready_to_justify, + find_next_justifying_slot, ) from eth2spec.test.helpers.state import ( next_epoch, @@ -280,301 +286,22 @@ def test_on_block_finalized_skip_slots_not_in_skip_chain(spec, state): yield 'steps', test_steps -@with_all_phases -@spec_state_test -@with_presets([MINIMAL], reason="mainnet config requires too many pre-generated public/private keys") -def test_on_block_update_justified_checkpoint_within_safe_slots(spec, state): - """ - Test `should_update_justified_checkpoint`: - compute_slots_since_epoch_start(get_current_slot(store)) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED - """ - 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 - - # Skip epoch 0 & 1 - for _ in range(2): - next_epoch(spec, state) - # Fill epoch 2 - state, store, _ = yield from apply_next_epoch_with_attestations( - spec, state, store, True, False, test_steps=test_steps) - assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 0 - assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 2 - # Skip epoch 3 & 4 - for _ in range(2): - next_epoch(spec, state) - # Epoch 5: Attest current epoch - state, store, _ = yield from apply_next_epoch_with_attestations( - spec, state, store, True, False, participation_fn=_drop_random_one_third, test_steps=test_steps) - assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 0 - assert state.current_justified_checkpoint.epoch == 2 - assert store.justified_checkpoint.epoch == 2 - assert state.current_justified_checkpoint == store.justified_checkpoint - - # Skip epoch 6 - next_epoch(spec, state) - - pre_state = state.copy() - - # Build a block to justify epoch 5 - signed_block = state_transition_with_full_block(spec, state, True, True) - assert state.finalized_checkpoint.epoch == 0 - assert state.current_justified_checkpoint.epoch == 5 - assert state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch - assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH < spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED - # Run on_block - yield from tick_and_add_block(spec, store, signed_block, test_steps) - # Ensure justified_checkpoint has been changed but finality is unchanged - assert store.justified_checkpoint.epoch == 5 - assert store.justified_checkpoint == state.current_justified_checkpoint - assert store.finalized_checkpoint.epoch == pre_state.finalized_checkpoint.epoch == 0 - - yield 'steps', test_steps - - -@with_all_phases -@with_presets([MINIMAL], reason="It assumes that `MAX_ATTESTATIONS` >= 2/3 attestations of an epoch") -@spec_state_test -def test_on_block_outside_safe_slots_but_finality(spec, state): - """ - Test `should_update_justified_checkpoint` case - - compute_slots_since_epoch_start(get_current_slot(store)) > SAFE_SLOTS_TO_UPDATE_JUSTIFIED - - new_justified_checkpoint and store.justified_checkpoint.root are NOT conflicting - - Thus should_update_justified_checkpoint returns True. - - Part of this script is similar to `test_new_justified_is_later_than_store_justified`. - """ - 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 - - # Skip epoch 0 - next_epoch(spec, state) - # Fill epoch 1 to 3, attest current epoch - for _ in range(3): - state, store, _ = yield from apply_next_epoch_with_attestations( - spec, state, store, True, False, test_steps=test_steps) - assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 - assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 - - # Skip epoch 4-6 - for _ in range(3): - next_epoch(spec, state) - - # epoch 7 - state, store, _ = yield from apply_next_epoch_with_attestations( - spec, state, store, True, True, test_steps=test_steps) - assert state.finalized_checkpoint.epoch == 2 - assert state.current_justified_checkpoint.epoch == 7 - - # epoch 8, attest the first 5 blocks - state, store, _ = yield from apply_next_slots_with_attestations( - spec, state, store, 5, True, True, test_steps) - assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 - assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 7 - - # Propose a block at epoch 9, 5th slot - next_epoch(spec, state) - next_slots(spec, state, 4) - signed_block = state_transition_with_full_attestations_block(spec, state, True, True) - yield from tick_and_add_block(spec, store, signed_block, test_steps) - assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 - assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 7 - - # Propose an empty block at epoch 10, SAFE_SLOTS_TO_UPDATE_JUSTIFIED + 2 slot - # This block would trigger justification and finality updates on store - next_epoch(spec, state) - next_slots(spec, state, 4) - block = build_empty_block_for_next_slot(spec, state) - signed_block = state_transition_and_sign_block(spec, state, block) - assert state.finalized_checkpoint.epoch == 7 - assert state.current_justified_checkpoint.epoch == 8 - # Step time past safe slots and run on_block - if store.time < spec.compute_time_at_slot(state, signed_block.message.slot): - time = store.genesis_time + signed_block.message.slot * 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 - yield from add_block(spec, store, signed_block, test_steps) - - # Ensure justified_checkpoint finality has been changed - assert store.finalized_checkpoint.epoch == 7 - assert store.finalized_checkpoint == state.finalized_checkpoint - assert store.justified_checkpoint.epoch == 8 - assert store.justified_checkpoint == state.current_justified_checkpoint - - yield 'steps', test_steps - - -@with_all_phases -@with_presets([MINIMAL], reason="It assumes that `MAX_ATTESTATIONS` >= 2/3 attestations of an epoch") -@spec_state_test -def test_new_justified_is_later_than_store_justified(spec, state): - """ - J: Justified - F: Finalized - fork_1_state (forked from genesis): - epoch - [0] <- [1] <- [2] <- [3] <- [4] - F J - - fork_2_state (forked from fork_1_state's epoch 2): - epoch - └──── [3] <- [4] <- [5] <- [6] - F J - - fork_3_state (forked from genesis): - [0] <- [1] <- [2] <- [3] <- [4] <- [5] - F J - """ - # The 1st fork, from genesis - fork_1_state = state.copy() - # The 3rd fork, from genesis - fork_3_state = state.copy() - - 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 - - # ----- Process fork_1_state - # Skip epoch 0 - next_epoch(spec, fork_1_state) - # Fill epoch 1 with previous epoch attestations - fork_1_state, store, _ = yield from apply_next_epoch_with_attestations( - spec, fork_1_state, store, False, True, test_steps=test_steps) - - # Fork `fork_2_state` at the start of epoch 2 - fork_2_state = fork_1_state.copy() - assert spec.get_current_epoch(fork_2_state) == 2 - - # Skip epoch 2 - next_epoch(spec, fork_1_state) - # # Fill epoch 3 & 4 with previous epoch attestations - for _ in range(2): - fork_1_state, store, _ = yield from apply_next_epoch_with_attestations( - spec, fork_1_state, store, False, True, test_steps=test_steps) - - assert fork_1_state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 0 - assert fork_1_state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 - assert store.justified_checkpoint == fork_1_state.current_justified_checkpoint - - # ------ fork_2_state: Create a chain to set store.best_justified_checkpoint - # NOTE: The goal is to make `store.best_justified_checkpoint.epoch > store.justified_checkpoint.epoch` - all_blocks = [] - - # Proposed an empty block at epoch 2, 1st slot - block = build_empty_block_for_next_slot(spec, fork_2_state) - signed_block = state_transition_and_sign_block(spec, fork_2_state, block) - yield from tick_and_add_block(spec, store, signed_block, test_steps) - assert fork_2_state.current_justified_checkpoint.epoch == 0 - - # Skip to epoch 4 - for _ in range(2): - next_epoch(spec, fork_2_state) - assert fork_2_state.current_justified_checkpoint.epoch == 0 - - # Propose a block at epoch 4, 5th slot - # Propose a block at epoch 5, 5th slot - for _ in range(2): - next_epoch(spec, fork_2_state) - next_slots(spec, fork_2_state, 4) - signed_block = state_transition_with_full_attestations_block(spec, fork_2_state, True, True) - yield from tick_and_add_block(spec, store, signed_block, test_steps) - assert fork_2_state.current_justified_checkpoint.epoch == 0 - - # Propose a block at epoch 6, SAFE_SLOTS_TO_UPDATE_JUSTIFIED + 2 slot - next_epoch(spec, fork_2_state) - next_slots(spec, fork_2_state, spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED + 2) - signed_block = state_transition_with_full_attestations_block(spec, fork_2_state, True, True) - assert fork_2_state.finalized_checkpoint.epoch == 0 - assert fork_2_state.current_justified_checkpoint.epoch == 5 - # Check SAFE_SLOTS_TO_UPDATE_JUSTIFIED - time = store.genesis_time + fork_2_state.slot * spec.config.SECONDS_PER_SLOT - on_tick_and_append_step(spec, store, time, test_steps) - assert spec.compute_slots_since_epoch_start(spec.get_current_slot(store)) >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED - # Run on_block - yield from add_block(spec, store, signed_block, test_steps) - assert store.finalized_checkpoint.epoch == 0 - assert store.justified_checkpoint.epoch == 3 - assert store.best_justified_checkpoint.epoch == 5 - - # ------ fork_3_state: Create another chain to test the - # "Update justified if new justified is later than store justified" case - all_blocks = [] - for _ in range(3): - next_epoch(spec, fork_3_state) - - # epoch 3 - _, signed_blocks, fork_3_state = next_epoch_with_attestations(spec, fork_3_state, True, True) - all_blocks += signed_blocks - assert fork_3_state.finalized_checkpoint.epoch == 0 - - # epoch 4, attest the first 5 blocks - _, blocks, fork_3_state = next_slots_with_attestations(spec, fork_3_state, 5, True, True) - all_blocks += blocks.copy() - assert fork_3_state.finalized_checkpoint.epoch == 0 - - # Propose a block at epoch 5, 5th slot - next_epoch(spec, fork_3_state) - next_slots(spec, fork_3_state, 4) - signed_block = state_transition_with_full_block(spec, fork_3_state, True, True) - all_blocks.append(signed_block.copy()) - assert fork_3_state.finalized_checkpoint.epoch == 0 - - # Propose a block at epoch 6, 5th slot - next_epoch(spec, fork_3_state) - next_slots(spec, fork_3_state, 4) - signed_block = state_transition_with_full_block(spec, fork_3_state, True, True) - all_blocks.append(signed_block.copy()) - assert fork_3_state.finalized_checkpoint.epoch == 3 - assert fork_3_state.current_justified_checkpoint.epoch == 4 - - # Apply blocks of `fork_3_state` to `store` - for block in all_blocks: - if store.time < spec.compute_time_at_slot(fork_2_state, block.message.slot): - time = store.genesis_time + block.message.slot * spec.config.SECONDS_PER_SLOT - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, block, test_steps) - - assert store.finalized_checkpoint == fork_3_state.finalized_checkpoint - assert store.justified_checkpoint == fork_3_state.current_justified_checkpoint - assert store.justified_checkpoint != store.best_justified_checkpoint - assert store.best_justified_checkpoint == fork_2_state.current_justified_checkpoint - - yield 'steps', test_steps - - +""" @with_all_phases @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_new_finalized_slot_is_not_justified_checkpoint_ancestor(spec, state): - """ - J: Justified - F: Finalized - state (forked from genesis): - epoch - [0] <- [1] <- [2] <- [3] <- [4] <- [5] - F J + # J: Justified + # F: Finalized + # state (forked from genesis): + # epoch + # [0] <- [1] <- [2] <- [3] <- [4] <- [5] + # F J + + # another_state (forked from epoch 0): + # └──── [1] <- [2] <- [3] <- [4] <- [5] + # F J - another_state (forked from epoch 0): - └──── [1] <- [2] <- [3] <- [4] <- [5] - F J - """ test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -631,9 +358,15 @@ def test_new_finalized_slot_is_not_justified_checkpoint_ancestor(spec, state): assert ancestor_at_finalized_slot != store.finalized_checkpoint.root assert store.finalized_checkpoint == another_state.finalized_checkpoint - assert store.justified_checkpoint == another_state.current_justified_checkpoint + + # NOTE: inconsistent justified/finalized checkpoints in this edge case. + # This can only happen when >1/3 validators are slashable, as this testcase requires that + # store.justified_checkpoint is higher than store.finalized_checkpoint and on a different branch. + # Ignoring this testcase for now. + assert store.justified_checkpoint != another_state.current_justified_checkpoint yield 'steps', test_steps +""" @with_all_phases @@ -701,7 +434,9 @@ def test_new_finalized_slot_is_justified_checkpoint_ancestor(spec, state): assert ancestor_at_finalized_slot == store.finalized_checkpoint.root assert store.finalized_checkpoint == another_state.finalized_checkpoint - assert store.justified_checkpoint == another_state.current_justified_checkpoint + + # NOTE: inconsistent justified/finalized checkpoints in this edge case + assert store.justified_checkpoint != another_state.current_justified_checkpoint yield 'steps', test_steps @@ -797,3 +532,797 @@ def test_proposer_boost_root_same_slot_untimely_block(spec, state): }) yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justification_withholding(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 + + for _ in range(2): + next_epoch(spec, state) + + for _ in range(2): + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.get_current_epoch(state) == 4 + + # ------------ + + # Create attacker's fork that can justify epoch 4 + # Do not apply attacker's blocks to store + attacker_state = state.copy() + attacker_signed_blocks = [] + + while not is_ready_to_justify(spec, attacker_state): + attacker_state, signed_blocks, attacker_state = next_slots_with_attestations( + spec, attacker_state, 1, True, False) + attacker_signed_blocks += signed_blocks + + assert attacker_state.finalized_checkpoint.epoch == 2 + assert attacker_state.current_justified_checkpoint.epoch == 3 + assert spec.get_current_epoch(attacker_state) == 4 + + # ------------ + + # The honest fork sees all except the last block from attacker_signed_blocks + # Apply honest fork to store + honest_signed_blocks = attacker_signed_blocks[:-1] + assert len(honest_signed_blocks) > 0 + + for signed_block in honest_signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + + last_honest_block = honest_signed_blocks[-1].message + honest_state = store.block_states[hash_tree_root(last_honest_block)].copy() + + assert honest_state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert honest_state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.get_current_epoch(honest_state) == 4 + + # Create & apply an honest block in epoch 5 that can justify epoch 4 + next_epoch(spec, honest_state) + assert spec.get_current_epoch(honest_state) == 5 + + honest_block = build_empty_block_for_next_slot(spec, honest_state) + honest_block.body.attestations = attacker_signed_blocks[-1].message.body.attestations + signed_block = state_transition_and_sign_block(spec, honest_state, honest_block) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.get_head(store) == hash_tree_root(honest_block) + assert is_ready_to_justify(spec, honest_state) + + # ------------ + + # When the attacker's block is received, the honest block is still the head + # This relies on the honest block's LMD score increasing due to proposer boost + yield from tick_and_add_block(spec, store, attacker_signed_blocks[-1], test_steps) + assert store.finalized_checkpoint.epoch == 3 + assert store.justified_checkpoint.epoch == 4 + assert spec.get_head(store) == hash_tree_root(honest_block) + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justification_withholding_reverse_order(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 + + for _ in range(2): + next_epoch(spec, state) + + for _ in range(2): + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.get_current_epoch(state) == 4 + + # ------------ + + # Create attacker's fork that can justify epoch 4 + attacker_state = state.copy() + attacker_signed_blocks = [] + + while not is_ready_to_justify(spec, attacker_state): + attacker_state, signed_blocks, attacker_state = next_slots_with_attestations( + spec, attacker_state, 1, True, False) + assert len(signed_blocks) == 1 + attacker_signed_blocks += signed_blocks + yield from tick_and_add_block(spec, store, signed_blocks[0], test_steps) + + assert attacker_state.finalized_checkpoint.epoch == 2 + assert attacker_state.current_justified_checkpoint.epoch == 3 + assert spec.get_current_epoch(attacker_state) == 4 + attackers_head = hash_tree_root(attacker_signed_blocks[-1].message) + assert spec.get_head(store) == attackers_head + + # ------------ + + # The honest fork sees all except the last block from attacker_signed_blocks + honest_signed_blocks = attacker_signed_blocks[:-1] + assert len(honest_signed_blocks) > 0 + + last_honest_block = honest_signed_blocks[-1].message + honest_state = store.block_states[hash_tree_root(last_honest_block)].copy() + + assert honest_state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert honest_state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.get_current_epoch(honest_state) == 4 + + # Create an honest block in epoch 5 that can justify epoch 4 + next_epoch(spec, honest_state) + assert spec.get_current_epoch(honest_state) == 5 + + honest_block = build_empty_block_for_next_slot(spec, honest_state) + honest_block.body.attestations = attacker_signed_blocks[-1].message.body.attestations + signed_block = state_transition_and_sign_block(spec, honest_state, honest_block) + assert honest_state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert honest_state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert is_ready_to_justify(spec, honest_state) + + # When the honest block is received, the honest block becomes the head + # This relies on the honest block's LMD score increasing due to proposer boost + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert store.finalized_checkpoint.epoch == 3 + assert store.justified_checkpoint.epoch == 4 + assert spec.get_head(store) == hash_tree_root(honest_block) + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justification_update_beginning_of_epoch(spec, state): + """ + Check that the store's justified checkpoint is updated when a block containing better justification is + revealed at the first slot of an epoch + """ + 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 + + # Create a block that has new justification information contained within it, but don't add to store yet + another_state = state.copy() + _, signed_blocks, another_state = next_epoch_with_attestations(spec, another_state, True, False) + assert spec.compute_epoch_at_slot(another_state.slot) == 5 + assert another_state.current_justified_checkpoint.epoch == 4 + + # Tick store to the start of the next epoch + slot = spec.get_current_slot(store) + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert store.justified_checkpoint.epoch == 4 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justification_update_end_of_epoch(spec, state): + """ + Check that the store's justified checkpoint is updated when a block containing better justification is + revealed at the last slot of an epoch + """ + 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 + + # Create a block that has new justification information contained within it, but don't add to store yet + another_state = state.copy() + _, signed_blocks, another_state = next_epoch_with_attestations(spec, another_state, True, False) + assert spec.compute_epoch_at_slot(another_state.slot) == 5 + assert another_state.current_justified_checkpoint.epoch == 4 + + # Tick store to the last slot of the next epoch + slot = spec.get_current_slot(store) + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + slot = slot + spec.SLOTS_PER_EPOCH - 1 + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert store.justified_checkpoint.epoch == 4 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_incompatible_justification_update_start_of_epoch(spec, state): + """ + Check that the store's justified checkpoint is updated when a block containing better justification is + revealed at the start slot of an epoch, even when the better justified checkpoint is not a descendant of + the store's justified checkpoint + """ + 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 + + # Copy the state to create a fork later + another_state = state.copy() + + # Fill epoch 4 and 5 + for _ in range(2): + 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)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 5 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 4 + + # Create a block that has new justification information contained within it, but don't add to store yet + next_epoch(spec, another_state) + signed_blocks = [] + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, False, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 6 + assert another_state.current_justified_checkpoint.epoch == 3 + assert another_state.finalized_checkpoint.epoch == 2 + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, True, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 7 + assert another_state.current_justified_checkpoint.epoch == 6 + assert another_state.finalized_checkpoint.epoch == 2 + last_block_root = another_state.latest_block_header.parent_root + + # Tick store to the last slot of the next epoch + slot = another_state.slot + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 8 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + finalized_slot = spec.compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + assert spec.get_ancestor(store, last_block_root, finalized_slot) == state.finalized_checkpoint.root + justified_slot = spec.compute_start_slot_at_epoch(state.current_justified_checkpoint.epoch) + assert spec.get_ancestor(store, last_block_root, justified_slot) != state.current_justified_checkpoint.root + assert store.finalized_checkpoint.epoch == 4 + assert store.justified_checkpoint.epoch == 6 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_incompatible_justification_update_end_of_epoch(spec, state): + """ + Check that the store's justified checkpoint is updated when a block containing better justification is + revealed at the last slot of an epoch, even when the better justified checkpoint is not a descendant of + the store's justified checkpoint + """ + 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 + + # Copy the state to create a fork later + another_state = state.copy() + + # Fill epoch 4 and 5 + for _ in range(2): + 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)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 5 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 4 + + # Create a block that has new justification information contained within it, but don't add to store yet + next_epoch(spec, another_state) + signed_blocks = [] + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, False, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 6 + assert another_state.current_justified_checkpoint.epoch == 3 + assert another_state.finalized_checkpoint.epoch == 2 + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, True, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 7 + assert another_state.current_justified_checkpoint.epoch == 6 + assert another_state.finalized_checkpoint.epoch == 2 + last_block_root = another_state.latest_block_header.parent_root + + # Tick store to the last slot of the next epoch + slot = another_state.slot + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + slot = slot + spec.SLOTS_PER_EPOCH - 1 + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 8 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + finalized_slot = spec.compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + assert spec.get_ancestor(store, last_block_root, finalized_slot) == state.finalized_checkpoint.root + justified_slot = spec.compute_start_slot_at_epoch(state.current_justified_checkpoint.epoch) + assert spec.get_ancestor(store, last_block_root, justified_slot) != state.current_justified_checkpoint.root + assert store.finalized_checkpoint.epoch == 4 + assert store.justified_checkpoint.epoch == 6 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justified_update_not_realized_finality(spec, state): + """ + Check that the store updates its justified checkpoint if a higher justified checkpoint is found that is + a descendant of the finalized checkpoint, but does not know about the finality + """ + 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 + + # We'll make the current head block the finalized block + finalized_root = spec.get_head(store) + finalized_block = store.blocks[finalized_root] + assert spec.compute_epoch_at_slot(finalized_block.slot) == 4 + assert spec.get_head(store) == finalized_root + # Copy the post-state to use later + another_state = state.copy() + + # Create a fork that finalizes our block + for _ in range(2): + 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)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 5 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 4 + assert state.finalized_checkpoint.root == store.finalized_checkpoint.root == finalized_root + + # Create a fork for a better justification that is a descendant of the finalized block, + # but does not realize the finality. + # Do not add these blocks to the store yet + next_epoch(spec, another_state) + signed_blocks = [] + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, False, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 6 + assert another_state.current_justified_checkpoint.epoch == 3 + assert another_state.finalized_checkpoint.epoch == 2 + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, True, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 7 + assert another_state.current_justified_checkpoint.epoch == 6 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert store.justified_checkpoint.epoch == 6 + assert store.finalized_checkpoint.epoch == 4 + last_block = signed_blocks[-1] + last_block_root = last_block.message.hash_tree_root() + ancestor_at_finalized_slot = spec.get_ancestor(store, last_block_root, finalized_block.slot) + assert ancestor_at_finalized_slot == store.finalized_checkpoint.root + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justified_update_monotonic(spec, state): + """ + Check that the store does not update it's justified checkpoint with lower justified checkpoints. + This testcase checks that the store's justified checkpoint remains the same even when we input a block that has: + - a higher finalized checkpoint than the store's finalized checkpoint, and + - a lower justified checkpoint than the store's justified checkpoint + """ + 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 store.finalized_checkpoint.epoch == 2 + + # We'll eventually make the current head block the finalized block + finalized_root = spec.get_head(store) + finalized_block = store.blocks[finalized_root] + assert spec.compute_epoch_at_slot(finalized_block.slot) == 4 + assert spec.get_head(store) == finalized_root + # Copy into another variable so we can use `state` later + another_state = state.copy() + + # Create a fork with justification that is a descendant of the finalized block + # Do not add these blocks to the store yet + next_epoch(spec, another_state) + signed_blocks = [] + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, False, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 6 + assert another_state.current_justified_checkpoint.epoch == 3 + assert another_state.finalized_checkpoint.epoch == 2 + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, True, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 7 + assert another_state.current_justified_checkpoint.epoch == 6 + assert another_state.finalized_checkpoint.epoch == 2 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 7 + assert store.justified_checkpoint.epoch == 6 + assert store.finalized_checkpoint.epoch == 2 + last_block = signed_blocks[-1] + last_block_root = last_block.message.hash_tree_root() + ancestor_at_finalized_slot = spec.get_ancestor(store, last_block_root, finalized_block.slot) + assert ancestor_at_finalized_slot == finalized_root + + # Create a fork with lower justification that also finalizes our chosen block + for _ in range(2): + 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)) == 7 + assert state.current_justified_checkpoint.epoch == 5 + # Check that store's finalized checkpoint is updated + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 4 + # Check that store's justified checkpoint is not updated + assert store.justified_checkpoint.epoch == 6 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_justified_update_always_if_better(spec, state): + """ + Check that the store updates it's justified checkpoint with any higher justified checkpoint. + This testcase checks that the store's justified checkpoint is updated when we input a block that has: + - a lower finalized checkpoint than the store's finalized checkpoint, and + - a higher justified checkpoint than the store's justified checkpoint + """ + 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 store.finalized_checkpoint.epoch == 2 + + # We'll eventually make the current head block the finalized block + finalized_root = spec.get_head(store) + finalized_block = store.blocks[finalized_root] + assert spec.compute_epoch_at_slot(finalized_block.slot) == 4 + assert spec.get_head(store) == finalized_root + # Copy into another variable to use later + another_state = state.copy() + + # Create a fork with lower justification that also finalizes our chosen block + for _ in range(2): + 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)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 5 + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 4 + + # Create a fork with higher justification that is a descendant of the finalized block + # Do not add these blocks to the store yet + next_epoch(spec, another_state) + signed_blocks = [] + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, False, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 6 + assert another_state.current_justified_checkpoint.epoch == 3 + assert another_state.finalized_checkpoint.epoch == 2 + _, signed_blocks_temp, another_state = next_epoch_with_attestations(spec, another_state, True, False) + signed_blocks += signed_blocks_temp + assert spec.compute_epoch_at_slot(another_state.slot) == 7 + assert another_state.current_justified_checkpoint.epoch == 6 + assert another_state.finalized_checkpoint.epoch == 2 + + # Now add the blocks & check that justification update was triggered + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 7 + assert store.justified_checkpoint.epoch == 6 + assert store.finalized_checkpoint.epoch == 4 + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_pull_up_past_epoch_block(spec, state): + """ + Check that the store pulls-up a block from the past epoch to realize it's justification & finalization information + """ + 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 store.finalized_checkpoint.epoch == 2 + + # Create a chain within epoch 4 that contains a justification for epoch 4 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, True) + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) == 4 + + # Tick store to the next epoch + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert store.finalized_checkpoint.epoch == 2 + + # Add the previously created chain to the store and check for updates + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert store.justified_checkpoint.epoch == 4 + assert store.finalized_checkpoint.epoch == 3 + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_not_pull_up_current_epoch_block(spec, state): + """ + Check that the store does not pull-up a block from the current epoch if the previous epoch is not justified + """ + 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 store.finalized_checkpoint.epoch == 2 + + # Skip to the next epoch + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(state.slot) == 5 + + # Create a chain within epoch 5 that contains a justification for epoch 5 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, True) + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) == 5 + + # Add the previously created chain to the store and check that store does not apply pull-up updates + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert store.justified_checkpoint.epoch == 3 + assert store.finalized_checkpoint.epoch == 2 + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +@with_presets([MINIMAL], reason="too slow") +def test_pull_up_on_tick(spec, state): + """ + Check that the store pulls-up current epoch tips on the on_tick transition to the next epoch + """ + 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 store.finalized_checkpoint.epoch == 2 + + # Skip to the next epoch + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(state.slot) == 5 + + # Create a chain within epoch 5 that contains a justification for epoch 5 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, True) + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) == 5 + + # Add the previously created chain to the store and check that store does not apply pull-up updates, + # since the previous epoch was not justified + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert store.justified_checkpoint.epoch == 3 + assert store.finalized_checkpoint.epoch == 2 + + # Now tick the store to the next epoch and check that pull-up tip updates were applied + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(state.slot) == 6 + assert store.justified_checkpoint.epoch == 5 + # There's no new finality, so no finality updates expected + assert store.finalized_checkpoint.epoch == 3 + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_reorg.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_reorg.py new file mode 100644 index 000000000..30f1b06c7 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_reorg.py @@ -0,0 +1,498 @@ +from eth2spec.test.context import ( + spec_state_test, + with_all_phases, + with_presets, +) +from eth2spec.test.helpers.constants import ( + MINIMAL, +) +from eth2spec.test.helpers.attestations import ( + state_transition_with_full_block, + get_valid_attestation, + get_valid_attestation_at_slot, +) +from eth2spec.test.helpers.block import ( + build_empty_block, + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + add_attestations, + tick_and_add_block, + apply_next_epoch_with_attestations, + find_next_justifying_slot, + is_ready_to_justify, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, + next_epoch, + next_slot, + transition_to, +) + + +TESTING_PRESETS = [MINIMAL] + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_simple_attempted_reorg_without_enough_ffg_votes(spec, state): + """ + [Case 1] + + { epoch 4 }{ epoch 5 } + [c4]<--[a]<--[-]<--[y] + ↑____[-]<--[z] + + At c4, c3 is the latest justified checkpoint (or something earlier) + + The block y doesn't have enough votes to justify c4. + The block z also doesn't have enough votes to justify c4. + """ + 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 state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # create block_a, it needs 2 more full blocks to justify epoch 4 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, True) + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) + for signed_block in signed_blocks[:-2]: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + state = store.block_states[spec.get_head(store)].copy() + assert state.current_justified_checkpoint.epoch == 3 + next_slot(spec, state) + state_a = state.copy() + + # to test the "no withholding" situation, temporarily store the blocks in lists + signed_blocks_of_y = [] + signed_blocks_of_z = [] + + # add an empty block on chain y + block_y = build_empty_block_for_next_slot(spec, state) + signed_block_y = state_transition_and_sign_block(spec, state, block_y) + signed_blocks_of_y.append(signed_block_y) + + # chain y has some on-chain attestations, but not enough to justify c4 + signed_block_y = state_transition_with_full_block(spec, state, True, True) + assert not is_ready_to_justify(spec, state) + signed_blocks_of_y.append(signed_block_y) + assert store.justified_checkpoint.epoch == 3 + + state = state_a.copy() + signed_block_z = None + # add one block on chain z, which is not enough to justify c4 + attestation = get_valid_attestation(spec, state, slot=state.slot, signed=True) + block_z = build_empty_block_for_next_slot(spec, state) + block_z.body.attestations = [attestation] + signed_block_z = state_transition_and_sign_block(spec, state, block_z) + signed_blocks_of_z.append(signed_block_z) + + # add an empty block on chain z + block_z = build_empty_block_for_next_slot(spec, state) + signed_block_z = state_transition_and_sign_block(spec, state, block_z) + signed_blocks_of_z.append(signed_block_z) + + # ensure z couldn't justify c4 + assert not is_ready_to_justify(spec, state) + + # apply blocks to store + # (i) slot block_a.slot + 1 + signed_block_y = signed_blocks_of_y.pop(0) + yield from tick_and_add_block(spec, store, signed_block_y, test_steps) + # apply block of chain `z` + signed_block_z = signed_blocks_of_z.pop(0) + yield from tick_and_add_block(spec, store, signed_block_z, test_steps) + + # (ii) slot block_a.slot + 2 + # apply block of chain `z` + signed_block_z = signed_blocks_of_z.pop(0) + yield from tick_and_add_block(spec, store, signed_block_z, test_steps) + # apply block of chain `y` + signed_block_y = signed_blocks_of_y.pop(0) + yield from tick_and_add_block(spec, store, signed_block_y, test_steps) + # chain `y` remains the winner since it arrives earlier than `z` + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + assert len(signed_blocks_of_y) == len(signed_blocks_of_z) == 0 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4 + + # tick to the prior of the epoch boundary + slot = state.slot + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) - 1 + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4 + # chain `y` reminds the winner + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + + # to next block + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + yield 'steps', test_steps + + +def _run_delayed_justification(spec, state, attemped_reorg, is_justifying_previous_epoch): + """ + """ + 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 2 + for _ in range(2): + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + + if is_justifying_previous_epoch: + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, False, False, test_steps=test_steps) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 2 + else: + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + if is_justifying_previous_epoch: + # try to find the block that can justify epoch 3 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, False, True) + else: + # try to find the block that can justify epoch 4 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, True) + + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) + for signed_block in signed_blocks: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + spec.get_head(store) == signed_block.message.hash_tree_root() + state = store.block_states[spec.get_head(store)].copy() + if is_justifying_previous_epoch: + assert state.current_justified_checkpoint.epoch == 2 + else: + assert state.current_justified_checkpoint.epoch == 3 + + assert is_ready_to_justify(spec, state) + state_b = state.copy() + + # add chain y + if is_justifying_previous_epoch: + signed_block_y = state_transition_with_full_block(spec, state, False, True) + else: + signed_block_y = state_transition_with_full_block(spec, state, True, True) + yield from tick_and_add_block(spec, store, signed_block_y, test_steps) + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + if is_justifying_previous_epoch: + assert store.justified_checkpoint.epoch == 2 + else: + assert store.justified_checkpoint.epoch == 3 + + # add attestations of y + temp_state = state.copy() + next_slot(spec, temp_state) + attestations_for_y = list(get_valid_attestation_at_slot(temp_state, spec, signed_block_y.message.slot)) + current_time = temp_state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + yield from add_attestations(spec, store, attestations_for_y, test_steps) + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + + if attemped_reorg: + # add chain z + state = state_b.copy() + slot = state.slot + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) - 1 + transition_to(spec, state, slot) + block_z = build_empty_block_for_next_slot(spec, state) + assert spec.compute_epoch_at_slot(block_z.slot) == 5 + signed_block_z = state_transition_and_sign_block(spec, state, block_z) + yield from tick_and_add_block(spec, store, signed_block_z, test_steps) + else: + # next epoch + state = state_b.copy() + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + + # no reorg + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + if is_justifying_previous_epoch: + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + else: + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 4 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_simple_attempted_reorg_delayed_justification_current_epoch(spec, state): + """ + [Case 2] + + { epoch 4 }{ epoch 5 } + [c4]<--[b]<--[y] + ↑______________[z] + At c4, c3 is the latest justified checkpoint (or something earlier) + + block_b: the block that can justify c4. + z: the child of block of x at the first slot of epoch 5. + block z can reorg the chain from block y. + """ + yield from _run_delayed_justification(spec, state, attemped_reorg=True, is_justifying_previous_epoch=False) + + +def _run_include_votes_of_another_empty_chain(spec, state, enough_ffg, is_justifying_previous_epoch): + 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 2 + for _ in range(2): + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + if is_justifying_previous_epoch: + block_a = build_empty_block_for_next_slot(spec, state) + signed_block_a = state_transition_and_sign_block(spec, state, block_a) + yield from tick_and_add_block(spec, store, signed_block_a, test_steps) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 2 + else: + # fill one more epoch + state, store, _ = yield from apply_next_epoch_with_attestations( + spec, state, store, True, True, test_steps=test_steps) + signed_block_a = state_transition_with_full_block(spec, state, True, True) + yield from tick_and_add_block(spec, store, signed_block_a, test_steps) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + spec.get_head(store) == signed_block_a.message.hash_tree_root() + + state = store.block_states[spec.get_head(store)].copy() + if is_justifying_previous_epoch: + assert state.current_justified_checkpoint.epoch == 2 + else: + assert state.current_justified_checkpoint.epoch == 3 + state_a = state.copy() + + if is_justifying_previous_epoch: + # try to find the block that can justify epoch 3 + _, justifying_slot = find_next_justifying_slot(spec, state, False, True) + else: + # try to find the block that can justify epoch 4 + _, justifying_slot = find_next_justifying_slot(spec, state, True, True) + + last_slot_of_z = justifying_slot if enough_ffg else justifying_slot - 1 + last_slot_of_y = justifying_slot if is_justifying_previous_epoch else last_slot_of_z - 1 + + # to test the "no withholding" situation, temporarily store the blocks in lists + signed_blocks_of_y = [] + + # build an empty chain to the slot prior epoch boundary + signed_blocks_of_empty_chain = [] + states_of_empty_chain = [] + + for slot in range(state.slot + 1, last_slot_of_y + 1): + block = build_empty_block(spec, state, slot=slot) + signed_block = state_transition_and_sign_block(spec, state, block) + signed_blocks_of_empty_chain.append(signed_block) + states_of_empty_chain.append(state.copy()) + signed_blocks_of_y.append(signed_block) + + signed_block_y = signed_blocks_of_empty_chain[-1] + + # create 2/3 votes for the empty chain + attestations_for_y = [] + # target_is_current = not is_justifying_previous_epoch + attestations = list(get_valid_attestation_at_slot(state, spec, state_a.slot)) + attestations_for_y.append(attestations) + for state in states_of_empty_chain: + attestations = list(get_valid_attestation_at_slot(state, spec, state.slot)) + attestations_for_y.append(attestations) + + state = state_a.copy() + signed_block_z = None + + for slot in range(state_a.slot + 1, last_slot_of_z + 1): + # apply chain y, the empty chain + if slot <= last_slot_of_y and len(signed_blocks_of_y) > 0: + signed_block_y = signed_blocks_of_y.pop(0) + assert signed_block_y.message.slot == slot + yield from tick_and_add_block(spec, store, signed_block_y, test_steps) + + # apply chain z, a fork chain that includes these attestations_for_y + block = build_empty_block(spec, state, slot=slot) + if ( + len(attestations_for_y) > 0 and ( + (not is_justifying_previous_epoch) + or (is_justifying_previous_epoch and attestations_for_y[0][0].data.slot == slot - 5) + ) + ): + block.body.attestations = attestations_for_y.pop(0) + signed_block_z = state_transition_and_sign_block(spec, state, block) + if signed_block_y != signed_block_z: + yield from tick_and_add_block(spec, store, signed_block_z, test_steps) + if is_ready_to_justify(spec, state): + break + + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + + if is_justifying_previous_epoch: + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 2 + else: + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + if enough_ffg: + assert is_ready_to_justify(spec, state) + else: + assert not is_ready_to_justify(spec, state) + + # to next epoch + next_epoch(spec, state) + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + + if enough_ffg: + # reorg + assert spec.get_head(store) == signed_block_z.message.hash_tree_root() + if is_justifying_previous_epoch: + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + else: + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 4 + else: + # no reorg + assert spec.get_head(store) == signed_block_y.message.hash_tree_root() + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch(spec, state): + """ + [Case 3] + """ + yield from _run_include_votes_of_another_empty_chain( + spec, state, enough_ffg=True, is_justifying_previous_epoch=False) + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch(spec, state): + """ + [Case 4] + """ + yield from _run_include_votes_of_another_empty_chain( + spec, state, enough_ffg=False, is_justifying_previous_epoch=False) + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_delayed_justification_current_epoch(spec, state): + """ + [Case 5] + + To compare with ``test_simple_attempted_reorg_delayed_justification_current_epoch``, + this is the basic case if there is no chain z + + { epoch 4 }{ epoch 5 } + [c4]<--[b]<--[y] + + At c4, c3 is the latest justified checkpoint. + + block_b: the block that can justify c4. + """ + yield from _run_delayed_justification(spec, state, attemped_reorg=False, is_justifying_previous_epoch=False) + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_delayed_justification_previous_epoch(spec, state): + """ + [Case 6] + + Similar to ``test_delayed_justification_current_epoch``, + but includes attestations during epoch N to justify checkpoint N-1. + + { epoch 3 }{ epoch 4 }{ epoch 5 } + [c3]<---------------[c4]---[b]<---------------------------------[y] + + """ + yield from _run_delayed_justification(spec, state, attemped_reorg=False, is_justifying_previous_epoch=True) + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_simple_attempted_reorg_delayed_justification_previous_epoch(spec, state): + """ + [Case 7] + + Similar to ``test_simple_attempted_reorg_delayed_justification_current_epoch``, + but includes attestations during epoch N to justify checkpoint N-1. + + { epoch 3 }{ epoch 4 }{ epoch 5 } + [c3]<---------------[c4]<--[b]<--[y] + ↑______________[z] + + At c4, c2 is the latest justified checkpoint. + + block_b: the block that can justify c3. + z: the child of block of x at the first slot of epoch 5. + block z can reorg the chain from block y. + """ + yield from _run_delayed_justification(spec, state, attemped_reorg=True, is_justifying_previous_epoch=True) + + +@with_all_phases +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_include_votes_another_empty_chain_with_enough_ffg_votes_previous_epoch(spec, state): + """ + [Case 8] + + Similar to ``test_include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch``, + but includes attestations during epoch N to justify checkpoint N-1. + + """ + yield from _run_include_votes_of_another_empty_chain( + spec, state, enough_ffg=True, is_justifying_previous_epoch=True) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_withholding.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_withholding.py new file mode 100644 index 000000000..61926875a --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_withholding.py @@ -0,0 +1,205 @@ +from eth2spec.test.context import ( + spec_state_test, + with_altair_and_later, + with_presets, +) +from eth2spec.test.helpers.constants import ( + MINIMAL, +) +from eth2spec.test.helpers.attestations import ( + state_transition_with_full_block, +) +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + tick_and_add_block, + apply_next_epoch_with_attestations, + find_next_justifying_slot, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, + next_epoch, +) + + +TESTING_PRESETS = [MINIMAL] + + +@with_altair_and_later +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_withholding_attack(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 + + # Create the attack block that includes justifying attestations for epoch 4 + # This block is withheld & revealed only in epoch 5 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, False) + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) + assert len(signed_blocks) > 1 + signed_attack_block = signed_blocks[-1] + for signed_block in signed_blocks[:-1]: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert spec.get_head(store) == signed_blocks[-2].message.hash_tree_root() + state = store.block_states[spec.get_head(store)].copy() + assert spec.compute_epoch_at_slot(state.slot) == 4 + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 4 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # Create an honest chain in epoch 5 that includes the justifying attestations from the attack block + next_epoch(spec, state) + assert spec.compute_epoch_at_slot(state.slot) == 5 + assert state.current_justified_checkpoint.epoch == 3 + # Create two block in the honest chain with full attestations, and add to the store + for _ in range(2): + signed_block = state_transition_with_full_block(spec, state, True, False) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + # Create final block in the honest chain that includes the justifying attestations from the attack block + honest_block = build_empty_block_for_next_slot(spec, state) + honest_block.body.attestations = signed_attack_block.message.body.attestations + signed_honest_block = state_transition_and_sign_block(spec, state, honest_block) + # Add the honest block to the store + yield from tick_and_add_block(spec, store, signed_honest_block, test_steps) + assert spec.get_head(store) == signed_honest_block.message.hash_tree_root() + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # Tick to the next slot so proposer boost is not a factor in choosing the head + current_time = (honest_block.slot + 1) * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.get_head(store) == signed_honest_block.message.hash_tree_root() + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # Upon revealing the withheld attack block, the honest block should still be the head + yield from tick_and_add_block(spec, store, signed_attack_block, test_steps) + assert spec.get_head(store) == signed_honest_block.message.hash_tree_root() + # As a side effect of the pull-up logic, the attack block is pulled up and store.justified_checkpoint is updated + assert store.justified_checkpoint.epoch == 4 + + # Even after going to the next epoch, the honest block should remain the head + slot = spec.get_current_slot(store) + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 6 + assert spec.get_head(store) == signed_honest_block.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_altair_and_later +@spec_state_test +@with_presets(TESTING_PRESETS, reason="too slow") +def test_withholding_attack_unviable_honest_chain(spec, state): + """ + Checks that the withholding attack succeeds for one epoch if the honest chain has a voting source beyond + two epochs ago. + """ + 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 + + next_epoch(spec, state) + assert spec.compute_epoch_at_slot(state.slot) == 5 + + # Create the attack block that includes justifying attestations for epoch 5 + # This block is withheld & revealed only in epoch 6 + signed_blocks, justifying_slot = find_next_justifying_slot(spec, state, True, False) + assert spec.compute_epoch_at_slot(justifying_slot) == spec.get_current_epoch(state) + assert len(signed_blocks) > 1 + signed_attack_block = signed_blocks[-1] + for signed_block in signed_blocks[:-1]: + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert spec.get_head(store) == signed_block.message.hash_tree_root() + assert spec.get_head(store) == signed_blocks[-2].message.hash_tree_root() + state = store.block_states[spec.get_head(store)].copy() + assert spec.compute_epoch_at_slot(state.slot) == 5 + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 5 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # Create an honest chain in epoch 6 that includes the justifying attestations from the attack block + next_epoch(spec, state) + assert spec.compute_epoch_at_slot(state.slot) == 6 + assert state.current_justified_checkpoint.epoch == 3 + # Create two block in the honest chain with full attestations, and add to the store + for _ in range(2): + signed_block = state_transition_with_full_block(spec, state, True, False) + assert state.current_justified_checkpoint.epoch == 3 + yield from tick_and_add_block(spec, store, signed_block, test_steps) + # Create final block in the honest chain that includes the justifying attestations from the attack block + honest_block = build_empty_block_for_next_slot(spec, state) + honest_block.body.attestations = signed_attack_block.message.body.attestations + signed_honest_block = state_transition_and_sign_block(spec, state, honest_block) + honest_block_root = signed_honest_block.message.hash_tree_root() + assert state.current_justified_checkpoint.epoch == 3 + # Add the honest block to the store + yield from tick_and_add_block(spec, store, signed_honest_block, test_steps) + current_epoch = spec.compute_epoch_at_slot(spec.get_current_slot(store)) + assert current_epoch == 6 + # assert store.voting_source[honest_block_root].epoch == 3 + assert spec.get_head(store) == honest_block_root + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # Tick to the next slot so proposer boost is not a factor in choosing the head + current_time = (honest_block.slot + 1) * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.get_head(store) == honest_block_root + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 6 + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + + # Upon revealing the withheld attack block, it should become the head + yield from tick_and_add_block(spec, store, signed_attack_block, test_steps) + # The attack block is pulled up and store.justified_checkpoint is updated + assert store.justified_checkpoint.epoch == 5 + attack_block_root = signed_attack_block.message.hash_tree_root() + assert spec.get_head(store) == attack_block_root + + # After going to the next epoch, the honest block should become the head + slot = spec.get_current_slot(store) + spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + current_time = slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert spec.compute_epoch_at_slot(spec.get_current_slot(store)) == 7 + # assert store.voting_source[honest_block_root].epoch == 5 + assert spec.get_head(store) == honest_block_root + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_block.py deleted file mode 100644 index 92382c884..000000000 --- a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_block.py +++ /dev/null @@ -1,87 +0,0 @@ -from copy import deepcopy - -from eth2spec.utils.ssz.ssz_impl import hash_tree_root -from eth2spec.test.context import ( - spec_state_test, - with_all_phases, -) -from eth2spec.test.helpers.block import ( - build_empty_block_for_next_slot, -) -from eth2spec.test.helpers.fork_choice import ( - get_genesis_forkchoice_store, - run_on_block, - apply_next_epoch_with_attestations, -) -from eth2spec.test.helpers.state import ( - next_epoch, - state_transition_and_sign_block, -) - - -@with_all_phases -@spec_state_test -def test_on_block_outside_safe_slots_and_multiple_better_justified(spec, state): - """ - NOTE: test_new_justified_is_later_than_store_justified also tests best_justified_checkpoint - """ - # Initialization - store = get_genesis_forkchoice_store(spec, state) - - next_epoch(spec, state) - spec.on_tick(store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT) - state, store, last_signed_block = yield from apply_next_epoch_with_attestations( - spec, state, store, True, False) - last_block_root = hash_tree_root(last_signed_block.message) - - # NOTE: Mock fictitious justified checkpoint in store - store.justified_checkpoint = spec.Checkpoint( - epoch=spec.compute_epoch_at_slot(last_signed_block.message.slot), - root=spec.Root("0x4a55535449464945440000000000000000000000000000000000000000000000") - ) - - next_epoch(spec, state) - spec.on_tick(store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT) - - # Create new higher justified checkpoint not in branch of store's justified checkpoint - just_block = build_empty_block_for_next_slot(spec, state) - store.blocks[just_block.hash_tree_root()] = just_block - - # Step time past safe slots - spec.on_tick(store, store.time + spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED * spec.config.SECONDS_PER_SLOT) - assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED - - previously_finalized = store.finalized_checkpoint - 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): - # Mutate store - 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) - - # NOTE: Mock store so that the modified state could be accessed - parent_block = store.blocks[last_block_root].copy() - parent_block.state_root = just_state.hash_tree_root() - store.blocks[block.parent_root] = parent_block - store.block_states[block.parent_root] = just_state.copy() - assert block.parent_root in store.blocks.keys() - assert block.parent_root in store.block_states.keys() - - run_on_block(spec, store, signed_block) - - assert store.finalized_checkpoint == previously_finalized - assert store.justified_checkpoint == previously_justified - # ensure the best from the series was stored - assert store.best_justified_checkpoint == best_justified_checkpoint diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py index 0d9f6ddf5..33d1bbac4 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py +++ b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py @@ -18,7 +18,6 @@ def run_on_tick(spec, store, time, new_justified_checkpoint=False): assert store.time == time if new_justified_checkpoint: - assert store.justified_checkpoint == store.best_justified_checkpoint assert store.justified_checkpoint.epoch > previous_justified_checkpoint.epoch assert store.justified_checkpoint.root != previous_justified_checkpoint.root else: @@ -32,12 +31,12 @@ def test_basic(spec, state): run_on_tick(spec, store, store.time + 1) +""" @with_all_phases @spec_state_test def test_update_justified_single_on_store_finalized_chain(spec, state): store = get_genesis_forkchoice_store(spec, state) - # [Mock store.best_justified_checkpoint] # Create a block at epoch 1 next_epoch(spec, state) block = build_empty_block_for_next_slot(spec, state) @@ -58,8 +57,6 @@ def test_update_justified_single_on_store_finalized_chain(spec, state): state_transition_and_sign_block(spec, state, block) store.blocks[block.hash_tree_root()] = block store.block_states[block.hash_tree_root()] = state - # Mock store.best_justified_checkpoint - store.best_justified_checkpoint = state.current_justified_checkpoint.copy() run_on_tick( spec, @@ -67,6 +64,7 @@ def test_update_justified_single_on_store_finalized_chain(spec, state): store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, new_justified_checkpoint=True ) +""" @with_all_phases @@ -89,7 +87,6 @@ def test_update_justified_single_not_on_store_finalized_chain(spec, state): root=block.hash_tree_root(), ) - # [Mock store.best_justified_checkpoint] # Create a block at epoch 1 state = init_state.copy() next_epoch(spec, state) @@ -112,79 +109,9 @@ def test_update_justified_single_not_on_store_finalized_chain(spec, state): state_transition_and_sign_block(spec, state, block) store.blocks[block.hash_tree_root()] = block.copy() store.block_states[block.hash_tree_root()] = state.copy() - # Mock store.best_justified_checkpoint - store.best_justified_checkpoint = state.current_justified_checkpoint.copy() run_on_tick( spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, ) - - -@with_all_phases -@spec_state_test -def test_no_update_same_slot_at_epoch_boundary(spec, state): - store = get_genesis_forkchoice_store(spec, state) - seconds_per_epoch = spec.config.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH - - store.best_justified_checkpoint = spec.Checkpoint( - epoch=store.justified_checkpoint.epoch + 1, - root=b'\x55' * 32, - ) - - # set store time to already be at epoch boundary - store.time = seconds_per_epoch - - run_on_tick(spec, store, store.time + 1) - - -@with_all_phases -@spec_state_test -def test_no_update_not_epoch_boundary(spec, state): - store = get_genesis_forkchoice_store(spec, state) - - store.best_justified_checkpoint = spec.Checkpoint( - epoch=store.justified_checkpoint.epoch + 1, - root=b'\x55' * 32, - ) - - run_on_tick(spec, store, store.time + spec.config.SECONDS_PER_SLOT) - - -@with_all_phases -@spec_state_test -def test_no_update_new_justified_equal_epoch(spec, state): - store = get_genesis_forkchoice_store(spec, state) - seconds_per_epoch = spec.config.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH - - store.best_justified_checkpoint = spec.Checkpoint( - epoch=store.justified_checkpoint.epoch + 1, - root=b'\x55' * 32, - ) - - store.justified_checkpoint = spec.Checkpoint( - epoch=store.best_justified_checkpoint.epoch, - root=b'\44' * 32, - ) - - run_on_tick(spec, store, store.time + seconds_per_epoch) - - -@with_all_phases -@spec_state_test -def test_no_update_new_justified_later_epoch(spec, state): - store = get_genesis_forkchoice_store(spec, state) - seconds_per_epoch = spec.config.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH - - store.best_justified_checkpoint = spec.Checkpoint( - epoch=store.justified_checkpoint.epoch + 1, - root=b'\x55' * 32, - ) - - store.justified_checkpoint = spec.Checkpoint( - epoch=store.best_justified_checkpoint.epoch + 1, - root=b'\44' * 32, - ) - - run_on_tick(spec, store, store.time + seconds_per_epoch) diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index f79d436eb..c94b95933 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -146,10 +146,6 @@ finalized_checkpoint: { epoch: int, -- Integer value from store.finalized_checkpoint.epoch root: string, -- Encoded 32-byte value from store.finalized_checkpoint.root } -best_justified_checkpoint: { - epoch: int, -- Integer value from store.best_justified_checkpoint.epoch - root: string, -- Encoded 32-byte value from store.best_justified_checkpoint.root -} proposer_boost_root: string -- Encoded 32-byte value from store.proposer_boost_root ``` @@ -160,7 +156,6 @@ For example: head: {slot: 32, root: '0xdaa1d49d57594ced0c35688a6da133abb086d191a2ebdfd736fad95299325aeb'} justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'} finalized_checkpoint: {epoch: 2, root: '0x40d32d6283ec11c53317a46808bc88f55657d93b95a1af920403187accf48f4f'} - best_justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'} proposer_boost_root: '0xdaa1d49d57594ced0c35688a6da133abb086d191a2ebdfd736fad95299325aeb' ``` diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index c106810f8..945e68700 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -7,6 +7,8 @@ if __name__ == "__main__": 'get_head', 'on_block', 'ex_ante', + 'reorg', + 'withholding', ]} # No additional Altair specific finality tests, yet. altair_mods = phase_0_mods