import { routes } from "@lodestar/api";
import { BeaconConfig } from "@lodestar/config";
import { CachedBeaconStateAllForks } from "@lodestar/state-transition";
import { Epoch, RootHex, phase0 } from "@lodestar/types";
import { Logger } from "@lodestar/utils";
import { Metrics } from "../../metrics/index.js";
import { BufferPool } from "../../util/bufferPool.js";
import { IClock } from "../../util/clock.js";
import { CPStateDatastore, DatastoreKey } from "./datastore/index.js";
import { BlockStateCache, CheckpointHex, CheckpointStateCache } from "./types.js";
export type PersistentCheckpointStateCacheOpts = {
    /** Keep max n state epochs in memory, persist the rest to disk */
    maxCPStateEpochsInMemory?: number;
    /** Keep max n state epochs on disk */
    maxCPStateEpochsOnDisk?: number;
};
type PersistentCheckpointStateCacheModules = {
    config: BeaconConfig;
    metrics?: Metrics | null;
    logger: Logger;
    clock?: IClock | null;
    signal?: AbortSignal;
    datastore: CPStateDatastore;
    blockStateCache: BlockStateCache;
    bufferPool?: BufferPool;
};
type LoadedStateBytesData = {
    persistedKey: DatastoreKey;
    stateBytes: Uint8Array;
};
/**
 * Before n-historical states, lodestar keeps all checkpoint states since finalized
 * Since Sep 2024, lodestar stores 3 most recent checkpoint states in memory and the rest on disk. The finalized state
 * may not be available in memory, and stay on disk instead.
 */
export declare const DEFAULT_MAX_CP_STATE_EPOCHS_IN_MEMORY = 3;
/**
 * By default we don't prune any persistent checkpoint states as it's not safe to delete them during
 * long non-finality as we don't know the state of the chain and there could be a deep (hundreds of epochs) reorg
 * if there two competing chains with similar weight but we wouldn't have a close enough state to pivot to this chain
 * and instead require a resync from last finalized checkpoint state which could be very far in the past.
 */
export declare const DEFAULT_MAX_CP_STATE_ON_DISK: number;
/**
 * An implementation of CheckpointStateCache that keep up to n epoch checkpoint states in memory and persist the rest to disk
 * - If it's more than `maxEpochsInMemory` epochs old, it will persist n last epochs to disk based on the view of the block
 * - Once a chain gets finalized we'll prune all states from memory and disk for epochs < finalizedEpoch
 * - In get*() apis if shouldReload is true, it will reload from disk. The reload() api is expensive and should only be called in some important flows:
 *   - Get state for block processing
 *   - updateHeadState
 *   - as with any cache, the state could be evicted from memory at any time, so we should always check if the state is in memory or not
 * - Each time we process a state, we only persist exactly 1 checkpoint state per epoch based on the view of block and prune all others. The persisted
 * checkpoint state could be finalized and used later in archive task, it's also used to regen states.
 * - When we process multiple states in the same epoch, we could persist different checkpoint states of the same epoch because each block could have its
 * own view. See unit test of this file `packages/beacon-node/test/unit/chain/stateCache/persistentCheckpointsCache.test.ts` for more details.
 *
 * The below diagram shows Previous Root Checkpoint State is persisted for epoch (n-2) and Current Root Checkpoint State is persisted for epoch (n-1)
 * while at epoch (n) and (n+1) we have both of them in memory
 *
 * ╔════════════════════════════════════╗═══════════════╗
 * ║      persisted to db or fs         ║   in memory   ║
 * ║        reload if needed            ║               ║
 * ║ -----------------------------------║---------------║
 * ║        epoch:       (n-2)   (n-1)  ║  n     (n+1)  ║
 * ║               |-------|-------|----║--|-------|----║
 * ║                      ^        ^    ║ ^       ^     ║
 * ║                                    ║  ^       ^    ║
 * ╚════════════════════════════════════╝═══════════════╝
 *
 * The "in memory" checkpoint states are similar to the old implementation: we have both Previous Root Checkpoint State and Current Root Checkpoint State per epoch.
 * However in the "persisted to db or fs" part
 *   - if there is no reorg, we only store 1 checkpoint state per epoch, the one that could potentially be justified/finalized later based on the view of the state
 *   - if there is reorg, we may store >=2 checkpoint states per epoch, including any checkpoints with unknown roots to the processed state
 *   - the goal is to make sure we can regen any states later if needed, and we have the checkpoint state that could be justified/finalized later
 */
