From dfb3ef8f2c86e7fb264a88b826f93870b4ad0722 Mon Sep 17 00:00:00 2001 From: Antonio Sanso Date: Wed, 24 Nov 2021 08:42:27 +0100 Subject: [PATCH 01/45] Update ssz_container.py --- tests/generators/ssz_generic/ssz_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/generators/ssz_generic/ssz_container.py b/tests/generators/ssz_generic/ssz_container.py index 9cd155f76b..8f3a59ce3b 100644 --- a/tests/generators/ssz_generic/ssz_container.py +++ b/tests/generators/ssz_generic/ssz_container.py @@ -106,13 +106,13 @@ def invalid_cases(): RandomizationMode.mode_max_count]: if len(offsets) != 0: for offset_index in offsets: - yield f'{name}_offset_{offset_index}_plus_one', \ + yield f'{name}_{mode.to_name()}_offset_{offset_index}_plus_one', \ invalid_test_case(lambda: mod_offset( b=serialize(container_case_fn(rng, mode, typ)), offset_index=offset_index, change=lambda x: x + 1 )) - yield f'{name}_offset_{offset_index}_zeroed', \ + yield f'{name}_{mode.to_name()}_offset_{offset_index}_zeroed', \ invalid_test_case(lambda: mod_offset( b=serialize(container_case_fn(rng, mode, typ)), offset_index=offset_index, From 00bede59083a13bc5e8b58056f4062877c2f35a3 Mon Sep 17 00:00:00 2001 From: Mikhail Kalinin Date: Wed, 24 Nov 2021 17:00:32 +0600 Subject: [PATCH 02/45] Remove client-settings.md --- specs/merge/client-settings.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 specs/merge/client-settings.md diff --git a/specs/merge/client-settings.md b/specs/merge/client-settings.md deleted file mode 100644 index 64f912372e..0000000000 --- a/specs/merge/client-settings.md +++ /dev/null @@ -1,29 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [The Merge -- Client Settings](#the-merge----client-settings) - - [Override terminal total difficulty](#override-terminal-total-difficulty) - - [Override terminal block hash](#override-terminal-block-hash) - - - -# The Merge -- Client Settings - -**Notice**: This document is a work-in-progress for researchers and implementers. - -This document specifies configurable settings that clients must implement for the Merge. - -### Override terminal total difficulty - -To coordinate manual overrides to [`TERMINAL_TOTAL_DIFFICULTY`](./beacon-chain.md#Transition-settings) parameter, clients must provide `--terminal-total-difficulty-override` as a configurable setting. The value provided by this setting must take precedence over pre-configured `TERMINAL_TOTAL_DIFFICULTY` parameter. Clients should accept the setting as a decimal value (i.e., *not* hexadecimal). - -Except under exceptional scenarios, this setting is not expected to be used. Sufficient warning to the user about this exceptional configurable setting should be provided. - -### Override terminal block hash - -To allow for transition coordination around a specific PoW block, clients must also provide `--terminal-block-hash-override` and `--terminal-block-hash-epoch-override` as configurable settings. -* The value provided by `--terminal-block-hash-override` takes precedence over the pre-configured `TERMINAL_BLOCK_HASH` parameter. -* The value provided by `--terminal-block-hash-epoch-override` takes precedence over the pre-configured `TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH` parameter. - -Except under exceptional scenarios, these settings are not expected to be used. Sufficient warning to the user about this exceptional configurable setting should be provided. From 6ece777ff51b50883ac8ad94b9ed7dba1ed28115 Mon Sep 17 00:00:00 2001 From: Mikhail Kalinin Date: Wed, 24 Nov 2021 20:32:27 +0600 Subject: [PATCH 03/45] Bring more clarity to notify_forkchoice_updated calls --- specs/merge/fork-choice.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index e149989253..422ac9dd7d 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -66,6 +66,10 @@ def notify_forkchoice_updated(self: ExecutionEngine, *Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` must be called with `finalized_block_hash = Hash32()`. +*Note*: Client software must not call this function as long as the paylod of the head of the chain is empty, i.e. `get_head(store).body.payload == ExecutionPayload()`. + +*Note*: Client software must call this function to initiate payload build process to produce the merge transition block, in this case the `head_block_hash` parameter must be set to the hash of a terminal PoW block. + ## Helpers ### `PayloadAttributes` From b4377333fabb302122cd168c4654e8f81cf912f6 Mon Sep 17 00:00:00 2001 From: Mikhail Kalinin Date: Thu, 25 Nov 2021 16:27:59 +0600 Subject: [PATCH 04/45] Polish notify_forkchoice_updated description as per review Co-authored-by: Hsiao-Wei Wang --- specs/merge/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 422ac9dd7d..1b3a61fe2e 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -68,7 +68,7 @@ As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice *Note*: Client software must not call this function as long as the paylod of the head of the chain is empty, i.e. `get_head(store).body.payload == ExecutionPayload()`. -*Note*: Client software must call this function to initiate payload build process to produce the merge transition block, in this case the `head_block_hash` parameter must be set to the hash of a terminal PoW block. +*Note*: Client software MUST call this function to initiate the payload build process to produce the merge transition block; the `head_block_hash` parameter MUST be set to the hash of a terminal PoW block in this case. ## Helpers From 7a8f23667902d16d3580fb43b31333dfbb220d56 Mon Sep 17 00:00:00 2001 From: Mikhail Kalinin Date: Thu, 25 Nov 2021 16:33:30 +0600 Subject: [PATCH 05/45] Fix notify_forkchoice_updated description as per review --- specs/merge/fork-choice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 1b3a61fe2e..71b708ab4b 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -64,9 +64,9 @@ def notify_forkchoice_updated(self: ExecutionEngine, ``` *Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). -As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` must be called with `finalized_block_hash = Hash32()`. +As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` MUST be called with `finalized_block_hash = Hash32()`. -*Note*: Client software must not call this function as long as the paylod of the head of the chain is empty, i.e. `get_head(store).body.payload == ExecutionPayload()`. +*Note*: Client software MUST NOT call this function until the transition conditions are met on the PoW network, i.e. there exist a block for which `is_valid_terminal_pow_block` function returns `True`. *Note*: Client software MUST call this function to initiate the payload build process to produce the merge transition block; the `head_block_hash` parameter MUST be set to the hash of a terminal PoW block in this case. From d237491dfe04ca54d7c60b0791631f15bb7f5366 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Fri, 26 Nov 2021 06:47:05 -0700 Subject: [PATCH 06/45] minor typo --- specs/merge/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 71b708ab4b..95d613e9f7 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -66,7 +66,7 @@ def notify_forkchoice_updated(self: ExecutionEngine, *Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` MUST be called with `finalized_block_hash = Hash32()`. -*Note*: Client software MUST NOT call this function until the transition conditions are met on the PoW network, i.e. there exist a block for which `is_valid_terminal_pow_block` function returns `True`. +*Note*: Client software MUST NOT call this function until the transition conditions are met on the PoW network, i.e. there exists a block for which `is_valid_terminal_pow_block` function returns `True`. *Note*: Client software MUST call this function to initiate the payload build process to produce the merge transition block; the `head_block_hash` parameter MUST be set to the hash of a terminal PoW block in this case. From 25f2efab198ee41dd31bcd4500eb5cc39dab4a70 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Fri, 26 Nov 2021 15:11:19 -0600 Subject: [PATCH 07/45] Simplify sync protocol and update to calculate optimistic heads 1. Simplify `valid_updates` to `best_valid_update` so the `LightClientStore` only needs to store O(1) data 2. Track an optimistic head, by looking for the highest-slot header which passes a safety threshold --- specs/altair/sync-protocol.md | 88 ++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 24c35f8912..b309b61f45 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -18,7 +18,10 @@ - [`LightClientStore`](#lightclientstore) - [Helper functions](#helper-functions) - [`get_subtree_index`](#get_subtree_index) + - [`get_signed_header`](#get_signed_header) + - [`get_safety_threshold`](#get_safety_threshold) - [Light client state updates](#light-client-state-updates) + - [`process_slot`](#process_slot) - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_light_client_update) - [`process_light_client_update`](#process_light_client_update) @@ -47,9 +50,10 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. ### Misc -| Name | Value | -| - | - | -| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | +| Name | Value | Notes | +| - | - | - | +| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | | +| `SAFETY_THRESHOLD_CALCULATION_PERIOD` | `4096` | ~13.6 hours | ## Containers @@ -86,10 +90,12 @@ class LightClientUpdate(Container): ### `LightClientStore` ```python -@dataclass class LightClientStore(object): snapshot: LightClientSnapshot - valid_updates: Set[LightClientUpdate] + best_valid_update: Optional[LightClientUpdate] + optimistic_header: BeaconBlockHeader + previous_period_max_attendance: uint64 + current_period_max_attendance: uint64 ``` ## Helper functions @@ -101,9 +107,38 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: return uint64(generalized_index % 2**(floorlog2(generalized_index))) ``` +### `get_signed_header` + +```python +def get_signed_header(update: LightClientUpdate): + if update.finality_header is None: + return update.header + else: + return update.finality_header +``` + +### `get_safety_threshold` + +```python +def get_safety_threshold(store: LightClientStore): + return max( + store.previous_period_max_attendance, + store.current_period_max_attendance + ) // 2 +``` + ## Light client state updates -A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. +A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot` is processed every time the current slot increments. + +### `process_slot` + +```python +def process_slot(store: LightClientStore, current_slot: Slot): + if current_slot % SAFETY_THRESHOLD_CALCULATION_PERIOD == 0: + store.previous_period_max_attendance = store.current_period_max_attendance + store.current_period_max_attendance = 0 +``` #### `validate_light_client_update` @@ -172,24 +207,43 @@ def apply_light_client_update(snapshot: LightClientSnapshot, update: LightClient #### `process_light_client_update` ```python -def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot, +def process_light_client_update(store: LightClientStore, + update: LightClientUpdate, + current_slot: Slot, genesis_validators_root: Root) -> None: + validate_light_client_update(store.snapshot, update, genesis_validators_root) - store.valid_updates.add(update) - - update_timeout = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD + + # Update the best update in case we have to force-update to it if the timeout elapses + if ( + sum(update.sync_committee_bits) > sum(store.best_finalization_update.sync_committee_bits) and + get_signed_header(update).slot > store.snapshot.header.slot + ): + store.best_finalization_update = update + + # Track the maximum attendance in the committee signatures + store.current_period_max_attendance = max( + store.current_period_max_attendance, + update.sync_committee_bits.count(1) + ) + + # Update the optimistic header + if ( + sum(update.sync_committee_bits) > get_safety_threshold(store) and + update.header.slot > store.optimistic_header.slot + ): + store.optimistic_header = update.header + + # Update finalized header if ( sum(update.sync_committee_bits) * 3 >= len(update.sync_committee_bits) * 2 and update.finality_header != BeaconBlockHeader() ): - # Apply update if (1) 2/3 quorum is reached and (2) we have a finality proof. - # Note that (2) means that the current light client design needs finality. - # It may be changed to re-organizable light client design. See the on-going issue consensus-specs#2182. + # Normal update through 2/3 threshold apply_light_client_update(store.snapshot, update) - store.valid_updates = set() + store.best_valid_update = None elif current_slot > store.snapshot.header.slot + update_timeout: # Forced best update when the update timeout has elapsed - apply_light_client_update(store.snapshot, - max(store.valid_updates, key=lambda update: sum(update.sync_committee_bits))) - store.valid_updates = set() + apply_light_client_update(store.snapshot, store.best_valid_update) + store.best_valid_update = None ``` From 013e814d2d2e0c9f46eb31e36f9f7e27c2cdf39e Mon Sep 17 00:00:00 2001 From: vbuterin Date: Fri, 26 Nov 2021 15:32:37 -0600 Subject: [PATCH 08/45] Update sync-protocol.md --- specs/altair/sync-protocol.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index b309b61f45..b077d50211 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -156,10 +156,8 @@ def validate_light_client_update(snapshot: LightClientSnapshot, # Verify update header root is the finalized root of the finality header, if specified if update.finality_header == BeaconBlockHeader(): - signed_header = update.header assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))] else: - signed_header = update.finality_header assert is_valid_merkle_branch( leaf=hash_tree_root(update.header), branch=update.finality_branch, @@ -188,7 +186,7 @@ def validate_light_client_update(snapshot: LightClientSnapshot, # Verify sync committee aggregate signature participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit] domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root) - signing_root = compute_signing_root(signed_header, domain) + signing_root = compute_signing_root(get_signed_header(update), domain) assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature) ``` From e1041649608be5fafe58f42a726948ca64064170 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Sat, 27 Nov 2021 07:25:27 -0600 Subject: [PATCH 09/45] Rework data structures (#2747) 1. Replace `header` and `finality_header` with `attested_header` (always the header signed by the committee) and `finailzed_header` (always the header verified by the Merkle branch) 2. Remove `LightClientSnapshot`, fold its fields into `LightClientStore` for simplicity --- specs/altair/sync-protocol.md | 115 +++++++++++++++++----------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index b077d50211..f90469f987 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -13,7 +13,6 @@ - [Preset](#preset) - [Misc](#misc) - [Containers](#containers) - - [`LightClientSnapshot`](#lightclientsnapshot) - [`LightClientUpdate`](#lightclientupdate) - [`LightClientStore`](#lightclientstore) - [Helper functions](#helper-functions) @@ -22,6 +21,7 @@ - [`get_safety_threshold`](#get_safety_threshold) - [Light client state updates](#light-client-state-updates) - [`process_slot`](#process_slot) + - [`get_active_header`](#get_active_header) - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_light_client_update) - [`process_light_client_update`](#process_light_client_update) @@ -57,28 +57,17 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. ## Containers -### `LightClientSnapshot` - -```python -class LightClientSnapshot(Container): - # Beacon block header - header: BeaconBlockHeader - # Sync committees corresponding to the header - current_sync_committee: SyncCommittee - next_sync_committee: SyncCommittee -``` - ### `LightClientUpdate` ```python class LightClientUpdate(Container): - # Update beacon block header - header: BeaconBlockHeader + # The beacon block header that is attested to by the sync committee + attested_header: BeaconBlockHeader # Next sync committee corresponding to the header next_sync_committee: SyncCommittee next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)] - # Finality proof for the update header - finality_header: BeaconBlockHeader + # The finalized beacon block header attested to by Merkle branch + finalized_header: BeaconBlockHeader finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)] # Sync committee aggregate signature sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE] @@ -91,9 +80,16 @@ class LightClientUpdate(Container): ```python class LightClientStore(object): - snapshot: LightClientSnapshot + # Beacon block header that is finalized + finalized_header: BeaconBlockHeader + # Sync committees corresponding to the header + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # Best available header to switch finalized head to if we see nothing else best_valid_update: Optional[LightClientUpdate] + # Most recent available reasonably-safe header optimistic_header: BeaconBlockHeader + # Max number of participants in a sync committee (used to calculate safety threshold) previous_period_max_attendance: uint64 current_period_max_attendance: uint64 ``` @@ -107,16 +103,6 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: return uint64(generalized_index % 2**(floorlog2(generalized_index))) ``` -### `get_signed_header` - -```python -def get_signed_header(update: LightClientUpdate): - if update.finality_header is None: - return update.header - else: - return update.finality_header -``` - ### `get_safety_threshold` ```python @@ -140,44 +126,57 @@ def process_slot(store: LightClientStore, current_slot: Slot): store.current_period_max_attendance = 0 ``` +### `get_active_header` + +```python +def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: + # Is the update trying to convince us of a finalized header or an optimistic header? + if update.finalized_header BeaconBlockHeader(): + return update.finalized_header + else: + return update.attested_header +``` + #### `validate_light_client_update` ```python -def validate_light_client_update(snapshot: LightClientSnapshot, +def validate_light_client_update(store: LightClientStore, update: LightClientUpdate, genesis_validators_root: Root) -> None: - # Verify update slot is larger than snapshot slot - assert update.header.slot > snapshot.header.slot + + # Verify update slot is larger than slot of current best finalized header + active_header = get_active_header(update) + assert active_header.slot > store.finalized_header.slot # Verify update does not skip a sync committee period - snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - assert update_period in (snapshot_period, snapshot_period + 1) + finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + assert update_period in (finalized_period, finalized_period + 1) # Verify update header root is the finalized root of the finality header, if specified - if update.finality_header == BeaconBlockHeader(): + if update.finalized_header == BeaconBlockHeader(): assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))] else: assert is_valid_merkle_branch( - leaf=hash_tree_root(update.header), + leaf=hash_tree_root(update.finalized_header), branch=update.finality_branch, depth=floorlog2(FINALIZED_ROOT_INDEX), index=get_subtree_index(FINALIZED_ROOT_INDEX), - root=update.finality_header.state_root, + root=update.attested_header.state_root, ) # Verify update next sync committee if the update period incremented - if update_period == snapshot_period: - sync_committee = snapshot.current_sync_committee + if update_period == finalized_period: + sync_committee = store.current_sync_committee assert update.next_sync_committee_branch == [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))] else: - sync_committee = snapshot.next_sync_committee + sync_committee = store.next_sync_committee assert is_valid_merkle_branch( leaf=hash_tree_root(update.next_sync_committee), branch=update.next_sync_committee_branch, depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX), index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX), - root=update.header.state_root, + root=active_header.state_root, ) # Verify sync committee has sufficient participants @@ -186,20 +185,21 @@ def validate_light_client_update(snapshot: LightClientSnapshot, # Verify sync committee aggregate signature participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit] domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root) - signing_root = compute_signing_root(get_signed_header(update), domain) + signing_root = compute_signing_root(update.attested_header, domain) assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature) ``` #### `apply_light_client_update` ```python -def apply_light_client_update(snapshot: LightClientSnapshot, update: LightClientUpdate) -> None: - snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - if update_period == snapshot_period + 1: - snapshot.current_sync_committee = snapshot.next_sync_committee - snapshot.next_sync_committee = update.next_sync_committee - snapshot.header = update.header +def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None: + active_header = get_active_header(update) + finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if update_period == finalized_period + 1: + store.current_sync_committee = store.next_sync_committee + store.next_sync_committee = update.next_sync_committee + store.finalized_header = active_header ``` #### `process_light_client_update` @@ -210,13 +210,10 @@ def process_light_client_update(store: LightClientStore, current_slot: Slot, genesis_validators_root: Root) -> None: - validate_light_client_update(store.snapshot, update, genesis_validators_root) + validate_light_client_update(store, update, genesis_validators_root) # Update the best update in case we have to force-update to it if the timeout elapses - if ( - sum(update.sync_committee_bits) > sum(store.best_finalization_update.sync_committee_bits) and - get_signed_header(update).slot > store.snapshot.header.slot - ): + if sum(update.sync_committee_bits) > sum(store.best_finalization_update.sync_committee_bits): store.best_finalization_update = update # Track the maximum attendance in the committee signatures @@ -228,20 +225,20 @@ def process_light_client_update(store: LightClientStore, # Update the optimistic header if ( sum(update.sync_committee_bits) > get_safety_threshold(store) and - update.header.slot > store.optimistic_header.slot + update.attested_header.slot > store.optimistic_header.slot ): - store.optimistic_header = update.header + store.optimistic_header = update.attested_header # Update finalized header if ( sum(update.sync_committee_bits) * 3 >= len(update.sync_committee_bits) * 2 - and update.finality_header != BeaconBlockHeader() + and update.finalized_header != BeaconBlockHeader() ): # Normal update through 2/3 threshold - apply_light_client_update(store.snapshot, update) + apply_light_client_update(store, update) store.best_valid_update = None - elif current_slot > store.snapshot.header.slot + update_timeout: + elif current_slot > store.finalized_header.slot + update_timeout: # Forced best update when the update timeout has elapsed - apply_light_client_update(store.snapshot, store.best_valid_update) + apply_light_client_update(store, store.best_valid_update) store.best_valid_update = None ``` From 77188726de0b71a89afe40e5e5c97c85a96a163c Mon Sep 17 00:00:00 2001 From: vbuterin Date: Sat, 27 Nov 2021 07:27:16 -0600 Subject: [PATCH 10/45] Fixed ToC and get_active_header positioninf --- specs/altair/sync-protocol.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index f90469f987..ca7aa74512 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -17,11 +17,10 @@ - [`LightClientStore`](#lightclientstore) - [Helper functions](#helper-functions) - [`get_subtree_index`](#get_subtree_index) - - [`get_signed_header`](#get_signed_header) + - [`get_active_header`](#get_active_header) - [`get_safety_threshold`](#get_safety_threshold) - [Light client state updates](#light-client-state-updates) - [`process_slot`](#process_slot) - - [`get_active_header`](#get_active_header) - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_light_client_update) - [`process_light_client_update`](#process_light_client_update) @@ -103,6 +102,17 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: return uint64(generalized_index % 2**(floorlog2(generalized_index))) ``` +### `get_active_header` + +```python +def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: + # Is the update trying to convince us of a finalized header or an optimistic header? + if update.finalized_header BeaconBlockHeader(): + return update.finalized_header + else: + return update.attested_header +``` + ### `get_safety_threshold` ```python @@ -126,17 +136,6 @@ def process_slot(store: LightClientStore, current_slot: Slot): store.current_period_max_attendance = 0 ``` -### `get_active_header` - -```python -def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: - # Is the update trying to convince us of a finalized header or an optimistic header? - if update.finalized_header BeaconBlockHeader(): - return update.finalized_header - else: - return update.attested_header -``` - #### `validate_light_client_update` ```python From c4f70970c41205b874c289f2f4f2e2554a18f78e Mon Sep 17 00:00:00 2001 From: vbuterin Date: Sun, 28 Nov 2021 08:31:48 -0600 Subject: [PATCH 11/45] Update specs/altair/sync-protocol.md Co-authored-by: terence tsao --- specs/altair/sync-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index ca7aa74512..dd61b6545a 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -107,7 +107,7 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: ```python def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: # Is the update trying to convince us of a finalized header or an optimistic header? - if update.finalized_header BeaconBlockHeader(): + if update.finalized_header != BeaconBlockHeader(): return update.finalized_header else: return update.attested_header From 06af6296c5727e4e0faedb54da0938ecfc75aae7 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Mon, 29 Nov 2021 07:04:05 -0600 Subject: [PATCH 12/45] Updated in response to comments --- specs/altair/sync-protocol.md | 40 +++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index dd61b6545a..6ce46bf6b0 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -52,7 +52,8 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. | Name | Value | Notes | | - | - | - | | `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | | -| `SAFETY_THRESHOLD_CALCULATION_PERIOD` | `4096` | ~13.6 hours | +| `SAFETY_THRESHOLD_PERIOD` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~13.6 hours | +| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours | ## Containers @@ -88,9 +89,9 @@ class LightClientStore(object): best_valid_update: Optional[LightClientUpdate] # Most recent available reasonably-safe header optimistic_header: BeaconBlockHeader - # Max number of participants in a sync committee (used to calculate safety threshold) - previous_period_max_attendance: uint64 - current_period_max_attendance: uint64 + # Max number of active participants in a sync committee (used to calculate safety threshold) + previous_max_active_participants: uint64 + current_max_active_participants: uint64 ``` ## Helper functions @@ -106,7 +107,9 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: ```python def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: - # Is the update trying to convince us of a finalized header or an optimistic header? + # The "active header" is the header that the update is trying to convince us + # to accept. If a finalized header is present, it's the finalized header, + # otherwise it's the attested header if update.finalized_header != BeaconBlockHeader(): return update.finalized_header else: @@ -118,8 +121,8 @@ def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: ```python def get_safety_threshold(store: LightClientStore): return max( - store.previous_period_max_attendance, - store.current_period_max_attendance + store.previous_max_active_participants, + store.current_max_active_participants ) // 2 ``` @@ -131,9 +134,9 @@ A light client maintains its state in a `store` object of type `LightClientStore ```python def process_slot(store: LightClientStore, current_slot: Slot): - if current_slot % SAFETY_THRESHOLD_CALCULATION_PERIOD == 0: - store.previous_period_max_attendance = store.current_period_max_attendance - store.current_period_max_attendance = 0 + if current_slot % SAFETY_THRESHOLD_PERIOD == 0: + store.previous_max_active_participants = store.current_max_active_participants + store.current_max_active_participants = 0 ``` #### `validate_light_client_update` @@ -141,11 +144,12 @@ def process_slot(store: LightClientStore, current_slot: Slot): ```python def validate_light_client_update(store: LightClientStore, update: LightClientUpdate, + current_slot: Slot, genesis_validators_root: Root) -> None: # Verify update slot is larger than slot of current best finalized header active_header = get_active_header(update) - assert active_header.slot > store.finalized_header.slot + assert current_slot >= active_header.slot > store.finalized_header.slot # Verify update does not skip a sync committee period finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD @@ -209,15 +213,15 @@ def process_light_client_update(store: LightClientStore, current_slot: Slot, genesis_validators_root: Root) -> None: - validate_light_client_update(store, update, genesis_validators_root) + validate_light_client_update(store, update, current_slot, genesis_validators_root) # Update the best update in case we have to force-update to it if the timeout elapses - if sum(update.sync_committee_bits) > sum(store.best_finalization_update.sync_committee_bits): - store.best_finalization_update = update + if sum(update.sync_committee_bits) > sum(store.best_valid_update.sync_committee_bits): + store.best_valid_update = update - # Track the maximum attendance in the committee signatures - store.current_period_max_attendance = max( - store.current_period_max_attendance, + # Track the maximum numebr of active participants in the committee signatures + store.current_max_active_participants = max( + store.current_max_active_participants, update.sync_committee_bits.count(1) ) @@ -236,7 +240,7 @@ def process_light_client_update(store: LightClientStore, # Normal update through 2/3 threshold apply_light_client_update(store, update) store.best_valid_update = None - elif current_slot > store.finalized_header.slot + update_timeout: + elif current_slot > store.finalized_header.slot + UPDATE_TIMEOUT: # Forced best update when the update timeout has elapsed apply_light_client_update(store, store.best_valid_update) store.best_valid_update = None From 6fa19705fb5f1414592d6f3cb66117a738285950 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Mon, 29 Nov 2021 07:05:01 -0600 Subject: [PATCH 13/45] Clarified next sync committee comment --- specs/altair/sync-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 6ce46bf6b0..0af20cbca1 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -63,7 +63,7 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. class LightClientUpdate(Container): # The beacon block header that is attested to by the sync committee attested_header: BeaconBlockHeader - # Next sync committee corresponding to the header + # Next sync committee corresponding to the active header next_sync_committee: SyncCommittee next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)] # The finalized beacon block header attested to by Merkle branch From 7de1495a42f89a876fb84ac1701a4cbe3e3eab6b Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 30 Nov 2021 20:38:42 +0800 Subject: [PATCH 14/45] Fix lint (#2750) --- presets/mainnet/altair.yaml | 4 ++++ presets/minimal/altair.yaml | 4 ++++ setup.py | 1 + specs/altair/sync-protocol.md | 25 ++++++++++++++++--------- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/presets/mainnet/altair.yaml b/presets/mainnet/altair.yaml index 9a17b78032..21e3cc3285 100644 --- a/presets/mainnet/altair.yaml +++ b/presets/mainnet/altair.yaml @@ -22,3 +22,7 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256 # --------------------------------------------------------------- # 1 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 32 * 256) +UPDATE_TIMEOUT: 8192 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD // 2 (= 32 * 256 // 2) +SAFETY_THRESHOLD_PERIOD: 4096 diff --git a/presets/minimal/altair.yaml b/presets/minimal/altair.yaml index 88d78bea36..7cdbd58ea7 100644 --- a/presets/minimal/altair.yaml +++ b/presets/minimal/altair.yaml @@ -22,3 +22,7 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8 # --------------------------------------------------------------- # 1 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 8 * 8) +UPDATE_TIMEOUT: 64 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD // 2 (= 8 * 8 // 2) +SAFETY_THRESHOLD_PERIOD: 32 diff --git a/setup.py b/setup.py index 0ced87be2e..7b74fc1557 100644 --- a/setup.py +++ b/setup.py @@ -683,6 +683,7 @@ def combine_dicts(old_dict: Dict[str, T], new_dict: Dict[str, T]) -> Dict[str, T 'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256', 'bytes', 'byte', 'ByteList', 'ByteVector', 'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set', + 'Optional', ] diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 0af20cbca1..ce7ae62523 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -52,8 +52,8 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. | Name | Value | Notes | | - | - | - | | `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | | -| `SAFETY_THRESHOLD_PERIOD` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~13.6 hours | | `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours | +| `SAFETY_THRESHOLD_PERIOD` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD // 2` | ~13.6 hours | ## Containers @@ -79,6 +79,7 @@ class LightClientUpdate(Container): ### `LightClientStore` ```python +@dataclass class LightClientStore(object): # Beacon block header that is finalized finalized_header: BeaconBlockHeader @@ -119,7 +120,7 @@ def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: ### `get_safety_threshold` ```python -def get_safety_threshold(store: LightClientStore): +def get_safety_threshold(store: LightClientStore) -> uint64: return max( store.previous_max_active_participants, store.current_max_active_participants @@ -130,10 +131,10 @@ def get_safety_threshold(store: LightClientStore): A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot` is processed every time the current slot increments. -### `process_slot` +#### `process_slot` ```python -def process_slot(store: LightClientStore, current_slot: Slot): +def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None: if current_slot % SAFETY_THRESHOLD_PERIOD == 0: store.previous_max_active_participants = store.current_max_active_participants store.current_max_active_participants = 0 @@ -216,13 +217,16 @@ def process_light_client_update(store: LightClientStore, validate_light_client_update(store, update, current_slot, genesis_validators_root) # Update the best update in case we have to force-update to it if the timeout elapses - if sum(update.sync_committee_bits) > sum(store.best_valid_update.sync_committee_bits): + if ( + store.best_valid_update is None + or sum(update.sync_committee_bits) > sum(store.best_valid_update.sync_committee_bits) + ): store.best_valid_update = update - # Track the maximum numebr of active participants in the committee signatures + # Track the maximum number of active participants in the committee signatures store.current_max_active_participants = max( - store.current_max_active_participants, - update.sync_committee_bits.count(1) + store.current_max_active_participants, + update.sync_committee_bits.count(1), ) # Update the optimistic header @@ -240,7 +244,10 @@ def process_light_client_update(store: LightClientStore, # Normal update through 2/3 threshold apply_light_client_update(store, update) store.best_valid_update = None - elif current_slot > store.finalized_header.slot + UPDATE_TIMEOUT: + elif ( + current_slot > store.finalized_header.slot + UPDATE_TIMEOUT + and store.best_valid_update is not None + ): # Forced best update when the update timeout has elapsed apply_light_client_update(store, store.best_valid_update) store.best_valid_update = None From c30662b696e5ae3e8c4b0afb0bcfc736bbf547ed Mon Sep 17 00:00:00 2001 From: vbuterin Date: Tue, 30 Nov 2021 06:39:25 -0600 Subject: [PATCH 15/45] Consistently use sum instead of count(1) --- specs/altair/sync-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index ce7ae62523..401effc16e 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -226,7 +226,7 @@ def process_light_client_update(store: LightClientStore, # Track the maximum number of active participants in the committee signatures store.current_max_active_participants = max( store.current_max_active_participants, - update.sync_committee_bits.count(1), + sum(update.sync_committee_bits), ) # Update the optimistic header From 402c663b51805f725955356f5b9091502bb0505b Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 30 Nov 2021 21:57:43 +0800 Subject: [PATCH 16/45] Fix function name leftover --- specs/altair/sync-protocol.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 401effc16e..8da4a4d988 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -20,7 +20,7 @@ - [`get_active_header`](#get_active_header) - [`get_safety_threshold`](#get_safety_threshold) - [Light client state updates](#light-client-state-updates) - - [`process_slot`](#process_slot) + - [`process_slot_for_light_client_store`](#process_slot_for_light_client_store) - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_light_client_update) - [`process_light_client_update`](#process_light_client_update) @@ -129,9 +129,9 @@ def get_safety_threshold(store: LightClientStore) -> uint64: ## Light client state updates -A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot` is processed every time the current slot increments. +A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot_for_light_client_store` is processed every time the current slot increments. -#### `process_slot` +#### `process_slot_for_light_client_store` ```python def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None: From d7f6a42729a7e734a51d3e5434ebe194f5edddaf Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 30 Nov 2021 23:55:03 +0800 Subject: [PATCH 17/45] [WIP] Add ex-ante fork choice test cases --- specs/phase0/fork-choice.md | 1 + tests/core/pyspec/eth2spec/test/context.py | 5 +- .../eth2spec/test/helpers/fork_choice.py | 10 +- .../test/phase0/fork_choice/test_ex_ante.py | 290 ++++++++++++++++++ 4 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index d082ede306..ca38926fc9 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -263,6 +263,7 @@ def get_head(store: Store) -> Root: if len(children) == 0: return head # Sort by latest attesting balance with ties broken lexicographically + # Ties broken by favoring block with lexicographically higher root head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root)) ``` diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 184c0d6098..346d2fc707 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -484,8 +484,9 @@ def wrapper(*args, spec: Spec, **kw): # Retain types of all config values test_config = {k: config_types[k](v) for k, v in tmp_config.items()} - # Output the config for test vectors (TODO: check config YAML encoding) - yield 'config', 'data', test_config + # FIXME: config YAML encoding issue + # Output the config for test vectors + # yield 'config', 'data', test_config spec.config = spec.Configuration(**test_config) diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index 0b06f283f6..f056b9acd0 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -42,6 +42,12 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, return post_state +def add_attestation(spec, store, attestation, test_steps, is_from_block=False): + spec.on_attestation(store, attestation, is_from_block=is_from_block) + yield get_attestation_file_name(attestation), attestation + test_steps.append({'attestation': get_attestation_file_name(attestation)}) + + 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)] @@ -52,9 +58,7 @@ def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_bl spec.on_tick(store, next_epoch_time) test_steps.append({'tick': int(next_epoch_time)}) - spec.on_attestation(store, attestation, is_from_block=is_from_block) - yield get_attestation_file_name(attestation), attestation - test_steps.append({'attestation': get_attestation_file_name(attestation)}) + yield from add_attestation(spec, store, attestation, test_steps, is_from_block) def run_on_attestation(spec, store, attestation, is_from_block=False, valid=True): diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py new file mode 100644 index 0000000000..d19cb69b34 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -0,0 +1,290 @@ +from eth2spec.test.context import ( + MAINNET, + spec_configured_state_test, + spec_state_test, + with_all_phases, + with_presets, +) +from eth2spec.test.helpers.attestations import ( + get_valid_attestation, + sign_attestation, +) +from eth2spec.test.helpers.block import ( + build_empty_block, +) +from eth2spec.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + add_attestation, + add_block, + tick_and_add_block, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, +) + + +def _apply_base_block_a(spec, state, store, test_steps): + # On receiving block A at slot `N` + block = build_empty_block(spec, state, slot=state.slot + 1) + signed_block_a = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block_a, test_steps) + assert spec.get_head(store) == signed_block_a.message.hash_tree_root() + + +@with_all_phases +@spec_state_test +def test_ex_ante_secnario_1_with_boost(spec, state): + """ + With a single adversarial attestation + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size 1 + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 received at N+2 — B is head due to boost proposer + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head that has higher proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — C is head + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +def test_ex_ante_secnario_1_without_boost(spec, state): + """ + With a single adversarial attestation + + NOTE: this case disabled proposer score boost by setting config `PROPOSER_SCORE_BOOST` to `0` + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size 1 + """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 received at N+2 — B is head due to boost proposer + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — B is head + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@with_presets([MAINNET], reason="to create larger committee") +@spec_state_test +def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): + """ + Adversarial attestations > proposer boost + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size > proposer_boost + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Full attestation received at N+2 — B is head due to boost proposer + attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head that has higher proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. + # (B's proposer_score = C's attestation_score = 0) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +@with_presets([MAINNET], reason="to create larger committee") +def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, state): + """ + Adversarial attestations > proposer boost + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size > proposer_boost + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Full attestation received at N+2 — B is head due to boost proposer + attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps From 0bc0769767287e33277b6e346463c6431249a761 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Tue, 30 Nov 2021 15:44:56 -0500 Subject: [PATCH 18/45] Simplify logic in calculating proposer score --- specs/phase0/fork-choice.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index d082ede306..279a08f8ca 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -182,14 +182,12 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) proposer_score = Gwei(0) - if store.proposer_boost_root != Root(): - block = store.blocks[root] - if get_ancestor(store, root, block.slot) == store.proposer_boost_root: - num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) - avg_balance = get_total_active_balance(state) // num_validators - committee_size = num_validators // SLOTS_PER_EPOCH - committee_weight = committee_size * avg_balance - proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 + if store.proposer_boost_root != Root() and root == store.proposer_boost_root: + num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) + avg_balance = get_total_active_balance(state) // num_validators + committee_size = num_validators // SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 return attestation_score + proposer_score ``` From ad588d79f6aa25e0974f7e47afff52311f1f188a Mon Sep 17 00:00:00 2001 From: dankrad Date: Tue, 30 Nov 2021 22:52:16 +0000 Subject: [PATCH 19/45] Update beacon-chain.md 5 is not a primitive root of the BLS field, it needs to be 7 --- specs/sharding/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/sharding/beacon-chain.md b/specs/sharding/beacon-chain.md index 8190951b47..cf3d7bea17 100644 --- a/specs/sharding/beacon-chain.md +++ b/specs/sharding/beacon-chain.md @@ -101,7 +101,7 @@ The following values are (non-configurable) constants used throughout the specif | Name | Value | Notes | | - | - | - | -| `PRIMITIVE_ROOT_OF_UNITY` | `5` | Primitive root of unity of the BLS12_381 (inner) modulus | +| `PRIMITIVE_ROOT_OF_UNITY` | `7` | Primitive root of unity of the BLS12_381 (inner) modulus | | `DATA_AVAILABILITY_INVERSE_CODING_RATE` | `2**1` (= 2) | Factor by which samples are extended for data availability encoding | | `POINTS_PER_SAMPLE` | `uint64(2**3)` (= 8) | 31 * 8 = 248 bytes | | `MODULUS` | `0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001` (curve order of BLS12_381) | From bb8168fd956d753ea5ff84f94d6f0b334fbad61d Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 2 Dec 2021 00:46:19 +0800 Subject: [PATCH 20/45] Make config dict compatible with yaml.dump --- tests/core/pyspec/eth2spec/test/context.py | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 346d2fc707..2a6f2e324a 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -1,6 +1,7 @@ import pytest from copy import deepcopy from dataclasses import dataclass +from eth_utils import encode_hex from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal from eth2spec.altair import mainnet as spec_altair_mainnet, minimal as spec_altair_minimal @@ -464,6 +465,22 @@ def wrapper(*args, spec: Spec, **kw): return decorator +def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Get dict of Python built-in types from a dict of SSZ objects. + """ + result = {} + for k, v in ssz_dict.items(): + if isinstance(v, int): + value = int(v) + elif isinstance(v, bytes): + value = encode_hex(v) + else: + value = str(v) + result[k] = value + return result + + def with_config_overrides(config_overrides): """ WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs. @@ -484,9 +501,10 @@ def wrapper(*args, spec: Spec, **kw): # Retain types of all config values test_config = {k: config_types[k](v) for k, v in tmp_config.items()} - # FIXME: config YAML encoding issue - # Output the config for test vectors - # yield 'config', 'data', test_config + # To output the changed config to could be serialized with yaml test vectors, + # the dict SSZ objects have to be converted into Python built-in types. + output_config = _get_basic_dict(test_config) + yield 'config', 'data', output_config spec.config = spec.Configuration(**test_config) From dc52b351457e11be14a40cf79873903b5d8ad368 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 2 Dec 2021 01:37:01 +0800 Subject: [PATCH 21/45] Add `PROPOSER_SCORE_BOOST` to meta.yaml and fix comments --- .../eth2spec/test/phase0/fork_choice/test_ex_ante.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index d19cb69b34..806fe7ec48 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -153,7 +153,7 @@ def _filter_participant_set(participants): assert spec.get_head(store) == signed_block_c.message.hash_tree_root() # Block B received at N+2 - # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. yield from add_block(spec, store, signed_block_b, test_steps) if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): assert spec.get_head(store) == signed_block_b.message.hash_tree_root() @@ -240,6 +240,9 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, Block C (parent A) - slot N+2 Attestation_1 (Block B) - slot N+1 – size > proposer_boost """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -276,7 +279,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, assert spec.get_head(store) == signed_block_c.message.hash_tree_root() # Block B received at N+2 - # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. yield from add_block(spec, store, signed_block_b, test_steps) if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): assert spec.get_head(store) == signed_block_b.message.hash_tree_root() From 3e2890bb3e7b586f54d71b0958caef4347b0e644 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Wed, 1 Dec 2021 18:03:40 -0800 Subject: [PATCH 22/45] Apply proposer boost to ancestors correctly --- specs/phase0/fork-choice.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 279a08f8ca..281bf4e4d3 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -182,12 +182,13 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) proposer_score = Gwei(0) - if store.proposer_boost_root != Root() and root == store.proposer_boost_root: - num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) - avg_balance = get_total_active_balance(state) // num_validators - committee_size = num_validators // SLOTS_PER_EPOCH - committee_weight = committee_size * avg_balance - proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 + if store.proposer_boost_root != Root(): + if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root: + num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) + avg_balance = get_total_active_balance(state) // num_validators + committee_size = num_validators // SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 return attestation_score + proposer_score ``` From 6f3379c7bee3da83caaa4da347e4ef8801e2a257 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Wed, 1 Dec 2021 20:11:52 -0800 Subject: [PATCH 23/45] Apply HWW's suggestion Co-authored-by: Hsiao-Wei Wang --- specs/phase0/fork-choice.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 281bf4e4d3..497c06dc6d 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -182,13 +182,15 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) proposer_score = Gwei(0) - if store.proposer_boost_root != Root(): - if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root: - num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) - avg_balance = get_total_active_balance(state) // num_validators - committee_size = num_validators // SLOTS_PER_EPOCH - committee_weight = committee_size * avg_balance - proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 + if ( + store.proposer_boost_root != Root() + and get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root + ): + num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) + avg_balance = get_total_active_balance(state) // num_validators + committee_size = num_validators // SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 return attestation_score + proposer_score ``` From a399d953d3c2e32744c94eb6397a6454dbac7775 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 2 Dec 2021 17:21:42 +0800 Subject: [PATCH 24/45] Add assertion to `test_shorter_chain_but_heavier_weight` --- .../pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py | 3 +++ 1 file changed, 3 insertions(+) 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 d2c84fce79..a524cbd565 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 @@ -166,6 +166,9 @@ def test_shorter_chain_but_heavier_weight(spec, state): signed_short_block = state_transition_and_sign_block(spec, short_state, short_block) yield from tick_and_add_block(spec, store, signed_short_block, test_steps) + # Since the long chain has higher proposer_score at slot 1, the latest long block is the head + assert spec.get_head(store) == spec.hash_tree_root(long_block) + short_attestation = get_valid_attestation(spec, short_state, short_block.slot, signed=True) yield from tick_and_run_on_attestation(spec, store, short_attestation, test_steps) From 0d8fab3986e3338c76747e1582aa53b5707c345b Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Thu, 2 Dec 2021 09:57:28 -0800 Subject: [PATCH 25/45] Apply Danny's suggestion --- specs/phase0/fork-choice.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 497c06dc6d..fe546142b4 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -181,11 +181,13 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: if (i in store.latest_messages and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) + if store.proposer_boost_root == Root(): + # Return only attestation score if ``proposer_boost_root`` is not set + return attestation_score + # Calculate proposer score if ``proposer_boost_root`` is set proposer_score = Gwei(0) - if ( - store.proposer_boost_root != Root() - and get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root - ): + # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` + if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root: num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) avg_balance = get_total_active_balance(state) // num_validators committee_size = num_validators // SLOTS_PER_EPOCH From 6308cee7a64d1820a38af0a3968481000a7a3081 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 3 Dec 2021 02:17:57 +0800 Subject: [PATCH 26/45] Fix typo --- .../pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index 806fe7ec48..c47ccefda9 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -34,7 +34,7 @@ def _apply_base_block_a(spec, state, store, test_steps): @with_all_phases @spec_state_test -def test_ex_ante_secnario_1_with_boost(spec, state): +def test_ex_ante_scenario_1_with_boost(spec, state): """ With a single adversarial attestation @@ -98,7 +98,7 @@ def _filter_participant_set(participants): @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, }) -def test_ex_ante_secnario_1_without_boost(spec, state): +def test_ex_ante_scenario_1_without_boost(spec, state): """ With a single adversarial attestation From 3a9777eebc0bc0aedf3f2286a5282785dad97341 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Thu, 2 Dec 2021 14:00:06 -0700 Subject: [PATCH 27/45] Update specs/phase0/fork-choice.md --- specs/phase0/fork-choice.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index fe546142b4..d3b0e2dc54 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -184,6 +184,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: if store.proposer_boost_root == Root(): # Return only attestation score if ``proposer_boost_root`` is not set return attestation_score + # Calculate proposer score if ``proposer_boost_root`` is set proposer_score = Gwei(0) # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` From 9dc1b7af7e6f4a0b3fa79aa10a4deff71fb50c37 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 7 Dec 2021 00:23:01 +0800 Subject: [PATCH 28/45] Test with strict `proposer_boost + 1` attestation participants --- .../test/phase0/fork_choice/test_ex_ante.py | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index c47ccefda9..b3a8b38fb2 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -167,6 +167,26 @@ def _filter_participant_set(participants): yield 'steps', test_steps +def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root): + """ + Return the minimum attestation participant count such that attestation_score > proposer_score + """ + # calculate proposer boost score + block = store.blocks[root] + proposer_score = 0 + if spec.get_ancestor(store, root, block.slot) == proposer_boost_root: + num_validators = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) + avg_balance = spec.get_total_active_balance(state) // num_validators + committee_size = num_validators // spec.SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * spec.config.PROPOSER_SCORE_BOOST) // 100 + + # calculate minimum participant count such that attestation_score > proposer_score + base_effective_balance = state.validators[0].effective_balance + + return proposer_score // base_effective_balance + 1 + + @with_all_phases @with_presets([MAINNET], reason="to create larger committee") @spec_state_test @@ -177,7 +197,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size > proposer_boost + Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants """ test_steps = [] # Initialization @@ -202,12 +222,6 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st block = build_empty_block(spec, state_c, slot=state_a.slot + 2) signed_block_c = state_transition_and_sign_block(spec, state_c, block) - # Full attestation received at N+2 — B is head due to boost proposer - attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 - sign_attestation(spec, state_b, attestation) - # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -218,6 +232,20 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st yield from add_block(spec, store, signed_block_b, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + # Attestation of proposer_boost + 1 participants + proposer_boost_root = signed_block_b.message.hash_tree_root() + root = signed_block_b.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + sign_attestation(spec, state_b, attestation) + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. # (B's proposer_score = C's attestation_score = 0) yield from add_attestation(spec, store, attestation, test_steps) @@ -238,7 +266,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size > proposer_boost + Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants """ # For testing `PROPOSER_SCORE_BOOST = 0` case yield 'PROPOSER_SCORE_BOOST', 'meta', 0 @@ -266,12 +294,6 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, block = build_empty_block(spec, state_c, slot=state_a.slot + 2) signed_block_c = state_transition_and_sign_block(spec, state_c, block) - # Full attestation received at N+2 — B is head due to boost proposer - attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 - sign_attestation(spec, state_b, attestation) - # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -286,6 +308,20 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, else: assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + # Attestation of proposer_boost + 1 participants + proposer_boost_root = signed_block_b.message.hash_tree_root() + root = signed_block_b.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + sign_attestation(spec, state_b, attestation) + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score yield from add_attestation(spec, store, attestation, test_steps) assert spec.get_head(store) == signed_block_b.message.hash_tree_root() From 57be9d064f933626aaa0c3d6bbcd713ccc59289d Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 16:51:11 +0100 Subject: [PATCH 29/45] update per-test config to be unique per-test --- tests/core/pyspec/eth2spec/test/context.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 2a6f2e324a..4916e008c6 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -491,22 +491,18 @@ def with_config_overrides(config_overrides): """ def decorator(fn): def wrapper(*args, spec: Spec, **kw): - # remember the old config - old_config = spec.config + spec = deepcopy(spec) # apply our overrides to a copy of it, and apply it to the spec - tmp_config = deepcopy(old_config._asdict()) # not a private method, there are multiple - tmp_config.update(config_overrides) - config_types = spec.Configuration.__annotations__ - # Retain types of all config values - test_config = {k: config_types[k](v) for k, v in tmp_config.items()} + spec.config.update(config_overrides) # To output the changed config to could be serialized with yaml test vectors, # the dict SSZ objects have to be converted into Python built-in types. - output_config = _get_basic_dict(test_config) + output_config = _get_basic_dict(spec.config) yield 'config', 'data', output_config - spec.config = spec.Configuration(**test_config) + # Output the config for test vectors (TODO: check config YAML encoding) + yield 'config', 'data', spec.config # Run the function out = fn(*args, spec=spec, **kw) @@ -514,10 +510,6 @@ def wrapper(*args, spec: Spec, **kw): # it's generating things, and we need to complete it before setting back the config. if out is not None: yield from out - - # Restore the old config and apply it - spec.config = old_config - return wrapper return decorator From 733f37715e342823a0a9a93810051e573d62b0a4 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 17:51:32 +0100 Subject: [PATCH 30/45] use `importlib` to perform an actual spec copy --- tests/core/pyspec/eth2spec/test/context.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 4916e008c6..0764bbe4af 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -1,6 +1,6 @@ import pytest -from copy import deepcopy from dataclasses import dataclass +import importlib from eth_utils import encode_hex from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal @@ -481,6 +481,16 @@ def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: return result +def _get_copy_of_spec(spec): + fork = spec.fork + preset = spec.config.PRESET_BASE + path = f"eth2spec.{fork}.{preset}" + + module_spec = importlib.util.find_spec(path) + module = importlib.util.module_from_spec(module_spec) + return module + + def with_config_overrides(config_overrides): """ WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs. @@ -491,7 +501,7 @@ def with_config_overrides(config_overrides): """ def decorator(fn): def wrapper(*args, spec: Spec, **kw): - spec = deepcopy(spec) + spec = _get_copy_of_spec(spec) # apply our overrides to a copy of it, and apply it to the spec spec.config.update(config_overrides) @@ -501,9 +511,6 @@ def wrapper(*args, spec: Spec, **kw): output_config = _get_basic_dict(spec.config) yield 'config', 'data', output_config - # Output the config for test vectors (TODO: check config YAML encoding) - yield 'config', 'data', spec.config - # Run the function out = fn(*args, spec=spec, **kw) # If it's not returning None like a normal test function, From 55c9c03f08696693af9f4e30e235d50e799f0bcd Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 18:11:22 +0100 Subject: [PATCH 31/45] simply module import and fix config adjustment --- tests/core/pyspec/eth2spec/test/context.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 0764bbe4af..726723992a 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -484,10 +484,10 @@ def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: def _get_copy_of_spec(spec): fork = spec.fork preset = spec.config.PRESET_BASE - path = f"eth2spec.{fork}.{preset}" - - module_spec = importlib.util.find_spec(path) + module_path = f"eth2spec.{fork}.{preset}" + module_spec = importlib.util.find_spec(module_path) module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) return module @@ -504,13 +504,18 @@ def wrapper(*args, spec: Spec, **kw): spec = _get_copy_of_spec(spec) # apply our overrides to a copy of it, and apply it to the spec - spec.config.update(config_overrides) + config = spec.config._asdict() + config.update(config_overrides) + config_types = spec.Configuration.__annotations__ + modified_config = {k: config_types[k](v) for k, v in config.items()} # To output the changed config to could be serialized with yaml test vectors, # the dict SSZ objects have to be converted into Python built-in types. - output_config = _get_basic_dict(spec.config) + output_config = _get_basic_dict(modified_config) yield 'config', 'data', output_config + spec.config = spec.Configuration(**modified_config) + # Run the function out = fn(*args, spec=spec, **kw) # If it's not returning None like a normal test function, From db2be42baaf763d44b409deff4c84700143d70cf Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 18:50:00 +0100 Subject: [PATCH 32/45] use a specific `spec` rather than the pre-defined phases --- tests/core/pyspec/eth2spec/test/context.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 726723992a..260cb4d7d5 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -86,10 +86,9 @@ class SpecForks(TypedDict, total=False): def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int], spec: Spec, phases: SpecForks): - phase = phases[spec.fork] - balances = balances_fn(phase) - activation_threshold = threshold_fn(phase) - state = create_genesis_state(spec=phase, validator_balances=balances, + balances = balances_fn(spec) + activation_threshold = threshold_fn(spec) + state = create_genesis_state(spec=spec, validator_balances=balances, activation_threshold=activation_threshold) return state From dc9db78a67e68e69503e29e20d66aa9c855546cb Mon Sep 17 00:00:00 2001 From: Caspar Schwarz-Schilling Date: Wed, 8 Dec 2021 17:05:44 +0100 Subject: [PATCH 33/45] Add sandwich test scenarios --- .../test/phase0/fork_choice/test_ex_ante.py | 391 +++++++++++++++++- 1 file changed, 387 insertions(+), 4 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index b3a8b38fb2..a200e10af5 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -60,7 +60,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): state_b = state_a.copy() block = build_empty_block(spec, state_a, slot=state_a.slot + 1) signed_block_b = state_transition_and_sign_block(spec, state_b, block) - + # Block C at slot `N + 2`, parent is A state_c = state_a.copy() block = build_empty_block(spec, state_c, slot=state_a.slot + 2) @@ -89,7 +89,7 @@ def _filter_participant_set(participants): # Attestation_1 received at N+2 — C is head yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() yield 'steps', test_steps @@ -188,7 +188,7 @@ def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_ro @with_all_phases -@with_presets([MAINNET], reason="to create larger committee") +@with_presets([MAINNET], reason="to create non-duplicate committee") @spec_state_test def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): """ @@ -258,7 +258,7 @@ def _filter_participant_set(participants): @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, }) -@with_presets([MAINNET], reason="to create larger committee") +@with_presets([MAINNET], reason="to create non-duplicate committee") def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, state): """ Adversarial attestations > proposer boost @@ -327,3 +327,386 @@ def _filter_participant_set(participants): assert spec.get_head(store) == signed_block_b.message.hash_tree_root() yield 'steps', test_steps + +@with_all_phases +@spec_state_test +def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): + """ + Simple Sandwich test with boost and no attestations. + Obejcts: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head (with boost) + Block D received at N+3 — D is head (with boost) + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - D is head, it has proposer score boost + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): + """ + Simple Sandwich test with no boost and no attestations. + Obejcts: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — B or C is head (chosen lexicographically; without boost) + Block D received at N+3 — D or C is head (chosen lexicographically; without boost) + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 + # Block D and C have the same score 0. Use a lexicographical order for tie-breaking. + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_d, test_steps) + if signed_block_d.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@spec_state_test +def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): + """ + Boosting necessary to sandwich attack. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_1 (Block C); size 1 - slot N+2 (honest) + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+3 — C is head + Block D received at N+3 — D is head + + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 at N+2 voting for block C + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_c, attestation) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+3 — C is head + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - D is head, it has proposer score boost + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): + """ + Boost necessary to sandwich attack: no boost, so not successful here. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_1 (Block C); size 1 - slot N+2 (honest) + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — B or C is head (chosen lexicographically) + Attestation_1 received at N+3 — C is head + Block D received at N+3 — C is head + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C have the same score, 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 at N+2 voting for block C + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_c, attestation) + + # Attestation_1 received at N+3 - C is head + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - C is head, because block D has no proposer boost + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@with_presets([MAINNET], reason="to create non-duplicate committee") +@spec_state_test +def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state): + """ + Boost not sufficient to sandwich attack. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_set_1 (Block C); size proposer_boost + 1 - slot N+2 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_set_1 received — C is head + Block D received at N+3 — C is head + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_set_1 at N+2 voting for block C + proposer_boost_root = signed_block_c.message.hash_tree_root() + root = signed_block_c.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + sign_attestation(spec, state_c, attestation) + + # Attestation_1 received at N+3 — B is head because B's attestation_score > C's proposer_score. + # (B's proposer_score = C's attestation_score = 0) + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - C is head, D's boost not sufficient! + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps From 79e1d5e0216320ffef462a8f08839e56d0614106 Mon Sep 17 00:00:00 2001 From: Caspar Schwarz-Schilling Date: Wed, 8 Dec 2021 18:14:46 +0100 Subject: [PATCH 34/45] Fix head view comparison --- .../pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index a200e10af5..bf526642a2 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -455,7 +455,7 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) yield from add_block(spec, store, signed_block_d, test_steps) - if signed_block_d.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): assert spec.get_head(store) == signed_block_d.message.hash_tree_root() else: assert spec.get_head(store) == signed_block_c.message.hash_tree_root() From 9f614fca83b0e974f25488f9fc523702660b2a42 Mon Sep 17 00:00:00 2001 From: Caspar Schwarz-Schilling Date: Wed, 8 Dec 2021 18:31:14 +0100 Subject: [PATCH 35/45] Fix linting --- .../test/phase0/fork_choice/test_ex_ante.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index bf526642a2..e85e9e9370 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -60,7 +60,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): state_b = state_a.copy() block = build_empty_block(spec, state_a, slot=state_a.slot + 1) signed_block_b = state_transition_and_sign_block(spec, state_b, block) - + # Block C at slot `N + 2`, parent is A state_c = state_a.copy() block = build_empty_block(spec, state_c, slot=state_a.slot + 2) @@ -89,7 +89,7 @@ def _filter_participant_set(participants): # Attestation_1 received at N+2 — C is head yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() yield 'steps', test_steps @@ -328,6 +328,7 @@ def _filter_participant_set(participants): yield 'steps', test_steps + @with_all_phases @spec_state_test def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): @@ -371,7 +372,7 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -390,6 +391,7 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): yield 'steps', test_steps + @with_all_phases @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, @@ -435,7 +437,7 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -462,6 +464,7 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): yield 'steps', test_steps + @with_all_phases @spec_state_test def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): @@ -519,7 +522,7 @@ def _filter_participant_set(participants): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -542,6 +545,7 @@ def _filter_participant_set(participants): yield 'steps', test_steps + @with_all_phases @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, @@ -589,7 +593,7 @@ def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -614,7 +618,7 @@ def _filter_participant_set(participants): attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 sign_attestation(spec, state_c, attestation) - + # Attestation_1 received at N+3 - C is head time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -627,6 +631,7 @@ def _filter_participant_set(participants): yield 'steps', test_steps + @with_all_phases @with_presets([MAINNET], reason="to create non-duplicate committee") @spec_state_test @@ -673,7 +678,7 @@ def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) From addc03cb7c1111726fb838b67d8d9b69b97a0312 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 9 Dec 2021 17:47:20 +0800 Subject: [PATCH 36/45] Enable ex-ante tests in testgen and minor fixes --- .../test/phase0/fork_choice/test_ex_ante.py | 13 +++++++++++-- tests/generators/fork_choice/main.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index e85e9e9370..b4013c18de 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -34,7 +34,7 @@ def _apply_base_block_a(spec, state, store, test_steps): @with_all_phases @spec_state_test -def test_ex_ante_scenario_1_with_boost(spec, state): +def test_ex_ante_vanilla_with_boost(spec, state): """ With a single adversarial attestation @@ -98,7 +98,7 @@ def _filter_participant_set(participants): @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, }) -def test_ex_ante_scenario_1_without_boost(spec, state): +def test_ex_ante_vanilla_without_boost(spec, state): """ With a single adversarial attestation @@ -244,6 +244,7 @@ def _filter_participant_set(participants): spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set ) attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_b, attestation) # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. @@ -320,6 +321,7 @@ def _filter_participant_set(participants): spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set ) attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_b, attestation) # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score @@ -410,6 +412,9 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): Block B received at N+2 — B or C is head (chosen lexicographically; without boost) Block D received at N+3 — D or C is head (chosen lexicographically; without boost) """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -566,6 +571,9 @@ def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): Attestation_1 received at N+3 — C is head Block D received at N+3 — C is head """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -701,6 +709,7 @@ def _filter_participant_set(participants): spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set ) attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_c, attestation) # Attestation_1 received at N+3 — B is head because B's attestation_score > C's proposer_score. diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index 562f851d11..b194dc3bdf 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -6,6 +6,7 @@ phase_0_mods = {key: 'eth2spec.test.phase0.fork_choice.test_' + key for key in [ 'get_head', 'on_block', + 'ex_ante', ]} # No additional Altair specific finality tests, yet. altair_mods = phase_0_mods From 11a42f8d0661ddcc56831d1788b3066bd1d06372 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 9 Dec 2021 17:58:31 +0800 Subject: [PATCH 37/45] Remove `*_without_boost` tests --- .../test/phase0/fork_choice/test_ex_ante.py | 315 ------------------ 1 file changed, 315 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index b4013c18de..00d58356fc 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -1,6 +1,5 @@ from eth2spec.test.context import ( MAINNET, - spec_configured_state_test, spec_state_test, with_all_phases, with_presets, @@ -94,79 +93,6 @@ def _filter_participant_set(participants): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -def test_ex_ante_vanilla_without_boost(spec, state): - """ - With a single adversarial attestation - - NOTE: this case disabled proposer score boost by setting config `PROPOSER_SCORE_BOOST` to `0` - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size 1 - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - test_steps = [] - # Initialization - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - yield 'anchor_state', state - yield 'anchor_block', anchor_block - current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, current_time, test_steps) - assert store.time == current_time - - # On receiving block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Attestation_1 received at N+2 — B is head due to boost proposer - def _filter_participant_set(participants): - return [next(iter(participants))] - - attestation = get_valid_attestation( - spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set - ) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 - sign_attestation(spec, state_b, attestation) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Attestation_1 received at N+2 — B is head - yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - - yield 'steps', test_steps - - def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root): """ Return the minimum attestation participant count such that attestation_score > proposer_score @@ -255,82 +181,6 @@ def _filter_participant_set(participants): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -@with_presets([MAINNET], reason="to create non-duplicate committee") -def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, state): - """ - Adversarial attestations > proposer boost - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - test_steps = [] - # Initialization - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - yield 'anchor_state', state - yield 'anchor_block', anchor_block - current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, current_time, test_steps) - assert store.time == current_time - - # On receiving block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Attestation of proposer_boost + 1 participants - proposer_boost_root = signed_block_b.message.hash_tree_root() - root = signed_block_b.message.hash_tree_root() - participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) - - def _filter_participant_set(participants): - return [index for i, index in enumerate(participants) if i < participant_num] - - attestation = get_valid_attestation( - spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set - ) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num - sign_attestation(spec, state_b, attestation) - - # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score - yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - - yield 'steps', test_steps - - @with_all_phases @spec_state_test def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): @@ -394,82 +244,6 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): - """ - Simple Sandwich test with no boost and no attestations. - Obejcts: - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Block D (parent B) - slot N+3 - Steps: - Block A received at N — A is head - Block C received at N+2 — C is head - Block B received at N+2 — B or C is head (chosen lexicographically; without boost) - Block D received at N+3 — D or C is head (chosen lexicographically; without boost) - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - test_steps = [] - # Initialization - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - yield 'anchor_state', state - yield 'anchor_block', anchor_block - current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, current_time, test_steps) - assert store.time == current_time - - # On receiving block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Block D at slot `N + 3`, parent is B - state_d = state_b.copy() - block = build_empty_block(spec, state_d, slot=state_a.slot + 3) - signed_block_d = state_transition_and_sign_block(spec, state_d, block) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block D received at N+3 - # Block D and C have the same score 0. Use a lexicographical order for tie-breaking. - time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_d, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_d.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - yield 'steps', test_steps - - @with_all_phases @spec_state_test def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): @@ -551,95 +325,6 @@ def _filter_participant_set(participants): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): - """ - Boost necessary to sandwich attack: no boost, so not successful here. - Objects: - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Block D (parent B) - slot N+3 - Attestation_1 (Block C); size 1 - slot N+2 (honest) - Steps: - Block A received at N — A is head - Block C received at N+2 — C is head - Block B received at N+2 — B or C is head (chosen lexicographically) - Attestation_1 received at N+3 — C is head - Block D received at N+3 — C is head - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - test_steps = [] - # Initialization - store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) - yield 'anchor_state', state - yield 'anchor_block', anchor_block - current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, current_time, test_steps) - assert store.time == current_time - - # On receiving block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Block D at slot `N + 3`, parent is B - state_d = state_b.copy() - block = build_empty_block(spec, state_d, slot=state_a.slot + 3) - signed_block_d = state_transition_and_sign_block(spec, state_d, block) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score, 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Attestation_1 at N+2 voting for block C - def _filter_participant_set(participants): - return [next(iter(participants))] - - attestation = get_valid_attestation( - spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set - ) - attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 - sign_attestation(spec, state_c, attestation) - - # Attestation_1 received at N+3 - C is head - time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block D received at N+3 - C is head, because block D has no proposer boost - yield from add_block(spec, store, signed_block_d, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - yield 'steps', test_steps - - @with_all_phases @with_presets([MAINNET], reason="to create non-duplicate committee") @spec_state_test From 916193bd69848ca80981458226acc2bb9f4840a6 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Fri, 10 Dec 2021 07:48:14 -0600 Subject: [PATCH 38/45] Updates in response to comments --- specs/altair/sync-protocol.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 8da4a4d988..a36bf782b6 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -53,7 +53,6 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. | - | - | - | | `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | | | `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours | -| `SAFETY_THRESHOLD_PERIOD` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD // 2` | ~13.6 hours | ## Containers @@ -70,8 +69,7 @@ class LightClientUpdate(Container): finalized_header: BeaconBlockHeader finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)] # Sync committee aggregate signature - sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE] - sync_committee_signature: BLSSignature + sync_committee_aggregate: SyncAggregate # Fork version for the aggregate signature fork_version: Version ``` @@ -135,9 +133,16 @@ A light client maintains its state in a `store` object of type `LightClientStore ```python def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None: - if current_slot % SAFETY_THRESHOLD_PERIOD == 0: + if current_slot % UPDATE_TIMEOUT == 0: store.previous_max_active_participants = store.current_max_active_participants store.current_max_active_participants = 0 + if ( + current_slot > store.finalized_header.slot + UPDATE_TIMEOUT + and store.best_valid_update is not None + ): + # Forced best update when the update timeout has elapsed + apply_light_client_update(store, store.best_valid_update) + store.best_valid_update = None ``` #### `validate_light_client_update` @@ -157,7 +162,8 @@ def validate_light_client_update(store: LightClientStore, update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD assert update_period in (finalized_period, finalized_period + 1) - # Verify update header root is the finalized root of the finality header, if specified + # Verify that the `finalized_header`, if present, actually is the finalized header saved in the + # state of the `attested header` if update.finalized_header == BeaconBlockHeader(): assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))] else: @@ -182,15 +188,17 @@ def validate_light_client_update(store: LightClientStore, index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX), root=active_header.state_root, ) + + sync_aggregate = update.sync_committee_aggregate # Verify sync committee has sufficient participants - assert sum(update.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS + assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS # Verify sync committee aggregate signature - participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit] + participant_pubkeys = [pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) if bit] domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root) signing_root = compute_signing_root(update.attested_header, domain) - assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature) + assert bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature) ``` #### `apply_light_client_update` @@ -244,11 +252,4 @@ def process_light_client_update(store: LightClientStore, # Normal update through 2/3 threshold apply_light_client_update(store, update) store.best_valid_update = None - elif ( - current_slot > store.finalized_header.slot + UPDATE_TIMEOUT - and store.best_valid_update is not None - ): - # Forced best update when the update timeout has elapsed - apply_light_client_update(store, store.best_valid_update) - store.best_valid_update = None ``` From 4cea972cc0f70e5f64e42d1832fdce3b0470af8a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Dec 2021 09:01:16 +0800 Subject: [PATCH 39/45] Apply suggestions from code review Co-authored-by: Caspar Schwarz-Schilling <31305984+casparschwa@users.noreply.github.com> --- .../test/phase0/fork_choice/test_ex_ante.py | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index 00d58356fc..660b00eb7f 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -33,14 +33,19 @@ def _apply_base_block_a(spec, state, store, test_steps): @with_all_phases @spec_state_test -def test_ex_ante_vanilla_with_boost(spec, state): +def test_ex_ante_vanilla(spec, state): """ With a single adversarial attestation - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size 1 + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B); size `1` - slot N+1 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+2 — C is head """ test_steps = [] # Initialization @@ -65,7 +70,7 @@ def test_ex_ante_vanilla_with_boost(spec, state): block = build_empty_block(spec, state_c, slot=state_a.slot + 2) signed_block_c = state_transition_and_sign_block(spec, state_c, block) - # Attestation_1 received at N+2 — B is head due to boost proposer + # Attestation_1 at slot `N + 1` voting for block B def _filter_participant_set(participants): return [next(iter(participants))] @@ -82,7 +87,7 @@ def _filter_participant_set(participants): yield from add_block(spec, store, signed_block_c, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - # Block B received at N+2 — C is head that has higher proposer score boost + # Block B received at N+2 — C is head due to proposer score boost yield from add_block(spec, store, signed_block_b, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() @@ -119,11 +124,16 @@ def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_ro def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): """ Adversarial attestations > proposer boost - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_set_1 (Block B); size `proposer_boost + 1` - slot N+1 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+2 — B is head """ test_steps = [] # Initialization @@ -154,11 +164,11 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st yield from add_block(spec, store, signed_block_c, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - # Block B received at N+2 — C is head that has higher proposer score boost + # Block B received at N+2 — C is head due to proposer score boost yield from add_block(spec, store, signed_block_b, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - # Attestation of proposer_boost + 1 participants + # Attestation_set_1 at slot `N + 1` voting for block B proposer_boost_root = signed_block_b.message.hash_tree_root() root = signed_block_b.message.hash_tree_root() participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) @@ -173,7 +183,7 @@ def _filter_participant_set(participants): assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_b, attestation) - # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. + # Attestation_set_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. # (B's proposer_score = C's attestation_score = 0) yield from add_attestation(spec, store, attestation, test_steps) assert spec.get_head(store) == signed_block_b.message.hash_tree_root() @@ -183,7 +193,7 @@ def _filter_participant_set(participants): @with_all_phases @spec_state_test -def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): +def test_ex_ante_sandwich_without_attestations(spec, state): """ Simple Sandwich test with boost and no attestations. Obejcts: @@ -246,7 +256,7 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): @with_all_phases @spec_state_test -def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): +def test_ex_ante_sandwich_with_honest_attestation(spec, state): """ Boosting necessary to sandwich attack. Objects: From 6edf840992af06f1aac868fc3a3a1e28e09c8098 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Dec 2021 09:06:56 +0800 Subject: [PATCH 40/45] fix lint --- .../pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index 660b00eb7f..d931011565 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -36,7 +36,7 @@ def _apply_base_block_a(spec, state, store, test_steps): def test_ex_ante_vanilla(spec, state): """ With a single adversarial attestation - Objects: + Objects: Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 @@ -124,7 +124,7 @@ def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_ro def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): """ Adversarial attestations > proposer boost - Objects: + Objects: Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 From 2f618f7b483310303525dfe592d176f3fc91f087 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Dec 2021 21:38:58 +0800 Subject: [PATCH 41/45] Fix lint and presets --- presets/mainnet/altair.yaml | 2 -- presets/minimal/altair.yaml | 2 -- specs/altair/sync-protocol.md | 5 ++++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/presets/mainnet/altair.yaml b/presets/mainnet/altair.yaml index 21e3cc3285..813ef72122 100644 --- a/presets/mainnet/altair.yaml +++ b/presets/mainnet/altair.yaml @@ -24,5 +24,3 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 # SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 32 * 256) UPDATE_TIMEOUT: 8192 -# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD // 2 (= 32 * 256 // 2) -SAFETY_THRESHOLD_PERIOD: 4096 diff --git a/presets/minimal/altair.yaml b/presets/minimal/altair.yaml index 7cdbd58ea7..5e472c49cf 100644 --- a/presets/minimal/altair.yaml +++ b/presets/minimal/altair.yaml @@ -24,5 +24,3 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 # SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 8 * 8) UPDATE_TIMEOUT: 64 -# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD // 2 (= 8 * 8 // 2) -SAFETY_THRESHOLD_PERIOD: 32 diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index a36bf782b6..6dae145b95 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -195,7 +195,10 @@ def validate_light_client_update(store: LightClientStore, assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS # Verify sync committee aggregate signature - participant_pubkeys = [pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) if bit] + participant_pubkeys = [ + pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) + if bit + ] domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root) signing_root = compute_signing_root(update.attested_header, domain) assert bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature) From 25d88fee244cc687b0cd98a5a2b7cadb9d01f94a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Dec 2021 22:05:09 +0800 Subject: [PATCH 42/45] Fix `process_light_client_update` --- specs/altair/sync-protocol.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 6dae145b95..fb86c2bf11 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -226,30 +226,32 @@ def process_light_client_update(store: LightClientStore, genesis_validators_root: Root) -> None: validate_light_client_update(store, update, current_slot, genesis_validators_root) - + + sync_committee_bits = update.sync_committee_aggregate.sync_committee_bits + # Update the best update in case we have to force-update to it if the timeout elapses if ( store.best_valid_update is None - or sum(update.sync_committee_bits) > sum(store.best_valid_update.sync_committee_bits) + or sum(sync_committee_bits) > sum(store.best_valid_update.sync_committee_aggregate.sync_committee_bits) ): store.best_valid_update = update # Track the maximum number of active participants in the committee signatures store.current_max_active_participants = max( store.current_max_active_participants, - sum(update.sync_committee_bits), + sum(sync_committee_bits), ) # Update the optimistic header if ( - sum(update.sync_committee_bits) > get_safety_threshold(store) and + sum(sync_committee_bits) > get_safety_threshold(store) and update.attested_header.slot > store.optimistic_header.slot ): store.optimistic_header = update.attested_header # Update finalized header if ( - sum(update.sync_committee_bits) * 3 >= len(update.sync_committee_bits) * 2 + sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2 and update.finalized_header != BeaconBlockHeader() ): # Normal update through 2/3 threshold From 257c2413a57ec4b311f462931b47861be8ec9ada Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Dec 2021 22:06:25 +0800 Subject: [PATCH 43/45] Update test_sync_protocol.py per the new optimistic_header and data structure --- .../altair/unittests/test_sync_protocol.py | 110 +++++++++--------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py b/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py index 15444df819..30444c4ce4 100644 --- a/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py +++ b/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from eth2spec.test.context import ( spec_state_test, with_presets, @@ -19,20 +21,24 @@ from eth2spec.test.helpers.merkle import build_proof -@with_altair_and_later -@spec_state_test -def test_process_light_client_update_not_updated(spec, state): - pre_snapshot = spec.LightClientSnapshot( - header=spec.BeaconBlockHeader(), +def _initialize_light_client_store(spec, state): + return spec.LightClientStore( + finalized_header=spec.BeaconBlockHeader(), current_sync_committee=state.current_sync_committee, next_sync_committee=state.next_sync_committee, + best_valid_update=None, + optimistic_header=spec.BeaconBlockHeader(), + previous_max_active_participants=0, + current_max_active_participants=0, ) - store = spec.LightClientStore( - snapshot=pre_snapshot, - valid_updates=set(), - ) - # Block at slot 1 doesn't increase sync committee period, so it won't update snapshot + +@with_altair_and_later +@spec_state_test +def test_process_light_client_update_not_timeout(spec, state): + store = _initialize_light_client_store(spec, state) + + # Block at slot 1 doesn't increase sync committee period, so it won't force update store.finalized_header block = build_empty_block_for_next_slot(spec, state) signed_block = state_transition_and_sign_block(spec, state, block) block_header = spec.BeaconBlockHeader( @@ -52,6 +58,10 @@ def test_process_light_client_update_not_updated(spec, state): block_header.slot, committee, ) + sync_committee_aggregate = spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))] # Ensure that finality checkpoint is genesis @@ -61,40 +71,34 @@ def test_process_light_client_update_not_updated(spec, state): finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))] update = spec.LightClientUpdate( - header=block_header, + attested_header=block_header, next_sync_committee=state.next_sync_committee, next_sync_committee_branch=next_sync_committee_branch, - finality_header=finality_header, + finalized_header=finality_header, finality_branch=finality_branch, - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, + sync_committee_aggregate=sync_committee_aggregate, fork_version=state.fork.current_version, ) + pre_store = deepcopy(store) + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) - assert len(store.valid_updates) == 1 - assert store.valid_updates.pop() == update - assert store.snapshot == pre_snapshot + assert store.current_max_active_participants > 0 + assert store.optimistic_header == update.attested_header + assert store.finalized_header == pre_store.finalized_header + assert store.best_valid_update == update @with_altair_and_later @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_process_light_client_update_timeout(spec, state): - pre_snapshot = spec.LightClientSnapshot( - header=spec.BeaconBlockHeader(), - current_sync_committee=state.current_sync_committee, - next_sync_committee=state.next_sync_committee, - ) - store = spec.LightClientStore( - snapshot=pre_snapshot, - valid_updates=set(), - ) + store = _initialize_light_client_store(spec, state) # Forward to next sync committee period - next_slots(spec, state, spec.SLOTS_PER_EPOCH * (spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD)) - snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + next_slots(spec, state, spec.UPDATE_TIMEOUT) + snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD assert snapshot_period + 1 == update_period @@ -119,6 +123,10 @@ def test_process_light_client_update_timeout(spec, state): committee, block_root=spec.Root(block_header.hash_tree_root()), ) + sync_committee_aggregate = spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) # Sync committee is updated next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX) @@ -127,36 +135,30 @@ def test_process_light_client_update_timeout(spec, state): finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))] update = spec.LightClientUpdate( - header=block_header, + attested_header=block_header, next_sync_committee=state.next_sync_committee, next_sync_committee_branch=next_sync_committee_branch, - finality_header=finality_header, + finalized_header=finality_header, finality_branch=finality_branch, - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, + sync_committee_aggregate=sync_committee_aggregate, fork_version=state.fork.current_version, ) + pre_store = deepcopy(store) + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) - # snapshot has been updated - assert len(store.valid_updates) == 0 - assert store.snapshot.header == update.header + assert store.current_max_active_participants > 0 + assert store.optimistic_header == update.attested_header + assert store.best_valid_update == update + assert store.finalized_header == pre_store.finalized_header @with_altair_and_later @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_process_light_client_update_finality_updated(spec, state): - pre_snapshot = spec.LightClientSnapshot( - header=spec.BeaconBlockHeader(), - current_sync_committee=state.current_sync_committee, - next_sync_committee=state.next_sync_committee, - ) - store = spec.LightClientStore( - snapshot=pre_snapshot, - valid_updates=set(), - ) + store = _initialize_light_client_store(spec, state) # Change finality blocks = [] @@ -167,7 +169,7 @@ def test_process_light_client_update_finality_updated(spec, state): # Ensure that finality checkpoint has changed assert state.finalized_checkpoint.epoch == 3 # Ensure that it's same period - snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD assert snapshot_period == update_period @@ -199,20 +201,24 @@ def test_process_light_client_update_finality_updated(spec, state): committee, block_root=spec.Root(block_header.hash_tree_root()), ) + sync_committee_aggregate = spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) update = spec.LightClientUpdate( - header=finalized_block_header, + attested_header=block_header, next_sync_committee=state.next_sync_committee, next_sync_committee_branch=next_sync_committee_branch, - finality_header=block_header, # block_header is the signed header + finalized_header=finalized_block_header, finality_branch=finality_branch, - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, + sync_committee_aggregate=sync_committee_aggregate, fork_version=state.fork.current_version, ) spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) - # snapshot has been updated - assert len(store.valid_updates) == 0 - assert store.snapshot.header == update.header + assert store.current_max_active_participants > 0 + assert store.optimistic_header == update.attested_header + assert store.finalized_header == update.finalized_header + assert store.best_valid_update is None From de892382db6135197c68dadb205e834a8b44ed51 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Wed, 15 Dec 2021 23:43:54 +0800 Subject: [PATCH 44/45] Minor style fixes --- specs/altair/sync-protocol.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index fb86c2bf11..c8c7c3d4dc 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -120,8 +120,8 @@ def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: ```python def get_safety_threshold(store: LightClientStore) -> uint64: return max( - store.previous_max_active_participants, - store.current_max_active_participants + store.previous_max_active_participants, + store.current_max_active_participants, ) // 2 ``` @@ -152,7 +152,6 @@ def validate_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot, genesis_validators_root: Root) -> None: - # Verify update slot is larger than slot of current best finalized header active_header = get_active_header(update) assert current_slot >= active_header.slot > store.finalized_header.slot @@ -224,7 +223,6 @@ def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot, genesis_validators_root: Root) -> None: - validate_light_client_update(store, update, current_slot, genesis_validators_root) sync_committee_bits = update.sync_committee_aggregate.sync_committee_bits @@ -235,20 +233,20 @@ def process_light_client_update(store: LightClientStore, or sum(sync_committee_bits) > sum(store.best_valid_update.sync_committee_aggregate.sync_committee_bits) ): store.best_valid_update = update - + # Track the maximum number of active participants in the committee signatures store.current_max_active_participants = max( store.current_max_active_participants, sum(sync_committee_bits), ) - + # Update the optimistic header if ( - sum(sync_committee_bits) > get_safety_threshold(store) and - update.attested_header.slot > store.optimistic_header.slot + sum(sync_committee_bits) > get_safety_threshold(store) + and update.attested_header.slot > store.optimistic_header.slot ): store.optimistic_header = update.attested_header - + # Update finalized header if ( sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2 From cba3ddfc3de99e8dcc770a77e210c99693e47604 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Wed, 22 Dec 2021 15:47:38 +0800 Subject: [PATCH 45/45] bump VERSION.txt to 1.1.7 --- tests/core/pyspec/eth2spec/VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt index ab679818ce..a5ba932511 100644 --- a/tests/core/pyspec/eth2spec/VERSION.txt +++ b/tests/core/pyspec/eth2spec/VERSION.txt @@ -1 +1 @@ -1.1.6 \ No newline at end of file +1.1.7 \ No newline at end of file