@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
175 lines (159 loc) • 6.41 kB
text/typescript
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);
}
}