@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
196 lines • 8.31 kB
TypeScript
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