@aztec/merkle-tree 0.16.1 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dest/index.d.ts +5 -1
  2. package/dest/index.d.ts.map +1 -1
  3. package/dest/index.js +6 -2
  4. package/dest/interfaces/append_only_tree.d.ts +2 -1
  5. package/dest/interfaces/append_only_tree.d.ts.map +1 -1
  6. package/dest/interfaces/indexed_tree.d.ts +38 -17
  7. package/dest/interfaces/indexed_tree.d.ts.map +1 -1
  8. package/dest/interfaces/merkle_tree.d.ts +7 -0
  9. package/dest/interfaces/merkle_tree.d.ts.map +1 -1
  10. package/dest/interfaces/update_only_tree.d.ts +3 -3
  11. package/dest/interfaces/update_only_tree.d.ts.map +1 -1
  12. package/dest/load_tree.d.ts +2 -1
  13. package/dest/load_tree.d.ts.map +1 -1
  14. package/dest/load_tree.js +1 -2
  15. package/dest/new_tree.d.ts +1 -1
  16. package/dest/new_tree.d.ts.map +1 -1
  17. package/dest/new_tree.js +2 -2
  18. package/dest/snapshots/append_only_snapshot.d.ts +30 -0
  19. package/dest/snapshots/append_only_snapshot.d.ts.map +1 -0
  20. package/dest/snapshots/append_only_snapshot.js +200 -0
  21. package/dest/snapshots/base_full_snapshot.d.ts +50 -0
  22. package/dest/snapshots/base_full_snapshot.d.ts.map +1 -0
  23. package/dest/snapshots/base_full_snapshot.js +179 -0
  24. package/dest/snapshots/full_snapshot.d.ts +22 -0
  25. package/dest/snapshots/full_snapshot.d.ts.map +1 -0
  26. package/dest/snapshots/full_snapshot.js +21 -0
  27. package/dest/snapshots/indexed_tree_snapshot.d.ts +15 -0
  28. package/dest/snapshots/indexed_tree_snapshot.d.ts.map +1 -0
  29. package/dest/snapshots/indexed_tree_snapshot.js +75 -0
  30. package/dest/snapshots/snapshot_builder.d.ts +76 -0
  31. package/dest/snapshots/snapshot_builder.d.ts.map +1 -0
  32. package/dest/snapshots/snapshot_builder.js +2 -0
  33. package/dest/snapshots/snapshot_builder_test_suite.d.ts +5 -0
  34. package/dest/snapshots/snapshot_builder_test_suite.d.ts.map +1 -0
  35. package/dest/snapshots/snapshot_builder_test_suite.js +163 -0
  36. package/dest/sparse_tree/sparse_tree.d.ts +5 -0
  37. package/dest/sparse_tree/sparse_tree.d.ts.map +1 -1
  38. package/dest/sparse_tree/sparse_tree.js +18 -1
  39. package/dest/standard_indexed_tree/standard_indexed_tree.d.ts +111 -81
  40. package/dest/standard_indexed_tree/standard_indexed_tree.d.ts.map +1 -1
  41. package/dest/standard_indexed_tree/standard_indexed_tree.js +225 -259
  42. package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.d.ts.map +1 -1
  43. package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.js +13 -19
  44. package/dest/standard_tree/standard_tree.d.ts +5 -0
  45. package/dest/standard_tree/standard_tree.d.ts.map +1 -1
  46. package/dest/standard_tree/standard_tree.js +24 -1
  47. package/dest/tree_base.d.ts +9 -4
  48. package/dest/tree_base.d.ts.map +1 -1
  49. package/dest/tree_base.js +16 -7
  50. package/package.json +4 -3
  51. package/src/index.ts +5 -1
  52. package/src/interfaces/append_only_tree.ts +2 -1
  53. package/src/interfaces/indexed_tree.ts +50 -28
  54. package/src/interfaces/merkle_tree.ts +8 -0
  55. package/src/interfaces/update_only_tree.ts +3 -4
  56. package/src/load_tree.ts +2 -2
  57. package/src/new_tree.ts +2 -2
  58. package/src/snapshots/append_only_snapshot.ts +243 -0
  59. package/src/snapshots/base_full_snapshot.ts +232 -0
  60. package/src/snapshots/full_snapshot.ts +26 -0
  61. package/src/snapshots/indexed_tree_snapshot.ts +108 -0
  62. package/src/snapshots/snapshot_builder.ts +84 -0
  63. package/src/snapshots/snapshot_builder_test_suite.ts +218 -0
  64. package/src/sparse_tree/sparse_tree.ts +16 -0
  65. package/src/standard_indexed_tree/standard_indexed_tree.ts +325 -304
  66. package/src/standard_indexed_tree/test/standard_indexed_tree_with_append.ts +23 -21
  67. package/src/standard_tree/standard_tree.ts +21 -0
  68. package/src/tree_base.ts +28 -7
@@ -1,83 +1,107 @@
1
1
  import { toBigIntBE, toBufferBE } from '@aztec/foundation/bigint-buffer';
2
2
  import { createDebugLogger } from '@aztec/foundation/log';
3
- import { SiblingPath } from '@aztec/types';
4
-
5
- import { IndexedTree, LeafData } from '../interfaces/indexed_tree.js';
3
+ import { IndexedTreeLeaf, IndexedTreeLeafPreimage } from '@aztec/foundation/trees';
4
+ import { Hasher, SiblingPath } from '@aztec/types';
5
+
6
+ import { LevelUp } from 'levelup';
7
+
8
+ import {
9
+ BatchInsertionResult,
10
+ IndexedTree,
11
+ IndexedTreeSnapshot,
12
+ IndexedTreeSnapshotBuilder,
13
+ LowLeafWitnessData,
14
+ } from '../index.js';
6
15
  import { TreeBase } from '../tree_base.js';
