UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

228 lines • 11 kB
import { computeEpochAtSlot } from "@lodestar/state-transition"; import { toRootHex } from "@lodestar/utils"; import { JobItemQueue } from "../../util/queue/index.js"; import { toCheckpointHex } from "../stateCache/index.js"; import { RegenError, RegenErrorCode } from "./errors.js"; import { RegenCaller, RegenFnName, } from "./interface.js"; import { 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; /** * 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 { constructor(modules) { this.jobQueueProcessor = async (regenRequest) => { const metricsLabels = { caller: regenRequest.args.at(-1), entrypoint: regenRequest.key, }; let timer; try { timer = this.metrics?.regenFnCallDuration.startTimer(metricsLabels); switch (regenRequest.key) { case "getPreState": return await this.regen.getPreState(...regenRequest.args); case "getCheckpointState": return await this.regen.getCheckpointState(...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(); } }; this.regen = new StateRegenerator(modules); this.jobQueue = new JobItemQueue(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() { if (this.checkpointStateCache.init) { return this.checkpointStateCache.init(); } } canAcceptWork() { return this.jobQueue.jobLen < REGEN_CAN_ACCEPT_WORK_THRESHOLD; } dropCache() { this.blockStateCache.clear(); this.checkpointStateCache.clear(); } dumpCacheSummary() { return [...this.blockStateCache.dumpSummary(), ...this.checkpointStateCache.dumpSummary()]; } /** * Get a state from block state cache. * This is not for block processing so don't transfer cache */ getStateSync(stateRoot) { return this.blockStateCache.get(stateRoot, { dontTransferCache: true }); } /** * Get state for block processing. * By default, do not transfer cache except for the block at clock slot * which is usually the gossip block. */ getPreStateSync(block, opts = { dontTransferCache: true }) { const parentRoot = toRootHex(block.parentRoot); const parentBlock = this.forkChoice.getBlockHex(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, opts); 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, opts); if (state) { return state; } } return null; } async getCheckpointStateOrBytes(cp) { return this.checkpointStateCache.getStateOrBytes(cp); } /** * Get checkpoint state from cache, this function is not for block processing so don't transfer cache */ getCheckpointStateSync(cp) { return this.checkpointStateCache.get(cp, { dontTransferCache: true }); } /** * Get state closest to head, this function is not for block processing so don't transfer cache */ getClosestHeadState(head) { const opts = { dontTransferCache: true }; return (this.checkpointStateCache.getLatest(head.blockRoot, Infinity, opts) || this.blockStateCache.get(head.stateRoot, opts)); } pruneOnCheckpoint(finalizedEpoch, justifiedEpoch, headStateRoot) { this.checkpointStateCache.prune(finalizedEpoch, justifiedEpoch); this.blockStateCache.prune(headStateRoot); } pruneOnFinalized(finalizedEpoch) { this.checkpointStateCache.pruneFinalized(finalizedEpoch); this.blockStateCache.deleteAllBeforeEpoch(finalizedEpoch); } processState(blockRootHex, postState) { 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, item) { this.checkpointStateCache.add(cp, item); } updateHeadState(newHead, maybeHeadState) { 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 : // maybeHeadState was already in block state cache so we don't transfer the cache this.blockStateCache.get(newHeadStateRoot, { dontTransferCache: true }); 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; // transfer cache here because we want to regen state asap const cloneOpts = { dontTransferCache: false }; this.regen.getState(newHeadStateRoot, RegenCaller.processBlock, cloneOpts, allowDiskReload).then((headStateRegen) => this.blockStateCache.setHeadState(headStateRegen), (e) => this.logger.error("Error on head state regen", logCtx, e)); } } updatePreComputedCheckpoint(rootHex, epoch) { 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, opts, rCaller) { this.metrics?.regenFnCallTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getPreState }); // First attempt to fetch the state from caches before queueing const cachedState = this.getPreStateSync(block, opts); 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] }); } async getCheckpointState(cp, opts, rCaller) { this.metrics?.regenFnCallTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getCheckpointState }); // First attempt to fetch the state from cache before queueing const checkpointState = this.checkpointStateCache.get(toCheckpointHex(cp), opts); if (checkpointState) { return checkpointState; } // The state is not immediately available in the caches, enqueue the job this.metrics?.regenFnQueuedTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getCheckpointState }); return this.jobQueue.push({ key: "getCheckpointState", args: [cp, 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(blockRoot, slot, opts, rCaller) { 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: [blockRoot, slot, opts, rCaller] }); } async getState(stateRoot, rCaller, opts = { dontTransferCache: true }) { 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, opts); 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, opts] }); } } //# sourceMappingURL=queued.js.map