UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

175 lines (159 loc) 6.41 kB
import {ChainForkConfig} from "@lodestar/config"; import {CheckpointWithHex, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {RootHex} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {IClock} from "../../util/clock.js"; import {SerializedCache} from "../../util/serializedCache.js"; import {isDaOutOfRange} from "../blocks/blockInput/index.js"; import {CreateFromBidProps, CreateFromBlockProps, PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {ChainEvent, ChainEventEmitter} from "../emitter.js"; export type {PayloadEnvelopeInputState} from "../blocks/payloadEnvelopeInput/index.js"; export {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; export type SeenPayloadEnvelopeInputModules = { config: ChainForkConfig; clock: IClock; forkChoice: IForkChoice; chainEvents: ChainEventEmitter; signal: AbortSignal; serializedCache: SerializedCache; metrics: Metrics | null; logger?: Logger; }; /** * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root. * * Created whenever we have a block because it needs block bid. * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips * ticks; subsequent ticks settle it back. */ export class SeenPayloadEnvelopeInput { private readonly config: ChainForkConfig; private readonly clock: IClock; private readonly forkChoice: IForkChoice; private readonly chainEvents: ChainEventEmitter; private readonly signal: AbortSignal; private readonly serializedCache: SerializedCache; private readonly metrics: Metrics | null; private readonly logger?: Logger; private payloadInputs = new Map<RootHex, PayloadEnvelopeInput>(); constructor({ config, clock, forkChoice, chainEvents, signal, serializedCache, metrics, logger, }: SeenPayloadEnvelopeInputModules) { this.config = config; this.clock = clock; this.forkChoice = forkChoice; this.chainEvents = chainEvents; this.signal = signal; this.serializedCache = serializedCache; this.metrics = metrics; this.logger = logger; if (metrics) { metrics.seenCache.payloadEnvelopeInput.count.addCollect(() => { metrics.seenCache.payloadEnvelopeInput.count.set(this.payloadInputs.size); metrics.seenCache.payloadEnvelopeInput.serializedObjectRefs.set( Array.from(this.payloadInputs.values()).reduce( (count, payloadInput) => count + payloadInput.getSerializedCacheKeys().length, 0 ) ); }); } this.chainEvents.on(ChainEvent.forkChoiceFinalized, this.pruneFinalized); this.signal.addEventListener("abort", () => { this.chainEvents.off(ChainEvent.forkChoiceFinalized, this.pruneFinalized); }); } private pruneFinalized = (checkpoint: CheckpointWithHex): void => { const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch); let deletedCount = 0; for (const [, input] of this.payloadInputs) { if (input.slot < finalizedSlot) { this.evictPayloadInput(input); deletedCount++; } } this.logger?.debug("SeenPayloadEnvelopeInput.pruneFinalized deleted entries", { finalizedSlot, finalizedRoot: checkpoint.rootHex, deletedCount, }); }; add(props: Omit<CreateFromBlockProps, "daOutOfRange">): PayloadEnvelopeInput { const existing = this.payloadInputs.get(props.blockRootHex); if (existing !== undefined) { this.logger?.verbose("SeenPayloadEnvelopeInput.add reused existing entry", { slot: existing.slot, root: props.blockRootHex, }); return existing; } const daOutOfRange = isDaOutOfRange(this.config, props.forkName, props.block.message.slot, this.clock.currentEpoch); const input = PayloadEnvelopeInput.createFromBlock({...props, daOutOfRange}); this.payloadInputs.set(props.blockRootHex, input); this.metrics?.seenCache.payloadEnvelopeInput.created.inc(); this.logger?.verbose("SeenPayloadEnvelopeInput.add created new entry", { slot: input.slot, root: props.blockRootHex, daOutOfRange, }); return input; } /** * Used at chain initialization to seed the anchor block's PayloadEnvelopeInput from * `state.latestExecutionPayloadBid`. */ addFromBid(props: Omit<CreateFromBidProps, "daOutOfRange">): PayloadEnvelopeInput { const existing = this.payloadInputs.get(props.blockRootHex); if (existing !== undefined) { return existing; } const daOutOfRange = isDaOutOfRange(this.config, props.forkName, props.slot, this.clock.currentEpoch); const input = PayloadEnvelopeInput.createFromBid({...props, daOutOfRange}); this.payloadInputs.set(props.blockRootHex, input); this.metrics?.seenCache.payloadEnvelopeInput.created.inc(); this.logger?.verbose("SeenPayloadEnvelopeInput.addFromBid created new entry", { slot: input.slot, root: props.blockRootHex, daOutOfRange, }); return input; } get(blockRootHex: RootHex): PayloadEnvelopeInput | undefined { return this.payloadInputs.get(blockRootHex); } hasPayload(blockRootHex: RootHex): boolean { return this.payloadInputs.get(blockRootHex)?.hasPayloadEnvelope() ?? false; } size(): number { return this.payloadInputs.size; } pruneBelowParent(parentBlock: ProtoBlock): void { for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) { if (block.slot < parentBlock.slot) { const input = this.payloadInputs.get(block.blockRoot); if (input) { this.evictPayloadInput(input); this.logger?.verbose("SeenPayloadEnvelopeInput.pruneBelowParent deleted", { slot: block.slot, root: block.blockRoot, }); } } } } private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void { this.serializedCache.delete(payloadInput.getSerializedCacheKeys()); this.payloadInputs.delete(payloadInput.blockRootHex); } }