UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

795 lines (723 loc) • 30.9 kB
import {BitArray} from "@chainsafe/ssz"; import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; import { LightClientUpdateSummary, isBetterUpdate, toLightClientUpdateSummary, upgradeLightClientHeader, } from "@lodestar/light-client/spec"; import { ForkName, ForkPostAltair, ForkPostBellatrix, ForkPreGloas, ForkSeq, MIN_SYNC_COMMITTEE_PARTICIPANTS, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SIZE, forkPostAltair, highestFork, isForkPostElectra, } from "@lodestar/params"; import { type IBeaconStateViewAltair, computeStartSlotAtEpoch, computeSyncPeriodAtEpoch, computeSyncPeriodAtSlot, executionPayloadToPayloadHeader, } from "@lodestar/state-transition"; import { BeaconBlock, BeaconBlockBody, LightClientBootstrap, LightClientFinalityUpdate, LightClientHeader, LightClientOptimisticUpdate, LightClientUpdate, Root, RootHex, SSZTypesFor, Slot, SyncPeriod, altair, electra, phase0, ssz, sszTypesFor, } from "@lodestar/types"; import {Logger, MapDef, byteArrayEquals, pruneSetToMax, toRootHex} from "@lodestar/utils"; import {ZERO_HASH} from "../../constants/index.js"; import {IBeaconDb} from "../../db/index.js"; import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js"; import {Metrics} from "../../metrics/index.js"; import {IClock} from "../../util/clock.js"; import {ChainEventEmitter} from "../emitter.js"; import {LightClientServerError, LightClientServerErrorCode} from "../errors/lightClientError.js"; import {getBlockBodyExecutionHeaderProof, getCurrentSyncCommitteeBranch, getNextSyncCommitteeBranch} from "./proofs.js"; export type LightClientServerOpts = { disableLightClientServerOnImportBlockHead?: boolean; disableLightClientServer?: boolean; }; type DependentRootHex = RootHex; type BlockRooHex = RootHex; 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; }; const MAX_CACHED_FINALIZED_HEADERS = 3; const MAX_PREV_HEAD_DATA = 32; /** * 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 class LightClientServer { private readonly db: IBeaconDb; private readonly config: ChainForkConfig; private readonly metrics: Metrics | null; private readonly emitter: ChainEventEmitter; private readonly logger: Logger; private readonly clock: IClock; private readonly signal: AbortSignal; private readonly knownSyncCommittee = new MapDef<SyncPeriod, Set<DependentRootHex>>(() => new Set()); private storedCurrentSyncCommittee = false; /** * Keep in memory since this data is very transient, not useful after a few slots */ private readonly prevHeadData = new Map<BlockRooHex, SyncAttestedData>(); private checkpointHeaders = new Map<BlockRooHex, LightClientHeader>(); private latestHeadUpdate: LightClientOptimisticUpdate | null = null; private readonly zero: Pick< altair.LightClientUpdate | electra.LightClientUpdate, "finalityBranch" | "finalizedHeader" >; private finalized: LightClientFinalityUpdate | null = null; constructor( private readonly opts: LightClientServerOpts, modules: LightClientServerModules ) { const {config, clock, db, metrics, emitter, logger, signal} = modules; this.config = config; this.clock = clock; this.db = db; this.metrics = metrics; this.emitter = emitter; this.logger = logger; this.signal = signal; this.zero = { // Assign the hightest fork's default value because it can always be typecasted down to correct fork finalizedHeader: sszTypesFor(highestFork(forkPostAltair)).LightClientHeader.defaultValue(), // Electra finalityBranch has fixed length of 5 whereas altair has 4. The fifth element will be ignored // when serializing as altair LightClientUpdate finalityBranch: ssz.electra.LightClientUpdate.fields.finalityBranch.defaultValue(), }; if (metrics) { metrics.lightclientServer.highestSlot.addCollect(() => { if (this.latestHeadUpdate) { metrics.lightclientServer.highestSlot.set( {item: "latest_head_update"}, this.latestHeadUpdate.attestedHeader.beacon.slot ); } if (this.finalized) { metrics.lightclientServer.highestSlot.set( {item: "latest_finalized_update"}, this.finalized.attestedHeader.beacon.slot ); } }); } } /** * 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 { // TEMP: To disable this functionality for fork_choice spec tests. // Since the tests have deep-reorgs attested data is not available often printing lots of error logs. // While this function is only called for head blocks, best to disable. if (this.opts.disableLightClientServerOnImportBlockHead) { return; } // TODO GLOAS: Light client updates for gloas are not yet updated in the spec. // The block body no longer contains execution payload, so `blockToLightClientHeader` // cannot construct a header from a gloas block. Skip all light client processing // for post-gloas blocks, revisit once there is a spec for it. if (this.config.getForkSeq(block.slot) >= ForkSeq.gloas) { return; } // What is the syncAggregate signing? // From the state-transition // ``` // const previousSlot = Math.max(block.slot, 1) - 1; // const rootSigned = getBlockRootAtSlot(state, previousSlot); // ``` // In skipped slots the next value of blockRoots is set to the last block root. // So rootSigned will always equal to the parentBlock. const signedBlockRoot = block.parentRoot; const syncPeriod = computeSyncPeriodAtSlot(block.slot); this.onSyncAggregate(syncPeriod, block.body.syncAggregate, block.slot, signedBlockRoot).catch((e) => { if (!this.signal.aborted) { this.logger.error("Error onSyncAggregate", {}, e); this.metrics?.lightclientServer.onSyncAggregate.inc({event: "error"}); } }); this.persistPostBlockImportData(block, postState, parentBlockSlot).catch((e) => { if (!this.signal.aborted) { this.logger.error("Error persistPostBlockImportData", {}, e); } }); } /** * API ROUTE to get `currentSyncCommittee` and `nextSyncCommittee` from a trusted state root */ async getBootstrap(blockRoot: Uint8Array): Promise<LightClientBootstrap> { const syncCommitteeWitness = await this.db.syncCommitteeWitness.get(blockRoot); if (!syncCommitteeWitness) { throw new LightClientServerError( {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}, `syncCommitteeWitness not available ${toRootHex(blockRoot)}` ); } const [currentSyncCommittee, nextSyncCommittee] = await Promise.all([ this.db.syncCommittee.get(syncCommitteeWitness.currentSyncCommitteeRoot), this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot), ]); if (!currentSyncCommittee) { throw new LightClientServerError( {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}, "currentSyncCommittee not available" ); } if (!nextSyncCommittee) { throw new LightClientServerError( {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}, "nextSyncCommittee not available" ); } const header = await this.db.checkpointHeader.get(blockRoot); if (!header) { throw new LightClientServerError({code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}, "header not available"); } return { header, currentSyncCommittee, currentSyncCommitteeBranch: getCurrentSyncCommitteeBranch(syncCommitteeWitness), }; } /** * 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 */ async getUpdate(period: number): Promise<LightClientUpdate> { // Signature data const update = await this.db.bestLightClientUpdate.get(period); if (!update) { throw new LightClientServerError( {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}, `No partialUpdate available for period ${period}` ); } return update; } /** * API ROUTE to get the sync committee hash from the best available update for `period`. */ async getCommitteeRoot(period: number): Promise<Uint8Array> { const {attestedHeader} = await this.getUpdate(period); const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(attestedHeader.beacon); const syncCommitteeWitness = await this.db.syncCommitteeWitness.get(blockRoot); if (!syncCommitteeWitness) { throw new LightClientServerError( {code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE}, `syncCommitteeWitness not available ${toRootHex(blockRoot)} period ${period}` ); } return syncCommitteeWitness.currentSyncCommitteeRoot; } /** * API ROUTE to poll LightclientHeaderUpdate. * Clients should use the SSE type `light_client_optimistic_update` if available */ getOptimisticUpdate(): LightClientOptimisticUpdate | null { return this.latestHeadUpdate; } getFinalityUpdate(): LightClientFinalityUpdate | null { return this.finalized; } /** * With forkchoice data compute which block roots will never become checkpoints and prune them. */ async pruneNonCheckpointData(nonCheckpointBlockRoots: Uint8Array[]): Promise<void> { // TODO: Batch delete with native leveldb batching not just Promise.all() await Promise.all([ this.db.syncCommitteeWitness.batchDelete(nonCheckpointBlockRoots), this.db.checkpointHeader.batchDelete(nonCheckpointBlockRoots), ]); } private async persistPostBlockImportData( block: BeaconBlock<ForkPostAltair>, postState: IBeaconStateViewAltair, parentBlockSlot: Slot ): Promise<void> { const blockSlot = block.slot; const fork = this.config.getForkName(blockSlot); const header = blockToLightClientHeader(fork, block); const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(header.beacon); const blockRootHex = toRootHex(blockRoot); const syncCommitteeWitness = postState.getSyncCommitteesWitness(); // Only store current sync committee once per run if (!this.storedCurrentSyncCommittee) { await Promise.all([ this.storeSyncCommittee(postState.currentSyncCommittee, syncCommitteeWitness.currentSyncCommitteeRoot), this.storeSyncCommittee(postState.nextSyncCommittee, syncCommitteeWitness.nextSyncCommitteeRoot), ]); this.storedCurrentSyncCommittee = true; this.logger.debug("Stored currentSyncCommittee", {slot: blockSlot}); } // Only store next sync committee once per dependent root const parentBlockPeriod = computeSyncPeriodAtSlot(parentBlockSlot); const period = computeSyncPeriodAtSlot(blockSlot); if (parentBlockPeriod < period) { // If the parentBlock is in a previous epoch it must be the dependentRoot of this epoch transition const dependentRoot = toRootHex(block.parentRoot); const periodDependentRoots = this.knownSyncCommittee.getOrDefault(period); if (!periodDependentRoots.has(dependentRoot)) { periodDependentRoots.add(dependentRoot); await this.storeSyncCommittee(postState.nextSyncCommittee, syncCommitteeWitness.nextSyncCommitteeRoot); this.logger.debug("Stored nextSyncCommittee", {period, slot: blockSlot, dependentRoot}); } } // Ensure referenced syncCommittee are persisted before persiting this one await this.db.syncCommitteeWitness.put(blockRoot, syncCommitteeWitness); // Store header in case it is referenced latter by a future finalized checkpoint await this.db.checkpointHeader.put(blockRoot, header); // Store finalized checkpoint data const finalizedCheckpoint = postState.finalizedCheckpoint; const finalizedCheckpointPeriod = computeSyncPeriodAtEpoch(finalizedCheckpoint.epoch); const isFinalized = finalizedCheckpointPeriod === period && // Consider the edge case of genesis: Genesis state's finalizedCheckpoint is zero'ed. // If finalizedCheckpoint is zeroed, consider not finalized (ignore) since there won't exist a // finalized header for that root finalizedCheckpoint.epoch !== 0 && !byteArrayEquals(finalizedCheckpoint.root, ZERO_HASH); this.prevHeadData.set( blockRootHex, isFinalized ? { isFinalized: true, attestedHeader: header, blockRoot, finalityBranch: postState.getFinalizedRootProof(), finalizedCheckpoint, } : { isFinalized: false, attestedHeader: header, blockRoot, } ); pruneSetToMax(this.prevHeadData, MAX_PREV_HEAD_DATA); } /** * 1. Subscribe to gossip topics `sync_committee_{subnet_id}` and collect `sync_committee_message` * ``` * slot: Slot * beacon_block_root: Root * validator_index: ValidatorIndex * signature: BLSSignature * ``` * * 2. Subscribe to `sync_committee_contribution_and_proof` and collect `signed_contribution_and_proof` * ``` * slot: Slot * beacon_block_root: Root * subcommittee_index: uint64 * aggregation_bits: Bitvector[SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT] * signature: BLSSignature * ``` * * 3. On new blocks use `block.body.sync_aggregate`, `block.parent_root` and `block.slot - 1` * * @param syncPeriod The sync period of the sync aggregate and signed block root */ private async onSyncAggregate( syncPeriod: SyncPeriod, syncAggregate: altair.SyncAggregate, signatureSlot: Slot, signedBlockRoot: Root ): Promise<void> { this.metrics?.lightclientServer.onSyncAggregate.inc({event: "processed"}); const signedBlockRootHex = toRootHex(signedBlockRoot); const attestedData = this.prevHeadData.get(signedBlockRootHex); if (!attestedData) { // Log cacheSize since at start this.prevHeadData will be empty this.logger.debug("attestedData not available", {root: signedBlockRootHex, cacheSize: this.prevHeadData.size}); this.metrics?.lightclientServer.onSyncAggregate.inc({event: "ignore_no_attested_data"}); return; } const {attestedHeader, isFinalized} = attestedData; const attestedPeriod = computeSyncPeriodAtSlot(attestedHeader.beacon.slot); if (syncPeriod !== attestedPeriod) { this.logger.debug("attested data period different than signature period", {syncPeriod, attestedPeriod}); this.metrics?.lightclientServer.onSyncAggregate.inc({event: "ignore_attested_period_diff"}); return; } const headerUpdate: LightClientOptimisticUpdate = { attestedHeader, syncAggregate, signatureSlot, }; const syncAggregateParticipation = sumBits(syncAggregate.syncCommitteeBits); if (syncAggregateParticipation < MIN_SYNC_COMMITTEE_PARTICIPANTS) { this.logger.debug("sync committee below required MIN_SYNC_COMMITTEE_PARTICIPANTS", { syncPeriod, attestedPeriod, syncAggregateParticipation, }); this.metrics?.lightclientServer.onSyncAggregate.inc({event: "ignore_sync_committee_low"}); return; } // Fork of LightClientOptimisticUpdate and LightClientFinalityUpdate is based off on attested header's fork const attestedFork = this.config.getForkName(attestedHeader.beacon.slot); // Check if node is syncing / too far behind to avoid emitting stale light client updates const isStaleLightClientUpdate = this.clock.currentSlot - signatureSlot > SLOTS_PER_EPOCH; if (!isStaleLightClientUpdate) { // Emit update // Note: Always emit optimistic update even if we have emitted one with higher or equal attested_header.slot this.emitter.emit(routes.events.EventType.lightClientOptimisticUpdate, { version: attestedFork, data: headerUpdate, }); } else { this.metrics?.lightclientServer.staleLightClientUpdates.inc(); } // Persist latest best update for getLatestHeadUpdate() // TODO: Once SyncAggregate are constructed from P2P too, count bits to decide "best" if (!this.latestHeadUpdate || attestedHeader.beacon.slot > this.latestHeadUpdate.attestedHeader.beacon.slot) { this.latestHeadUpdate = headerUpdate; this.metrics?.lightclientServer.onSyncAggregate.inc({event: "update_latest_head_update"}); } if (isFinalized) { const finalizedCheckpointRoot = attestedData.finalizedCheckpoint.root; let finalizedHeader = await this.getFinalizedHeader(finalizedCheckpointRoot); if ( finalizedHeader && (!this.finalized || finalizedHeader.beacon.slot > this.finalized.finalizedHeader.beacon.slot || syncAggregateParticipation > sumBits(this.finalized.syncAggregate.syncCommitteeBits)) ) { if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) { finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader); } this.finalized = { attestedHeader, finalizedHeader, syncAggregate, finalityBranch: attestedData.finalityBranch, signatureSlot, }; this.metrics?.lightclientServer.onSyncAggregate.inc({event: "update_latest_finalized_update"}); if (!isStaleLightClientUpdate) { // Note: Ignores gossip rule to always emit finality_update with higher finalized_header.slot, for simplicity this.emitter.emit(routes.events.EventType.lightClientFinalityUpdate, { version: attestedFork, data: this.finalized, }); } } } // Check if this update is better, otherwise ignore try { await this.maybeStoreNewBestUpdate(syncPeriod, syncAggregate, signatureSlot, attestedData); } catch (e) { this.logger.error( "Error updating best LightClientUpdate", {syncPeriod, slot: attestedHeader.beacon.slot, blockRoot: toRootHex(attestedData.blockRoot)}, e as Error ); } } /** * Given a new `syncAggregate` maybe persist a new best partial update if its better than the current stored for * that sync period. */ private async maybeStoreNewBestUpdate( syncPeriod: SyncPeriod, syncAggregate: altair.SyncAggregate, signatureSlot: Slot, attestedData: SyncAttestedData ): Promise<void> { const prevBestUpdate = await this.db.bestLightClientUpdate.get(syncPeriod); const {attestedHeader} = attestedData; if (prevBestUpdate) { const prevBestUpdateSummary = toLightClientUpdateSummary(prevBestUpdate); const nextBestUpdate: LightClientUpdateSummary = { activeParticipants: sumBits(syncAggregate.syncCommitteeBits), attestedHeaderSlot: attestedHeader.beacon.slot, signatureSlot, // The actual finalizedHeader is fetched below. To prevent a DB read we approximate the actual slot. // If update is not finalized finalizedHeaderSlot does not matter (see is_better_update), so setting // to zero to set it some number. finalizedHeaderSlot: attestedData.isFinalized ? computeStartSlotAtEpoch(attestedData.finalizedCheckpoint.epoch) : 0, // All updates include a valid `nextSyncCommitteeBranch`, see below code isSyncCommitteeUpdate: true, isFinalityUpdate: attestedData.isFinalized, }; if (!isBetterUpdate(nextBestUpdate, prevBestUpdateSummary)) { this.metrics?.lightclientServer.updateNotBetter.inc(); return; } } const syncCommitteeWitness = await this.db.syncCommitteeWitness.get(attestedData.blockRoot); if (!syncCommitteeWitness) { throw Error(`syncCommitteeWitness not available at ${toRootHex(attestedData.blockRoot)}`); } const attestedFork = this.config.getForkName(attestedHeader.beacon.slot); const numWitness = syncCommitteeWitness.witness.length; if (isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS_ELECTRA) { throw Error(`Expected ${NUM_WITNESS_ELECTRA} witnesses in post-Electra numWitness=${numWitness}`); } if (!isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) { throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWitness=${numWitness}`); } const nextSyncCommittee = await this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot); if (!nextSyncCommittee) { throw Error("nextSyncCommittee not available"); } const nextSyncCommitteeBranch = getNextSyncCommitteeBranch(syncCommitteeWitness); const finalizedHeaderAttested = attestedData.isFinalized ? await this.getFinalizedHeader(attestedData.finalizedCheckpoint.root) : null; let isFinalized: boolean, finalityBranch: Uint8Array[], finalizedHeader: LightClientHeader; if ( attestedData.isFinalized && finalizedHeaderAttested && computeSyncPeriodAtSlot(finalizedHeaderAttested.beacon.slot) === syncPeriod ) { isFinalized = true; finalityBranch = attestedData.finalityBranch; finalizedHeader = finalizedHeaderAttested; // Fork of LightClientUpdate is based off on attested header's fork if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) { finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader); } } else { isFinalized = false; finalityBranch = this.zero.finalityBranch; // No need to upgrade finalizedHeader because its anyway set to zero of highest fork finalizedHeader = this.zero.finalizedHeader; } const newUpdate = { attestedHeader, nextSyncCommittee: nextSyncCommittee, nextSyncCommitteeBranch, finalizedHeader, finalityBranch, syncAggregate, signatureSlot, } as LightClientUpdate; // attestedData and the block of syncAggregate may not be in same sync period // should not use attested data slot as sync period // see https://github.com/ChainSafe/lodestar/issues/3933 await this.db.bestLightClientUpdate.put(syncPeriod, newUpdate); this.logger.debug("Stored new PartialLightClientUpdate", { syncPeriod, isFinalized, participation: sumBits(syncAggregate.syncCommitteeBits) / SYNC_COMMITTEE_SIZE, }); // Count total persisted updates per type. DB metrics don't diff between each type. // The frequency of finalized vs non-finalized is critical to debug if finalizedHeader is not available this.metrics?.lightclientServer.onSyncAggregate.inc({ event: isFinalized ? "store_finalized_update" : "store_nonfinalized_update", }); this.metrics?.lightclientServer.highestSlot.set( {item: isFinalized ? "best_finalized_update" : "best_nonfinalized_update"}, newUpdate.attestedHeader.beacon.slot ); } private async storeSyncCommittee(syncCommittee: altair.SyncCommittee, syncCommitteeRoot: Uint8Array): Promise<void> { const isKnown = await this.db.syncCommittee.has(syncCommitteeRoot); if (!isKnown) { await this.db.syncCommittee.putBinary(syncCommitteeRoot, ssz.altair.SyncCommittee.serialize(syncCommittee)); } } /** * Get finalized header from db. Keeps a small in-memory cache to speed up most of the lookups */ private async getFinalizedHeader(finalizedBlockRoot: Uint8Array): Promise<LightClientHeader | null> { const finalizedBlockRootHex = toRootHex(finalizedBlockRoot); const cachedFinalizedHeader = this.checkpointHeaders.get(finalizedBlockRootHex); if (cachedFinalizedHeader) { return cachedFinalizedHeader; } const finalizedHeader = await this.db.checkpointHeader.get(finalizedBlockRoot); if (!finalizedHeader) { // finalityHeader is not available during sync, since started after the finalized checkpoint. // See https://github.com/ChainSafe/lodestar/issues/3495 // To prevent excesive logging this condition is not considered an error, but the lightclient updater // will just create a non-finalized update. this.logger.debug("finalizedHeader not available", {root: finalizedBlockRootHex}); return null; } this.checkpointHeaders.set(finalizedBlockRootHex, finalizedHeader); pruneSetToMax(this.checkpointHeaders, MAX_CACHED_FINALIZED_HEADERS); return finalizedHeader; } } export function sumBits(bits: BitArray): number { return bits.getTrueBitIndexes().length; } // TODO GLOAS: Pending light-client spec but this function probably won't be used // in Gloas. So we can assume any types here are pre-gloas export function blockToLightClientHeader( fork: ForkName, block: BeaconBlock<ForkPostAltair & ForkPreGloas> ): LightClientHeader { const blockSlot = block.slot; const beacon: phase0.BeaconBlockHeader = { slot: blockSlot, proposerIndex: block.proposerIndex, parentRoot: block.parentRoot, stateRoot: block.stateRoot, bodyRoot: (ssz[fork].BeaconBlockBody as SSZTypesFor<ForkPostAltair & ForkPreGloas, "BeaconBlockBody">).hashTreeRoot( block.body ), }; if (ForkSeq[fork] >= ForkSeq.capella) { const blockBody = block.body as BeaconBlockBody<ForkPostBellatrix & ForkPreGloas>; const execution = executionPayloadToPayloadHeader(ForkSeq[fork], blockBody.executionPayload); return { beacon, execution, executionBranch: getBlockBodyExecutionHeaderProof(fork as ForkPostBellatrix, blockBody), } as LightClientHeader; } return {beacon}; }