fix(beacon-node): protect just-inserted entry from pruneToMaxSize eviction - #9490
Conversation
…ction When `pruneToMaxSize()` fires from inside `add()` / `addFromBid()` and the cache is at cap, the lowest-slot entry is evicted. If the just-inserted entry has a lower slot than every existing entry (out-of-order range-sync response, reorg-recovery insert, etc.), the prune evicts the entry the caller just added — defeating the insertion. Pass `props.blockRootHex` as `excludeRoot` so the just-inserted entry is filtered out of eviction candidates. Adds a fast O(N) min-finding path for the per-insertion `itemsToDelete === 1` case to avoid an O(N log N) sort of the whole cache on the hot path; the multi-item path keeps the existing sort-based approach. Includes a regression test that fills the cache with high-slot entries and inserts a lower-slot entry to verify the new entry survives and the previously-lowest entry is evicted instead. 🤖 Generated with AI assistance
There was a problem hiding this comment.
Code Review
This pull request updates the pruneToMaxSize method in SeenPayloadEnvelopeInput to accept an optional excludeRoot parameter. This prevents a newly inserted entry from being immediately evicted during a cache prune if its slot is lower than existing entries (e.g., during reorgs or out-of-order syncs). Additionally, an O(N) fast-path optimization is introduced for single-item evictions to avoid sorting the entire cache. A corresponding unit test has been added to verify this behavior. I have no further feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Summary
Addresses Gemini's two-part review on #9489 (correctness + perf) by:
props.blockRootHexasexcludeRootfrom eachadd/addFromBidcall topruneToMaxSize.excludeRootout of the eviction-candidate set insidepruneToMaxSize.itemsToDelete === 1case (per-insertion call), keeping the sort-based path for multi-item evictions.Why
pruneToMaxSize()fires from insideadd()/addFromBid()and evicts the lowest-slot entry when over cap. If the just-inserted entry has a lower slot than every existing entry (out-of-order range-sync response, reorg-recovery insert), the prune evicts the entry the caller just added — defeating the insertion and surfacing as aMissing PayloadEnvelopeInputthrow downstream atdownloadByRange.ts:230.Same correctness gap Codex flagged in
discussion_r3380314898for the single-add case. Gemini's review (discussion_r3380335185) also flagged the O(N log N) sort cost on every insertion when only one eviction is needed.Scope notes
This protects the single-add case (gossip from a non-canonical fork that happens to be at a slot lower than everything in the cache, or a reorg-driven re-insert). The multi-add batch-seed scenario Codex described in
discussion_r3380314898—cacheByRangeResponses()seeding 32 slots back-to-back with later iterations evicting earlier same-batch entries — is not addressed here; that requires either askipPruneflag plumbed through batch seeding or a local seed-map fallback incacheByRangeResponses. Happy to follow up with that if you want it in scope here too.Test plan
Type Checks (24)passes.Unit Tests— the new testpruneToMaxSize does not evict the just-inserted entry even if it has the lowest slotfills the cache with high-slot entries (range-sync look-ahead state), inserts a low-slot entry, and asserts the new entry survives while the previously-lowest existing entry is evicted.pruneToMaxSize bounds the cache on insertion, evicting the lowest-slot entriestest still passes.🤖 Generated with Claude Code