Skip to content

Commit

Permalink
Nullifier cache only hits db if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
spalladino committed Jan 7, 2025
1 parent 2caec75 commit 83affff
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { MerkleTreeId, type MerkleTreeReadOperations } from '@aztec/circuit-types';
import { times } from '@aztec/foundation/collection';

import { type MockProxy, mock } from 'jest-mock-extended';

import { NullifierCache } from './nullifier_cache.js';

describe('NullifierCache', () => {
let nullifierCache: NullifierCache;
let db: MockProxy<MerkleTreeReadOperations>;
let nullifiers: Buffer[];

beforeEach(() => {
db = mock<MerkleTreeReadOperations>();
nullifierCache = new NullifierCache(db);
nullifiers = [Buffer.alloc(1, 1), Buffer.alloc(1, 2), Buffer.alloc(1, 3)];
});

it('checks nullifier existence against cache', async () => {
nullifierCache.addNullifiers([nullifiers[0], nullifiers[1]]);
db.findLeafIndices.mockResolvedValue([]);
await expect(nullifierCache.nullifiersExist(nullifiers)).resolves.toEqual([true, true, false]);
});

it('checks nullifier existence against db', async () => {
db.findLeafIndices.mockResolvedValue([1n, 2n, undefined]);
await expect(nullifierCache.nullifiersExist(nullifiers)).resolves.toEqual([true, true, false]);
});

it('checks nullifier existence against db only on cache miss', async () => {
nullifierCache.addNullifiers([nullifiers[0]]);
db.findLeafIndices.mockResolvedValue([2n, undefined]);
const result = await nullifierCache.nullifiersExist(nullifiers);
expect(db.findLeafIndices).toHaveBeenCalledWith(MerkleTreeId.NULLIFIER_TREE, [nullifiers[1], nullifiers[2]]);
expect(result).toEqual([true, true, false]);
});

it('checks existence with several nullifiers', async () => {
// Split 60 nullifiers evenly across db, cache, or not found
const nullifiers = times(60, i => Buffer.alloc(1, i));
const where = nullifiers.map((_, i) =>
i % 3 === 0 ? ('db' as const) : i % 3 === 1 ? ('cache' as const) : ('none' as const),
);

// Add to the cache nullifiers flagged as cache
nullifierCache.addNullifiers(nullifiers.filter((_, i) => where[i] === 'cache'));
// The db should be queried only with nullifiers not in the cache, return true for half of them then
db.findLeafIndices.mockResolvedValue(times(40, i => (i % 2 === 0 ? BigInt(i) : undefined)));

const result = await nullifierCache.nullifiersExist(nullifiers);
expect(db.findLeafIndices).toHaveBeenCalledWith(
MerkleTreeId.NULLIFIER_TREE,
nullifiers.filter((_, i) => where[i] !== 'cache'),
);
expect(result).toEqual(times(60, i => where[i] !== 'none'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ export class NullifierCache implements NullifierSource {
this.nullifiers = new Set();
}

async nullifiersExist(nullifiers: Buffer[]): Promise<boolean[]> {
const dbIndices = await this.db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, nullifiers);
return nullifiers.map((n, index) => this.nullifiers.has(n.toString()) || dbIndices[index] !== undefined);
public async nullifiersExist(nullifiers: Buffer[]): Promise<boolean[]> {
const cacheResults = nullifiers.map(n => this.nullifiers.has(n.toString()));
const toCheckDb = nullifiers.filter((_n, index) => !cacheResults[index]);
const dbHits = await this.db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, toCheckDb);

let dbIndex = 0;
return nullifiers.map((_n, index) => cacheResults[index] || dbHits[dbIndex++] !== undefined);
}

addNullifiers(nullifiers: Buffer[]) {
public addNullifiers(nullifiers: Buffer[]) {
for (const nullifier of nullifiers) {
this.nullifiers.add(nullifier.toString());
}
Expand Down

0 comments on commit 83affff

Please sign in to comment.