Skip to content
Merged
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
3 changes: 2 additions & 1 deletion yarn-project/archiver/src/l1/calldata_retriever.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MULTI_CALL_3_ADDRESS, type ViemCommitteeAttestations, type ViemHeader } from '@aztec/ethereum/contracts';
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
import { CheckpointNumber } from '@aztec/foundation/branded-types';
import { LruSet } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/curves/bn254';
import { EthAddress } from '@aztec/foundation/eth-address';
import type { Logger } from '@aztec/foundation/log';
Expand Down Expand Up @@ -44,7 +45,7 @@ type CheckpointData = {
*/
export class CalldataRetriever {
/** Tx hashes we've already logged for trace+debug failure (log once per tx per process). */
private static readonly traceFailureWarnedTxHashes = new Set<string>();
private static readonly traceFailureWarnedTxHashes = new LruSet<string>(1000);

/** Clears the trace-failure warned set. For testing only. */
static resetTraceFailureWarnedForTesting(): void {
Expand Down
1 change: 1 addition & 0 deletions yarn-project/foundation/src/collection/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './array.js';
export * from './lru_set.js';
export * from './object.js';
131 changes: 131 additions & 0 deletions yarn-project/foundation/src/collection/lru_set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { LruSet } from './lru_set.js';

describe('LruSet', () => {
it('stores and retrieves items', () => {
const set = new LruSet<string>(3);
set.add('a');
set.add('b');
expect(set.has('a')).toBe(true);
expect(set.has('b')).toBe(true);
expect(set.has('c')).toBe(false);
});

it('reports correct size', () => {
const set = new LruSet<number>(5);
expect(set.size).toBe(0);
set.add(1);
expect(set.size).toBe(1);
set.add(2);
set.add(3);
expect(set.size).toBe(3);
});

it('does not grow beyond maxSize', () => {
const set = new LruSet<number>(3);
set.add(1);
set.add(2);
set.add(3);
set.add(4);
expect(set.size).toBe(3);
expect(set.has(1)).toBe(false); // evicted (least recent)
expect(set.has(2)).toBe(true);
expect(set.has(3)).toBe(true);
expect(set.has(4)).toBe(true);
});

it('evicts least recently used, not least recently added', () => {
const set = new LruSet<string>(3);
set.add('a');
set.add('b');
set.add('c');

// Access 'a' via has(), making it the most recently used
expect(set.has('a')).toBe(true);

// Now 'b' is the least recently used. Adding 'd' should evict 'b'.
set.add('d');
expect(set.has('b')).toBe(false); // evicted
expect(set.has('a')).toBe(true); // kept (was refreshed)
expect(set.has('c')).toBe(true);
expect(set.has('d')).toBe(true);
});

it('refreshes recency on add() of existing item', () => {
const set = new LruSet<string>(3);
set.add('a');
set.add('b');
set.add('c');

// Re-add 'a', refreshing its recency
set.add('a');

// 'b' is now least recent. Adding 'd' should evict 'b'.
set.add('d');
expect(set.has('b')).toBe(false); // evicted
expect(set.has('a')).toBe(true);
expect(set.size).toBe(3);
});

it('does not duplicate on add() of existing item', () => {
const set = new LruSet<number>(5);
set.add(1);
set.add(2);
set.add(1);
set.add(1);
expect(set.size).toBe(2);
});

it('clears all entries', () => {
const set = new LruSet<number>(5);
set.add(1);
set.add(2);
set.add(3);
set.clear();
expect(set.size).toBe(0);
expect(set.has(1)).toBe(false);
expect(set.has(2)).toBe(false);
expect(set.has(3)).toBe(false);
});

it('works correctly after clear and re-add', () => {
const set = new LruSet<string>(2);
set.add('a');
set.add('b');
set.clear();
set.add('c');
set.add('d');
expect(set.size).toBe(2);
expect(set.has('a')).toBe(false);
expect(set.has('c')).toBe(true);
expect(set.has('d')).toBe(true);
});

it('works with maxSize of 1', () => {
const set = new LruSet<number>(1);
set.add(1);
expect(set.has(1)).toBe(true);
set.add(2);
expect(set.has(1)).toBe(false);
expect(set.has(2)).toBe(true);
expect(set.size).toBe(1);
});

it('throws on invalid maxSize', () => {
expect(() => new LruSet<number>(0)).toThrow('LruSet maxSize must be at least 1');
expect(() => new LruSet<number>(-1)).toThrow('LruSet maxSize must be at least 1');
});

it('handles sequential evictions correctly', () => {
const set = new LruSet<number>(3);
// Fill to capacity
for (let i = 0; i < 3; i++) {
set.add(i);
}
// Evict each one in FIFO order (no access refreshes)
for (let i = 3; i < 10; i++) {
set.add(i);
expect(set.size).toBe(3);
expect(set.has(i - 3)).toBe(false); // oldest was evicted
}
});
});
115 changes: 115 additions & 0 deletions yarn-project/foundation/src/collection/lru_set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/** Node in a doubly-linked list used by {@link LruSet}. */
type LruNode<T> = {
value: T;
prev: LruNode<T> | undefined;
next: LruNode<T> | undefined;
};

/**
* A bounded set with Least Recently Used (LRU) eviction.
* Both {@link has} and {@link add} count as an access and refresh the entry's
* recency, so items that are actively checked stay in the set longest.
*
* Uses a doubly-linked list for O(1) ordering and a Map for O(1) lookup.
* Head = least recent, tail = most recent.
*/
export class LruSet<T> {
/** Map from value to its linked-list node for O(1) lookup. */
private readonly map = new Map<T, LruNode<T>>();
private head: LruNode<T> | undefined;
private tail: LruNode<T> | undefined;

constructor(private readonly maxSize: number) {
if (maxSize < 1) {
throw new Error('LruSet maxSize must be at least 1');
}
}

/** Number of entries in the set. */
get size(): number {
return this.map.size;
}

/**
* Returns true if the item is in the set.
* Refreshes the item's recency so it becomes the most recently used.
*/
has(item: T): boolean {
const node = this.map.get(item);
if (!node) {
return false;
}
this.moveToTail(node);
return true;
}

/**
* Adds an item to the set. If the item already exists, refreshes its recency.
* If the set is at capacity, evicts the least recently used item.
*/
add(item: T): void {
const existing = this.map.get(item);
if (existing) {
this.moveToTail(existing);
return;
}

if (this.map.size >= this.maxSize) {
this.evictHead();
}

const node: LruNode<T> = { value: item, prev: this.tail, next: undefined };
if (this.tail) {
this.tail.next = node;
} else {
this.head = node;
}
this.tail = node;
this.map.set(item, node);
}

/** Removes all entries from the set. */
clear(): void {
this.map.clear();
this.head = undefined;
this.tail = undefined;
}

/** Unlinks a node from its current position and relinks it at the tail. */
private moveToTail(node: LruNode<T>): void {
if (node === this.tail) {
return;
}

// Unlink
if (node.prev) {
node.prev.next = node.next;
} else {
this.head = node.next;
}
if (node.next) {
node.next.prev = node.prev;
}

// Relink at tail
node.prev = this.tail;
node.next = undefined;
if (this.tail) {
this.tail.next = node;
}
this.tail = node;
}

/** Evicts the head (least recently used) node. */
private evictHead(): void {
const oldHead = this.head!;
this.map.delete(oldHead.value);

this.head = oldHead.next;
if (this.head) {
this.head.prev = undefined;
} else {
this.tail = undefined;
}
}
}
Loading