UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

196 lines • 8.31 kB
import { BitArray } from "@chainsafe/ssz"; import { ChainForkConfig } from "@lodestar/config"; import { ForkName, ForkPostAltair, ForkPreGloas } from "@lodestar/params"; import { type IBeaconStateViewAltair } from "@lodestar/state-transition"; import { BeaconBlock, LightClientBootstrap, LightClientFinalityUpdate, LightClientHeader, LightClientOptimisticUpdate, LightClientUpdate, Slot, phase0 } from "@lodestar/types"; import { Logger } from "@lodestar/utils"; import { IBeaconDb } from "../../db/index.js"; import { Metrics } from "../../metrics/index.js"; import { IClock } from "../../util/clock.js"; import { ChainEventEmitter } from "../emitter.js"; export type LightClientServerOpts = { disableLightClientServerOnImportBlockHead?: boolean; disableLightClientServer?: boolean; }; export type SyncAttestedData = { attestedHeader: LightClientHeader; /** Precomputed root to prevent re-hashing */ blockRoot: Uint8Array; } & ({ isFinalized: true; finalityBranch: Uint8Array[]; finalizedCheckpoint: phase0.Checkpoint; } | { isFinalized: false; }); type LightClientServerModules = { config: ChainForkConfig; clock: IClock; db: IBeaconDb; metrics: Metrics | null; emitter: ChainEventEmitter; logger: Logger; signal: AbortSignal; }; /** * Compute and cache "init" proofs as the chain advances. * Will compute proofs for: * - All finalized blocks * - All non-finalized checkpoint blocks * * Params: * - How many epochs ago do you consider a re-org can happen? 10 * - How many consecutive slots in a epoch you consider can be skipped? 32 * * ### What data to store? * * An altair beacon state has 24 fields, with a depth of 5. * | field | gindex | index | * | --------------------- | ------ | ----- | * | finalizedCheckpoint | 52 | 20 | * | currentSyncCommittee | 54 | 22 | * | nextSyncCommittee | 55 | 23 | * * Fields `currentSyncCommittee` and `nextSyncCommittee` are contiguous fields. Since they change its * more optimal to only store the witnesses different blocks of interest. * * ```ts * SyncCommitteeWitness = Container({ * witness: Vector[Bytes32, 4], * currentSyncCommitteeRoot: Bytes32, * nextSyncCommitteeRoot: Bytes32, * }) * ``` * * To produce finalized light-client updates, need the FinalizedCheckpointWitness + the finalized header the checkpoint * points too. It's cheaper to send a full BeaconBlockHeader `3*32 + 2*8` than a proof to `state_root` `(3+1)*32`. * * ```ts * FinalizedCheckpointWitness = Container({ * witness: Vector[Bytes32, 5], * root: Bytes32, * epoch: Epoch, * }) * ``` * * ### When to store data? * * Lightclient servers don't really need to support serving data for light-client at all possible roots to have a * functional use-case. * - For init proofs light-clients will probably use a finalized weak-subjectivity checkpoint * - For sync updates, light-clients need any update within a given period * * Fully tree-backed states are not guaranteed to be available at any time but just after processing a block. Then, * the server must pre-compute all data for all blocks until there's certainity of what block becomes a checkpoint * and which blocks doesn't. * * - SyncAggregate -> ParentBlock -> FinalizedCheckpoint -> nextSyncCommittee * * After importing a new block + postState: * - Persist SyncCommitteeWitness, indexed by block root of state's witness, always * - Persist currentSyncCommittee, indexed by hashTreeRoot, once (not necessary after the first run) * - Persist nextSyncCommittee, indexed by hashTreeRoot, for each period + dependentRoot * - Persist FinalizedCheckpointWitness only if checkpoint period = syncAggregate period * * TODO: Prune strategy: * - [Low value] On finalized or in finalized lookup, prune SyncCommittee that's not finalized * - [High value] After some time prune un-used FinalizedCheckpointWitness + finalized headers * - [High value] After some time prune to-be-checkpoint items that will never become checkpoints * - After sync period is over all pending headers are useless * * !!! BEST = finalized + highest bit count + oldest (less chance of re-org, less writes) * * Then when light-client requests the best finalized update at period N: * - Fetch best finalized SyncAggregateHeader in period N * - Fetch FinalizedCheckpointWitness at that header's block root * - Fetch SyncCommitteeWitness at that FinalizedCheckpointWitness.header.root * - Fetch SyncCommittee at that SyncCommitteeWitness.nextSyncCommitteeRoot * * When light-client request best non-finalized update at period N: * - Fetch best non-finalized SyncAggregateHeader in period N * - Fetch SyncCommitteeWitness at that SyncAggregateHeader.header.root * - Fetch SyncCommittee at that SyncCommitteeWitness.nextSyncCommitteeRoot * * ``` * Finalized Block Sync * Checkpoint Header Aggreate * ----------------------|-----------------------|-------|---------> time * <--------------------- <---- * finalizes signs * ``` * * ### What's the cost of this data? * * To estimate the data costs, let's analyze monthly. Yearly may not make sense due to weak subjectivity: * - 219145 slots / month * - 6848 epochs / month * - 27 sync periods / month * * The byte size of a SyncCommittee (mainnet preset) is fixed to `48 * (512 + 1) = 24624`. So with SyncCommittee only * the data cost to store them is `24624 * 27 = 664848` ~ 0.6 MB/m. * * Storing 4 witness per block costs `219145 * 4 * 32 = 28050560 ~ 28 MB/m`. * Storing 4 witness per epoch costs `6848 * 4 * 32 = 876544 ~ 0.9 MB/m`. */ export declare class LightClientServer { private readonly opts; private readonly db; private readonly config; private readonly metrics; private readonly emitter; private readonly logger; private readonly clock; private readonly signal; private readonly knownSyncCommittee; private storedCurrentSyncCommittee; /** * Keep in memory since this data is very transient, not useful after a few slots */ private readonly prevHeadData; private checkpointHeaders; private latestHeadUpdate; private readonly zero; private finalized; constructor(opts: LightClientServerOpts, modules: LightClientServerModules); /** * Call after importing a block head, having the postState available in memory for proof generation. * - Persist state witness * - Use block's syncAggregate */ onImportBlockHead(block: BeaconBlock<ForkPostAltair>, postState: IBeaconStateViewAltair, parentBlockSlot: Slot): void; /** * API ROUTE to get `currentSyncCommittee` and `nextSyncCommittee` from a trusted state root */ getBootstrap(blockRoot: Uint8Array): Promise<LightClientBootstrap>; /** * API ROUTE to get the best available update for `period` to transition to the next sync committee. * Criteria for best in priority order: * - Is finalized * - Has the most bits * - Signed header at the oldest slot */ getUpdate(period: number): Promise<LightClientUpdate>; /** * API ROUTE to get the sync committee hash from the best available update for `period`. */ getCommitteeRoot(period: number): Promise<Uint8Array>; /** * API ROUTE to poll LightclientHeaderUpdate. * Clients should use the SSE type `light_client_optimistic_update` if available */ getOptimisticUpdate(): LightClientOptimisticUpdate | null; getFinalityUpdate(): LightClientFinalityUpdate | null; /** * With forkchoice data compute which block roots will never become checkpoints and prune them. */ pruneNonCheckpointData(nonCheckpointBlockRoots: Uint8Array[]): Promise<void>; private persistPostBlockImportData; private onSyncAggregate; private maybeStoreNewBestUpdate; private storeSyncCommittee; private getFinalizedHeader; } export declare function sumBits(bits: BitArray): number; export declare function blockToLightClientHeader(fork: ForkName, block: BeaconBlock<ForkPostAltair & ForkPreGloas>): LightClientHeader; export {}; //# sourceMappingURL=index.d.ts.map