From 88206117c6d7be6752455f8fa1209cce05da4287 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 15 Jul 2022 13:37:27 +0200 Subject: [PATCH] Extract full node specifics to separate doc --- README.md | 2 +- setup.py | 1 + specs/altair/light-client/full-node.md | 135 +++++++++++++++++++++ specs/altair/light-client/sync-protocol.md | 119 +----------------- 4 files changed, 141 insertions(+), 116 deletions(-) create mode 100644 specs/altair/light-client/full-node.md diff --git a/README.md b/README.md index 6b8198373..9431dd201 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Features are researched and developed in parallel, and then consolidated into se | Seq. | Code Name | Fork Epoch | Specs | | - | - | - | - | | 0 | **Phase0** |`0` | | -| 1 | **Altair** | `74240` | | +| 1 | **Altair** | `74240` | | | 2 | **Bellatrix**
(["The Merge"](https://ethereum.org/en/upgrades/merge/)) | TBD | | ### In-development Specifications diff --git a/setup.py b/setup.py index 16b002e2d..36e9dc656 100644 --- a/setup.py +++ b/setup.py @@ -889,6 +889,7 @@ class PySpecCommand(Command): """ if self.spec_fork in (ALTAIR, BELLATRIX, CAPELLA): self.md_doc_paths += """ + specs/altair/light-client/full-node.md specs/altair/light-client/sync-protocol.md specs/altair/beacon-chain.md specs/altair/bls.md diff --git a/specs/altair/light-client/full-node.md b/specs/altair/light-client/full-node.md new file mode 100644 index 000000000..a7dd55101 --- /dev/null +++ b/specs/altair/light-client/full-node.md @@ -0,0 +1,135 @@ +# Altair Light Client -- Full Node + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Helper functions](#helper-functions) + - [`compute_merkle_proof_for_state`](#compute_merkle_proof_for_state) +- [Deriving light client data](#deriving-light-client-data) + - [`create_light_client_bootstrap`](#create_light_client_bootstrap) + - [`create_light_client_update`](#create_light_client_update) + + + + +## Introduction + +This document provides helper functions to enable full nodes to serve light client data. Full nodes SHOULD implement the described functionality to enable light clients to sync with the network. + +## Helper functions + +### `compute_merkle_proof_for_state` + +```python +def compute_merkle_proof_for_state(state: BeaconState, + index: GeneralizedIndex) -> Sequence[Bytes32]: + ... +``` + +## Deriving light client data + +Full nodes are expected to derive light client data from historic blocks and states and provide it to other clients. + +### `create_light_client_bootstrap` + +```python +def create_light_client_bootstrap(state: BeaconState) -> LightClientBootstrap: + assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH + assert state.slot == state.latest_block_header.slot + + return LightClientBootstrap( + header=BeaconBlockHeader( + slot=state.latest_block_header.slot, + proposer_index=state.latest_block_header.proposer_index, + parent_root=state.latest_block_header.parent_root, + state_root=hash_tree_root(state), + body_root=state.latest_block_header.body_root, + ), + current_sync_committee=state.current_sync_committee, + current_sync_committee_branch=compute_merkle_proof_for_state(state, CURRENT_SYNC_COMMITTEE_INDEX) + ) +``` + +Full nodes SHOULD provide `LightClientBootstrap` for all finalized epoch boundary blocks in the epoch range `[max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]` where `current_epoch` is defined by the current wall-clock time. Full nodes MAY also provide `LightClientBootstrap` for other blocks. + +Blocks are considered to be epoch boundary blocks if their block root can occur as part of a valid `Checkpoint`, i.e., if their slot is the initial slot of an epoch, or if all following slots through the initial slot of the next epoch are empty (no block proposed / orphaned). + +`LightClientBootstrap` is computed from the block's immediate post state (without applying empty slots). + +### `create_light_client_update` + +To form a `LightClientUpdate`, the following historical states and blocks are needed: +- `state`: the post state of any block with a post-Altair parent block +- `block`: the corresponding block +- `attested_state`: the post state of the block referred to by `block.parent_root` +- `finalized_block`: the block referred to by `attested_state.finalized_checkpoint.root`, if locally available (may be unavailable, e.g., when using checkpoint sync, or if it was pruned locally) + +```python +def create_light_client_update(state: BeaconState, + block: SignedBeaconBlock, + attested_state: BeaconState, + finalized_block: Optional[SignedBeaconBlock]) -> LightClientUpdate: + assert compute_epoch_at_slot(attested_state.slot) >= ALTAIR_FORK_EPOCH + assert sum(block.message.body.sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS + + assert state.slot == state.latest_block_header.slot + header = state.latest_block_header.copy() + header.state_root = hash_tree_root(state) + assert hash_tree_root(header) == hash_tree_root(block.message) + update_signature_period = compute_sync_committee_period(compute_epoch_at_slot(block.message.slot)) + + assert attested_state.slot == attested_state.latest_block_header.slot + attested_header = attested_state.latest_block_header.copy() + attested_header.state_root = hash_tree_root(attested_state) + assert hash_tree_root(attested_header) == block.message.parent_root + update_attested_period = compute_sync_committee_period(compute_epoch_at_slot(attested_header.slot)) + + # `next_sync_committee` is only useful if the message is signed by the current sync committee + if update_attested_period == update_signature_period: + next_sync_committee = attested_state.next_sync_committee + next_sync_committee_branch = compute_merkle_proof_for_state(attested_state, NEXT_SYNC_COMMITTEE_INDEX) + else: + next_sync_committee = SyncCommittee() + next_sync_committee_branch = [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))] + + # Indicate finality whenever possible + if finalized_block is not None: + if finalized_block.message.slot != GENESIS_SLOT: + finalized_header = BeaconBlockHeader( + slot=finalized_block.message.slot, + proposer_index=finalized_block.message.proposer_index, + parent_root=finalized_block.message.parent_root, + state_root=finalized_block.message.state_root, + body_root=hash_tree_root(finalized_block.message.body), + ) + assert hash_tree_root(finalized_header) == attested_state.finalized_checkpoint.root + else: + assert attested_state.finalized_checkpoint.root == Bytes32() + finalized_header = BeaconBlockHeader() + finality_branch = compute_merkle_proof_for_state(attested_state, FINALIZED_ROOT_INDEX) + else: + finalized_header = BeaconBlockHeader() + finality_branch = [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))] + + return LightClientUpdate( + attested_header=attested_header, + next_sync_committee=next_sync_committee, + next_sync_committee_branch=next_sync_committee_branch, + finalized_header=finalized_header, + finality_branch=finality_branch, + sync_aggregate=block.message.body.sync_aggregate, + signature_slot=block.message.slot, + ) +``` + +Full nodes SHOULD provide the best derivable `LightClientUpdate` (according to `is_better_update`) for each sync committee period covering any epochs in range `[max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]` where `current_epoch` is defined by the current wall-clock time. Full nodes MAY also provide `LightClientUpdate` for other sync committee periods. + +- `LightClientUpdate` are assigned to sync committee periods based on their `attested_header.slot` +- `LightClientUpdate` are only considered if `compute_sync_committee_period(compute_epoch_at_slot(update.attested_header.slot)) == compute_sync_committee_period(compute_epoch_at_slot(update.signature_slot))` +- Only `LightClientUpdate` with `next_sync_committee` as selected by fork choice are provided, regardless of ranking by `is_better_update`. To uniquely identify a non-finalized sync committee fork, all of `period`, `current_sync_committee` and `next_sync_committee` need to be incorporated, as sync committees may reappear over time. diff --git a/specs/altair/light-client/sync-protocol.md b/specs/altair/light-client/sync-protocol.md index c8697d13c..a7ac8e367 100644 --- a/specs/altair/light-client/sync-protocol.md +++ b/specs/altair/light-client/sync-protocol.md @@ -1,4 +1,4 @@ -# Altair -- Minimal Light Client +# Altair Light Client -- Sync Protocol **Notice**: This document is a work-in-progress for researchers and implementers. @@ -24,7 +24,6 @@ - [`get_safety_threshold`](#get_safety_threshold) - [`compute_sync_committee_period_at_slot`](#compute_sync_committee_period_at_slot) - [`get_subtree_index`](#get_subtree_index) - - [`compute_merkle_proof_for_state`](#compute_merkle_proof_for_state) - [Light client initialization](#light-client-initialization) - [`initialize_light_client_store`](#initialize_light_client_store) - [Light client state updates](#light-client-state-updates) @@ -32,9 +31,6 @@ - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_light_client_update) - [`process_light_client_update`](#process_light_client_update) -- [Deriving light client data](#deriving-light-client-data) - - [`create_light_client_bootstrap`](#create_light_client_bootstrap) - - [`create_light_client_update`](#create_light_client_update) @@ -49,6 +45,9 @@ and metered VMs (e.g. blockchain VMs for cross-chain bridges). This document suggests a minimal light client design for the beacon chain that uses sync committees introduced in [this beacon chain extension](./beacon-chain.md). +Additional documents describe how the light client sync protocol can be used: +- [Full node](./full-node.md) + ## Constants | Name | Value | @@ -219,14 +218,6 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: return uint64(generalized_index % 2**(floorlog2(generalized_index))) ``` -### `compute_merkle_proof_for_state` - -```python -def compute_merkle_proof_for_state(state: BeaconState, - index: GeneralizedIndex) -> Sequence[Bytes32]: - ... -``` - ## Light client initialization A light client maintains its state in a `store` object of type `LightClientStore`. `initialize_light_client_store` initializes a new `store` with a received `LightClientBootstrap` derived from a given `trusted_block_root`. @@ -429,105 +420,3 @@ def process_light_client_update(store: LightClientStore, apply_light_client_update(store, update) store.best_valid_update = None ``` - -## Deriving light client data - -Full nodes are expected to derive light client data from historic blocks and states and provide it to other clients. - -### `create_light_client_bootstrap` - -```python -def create_light_client_bootstrap(state: BeaconState) -> LightClientBootstrap: - assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH - assert state.slot == state.latest_block_header.slot - - return LightClientBootstrap( - header=BeaconBlockHeader( - slot=state.latest_block_header.slot, - proposer_index=state.latest_block_header.proposer_index, - parent_root=state.latest_block_header.parent_root, - state_root=hash_tree_root(state), - body_root=state.latest_block_header.body_root, - ), - current_sync_committee=state.current_sync_committee, - current_sync_committee_branch=compute_merkle_proof_for_state(state, CURRENT_SYNC_COMMITTEE_INDEX) - ) -``` - -Full nodes SHOULD provide `LightClientBootstrap` for all finalized epoch boundary blocks in the epoch range `[max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]` where `current_epoch` is defined by the current wall-clock time. Full nodes MAY also provide `LightClientBootstrap` for other blocks. - -Blocks are considered to be epoch boundary blocks if their block root can occur as part of a valid `Checkpoint`, i.e., if their slot is the initial slot of an epoch, or if all following slots through the initial slot of the next epoch are empty (no block proposed / orphaned). - -`LightClientBootstrap` is computed from the block's immediate post state (without applying empty slots). - -### `create_light_client_update` - -To form a `LightClientUpdate`, the following historical states and blocks are needed: -- `state`: the post state of any block with a post-Altair parent block -- `block`: the corresponding block -- `attested_state`: the post state of the block referred to by `block.parent_root` -- `finalized_block`: the block referred to by `attested_state.finalized_checkpoint.root`, if locally available (may be unavailable, e.g., when using checkpoint sync, or if it was pruned locally) - -```python -def create_light_client_update(state: BeaconState, - block: SignedBeaconBlock, - attested_state: BeaconState, - finalized_block: Optional[SignedBeaconBlock]) -> LightClientUpdate: - assert compute_epoch_at_slot(attested_state.slot) >= ALTAIR_FORK_EPOCH - assert sum(block.message.body.sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS - - assert state.slot == state.latest_block_header.slot - header = state.latest_block_header.copy() - header.state_root = hash_tree_root(state) - assert hash_tree_root(header) == hash_tree_root(block.message) - update_signature_period = compute_sync_committee_period(compute_epoch_at_slot(block.message.slot)) - - assert attested_state.slot == attested_state.latest_block_header.slot - attested_header = attested_state.latest_block_header.copy() - attested_header.state_root = hash_tree_root(attested_state) - assert hash_tree_root(attested_header) == block.message.parent_root - update_attested_period = compute_sync_committee_period(compute_epoch_at_slot(attested_header.slot)) - - # `next_sync_committee` is only useful if the message is signed by the current sync committee - if update_attested_period == update_signature_period: - next_sync_committee = attested_state.next_sync_committee - next_sync_committee_branch = compute_merkle_proof_for_state(attested_state, NEXT_SYNC_COMMITTEE_INDEX) - else: - next_sync_committee = SyncCommittee() - next_sync_committee_branch = [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))] - - # Indicate finality whenever possible - if finalized_block is not None: - if finalized_block.message.slot != GENESIS_SLOT: - finalized_header = BeaconBlockHeader( - slot=finalized_block.message.slot, - proposer_index=finalized_block.message.proposer_index, - parent_root=finalized_block.message.parent_root, - state_root=finalized_block.message.state_root, - body_root=hash_tree_root(finalized_block.message.body), - ) - assert hash_tree_root(finalized_header) == attested_state.finalized_checkpoint.root - else: - assert attested_state.finalized_checkpoint.root == Bytes32() - finalized_header = BeaconBlockHeader() - finality_branch = compute_merkle_proof_for_state(attested_state, FINALIZED_ROOT_INDEX) - else: - finalized_header = BeaconBlockHeader() - finality_branch = [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))] - - return LightClientUpdate( - attested_header=attested_header, - next_sync_committee=next_sync_committee, - next_sync_committee_branch=next_sync_committee_branch, - finalized_header=finalized_header, - finality_branch=finality_branch, - sync_aggregate=block.message.body.sync_aggregate, - signature_slot=block.message.slot, - ) -``` - -Full nodes SHOULD provide the best derivable `LightClientUpdate` (according to `is_better_update`) for each sync committee period covering any epochs in range `[max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]` where `current_epoch` is defined by the current wall-clock time. Full nodes MAY also provide `LightClientUpdate` for other sync committee periods. - -- `LightClientUpdate` are assigned to sync committee periods based on their `attested_header.slot` -- `LightClientUpdate` are only considered if `compute_sync_committee_period(compute_epoch_at_slot(update.attested_header.slot)) == compute_sync_committee_period(compute_epoch_at_slot(update.signature_slot))` -- Only `LightClientUpdate` with `next_sync_committee` as selected by fork choice are provided, regardless of ranking by `is_better_update`. To uniquely identify a non-finalized sync committee fork, all of `period`, `current_sync_committee` and `next_sync_committee` need to be incorporated, as sync committees may reappear over time.