Skip to content

Latest commit

 

History

History
165 lines (131 loc) · 8.7 KB

full-node.md

File metadata and controls

165 lines (131 loc) · 8.7 KB

Altair Light Client -- Full Node

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

Table of contents

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

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

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)
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.

create_light_client_finality_update

def create_light_client_finality_update(update: LightClientUpdate) -> LightClientFinalityUpdate:
    return LightClientFinalityUpdate(
        attested_header=update.attested_header,
        finalized_header=update.finalized_header,
        finality_branch=update.finality_branch,
        sync_aggregate=update.sync_aggregate,
        signature_slot=update.signature_slot,
    )

Full nodes SHOULD provide the LightClientFinalityUpdate with the highest attested_header.slot (if multiple, highest signature_slot) as selected by fork choice, and SHOULD support a push mechanism to deliver new LightClientFinalityUpdate whenever finalized_header changes.

create_light_client_optimistic_update

def create_light_client_optimistic_update(update: LightClientUpdate) -> LightClientOptimisticUpdate:
    return LightClientOptimisticUpdate(
        attested_header=update.attested_header,
        sync_aggregate=update.sync_aggregate,
        signature_slot=update.signature_slot,
    )

Full nodes SHOULD provide the LightClientOptimisticUpdate with the highest attested_header.slot (if multiple, highest signature_slot) as selected by fork choice, and SHOULD support a push mechanism to deliver new LightClientOptimisticUpdate whenever attested_header changes.