diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 56c20a439c..6b9e0211c8 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -104,6 +104,13 @@ REORG_PARENT_WEIGHT_THRESHOLD: 160 REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 +# Confirmation rule +# --------------------------------------------------------------- +# 20% +CONFIRMATION_BYZANTINE_THRESHOLD: 20 +# 0 Gwei +CONFIRMATION_SLASHING_THRESHOLD: 0 + # Deposit contract # --------------------------------------------------------------- # Ethereum PoW Mainnet diff --git a/configs/minimal.yaml b/configs/minimal.yaml index a2b4f2e736..3eec273ac1 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -103,6 +103,12 @@ REORG_PARENT_WEIGHT_THRESHOLD: 160 # `2` epochs REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 +# Confirmation rule +# --------------------------------------------------------------- +# 20% +CONFIRMATION_BYZANTINE_THRESHOLD: 20 +# 0 Gwei +CONFIRMATION_SLASHING_THRESHOLD: 0 # Deposit contract # --------------------------------------------------------------- diff --git a/specs/_features/eip7732/confirmation-rule.md b/specs/_features/eip7732/confirmation-rule.md new file mode 100644 index 0000000000..17aad0e8ac --- /dev/null +++ b/specs/_features/eip7732/confirmation-rule.md @@ -0,0 +1,60 @@ +# Fork Choice -- Confirmation Rule + +## Table of contents + + + + +- [Confirmation Rule](#confirmation-rule) + - [Helper Functions](#helper-functions) + - [Modified `get_ffg_support`](#modified-get_ffg_support) + + + + +## Confirmation Rule + +### Helper Functions + +#### Modified `get_ffg_support` + +```python +def get_ffg_support(store: Store, checkpoint: Root) -> Gwei: + """ + Returns the total weight supporting the checkpoint in the block's chain at block's epoch. + """ + current_epoch = get_current_store_epoch(store) + + # This function is only applicable to current and previous epoch blocks + assert current_epoch in [checkpoint.epoch, checkpoint.epoch + 1] + + if checkpoint not in store.checkpoint_states: + return Gwei(0) + + checkpoint_state = store.checkpoint_states[checkpoint] + + leaf_roots = [ + leaf for leaf in get_leaf_block_roots(store, checkpoint.root) + if get_checkpoint_block(store, leaf, checkpoint.epoch) == checkpoint.root] + + active_checkpoint_indices = get_active_validator_indices(checkpoint_state, checkpoint.epoch) + participating_indices_from_blocks = set().union(*[ + get_epoch_participating_indices( + store.block_states[root], + active_checkpoint_indices, + checkpoint.epoch == current_epoch + ) + for root in leaf_roots + ]) + + participating_indices_from_lmds = set([ + i + for i in store.latest_messages + if get_checkpoint_block( + store, store.latest_messages[i].root, + compute_epoch_at_slot(store.latest_messages[i].slot), # Modified in EIP7732 + ) == checkpoint.root + ]) + + return get_total_balance(checkpoint_state, participating_indices_from_blocks.union(participating_indices_from_lmds)) +``` \ No newline at end of file diff --git a/specs/_features/eip7732/fork-choice.md b/specs/_features/eip7732/fork-choice.md index 0eb49ddfc1..ea1f017d12 100644 --- a/specs/_features/eip7732/fork-choice.md +++ b/specs/_features/eip7732/fork-choice.md @@ -100,6 +100,9 @@ class Store(object): unrealized_justified_checkpoint: Checkpoint unrealized_finalized_checkpoint: Checkpoint proposer_boost_root: Root + highest_confirmed_block_current_epoch: Root + highest_confirmed_block_previous_epoch: Root + leaves_last_slot_previous_epoch: Set[Root] payload_withhold_boost_root: Root # [New in EIP-7732] payload_withhold_boost_full: boolean # [New in EIP-7732] payload_reveal_boost_root: Root # [New in EIP-7732] @@ -140,6 +143,9 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, unrealized_justifications={anchor_root: justified_checkpoint}, + highest_confirmed_block_current_epoch=justified_checkpoint.root, + highest_confirmed_block_previous_epoch=justified_checkpoint.root, + leaves_last_slot_previous_epoch=set(), execution_payload_states={anchor_root: copy(anchor_state)}, # [New in EIP-7732] ptc_vote={anchor_root: Vector[uint8, PTC_SIZE]()}, ) diff --git a/specs/bellatrix/confirmation-rule.md b/specs/bellatrix/confirmation-rule.md new file mode 100644 index 0000000000..2fd17aa73f --- /dev/null +++ b/specs/bellatrix/confirmation-rule.md @@ -0,0 +1,470 @@ +# Fork Choice -- Confirmation Rule + +## Table of contents + + + + +- [Introduction](#introduction) +- [Confirmation Rule](#confirmation-rule) + - [Constants](#constants) + - [Configuration](#configuration) + - [Helper Functions](#helper-functions) + - [`is_full_validator_set_covered`](#is_full_validator_set_covered) + - [`is_full_validator_set_for_block_covered`](#is_full_validator_set_for_block_covered) + - [`ceil_div`](#ceil_div) + - [`adjust_committee_weight_estimate_to_ensure_safety`](#adjust_committee_weight_estimate_to_ensure_safety) + - [`get_committee_weight_between_slots`](#get_committee_weight_between_slots) + - [`is_one_confirmed`](#is_one_confirmed) + - [`is_lmd_confirmed`](#is_lmd_confirmed) + - [`get_total_active_balance_for_block_root`](#get_total_active_balance_for_block_root) + - [`get_remaining_weight_in_current_epoch`](#get_remaining_weight_in_current_epoch) + - [`get_leaf_block_roots`](#get_leaf_block_roots) + - [`get_current_epoch_participating_indices`](#get_current_epoch_participating_indices) + - [`get_ffg_support`](#get_ffg_support) + - [`is_ffg_confirmed`](#is_ffg_confirmed) + - [`is_confirmed_no_caching`](#is_confirmed_no_caching) + - [`is_confirmed`](#is_confirmed) + - [`find_confirmed_block`](#find_confirmed_block) + - [`immediately_after_on_tick_if_slot_changed`](#immediately_after_on_tick_if_slot_changed) + + + + +## Introduction + +This document specifies a fast block confirmation rule for the Ethereum protocol. + +*Note*: Confirmation is not a substitute for finality! The safety of confirmations is weaker than that of finality. + +The research paper for this rule can be found [here](https://arxiv.org/abs/2405.00549). + +This rule makes the following network synchrony assumption: starting from the current slot, attestations created by honest validators in any slot are received by the end of that slot. +Consequently, this rule provides confirmations to users who believe in the above assumption. If this assumption is broken, confirmed blocks can be reorged without any adversarial behavior and without slashing. + + +*Note*: This algorithm uses unbounded integer arithmetic in some places. The rest of `consensus-specs` uses `uint64` arithmetic exclusively to ensure that results fit into length-limited fields - a property crucial for consensus objects (such as the `BeaconBlockBody`). This document describes a local confirmation rule that does not require storing anything in length-limited fields. Using unbounded integer arithmetic here prevents possible overflowing issues for the spec tests generated using this Python specification (or when executing these specifications directly). + +## Confirmation Rule + +This section specifies an algorithm to determine whether a block is confirmed. + +### Constants + +The following values are (non-configurable) constants used throughout this specification. + +| Name | Value | Description | +|-------------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `UNCONFIRMED_SCORE` | `int(-1)` | Value returned by the `get_*_score` methods to indicate that the block passed in input cannot be confirmed even if all validators are honest. | +| `MAX_CONFIRMATION_SCORE` | `int(33)` | Maximum possible value of the confirmation score corresponding to the maximum percentage of Byzantine stake. | +| `COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR` | `int(5)` | Per mille value to add to the estimation of the committee weight across a range of slots not covering a full epoch in order to ensure the safety of the confirmation rule with high probability. See [here](https://gist.github.com/saltiniroberto/9ee53d29c33878d79417abb2b4468c20) for an explanation about the value chosen. | + +### Configuration + +The confirmation rule can be configured to the desired tolerance of Byzantine validators, for which the algorithm takes the following input parameters: + +| Input Parameter | Type | Max. Value | Description | +|------------------------------------|----------|:-------------------------|---------------------------------------------------------------------------------------------------------| +| `CONFIRMATION_BYZANTINE_THRESHOLD` | `uint64` | `MAX_CONFIRMATION_SCORE` | assumed maximum percentage of Byzantine validators among the validator set. | +| `CONFIRMATION_SLASHING_THRESHOLD` | `Gwei` | `2**64 - 1` | assumed maximum amount of stake that the adversary is willing to get slashed in order to reorg a block. | + + +### Helper Functions + +#### `is_full_validator_set_covered` + +```python +def is_full_validator_set_covered(start_slot: Slot, end_slot: Slot) -> bool: + """ + Returns whether the range from ``start_slot`` to ``end_slot`` (inclusive of both) includes an entire epoch + """ + start_epoch = compute_epoch_at_slot(start_slot) + end_epoch = compute_epoch_at_slot(end_slot) + + return ( + end_epoch > start_epoch + 1 + or (end_epoch == start_epoch + 1 and start_slot % SLOTS_PER_EPOCH == 0)) +``` + +#### `is_full_validator_set_for_block_covered` + +```python +def is_full_validator_set_for_block_covered(store: Store, block_root: Root) -> bool: + """ + Returns whether the range from ``start_slot`` to ``end_slot`` (inclusive of both) includes and entire epoch + """ + current_slot = get_current_slot(store) + block = store.blocks[block_root] + parent_block = store.blocks[block.parent_root] + + return is_full_validator_set_covered(Slot(parent_block.slot + 1), current_slot) +``` + +#### `ceil_div` + +```python +def ceil_div(numerator: int, denominator: int) -> int: + """ + Returns ``ceil(numerator / denominator)`` using only integer arithmetic + """ + if numerator % denominator == 0: + return numerator // denominator + else: + return (numerator // denominator) + 1 +``` + +#### `adjust_committee_weight_estimate_to_ensure_safety` + +```python +def adjust_committee_weight_estimate_to_ensure_safety(estimate: Gwei) -> Gwei: + """ + Adjusts the ``estimate`` of the weight of a committee for a sequence of slots not covering a full epoch to + ensure the safety of the confirmation rule with high probability. + + See https://gist.github.com/saltiniroberto/9ee53d29c33878d79417abb2b4468c20 for an explanation of why this is + required. + """ + return Gwei(ceil_div(int(estimate * (1000 + COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR)), 1000)) +``` + +#### `get_committee_weight_between_slots` + +```python +def get_committee_weight_between_slots(state: BeaconState, start_slot: Slot, end_slot: Slot) -> Gwei: + """ + Returns the total weight of committees between ``start_slot`` and ``end_slot`` (inclusive of both). + """ + total_active_balance = get_total_active_balance(state) + + start_epoch = compute_epoch_at_slot(start_slot) + end_epoch = compute_epoch_at_slot(end_slot) + + if start_slot > end_slot: + return Gwei(0) + + # If an entire epoch is covered by the range, return the total active balance + if is_full_validator_set_covered(start_slot, end_slot): + return total_active_balance + + if start_epoch == end_epoch: + return Gwei(ceil_div((end_slot - start_slot + 1) * int(total_active_balance), SLOTS_PER_EPOCH)) + else: + # A range that spans an epoch boundary, but does not span any full epoch + # needs pro-rata calculation + + # See https://gist.github.com/saltiniroberto/9ee53d29c33878d79417abb2b4468c20 + # for an explanation of the formula used below. + + # First, calculate the number of committees in the end epoch + num_slots_in_end_epoch = int(compute_slots_since_epoch_start(end_slot) + 1) + # Next, calculate the number of slots remaining in the end epoch + remaining_slots_in_end_epoch = int(SLOTS_PER_EPOCH - num_slots_in_end_epoch) + # Then, calculate the number of slots in the start epoch + num_slots_in_start_epoch = int(SLOTS_PER_EPOCH - compute_slots_since_epoch_start(start_slot)) + + end_epoch_weight_mul_by_slots_per_epoch = num_slots_in_end_epoch * int(total_active_balance) + start_epoch_weight_mul_by_slots_per_epoch = ceil_div( + num_slots_in_start_epoch * remaining_slots_in_end_epoch * int(total_active_balance), + SLOTS_PER_EPOCH + ) + + # Each committee from the end epoch only contributes a pro-rated weight + return adjust_committee_weight_estimate_to_ensure_safety( + Gwei(ceil_div( + start_epoch_weight_mul_by_slots_per_epoch + end_epoch_weight_mul_by_slots_per_epoch, + SLOTS_PER_EPOCH + )) + ) +``` + +#### `is_one_confirmed` + +```python +def is_one_confirmed(store: Store, block_root: Root) -> bool: + current_slot = get_current_slot(store) + block = store.blocks[block_root] + parent_block = store.blocks[block.parent_root] + support = int(get_weight(store, block_root)) + justified_state = store.checkpoint_states[store.justified_checkpoint] + maximum_support = int( + get_committee_weight_between_slots(justified_state, Slot(parent_block.slot + 1), Slot(current_slot - 1)) + ) + proposer_score = int(get_proposer_score(store)) + + # Returns whether the following condition is true using only integer arithmetic + # support / maximum_support > + # 0.5 * (1 + proposer_score / maximum_support) + CONFIRMATION_BYZANTINE_THRESHOLD / 100 + + return ( + 100 * support > + 50 * maximum_support + 50 * proposer_score + CONFIRMATION_BYZANTINE_THRESHOLD * maximum_support + ) +``` + +#### `is_lmd_confirmed` + +```python +def is_lmd_confirmed(store: Store, block_root: Root) -> bool: + if block_root == store.finalized_checkpoint.root: + return True + + if is_full_validator_set_for_block_covered(store, block_root): + return is_one_confirmed(store, block_root) + else: + block = store.blocks[block_root] + return ( + is_one_confirmed(store, block_root) + and is_lmd_confirmed(store, block.parent_root) + ) +``` + +#### `get_total_active_balance_for_block_root` + +```python +def get_total_active_balance_for_block_root(store: Store, block_root: Root) -> Gwei: + assert block_root in store.block_states + + state = store.block_states[block_root] + + return get_total_active_balance(state) +``` + +#### `get_remaining_weight_in_current_epoch` + +```python +def get_remaining_weight_in_current_epoch(store: Store, checkpoint: Checkpoint) -> Gwei: + """ + Returns the total weight of votes for this epoch from future committees after the current slot. + """ + assert checkpoint in store.checkpoint_states + + state = store.checkpoint_states[checkpoint] + + current_slot = get_current_slot(store) + first_slot_next_epoch = compute_start_slot_at_epoch(Epoch(compute_epoch_at_slot(current_slot) + 1)) + return get_committee_weight_between_slots(state, Slot(current_slot + 1), Slot(first_slot_next_epoch - 1)) +``` + +#### `get_leaf_block_roots` + +```python +def get_leaf_block_roots(store: Store, block_root: Root) -> Set[Root]: + children = [ + root for root in store.blocks.keys() + if store.blocks[root].parent_root == block_root + ] + + if any(children): + # Get leaves of all children and add to the set. + leaf_block_roots: Set[Root] = set() + for child_leaf_block_roots in [get_leaf_block_roots(store, child) for child in children]: + leaf_block_roots = leaf_block_roots.union(child_leaf_block_roots) + return leaf_block_roots + else: + # This block is a leaf. + return set([block_root]) + +``` + +#### `get_current_epoch_participating_indices` + +```python +def get_epoch_participating_indices(state: BeaconState, + active_validator_indices: Sequence[ValidatorIndex], + is_current_epoch: bool) -> Set[ValidatorIndex]: + if is_current_epoch: + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + + return set([ + i for i in active_validator_indices + if has_flag(epoch_participation[i], TIMELY_TARGET_FLAG_INDEX) + ]) +``` + +#### `get_ffg_support` + +```python +def get_ffg_support(store: Store, checkpoint: Root) -> Gwei: + """ + Returns the total weight supporting the checkpoint in the block's chain at block's epoch. + """ + current_epoch = get_current_store_epoch(store) + + # This function is only applicable to current and previous epoch blocks + assert current_epoch in [checkpoint.epoch, checkpoint.epoch + 1] + + if checkpoint not in store.checkpoint_states: + return Gwei(0) + + checkpoint_state = store.checkpoint_states[checkpoint] + + leaf_roots = [ + leaf for leaf in get_leaf_block_roots(store, checkpoint.root) + if get_checkpoint_block(store, leaf, checkpoint.epoch) == checkpoint.root] + + active_checkpoint_indices = get_active_validator_indices(checkpoint_state, checkpoint.epoch) + participating_indices_from_blocks = set().union(*[ + get_epoch_participating_indices( + store.block_states[root], + active_checkpoint_indices, + checkpoint.epoch == current_epoch + ) + for root in leaf_roots + ]) + + participating_indices_from_lmds = set([ + i + for i in store.latest_messages + if get_checkpoint_block(store, store.latest_messages[i].root, store.latest_messages[i].epoch) == checkpoint.root + ]) + + return get_total_balance(checkpoint_state, participating_indices_from_blocks.union(participating_indices_from_lmds)) +``` + + +#### `is_ffg_confirmed` + +```python +def is_ffg_confirmed(store: Store, block_root: Root, epoch: Epoch) -> bool: + """ + Returns whether the `block_root`'s checkpoint will be justified by the end of this epoch. + """ + current_epoch = get_current_store_epoch(store) + + block = store.blocks[block_root] + block_epoch = compute_epoch_at_slot(block.slot) + checkpoint = Checkpoint( + epoch=epoch, + root=get_checkpoint_block(store, block_root, epoch) + ) + + store_target_checkpoint_state(store, checkpoint) + + # This function is only applicable to current and previous epoch blocks + assert current_epoch in [block_epoch, block_epoch + 1] + + total_active_balance = int(get_total_active_balance(store.checkpoint_states[checkpoint])) + + ffg_support_for_checkpoint = int(get_ffg_support(store, checkpoint)) + + if epoch == current_epoch: + remaining_ffg_weight = int(get_remaining_weight_in_current_epoch(store, checkpoint)) + else: + remaining_ffg_weight = 0 + + max_adversarial_ffg_support_for_checkpoint = int( + min( + ceil_div(total_active_balance * CONFIRMATION_BYZANTINE_THRESHOLD, 100), + CONFIRMATION_SLASHING_THRESHOLD, + ffg_support_for_checkpoint + ) + ) + + # Returns whether the following condition is true using only integer arithmetic + # 2 / 3 * total_active_balance <= ( + # ffg_support_for_checkpoint - max_adversarial_ffg_support_for_checkpoint + + # (1 - CONFIRMATION_BYZANTINE_THRESHOLD / 100) * remaining_ffg_weight + # ) + + return ( + 200 * total_active_balance <= + ffg_support_for_checkpoint * 300 + (300 - 3 * CONFIRMATION_BYZANTINE_THRESHOLD) * + remaining_ffg_weight - max_adversarial_ffg_support_for_checkpoint * 300 + ) +``` + +### `is_confirmed_no_caching` + +```python +def is_confirmed_no_caching(store: Store, block_root: Root) -> bool: + current_epoch = get_current_store_epoch(store) + + block = store.blocks[block_root] + block_state = store.block_states[block_root] + block_epoch = compute_epoch_at_slot(block.slot) + + if current_epoch == block_epoch: + return ( + get_checkpoint_block(store, block_root, Epoch(current_epoch - 1)) == + block_state.current_justified_checkpoint.root + and is_lmd_confirmed(store, block_root) + and is_ffg_confirmed(store, block_root, current_epoch) + ) + else: + return ( + compute_slots_since_epoch_start(get_current_slot(store)) == 0 + and is_ffg_confirmed(store, block_root, Epoch(current_epoch - 1)) + and any( + [ + get_voting_source(store, leaf).epoch + 2 >= current_epoch + and is_lmd_confirmed(store, leaf) + for leaf in store.leaves_last_slot_previous_epoch + ] + ) + ) +``` + +### `is_confirmed` + +```python +def is_confirmed(store: Store, block_root: Root) -> bool: + highest_confirmed_root_since_last_epoch = ( + store.highest_confirmed_block_current_epoch + if store.blocks[store.highest_confirmed_block_current_epoch].slot > + store.blocks[store.highest_confirmed_block_previous_epoch].slot + else store.highest_confirmed_block_previous_epoch) + + return get_ancestor(store, highest_confirmed_root_since_last_epoch, store.blocks[block_root].slot) == block_root +``` + +#### `find_confirmed_block` + +```python +def find_confirmed_block(store: Store, block_root: Root) -> Root: + + if block_root == store.finalized_checkpoint.root: + return block_root + + block = store.blocks[block_root] + current_epoch = get_current_store_epoch(store) + + block_epoch = compute_epoch_at_slot(block.slot) + + # If `block_epoch` is not either the current or previous epoch, then return `store.finalized_checkpoint.root` + if current_epoch not in [block_epoch, block_epoch + 1]: + return store.finalized_checkpoint.root + + if is_confirmed_no_caching(store, block_root): + return block_root + else: + return find_confirmed_block(store, block.parent_root) + +``` + +#### `immediately_after_on_tick_if_slot_changed` + +```python +def immediately_after_on_tick_if_slot_changed(store: Store) -> None: + """ + This method must be executed immediately after `on_tick` whenever the current slot changes. + Importantly, any attestation that could not be fed into `on_attestation` at the previous slot + because of ageing reasons, it must be processed through `on_attestation` before executing this method. + The reason for not calling this method directly from `on_tick` is that, due to the spec architecture, + it is impossible to specify the behaviour described above. + Separating the code execution in this way is therefore necessary to be able to test this functionality properly. + """ + current_slot = get_current_slot(store) + + if compute_slots_since_epoch_start(Slot(current_slot + 1)) == 0: + store.leaves_last_slot_previous_epoch = get_leaf_block_roots(store, store.finalized_checkpoint.root) + + highest_confirmed_root = find_confirmed_block(store, get_head(store)) + if (store.blocks[highest_confirmed_root].slot > store.blocks[store.highest_confirmed_block_current_epoch].slot + or compute_slots_since_epoch_start(current_slot) == 1): + store.highest_confirmed_block_current_epoch = highest_confirmed_root + + if compute_slots_since_epoch_start(current_slot) == 0: + store.highest_confirmed_block_previous_epoch = store.highest_confirmed_block_current_epoch +``` \ No newline at end of file diff --git a/specs/bellatrix/fork-choice.md b/specs/bellatrix/fork-choice.md index 17fb8e024d..608ea2720c 100644 --- a/specs/bellatrix/fork-choice.md +++ b/specs/bellatrix/fork-choice.md @@ -13,6 +13,8 @@ - [`safe_block_hash`](#safe_block_hash) - [`should_override_forkchoice_update`](#should_override_forkchoice_update) - [Helpers](#helpers) + - [Modified `Store`](#modified-store) + - [Modified `get_forkchoice_store`](#modified-get_forkchoice_store) - [`PayloadAttributes`](#payloadattributes) - [`PowBlock`](#powblock) - [`get_pow_block`](#get_pow_block) @@ -159,6 +161,61 @@ the result of `should_override_forkchoice_update` (when proposer reorgs are enab ## Helpers +### Modified `Store` + +*Note*: It's not a hard fork change. `highest_confirmed_block_current_epoch`, `highest_confirmed_block_previous_epoch`, and `leaves_last_slot_previous_epoch` are added for [confirmation rule](confirmation-rule.md). + +```python +@dataclass +class Store(object): + time: uint64 + genesis_time: uint64 + justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_checkpoint: Checkpoint + proposer_boost_root: Root + highest_confirmed_block_current_epoch: Root # New for confirmation rule + highest_confirmed_block_previous_epoch: Root # New for confirmation rule + leaves_last_slot_previous_epoch: Set[Root] # New for confirmation rule + equivocating_indices: Set[ValidatorIndex] + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) + block_states: Dict[Root, BeaconState] = field(default_factory=dict) + block_timeliness: Dict[Root, boolean] = 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) +``` + +#### Modified `get_forkchoice_store` + +```python +def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -> Store: + assert anchor_block.state_root == hash_tree_root(anchor_state) + anchor_root = hash_tree_root(anchor_block) + anchor_epoch = get_current_epoch(anchor_state) + justified_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + finalized_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + proposer_boost_root = Root() + return Store( + time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot), + genesis_time=anchor_state.genesis_time, + justified_checkpoint=justified_checkpoint, + finalized_checkpoint=finalized_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}, + highest_confirmed_block_current_epoch=justified_checkpoint.root, # New for confirmation rule + highest_confirmed_block_previous_epoch=justified_checkpoint.root, # New for confirmation rule + leaves_last_slot_previous_epoch=set(), # New for confirmation rule + ) +``` + ### `PayloadAttributes` Used to signal to initiate the payload build process via `notify_forkchoice_updated`. diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index e7b4d1c28c..807e839b7d 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -173,7 +173,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - 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} + unrealized_justifications={anchor_root: justified_checkpoint}, ) ``` @@ -551,7 +551,7 @@ def on_tick_per_slot(store: Store, time: uint64) -> None: # 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) diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/confirmation_rule/test_confirmation_rule.py b/tests/core/pyspec/eth2spec/test/bellatrix/confirmation_rule/test_confirmation_rule.py new file mode 100644 index 0000000000..0f55a421c1 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/bellatrix/confirmation_rule/test_confirmation_rule.py @@ -0,0 +1,527 @@ +from eth2spec.test.context import spec_state_test, with_config_overrides, with_presets, with_bellatrix_and_later +from eth2spec.test.helpers.attestations import ( + get_valid_attestation_at_slot, + get_valid_attestations_at_slot, + next_epoch_with_attestations, + next_slots_with_attestations, + state_transition_with_full_block +) +from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.helpers.fork_choice import ( + add_attestations, + add_block, + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step +) + +from eth2spec.test.helpers.state import ( + next_epoch, +) + + +def on_tick_and_append_step_no_checks(spec, store, time, test_steps): + on_tick_and_append_step(spec, store, time, test_steps, store_checks=False) + + +def tick_to_next_slot(spec, store, test_steps): + time = store.genesis_time + (spec.get_current_slot(store) + 1) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step_no_checks(spec, store, time, test_steps) + + +def conf_rule_apply_next_epoch_with_attestations( + spec, state, store, fill_cur_epoch, fill_prev_epoch, participation_fn=None, test_steps=None, + is_optimistic=False, store_checks=True +): + + if test_steps is None: + test_steps = [] + + _, new_signed_blocks, post_state = next_epoch_with_attestations( + spec, state, fill_cur_epoch, fill_prev_epoch, participation_fn=participation_fn) + for signed_block in new_signed_blocks: + block = signed_block.message + yield from conf_rule_tick_and_add_block( + spec, store, signed_block, test_steps, is_optimistic=is_optimistic, store_checks=store_checks) + block_root = block.hash_tree_root() + assert store.blocks[block_root] == block + last_signed_block = signed_block + + assert store.block_states[block_root].hash_tree_root() == post_state.hash_tree_root() + + return post_state, store, last_signed_block + + +def conf_rule_apply_next_slots_with_attestations( + spec, state, store, slots, fill_cur_epoch, fill_prev_epoch, participation_fn=None, test_steps=None, + is_optimistic=False, store_checks=True +): + _, new_signed_blocks, post_state = next_slots_with_attestations( + spec, state, slots, fill_cur_epoch, fill_prev_epoch, participation_fn=participation_fn) + + for signed_block in new_signed_blocks: + block = signed_block.message + yield from conf_rule_tick_and_add_block( + spec, store, signed_block, test_steps, is_optimistic=is_optimistic, store_checks=store_checks) + block_root = block.hash_tree_root() + assert store.blocks[block_root] == block + last_signed_block = signed_block + + assert store.block_states[block_root].hash_tree_root() == post_state.hash_tree_root() + + return post_state, store, last_signed_block + + +def apply_next_epoch_with_attestations_no_checks_and_optimistic( + spec, state, store, fill_cur_epoch, fill_prev_epoch, participation_fn=None, test_steps=None +): + post_state, store, last_signed_block = yield from conf_rule_apply_next_epoch_with_attestations( + spec, state, store, fill_cur_epoch, fill_prev_epoch, participation_fn=participation_fn, + test_steps=test_steps, is_optimistic=True, store_checks=False) + + return post_state, store, last_signed_block + + +def apply_next_slots_with_attestations_no_checks_and_optimistic( + spec, state, store, slots, fill_cur_epoch, fill_prev_epoch, test_steps, participation_fn=None +): + post_state, store, last_signed_block = yield from conf_rule_apply_next_slots_with_attestations( + spec, state, store, slots, fill_cur_epoch, fill_prev_epoch, + participation_fn=participation_fn, test_steps=test_steps, + is_optimistic=True, store_checks=False) + + return post_state, store, last_signed_block + + +def conf_rule_tick_and_add_block( + spec, store, signed_block, test_steps, valid=True, merge_block=False, block_not_found=False, + is_optimistic=False, blob_data=None, store_checks=True +): + + pre_state = store.block_states[signed_block.message.parent_root] + if merge_block: + assert spec.is_merge_transition_block(pre_state, signed_block.message.body) + + 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, store_checks) + yield from add_attestations(spec, store, signed_block.message.body.attestations, test_steps, False) + spec.immediately_after_on_tick_if_slot_changed(store) + + post_state = yield from add_block( + spec, store, signed_block, test_steps, + valid=valid, + block_not_found=block_not_found, + is_optimistic=is_optimistic, + blob_data=blob_data, + ) + + return post_state + + +def apply_next_epoch_with_attestations_in_blocks_and_on_attestation_no_checks_and_opt( + spec, state, store, fill_cur_epoch, fill_prev_epoch, participation_fn=None, test_steps=None +): + + post_state = state.copy() + + for _ in range(spec.SLOTS_PER_EPOCH): + attestations = list(get_valid_attestations_at_slot( + post_state, + spec, + post_state.slot - spec.MIN_ATTESTATION_INCLUSION_DELAY + 1, + None + )) + last_signed_block = state_transition_with_full_block( + spec, + post_state, + fill_cur_epoch, + fill_prev_epoch, + participation_fn, + ) + + yield from conf_rule_tick_and_add_block( + spec, store, last_signed_block, test_steps, is_optimistic=True, store_checks=False) + + yield from add_attestations(spec, store, attestations, test_steps, False) + + return post_state, store, last_signed_block + + +def get_ancestor(store, root, ancestor_number): + if ancestor_number == 0: + return root + else: + return get_ancestor(store, store.blocks[root].parent_root, ancestor_number - 1) + + +def get_block_root_from_head(spec, store, depth): + head_root = spec.get_head(store) + return get_ancestor(store, head_root, depth) + + +def get_valid_attestation_for_block(spec, store, block_root, perc): + """ + Get attestation filled by `perc`% + """ + return list( + get_valid_attestation_at_slot( + store.block_states[block_root], + spec, + spec.get_slots_since_genesis(store), + lambda slot, index, comm: set(list(comm)[0: int(len(comm) * perc)]), + ) + ) + + +def check_is_confirmed(spec, store, block_root, test_steps, expected=None): + confirmed = int(spec.is_confirmed(store, block_root)) + + if expected is not None: + assert confirmed == expected + test_steps.append({"check_is_confirmed": {"result": confirmed, "block_root": str(block_root)}}) + + +def check_get_confirmation_score(spec, store, block_root, test_steps, expected=None): + confirmation_score = int(spec.get_confirmation_score(store, block_root)) + if expected is not None: + assert confirmation_score == expected + test_steps.append( + {"check_get_confirmation_score": {"result": confirmation_score, "block_root": str(block_root)}} + ) + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 0, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_confirm_current_epoch_no_byz(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(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_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + state, store, _ = yield from apply_next_slots_with_attestations_no_checks_and_optimistic( + spec, state, store, 2, True, True, test_steps=test_steps + ) + + root = get_block_root_from_head(spec, store, 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) == spec.get_current_store_epoch(store) + + check_is_confirmed(spec, store, root, test_steps, True) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 0, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_confirm_previous_epoch_no_byz(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(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_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + root = get_block_root_from_head(spec, store, 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) + 1 == spec.get_current_store_epoch(store) + + check_is_confirmed(spec, store, root, test_steps, True) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 0, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_confirm_prior_to_previous_epoch_no_byz(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(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_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + root = get_block_root_from_head(spec, store, spec.SLOTS_PER_EPOCH + 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) + 2 == spec.get_current_store_epoch(store) + + check_is_confirmed(spec, store, root, test_steps, True) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 0, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_no_confirm_current_epoch_due_to_no_lmd_confirmed(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(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_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + sate_copy = state.copy() + + _, store, _ = yield from apply_next_slots_with_attestations_no_checks_and_optimistic( + spec, sate_copy, store, 3, True, True, test_steps=test_steps + ) + + state, store, last_signed_block = yield from apply_next_slots_with_attestations_no_checks_and_optimistic( + spec, state, store, 3, False, False, test_steps=test_steps + ) + + root = get_ancestor(store, last_signed_block.message.parent_root, 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) == spec.get_current_store_epoch(store) + + assert not spec.is_lmd_confirmed(store, root) + assert spec.is_ffg_confirmed(store, root, spec.get_current_store_epoch(store)) + check_is_confirmed(spec, store, root, test_steps, False) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 0, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_no_confirm_current_epoch_due_to_justified_checkpoint(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, + test_steps) + + # Fill epoch 1 to 2 + for _ in range(1): + state, store, _ = yield from apply_next_epoch_with_attestations_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + state, store, _ = yield from apply_next_slots_with_attestations_no_checks_and_optimistic( + spec, state, store, 2, True, True, test_steps=test_steps + ) + + root = get_block_root_from_head(spec, store, 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) == spec.get_current_store_epoch(store) + + assert spec.is_lmd_confirmed(store, root) + assert spec.is_ffg_confirmed(store, root, spec.get_current_store_epoch(store)) + + check_is_confirmed(spec, store, root, test_steps, False) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 0, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_no_confirm_previous_epoch_due_to_justified_checkpoint(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks( + spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, test_steps) + + for _ in range(2): + state, store, _ = yield from apply_next_epoch_with_attestations_in_blocks_and_on_attestation_no_checks_and_opt( + spec, state, store, False, False, test_steps=test_steps) + + root = get_block_root_from_head(spec, store, 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) + 1 == spec.get_current_store_epoch(store) + + assert spec.is_lmd_confirmed(store, root) + assert spec.is_ffg_confirmed(store, root, spec.compute_epoch_at_slot(block.slot)) + check_is_confirmed(spec, store, root, test_steps, False) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 50, + 'CONFIRMATION_SLASHING_THRESHOLD': 0 +}) +def test_no_confirm_previous_epoch_but_ffg_confirmed(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(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_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + root = get_block_root_from_head(spec, store, 1) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) + 1 == spec.get_current_store_epoch(store) + + assert spec.is_ffg_confirmed(store, root, spec.compute_epoch_at_slot(block.slot)) + block_state = store.block_states[root] + assert block_state.current_justified_checkpoint.epoch + 2 == spec.get_current_store_epoch(store) + check_is_confirmed(spec, store, root, test_steps, False) + + yield "steps", test_steps + + +@with_bellatrix_and_later +@with_presets([MINIMAL]) +@spec_state_test +@with_config_overrides({ + 'CONFIRMATION_BYZANTINE_THRESHOLD': 15, + 'CONFIRMATION_SLASHING_THRESHOLD': 2048000000000 +}) +def test_no_confirm_current_epoch_but_lmd_confirmed(spec, state): + assert spec.get_current_epoch(state) == spec.GENESIS_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_no_checks(spec, store, current_time, test_steps) + assert store.time == current_time + + next_epoch(spec, state) + on_tick_and_append_step_no_checks(spec, store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, + test_steps) + + for _ in range(2): + state, store, _ = yield from apply_next_epoch_with_attestations_no_checks_and_optimistic( + spec, state, store, True, True, test_steps=test_steps + ) + + state, store, _ = yield from apply_next_slots_with_attestations_no_checks_and_optimistic( + spec, state, store, 3, True, True, test_steps=test_steps + ) + + root = get_block_root_from_head(spec, store, 2) + block = store.blocks[root] + + assert spec.compute_epoch_at_slot(block.slot) == spec.get_current_store_epoch(store) + + assert spec.is_lmd_confirmed(store, root) + block_state = store.block_states[root] + assert block_state.current_justified_checkpoint.epoch + 1 == spec.get_current_store_epoch(store) + + yield "steps", test_steps diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index 8598870fb6..db1487937e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -52,7 +52,7 @@ 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, - blob_data=None): + blob_data=None, store_checks=True): pre_state = store.block_states[signed_block.message.parent_root] if merge_block: assert spec.is_merge_transition_block(pre_state, signed_block.message.body) @@ -60,7 +60,7 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, 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) + on_tick_and_append_step(spec, store, time, test_steps, store_checks) post_state = yield from add_block( spec, store, signed_block, test_steps, @@ -143,11 +143,12 @@ def get_blobs_file_name(blobs=None, blobs_root=None): return f"blobs_{encode_hex(blobs_root)}" -def on_tick_and_append_step(spec, store, time, test_steps): +def on_tick_and_append_step(spec, store, time, test_steps, store_checks=True): assert time >= store.time spec.on_tick(store, time) test_steps.append({'tick': int(time)}) - output_store_checks(spec, store, test_steps) + if store_checks: + output_store_checks(spec, store, test_steps) def run_on_block(spec, store, signed_block, valid=True): @@ -306,7 +307,9 @@ def apply_next_epoch_with_attestations(spec, fill_cur_epoch, fill_prev_epoch, participation_fn=None, - test_steps=None): + test_steps=None, + is_optimistic=False, + store_checks=True): if test_steps is None: test_steps = [] @@ -314,7 +317,8 @@ def apply_next_epoch_with_attestations(spec, spec, state, fill_cur_epoch, fill_prev_epoch, participation_fn=participation_fn) for signed_block in new_signed_blocks: block = signed_block.message - yield from tick_and_add_block(spec, store, signed_block, test_steps) + yield from tick_and_add_block(spec, store, signed_block, test_steps, + is_optimistic=is_optimistic, store_checks=store_checks) block_root = block.hash_tree_root() assert store.blocks[block_root] == block last_signed_block = signed_block @@ -331,12 +335,15 @@ def apply_next_slots_with_attestations(spec, fill_cur_epoch, fill_prev_epoch, test_steps, - participation_fn=None): + participation_fn=None, + is_optimistic=False, + store_checks=True): _, new_signed_blocks, post_state = next_slots_with_attestations( spec, state, slots, fill_cur_epoch, fill_prev_epoch, participation_fn=participation_fn) for signed_block in new_signed_blocks: block = signed_block.message - yield from tick_and_add_block(spec, store, signed_block, test_steps) + yield from tick_and_add_block(spec, store, signed_block, test_steps, + is_optimistic=is_optimistic, store_checks=store_checks) block_root = block.hash_tree_root() assert store.blocks[block_root] == block last_signed_block = signed_block diff --git a/tests/formats/confirmation_rule/README.md b/tests/formats/confirmation_rule/README.md new file mode 100644 index 0000000000..95d7a93a5b --- /dev/null +++ b/tests/formats/confirmation_rule/README.md @@ -0,0 +1,98 @@ +# Confirmation rule tests + +The aim of the confirmation rule tests is to provide test coverage of the various components of the confirmation rule. + +## Test case format + +### `meta.yaml` + +```yaml +description: string -- Optional. Description of test case, purely for debugging purposes. +bls_setting: int -- see general test-format spec. +``` + +### `anchor_state.ssz_snappy` + +An SSZ-snappy encoded `BeaconState`, the state to initialize store with `get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock)` helper. + +### `anchor_block.ssz_snappy` + +An SSZ-snappy encoded `BeaconBlock`, the block to initialize store with `get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock)` helper. + +### `steps.yaml` + +The steps to execute in sequence. There may be multiple items of the following types: + +#### `on_tick` execution step + +The parameter that is required for executing `on_tick(store, time)`. + +```yaml +{ + tick: int -- to execute `on_tick(store, time)`. +} +``` + +After this step, the `store` object may have been updated. + +#### `on_attestation` execution step + +The parameter that is required for executing `on_attestation(store, attestation)`. + +```yaml +{ + attestation: string -- the name of the `attestation_<32-byte-root>.ssz_snappy` file. + To execute `on_attestation(store, attestation)` with the given attestation. +} +``` +The file is located in the same folder (see below). + +After this step, the `store` object may have been updated. + +#### `on_block` execution step + +The parameter that is required for executing `on_block(store, block)`. + +```yaml +{ + block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. + To execute `on_block(store, block)` with the given attestation. +} +``` +The file is located in the same folder (see below). + +After this step, the `store` object may have been updated. + +#### `on_attester_slashing` execution step + +The parameter that is required for executing `on_attester_slashing(store, attester_slashing)`. + +```yaml +{ + attester_slashing: string -- the name of the `attester_slashing_<32-byte-root>.ssz_snappy` file. + To execute `on_attester_slashing(store, attester_slashing)` with the given attester slashing. +} +``` + +The file is located in the same folder (see below). + +After this step, the `store` object may have been updated. + +#### Checks step + +The checks to verify the execution of the confirmation rule algorithm + +```yaml + check_is_confirmed: { + result: bool, -- return value of `is_confirmed(store, block_root)` + block_root: string -- block to execute is_confirmed on + } +``` + +## Condition + +1. Deserialize `anchor_state.ssz_snappy` and `anchor_block.ssz_snappy` to initialize the local store object by with `get_forkchoice_store(anchor_state, anchor_block)` helper. +2. Iterate sequentially through `steps.yaml` + - For each execution, look up the corresponding ssz_snappy file. Execute the corresponding helper function on the current store. + - For the `on_block` execution step: if `len(block.message.body.attestations) > 0`, execute each attestation with `on_attestation(store, attestation)` after executing `on_block(store, block)`. + - For each `checks` step, the assertions on the values returned by the confirmation rule algorithm must be satisfied. diff --git a/tests/generators/confirmation_rule/README.md b/tests/generators/confirmation_rule/README.md new file mode 100644 index 0000000000..9014c3c24d --- /dev/null +++ b/tests/generators/confirmation_rule/README.md @@ -0,0 +1,5 @@ +# Confirmation rule tests + +The confirmation rule tests cover the confirmation rule implementations + +Information on the format of the tests can be found in the [confirmation rule test formats documentation](../../formats/confirmation_rule/README.md). diff --git a/tests/generators/confirmation_rule/main.py b/tests/generators/confirmation_rule/main.py new file mode 100644 index 0000000000..404fb8e9c8 --- /dev/null +++ b/tests/generators/confirmation_rule/main.py @@ -0,0 +1,22 @@ +from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators +from eth2spec.test.helpers.constants import BELLATRIX, CAPELLA, DENEB, EIP6110 + + +if __name__ == "__main__": + # Note: Confirmation rule tests start from Bellatrix + bellatrix_mods = {key: 'eth2spec.test.bellatrix.confirmation_rule.test_' + key for key in [ + 'confirmation_rule' + ]} + + capella_mods = bellatrix_mods # No additional Capella specific fork choice tests + deneb_mods = capella_mods # No additional Deneb specific fork choice tests + eip6110_mods = deneb_mods # No additional EIP6110 specific fork choice tests + + all_mods = { + BELLATRIX: bellatrix_mods, + CAPELLA: capella_mods, + DENEB: deneb_mods, + EIP6110: eip6110_mods, + } + + run_state_test_generators(runner_name="confirmation_rule", all_mods=all_mods) diff --git a/tests/generators/confirmation_rule/requirements.txt b/tests/generators/confirmation_rule/requirements.txt new file mode 100644 index 0000000000..1822486863 --- /dev/null +++ b/tests/generators/confirmation_rule/requirements.txt @@ -0,0 +1,2 @@ +pytest>=4.4 +../../../[generator]