@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
274 lines (236 loc) • 10.4 kB
text/typescript
import {routes} from "@lodestar/api";
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types";
import {Logger, toRootHex} from "@lodestar/utils";
import {Metrics} from "../../metrics/index.js";
import {JobItemQueue} from "../../util/queue/index.js";
import {BlockStateCache, CheckpointHex, CheckpointStateCache} from "../stateCache/types.js";
import {RegenError, RegenErrorCode} from "./errors.js";
import {
IStateRegenerator,
IStateRegeneratorInternal,
RegenCaller,
RegenFnName,
StateRegenerationOpts,
} from "./interface.js";
import {RegenModules, StateRegenerator} from "./regen.js";
const REGEN_QUEUE_MAX_LEN = 256;
// TODO: Should this constant be lower than above? 256 feels high
const REGEN_CAN_ACCEPT_WORK_THRESHOLD = 16;
type QueuedStateRegeneratorModules = RegenModules & {
signal: AbortSignal;
};
type RegenRequestKey = keyof IStateRegeneratorInternal;
type RegenRequestByKey = {[K in RegenRequestKey]: {key: K; args: Parameters<IStateRegeneratorInternal[K]>}};
export type RegenRequest = RegenRequestByKey[RegenRequestKey];
/**
* Regenerates states that have already been processed by the fork choice
*
* All requests are queued so that only a single state at a time may be regenerated at a time
*/
export class QueuedStateRegenerator implements IStateRegenerator {
readonly jobQueue: JobItemQueue<[RegenRequest], IBeaconStateView>;
private readonly regen: StateRegenerator;
private readonly forkChoice: IForkChoice;
private readonly blockStateCache: BlockStateCache;
private readonly checkpointStateCache: CheckpointStateCache;
private readonly metrics: Metrics | null;
private readonly logger: Logger;
constructor(modules: QueuedStateRegeneratorModules) {
this.regen = new StateRegenerator(modules);
this.jobQueue = new JobItemQueue<[RegenRequest], IBeaconStateView>(
this.jobQueueProcessor,
{maxLength: REGEN_QUEUE_MAX_LEN, signal: modules.signal},
modules.metrics ? modules.metrics.regenQueue : undefined
);
this.forkChoice = modules.forkChoice;
this.blockStateCache = modules.blockStateCache;
this.checkpointStateCache = modules.checkpointStateCache;
this.metrics = modules.metrics;
this.logger = modules.logger;
}
async init(): Promise<void> {
if (this.checkpointStateCache.init) {
return this.checkpointStateCache.init();
}
}
canAcceptWork(): boolean {
return this.jobQueue.jobLen < REGEN_CAN_ACCEPT_WORK_THRESHOLD;
}
dropCache(): void {
this.blockStateCache.clear();
this.checkpointStateCache.clear();
}
dumpCacheSummary(): routes.lodestar.StateCacheItem[] {
return [...this.blockStateCache.dumpSummary(), ...this.checkpointStateCache.dumpSummary()];
}
/**
* Get a state from block state cache.
*/
getStateSync(stateRoot: RootHex): IBeaconStateView | null {
return this.blockStateCache.get(stateRoot);
}
/**
* Get state for block processing.
*/
getPreStateSync(block: BeaconBlock): IBeaconStateView | null {
const parentRoot = toRootHex(block.parentRoot);
const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRoot);
if (!parentBlock) {
throw new RegenError({
code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
blockRoot: block.parentRoot,
});
}
const parentEpoch = computeEpochAtSlot(parentBlock.slot);
const blockEpoch = computeEpochAtSlot(block.slot);
// Check the checkpoint cache (if the pre-state is a checkpoint state)
if (parentEpoch < blockEpoch) {
const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch);
if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) {
return checkpointState;
}
}
// Check the state cache, only if the state doesn't need to go through an epoch transition.
// Otherwise the state transition may not be cached and wasted. Queue for regen since the
// work required will still be significant.
if (parentEpoch === blockEpoch) {
const state = this.blockStateCache.get(parentBlock.stateRoot);
if (state) {
return state;
}
}
return null;
}
async getCheckpointStateOrBytes(cp: CheckpointHex): Promise<IBeaconStateView | Uint8Array | null> {
return this.checkpointStateCache.getStateOrBytes(cp);
}
/**
* Get checkpoint state from cache
*/
getCheckpointStateSync(cp: CheckpointHex): IBeaconStateView | null {
return this.checkpointStateCache.get(cp);
}
/**
* Get state closest to head
*/
getClosestHeadState(head: ProtoBlock): IBeaconStateView | null {
return this.checkpointStateCache.getLatest(head.blockRoot, Infinity) || this.blockStateCache.get(head.stateRoot);
}
pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void {
this.checkpointStateCache.prune(finalizedEpoch, justifiedEpoch);
this.blockStateCache.prune(headStateRoot);
}
pruneOnFinalized(finalizedEpoch: number): void {
this.checkpointStateCache.pruneFinalized(finalizedEpoch);
this.blockStateCache.deleteAllBeforeEpoch(finalizedEpoch);
}
processState(blockRootHex: RootHex, postState: IBeaconStateView): void {
this.blockStateCache.add(postState);
this.checkpointStateCache.processState(blockRootHex, postState).catch((e) => {
this.logger.debug("Error processing block state", {blockRootHex, slot: postState.slot}, e);
});
}
addCheckpointState(cp: phase0.Checkpoint, item: IBeaconStateView): void {
this.checkpointStateCache.add(cp, item);
}
updateHeadState(newHead: ProtoBlock, maybeHeadState: IBeaconStateView): void {
const {stateRoot: newHeadStateRoot, blockRoot: newHeadBlockRoot, slot: newHeadSlot} = newHead;
const maybeHeadStateRoot = toRootHex(maybeHeadState.hashTreeRoot());
const logCtx = {
newHeadSlot,
newHeadBlockRoot,
newHeadStateRoot,
maybeHeadSlot: maybeHeadState.slot,
maybeHeadStateRoot,
};
const headState =
newHeadStateRoot === maybeHeadStateRoot ? maybeHeadState : this.blockStateCache.get(newHeadStateRoot);
if (headState) {
this.blockStateCache.setHeadState(headState);
} else {
// Trigger regen on head change if necessary
this.logger.warn("Head state not available, triggering regen", logCtx);
// for the old BlockStateCacheImpl only
// - head has changed, so the existing cached head state is no longer useful. Set strong reference to null to free
// up memory for regen step below. During regen, node won't be functional but eventually head will be available
// for the new FIFOBlockStateCache, this has no affect
this.blockStateCache.setHeadState(null);
// for the new FIFOBlockStateCache, it's important to reload state to regen head state here if needed
const allowDiskReload = true;
this.regen.getState(newHeadStateRoot, RegenCaller.processBlock, allowDiskReload).then(
(headStateRegen) => this.blockStateCache.setHeadState(headStateRegen),
(e) => this.logger.error("Error on head state regen", logCtx, e)
);
}
}
updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null {
return this.checkpointStateCache.updatePreComputedCheckpoint(rootHex, epoch);
}
/**
* Get the state to run with `block`.
* - State after `block.parentRoot` dialed forward to block.slot
*/
async getPreState(block: BeaconBlock, opts: StateRegenerationOpts, rCaller: RegenCaller): Promise<IBeaconStateView> {
this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getPreState});
// First attempt to fetch the state from caches before queueing
const cachedState = this.getPreStateSync(block);
if (cachedState !== null) {
return cachedState;
}
// The state is not immediately available in the caches, enqueue the job
this.metrics?.regenFnQueuedTotal.inc({caller: rCaller, entrypoint: RegenFnName.getPreState});
return this.jobQueue.push({key: "getPreState", args: [block, opts, rCaller]});
}
/**
* Get state of provided `blockRoot` and dial forward to `slot`
* Use this api with care because we don't want the queue to be busy
* For the context, gossip block validation uses this api so we want it to be as fast as possible
* @returns
*/
async getBlockSlotState(
block: ProtoBlock,
slot: Slot,
opts: StateRegenerationOpts,
rCaller: RegenCaller
): Promise<IBeaconStateView> {
this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getBlockSlotState});
// The state is not immediately available in the caches, enqueue the job
return this.jobQueue.push({key: "getBlockSlotState", args: [block, slot, opts, rCaller]});
}
async getState(stateRoot: RootHex, rCaller: RegenCaller): Promise<IBeaconStateView> {
this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getState});
// First attempt to fetch the state from cache before queueing
const state = this.blockStateCache.get(stateRoot);
if (state) {
return state;
}
// The state is not immediately available in the cache, enqueue the job
this.metrics?.regenFnQueuedTotal.inc({caller: rCaller, entrypoint: RegenFnName.getState});
return this.jobQueue.push({key: "getState", args: [stateRoot, rCaller]});
}
private jobQueueProcessor = async (regenRequest: RegenRequest): Promise<IBeaconStateView> => {
const metricsLabels = {
caller: regenRequest.args.at(-1) as RegenCaller,
entrypoint: regenRequest.key as RegenFnName,
};
let timer: (() => number) | undefined;
try {
timer = this.metrics?.regenFnCallDuration.startTimer(metricsLabels);
switch (regenRequest.key) {
case "getPreState":
return await this.regen.getPreState(...regenRequest.args);
case "getBlockSlotState":
return await this.regen.getBlockSlotState(...regenRequest.args);
case "getState":
return await this.regen.getState(...regenRequest.args);
}
} catch (e) {
this.metrics?.regenFnTotalErrors.inc(metricsLabels);
throw e;
} finally {
if (timer) timer();
}
};
}