Skip to content

SeenPayloadEnvelopeInput cache missing pruning mechanisms #9073

Description

@lodekeeper

Summary

SeenPayloadEnvelopeInput (added in PR #8962) has significantly fewer pruning mechanisms than the equivalent SeenBlockInput cache. This means entries can accumulate and leak memory in several scenarios that the block input cache handles correctly.

Analysis

Pruning comparison: SeenBlockInput vs SeenPayloadEnvelopeInput

Pruning mechanism SeenBlockInput SeenPayloadEnvelopeInput
After DB persist (.finally()) ✅ writeBlockInputToDb.ts:133 ✅ writePayloadEnvelopeInputToDb.ts:49
On finalization event ✅ onFinalized + pruneToMaxSize() ✅ onFinalized (no max-size cap)
On gossip validation error (REJECT/IGNORE) ✅ gossipHandlers.ts:205 ❌ Missing
On processBlock error (gossip path) ✅ gossipHandlers.ts:492 ❌ Missing
After range sync batch processing ✅ range.ts:223 ❌ N/A (no range sync for envelopes yet)
On unknown block removal (bad chain) ✅ unknownBlock.ts:681 ❌ N/A (no unknown envelope sync yet)
pruneToMaxSize() cap ✅ MAX_BLOCK_INPUT_CACHE_SIZE ❌ Missing — no upper bound

Specific concerns

1. No pruneToMaxSize() — unbounded growth during non-finality

SeenBlockInput has MAX_BLOCK_INPUT_CACHE_SIZE = (MAX_LOOK_AHEAD_EPOCHS + 1) * SLOTS_PER_EPOCH and calls pruneToMaxSize() after every prune() and onFinalized(). SeenPayloadEnvelopeInput has no equivalent — during extended non-finality, entries accumulate with no upper bound besides finalization.

2. No pruning on importExecutionPayload failure

When processExecutionPayload fails (EL error, invalid signature, state transition error), the PayloadEnvelopeInput stays in the cache permanently (until finalization). The gossip handler catches the error but only logs it:

chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
  chain.logger.debug("Error processing execution payload from gossip", ...);
  // No prune! Entry stays in cache forever
});

Compare with SeenBlockInput which explicitly prunes on block processing errors:

chain.seenBlockInputCache.prune(blockInput.blockRootHex);

3. .finally() prunes even on DB write failure — data loss

persistPayloadEnvelopeInput prunes the in-memory cache in .finally(), which runs after .catch(). If the DB write fails (transient error), the envelope data is evicted from both memory and DB — irrecoverable. This is the same pattern as persistBlockInput (existing technical debt), but worth noting as it applies here too.

4. No pruning on gossip validation rejection

If validateGossipExecutionPayloadEnvelope passes but addPayloadEnvelope throws (e.g., duplicate envelope), the cache entry persists. The block input cache handles analogous cases by pruning on REJECT/IGNORE actions.

Suggested fixes

  1. Add pruneToMaxSize() with a reasonable constant (e.g., MAX_PAYLOAD_ENVELOPE_INPUT_CACHE_SIZE), called after prune() and onFinalized()
  2. Prune on processExecutionPayload failure in the gossip handler (match block input pattern)
  3. Consider retry-then-prune for DB write failures instead of unconditional .finally() prune (applies to both block and payload envelope caches — tracked separately)

Related

AI assisted

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

spec-gloasIssues targeting the Glamsterdam spec version

Type

No type

Fields

Priority

None yet

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions