diff --git a/packages/api/src/beacon/routes/lodestar.ts b/packages/api/src/beacon/routes/lodestar.ts index 85c79d82593..4ca8523b6ab 100644 --- a/packages/api/src/beacon/routes/lodestar.ts +++ b/packages/api/src/beacon/routes/lodestar.ts @@ -67,6 +67,7 @@ export type StateCacheItem = { reads: number; /** Unix timestamp (ms) of the last read */ lastRead: number; + checkpointState: boolean; }; export type LodestarNodePeer = NodePeer & { @@ -88,8 +89,6 @@ export type Api = { getBlockProcessorQueueItems(): Promise>; /** Dump a summary of the states in the StateContextCache */ getStateCacheItems(): Promise>; - /** Dump a summary of the states in the CheckpointStateCache */ - getCheckpointStateCacheItems(): Promise>; /** Dump peer gossip stats by peer */ getGossipPeerScoreStats(): Promise>; /** Dump lodestar score stats by peer */ @@ -132,7 +131,6 @@ export const routesData: RoutesData = { getRegenQueueItems: {url: "/eth/v1/lodestar/regen-queue-items", method: "GET"}, getBlockProcessorQueueItems: {url: "/eth/v1/lodestar/block-processor-queue-items", method: "GET"}, getStateCacheItems: {url: "/eth/v1/lodestar/state-cache-items", method: "GET"}, - getCheckpointStateCacheItems: {url: "/eth/v1/lodestar/checkpoint-state-cache-items", method: "GET"}, getGossipPeerScoreStats: {url: "/eth/v1/lodestar/gossip-peer-score-stats", method: "GET"}, getLodestarPeerScoreStats: {url: "/eth/v1/lodestar/lodestar-peer-score-stats", method: "GET"}, runGC: {url: "/eth/v1/lodestar/gc", method: "POST"}, @@ -153,7 +151,6 @@ export type ReqTypes = { getRegenQueueItems: ReqEmpty; getBlockProcessorQueueItems: ReqEmpty; getStateCacheItems: ReqEmpty; - getCheckpointStateCacheItems: ReqEmpty; getGossipPeerScoreStats: ReqEmpty; getLodestarPeerScoreStats: ReqEmpty; runGC: ReqEmpty; @@ -183,7 +180,6 @@ export function getReqSerializers(): ReqSerializers { getRegenQueueItems: reqEmpty, getBlockProcessorQueueItems: reqEmpty, getStateCacheItems: reqEmpty, - getCheckpointStateCacheItems: reqEmpty, getGossipPeerScoreStats: reqEmpty, getLodestarPeerScoreStats: reqEmpty, runGC: reqEmpty, @@ -222,7 +218,6 @@ export function getReturnTypes(): ReturnTypes { getRegenQueueItems: jsonType("snake"), getBlockProcessorQueueItems: jsonType("snake"), getStateCacheItems: jsonType("snake"), - getCheckpointStateCacheItems: jsonType("snake"), getGossipPeerScoreStats: jsonType("snake"), getLodestarPeerScoreStats: jsonType("snake"), getPeers: jsonType("snake"), diff --git a/packages/beacon-node/src/api/impl/lodestar/index.ts b/packages/beacon-node/src/api/impl/lodestar/index.ts index 4aaf730fce0..4da8b723f2a 100644 --- a/packages/beacon-node/src/api/impl/lodestar/index.ts +++ b/packages/beacon-node/src/api/impl/lodestar/index.ts @@ -86,11 +86,7 @@ export function getLodestarApi({ }, async getStateCacheItems() { - return {data: (chain as BeaconChain)["stateCache"].dumpSummary()}; - }, - - async getCheckpointStateCacheItems() { - return {data: (chain as BeaconChain)["checkpointStateCache"].dumpSummary()}; + return {data: chain.regen.dumpCacheSummary()}; }, async getGossipPeerScoreStats() { @@ -109,8 +105,7 @@ export function getLodestarApi({ }, async dropStateCache() { - chain.stateCache.clear(); - chain.checkpointStateCache.clear(); + chain.regen.dropCache(); }, async connectPeer(peerIdStr, multiaddrStrs) { diff --git a/packages/beacon-node/src/chain/archiver/archiveStates.ts b/packages/beacon-node/src/chain/archiver/archiveStates.ts index 59873c34aa4..98b083b0513 100644 --- a/packages/beacon-node/src/chain/archiver/archiveStates.ts +++ b/packages/beacon-node/src/chain/archiver/archiveStates.ts @@ -4,7 +4,7 @@ import {Slot, Epoch} from "@lodestar/types"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {CheckpointWithHex} from "@lodestar/fork-choice"; import {IBeaconDb} from "../../db/index.js"; -import {CheckpointStateCache} from "../stateCache/index.js"; +import {IStateRegenerator} from "../regen/interface.js"; /** * Minimum number of epochs between single temp archived states @@ -26,7 +26,7 @@ export interface StatesArchiverOpts { */ export class StatesArchiver { constructor( - private readonly checkpointStateCache: CheckpointStateCache, + private readonly regen: IStateRegenerator, private readonly db: IBeaconDb, private readonly logger: Logger, private readonly opts: StatesArchiverOpts @@ -83,7 +83,7 @@ export class StatesArchiver { * Only the new finalized state is stored to disk */ async archiveState(finalized: CheckpointWithHex): Promise { - const finalizedState = this.checkpointStateCache.get(finalized); + const finalizedState = this.regen.getCheckpointStateSync(finalized); if (!finalizedState) { throw Error("No state in cache for finalized checkpoint state epoch #" + finalized.epoch); } diff --git a/packages/beacon-node/src/chain/archiver/index.ts b/packages/beacon-node/src/chain/archiver/index.ts index 7fd336cf052..20ee31da797 100644 --- a/packages/beacon-node/src/chain/archiver/index.ts +++ b/packages/beacon-node/src/chain/archiver/index.ts @@ -7,8 +7,8 @@ import {IBeaconDb} from "../../db/index.js"; import {JobItemQueue} from "../../util/queue/index.js"; import {IBeaconChain} from "../interface.js"; import {ChainEvent} from "../emitter.js"; -import {CheckpointStateCache} from "../stateCache/index.js"; import {Metrics} from "../../metrics/metrics.js"; +import {IStateRegenerator} from "../regen/interface.js"; import {StatesArchiver, StatesArchiverOpts} from "./archiveStates.js"; import {archiveBlocks, FinalizedData} from "./archiveBlocks.js"; @@ -51,7 +51,7 @@ export class Archiver { opts: ArchiverOpts, private readonly metrics: Metrics | null ) { - this.statesArchiver = new StatesArchiver(chain.checkpointStateCache, db, logger, opts); + this.statesArchiver = new StatesArchiver(chain.regen, db, logger, opts); this.prevFinalized = chain.forkChoice.getFinalizedCheckpoint(); this.jobQueue = new JobItemQueue<[CheckpointWithHex], void>(this.processFinalizedCheckpoint, { maxLength: PROCESS_FINALIZED_CHECKPOINT_QUEUE_LEN, @@ -84,11 +84,11 @@ export class Archiver { private onCheckpoint = (): void => { const headStateRoot = this.chain.forkChoice.getHead().stateRoot; - this.chain.checkpointStateCache.prune( + this.chain.regen.pruneOnCheckpoint( this.chain.forkChoice.getFinalizedCheckpoint().epoch, - this.chain.forkChoice.getJustifiedCheckpoint().epoch + this.chain.forkChoice.getJustifiedCheckpoint().epoch, + headStateRoot ); - this.chain.stateCache.prune(headStateRoot); }; private processFinalizedCheckpoint = async (finalized: CheckpointWithHex): Promise => { @@ -105,7 +105,7 @@ export class Archiver { this.chain.clock.currentEpoch ); this.collectFinalizedProposalStats( - this.chain.checkpointStateCache, + this.chain.regen, this.chain.forkChoice, this.chain.beaconProposerCache, finalizedData, @@ -117,8 +117,8 @@ export class Archiver { // should be after ArchiveBlocksTask to handle restart cleanly await this.statesArchiver.maybeArchiveState(finalized); - this.chain.checkpointStateCache.pruneFinalized(finalizedEpoch); - this.chain.stateCache.deleteAllBeforeEpoch(finalizedEpoch); + this.chain.regen.pruneOnFinalized(finalizedEpoch); + // tasks rely on extended fork choice this.chain.forkChoice.prune(finalized.rootHex); await this.updateBackfillRange(finalized); @@ -176,7 +176,7 @@ export class Archiver { }; private collectFinalizedProposalStats( - checkpointStateCache: CheckpointStateCache, + regen: IStateRegenerator, forkChoice: IForkChoice, beaconProposerCache: IBeaconChain["beaconProposerCache"], finalizedData: FinalizedData, @@ -241,7 +241,7 @@ export class Archiver { let finalizedAttachedValidatorsMissedCount = 0; for (const checkpointHex of finalizedProposersCheckpoints) { - const checkpointState = checkpointStateCache.get(checkpointHex); + const checkpointState = regen.getCheckpointStateSync(checkpointHex); // Generate stats for attached validators if we have state info if (checkpointState !== null) { diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index c5ed23c5624..32ce987735d 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -17,7 +17,6 @@ import {isOptimisticBlock} from "../../util/forkChoice.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; import {ChainEvent, ReorgEventData} from "../emitter.js"; import {REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC} from "../reprocess.js"; -import {RegenCaller} from "../regen/interface.js"; import type {BeaconChain} from "../chain.js"; import {FullyVerifiedBlock, ImportBlockOpts, AttestationImportOpt} from "./types.js"; import {getCheckpointFromState} from "./utils/checkpoint.js"; @@ -82,7 +81,7 @@ export async function importBlock( // This adds the state necessary to process the next block // Some block event handlers require state being in state cache so need to do this before emitting EventType.block - this.stateCache.add(postState); + this.regen.addPostState(postState); this.metrics?.importBlock.bySource.inc({source}); this.logger.verbose("Added block to forkchoice and state cache", {slot: block.message.slot, root: blockRootHex}); @@ -198,21 +197,7 @@ export async function importBlock( if (newHead.blockRoot !== oldHead.blockRoot) { // Set head state as strong reference - const headState = - newHead.stateRoot === toHexString(postState.hashTreeRoot()) ? postState : this.stateCache.get(newHead.stateRoot); - if (headState) { - this.stateCache.setHeadState(headState); - } else { - // Trigger regen on head change if necessary - this.logger.warn("Head state not available, triggering regen", {stateRoot: newHead.stateRoot}); - // 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 - this.stateCache.setHeadState(null); - this.regen.getState(newHead.stateRoot, RegenCaller.processBlock).then( - (headStateRegen) => this.stateCache.setHeadState(headStateRegen), - (e) => this.logger.error("Error on head state regen", {}, e) - ); - } + this.regen.updateHeadState(newHead.stateRoot, postState); this.emitter.emit(routes.events.EventType.head, { block: newHead.blockRoot, @@ -337,7 +322,7 @@ export async function importBlock( // Cache state to preserve epoch transition work const checkpointState = postState; const cp = getCheckpointFromState(checkpointState); - this.checkpointStateCache.add(cp, checkpointState); + this.regen.addCheckpointState(cp, checkpointState); this.emitter.emit(ChainEvent.checkpoint, cp, checkpointState); // Note: in-lined code from previos handler of ChainEvent.checkpoint diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 99773296e37..edd3331c190 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -103,8 +103,6 @@ export class BeaconChain implements IBeaconChain { readonly forkChoice: IForkChoice; readonly clock: IClock; readonly emitter: ChainEventEmitter; - readonly stateCache: StateContextCache; - readonly checkpointStateCache: CheckpointStateCache; readonly regen: QueuedStateRegenerator; readonly lightClientServer: LightClientServer; readonly reprocessController: ReprocessController; @@ -260,6 +258,7 @@ export class BeaconChain implements IBeaconChain { checkpointStateCache, db, metrics, + logger, emitter, signal, }); @@ -274,8 +273,6 @@ export class BeaconChain implements IBeaconChain { this.clock = clock; this.regen = regen; this.bls = bls; - this.checkpointStateCache = checkpointStateCache; - this.stateCache = stateCache; this.emitter = emitter; this.lightClientServer = lightClientServer; @@ -298,8 +295,6 @@ export class BeaconChain implements IBeaconChain { async close(): Promise { this.abortController.abort(); - this.stateCache.clear(); - this.checkpointStateCache.clear(); await this.bls.close(); } @@ -344,8 +339,7 @@ export class BeaconChain implements IBeaconChain { getHeadState(): CachedBeaconStateAllForks { // head state should always exist const head = this.forkChoice.getHead(); - const headState = - this.checkpointStateCache.getLatest(head.blockRoot, Infinity) || this.stateCache.get(head.stateRoot); + const headState = this.regen.getClosestHeadState(head); if (!headState) { throw Error(`headState does not exist for head root=${head.blockRoot} slot=${head.slot}`); } @@ -397,7 +391,7 @@ export class BeaconChain implements IBeaconChain { return null; } - const state = this.stateCache.get(block.stateRoot); + const state = this.regen.getStateSync(block.stateRoot); return state && {state, executionOptimistic: isOptimisticBlock(block)}; } } else { @@ -424,7 +418,7 @@ export class BeaconChain implements IBeaconChain { // - 1 every 100s of states that are persisted in the archive state // TODO: This is very inneficient for debug requests of serialized content, since it deserializes to serialize again - const cachedStateCtx = this.stateCache.get(stateRoot); + const cachedStateCtx = this.regen.getStateSync(stateRoot); if (cachedStateCtx) { const block = this.forkChoice.getBlock(cachedStateCtx.latestBlockHeader.hashTreeRoot()); return {state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block)}; @@ -679,7 +673,7 @@ export class BeaconChain implements IBeaconChain { checkpoint: CheckpointWithHex, blockState: CachedBeaconStateAllForks ): {state: CachedBeaconStateAllForks; stateId: string; shouldWarn: boolean} { - const state = this.checkpointStateCache.get(checkpoint); + const state = this.regen.getCheckpointStateSync(checkpoint); if (state) { return {state, stateId: "checkpoint_state", shouldWarn: false}; } @@ -692,7 +686,7 @@ export class BeaconChain implements IBeaconChain { // Find a state in the same branch of checkpoint at same epoch. Balances should exactly the same for (const descendantBlock of this.forkChoice.forwardIterateDescendants(checkpoint.rootHex)) { if (computeEpochAtSlot(descendantBlock.slot) === checkpoint.epoch) { - const descendantBlockState = this.stateCache.get(descendantBlock.stateRoot); + const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot); if (descendantBlockState) { return {state: descendantBlockState, stateId: "descendant_state_same_epoch", shouldWarn: true}; } @@ -708,7 +702,7 @@ export class BeaconChain implements IBeaconChain { // Note: must call .forwardIterateDescendants() again since nodes are not sorted for (const descendantBlock of this.forkChoice.forwardIterateDescendants(checkpoint.rootHex)) { if (computeEpochAtSlot(descendantBlock.slot) > checkpoint.epoch) { - const descendantBlockState = this.stateCache.get(descendantBlock.stateRoot); + const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot); if (descendantBlockState) { return {state: blockState, stateId: "descendant_state_latter_epoch", shouldWarn: true}; } @@ -847,8 +841,8 @@ export class BeaconChain implements IBeaconChain { this.seenBlockProposers.prune(computeStartSlotAtEpoch(cp.epoch)); // TODO: Improve using regen here - const headState = this.stateCache.get(this.forkChoice.getHead().stateRoot); - const finalizedState = this.checkpointStateCache.get(cp); + const headState = this.regen.getStateSync(this.forkChoice.getHead().stateRoot); + const finalizedState = this.regen.getCheckpointStateSync(cp); if (headState) { this.opPool.pruneAll(headState, finalizedState); } diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 179aa4b03e9..3956aeeb6a3 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -28,7 +28,6 @@ import {Metrics} from "../metrics/metrics.js"; import {IClock} from "../util/clock.js"; import {ChainEventEmitter} from "./emitter.js"; import {IStateRegenerator, RegenCaller} from "./regen/index.js"; -import {StateContextCache, CheckpointStateCache} from "./stateCache/index.js"; import {IBlsVerifier} from "./bls/index.js"; import { SeenAttesters, @@ -80,8 +79,6 @@ export interface IBeaconChain { readonly forkChoice: IForkChoice; readonly clock: IClock; readonly emitter: ChainEventEmitter; - readonly stateCache: StateContextCache; - readonly checkpointStateCache: CheckpointStateCache; readonly regen: IStateRegenerator; readonly lightClientServer: LightClientServer; readonly reprocessController: ReprocessController; diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index c6f25b1bfc1..040783185a0 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -108,7 +108,7 @@ export class PrepareNextSlotScheduler { // + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations if (isEpochTransition) { this.metrics?.precomputeNextEpochTransition.count.inc({result: "success"}, 1); - const previousHits = this.chain.checkpointStateCache.updatePreComputedCheckpoint(headRoot, nextEpoch); + const previousHits = this.chain.regen.updatePreComputedCheckpoint(headRoot, nextEpoch); if (previousHits === 0) { this.metrics?.precomputeNextEpochTransition.waste.inc(); } diff --git a/packages/beacon-node/src/chain/regen/interface.ts b/packages/beacon-node/src/chain/regen/interface.ts index 98d00558900..a0989f30c41 100644 --- a/packages/beacon-node/src/chain/regen/interface.ts +++ b/packages/beacon-node/src/chain/regen/interface.ts @@ -1,5 +1,8 @@ -import {allForks, phase0, Slot, RootHex} from "@lodestar/types"; +import {allForks, phase0, Slot, RootHex, Epoch} from "@lodestar/types"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {routes} from "@lodestar/api"; +import {ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointHex} from "../stateCache/index.js"; export enum RegenCaller { getDuties = "getDuties", @@ -27,10 +30,24 @@ export type StateCloneOpts = { dontTransferCache: boolean; }; +export interface IStateRegenerator extends IStateRegeneratorInternal { + dropCache(): void; + dumpCacheSummary(): routes.lodestar.StateCacheItem[]; + getStateSync(stateRoot: RootHex): CachedBeaconStateAllForks | null; + getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null; + getClosestHeadState(head: ProtoBlock): CachedBeaconStateAllForks | null; + pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void; + pruneOnFinalized(finalizedEpoch: Epoch): void; + addPostState(postState: CachedBeaconStateAllForks): void; + addCheckpointState(cp: phase0.Checkpoint, item: CachedBeaconStateAllForks): void; + updateHeadState(newHeadStateRoot: RootHex, maybeHeadState: CachedBeaconStateAllForks): void; + updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null; +} + /** * Regenerates states that have already been processed by the fork choice */ -export interface IStateRegenerator { +export interface IStateRegeneratorInternal { /** * Return a valid pre-state for a beacon block * This will always return a state in the latest viable epoch diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index 9c4f8f97bdf..c92a95428ad 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -1,11 +1,13 @@ -import {phase0, Slot, allForks, RootHex} from "@lodestar/types"; -import {IForkChoice} from "@lodestar/fork-choice"; +import {phase0, Slot, allForks, RootHex, Epoch} from "@lodestar/types"; +import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {CachedBeaconStateAllForks, computeEpochAtSlot} from "@lodestar/state-transition"; import {toHexString} from "@chainsafe/ssz"; -import {CheckpointStateCache, StateContextCache, toCheckpointHex} from "../stateCache/index.js"; +import {Logger} from "@lodestar/utils"; +import {routes} from "@lodestar/api"; +import {CheckpointHex, CheckpointStateCache, StateContextCache, toCheckpointHex} from "../stateCache/index.js"; import {Metrics} from "../../metrics/index.js"; import {JobItemQueue} from "../../util/queue/index.js"; -import {IStateRegenerator, RegenCaller, RegenFnName, StateCloneOpts} from "./interface.js"; +import {IStateRegenerator, IStateRegeneratorInternal, RegenCaller, RegenFnName, StateCloneOpts} from "./interface.js"; import {StateRegenerator, RegenModules} from "./regen.js"; import {RegenError, RegenErrorCode} from "./errors.js"; @@ -15,10 +17,11 @@ const REGEN_CAN_ACCEPT_WORK_THRESHOLD = 16; type QueuedStateRegeneratorModules = RegenModules & { signal: AbortSignal; + logger: Logger; }; -type RegenRequestKey = keyof IStateRegenerator; -type RegenRequestByKey = {[K in RegenRequestKey]: {key: K; args: Parameters}}; +type RegenRequestKey = keyof IStateRegeneratorInternal; +type RegenRequestByKey = {[K in RegenRequestKey]: {key: K; args: Parameters}}; export type RegenRequest = RegenRequestByKey[RegenRequestKey]; /** @@ -28,12 +31,13 @@ export type RegenRequest = RegenRequestByKey[RegenRequestKey]; */ export class QueuedStateRegenerator implements IStateRegenerator { readonly jobQueue: JobItemQueue<[RegenRequest], CachedBeaconStateAllForks>; - private regen: StateRegenerator; + private readonly regen: StateRegenerator; - private forkChoice: IForkChoice; - private stateCache: StateContextCache; - private checkpointStateCache: CheckpointStateCache; - private metrics: Metrics | null; + private readonly forkChoice: IForkChoice; + private readonly stateCache: StateContextCache; + private readonly checkpointStateCache: CheckpointStateCache; + private readonly metrics: Metrics | null; + private readonly logger: Logger; constructor(modules: QueuedStateRegeneratorModules) { this.regen = new StateRegenerator(modules); @@ -46,12 +50,77 @@ export class QueuedStateRegenerator implements IStateRegenerator { this.stateCache = modules.stateCache; this.checkpointStateCache = modules.checkpointStateCache; this.metrics = modules.metrics; + this.logger = modules.logger; } canAcceptWork(): boolean { return this.jobQueue.jobLen < REGEN_CAN_ACCEPT_WORK_THRESHOLD; } + dropCache(): void { + this.stateCache.clear(); + this.checkpointStateCache.clear(); + } + + dumpCacheSummary(): routes.lodestar.StateCacheItem[] { + return [...this.stateCache.dumpSummary(), ...this.checkpointStateCache.dumpSummary()]; + } + + getStateSync(stateRoot: RootHex): CachedBeaconStateAllForks | null { + return this.stateCache.get(stateRoot); + } + + getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null { + return this.checkpointStateCache.get(cp); + } + + getClosestHeadState(head: ProtoBlock): CachedBeaconStateAllForks | null { + return this.checkpointStateCache.getLatest(head.blockRoot, Infinity) || this.stateCache.get(head.stateRoot); + } + + pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void { + this.checkpointStateCache.prune(finalizedEpoch, justifiedEpoch); + this.stateCache.prune(headStateRoot); + } + + pruneOnFinalized(finalizedEpoch: number): void { + this.checkpointStateCache.pruneFinalized(finalizedEpoch); + this.stateCache.deleteAllBeforeEpoch(finalizedEpoch); + } + + addPostState(postState: CachedBeaconStateAllForks): void { + this.stateCache.add(postState); + } + + addCheckpointState(cp: phase0.Checkpoint, item: CachedBeaconStateAllForks): void { + this.checkpointStateCache.add(cp, item); + } + + updateHeadState(newHeadStateRoot: RootHex, maybeHeadState: CachedBeaconStateAllForks): void { + const headState = + newHeadStateRoot === toHexString(maybeHeadState.hashTreeRoot()) + ? maybeHeadState + : this.stateCache.get(newHeadStateRoot); + + if (headState) { + this.stateCache.setHeadState(headState); + } else { + // Trigger regen on head change if necessary + this.logger.warn("Head state not available, triggering regen", {stateRoot: newHeadStateRoot}); + // 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 + this.stateCache.setHeadState(null); + this.regen.getState(newHeadStateRoot, RegenCaller.processBlock).then( + (headStateRegen) => this.stateCache.setHeadState(headStateRegen), + (e) => this.logger.error("Error on head state regen", {}, 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 diff --git a/packages/beacon-node/src/chain/regen/regen.ts b/packages/beacon-node/src/chain/regen/regen.ts index 814a803c8a0..90145974ba6 100644 --- a/packages/beacon-node/src/chain/regen/regen.ts +++ b/packages/beacon-node/src/chain/regen/regen.ts @@ -18,7 +18,7 @@ import {IBeaconDb} from "../../db/index.js"; import {CheckpointStateCache, StateContextCache} from "../stateCache/index.js"; import {getCheckpointFromState} from "../blocks/utils/checkpoint.js"; import {ChainEvent, ChainEventEmitter} from "../emitter.js"; -import {IStateRegenerator, RegenCaller, StateCloneOpts} from "./interface.js"; +import {IStateRegeneratorInternal, RegenCaller, StateCloneOpts} from "./interface.js"; import {RegenError, RegenErrorCode} from "./errors.js"; export type RegenModules = { @@ -34,7 +34,7 @@ export type RegenModules = { /** * Regenerates states that have already been processed by the fork choice */ -export class StateRegenerator implements IStateRegenerator { +export class StateRegenerator implements IStateRegeneratorInternal { constructor(private readonly modules: RegenModules) {} /** diff --git a/packages/beacon-node/src/chain/stateCache/stateContextCache.ts b/packages/beacon-node/src/chain/stateCache/stateContextCache.ts index 688a6f218aa..44523abf799 100644 --- a/packages/beacon-node/src/chain/stateCache/stateContextCache.ts +++ b/packages/beacon-node/src/chain/stateCache/stateContextCache.ts @@ -122,6 +122,7 @@ export class StateContextCache { root: toHexString(state.hashTreeRoot()), reads: this.cache.readCount.get(key) ?? 0, lastRead: this.cache.lastRead.get(key) ?? 0, + checkpointState: false, })); } diff --git a/packages/beacon-node/src/chain/stateCache/stateContextCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/stateContextCheckpointsCache.ts index 660df3676da..0cb48f0e2de 100644 --- a/packages/beacon-node/src/chain/stateCache/stateContextCheckpointsCache.ts +++ b/packages/beacon-node/src/chain/stateCache/stateContextCheckpointsCache.ts @@ -6,7 +6,7 @@ import {routes} from "@lodestar/api"; import {Metrics} from "../../metrics/index.js"; import {MapTracker} from "./mapMetrics.js"; -type CheckpointHex = {epoch: Epoch; rootHex: RootHex}; +export type CheckpointHex = {epoch: Epoch; rootHex: RootHex}; const MAX_EPOCHS = 10; /** @@ -140,6 +140,7 @@ export class CheckpointStateCache { root: toHexString(state.hashTreeRoot()), reads: this.cache.readCount.get(key) ?? 0, lastRead: this.cache.lastRead.get(key) ?? 0, + checkpointState: true, })); } diff --git a/packages/beacon-node/test/e2e/chain/lightclient.test.ts b/packages/beacon-node/test/e2e/chain/lightclient.test.ts index 470f56d6366..8c07ed8d813 100644 --- a/packages/beacon-node/test/e2e/chain/lightclient.test.ts +++ b/packages/beacon-node/test/e2e/chain/lightclient.test.ts @@ -141,7 +141,7 @@ describe("chain / lightclient", function () { // Test fetching proofs const {proof, header} = await getHeadStateProof(lightclient, api, [["latestBlockHeader", "bodyRoot"]]); const stateRootHex = toHexString(header.beacon.stateRoot); - const lcHeadState = bn.chain.stateCache.get(stateRootHex); + const lcHeadState = bn.chain.regen.getStateSync(stateRootHex); if (!lcHeadState) { throw Error(`LC head state not in cache ${stateRootHex}`); } diff --git a/packages/beacon-node/test/unit/api/impl/events/events.test.ts b/packages/beacon-node/test/unit/api/impl/events/events.test.ts index ea3994a134f..798ee1be9bc 100644 --- a/packages/beacon-node/test/unit/api/impl/events/events.test.ts +++ b/packages/beacon-node/test/unit/api/impl/events/events.test.ts @@ -1,27 +1,21 @@ import {expect} from "chai"; -import sinon, {SinonStubbedInstance} from "sinon"; +import sinon from "sinon"; import {routes} from "@lodestar/api"; import {config} from "@lodestar/config/default"; import {ssz} from "@lodestar/types"; import {BeaconChain, ChainEventEmitter, HeadEventData} from "../../../../../src/chain/index.js"; import {getEventsApi} from "../../../../../src/api/impl/events/index.js"; -import {generateProtoBlock} from "../../../../utils/typeGenerator.js"; -import {generateCachedState} from "../../../../utils/state.js"; -import {StateContextCache} from "../../../../../src/chain/stateCache/index.js"; import {StubbedChainMutable} from "../../../../utils/stub/index.js"; import {ZERO_HASH_HEX} from "../../../../../src/constants/constants.js"; describe("Events api impl", function () { describe("beacon event stream", function () { - let chainStub: StubbedChainMutable<"stateCache" | "emitter">; - let stateCacheStub: SinonStubbedInstance; + let chainStub: StubbedChainMutable<"regen" | "emitter">; let chainEventEmmitter: ChainEventEmitter; let api: ReturnType; beforeEach(function () { chainStub = sinon.createStubInstance(BeaconChain) as typeof chainStub; - stateCacheStub = sinon.createStubInstance(StateContextCache); - chainStub.stateCache = stateCacheStub as unknown as StateContextCache; chainEventEmmitter = new ChainEventEmitter(); chainStub.emitter = chainEventEmmitter; api = getEventsApi({config, chain: chainStub}); @@ -52,8 +46,6 @@ describe("Events api impl", function () { it("should ignore not sent topics", async function () { const events = getEvents([routes.events.EventType.head]); - const headBlock = generateProtoBlock(); - stateCacheStub.get.withArgs(headBlock.stateRoot).returns(generateCachedState({slot: 1000})); chainEventEmmitter.emit(routes.events.EventType.attestation, ssz.phase0.Attestation.defaultValue()); chainEventEmmitter.emit(routes.events.EventType.head, headEventData); diff --git a/packages/beacon-node/test/unit/chain/archive/collectFinalizedProposalStats.test.ts b/packages/beacon-node/test/unit/chain/archive/collectFinalizedProposalStats.test.ts index 666ffd18993..8039efd047f 100644 --- a/packages/beacon-node/test/unit/chain/archive/collectFinalizedProposalStats.test.ts +++ b/packages/beacon-node/test/unit/chain/archive/collectFinalizedProposalStats.test.ts @@ -1,34 +1,36 @@ import {expect} from "chai"; import sinon from "sinon"; -import {RootHex, Slot, Epoch, ValidatorIndex} from "@lodestar/types"; +import {Slot, Epoch, ValidatorIndex} from "@lodestar/types"; import {ForkChoice, ProtoBlock, CheckpointWithHex} from "@lodestar/fork-choice"; -import {fromHexString} from "@chainsafe/ssz"; +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {ZERO_HASH_HEX, ZERO_HASH} from "../../../../src/constants/index.js"; import {StubbedBeaconDb, StubbedChainMutable} from "../../../utils/stub/index.js"; import {testLogger} from "../../../utils/logger.js"; import {Archiver, FinalizedStats} from "../../../../src/chain/archiver/index.js"; import {FinalizedData} from "../../../../src/chain/archiver/archiveBlocks.js"; -import {BeaconChain, CheckpointStateCache} from "../../../../src/chain/index.js"; +import {BeaconChain, CheckpointHex} from "../../../../src/chain/index.js"; import {BeaconProposerCache} from "../../../../src/chain/beaconProposerCache.js"; import {generateCachedState} from "../../../utils/state.js"; +import {QueuedStateRegenerator} from "../../../../src/chain/regen/queued.js"; describe("collectFinalizedProposalStats", function () { const logger = testLogger(); - let chainStub: StubbedChainMutable< - "forkChoice" | "stateCache" | "emitter" | "beaconProposerCache" | "checkpointStateCache" - >; - + let chainStub: StubbedChainMutable<"forkChoice" | "emitter" | "beaconProposerCache" | "regen">; let dbStub: StubbedBeaconDb; + let cpStateCache: Map; // let beaconProposerCacheStub = SinonStubbedInstance & BeaconProposerCache; let archiver: Archiver; beforeEach(function () { + cpStateCache = new Map(); + const regen = sinon.createStubInstance(QueuedStateRegenerator); + regen.getCheckpointStateSync.callsFake((cp) => cpStateCache.get(cpKey(cp)) ?? null); chainStub = sinon.createStubInstance(BeaconChain) as typeof chainStub; chainStub.forkChoice = sinon.createStubInstance(ForkChoice); const suggestedFeeRecipient = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; chainStub.beaconProposerCache = new BeaconProposerCache({suggestedFeeRecipient}); - chainStub.checkpointStateCache = new CheckpointStateCache({}); + chainStub.regen = regen; const controller = new AbortController(); dbStub = new StubbedBeaconDb(); @@ -156,14 +158,14 @@ describe("collectFinalizedProposalStats", function () { const finalized = makeCheckpoint(finalizedEpoch); addtoBeaconCache(chainStub["beaconProposerCache"], finalized.epoch, attachedValidators); - addDummyStateCache(chainStub["checkpointStateCache"], prevFinalized, allValidators); + addDummyStateCache(prevFinalized, allValidators); finalizedCanonicalCheckpoints.forEach((eachCheckpoint) => { - addDummyStateCache(chainStub["checkpointStateCache"], eachCheckpoint, allValidators); + addDummyStateCache(eachCheckpoint, allValidators); }); const finalizedData = {finalizedCanonicalCheckpoints, finalizedCanonicalBlocks, finalizedNonCanonicalBlocks}; const processedStats = archiver["collectFinalizedProposalStats"]( - chainStub.checkpointStateCache, + chainStub.regen, chainStub.forkChoice, chainStub.beaconProposerCache, finalizedData as FinalizedData, @@ -174,6 +176,17 @@ describe("collectFinalizedProposalStats", function () { expect(expectedStats).to.deep.equal(processedStats); }); } + + function addDummyStateCache(checkpoint: CheckpointHex, proposers: number[]): void { + const checkpointstate = generateCachedState(); + checkpointstate.epochCtx.proposers = proposers; + checkpointstate.epochCtx.epoch = checkpoint.epoch; + cpStateCache.set(cpKey(checkpoint), checkpointstate); + } + + function cpKey(cp: CheckpointHex): string { + return JSON.stringify(cp); + } }); function makeBlock(slot: Slot): ProtoBlock { @@ -191,16 +204,3 @@ function addtoBeaconCache(cache: BeaconProposerCache, epoch: number, proposers: cache.add(epoch, {validatorIndex: `${eachProposer}`, feeRecipient: suggestedFeeRecipient}); }); } - -function addDummyStateCache( - checkpointStateCache: BeaconChain["checkpointStateCache"], - checkpoint: {epoch: number; rootHex: RootHex}, - proposers: number[] -): void { - const rootCP = {epoch: checkpoint.epoch, root: fromHexString(checkpoint.rootHex)}; - - const checkpointstate = generateCachedState(); - checkpointstate.epochCtx.proposers = proposers; - checkpointstate.epochCtx.epoch = checkpoint.epoch; - checkpointStateCache.add(rootCP, checkpointstate); -} diff --git a/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts b/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts index afe668f87de..0d88e015c7c 100644 --- a/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts +++ b/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts @@ -11,7 +11,7 @@ import {IBeaconChain} from "../../../src/chain/interface.js"; import {IChainOptions} from "../../../src/chain/options.js"; import {Clock} from "../../../src/util/clock.js"; import {PrepareNextSlotScheduler} from "../../../src/chain/prepareNextSlot.js"; -import {StateRegenerator} from "../../../src/chain/regen/index.js"; +import {QueuedStateRegenerator} from "../../../src/chain/regen/index.js"; import {SinonStubFn} from "../../utils/types.js"; import {generateCachedBellatrixState} from "../../utils/state.js"; import {BeaconProposerCache} from "../../../src/chain/beaconProposerCache.js"; @@ -31,7 +31,7 @@ describe("PrepareNextSlot scheduler", () => { let chainStub: StubbedChain; let scheduler: PrepareNextSlotScheduler; let forkChoiceStub: SinonStubbedInstance & ForkChoice; - let regenStub: SinonStubbedInstance & StateRegenerator; + let regenStub: SinonStubbedInstance & QueuedStateRegenerator; let loggerStub: SinonStubbedInstance & LoggerNode; let beaconProposerCacheStub: SinonStubbedInstance & BeaconProposerCache; let getForkStub: SinonStubFn<(typeof config)["getForkName"]>; @@ -49,8 +49,8 @@ describe("PrepareNextSlot scheduler", () => { chainStub.forkChoice = forkChoiceStub; const emitter = new ChainEventEmitter(); chainStub.emitter = emitter; - regenStub = sandbox.createStubInstance(StateRegenerator) as SinonStubbedInstance & - StateRegenerator; + regenStub = sandbox.createStubInstance(QueuedStateRegenerator) as SinonStubbedInstance & + QueuedStateRegenerator; chainStub.regen = regenStub; loggerStub = createStubbedLogger(sandbox); beaconProposerCacheStub = sandbox.createStubInstance( diff --git a/packages/beacon-node/test/unit/chain/validation/attestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation.test.ts index 24cdd90042d..007882d5a60 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation.test.ts @@ -20,6 +20,7 @@ import {getAttestationValidData, AttestationValidDataOpts} from "../../../utils/ import {IStateRegenerator, RegenCaller} from "../../../../src/chain/regen/interface.js"; import {StateRegenerator} from "../../../../src/chain/regen/regen.js"; import {ZERO_HASH_HEX} from "../../../../src/constants/constants.js"; +import {QueuedStateRegenerator} from "../../../../src/chain/regen/queued.js"; describe("chain / validation / attestation", () => { const vc = 64; @@ -296,12 +297,12 @@ describe("getStateForAttestationVerification", () => { // eslint-disable-next-line @typescript-eslint/naming-convention const config = createChainForkConfig({...defaultChainConfig, CAPELLA_FORK_EPOCH: 2}); const sandbox = sinon.createSandbox(); - let regenStub: SinonStubbedInstance & StateRegenerator; + let regenStub: SinonStubbedInstance & QueuedStateRegenerator; let chain: IBeaconChain; beforeEach(() => { - regenStub = sandbox.createStubInstance(StateRegenerator) as SinonStubbedInstance & - StateRegenerator; + regenStub = sandbox.createStubInstance(QueuedStateRegenerator) as SinonStubbedInstance & + QueuedStateRegenerator; chain = { config: config as BeaconConfig, regen: regenStub, diff --git a/packages/beacon-node/test/unit/chain/validation/block.test.ts b/packages/beacon-node/test/unit/chain/validation/block.test.ts index 116a415637f..297c9c90d6c 100644 --- a/packages/beacon-node/test/unit/chain/validation/block.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/block.test.ts @@ -5,7 +5,7 @@ import {allForks, ssz} from "@lodestar/types"; import {ForkName} from "@lodestar/params"; import {BeaconChain} from "../../../../src/chain/index.js"; import {Clock} from "../../../../src/util/clock.js"; -import {StateRegenerator} from "../../../../src/chain/regen/index.js"; +import {QueuedStateRegenerator} from "../../../../src/chain/regen/index.js"; import {validateGossipBlock} from "../../../../src/chain/validation/index.js"; import {generateCachedState} from "../../../utils/state.js"; import {BlockErrorCode} from "../../../../src/chain/errors/index.js"; @@ -20,7 +20,7 @@ type StubbedChain = StubbedChainMutable<"clock" | "forkChoice" | "regen" | "bls" describe("gossip block validation", function () { let chain: StubbedChain; let forkChoice: SinonStubbedInstance; - let regen: SinonStubbedInstance; + let regen: SinonStubbedInstance; let verifySignature: SinonStubFn<() => Promise>; let job: allForks.SignedBeaconBlock; const proposerIndex = 0; @@ -37,7 +37,7 @@ describe("gossip block validation", function () { forkChoice = sinon.createStubInstance(ForkChoice); forkChoice.getBlockHex.returns(null); chain.forkChoice = forkChoice; - regen = chain.regen = sinon.createStubInstance(StateRegenerator); + regen = chain.regen = sinon.createStubInstance(QueuedStateRegenerator); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (chain as any).opts = {maxSkipSlots};