Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class SeenPayloadEnvelopeInput {
root: props.blockRootHex,
daOutOfRange,
});
this.pruneToMaxSize();
this.pruneToMaxSize(props.blockRootHex);
return input;
}

Expand All @@ -146,7 +146,7 @@ export class SeenPayloadEnvelopeInput {
root: props.blockRootHex,
daOutOfRange,
});
this.pruneToMaxSize();
this.pruneToMaxSize(props.blockRootHex);
return input;
}

Expand Down Expand Up @@ -177,20 +177,43 @@ export class SeenPayloadEnvelopeInput {
}
}

/** Evict the lowest-slot entries (oldest / furthest behind the head) once over the max size. */
private pruneToMaxSize(): void {
/**
* Evict the lowest-slot entries (oldest / furthest behind the head) once over the max size.
* `excludeRoot` protects the just-inserted entry from being evicted by its own triggering call,
* which matters when a reorg or out-of-order range-sync response inserts an entry whose slot is
* lower than every existing entry's slot.
*/
private pruneToMaxSize(excludeRoot?: RootHex): void {
let itemsToDelete = this.payloadInputs.size - MAX_PAYLOAD_ENVELOPE_INPUT_CACHE_SIZE;
if (itemsToDelete <= 0) {
return;
}

const sorted = [...this.payloadInputs.values()].sort((a, b) => a.slot - b.slot);
let deletedCount = 0;
for (const input of sorted) {
this.evictPayloadInput(input);
deletedCount++;
if (--itemsToDelete <= 0) {
break;
if (itemsToDelete === 1) {
// Fast path for the per-insertion call: find the lowest-slot non-excluded entry in O(N) instead of
// paying for an O(N log N) sort of the whole cache.
let minInput: PayloadEnvelopeInput | undefined;
for (const input of this.payloadInputs.values()) {
if (input.blockRootHex === excludeRoot) continue;
if (minInput === undefined || input.slot < minInput.slot) {
minInput = input;
}
}
if (minInput !== undefined) {
this.evictPayloadInput(minInput);
deletedCount = 1;
}
} else {
const sorted = [...this.payloadInputs.values()]
.filter((input) => input.blockRootHex !== excludeRoot)
.sort((a, b) => a.slot - b.slot);
for (const input of sorted) {
this.evictPayloadInput(input);
deletedCount++;
if (--itemsToDelete <= 0) {
break;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ describe("SeenPayloadEnvelopeInput", () => {
}
});

it("pruneToMaxSize does not evict the just-inserted entry even if it has the lowest slot", () => {
// Fill the cache with high-slot entries (range-sync look-ahead state)
const maxSize = (MAX_LOOK_AHEAD_EPOCHS + 1) * SLOTS_PER_EPOCH;
const highSlotBase = 1000;
const rootHexBySlot = new Map<number, string>();
for (let i = 0; i < maxSize; i++) {
const slot = highSlotBase + i;
rootHexBySlot.set(slot, addPayloadInput(slot));
}
expect(cache.size()).toBe(maxSize);

// Insert an entry whose slot is lower than every existing entry's slot — out-of-order range-sync
// response, reorg-recovery insert, etc. The triggering prune must NOT evict the just-inserted
// entry; it should evict the previously-lowest existing entry instead.
const lowSlot = 1;
const lowRootHex = addPayloadInput(lowSlot);

expect(cache.size()).toBe(maxSize);
// just-inserted entry survives
expect(cache.get(lowRootHex)).toBeDefined();
// previously-lowest entry is the one evicted
expect(cache.get(rootHexBySlot.get(highSlotBase) as string)).toBeUndefined();
});

it("add returns the existing entry on duplicate root", () => {
const {block, rootHex} = generateBlock({forkName: ForkName.gloas, slot: 1});
const props = {
Expand Down