eth2.0-specs/specs/lightclient/sync-protocol.md

6.8 KiB

Minimal Light Client

Notice: This document is a work-in-progress for researchers and implementers.

Table of contents

Introduction

Eth2 is designed to be light client friendly for constrained environments to access Eth2 with reasonable satefy 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
NEXT_SYNC_COMMITTEE_INDEX Index(BeaconState, 'next_sync_committee')
FINALIZED_ROOT_INDEX Index(BeaconState, 'finalized_checkpoint', 'root')

Configuration

Misc

Name Value
MIN_SYNC_COMMITTEE_PARTICIPANTS 1
MAX_VALID_LIGHT_CLIENT_UPDATES uint64(2**64 - 1)

Time parameters

Name Value Unit Duration
LIGHT_CLIENT_UPDATE_TIMEOUT Slot(2**13) slots ~27 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):
    # Updated snapshot
    snapshot: LightClientSnapshot
    # Header that the new snapshot is a finalized ancestor of
    signed_header: BeaconBlockHeader
    # Merkle branch proving ancestry of the header in the snapshot
    ancestry_branch: Vector[Bytes32, log2(FINALIZED_ROOT_INDEX)]
    # Merkle branches for the next sync committee
    next_sync_committee_branch: Vector[Bytes32, log2(NEXT_SYNC_COMMITTEE_INDEX)]
    # Sync committee aggregate signature
    sync_committee_bits: Bitlist[MAX_SYNC_COMMITTEE_SIZE]
    sync_committee_signature: BLSSignature
    # Fork version for the aggregate signature
    fork_version: Version

LightClientStore

class LightClientStore(Container):
    snapshot: LightClientSnapshot
    valid_updates: List[LightClientUpdate, MAX_VALID_LIGHT_CLIENT_UPDATES]

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 currect slot based on some local clock.

is_valid_light_client_update

def is_valid_light_client_update(store: LightClientStore, update: LightClientUpdate) -> bool:
    # Verify new slot is larger than old slot
    old_snapshot = store.snapshot
    new_snapshot = update.snapshot
    assert new_snapshot.header.slot > old_snapshot.header.slot

    # Verify update does not skip a sync committee period
    old_period = compute_epoch_at_slot(old_snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
    new_period = compute_epoch_at_slot(new_snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
    assert new_period in (old_period, old_period + 1)
    
    # Verify relationship between signed header and ancestor header
    if update.signed_header == new_snapshot.header:
        assert update.ancestry_branch == [ZERO_HASH for _ in range(log2(FINALIZED_ROOT_INDEX))]
    else:
        assert is_valid_merkle_branch(
            leaf=hash_tree_root(new_snapshot.header),
            branch=update.ancestry_branch,
            depth=log2(FINALIZED_ROOT_INDEX),
            index=FINALIZED_ROOT_INDEX % 2**log2(FINALIZED_ROOT_INDEX),
            root=update.signed_header.state_root,
        )        

    # Verify new snapshot sync committees
    if new_period == old_period:
        assert new_snapshot.current_sync_committee == old_snapshot.current_sync_committee
        assert new_snapshot.next_sync_committee == old_snapshot.next_sync_committee
    else new_period == old_period + 1:
        assert new_snapshot.current_sync_committee == old_snapshot.next_sync_committee
        assert is_valid_merkle_branch(
            leaf=hash_tree_root(new_snapshot.next_sync_committee),
            branch=update.next_sync_committee_branch,
            depth=log2(NEXT_SYNC_COMMITTEE_INDEX),
            index=NEXT_SYNC_COMMITTEE_INDEX % 2**log2(NEXT_SYNC_COMMITTEE_INDEX),
            root=new_snapshot.header.state_root,
        )

    # Verify sync committee bitfield length 
    sync_committee = new_snapshot.current_sync_committee
    assert len(update.sync_committee_bits) == len(sync_committee)
    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)
    signing_root = compute_signing_root(update.signed_header, domain)
    assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature)

    return True

process_update

def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot) -> None:
    # Validate update
    assert is_valid_light_client_update(store, update)
    valid_updates.append(update)

    # Immediate update "happy path" requires:
    # (i) 2/3 participation
    # (ii) an update that refers to the finalized ancestor of a signed block, and not the signed block directly
    
    if sum(update.sync_committee_bits) * 3 > len(update.sync_committee_bits) * 2 and update.snapshot.header != update.signed_header:
        # Immediate update when quorum is reached
        store.snapshot = update.snapshot
        valid_updates = []
    elif current_slot > old_snapshot.header.slot + LIGHT_CLIENT_UPDATE_TIMEOUT:
        # Forced best update when the update timeout has elapsed
        store.snapshot = max(valid_updates, key=lambda update: sum(update.sync_committee_bits)).new_snapshot
        valid_updates = []