Skip to content

Commit f7bbd83

Browse files
authored
feat(avm): more efficient low leaf search (#9870)
This swaps out the initial implementation of the tracking just the `indexedTreeMin` of a leaf seen by an indexed tree in favour of a vector of sorted keys that we binary search over. This has 2 benefits, especially in the scenario where the `indexedTreeMin` was storing very small key values. 1) logn search time for keys that we have already seen 2) better than linear searching of the merkleDB for keys we dont have, since we dont have to start searching from min
1 parent 3014a69 commit f7bbd83

File tree

2 files changed

+89
-65
lines changed

2 files changed

+89
-65
lines changed

yarn-project/simulator/src/avm/avm_tree.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ describe('Big Random Avm Ephemeral Container Test', () => {
340340
};
341341

342342
// Can be up to 64
343-
const ENTRY_COUNT = 64;
343+
const ENTRY_COUNT = 50;
344344
shuffleArray(noteHashes);
345345
shuffleArray(indexedHashes);
346346
shuffleArray(slots);

yarn-project/simulator/src/avm/avm_tree.ts

+88-64
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,10 @@ export class AvmEphemeralForest {
5151
constructor(
5252
public treeDb: MerkleTreeReadOperations,
5353
public treeMap: Map<MerkleTreeId, EphemeralAvmTree>,
54-
// This contains the preimage and the leaf index of leaf in the ephemeral tree that contains the lowest key (i.e. nullifier value or public data tree slot)
55-
public indexedTreeMin: Map<IndexedTreeId, [IndexedTreeLeafPreimage, bigint]>,
5654
// This contains the [leaf index,indexed leaf preimages] tuple that were updated or inserted in the ephemeral tree
5755
// This is needed since we have a sparse collection of keys sorted leaves in the ephemeral tree
5856
public indexedUpdates: Map<IndexedTreeId, Map<bigint, IndexedTreeLeafPreimage>>,
57+
public indexedSortedKeys: Map<IndexedTreeId, [Fr, bigint][]>,
5958
) {}
6059

6160
static async create(treeDb: MerkleTreeReadOperations): Promise<AvmEphemeralForest> {
@@ -65,15 +64,18 @@ export class AvmEphemeralForest {
6564
const tree = await EphemeralAvmTree.create(treeInfo.size, treeInfo.depth, treeDb, treeType);
6665
treeMap.set(treeType, tree);
6766
}
68-
return new AvmEphemeralForest(treeDb, treeMap, new Map(), new Map());
67+
const indexedSortedKeys = new Map<IndexedTreeId, [Fr, bigint][]>();
68+
indexedSortedKeys.set(MerkleTreeId.NULLIFIER_TREE as IndexedTreeId, []);
69+
indexedSortedKeys.set(MerkleTreeId.PUBLIC_DATA_TREE as IndexedTreeId, []);
70+
return new AvmEphemeralForest(treeDb, treeMap, new Map(), indexedSortedKeys);
6971
}
7072

7173
fork(): AvmEphemeralForest {
7274
return new AvmEphemeralForest(
7375
this.treeDb,
7476
cloneDeep(this.treeMap),
75-
cloneDeep(this.indexedTreeMin),
7677
cloneDeep(this.indexedUpdates),
78+
cloneDeep(this.indexedSortedKeys),
7779
);
7880
}
7981

@@ -166,7 +168,8 @@ export class AvmEphemeralForest {
166168
const insertionPath = tree.getSiblingPath(insertionIndex)!;
167169

168170
// Even though we append an empty leaf into the tree as a part of update - it doesnt seem to impact future inserts...
169-
this._updateMinInfo(MerkleTreeId.PUBLIC_DATA_TREE, [updatedPreimage], [index]);
171+
this._updateSortedKeys(treeId, [updatedPreimage.slot], [index]);
172+
170173
return {
171174
leafIndex: insertionIndex,
172175
insertionPath,
@@ -193,8 +196,10 @@ export class AvmEphemeralForest {
193196
);
194197
const insertionPath = this.appendIndexedTree(treeId, index, updatedLowLeaf, newPublicDataLeaf);
195198

196-
// Since we are appending, we might have a new minimum public data leaf
197-
this._updateMinInfo(MerkleTreeId.PUBLIC_DATA_TREE, [newPublicDataLeaf, updatedLowLeaf], [insertionIndex, index]);
199+
// Even though the low leaf key is not updated, we still need to update the sorted keys in case we have
200+
// not seen the low leaf before
201+
this._updateSortedKeys(treeId, [newPublicDataLeaf.slot, updatedLowLeaf.slot], [insertionIndex, index]);
202+
198203
return {
199204
leafIndex: insertionIndex,
200205
insertionPath: insertionPath,
@@ -208,28 +213,25 @@ export class AvmEphemeralForest {
208213
};
209214
}
210215

211-
/**
212-
* This is just a helper to compare the preimages and update the minimum public data leaf
213-
* @param treeId - The tree to be queried for a sibling path.
214-
* @param T - The type of the preimage (PublicData or Nullifier)
215-
* @param preimages - The preimages to be compared
216-
* @param indices - The indices of the preimages
217-
*/
218-
private _updateMinInfo<T extends IndexedTreeLeafPreimage>(
219-
treeId: IndexedTreeId,
220-
preimages: T[],
221-
indices: bigint[],
222-
): void {
223-
let currentMin = this.getMinInfo(treeId);
224-
if (currentMin === undefined) {
225-
currentMin = { preimage: preimages[0], index: indices[0] };
226-
}
227-
for (let i = 0; i < preimages.length; i++) {
228-
if (preimages[i].getKey() <= currentMin.preimage.getKey()) {
229-
currentMin = { preimage: preimages[i], index: indices[i] };
216+
private _updateSortedKeys(treeId: IndexedTreeId, keys: Fr[], index: bigint[]): void {
217+
// This is a reference
218+
const existingKeyVector = this.indexedSortedKeys.get(treeId)!;
219+
// Should already be sorted so not need to re-sort if we just update or splice
220+
for (let i = 0; i < keys.length; i++) {
221+
const foundIndex = existingKeyVector.findIndex(x => x[1] === index[i]);
222+
if (foundIndex === -1) {
223+
// New element, we splice it into the correct location
224+
const spliceIndex =
225+
this.searchForKey(
226+
keys[i],
227+
existingKeyVector.map(x => x[0]),
228+
) + 1;
229+
existingKeyVector.splice(spliceIndex, 0, [keys[i], index[i]]);
230+
} else {
231+
// Update the existing element
232+
existingKeyVector[foundIndex][0] = keys[i];
230233
}
231234
}
232-
this.setMinInfo(treeId, currentMin.preimage, currentMin.index);
233235
}
234236

235237
/**
@@ -258,8 +260,14 @@ export class AvmEphemeralForest {
258260
const newNullifierLeaf = new NullifierLeafPreimage(nullifier, preimage.nextNullifier, preimage.nextIndex);
259261
const insertionPath = this.appendIndexedTree(treeId, index, updatedLowNullifier, newNullifierLeaf);
260262

261-
// Since we are appending, we might have a new minimum nullifier leaf
262-
this._updateMinInfo(MerkleTreeId.NULLIFIER_TREE, [newNullifierLeaf, updatedLowNullifier], [insertionIndex, index]);
263+
// Even though the low nullifier key is not updated, we still need to update the sorted keys in case we have
264+
// not seen the low nullifier before
265+
this._updateSortedKeys(
266+
treeId,
267+
[newNullifierLeaf.nullifier, updatedLowNullifier.nullifier],
268+
[insertionIndex, index],
269+
);
270+
263271
return {
264272
leafIndex: insertionIndex,
265273
insertionPath: insertionPath,
@@ -286,31 +294,6 @@ export class AvmEphemeralForest {
286294
return insertionPath!;
287295
}
288296

289-
/**
290-
* This is wrapper around treeId to get the correct minimum leaf preimage
291-
*/
292-
private getMinInfo<ID extends IndexedTreeId, T extends IndexedTreeLeafPreimage>(
293-
treeId: ID,
294-
): { preimage: T; index: bigint } | undefined {
295-
const start = this.indexedTreeMin.get(treeId);
296-
if (start === undefined) {
297-
return undefined;
298-
}
299-
const [preimage, index] = start;
300-
return { preimage: preimage as T, index };
301-
}
302-
303-
/**
304-
* This is wrapper around treeId to set the correct minimum leaf preimage
305-
*/
306-
private setMinInfo<ID extends IndexedTreeId, T extends IndexedTreeLeafPreimage>(
307-
treeId: ID,
308-
preimage: T,
309-
index: bigint,
310-
): void {
311-
this.indexedTreeMin.set(treeId, [preimage, index]);
312-
}
313-
314297
/**
315298
* This is wrapper around treeId to set values in the indexedUpdates map
316299
*/
@@ -353,6 +336,28 @@ export class AvmEphemeralForest {
353336
return updates.has(index);
354337
}
355338

339+
private searchForKey(key: Fr, arr: Fr[]): number {
340+
// We are looking for the index of the largest element in the array that is less than the key
341+
let start = 0;
342+
let end = arr.length;
343+
// Note that the easiest way is to increment the search key by 1 and then do a binary search
344+
const searchKey = key.add(Fr.ONE);
345+
while (start < end) {
346+
const mid = Math.floor((start + end) / 2);
347+
if (arr[mid].cmp(searchKey) < 0) {
348+
// The key + 1 is greater than the arr element, so we can continue searching the top half
349+
start = mid + 1;
350+
} else {
351+
// The key + 1 is LT or EQ the arr element, so we can continue searching the bottom half
352+
end = mid;
353+
}
354+
}
355+
// We either found key + 1 or start is now at the index of the largest element that we would have inserted key + 1
356+
// Therefore start - 1 is the index of the element just below - note it can be -1 if the first element in the array is
357+
// greater than the key
358+
return start - 1;
359+
}
360+
356361
/**
357362
* This gets the low leaf preimage and the index of the low leaf in the indexed tree given a value (slot or nullifier value)
358363
* If the value is not found in the tree, it does an external lookup to the merkleDB
@@ -365,23 +370,42 @@ export class AvmEphemeralForest {
365370
treeId: ID,
366371
key: Fr,
367372
): Promise<PreimageWitness<T>> {
373+
const keyOrderedVector = this.indexedSortedKeys.get(treeId)!;
374+
375+
const vectorIndex = this.searchForKey(
376+
key,
377+
keyOrderedVector.map(x => x[0]),
378+
);
379+
// We have a match in our local updates
380+
let minPreimage = undefined;
381+
382+
if (vectorIndex !== -1) {
383+
const [_, leafIndex] = keyOrderedVector[vectorIndex];
384+
minPreimage = {
385+
preimage: this.getIndexedUpdates(treeId, leafIndex) as T,
386+
index: leafIndex,
387+
};
388+
}
368389
// This can probably be done better, we want to say if the minInfo is undefined (because this is our first operation) we do the external lookup
369-
const minPreimage = this.getMinInfo(treeId);
370390
const start = minPreimage?.preimage;
371391
const bigIntKey = key.toBigInt();
372-
// If the first element we have is already greater than the value, we need to do an external lookup
373-
if (minPreimage === undefined || (start?.getKey() ?? 0n) >= key.toBigInt()) {
374-
// The low public data witness is in the previous tree
392+
393+
// If we don't have a first element or if that first element is already greater than the target key, we need to do an external lookup
394+
// The low public data witness is in the previous tree
395+
if (start === undefined || start.getKey() > key.toBigInt()) {
396+
// This function returns the leaf index to the actual element if it exists or the leaf index to the low leaf otherwise
375397
const { index, alreadyPresent } = (await this.treeDb.getPreviousValueIndex(treeId, bigIntKey))!;
376398
const preimage = await this.treeDb.getLeafPreimage(treeId, index);
377399

378-
// Since we have never seen this before - we should insert it into our tree
379-
const siblingPath = (await this.treeDb.getSiblingPath(treeId, index)).toFields();
400+
// Since we have never seen this before - we should insert it into our tree, as we know we will modify this leaf node
401+
const siblingPath = await this.getSiblingPath(treeId, index);
402+
// const siblingPath = (await this.treeDb.getSiblingPath(treeId, index)).toFields();
380403

381-
// Is it enough to just insert the sibling path without inserting the leaf? - right now probably since we will update this low nullifier index in append
404+
// Is it enough to just insert the sibling path without inserting the leaf? - now probably since we will update this low nullifier index in append
382405
this.treeMap.get(treeId)!.insertSiblingPath(index, siblingPath);
383406

384407
const lowPublicDataPreimage = preimage as T;
408+
385409
return { preimage: lowPublicDataPreimage, index: index, update: alreadyPresent };
386410
}
387411

@@ -392,18 +416,18 @@ export class AvmEphemeralForest {
392416
// (3) Max Condition: curr.next_index == 0 and curr.key < key
393417
// Note the min condition does not need to be handled since indexed trees are prefilled with at least the 0 element
394418
let found = false;
395-
let curr = minPreimage.preimage as T;
419+
let curr = minPreimage!.preimage as T;
396420
let result: PreimageWitness<T> | undefined = undefined;
397421
// Temp to avoid infinite loops - the limit is the number of leaves we may have to read
398422
const LIMIT = 2n ** BigInt(getTreeHeight(treeId)) - 1n;
399423
let counter = 0n;
400-
let lowPublicDataIndex = minPreimage.index;
424+
let lowPublicDataIndex = minPreimage!.index;
401425
while (!found && counter < LIMIT) {
402426
if (curr.getKey() === bigIntKey) {
403427
// We found an exact match - therefore this is an update
404428
found = true;
405429
result = { preimage: curr, index: lowPublicDataIndex, update: true };
406-
} else if (curr.getKey() < bigIntKey && (curr.getNextKey() === 0n || curr.getNextKey() > bigIntKey)) {
430+
} else if (curr.getKey() < bigIntKey && (curr.getNextIndex() === 0n || curr.getNextKey() > bigIntKey)) {
407431
// We found it via sandwich or max condition, this is a low nullifier
408432
found = true;
409433
result = { preimage: curr, index: lowPublicDataIndex, update: false };

0 commit comments

Comments
 (0)