owned this note
owned this note
Published
Linked with GitHub
# Consensus Light Client Server Implementation Notes
> How we tweaked our Lodestar beacon node to serve light clients
## API endpoints
### `GET /eth/lightclient/init_proof/:epoch`
> Get an "init proof" required to bootstrap a light client from a trusted checkpoint
We assume that a light client may need to be bootstrapped from a trusted checkpoint, similar to beacon nodes during a checkpoint sync from a weak subjectivity checkpoint. The end result of the bootstrapping is that the light client has a trusted `LightClientSnapshot`, and can begin syncing.
The requested `epoch` is expected to be finalized (and is why the checkpoint root is not passed in the request).
This endpoint returns a beacon state multiproof containing `current_sync_committee`, `next_sync_committee`, `genesis_time`, and `genesis_validators_root`.
The multiproof should be created/verified against the `BeaconBlockHeader.state_root` at the trusted checkpoint root.
### `GET /eth/lightclient/best_updates?from=from_period&to=to_period`
Once the light client is bootstrapped with a `LightClientSnapshot`, its next task is to sync to the finalized state. This is accomplished by processing `LightClientUpdate`s, one after another, one per sync committee period. The consensus specs detail how to apply `LightClientUpdate`s to update a `LightClientSnapshot`.
This endpoint returns the 'best', ie the most voted and latest,
`LightClientUpdate` for every sync committee period between `from_period` and `to_period`.
With this response, a light client should be able to sync through sync committee `to_period`.
### `GET /eth/lightclient/latest_update_finalized`
Once a light client is synced to the most recent sync committee period, it may want to sync further to the latest finalized state. This can be triggered every epoch to remain synced to the finalized state.
This endpoint returns the latest `LightClientUpdate` that contains a finality proof.
### `GET /eth/lightclient/latest_update_unfinalized`
A light client may be able to navigate the unfinalized chain, and want to sync to the head (head - 1) of the chain.
This endpoint returns the latest `LightClientUpdate` that doesn't contain a finality proof.
## Producing init proofs
We want to provide init proofs for every recent finalized checkpoint.
We store the following information to disk:
- sync committee, indexed by period - To avoid excessive disk usage, the sync committees are stored separately so they may be deduplicated
- init proof w/o sync committees, indexed by epoch
We add a script that runs on each new finalized checkpoint.
The steps look as follows:
```
on_finalized(checkpoint):
# return early for any checkpoint that occurs before altair
# fetch the block header at checkpoint.root
# fetch the state at the block header state_root
# this state will be used to create the init proof
# create a multiproof with:
# - current_sync_committee
# - next_sync_committee
# - genesis_time
# - genesis_validators_root
# splice the sync committee sections from the rest of the multiproof
# store the multiproof, key by epoch
# store the sync committees, key by period
```
On init proof request (requested by epoch), the multiproof and relevant committees are stitched back together.
## Producing `LightClientUpdate`s
We want to provide a good update per sync committee period.
We also want to provide recent updates (most recent finalized and unfinalized).
A `LightClientUpdate` can be created / consumed in two different modes: with and without a finality header / branch.
### With a finality header / branch
When a `LightClientUpdate` contains a finality header / branch, the interpretation of the update is as follows:
- The `header` is the header corresponding to the finalized state
- The `next_sync_committee` is the committee from the finalized state
- The `next_sync_committee_branch` is a single proof from finalized state to its `next_sync_committee`
- The `finality_header` corresponds to a more recent state that contains the finalized state in its `finalized_checkpoint`.
- The `finality_branch` is a single proof from the more recent state to its its `finalized_checkpoint`.
- The `sync_committee_bits` and `sync_committee_signature` attest to the `finality_header`
### Without a finality header / branch
When a `LightClientUpdate` contains a zeroed finality header / branch, the interpretation of the update is as follows:
- The `header` corresponds to a recent state
- The `next_sync_committee` is the committee from the recent state
- The `next_sync_committee_branch` is a single proof from recent state to its `next_sync_committee`
- The `sync_committee_bits` and `sync_committee_signature` attest to the `header`
We store the following information to disk:
- sync committee, indexed by period - used to populate the `next_sync_committee`
- `header` and `next_sync_committee_branch`, indexed by epoch - used to populate the update in the finalized case
- `LightClientUpdate`, indexed by period - best update per committee period
- a single latest finalized `LightClientUpdate`
- a single latest unfinalized `LightClientUpdate`
On every finalized checkpoint, we store the `header` and `next_sync_committee_branch` of that finalized checkpoint.
On every new head, we run the following steps:
```
on_head(block, post_state):
# return early for any block that occurs before altair
# store (in cache), indexed by block root:
# - the block header, finalized checkpoint, finality branch and next committee branch
# these items are not used until the next block is processed
# get the prior block root
# this is the block that the current block's sync aggregate attests to
# get (from cache):
# - last block's block header, finalized checkpoint, finality branch and next committee branch
# these items are used for further steps
# create/store a finalized update:
# fetch the finalized header and next committee branch from disk
# assemble the update
# if the update is better than the pre-existing best
# overwrite the update to disk - the best update per committee period
# if the update is later than the pre-existing latest finalized update
# overwrite the update to disk - the latest finalized update
# create/store an unfinalized update:
# assemble the unfinalized update
# if the update is later than the pre-existing latest unfinalized update
# overwrite the update to disk - the latest unfinalized update
```