497 lines
21 KiB
Markdown
497 lines
21 KiB
Markdown
# Altair Light Client -- Sync Protocol
|
|
|
|
**Notice**: This document is a work-in-progress for researchers and implementers.
|
|
|
|
## Table of contents
|
|
|
|
<!-- TOC -->
|
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
|
|
- [Introduction](#introduction)
|
|
- [Constants](#constants)
|
|
- [Preset](#preset)
|
|
- [Misc](#misc)
|
|
- [Containers](#containers)
|
|
- [`LightClientBootstrap`](#lightclientbootstrap)
|
|
- [`LightClientUpdate`](#lightclientupdate)
|
|
- [`LightClientFinalityUpdate`](#lightclientfinalityupdate)
|
|
- [`LightClientOptimisticUpdate`](#lightclientoptimisticupdate)
|
|
- [`LightClientStore`](#lightclientstore)
|
|
- [Helper functions](#helper-functions)
|
|
- [`is_sync_committee_update`](#is_sync_committee_update)
|
|
- [`is_finality_update`](#is_finality_update)
|
|
- [`is_better_update`](#is_better_update)
|
|
- [`is_next_sync_committee_known`](#is_next_sync_committee_known)
|
|
- [`get_safety_threshold`](#get_safety_threshold)
|
|
- [`get_subtree_index`](#get_subtree_index)
|
|
- [`compute_sync_committee_period_at_slot`](#compute_sync_committee_period_at_slot)
|
|
- [Light client initialization](#light-client-initialization)
|
|
- [`initialize_light_client_store`](#initialize_light_client_store)
|
|
- [Light client state updates](#light-client-state-updates)
|
|
- [`validate_light_client_update`](#validate_light_client_update)
|
|
- [`apply_light_client_update`](#apply_light_client_update)
|
|
- [`process_light_client_store_force_update`](#process_light_client_store_force_update)
|
|
- [`process_light_client_update`](#process_light_client_update)
|
|
- [`process_light_client_finality_update`](#process_light_client_finality_update)
|
|
- [`process_light_client_optimistic_update`](#process_light_client_optimistic_update)
|
|
|
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
<!-- /TOC -->
|
|
|
|
## Introduction
|
|
|
|
The beacon chain is designed to be light client friendly for constrained environments to
|
|
access Ethereum with reasonable safety and liveness.
|
|
Such environments include resource-constrained devices (e.g. phones for trust-minimized wallets)
|
|
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)
|
|
- [Light client](./light-client.md)
|
|
- [Networking](./p2p-interface.md)
|
|
|
|
## Constants
|
|
|
|
| Name | Value |
|
|
| - | - |
|
|
| `FINALIZED_ROOT_INDEX` | `get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')` (= 105) |
|
|
| `CURRENT_SYNC_COMMITTEE_INDEX` | `get_generalized_index(BeaconState, 'current_sync_committee')` (= 54) |
|
|
| `NEXT_SYNC_COMMITTEE_INDEX` | `get_generalized_index(BeaconState, 'next_sync_committee')` (= 55) |
|
|
|
|
## Preset
|
|
|
|
### Misc
|
|
|
|
| Name | Value | Unit | Duration |
|
|
| - | - | - | - |
|
|
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | validators | |
|
|
| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | slots | ~27.3 hours |
|
|
|
|
## Containers
|
|
|
|
### `LightClientBootstrap`
|
|
|
|
```python
|
|
class LightClientBootstrap(Container):
|
|
# The requested beacon block header
|
|
header: BeaconBlockHeader
|
|
# Current sync committee corresponding to `header`
|
|
current_sync_committee: SyncCommittee
|
|
current_sync_committee_branch: Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_INDEX)]
|
|
```
|
|
|
|
### `LightClientUpdate`
|
|
|
|
```python
|
|
class LightClientUpdate(Container):
|
|
# The beacon block header that is attested to by the sync committee
|
|
attested_header: BeaconBlockHeader
|
|
# Next sync committee corresponding to `attested_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
|
|
finalized_header: BeaconBlockHeader
|
|
finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)]
|
|
# Sync committee aggregate signature
|
|
sync_aggregate: SyncAggregate
|
|
# Slot at which the aggregate signature was created (untrusted)
|
|
signature_slot: Slot
|
|
```
|
|
|
|
### `LightClientFinalityUpdate`
|
|
|
|
```python
|
|
class LightClientFinalityUpdate(Container):
|
|
# The beacon block header that is attested to by the sync committee
|
|
attested_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_aggregate: SyncAggregate
|
|
# Slot at which the aggregate signature was created (untrusted)
|
|
signature_slot: Slot
|
|
```
|
|
|
|
### `LightClientOptimisticUpdate`
|
|
|
|
```python
|
|
class LightClientOptimisticUpdate(Container):
|
|
# The beacon block header that is attested to by the sync committee
|
|
attested_header: BeaconBlockHeader
|
|
# Sync committee aggregate signature
|
|
sync_aggregate: SyncAggregate
|
|
# Slot at which the aggregate signature was created (untrusted)
|
|
signature_slot: Slot
|
|
```
|
|
|
|
### `LightClientStore`
|
|
|
|
```python
|
|
@dataclass
|
|
class LightClientStore(object):
|
|
# 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 active participants in a sync committee (used to calculate safety threshold)
|
|
previous_max_active_participants: uint64
|
|
current_max_active_participants: uint64
|
|
```
|
|
|
|
## Helper functions
|
|
|
|
### `is_sync_committee_update`
|
|
|
|
```python
|
|
def is_sync_committee_update(update: LightClientUpdate) -> bool:
|
|
return update.next_sync_committee_branch != [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]
|
|
```
|
|
|
|
### `is_finality_update`
|
|
|
|
```python
|
|
def is_finality_update(update: LightClientUpdate) -> bool:
|
|
return update.finality_branch != [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]
|
|
```
|
|
|
|
### `is_better_update`
|
|
|
|
```python
|
|
def is_better_update(new_update: LightClientUpdate, old_update: LightClientUpdate) -> bool:
|
|
# Compare supermajority (> 2/3) sync committee participation
|
|
max_active_participants = len(new_update.sync_aggregate.sync_committee_bits)
|
|
new_num_active_participants = sum(new_update.sync_aggregate.sync_committee_bits)
|
|
old_num_active_participants = sum(old_update.sync_aggregate.sync_committee_bits)
|
|
new_has_supermajority = new_num_active_participants * 3 >= max_active_participants * 2
|
|
old_has_supermajority = old_num_active_participants * 3 >= max_active_participants * 2
|
|
if new_has_supermajority != old_has_supermajority:
|
|
return new_has_supermajority > old_has_supermajority
|
|
if not new_has_supermajority and new_num_active_participants != old_num_active_participants:
|
|
return new_num_active_participants > old_num_active_participants
|
|
|
|
# Compare presence of relevant sync committee
|
|
new_has_relevant_sync_committee = is_sync_committee_update(new_update) and (
|
|
compute_sync_committee_period_at_slot(new_update.attested_header.slot)
|
|
== compute_sync_committee_period_at_slot(new_update.signature_slot)
|
|
)
|
|
old_has_relevant_sync_committee = is_sync_committee_update(old_update) and (
|
|
compute_sync_committee_period_at_slot(old_update.attested_header.slot)
|
|
== compute_sync_committee_period_at_slot(old_update.signature_slot)
|
|
)
|
|
if new_has_relevant_sync_committee != old_has_relevant_sync_committee:
|
|
return new_has_relevant_sync_committee
|
|
|
|
# Compare indication of any finality
|
|
new_has_finality = is_finality_update(new_update)
|
|
old_has_finality = is_finality_update(old_update)
|
|
if new_has_finality != old_has_finality:
|
|
return new_has_finality
|
|
|
|
# Compare sync committee finality
|
|
if new_has_finality:
|
|
new_has_sync_committee_finality = (
|
|
compute_sync_committee_period_at_slot(new_update.finalized_header.slot)
|
|
== compute_sync_committee_period_at_slot(new_update.attested_header.slot)
|
|
)
|
|
old_has_sync_committee_finality = (
|
|
compute_sync_committee_period_at_slot(old_update.finalized_header.slot)
|
|
== compute_sync_committee_period_at_slot(old_update.attested_header.slot)
|
|
)
|
|
if new_has_sync_committee_finality != old_has_sync_committee_finality:
|
|
return new_has_sync_committee_finality
|
|
|
|
# Tiebreaker 1: Sync committee participation beyond supermajority
|
|
if new_num_active_participants != old_num_active_participants:
|
|
return new_num_active_participants > old_num_active_participants
|
|
|
|
# Tiebreaker 2: Prefer older data (fewer changes to best)
|
|
if new_update.attested_header.slot != old_update.attested_header.slot:
|
|
return new_update.attested_header.slot < old_update.attested_header.slot
|
|
return new_update.signature_slot < old_update.signature_slot
|
|
```
|
|
|
|
### `is_next_sync_committee_known`
|
|
|
|
```python
|
|
def is_next_sync_committee_known(store: LightClientStore) -> bool:
|
|
return store.next_sync_committee != SyncCommittee()
|
|
```
|
|
|
|
### `get_safety_threshold`
|
|
|
|
```python
|
|
def get_safety_threshold(store: LightClientStore) -> uint64:
|
|
return max(
|
|
store.previous_max_active_participants,
|
|
store.current_max_active_participants,
|
|
) // 2
|
|
```
|
|
|
|
### `get_subtree_index`
|
|
|
|
```python
|
|
def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
|
|
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
|
|
```
|
|
|
|
### `compute_sync_committee_period_at_slot`
|
|
|
|
```python
|
|
def compute_sync_committee_period_at_slot(slot: Slot) -> uint64:
|
|
return compute_sync_committee_period(compute_epoch_at_slot(slot))
|
|
```
|
|
|
|
## 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`.
|
|
|
|
### `initialize_light_client_store`
|
|
|
|
```python
|
|
def initialize_light_client_store(trusted_block_root: Root,
|
|
bootstrap: LightClientBootstrap) -> LightClientStore:
|
|
assert hash_tree_root(bootstrap.header) == trusted_block_root
|
|
|
|
assert is_valid_merkle_branch(
|
|
leaf=hash_tree_root(bootstrap.current_sync_committee),
|
|
branch=bootstrap.current_sync_committee_branch,
|
|
depth=floorlog2(CURRENT_SYNC_COMMITTEE_INDEX),
|
|
index=get_subtree_index(CURRENT_SYNC_COMMITTEE_INDEX),
|
|
root=bootstrap.header.state_root,
|
|
)
|
|
|
|
return LightClientStore(
|
|
finalized_header=bootstrap.header,
|
|
current_sync_committee=bootstrap.current_sync_committee,
|
|
next_sync_committee=SyncCommittee(),
|
|
best_valid_update=None,
|
|
optimistic_header=bootstrap.header,
|
|
previous_max_active_participants=0,
|
|
current_max_active_participants=0,
|
|
)
|
|
```
|
|
|
|
## Light client state updates
|
|
|
|
- A light client receives objects of type `LightClientUpdate`, `LightClientFinalityUpdate` and `LightClientOptimisticUpdate`:
|
|
- **`update: LightClientUpdate`**: Every `update` triggers `process_light_client_update(store, update, current_slot, genesis_validators_root)` where `current_slot` is the current slot based on a local clock.
|
|
- **`finality_update: LightClientFinalityUpdate`**: Every `finality_update` triggers `process_light_client_finality_update(store, finality_update, current_slot, genesis_validators_root)`.
|
|
- **`optimistic_update: LightClientOptimisticUpdate`**: Every `optimistic_update` triggers `process_light_client_optimistic_update(store, optimistic_update, current_slot, genesis_validators_root)`.
|
|
- `process_light_client_store_force_update` MAY be called based on use case dependent heuristics if light client sync appears stuck.
|
|
|
|
### `validate_light_client_update`
|
|
|
|
```python
|
|
def validate_light_client_update(store: LightClientStore,
|
|
update: LightClientUpdate,
|
|
current_slot: Slot,
|
|
genesis_validators_root: Root) -> None:
|
|
# Verify sync committee has sufficient participants
|
|
sync_aggregate = update.sync_aggregate
|
|
assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS
|
|
|
|
# Verify update does not skip a sync committee period
|
|
assert current_slot >= update.signature_slot > update.attested_header.slot >= update.finalized_header.slot
|
|
store_period = compute_sync_committee_period_at_slot(store.finalized_header.slot)
|
|
update_signature_period = compute_sync_committee_period_at_slot(update.signature_slot)
|
|
if is_next_sync_committee_known(store):
|
|
assert update_signature_period in (store_period, store_period + 1)
|
|
else:
|
|
assert update_signature_period == store_period
|
|
|
|
# Verify update is relevant
|
|
update_attested_period = compute_sync_committee_period_at_slot(update.attested_header.slot)
|
|
update_has_next_sync_committee = not is_next_sync_committee_known(store) and (
|
|
is_sync_committee_update(update) and update_attested_period == store_period
|
|
)
|
|
assert (
|
|
update.attested_header.slot > store.finalized_header.slot
|
|
or update_has_next_sync_committee
|
|
)
|
|
|
|
# Verify that the `finality_branch`, if present, confirms `finalized_header`
|
|
# to match the finalized checkpoint root saved in the state of `attested_header`.
|
|
# Note that the genesis finalized checkpoint root is represented as a zero hash.
|
|
if not is_finality_update(update):
|
|
assert update.finalized_header == BeaconBlockHeader()
|
|
else:
|
|
if update.finalized_header.slot == GENESIS_SLOT:
|
|
assert update.finalized_header == BeaconBlockHeader()
|
|
finalized_root = Bytes32()
|
|
else:
|
|
finalized_root = hash_tree_root(update.finalized_header)
|
|
assert is_valid_merkle_branch(
|
|
leaf=finalized_root,
|
|
branch=update.finality_branch,
|
|
depth=floorlog2(FINALIZED_ROOT_INDEX),
|
|
index=get_subtree_index(FINALIZED_ROOT_INDEX),
|
|
root=update.attested_header.state_root,
|
|
)
|
|
|
|
# Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the
|
|
# state of the `attested_header`
|
|
if not is_sync_committee_update(update):
|
|
assert update.next_sync_committee == SyncCommittee()
|
|
else:
|
|
if update_attested_period == store_period and is_next_sync_committee_known(store):
|
|
assert update.next_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.attested_header.state_root,
|
|
)
|
|
|
|
# Verify sync committee aggregate signature
|
|
if update_signature_period == store_period:
|
|
sync_committee = store.current_sync_committee
|
|
else:
|
|
sync_committee = store.next_sync_committee
|
|
participant_pubkeys = [
|
|
pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys)
|
|
if bit
|
|
]
|
|
fork_version = compute_fork_version(compute_epoch_at_slot(update.signature_slot))
|
|
domain = compute_domain(DOMAIN_SYNC_COMMITTEE, 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)
|
|
```
|
|
|
|
### `apply_light_client_update`
|
|
|
|
```python
|
|
def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None:
|
|
store_period = compute_sync_committee_period_at_slot(store.finalized_header.slot)
|
|
update_finalized_period = compute_sync_committee_period_at_slot(update.finalized_header.slot)
|
|
if not is_next_sync_committee_known(store):
|
|
assert update_finalized_period == store_period
|
|
store.next_sync_committee = update.next_sync_committee
|
|
elif update_finalized_period == store_period + 1:
|
|
store.current_sync_committee = store.next_sync_committee
|
|
store.next_sync_committee = update.next_sync_committee
|
|
store.previous_max_active_participants = store.current_max_active_participants
|
|
store.current_max_active_participants = 0
|
|
if update.finalized_header.slot > store.finalized_header.slot:
|
|
store.finalized_header = update.finalized_header
|
|
if store.finalized_header.slot > store.optimistic_header.slot:
|
|
store.optimistic_header = store.finalized_header
|
|
```
|
|
|
|
### `process_light_client_store_force_update`
|
|
|
|
```python
|
|
def process_light_client_store_force_update(store: LightClientStore, current_slot: Slot) -> None:
|
|
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.
|
|
# Because the apply logic waits for `finalized_header.slot` to indicate sync committee finality,
|
|
# the `attested_header` may be treated as `finalized_header` in extended periods of non-finality
|
|
# to guarantee progression into later sync committee periods according to `is_better_update`.
|
|
if store.best_valid_update.finalized_header.slot <= store.finalized_header.slot:
|
|
store.best_valid_update.finalized_header = store.best_valid_update.attested_header
|
|
apply_light_client_update(store, store.best_valid_update)
|
|
store.best_valid_update = None
|
|
```
|
|
|
|
### `process_light_client_update`
|
|
|
|
```python
|
|
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_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 is_better_update(update, store.best_valid_update)
|
|
):
|
|
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
|
|
):
|
|
store.optimistic_header = update.attested_header
|
|
|
|
# Update finalized header
|
|
update_has_finalized_next_sync_committee = (
|
|
not is_next_sync_committee_known(store)
|
|
and is_sync_committee_update(update) and is_finality_update(update) and (
|
|
compute_sync_committee_period_at_slot(update.finalized_header.slot)
|
|
== compute_sync_committee_period_at_slot(update.attested_header.slot)
|
|
)
|
|
)
|
|
if (
|
|
sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2
|
|
and (
|
|
update.finalized_header.slot > store.finalized_header.slot
|
|
or update_has_finalized_next_sync_committee
|
|
)
|
|
):
|
|
# Normal update through 2/3 threshold
|
|
apply_light_client_update(store, update)
|
|
store.best_valid_update = None
|
|
```
|
|
|
|
### `process_light_client_finality_update`
|
|
|
|
```python
|
|
def process_light_client_finality_update(store: LightClientStore,
|
|
finality_update: LightClientFinalityUpdate,
|
|
current_slot: Slot,
|
|
genesis_validators_root: Root) -> None:
|
|
update = LightClientUpdate(
|
|
attested_header=finality_update.attested_header,
|
|
next_sync_committee=SyncCommittee(),
|
|
next_sync_committee_branch=[Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))],
|
|
finalized_header=finality_update.finalized_header,
|
|
finality_branch=finality_update.finality_branch,
|
|
sync_aggregate=finality_update.sync_aggregate,
|
|
signature_slot=finality_update.signature_slot,
|
|
)
|
|
process_light_client_update(store, update, current_slot, genesis_validators_root)
|
|
```
|
|
|
|
### `process_light_client_optimistic_update`
|
|
|
|
```python
|
|
def process_light_client_optimistic_update(store: LightClientStore,
|
|
optimistic_update: LightClientOptimisticUpdate,
|
|
current_slot: Slot,
|
|
genesis_validators_root: Root) -> None:
|
|
update = LightClientUpdate(
|
|
attested_header=optimistic_update.attested_header,
|
|
next_sync_committee=SyncCommittee(),
|
|
next_sync_committee_branch=[Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))],
|
|
finalized_header=BeaconBlockHeader(),
|
|
finality_branch=[Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))],
|
|
sync_aggregate=optimistic_update.sync_aggregate,
|
|
signature_slot=optimistic_update.signature_slot,
|
|
)
|
|
process_light_client_update(store, update, current_slot, genesis_validators_root)
|
|
```
|