export declare class PersistentCheckpointStateCache implements CheckpointStateCache {
    private readonly cache;
    /** Epoch -> Set<blockRoot> */
    private readonly epochIndex;
    private readonly config;
    private readonly metrics;
    private readonly logger;
    private readonly clock;
    private readonly signal;
    private preComputedCheckpoint;
    private preComputedCheckpointHits;
    private readonly maxEpochsInMemory;
    private readonly maxEpochsOnDisk;
    private readonly datastore;
    private readonly blockStateCache;
    private readonly bufferPool?;
    constructor({ config, metrics, logger, clock, signal, datastore, blockStateCache, bufferPool }: PersistentCheckpointStateCacheModules, opts: PersistentCheckpointStateCacheOpts);
    /**
     * Reload checkpoint state keys from the last run.
     */
    init(): Promise<void>;
    /**
     * Get a state from cache, it may reload from disk.
     * This is an expensive api, should only be called in some important flows:
     * - Validate a gossip block
     * - Get block for processing
     * - Regen head state
     */
    getOrReload(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | null>;
    /**
     * Return either state or state bytes loaded from db.
     */
    getStateOrBytes(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | Uint8Array | null>;
    /**
     * Return either state or state bytes with persisted key loaded from db.
     */
    getStateOrLoadDb(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | LoadedStateBytesData | null>;
    /**
     * Similar to get() api without reloading from disk
     */
    get(cpOrKey: CheckpointHex | string): CachedBeaconStateAllForks | null;
    /**
     * Add a state of a checkpoint to this cache, prune from memory if necessary.
     */
    add(cp: phase0.Checkpoint, state: CachedBeaconStateAllForks): void;
    /**
     * Searches in-memory state for the latest cached state with a `root` without reload, starting with `epoch` and descending
     */
    getLatest(rootHex: RootHex, maxEpoch: Epoch): CachedBeaconStateAllForks | null;
    /**
     * Searches state for the latest cached state with a `root`, reload if needed, starting with `epoch` and descending
     * This is expensive api, should only be called in some important flows:
     * - Validate a gossip block
     * - Get block for processing
     * - Regen head state
     */
    getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise<CachedBeaconStateAllForks | null>;
    /**
     * Update the precomputed checkpoint and return the number of his for the
     * previous one (if any).
     */
    updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null;
    /**
     * This is just to conform to the old implementation
     */
    prune(): void;
    /**
     * Prune all checkpoint states before the provided finalized epoch.
     */
    pruneFinalized(finalizedEpoch: Epoch): void;
    /**
     * After processing a block, prune from memory based on the view of that block.
     * This is likely persist 1 state per epoch, at the last 1/3 of slot 0 of an epoch although it'll be called on every last 1/3 of slot.
     * Given the following block b was processed with b2, b1, b0 are ancestors in epoch (n-2), (n-1), n respectively
     *
     *       epoch:          (n-2)       (n-1)         n         (n+1)
     *             |-----------|-----------|-----------|-----------|
     *                        ^            ^           ^    ^
     *                        |            |           |    |
     * block chain:           b2---------->b1--------->b0-->b
     *
     * After processing block b, if maxEpochsInMemory is:
     * - 2 then we'll persist {root: b2, epoch n-2} checkpoint state to disk
     * - 1 then we'll persist {root: b2, epoch n-2} and {root: b1, epoch n-1} checkpoint state to disk
     * - 0 then we'll persist {root: b2, epoch n-2} and {root: b1, epoch n-1} and {root: b0, epoch n} checkpoint state to disk
     *   - if any old epochs checkpoint states are persisted, no need to do it again
     *
     * Note that for each epoch there could be multiple checkpoint states, usually 2, one for Previous Root Checkpoint State and one for Current Root Checkpoint State.
     * We normally only persist 1 checkpoint state per epoch, the one that could potentially be justified/finalized later based on the view of the block.
     * Other checkpoint states are pruned from memory.
     *
     * This design also covers the reorg scenario. Given block c in the same epoch n where c.slot > b.slot, c is not descendant of b, and c is built on top of c0
     * instead of b0 (epoch (n - 1))
     *
     *       epoch:          (n-2)       (n-1)         n         (n+1)
     *             |-----------|-----------|-----------|-----------|
     *                        ^            ^       ^   ^    ^   ^
     *                        |            |       |   |    |   |
     * block chain:           b2---------->b1----->c0->b0-->b   |
     *                                             ║            |
     *                                             ╚═══════════>c (reorg)
     *
     * After processing block c, if maxEpochsInMemory is:
     * - 0 then we'll persist {root: c0, epoch: n} checkpoint state to disk. Note that regen should populate {root: c0, epoch: n} checkpoint state before.
     *
     *                           epoch:      (n-1)                                                           n                                                           (n+1)
     *                                         |-------------------------------------------------------------|-------------------------------------------------------------|
     *                                         ^                               ^                             ^                             ^
     *   _______                               |                               |                             |                             |
     *  |       |                              |                               |                             |                             |
     *  |  db   |====== reload ======> {root: b1, epoch: n-1} cp state ======> c0 block state ======> {root: c0, epoch: n} cp state =====> c block state
     *  |_______|
     *
     *
     *
     * - 1 then we'll persist {root: b1, epoch n-1} checkpoint state to disk. Note that at epoch n there is both {root: b0, epoch: n} and {root: c0, epoch: n} checkpoint states in memory
     * - 2 then we'll persist {root: b2, epoch n-2} checkpoint state to disk, there are also 2 checkpoint states in memory at epoch n, same to the above (maxEpochsInMemory=1)
     *
     * As of Mar 2024, it takes <=350ms to persist a holesky state on fast server
     */
    processState(blockRootHex: RootHex, state: CachedBeaconStateAllForks): Promise<number>;
    /**
     * Find a seed state to reload the state of provided checkpoint. Based on the design of n-historical state:
     *
     * ╔════════════════════════════════════╗═══════════════╗
     * ║      persisted to db or fs         ║   in memory   ║
     * ║        reload if needed            ║               ║
     * ║ -----------------------------------║---------------║
     * ║        epoch:       (n-2)   (n-1)  ║  n     (n+1)  ║
     * ║               |-------|-------|----║--|-------|----║
     * ║                      ^        ^    ║ ^       ^     ║
     * ║                                    ║  ^       ^    ║
     * ╚════════════════════════════════════╝═══════════════╝
     *
     * we always reload an epoch in the past. We'll start with epoch n then (n+1) prioritizing ones with the same view of `reloadedCp`.
     *
     * Use seed state from the block cache if cannot find any seed states within this cache.
     */
    findSeedStateToReload(reloadedCp: CheckpointHex): CachedBeaconStateAllForks;
    clear(): void;
    /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */
    dumpSummary(): routes.lodestar.StateCacheItem[];
    getStates(): IterableIterator<CachedBeaconStateAllForks>;
    /** ONLY FOR DEBUGGING PURPOSES. For spec tests on error */
    dumpCheckpointKeys(): string[];
    private processPastEpoch;
    private deleteAllEpochItems;
    /**
     * Prune persisted checkpoint states from disk.
     * Note that this should handle all possible errors and not throw.
     */
    private prunePersistedStates;
    /**
     * Serialize validators to bytes leveraging the buffer pool to save memory allocation.
     *   - As monitored on holesky as of Jan 2024, it helps save ~500ms state reload time (4.3s vs 3.8s)
     *   - Also `serializeState.test.ts` perf test shows a lot of differences allocating validators bytes once vs every time,
     * This is 2x - 3x faster than allocating memory every time.
     */
    private serializeStateValidators;
}
export declare function toCheckpointHex(checkpoint: phase0.Checkpoint): CheckpointHex;
export declare function toCheckpointKey(cp: CheckpointHex): string;
export {};
//# sourceMappingURL=persistentCheckpointsCache.d.ts.map