import { routes } from "@lodestar/api";
import { isBetterUpdate, toLightClientUpdateSummary, upgradeLightClientHeader, } from "@lodestar/light-client/spec";
import { ForkSeq, MIN_SYNC_COMMITTEE_PARTICIPANTS, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SIZE, forkPostAltair, highestFork, isForkPostElectra, } from "@lodestar/params";
import { computeStartSlotAtEpoch, computeSyncPeriodAtEpoch, computeSyncPeriodAtSlot, executionPayloadToPayloadHeader, } from "@lodestar/state-transition";
import { ssz, sszTypesFor, } from "@lodestar/types";
import { MapDef, byteArrayEquals, pruneSetToMax, toRootHex } from "@lodestar/utils";
import { ZERO_HASH } from "../../constants/index.js";
import { NUM_WITNESS, NUM_WITNESS_ELECTRA } from "../../db/repositories/lightclientSyncCommitteeWitness.js";
import { LightClientServerError, LightClientServerErrorCode } from "../errors/lightClientError.js";
import { getBlockBodyExecutionHeaderProof, getCurrentSyncCommitteeBranch, getFinalizedRootProof, getNextSyncCommitteeBranch, getSyncCommitteesWitness, } from "./proofs.js";
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 {
    opts;
    db;
    config;
    metrics;
    emitter;
    logger;
    clock;
    signal;
    knownSyncCommittee = new MapDef(() => new Set());
    storedCurrentSyncCommittee = false;
    /**
     * Keep in memory since this data is very transient, not useful after a few slots
     */
    prevHeadData = new Map();
    checkpointHeaders = new Map();
    latestHeadUpdate = null;
    zero;
    finalized = null;
    constructor(opts, modules) {
        this.opts = opts;
        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, postState, parentBlockSlot) {
        // 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;
        }
        // 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) {
        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) {
        // 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) {
        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() {
        return this.latestHeadUpdate;
    }
    getFinalityUpdate() {
        return this.finalized;
    }
    /**
     * With forkchoice data compute which block roots will never become checkpoints and prune them.
     */
    async pruneNonCheckpointData(nonCheckpointBlockRoots) {
        // TODO: Batch delete with native leveldb batching not just Promise.all()
        await Promise.all([
            this.db.syncCommitteeWitness.batchDelete(nonCheckpointBlockRoots),
            this.db.checkpointHeader.batchDelete(nonCheckpointBlockRoots),
        ]);
    }
    async persistPostBlockImportData(block, postState, parentBlockSlot) {
        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 = getSyncCommitteesWitness(fork, postState);
        // 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: getFinalizedRootProof(postState),
                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
     */
    async onSyncAggregate(syncPeriod, syncAggregate, signatureSlot, signedBlockRoot) {
        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 = {
            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);
        }
    }
    /**
     * Given a new `syncAggregate` maybe persist a new best partial update if its better than the current stored for
     * that sync period.
     */
    async maybeStoreNewBestUpdate(syncPeriod, syncAggregate, signatureSlot, attestedData) {
        const prevBestUpdate = await this.db.bestLightClientUpdate.get(syncPeriod);
        const { attestedHeader } = attestedData;
        if (prevBestUpdate) {
            const prevBestUpdateSummary = toLightClientUpdateSummary(prevBestUpdate);
            const nextBestUpdate = {
                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, finalityBranch, finalizedHeader;
        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,
        };
        // 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);
    }
    async storeSyncCommittee(syncCommittee, syncCommitteeRoot) {
        const isKnown = await this.db.syncCommittee.has(syncCommitteeRoot);
        if (!isKnown) {
            await this.db.syncCommittee.putBinary(syncCommitteeRoot, syncCommittee.serialize());
        }
    }
    /**
     * Get finalized header from db. Keeps a small in-memory cache to speed up most of the lookups
     */
    async getFinalizedHeader(finalizedBlockRoot) {
        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) {
    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, block) {
    const blockSlot = block.slot;
    const beacon = {
        slot: blockSlot,
        proposerIndex: block.proposerIndex,
        parentRoot: block.parentRoot,
        stateRoot: block.stateRoot,
        bodyRoot: ssz[fork].BeaconBlockBody.hashTreeRoot(block.body),
    };
    if (ForkSeq[fork] >= ForkSeq.capella) {
        const blockBody = block.body;
        const execution = executionPayloadToPayloadHeader(ForkSeq[fork], blockBody.executionPayload);
        return {
            beacon,
            execution,
            executionBranch: getBlockBodyExecutionHeaderProof(fork, blockBody),
        };
    }
    return { beacon };
}
//# sourceMappingURL=index.js.map