@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
241 lines • 14.3 kB
TypeScript
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