@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
261 lines • 15.6 kB
TypeScript
import { routes } from "@lodestar/api";
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 { StateRegenerationOpts } from "../regen/interface.js";
import { CPStateDatastore, DatastoreKey } from "./datastore/index.js";
import { BlockStateCache, CheckpointHex, CheckpointStateCache } from "./types.js";
export type PersistentCheckpointStateCacheOpts = {
/** Keep max n states in memory, persist the rest to disk */
maxCPStateEpochsInMemory?: number;
};
type PersistentCheckpointStateCacheModules = {
metrics?: Metrics | null;
logger: Logger;
clock?: IClock | null;
signal?: AbortSignal;
datastore: CPStateDatastore;
blockStateCache: BlockStateCache;
bufferPool?: BufferPool | null;
};
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;
/**
* 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 metrics;
private readonly logger;
private readonly clock;
private readonly signal;
private preComputedCheckpoint;
private preComputedCheckpointHits;
private readonly maxEpochsInMemory;
private readonly datastore;
private readonly blockStateCache;
private readonly bufferPool?;
constructor({ 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, opts?: StateRegenerationOpts): 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, opts?: StateRegenerationOpts): Promise<CachedBeaconStateAllForks | LoadedStateBytesData | null>;
/**
* Similar to get() api without reloading from disk
*/
get(cpOrKey: CheckpointHex | string, opts?: StateRegenerationOpts): 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, opts?: StateRegenerationOpts): 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, opts?: StateRegenerationOpts): 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[];
/**
* Prune or persist checkpoint states in an epoch
* 1) If there is 1 checkpoint state with known root, persist it. This is when there is skipped slot at block 0 of epoch
* slot: n
* |-----------------------|-----------------------|
* PRCS root |
*
* 2) If there are 2 checkpoint states, PRCS and CRCS and both roots are known to this state, persist CRCS. If the block is reorged,
* PRCS is regen and populated to this cache again.
* slot: n
* |-----------------------|-----------------------|
* PRCS root - prune |
* CRCS root - persist |
*
* 3) If there are any roots that unknown to this state, persist their cp state. This is to handle the current block is reorged later
*
* 4) (derived from above) If there are 2 checkpoint states, PRCS and an unknown root, persist both.
* - In the example below block slot (n + 1) reorged n
* - If we process state n + 1, CRCS is unknown to it
* - we need to also store CRCS to handle the case (n+2) switches to n again
*
* PRCS - persist
* | processState()
* | |
* -------------n+1
* / |
* n-1 ------n------------n+2
* |
* CRCS - persist
*
* - PRCS is the checkpoint state that could be justified/finalized later based on the view of the state
* - unknown root checkpoint state is persisted to handle the reorg back to that branch later
*
* Performance note:
* - In normal condition, we persist 1 checkpoint state per epoch.
* - In reorged condition, we may persist multiple (most likely 2) checkpoint states per epoch.
*/
private processPastEpoch;
/**
* Delete all items of an epoch from disk and memory
*/
private deleteAllEpochItems;
/**
* 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 {};
//# sourceMappingURL=persistentCheckpointsCache.d.ts.map