import { SLOTS_PER_EPOCH } from "@lodestar/params";
import { computeEpochAtSlot, computeStartSlotAtEpoch } from "@lodestar/state-transition";
import { AllocSource } from "../../../util/bufferPool.js";
import { getStateSlotFromBytes } from "../../../util/multifork.js";
import { serializeState } from "../../serializeState.js";
/**
 * Minimum number of epochs between single temp archived states
 * These states will be pruned once a new state is persisted
 */
export const PERSIST_TEMP_STATE_EVERY_EPOCHS = 32;
export { FrequencyStateArchiveStep };
var FrequencyStateArchiveStep;
(function (FrequencyStateArchiveStep) {
    FrequencyStateArchiveStep["LoadLastStoredSlot"] = "load_last_stored_slot";
    FrequencyStateArchiveStep["GetFinalizedState"] = "get_finalized_state";
    // SerializeState is tracked via stateSerializeDuration metric
    FrequencyStateArchiveStep["PersistState"] = "persist_state";
    FrequencyStateArchiveStep["LoadStoredSlotsToDelete"] = "load_stored_slots_to_delete";
    FrequencyStateArchiveStep["DeleteOldStates"] = "delete_old_states";
})(FrequencyStateArchiveStep || (FrequencyStateArchiveStep = {}));
/**
 * Archives finalized states from active bucket to archive bucket.
 *
 * Only the new finalized state is stored to disk
 */
export class FrequencyStateArchiveStrategy {
    regen;
    db;
    logger;
    opts;
    bufferPool;
    constructor(regen, db, logger, opts, bufferPool) {
        this.regen = regen;
        this.db = db;
        this.logger = logger;
        this.opts = opts;
        this.bufferPool = bufferPool;
    }
    async onFinalizedCheckpoint(_finalized, _metrics) { }
    async onCheckpoint(_stateRoot, _metrics) { }
    /**
     * Persist states every some epochs to
     * - Minimize disk space, storing the least states possible
     * - Minimize the sync progress lost on unexpected crash, storing temp state every few epochs
     *
     * At epoch `e` there will be states peristed at intervals of `PERSIST_STATE_EVERY_EPOCHS` = 32
     * and one at `PERSIST_TEMP_STATE_EVERY_EPOCHS` = 1024
     * ```
     *        |                |             |           .
     * epoch - 1024*2    epoch - 1024    epoch - 32    epoch
     * ```
     */
    async maybeArchiveState(finalized, metrics) {
        let timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
        const lastStoredSlot = await this.db.stateArchive.lastKey();
        timer?.({ step: FrequencyStateArchiveStep.LoadLastStoredSlot });
        const lastStoredEpoch = computeEpochAtSlot(lastStoredSlot ?? 0);
        const { archiveStateEpochFrequency } = this.opts;
        const logCtx = { finalizedEpoch: finalized.epoch, lastStoredEpoch, archiveStateEpochFrequency };
        if (finalized.epoch - lastStoredEpoch >= Math.min(PERSIST_TEMP_STATE_EVERY_EPOCHS, archiveStateEpochFrequency)) {
            this.logger.verbose("Start archiving state", logCtx);
            await this.archiveState(finalized, metrics);
            // Only check the current and previous intervals
            const minEpoch = Math.max(0, (Math.floor(finalized.epoch / archiveStateEpochFrequency) - 1) * archiveStateEpochFrequency);
            timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
            const storedStateSlots = await this.db.stateArchive.keys({
                lt: computeStartSlotAtEpoch(finalized.epoch),
                gte: computeStartSlotAtEpoch(minEpoch),
            });
            timer?.({ step: FrequencyStateArchiveStep.LoadStoredSlotsToDelete });
            const statesSlotsToDelete = computeStateSlotsToDelete(storedStateSlots, archiveStateEpochFrequency);
            timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
            if (statesSlotsToDelete.length > 0) {
                await this.db.stateArchive.batchDelete(statesSlotsToDelete);
            }
            timer?.({ step: FrequencyStateArchiveStep.DeleteOldStates });
            // More logs to investigate the rss spike issue https://github.com/ChainSafe/lodestar/issues/5591
            this.logger.verbose("Archived state completed", {
                ...logCtx,
                minEpoch,
                storedStateSlots: storedStateSlots.join(","),
                statesSlotsToDelete: statesSlotsToDelete.join(","),
            });
        }
        else {
            this.logger.verbose("Skip archiving state", logCtx);
        }
    }
    /**
     * Archives finalized states from active bucket to archive bucket.
     * Only the new finalized state is stored to disk
     */
    async archiveState(finalized, metrics) {
        // starting from Mar 2024, the finalized state could be from disk or in memory
        let timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
        const finalizedStateOrBytes = await this.regen.getCheckpointStateOrBytes(finalized);
        timer?.({ step: FrequencyStateArchiveStep.GetFinalizedState });
        const { rootHex } = finalized;
        if (!finalizedStateOrBytes) {
            throw Error(`No state in cache for finalized checkpoint state epoch #${finalized.epoch} root ${rootHex}`);
        }
        if (finalizedStateOrBytes instanceof Uint8Array) {
            const slot = getStateSlotFromBytes(finalizedStateOrBytes);
            timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
            await this.db.stateArchive.putBinary(slot, finalizedStateOrBytes);
            timer?.({ step: FrequencyStateArchiveStep.PersistState });
            this.logger.verbose("Archived finalized state bytes", { epoch: finalized.epoch, slot, root: rootHex });
        }
        else {
            // serialize state using BufferPool if provided
            const sszTimer = metrics?.stateSerializeDuration.startTimer({ source: AllocSource.ARCHIVE_STATE });
            await serializeState(finalizedStateOrBytes, AllocSource.ARCHIVE_STATE, async (stateBytes) => {
                sszTimer?.();
                timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
                await this.db.stateArchive.putBinary(finalizedStateOrBytes.slot, stateBytes);
                timer?.({ step: FrequencyStateArchiveStep.PersistState });
            }, this.bufferPool);
            // don't delete states before the finalized state, auto-prune will take care of it
            this.logger.verbose("Archived finalized state", {
                epoch: finalized.epoch,
                slot: finalizedStateOrBytes.slot,
                root: rootHex,
            });
        }
    }
}
/**
 * Keeps first epoch per interval of persistEveryEpochs, deletes the rest
 */
export function computeStateSlotsToDelete(storedStateSlots, persistEveryEpochs) {
    const persistEverySlots = persistEveryEpochs * SLOTS_PER_EPOCH;
    const intervalsWithStates = new Set();
    const stateSlotsToDelete = new Set();
    for (const slot of storedStateSlots) {
        const interval = Math.floor(slot / persistEverySlots);
        if (intervalsWithStates.has(interval)) {
            stateSlotsToDelete.add(slot);
        }
        else {
            intervalsWithStates.add(interval);
        }
    }
    return Array.from(stateSlotsToDelete.values());
}
//# sourceMappingURL=frequencyStateArchiveStrategy.js.map