5.8 KiB
Minimal Light Client Design
Notice: This document is a work-in-progress for researchers and implementers.
Table of contents
Introduction
Ethereum 2.0 is designed to be light client friendly. This allows low-resource clients such as mobile phones to access Ethereum 2.0 with reasonable safety and liveness. It also facilitates the development of "bridges" to external blockchains. This document suggests a minimal light client design for the beacon chain that uses the concept of "sync committees" introduced in [./beacon-chain.md](the the light-client-friendliness beacon chain extension).
Constants
Name | Value |
---|---|
SYNC_COMMITTEES_GENERALIZED_INDEX |
GeneralizedIndexConcat(GeneralizedIndex(BeaconBlock, 'state_root'), GeneralizedIndex(BeaconState, 'current_sync_committee')) |
FORK_GENERALIZED_INDEX |
GeneralizedIndexConcat(GeneralizedIndex(BeaconBlock, 'state_root'), GeneralizedIndex(BeaconState, 'fork')) |
Containers
LightClientUpdate
class LightClientUpdate(Container):
# Updated beacon header (and authenticating branch)
header: BeaconBlockHeader
# Sync committee signature to that header
aggregation_bits: Bitlist[MAX_SYNC_COMMITTEE_SIZE]
signature: BLSSignature
header_branch: Vector[Bytes32, BEACON_CHAIN_ROOT_IN_SHARD_BLOCK_HEADER_DEPTH]
# Updates fork version
new_fork: Fork
fork_branch: Vector[Bytes32, log_2(FORK_GENERALIZED_INDEX)]
# Updated period committee (and authenticating branch)
new_current_sync_committee: SyncCommittee
new_next_sync_committee: SyncCommittee
sync_committee_branch: Vector[Bytes32, log_2(SYNC_COMMITTEES_GENERALIZED_INDEX)]
Helpers
LightClientMemory
class LightClientMemory(Container):
# Beacon header which is not expected to revert
header: BeaconBlockHeader
# Fork version data
fork_version: Version
# period committees corresponding to the beacon header
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee
Light client state updates
The state of a light client is stored in a memory
object of type LightClientMemory
. To advance its state a light client requests an update
object of type LightClientUpdate
from the network by sending a request containing (memory.shard, memory.header.slot, slot_range_end)
. It calls validate_update(memory, update)
on each update that it receives in response. If sum(update.aggregate_bits) * 3 > len(update.aggregate_bits) * 2
for any valid update, it accepts that update immediately; otherwise, it waits around for some time and then finally calls update_memory(memory, update)
on the valid update with the highest sum(update.aggregate_bits)
.
validate_update
def validate_update(memory: LightClientMemory, update: LightClientUpdate) -> bool:
# Verify the update does not skip a period
current_period = compute_epoch_at_slot(memory.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
new_epoch = compute_epoch_of_shard_slot(update.header.slot)
new_period = new_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
assert new_period in (current_period, current_period + 1)
# Verify that it actually updates to a newer slot
assert update.header.slot > memory.header.slot
# Convenience as independent variable for convenience
committee = memory.current_sync_committee if new_period == current_period else memory.next_sync_committee
assert len(update.aggregation_bits) == len(committee)
# Verify signature
active_pubkeys = [p for (bit, p) in zip(update.aggregation_bits, committee.pubkeys) if bit]
domain = compute_domain(DOMAIN_SYNC_COMMITTEE, memory.fork_version)
signing_root = compute_signing_root(update.header, domain)
assert bls.FastAggregateVerify(pubkeys, signing_root, update.signature)
# Verify Merkle branches of new info
assert is_valid_merkle_branch(
leaf=hash_tree_root(update.new_fork),
branch=update.fork_branch,
depth=log2(FORK_GENERALIZED_INDEX),
index=FORK_GENERALIZED_INDEX % 2**log2(FORK_GENERALIZED_INDEX),
root=hash_tree_root(update.header),
)
assert is_valid_merkle_branch(
leaf=hash_tree_root(update.current_sync_committee),
branch=update.sync_committee_branch,
depth=log2(SYNC_COMMITTEES_GENERALIZED_INDEX),
index=SYNC_COMMITTEES_GENERALIZED_INDEX % 2**log2(SYNC_COMMITTEES_GENERALIZED_INDEX),
root=hash_tree_root(update.header),
)
# Verify consistency of committees
if new_period == current_period:
assert update.new_current_sync_committee == memory.current_sync_committee
assert update.new_next_sync_committee == memory.next_sync_committee
else:
assert update.new_current_sync_committee == memory.next_sync_committee
return True
update_memory
def update_memory(memory: LightClientMemory, update: LightClientUpdate) -> None:
memory.header = update.header
epoch = compute_epoch_at_slot(update.header.slot)
memory.fork_version = update.new_fork.previous_version if epoch < update.new_fork.epoch else update.new_fork.current_version
memory.current_sync_committee = update.new_current_sync_committee
memory.next_sync_committee == update.new_next_sync_committee