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
9.3 KiB
Altair -- Minimal Light Client
Notice: This document is a work-in-progress for researchers and implementers.
Table of contents
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-minimised 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.
Constants
Name | Value |
---|---|
FINALIZED_ROOT_INDEX |
get_generalized_index(BeaconState, 'finalized_checkpoint', 'root') |
NEXT_SYNC_COMMITTEE_INDEX |
get_generalized_index(BeaconState, 'next_sync_committee') |
Preset
Misc
Name | Value | Notes |
---|---|---|
MIN_SYNC_COMMITTEE_PARTICIPANTS |
1 |
|
SAFETY_THRESHOLD_CALCULATION_PERIOD |
4096 |
~13.6 hours |
Containers
LightClientSnapshot
class LightClientSnapshot(Container):
# Beacon block header
header: BeaconBlockHeader
# Sync committees corresponding to the header
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee
LightClientUpdate
class LightClientUpdate(Container):
# Update beacon block header
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
finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)]
# Sync committee aggregate signature
sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE]
sync_committee_signature: BLSSignature
# Fork version for the aggregate signature
fork_version: Version
LightClientStore
class LightClientStore(object):
snapshot: LightClientSnapshot
best_valid_update: Optional[LightClientUpdate]
optimistic_header: BeaconBlockHeader
previous_period_max_attendance: uint64
current_period_max_attendance: uint64
Helper functions
get_subtree_index
def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
get_signed_header
def get_signed_header(update: LightClientUpdate):
if update.finality_header is None:
return update.header
else:
return update.finality_header
get_safety_threshold
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. process_slot
is processed every time the current slot increments.
process_slot
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
def validate_light_client_update(snapshot: LightClientSnapshot,
update: LightClientUpdate,
genesis_validators_root: Root) -> None:
# Verify update slot is larger than snapshot slot
assert update.header.slot > snapshot.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)
# 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,
depth=floorlog2(FINALIZED_ROOT_INDEX),
index=get_subtree_index(FINALIZED_ROOT_INDEX),
root=update.finality_header.state_root,
)
# Verify update next sync committee if the update period incremented
if update_period == snapshot_period:
sync_committee = snapshot.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
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,
)
# Verify sync committee has sufficient participants
assert sum(update.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]
domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root)
signing_root = compute_signing_root(signed_header, domain)
assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature)
apply_light_client_update
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
process_light_client_update
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)
# 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()
):
# Normal update through 2/3 threshold
apply_light_client_update(store.snapshot, update)
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, store.best_valid_update)
store.best_valid_update = None