diff --git a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts index d5c07a563590..3421e6bd40ed 100644 --- a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts @@ -33,23 +33,14 @@ export async function persistPayloadEnvelopeInput( this: BeaconChain, payloadInput: PayloadEnvelopeInput ): Promise { - await writePayloadEnvelopeInputToDb - .call(this, payloadInput) - .catch((e) => { - this.logger.error( - "Error persisting payload envelope in hot db", - { - slot: payloadInput.slot, - root: payloadInput.blockRootHex, - }, - e - ); - }) - .finally(() => { - this.seenPayloadEnvelopeInputCache.prune(payloadInput.blockRootHex); - this.logger.debug("Pruned payload envelope input", { + await writePayloadEnvelopeInputToDb.call(this, payloadInput).catch((e) => { + this.logger.error( + "Error persisting payload envelope in hot db", + { slot: payloadInput.slot, root: payloadInput.blockRootHex, - }); - }); + }, + e + ); + }); } diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index ec3dfc3286c0..0f5e4293934f 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -83,7 +83,7 @@ export class PrepareNextSlotScheduler { const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot); const {slot: headSlot, blockRoot: headRoot} = headBlock; // may be updated below if we predict a proposer-boost-reorg - let updatedHeadRoot = headRoot; + let updatedHead = headBlock; // PS: previously this was comparing slots, but that gave no leway on the skipped // slots on epoch bounday. Making it more fluid. @@ -148,7 +148,7 @@ export class PrepareNextSlotScheduler { {dontTransferCache: !isEpochTransition}, RegenCaller.predictProposerHead ); - updatedHeadRoot = proposerHeadRoot; + updatedHead = proposerHead; } // Update the builder status, if enabled shoot an api call to check status @@ -166,7 +166,7 @@ export class PrepareNextSlotScheduler { let parentBlockHash: Bytes32; if (isStatePostGloas(updatedPrepareState)) { - parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHeadRoot) + parentBlockHash = this.chain.forkChoice.shouldExtendPayload(updatedHead.blockRoot) ? updatedPrepareState.latestExecutionPayloadBid.blockHash : updatedPrepareState.latestExecutionPayloadBid.parentBlockHash; } else { @@ -189,7 +189,7 @@ export class PrepareNextSlotScheduler { this.chain, this.logger, fork as ForkPostBellatrix, // State is of execution type - fromHex(updatedHeadRoot), + fromHex(updatedHead.blockRoot), parentBlockHash, safeBlockHash, finalizedBlockHash, @@ -203,6 +203,16 @@ export class PrepareNextSlotScheduler { }); } + if (ForkSeq[fork] >= ForkSeq.gloas) { + // Cutoff = slot of the parent of the block we'll actually build on (post-reorg). + // Steady state: cache holds just 2 entries — head (parent for next-slot production) + // and head.parent (proposer-boost-reorg fallback). Anything older is evicted. + const updatedHeadParent = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot); + if (updatedHeadParent) { + this.chain.seenPayloadEnvelopeInputCache.pruneBelow(updatedHeadParent.slot); + } + } + this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition); // If emitPayloadAttributes is true emit a SSE payloadAttributes event diff --git a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts index e36147638061..16610a2b7dd9 100644 --- a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts @@ -1,6 +1,6 @@ import {CheckpointWithHex} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {RootHex} from "@lodestar/types"; +import {RootHex, Slot} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {SerializedCache} from "../../util/serializedCache.js"; @@ -21,8 +21,15 @@ export type SeenPayloadEnvelopeInputModules = { /** * Cache for tracking PayloadEnvelopeInput instances, keyed by beacon block root. * - * Created during block import when a block is processed. - * Pruned on finalization and after payload is written to DB. + * Created during block import when a block is processed. Two pruning paths: + * - `prepareNextSlot` calls `pruneBelow(headParentSlot)` every slot once the head we'll build + * on is known. + * - `onFinalized` calls `pruneBelow(finalizedSlot)` on every finalization for bulk cleanup. + * + * 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 chainEvents: ChainEventEmitter; @@ -58,16 +65,7 @@ export class SeenPayloadEnvelopeInput { } private onFinalized = (checkpoint: CheckpointWithHex): void => { - // Prune all entries with slot < finalized slot - 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.onFinalized deleted cached entries", {deletedCount}); + this.pruneBelow(computeStartSlotAtEpoch(checkpoint.epoch)); }; add(props: CreateFromBlockProps): PayloadEnvelopeInput { @@ -88,17 +86,21 @@ export class SeenPayloadEnvelopeInput { return this.payloadInputs.get(blockRootHex)?.hasPayloadEnvelope() ?? false; } - prune(blockRootHex: RootHex): void { - const payloadInput = this.payloadInputs.get(blockRootHex); - if (payloadInput) { - this.evictPayloadInput(payloadInput); - } - } - size(): number { return this.payloadInputs.size; } + pruneBelow(slot: Slot): void { + let deletedCount = 0; + for (const [, input] of this.payloadInputs) { + if (input.slot < slot) { + this.evictPayloadInput(input); + deletedCount++; + } + } + this.logger?.debug("SeenPayloadEnvelopeInput.pruneBelow deleted entries", {slot, deletedCount}); + } + private evictPayloadInput(payloadInput: PayloadEnvelopeInput): void { this.serializedCache.delete(payloadInput.getSerializedCacheKeys()); this.payloadInputs.delete(payloadInput.blockRootHex);