7
16
 
8
17
  const log = createDebugLogger('aztec:standard-indexed-tree');
9
18
 
10
- const indexToKeyLeaf = (name: string, index: bigint) => {
11
- return `${name}:leaf:${toBufferBE(index, 32).toString('hex')}`;
12
- };
13
-
14
- const keyLeafToIndex = (key: string): bigint => {
15
- const index = key.split(':')[2];
16
- return toBigIntBE(Buffer.from(index, 'hex'));
17
- };
18
-
19
- const zeroLeaf: LeafData = {
20
- value: 0n,
21
- nextValue: 0n,
22
- nextIndex: 0n,
23
- };
24
-
25
19
  /**
26
- * All of the data to be return during batch insertion.
20
+ * Factory for creating leaf preimages.
27
21
  */
28
- export interface LowLeafWitnessData<N extends number> {
22
+ export interface PreimageFactory {
29
23
  /**
30
- * Preimage of the low nullifier that proves non membership.
24
+ * Creates a new preimage from a leaf.
25
+ * @param leaf - Leaf to create a preimage from.
26
+ * @param nextKey - Next key of the leaf.
27
+ * @param nextIndex - Next index of the leaf.
31
28
  */
32
- leafData: LeafData;
29
+ fromLeaf(leaf: IndexedTreeLeaf, nextKey: bigint, nextIndex: bigint): IndexedTreeLeafPreimage;
30
+ /**
31
+ * Creates a new preimage from a buffer.
32
+ * @param buffer - Buffer to create a preimage from.
33
+ */
34
+ fromBuffer(buffer: Buffer): IndexedTreeLeafPreimage;
35
+ /**
36
+ * Creates an empty preimage.
37
+ */
38
+ empty(): IndexedTreeLeafPreimage;
39
+ /**
40
+ * Creates a copy of a preimage.
41
+ * @param preimage - Preimage to be cloned.
42
+ */
43
+ clone(preimage: IndexedTreeLeafPreimage): IndexedTreeLeafPreimage;
44
+ }
45
+
46
+ /**
47
+ * Factory for creating leaves.
48
+ */
49
+ export interface LeafFactory {
33
50
  /**
34
- * Sibling path to prove membership of low nullifier.
51
+ * Creates a new leaf from a buffer.
52
+ * @param key - Key of the leaf.
35
53
  */
36
- siblingPath: SiblingPath<N>;
54
+ buildDummy(key: bigint): IndexedTreeLeaf;
37
55
  /**
38
- * The index of low nullifier.
56
+ * Creates a new leaf from a buffer.
57
+ * @param buffer - Buffer to create a leaf from.
39
58
  */
40
- index: bigint;
59
+ fromBuffer(buffer: Buffer): IndexedTreeLeaf;
41
60
  }
42
61
 
62
+ export const buildDbKeyForPreimage = (name: string, index: bigint) => {
63
+ return `${name}:leaf_by_index:${toBufferBE(index, 32).toString('hex')}`;
64
+ };
65
+
66
+ export const buildDbKeyForLeafIndex = (name: string, key: bigint) => {
67
+ return `${name}:leaf_index_by_leaf_key:${toBufferBE(key, 32).toString('hex')}`;
68
+ };
69
+
43
70
  /**
44
71
  * Pre-compute empty witness.
45
72
  * @param treeHeight - Height of tree for sibling path.
46
73
  * @returns An empty witness.
47
74
  */
48
- function getEmptyLowLeafWitness<N extends number>(treeHeight: N): LowLeafWitnessData<N> {
75
+ function getEmptyLowLeafWitness<N extends number>(
76
+ treeHeight: N,
77
+ leafPreimageFactory: PreimageFactory,
78
+ ): LowLeafWitnessData<N> {
49
79
  return {
50
- leafData: zeroLeaf,
80
+ leafPreimage: leafPreimageFactory.empty(),
51
81
  index: 0n,
52
82
  siblingPath: new SiblingPath(treeHeight, Array(treeHeight).fill(toBufferBE(0n, 32))),
53
83
  };
54
84
  }
55
85
 
56
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
57
- const encodeTreeValue = (leafData: LeafData) => {
58
- const valueAsBuffer = toBufferBE(leafData.value, 32);
59
- const indexAsBuffer = toBufferBE(leafData.nextIndex, 32);
60
- const nextValueAsBuffer = toBufferBE(leafData.nextValue, 32);
61
- return Buffer.concat([valueAsBuffer, indexAsBuffer, nextValueAsBuffer]);
62
- };
63
-
64
- const decodeTreeValue = (buf: Buffer) => {
65
- const value = toBigIntBE(buf.subarray(0, 32));
66
- const nextIndex = toBigIntBE(buf.subarray(32, 64));
67
- const nextValue = toBigIntBE(buf.subarray(64, 96));
68
- return {
69
- value,
70
- nextIndex,
71
- nextValue,
72
- } as LeafData;
73
- };
74
-
75
86
  /**
76
- * Indexed merkle tree.
87
+ * Standard implementation of an indexed tree.
77
88
  */
78
89
  export class StandardIndexedTree extends TreeBase implements IndexedTree {
79
- protected leaves: LeafData[] = [];
80
- protected cachedLeaves: { [key: number]: LeafData } = {};
90
+ #snapshotBuilder = new IndexedTreeSnapshotBuilder(this.db, this, this.leafPreimageFactory);
91
+ protected cachedLeafPreimages: { [key: string]: IndexedTreeLeafPreimage } = {};
92
+
93
+ public constructor(
94
+ db: LevelUp,
95
+ hasher: Hasher,
96
+ name: string,
97
+ depth: number,
98
+ size: bigint = 0n,
99
+ protected leafPreimageFactory: PreimageFactory,
100
+ protected leafFactory: LeafFactory,
101
+ root?: Buffer,
102
+ ) {
103
+ super(db, hasher, name, depth, size, root);
104
+ }
81
105
 
82
106
  /**
83
107
  * Appends the given leaves to the tree.
@@ -85,7 +109,7 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
85
109
  * @returns Empty promise.
86
110
  * @remarks Use batchInsert method instead.
87
111
  */
88
- public appendLeaves(_leaves: Buffer[]): Promise<void> {
112
+ appendLeaves(_leaves: Buffer[]): Promise<void> {
89
113
  throw new Error('Not implemented');
90
114
  }
91
115
 
@@ -113,88 +137,149 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
113
137
  * @param includeUncommitted - Indicates whether to include uncommitted leaves in the computation.
114
138
  * @returns The value of the leaf at the given index or undefined if the leaf is empty.
115
139
  */
116
- public getLeafValue(index: bigint, includeUncommitted: boolean): Promise<Buffer | undefined> {
117
- const leaf = this.getLatestLeafDataCopy(Number(index), includeUncommitted);
118
- if (!leaf) {
119
- return Promise.resolve(undefined);
120
- }
121
- return Promise.resolve(toBufferBE(leaf.value, 32));
140
+ public async getLeafValue(index: bigint, includeUncommitted: boolean): Promise<Buffer | undefined> {
141
+ const preimage = await this.getLatestLeafPreimageCopy(index, includeUncommitted);
142
+ return preimage && preimage.toBuffer();
122
143
  }
123
144
 
124
145
  /**
125
146
  * Finds the index of the largest leaf whose value is less than or equal to the provided value.
126
- * @param newValue - The new value to be inserted into the tree.
147
+ * @param newKey - The new key to be inserted into the tree.
127
148
  * @param includeUncommitted - If true, the uncommitted changes are included in the search.
128
149
  * @returns The found leaf index and a flag indicating if the corresponding leaf's value is equal to `newValue`.
129
150
  */
130
- findIndexOfPreviousValue(
131
- newValue: bigint,
151
+ async findIndexOfPreviousKey(
152
+ newKey: bigint,
132
153
  includeUncommitted: boolean,
133
- ): {
134
- /**
135
- * The index of the found leaf.
136
- */
137
- index: number;
138
- /**
139
- * A flag indicating if the corresponding leaf's value is equal to `newValue`.
140
- */
141
- alreadyPresent: boolean;
142
- } {
143
- const numLeaves = this.getNumLeaves(includeUncommitted);
144
- const diff: bigint[] = [];
145
-
146
- for (let i = 0; i < numLeaves; i++) {
147
- const storedLeaf = this.getLatestLeafDataCopy(i, includeUncommitted)!;
148
-
149
- // The stored leaf can be undefined if it addresses an empty leaf
150
- // If the leaf is empty we do the same as if the leaf was larger
151
- if (storedLeaf === undefined) {
152
- diff.push(newValue);
153
- } else if (storedLeaf.value > newValue) {
154
- diff.push(newValue);
155
- } else if (storedLeaf.value === newValue) {
156
- return { index: i, alreadyPresent: true };
157
- } else {
158
- diff.push(newValue - storedLeaf.value);
154
+ ): Promise<
155
+ | {
156
+ /**
157
+ * The index of the found leaf.
158
+ */
159
+ index: bigint;
160
+ /**
161
+ * A flag indicating if the corresponding leaf's value is equal to `newValue`.
162
+ */
163
+ alreadyPresent: boolean;
159
164
  }
165
+ | undefined
166
+ > {
167
+ let lowLeafIndex = await this.getDbLowLeafIndex(newKey);
168
+ let lowLeafPreimage = lowLeafIndex !== undefined ? await this.getDbPreimage(lowLeafIndex) : undefined;
169
+
170
+ if (includeUncommitted) {
171
+ const cachedLowLeafIndex = this.getCachedLowLeafIndex(newKey);
172
+ if (cachedLowLeafIndex !== undefined) {
173
+ const cachedLowLeafPreimage = this.getCachedPreimage(cachedLowLeafIndex)!;
174
+ if (!lowLeafPreimage || cachedLowLeafPreimage.getKey() > lowLeafPreimage.getKey()) {
175
+ lowLeafIndex = cachedLowLeafIndex;
176
+ lowLeafPreimage = cachedLowLeafPreimage;
177
+ }
178
+ }
179
+ }
180
+
181
+ if (lowLeafIndex === undefined || !lowLeafPreimage) {
182
+ return undefined;
160
183
  }
161
- const minIndex = this.findMinIndex(diff);
162
- return { index: minIndex, alreadyPresent: false };
184
+
185
+ return {
186
+ index: lowLeafIndex,
187
+ alreadyPresent: lowLeafPreimage.getKey() === newKey,
188
+ };
189
+ }
190
+
191
+ private getCachedLowLeafIndex(key: bigint): bigint | undefined {
192
+ const indexes = Object.getOwnPropertyNames(this.cachedLeafPreimages);
193
+ const lowLeafIndexes = indexes
194
+ .map(index => ({
195
+ index: BigInt(index),
196
+ key: this.cachedLeafPreimages[index].getKey(),
197
+ }))
198
+ .filter(({ key: candidateKey }) => candidateKey <= key)
199
+ .sort((a, b) => Number(b.key - a.key));
200
+ return lowLeafIndexes[0]?.index;
201
+ }
202
+
203
+ private getCachedLeafIndex(key: bigint): bigint | undefined {
204
+ const index = Object.keys(this.cachedLeafPreimages).find(index => {
205
+ return this.cachedLeafPreimages[index].getKey() === key;
206
+ });
207
+ if (index) {
208
+ return BigInt(index);
209
+ }
210
+ return undefined;
211
+ }
212
+
213
+ private async getDbLowLeafIndex(key: bigint): Promise<bigint | undefined> {
214
+ return await new Promise<bigint | undefined>((resolve, reject) => {
215
+ let lowLeafIndex: bigint | undefined;
216
+ this.db
217
+ .createReadStream({
218
+ gte: buildDbKeyForLeafIndex(this.getName(), 0n),
219
+ lte: buildDbKeyForLeafIndex(this.getName(), key),
220
+ limit: 1,
221
+ reverse: true,
222
+ })
223
+ .on('data', data => {
224
+ lowLeafIndex = toBigIntBE(data.value);
225
+ })
226
+ .on('close', function () {})
227
+ .on('end', function () {
228
+ resolve(lowLeafIndex);
229
+ })
230
+ .on('error', function () {
231
+ log.error('stream error');
232
+ reject();
233
+ });
234
+ });
235
+ }
236
+
237
+ private async getDbPreimage(index: bigint): Promise<IndexedTreeLeafPreimage | undefined> {
238
+ const dbPreimage = await this.db
239
+ .get(buildDbKeyForPreimage(this.getName(), index))
240
+ .then(data => this.leafPreimageFactory.fromBuffer(data))
241
+ .catch(() => undefined);
242
+ return dbPreimage;
243
+ }
244
+
245
+ private getCachedPreimage(index: bigint): IndexedTreeLeafPreimage | undefined {
246
+ return this.cachedLeafPreimages[index.toString()];
163
247
  }
164
248
 
165
249
  /**
166
- * Gets the latest LeafData copy.
167
- * @param index - Index of the leaf of which to obtain the LeafData copy.
250
+ * Gets the latest LeafPreimage copy.
251
+ * @param index - Index of the leaf of which to obtain the LeafPreimage copy.
168
252
  * @param includeUncommitted - If true, the uncommitted changes are included in the search.
169
- * @returns A copy of the leaf data at the given index or undefined if the leaf was not found.
253
+ * @returns A copy of the leaf preimage at the given index or undefined if the leaf was not found.
170
254
  */
171
- public getLatestLeafDataCopy(index: number, includeUncommitted: boolean): LeafData | undefined {
172
- const leaf = !includeUncommitted ? this.leaves[index] : this.cachedLeaves[index] ?? this.leaves[index];
173
- return leaf
174
- ? ({
175
- value: leaf.value,
176
- nextIndex: leaf.nextIndex,
177
- nextValue: leaf.nextValue,
178
- } as LeafData)
179
- : undefined;
255
+ public async getLatestLeafPreimageCopy(
256
+ index: bigint,
257
+ includeUncommitted: boolean,
258
+ ): Promise<IndexedTreeLeafPreimage | undefined> {
259
+ const preimage = !includeUncommitted
260
+ ? await this.getDbPreimage(index)
261
+ : this.getCachedPreimage(index) ?? (await this.getDbPreimage(index));
262
+ return preimage && this.leafPreimageFactory.clone(preimage);
180
263
  }
181
264
 
182
265
  /**
183
- * Finds the index of the minimum value in an array.
184
- * @param values - The collection of values to be searched.
185
- * @returns The index of the minimum value in the array.
266
+ * Returns the index of a leaf given its value, or undefined if no leaf with that value is found.
267
+ * @param value - The leaf value to look for.
268
+ * @param includeUncommitted - Indicates whether to include uncommitted data.
269
+ * @returns The index of the first leaf found with a given value (undefined if not found).
186
270
  */
187
- private findMinIndex(values: bigint[]) {
188
- if (!values.length) {
189
- return 0;
190
- }
191
- let minIndex = 0;
192
- for (let i = 1; i < values.length; i++) {
193
- if (values[minIndex] > values[i]) {
194
- minIndex = i;
195
- }
271
+ public async findLeafIndex(value: Buffer, includeUncommitted: boolean): Promise<bigint | undefined> {
272
+ const leaf = this.leafFactory.fromBuffer(value);
273
+ let index = await this.db
274
+ .get(buildDbKeyForLeafIndex(this.getName(), leaf.getKey()))
275
+ .then(data => toBigIntBE(data))
276
+ .catch(() => undefined);
277
+
278
+ if (includeUncommitted && index === undefined) {
279
+ const cachedIndex = this.getCachedLeafIndex(leaf.getKey());
280
+ index = cachedIndex;
196
281
  }
197
- return minIndex;
282
+ return index;
198
283
  }
199
284
 
200
285
  /**
@@ -216,66 +301,31 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
216
301
  throw new Error(`Prefilled size must be at least 1!`);
217
302
  }
218
303
 
219
- const leaves: LeafData[] = [];
304
+ const leaves: IndexedTreeLeafPreimage[] = [];
220
305
  for (let i = 0n; i < prefilledSize; i++) {
221
- const newLeaf = {
222
- value: toBigIntBE(Buffer.from([Number(i)])),
223
- nextIndex: i + 1n,
224
- nextValue: i + 1n,
225
- };
226
- leaves.push(newLeaf);
306
+ const newLeaf = this.leafFactory.buildDummy(i);
307
+ const newLeafPreimage = this.leafPreimageFactory.fromLeaf(newLeaf, i + 1n, i + 1n);
308
+ leaves.push(newLeafPreimage);
227
309
  }
228
310
 
229
- // Make the first leaf have 0 value
230
- leaves[0].value = 0n;
231
-
232
311
  // Make the last leaf point to the first leaf
233
- leaves[prefilledSize - 1].nextIndex = 0n;
234
- leaves[prefilledSize - 1].nextValue = 0n;
312
+ leaves[prefilledSize - 1] = this.leafPreimageFactory.fromLeaf(leaves[prefilledSize - 1].asLeaf(), 0n, 0n);
235
313
 
236
314
  await this.encodeAndAppendLeaves(leaves, true);
237
315
  await this.commit();
238
316
  }
239
317
 
240
- /**
241
- * Loads Merkle tree data from a database and assigns them to this object.
242
- */
243
- public async initFromDb(): Promise<void> {
244
- const startingIndex = 0n;
245
- const values: LeafData[] = [];
246
- const promise = new Promise<void>((resolve, reject) => {
247
- this.db
248
- .createReadStream({
249
- gte: indexToKeyLeaf(this.getName(), startingIndex),
250
- lte: indexToKeyLeaf(this.getName(), 2n ** BigInt(this.getDepth())),
251
- })
252
- .on('data', function (data) {
253
- const index = keyLeafToIndex(data.key.toString('utf-8'));
254
- values[Number(index)] = decodeTreeValue(data.value);
255
- })
256
- .on('close', function () {})
257
- .on('end', function () {
258
- resolve();
259
- })
260
- .on('error', function () {
261
- log.error('stream error');
262
- reject();
263
- });
264
- });
265
- await promise;
266
- this.leaves = values;
267
- }
268
-
269
318
  /**
270
319
  * Commits all the leaves to the database and removes them from a cache.
271
320
  */
272
321
  private async commitLeaves(): Promise<void> {
273
322
  const batch = this.db.batch();
274
- const keys = Object.getOwnPropertyNames(this.cachedLeaves);
323
+ const keys = Object.getOwnPropertyNames(this.cachedLeafPreimages);
275
324
  for (const key of keys) {
276
- const index = Number(key);
277
- batch.put(indexToKeyLeaf(this.getName(), BigInt(index)), encodeTreeValue(this.cachedLeaves[index]));
278
- this.leaves[index] = this.cachedLeaves[index];
325
+ const leaf = this.cachedLeafPreimages[key];
326
+ const index = BigInt(key);
327
+ batch.put(buildDbKeyForPreimage(this.getName(), index), leaf.toBuffer());
328
+ batch.put(buildDbKeyForLeafIndex(this.getName(), leaf.getKey()), toBufferBE(index, 32));
279
329
  }
280
330
  await batch.write();
281
331
  this.clearCachedLeaves();
@@ -285,20 +335,21 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
285
335
  * Clears the cache.
286
336
  */
287
337
  private clearCachedLeaves() {
288
- this.cachedLeaves = {};
338
+ this.cachedLeafPreimages = {};
289
339
  }
290
340
 
291
341
  /**
292
342
  * Updates a leaf in the tree.
293
- * @param leaf - New contents of the leaf.
343
+ * @param preimage - New contents of the leaf.
294
344
  * @param index - Index of the leaf to be updated.
295
345
  */
296
- protected async updateLeaf(leaf: LeafData, index: bigint) {
346
+ protected async updateLeaf(preimage: IndexedTreeLeafPreimage, index: bigint) {
297
347
  if (index > this.maxIndex) {
298
348
  throw Error(`Index out of bounds. Index ${index}, max index: ${this.maxIndex}.`);
299
349
  }
300
350
 
301
- const encodedLeaf = this.encodeLeaf(leaf, true);
351
+ this.cachedLeafPreimages[index.toString()] = preimage;
352
+ const encodedLeaf = this.encodeLeaf(preimage, true);
302
353
  await this.addLeafToCacheAndHashToRoot(encodedLeaf, index);
303
354
  const numLeaves = this.getNumLeaves(true);
304
355
  if (index >= numLeaves) {
@@ -317,8 +368,6 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
317
368
  *
318
369
  * This offers massive circuit performance savings over doing incremental insertions.
319
370
  *
320
- * A description of the algorithm can be found here: https://colab.research.google.com/drive/1A0gizduSi4FIiIJZ8OylwIpO9-OTqV-R
321
- *
322
371
  * WARNING: This function has side effects, it will insert values into the tree.
323
372
  *
324
373
  * Assumptions:
@@ -338,81 +387,78 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
338
387
  * roots.
339
388
  *
340
389
  * This become tricky when two items that are being batch inserted need to update the same low nullifier, or need to use
341
- * a value that is part of the same batch insertion as their low nullifier. In this case a zero low nullifier path is given
342
- * to the circuit, and it must determine from the set of batch inserted values if the insertion is valid.
390
+ * a value that is part of the same batch insertion as their low nullifier. What we do to avoid this case is to
391
+ * update the existing leaves in the tree with the nullifiers in high to low order, ensuring that this case never occurs.
392
+ * The circuit has to sort the nullifiers (or take a hint of the sorted nullifiers and prove that it's a valid permutation).
393
+ * Then we just batch insert the new nullifiers in the original order.
343
394
  *
344
395
  * The following example will illustrate attempting to insert 2,3,20,19 into a tree already containing 0,5,10,15
345
396
  *
346
397
  * The example will explore two cases. In each case the values low nullifier will exist within the batch insertion,
347
398
  * One where the low nullifier comes before the item in the set (2,3), and one where it comes after (20,19).
348
399
  *
400
+ * First, we sort the nullifiers high to low, that's 20,19,3,2
401
+ *
349
402
  * The original tree: Pending insertion subtree
350
403
  *
351
- * index 0 2 3 4 - - - -
404
+ * index 0 1 2 3 - - - -
352
405
  * ------------------------------------- ----------------------------
353
406
  * val 0 5 10 15 - - - -
354
407
  * nextIdx 1 2 3 0 - - - -
355
408
  * nextVal 5 10 15 0 - - - -
356
409
  *
357
410
  *
358
- * Inserting 2: (happy path)
359
- * 1. Find the low nullifier (0) - provide inclusion proof
411
+ * Inserting 20:
412
+ * 1. Find the low nullifier (3) - provide inclusion proof
360
413
  * 2. Update its pointers
361
- * 3. Insert 2 into the pending subtree
414
+ * 3. Insert 20 into the pending subtree
362
415
  *
363
- * index 0 2 3 4 5 - - -
416
+ * index 0 1 2 3 - - 6 -
364
417
  * ------------------------------------- ----------------------------
365
- * val 0 5 10 15 2 - - -
366
- * nextIdx 5 2 3 0 2 - - -
367
- * nextVal 2 10 15 0 5 - - -
418
+ * val 0 5 10 15 - - 20 -
419
+ * nextIdx 1 2 3 6 - - 0 -
420
+ * nextVal 5 10 15 20 - - 0 -
368
421
  *
369
- * Inserting 3: The low nullifier exists within the insertion current subtree
370
- * 1. When looking for the low nullifier for 3, we will receive 0 again as we have not inserted 2 into the main tree
371
- * This is problematic, as we cannot use either 0 or 2 as our inclusion proof.
372
- * Why cant we?
373
- * - Index 0 has a val 0 and nextVal of 2. This is NOT enough to prove non inclusion of 2.
374
- * - Our existing tree is in a state where we cannot prove non inclusion of 3.
375
- * We do not provide a non inclusion proof to out circuit, but prompt it to look within the insertion subtree.
376
- * 2. Update pending insertion subtree
377
- * 3. Insert 3 into pending subtree
422
+ * Inserting 19:
423
+ * 1. Find the low nullifier (3) - provide inclusion proof
424
+ * 2. Update its pointers
425
+ * 3. Insert 19 into the pending subtree
378
426
  *
379
- * (no inclusion proof provided)
380
- * index 0 2 3 4 5 6 - -
427
+ * index 0 1 2 3 - - 6 7
381
428
  * ------------------------------------- ----------------------------
382
- * val 0 5 10 15 2 3 - -
383
- * nextIdx 5 2 3 0 6 2 - -
384
- * nextVal 2 10 15 0 3 5 - -
429
+ * val 0 5 10 15 - - 20 19
430
+ * nextIdx 1 2 3 7 - - 0 6
431
+ * nextVal 5 10 15 19 - - 0 20
385
432
  *
386
- * Inserting 20: (happy path)
387
- * 1. Find the low nullifier (15) - provide inclusion proof
433
+ * Inserting 3:
434
+ * 1. Find the low nullifier (0) - provide inclusion proof
388
435
  * 2. Update its pointers
389
- * 3. Insert 20 into the pending subtree
436
+ * 3. Insert 3 into the pending subtree
390
437
  *
391
- * index 0 2 3 4 5 6 7 -
438
+ * index 0 1 2 3 - 5 6 7
392
439
  * ------------------------------------- ----------------------------
393
- * val 0 5 10 15 2 3 20 -
394
- * nextIdx 5 2 3 7 6 2 0 -
395
- * nextVal 2 10 15 20 3 5 0 -
440
+ * val 0 5 10 15 - 3 20 19
441
+ * nextIdx 5 2 3 7 - 1 0 6
442
+ * nextVal 3 10 15 19 - 5 0 20
396
443
  *
397
- * Inserting 19:
398
- * 1. In this case we can find a low nullifier, but we are updating a low nullifier that has already been updated
399
- * We can provide an inclusion proof of this intermediate tree state.
444
+ * Inserting 2:
445
+ * 1. Find the low nullifier (0) - provide inclusion proof
400
446
  * 2. Update its pointers
401
- * 3. Insert 19 into the pending subtree
447
+ * 3. Insert 2 into the pending subtree
402
448
  *
403
- * index 0 2 3 4 5 6 7 8
449
+ * index 0 1 2 3 4 5 6 7
404
450
  * ------------------------------------- ----------------------------
405
- * val 0 5 10 15 2 3 20 19
406
- * nextIdx 5 2 3 8 6 2 0 7
407
- * nextVal 2 10 15 19 3 5 0 20
451
+ * val 0 5 10 15 2 3 20 19
452
+ * nextIdx 4 2 3 7 5 1 0 6
453
+ * nextVal 2 10 15 19 3 5 0 20
408
454
  *
409
455
  * Perform subtree insertion
410
456
  *
411
- * index 0 2 3 4 5 6 7 8
457
+ * index 0 1 2 3 4 5 6 7
412
458
  * ---------------------------------------------------------------------
413
- * val 0 5 10 15 2 3 20 19
414
- * nextIdx 5 2 3 8 6 2 0 7
415
- * nextVal 2 10 15 19 3 5 0 20
459
+ * val 0 5 10 15 2 3 20 19
460
+ * nextIdx 4 2 3 7 5 1 0 6
461
+ * nextVal 2 10 15 19 3 5 0 20
416
462
  *
417
463
  * TODO: this implementation will change once the zero value is changed from h(0,0,0). Changes incoming over the next sprint
418
464
  * @param leaves - Values to insert into the tree.
@@ -426,107 +472,70 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
426
472
  >(
427
473
  leaves: Buffer[],
428
474
  subtreeHeight: SubtreeHeight,
429
- ): Promise<
430
- | [LowLeafWitnessData<TreeHeight>[], SiblingPath<SubtreeSiblingPathHeight>]
431
- | [undefined, SiblingPath<SubtreeSiblingPathHeight>]
432
- > {
433
- // Keep track of touched low leaves
434
- const touched = new Map<number, bigint[]>();
435
-
436
- const emptyLowLeafWitness = getEmptyLowLeafWitness(this.getDepth() as TreeHeight);
475
+ ): Promise<BatchInsertionResult<TreeHeight, SubtreeSiblingPathHeight>> {
476
+ const emptyLowLeafWitness = getEmptyLowLeafWitness(this.getDepth() as TreeHeight, this.leafPreimageFactory);
437
477
  // Accumulators
438
- const lowLeavesWitnesses: LowLeafWitnessData<TreeHeight>[] = [];
439
- const pendingInsertionSubtree: LeafData[] = [];
478
+ const lowLeavesWitnesses: LowLeafWitnessData<TreeHeight>[] = leaves.map(() => emptyLowLeafWitness);
479
+ const pendingInsertionSubtree: IndexedTreeLeafPreimage[] = leaves.map(() => this.leafPreimageFactory.empty());
440
480
 
441
481
  // Start info
442
482
  const startInsertionIndex = this.getNumLeaves(true);
443
483
 
484
+ const leavesToInsert = leaves.map(leaf => this.leafFactory.fromBuffer(leaf));
485
+ const sortedDescendingLeafTuples = leavesToInsert
486
+ .map((leaf, index) => ({ leaf, index }))
487
+ .sort((a, b) => Number(b.leaf.getKey() - a.leaf.getKey()));
488
+ const sortedDescendingLeaves = sortedDescendingLeafTuples.map(leafTuple => leafTuple.leaf);
489
+
444
490
  // Get insertion path for each leaf
445
- for (let i = 0; i < leaves.length; i++) {
446
- const newValue = toBigIntBE(leaves[i]);
491
+ for (let i = 0; i < leavesToInsert.length; i++) {
492
+ const newLeaf = sortedDescendingLeaves[i];
493
+ const originalIndex = leavesToInsert.indexOf(newLeaf);
447
494
 
448
- // Keep space and just insert zero values
449
- if (newValue === 0n) {
450
- pendingInsertionSubtree.push(zeroLeaf);
451
- lowLeavesWitnesses.push(emptyLowLeafWitness);
495
+ if (newLeaf.isEmpty()) {
452
496
  continue;
453
497
  }
454
498
 
455
- const indexOfPrevious = this.findIndexOfPreviousValue(newValue, true);
456
-
457
- // If a touched node has a value that is less than the current value
458
- const prevNodes = touched.get(indexOfPrevious.index);
459
- if (prevNodes && prevNodes.some(v => v < newValue)) {
460
- // check the pending low nullifiers for a low nullifier that works
461
- // This is the case where the next value is less than the pending
462
- for (let j = 0; j < pendingInsertionSubtree.length; j++) {
463
- if (pendingInsertionSubtree[j].value === 0n) {
464
- continue;
465
- }
466
-
467
- if (
468
- pendingInsertionSubtree[j].value < newValue &&
469
- (pendingInsertionSubtree[j].nextValue > newValue || pendingInsertionSubtree[j].nextValue === 0n)
470
- ) {
471
- // add the new value to the pending low nullifiers
472
- const currentLowLeaf: LeafData = {
473
- value: newValue,
474
- nextValue: pendingInsertionSubtree[j].nextValue,
475
- nextIndex: pendingInsertionSubtree[j].nextIndex,
476
- };
477
-
478
- pendingInsertionSubtree.push(currentLowLeaf);
479
-
480
- // Update the pending low leaf to point at the new value
481
- pendingInsertionSubtree[j].nextValue = newValue;
482
- pendingInsertionSubtree[j].nextIndex = startInsertionIndex + BigInt(i);
483
-
484
- break;
485
- }
486
- }
499
+ const indexOfPrevious = await this.findIndexOfPreviousKey(newLeaf.getKey(), true);
500
+ if (indexOfPrevious === undefined) {
501
+ return {
502
+ lowLeavesWitnessData: undefined,
503
+ sortedNewLeaves: sortedDescendingLeafTuples.map(leafTuple => leafTuple.leaf.toBuffer()),
504
+ sortedNewLeavesIndexes: sortedDescendingLeafTuples.map(leafTuple => leafTuple.index),
505
+ newSubtreeSiblingPath: await this.getSubtreeSiblingPath(subtreeHeight, true),
506
+ };
507
+ }
487
508
 
488
- // Any node updated in this space will need to calculate its low nullifier from a previously inserted value
489
- lowLeavesWitnesses.push(emptyLowLeafWitness);
490
- } else {
491
- // Update the touched mapping
492
- if (prevNodes) {
493
- prevNodes.push(newValue);
494
- touched.set(indexOfPrevious.index, prevNodes);
495
- } else {
496
- touched.set(indexOfPrevious.index, [newValue]);
497
- }
509
+ // get the low leaf (existence checked in getting index)
510
+ const lowLeafPreimage = (await this.getLatestLeafPreimageCopy(indexOfPrevious.index, true))!;
511
+ const siblingPath = await this.getSiblingPath<TreeHeight>(BigInt(indexOfPrevious.index), true);
498
512
 
499
- // get the low leaf
500
- const lowLeaf = this.getLatestLeafDataCopy(indexOfPrevious.index, true);
501
- if (lowLeaf === undefined) {
502
- return [undefined, await this.getSubtreeSiblingPath(subtreeHeight, true)];
503
- }
504
- const siblingPath = await this.getSiblingPath<TreeHeight>(BigInt(indexOfPrevious.index), true);
505
-
506
- const witness: LowLeafWitnessData<TreeHeight> = {
507
- leafData: { ...lowLeaf },
508
- index: BigInt(indexOfPrevious.index),
509
- siblingPath,
510
- };
513
+ const witness: LowLeafWitnessData<TreeHeight> = {
514
+ leafPreimage: lowLeafPreimage,
515
+ index: BigInt(indexOfPrevious.index),
516
+ siblingPath,
517
+ };
511
518
 
512
- // Update the running paths
513
- lowLeavesWitnesses.push(witness);
519
+ // Update the running paths
520
+ lowLeavesWitnesses[i] = witness;
514
521
 
515
- const currentLowLeaf: LeafData = {
516
- value: newValue,
517
- nextValue: lowLeaf.nextValue,
518
- nextIndex: lowLeaf.nextIndex,
519
- };
522
+ const currentPendingPreimageLeaf = this.leafPreimageFactory.fromLeaf(
523
+ newLeaf,
524
+ lowLeafPreimage.getNextKey(),
525
+ lowLeafPreimage.getNextIndex(),
526
+ );
520
527
 
521
- pendingInsertionSubtree.push(currentLowLeaf);
528
+ pendingInsertionSubtree[originalIndex] = currentPendingPreimageLeaf;
522
529
 
523
- lowLeaf.nextValue = newValue;
524
- lowLeaf.nextIndex = startInsertionIndex + BigInt(i);
530
+ const newLowLeafPreimage = this.leafPreimageFactory.fromLeaf(
531
+ lowLeafPreimage.asLeaf(),
532
+ newLeaf.getKey(),
533
+ startInsertionIndex + BigInt(originalIndex),
534
+ );
525
535
 
526
- const lowLeafIndex = indexOfPrevious.index;
527
- this.cachedLeaves[lowLeafIndex] = lowLeaf;
528
- await this.updateLeaf(lowLeaf, BigInt(lowLeafIndex));
529
- }
536
+ const lowLeafIndex = indexOfPrevious.index;
537
+ this.cachedLeafPreimages[lowLeafIndex.toString()] = newLowLeafPreimage;
538
+ await this.updateLeaf(newLowLeafPreimage, lowLeafIndex);
530
539
  }
531
540
 
532
541
  const newSubtreeSiblingPath = await this.getSubtreeSiblingPath<SubtreeHeight, SubtreeSiblingPathHeight>(
@@ -538,7 +547,13 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
538
547
  // Note: In this case we set `hash0Leaf` param to false because batch insertion algorithm use forced null leaf
539
548
  // inclusion. See {@link encodeLeaf} for a more through param explanation.
540
549
  await this.encodeAndAppendLeaves(pendingInsertionSubtree, false);
541
- return [lowLeavesWitnesses, newSubtreeSiblingPath];
550
+
551
+ return {
552
+ lowLeavesWitnessData: lowLeavesWitnesses,
553
+ sortedNewLeaves: sortedDescendingLeafTuples.map(leafTuple => leafTuple.leaf.toBuffer()),
554
+ sortedNewLeavesIndexes: sortedDescendingLeafTuples.map(leafTuple => leafTuple.index),
555
+ newSubtreeSiblingPath,
556
+ };
542
557
  }
543
558
 
544
559
  async getSubtreeSiblingPath<SubtreeHeight extends number, SubtreeSiblingPathHeight extends number>(
@@ -552,21 +567,29 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
552
567
  return fullSiblingPath.getSubtreeSiblingPath(subtreeHeight);
553
568
  }
554
569
 
570
+ snapshot(blockNumber: number): Promise<IndexedTreeSnapshot> {
571
+ return this.#snapshotBuilder.snapshot(blockNumber);
572
+ }
573
+
574
+ getSnapshot(block: number): Promise<IndexedTreeSnapshot> {
575
+ return this.#snapshotBuilder.getSnapshot(block);
576
+ }
577
+
555
578
  /**
556
579
  * Encodes leaves and appends them to a tree.
557
- * @param leaves - Leaves to encode.
580
+ * @param preimages - Leaves to encode.
558
581
  * @param hash0Leaf - Indicates whether 0 value leaf should be hashed. See {@link encodeLeaf}.
559
582
  * @returns Empty promise
560
583
  */
561
- private async encodeAndAppendLeaves(leaves: LeafData[], hash0Leaf: boolean): Promise<void> {
562
- const startInsertionIndex = Number(this.getNumLeaves(true));
584
+ private async encodeAndAppendLeaves(preimages: IndexedTreeLeafPreimage[], hash0Leaf: boolean): Promise<void> {
585
+ const startInsertionIndex = this.getNumLeaves(true);
563
586
 
564
- const serializedLeaves = leaves.map((leaf, i) => {
565
- this.cachedLeaves[startInsertionIndex + i] = leaf;
566
- return this.encodeLeaf(leaf, hash0Leaf);
587
+ const hashedLeaves = preimages.map((preimage, i) => {
588
+ this.cachedLeafPreimages[(startInsertionIndex + BigInt(i)).toString()] = preimage;
589
+ return this.encodeLeaf(preimage, hash0Leaf);
567
590
  });
568
591
 
569
- await super.appendLeaves(serializedLeaves);
592
+ await super.appendLeaves(hashedLeaves);
570
593
  }
571
594
 
572
595
  /**
@@ -577,14 +600,12 @@ export class StandardIndexedTree extends TreeBase implements IndexedTree {
577
600
  * nullifier it is improbable that a valid nullifier would be 0.
578
601
  * @returns Leaf encoded in a buffer.
579
602
  */
580
- private encodeLeaf(leaf: LeafData, hash0Leaf: boolean): Buffer {
603
+ private encodeLeaf(leaf: IndexedTreeLeafPreimage, hash0Leaf: boolean): Buffer {
581
604
  let encodedLeaf;
582
- if (!hash0Leaf && leaf.value == 0n) {
605
+ if (!hash0Leaf && leaf.getKey() == 0n) {
583
606
  encodedLeaf = toBufferBE(0n, 32);
584
607
  } else {
585
- encodedLeaf = this.hasher.hashInputs(
586
- [leaf.value, leaf.nextIndex, leaf.nextValue].map(val => toBufferBE(val, 32)),
587
- );
608
+ encodedLeaf = this.hasher.hashInputs(leaf.toHashInputs());
588
609
  }
589
610
  return encodedLeaf;
590
611
  }