UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

241 lines 14.3 kB
import { routes } from "@lodestar/api"; import { BeaconConfig } from "@lodestar/config"; import { IBeaconStateView } 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; }; /** checkpoint serialized as a string */ type CacheKey = string; 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<IBeaconStateView | null>; /** * Return either state or state bytes loaded from db. */ getStateOrBytes(cp: CheckpointHex): Promise<IBeaconStateView | Uint8Array | null>; /** * Return either state or state bytes with persisted key loaded from db. */ getStateOrLoadDb(cp: CheckpointHex): Promise<IBeaconStateView | LoadedStateBytesData | null>; /** * Similar to get() api without reloading from disk */ get(cpOrKey: CheckpointHex | CacheKey): IBeaconStateView | null; /** * Add a state of a checkpoint to this cache, prune from memory if necessary. */ add(cp: phase0.Checkpoint, state: IBeaconStateView): 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): IBeaconStateView | 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<IBeaconStateView | null>; /** * Update the precomputed checkpoint and return the number of hits 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: IBeaconStateView): 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): IBeaconStateView; clear(): void; /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */ dumpSummary(): routes.lodestar.StateCacheItem[]; getStates(): IterableIterator<IBeaconStateView>; /** 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