@aztec/merkle-tree 0.23.0 → 0.26.1
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.
- package/dest/index.d.ts +1 -0
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -1
- package/dest/pedersen.js +3 -3
- package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.d.ts +1 -1
- package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.d.ts.map +1 -1
- package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.js +2 -2
- package/package.json +5 -5
- package/src/hasher_with_stats.ts +51 -0
- package/src/index.ts +16 -0
- package/src/interfaces/append_only_tree.ts +13 -0
- package/src/interfaces/indexed_tree.ts +118 -0
- package/src/interfaces/merkle_tree.ts +60 -0
- package/src/interfaces/update_only_tree.ts +14 -0
- package/src/load_tree.ts +23 -0
- package/src/new_tree.ts +27 -0
- package/src/pedersen.ts +25 -0
- package/src/snapshots/append_only_snapshot.ts +262 -0
- package/src/snapshots/base_full_snapshot.ts +215 -0
- package/src/snapshots/full_snapshot.ts +26 -0
- package/src/snapshots/indexed_tree_snapshot.ts +108 -0
- package/src/snapshots/snapshot_builder.ts +84 -0
- package/src/snapshots/snapshot_builder_test_suite.ts +218 -0
- package/src/sparse_tree/sparse_tree.ts +49 -0
- package/src/standard_indexed_tree/standard_indexed_tree.ts +631 -0
- package/src/standard_indexed_tree/test/standard_indexed_tree_with_append.ts +81 -0
- package/src/standard_tree/standard_tree.ts +54 -0
- package/src/tree_base.ts +334 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { TreeBase } from '../tree_base.js';
|
|
2
|
+
import { TreeSnapshotBuilder } from './snapshot_builder.js';
|
|
3
|
+
|
|
4
|
+
/** Creates a test suit for snapshots */
|
|
5
|
+
export function describeSnapshotBuilderTestSuite<T extends TreeBase, S extends TreeSnapshotBuilder>(
|
|
6
|
+
getTree: () => T,
|
|
7
|
+
getSnapshotBuilder: () => S,
|
|
8
|
+
modifyTree: (tree: T) => Promise<void>,
|
|
9
|
+
) {
|
|
10
|
+
describe('SnapshotBuilder', () => {
|
|
11
|
+
let tree: T;
|
|
12
|
+
let snapshotBuilder: S;
|
|
13
|
+
let leaves: bigint[];
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tree = getTree();
|
|
17
|
+
snapshotBuilder = getSnapshotBuilder();
|
|
18
|
+
|
|
19
|
+
leaves = Array.from({ length: 4 }).map(() => BigInt(Math.floor(Math.random() * 2 ** tree.getDepth())));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('snapshot', () => {
|
|
23
|
+
it('takes snapshots', async () => {
|
|
24
|
+
await modifyTree(tree);
|
|
25
|
+
await tree.commit();
|
|
26
|
+
await expect(snapshotBuilder.snapshot(1)).resolves.toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is idempotent', async () => {
|
|
30
|
+
await modifyTree(tree);
|
|
31
|
+
await tree.commit();
|
|
32
|
+
|
|
33
|
+
const block = 1;
|
|
34
|
+
const snapshot = await snapshotBuilder.snapshot(block);
|
|
35
|
+
await expect(snapshotBuilder.snapshot(block)).resolves.toEqual(snapshot);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns the same path if tree has not diverged', async () => {
|
|
39
|
+
await modifyTree(tree);
|
|
40
|
+
await tree.commit();
|
|
41
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
42
|
+
|
|
43
|
+
const historicPaths = await Promise.all(leaves.map(leaf => snapshot.getSiblingPath(leaf)));
|
|
44
|
+
const expectedPaths = await Promise.all(leaves.map(leaf => tree.getSiblingPath(leaf, false)));
|
|
45
|
+
|
|
46
|
+
for (const [index, path] of historicPaths.entries()) {
|
|
47
|
+
expect(path).toEqual(expectedPaths[index]);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns historic paths if tree has diverged and no new snapshots have been taken', async () => {
|
|
52
|
+
await modifyTree(tree);
|
|
53
|
+
await tree.commit();
|
|
54
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
55
|
+
|
|
56
|
+
const expectedPaths = await Promise.all(leaves.map(leaf => tree.getSiblingPath(leaf, false)));
|
|
57
|
+
|
|
58
|
+
await modifyTree(tree);
|
|
59
|
+
await tree.commit();
|
|
60
|
+
|
|
61
|
+
const historicPaths = await Promise.all(leaves.map(leaf => snapshot.getSiblingPath(leaf)));
|
|
62
|
+
|
|
63
|
+
for (const [index, path] of historicPaths.entries()) {
|
|
64
|
+
expect(path).toEqual(expectedPaths[index]);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('retains old snapshots even if new one are created', async () => {
|
|
69
|
+
await modifyTree(tree);
|
|
70
|
+
await tree.commit();
|
|
71
|
+
|
|
72
|
+
const expectedPaths = await Promise.all(leaves.map(leaf => tree.getSiblingPath(leaf, false)));
|
|
73
|
+
|
|
74
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
75
|
+
|
|
76
|
+
await modifyTree(tree);
|
|
77
|
+
await tree.commit();
|
|
78
|
+
|
|
79
|
+
await snapshotBuilder.snapshot(2);
|
|
80
|
+
|
|
81
|
+
// check that snapshot 2 has not influenced snapshot(1) at all
|
|
82
|
+
const historicPaths = await Promise.all(leaves.map(leaf => snapshot.getSiblingPath(leaf)));
|
|
83
|
+
|
|
84
|
+
for (const [index, path] of historicPaths.entries()) {
|
|
85
|
+
expect(path).toEqual(expectedPaths[index]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('retains old snapshots even if new one are created and the tree diverges', async () => {
|
|
90
|
+
await modifyTree(tree);
|
|
91
|
+
await tree.commit();
|
|
92
|
+
|
|
93
|
+
const expectedPaths = await Promise.all(leaves.map(leaf => tree.getSiblingPath(leaf, false)));
|
|
94
|
+
|
|
95
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
96
|
+
|
|
97
|
+
await modifyTree(tree);
|
|
98
|
+
await tree.commit();
|
|
99
|
+
|
|
100
|
+
await snapshotBuilder.snapshot(2);
|
|
101
|
+
|
|
102
|
+
await modifyTree(tree);
|
|
103
|
+
await tree.commit();
|
|
104
|
+
|
|
105
|
+
// check that snapshot 2 has not influenced snapshot(1) at all
|
|
106
|
+
// and that the diverging tree does not influence the old snapshot
|
|
107
|
+
const historicPaths = await Promise.all(leaves.map(leaf => snapshot.getSiblingPath(leaf)));
|
|
108
|
+
|
|
109
|
+
for (const [index, path] of historicPaths.entries()) {
|
|
110
|
+
expect(path).toEqual(expectedPaths[index]);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getSnapshot', () => {
|
|
116
|
+
it('returns old snapshots', async () => {
|
|
117
|
+
await modifyTree(tree);
|
|
118
|
+
await tree.commit();
|
|
119
|
+
const expectedPaths = await Promise.all(leaves.map(leaf => tree.getSiblingPath(leaf, false)));
|
|
120
|
+
await snapshotBuilder.snapshot(1);
|
|
121
|
+
|
|
122
|
+
for (let i = 2; i < 5; i++) {
|
|
123
|
+
await modifyTree(tree);
|
|
124
|
+
await tree.commit();
|
|
125
|
+
await snapshotBuilder.snapshot(i);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const firstSnapshot = await snapshotBuilder.getSnapshot(1);
|
|
129
|
+
const historicPaths = await Promise.all(leaves.map(leaf => firstSnapshot.getSiblingPath(leaf)));
|
|
130
|
+
|
|
131
|
+
for (const [index, path] of historicPaths.entries()) {
|
|
132
|
+
expect(path).toEqual(expectedPaths[index]);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('throws if an unknown snapshot is requested', async () => {
|
|
137
|
+
await modifyTree(tree);
|
|
138
|
+
await tree.commit();
|
|
139
|
+
await snapshotBuilder.snapshot(1);
|
|
140
|
+
|
|
141
|
+
await expect(snapshotBuilder.getSnapshot(2)).rejects.toThrow();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('getRoot', () => {
|
|
146
|
+
it('returns the historical root of the tree when the snapshot was taken', async () => {
|
|
147
|
+
await modifyTree(tree);
|
|
148
|
+
await tree.commit();
|
|
149
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
150
|
+
const historicalRoot = tree.getRoot(false);
|
|
151
|
+
|
|
152
|
+
await modifyTree(tree);
|
|
153
|
+
await tree.commit();
|
|
154
|
+
|
|
155
|
+
expect(snapshot.getRoot()).toEqual(historicalRoot);
|
|
156
|
+
expect(snapshot.getRoot()).not.toEqual(tree.getRoot(false));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('getDepth', () => {
|
|
161
|
+
it('returns the same depth as the tree', async () => {
|
|
162
|
+
await modifyTree(tree);
|
|
163
|
+
await tree.commit();
|
|
164
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
165
|
+
expect(snapshot.getDepth()).toEqual(tree.getDepth());
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getNumLeaves', () => {
|
|
170
|
+
it('returns the historical leaves count when the snapshot was taken', async () => {
|
|
171
|
+
await modifyTree(tree);
|
|
172
|
+
await tree.commit();
|
|
173
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
174
|
+
const historicalNumLeaves = tree.getNumLeaves(false);
|
|
175
|
+
|
|
176
|
+
await modifyTree(tree);
|
|
177
|
+
await tree.commit();
|
|
178
|
+
|
|
179
|
+
expect(snapshot.getNumLeaves()).toEqual(historicalNumLeaves);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('getLeafValue', () => {
|
|
184
|
+
it('returns the historical leaf value when the snapshot was taken', async () => {
|
|
185
|
+
await modifyTree(tree);
|
|
186
|
+
await tree.commit();
|
|
187
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
188
|
+
const historicalLeafValue = tree.getLeafValue(0n, false);
|
|
189
|
+
expect(snapshot.getLeafValue(0n)).toEqual(historicalLeafValue);
|
|
190
|
+
|
|
191
|
+
await modifyTree(tree);
|
|
192
|
+
await tree.commit();
|
|
193
|
+
|
|
194
|
+
expect(snapshot.getLeafValue(0n)).toEqual(historicalLeafValue);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('findLeafIndex', () => {
|
|
199
|
+
it('returns the historical leaf index when the snapshot was taken', async () => {
|
|
200
|
+
await modifyTree(tree);
|
|
201
|
+
await tree.commit();
|
|
202
|
+
const snapshot = await snapshotBuilder.snapshot(1);
|
|
203
|
+
|
|
204
|
+
const initialLastLeafIndex = tree.getNumLeaves(false) - 1n;
|
|
205
|
+
let lastLeaf = tree.getLeafValue(initialLastLeafIndex, false);
|
|
206
|
+
expect(snapshot.findLeafIndex(lastLeaf!)).toBe(initialLastLeafIndex);
|
|
207
|
+
|
|
208
|
+
await modifyTree(tree);
|
|
209
|
+
await tree.commit();
|
|
210
|
+
|
|
211
|
+
const newLastLeafIndex = tree.getNumLeaves(false) - 1n;
|
|
212
|
+
lastLeaf = tree.getLeafValue(newLastLeafIndex, false);
|
|
213
|
+
|
|
214
|
+
expect(snapshot.findLeafIndex(lastLeaf!)).toBe(undefined);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { UpdateOnlyTree } from '../interfaces/update_only_tree.js';
|
|
2
|
+
import { FullTreeSnapshotBuilder } from '../snapshots/full_snapshot.js';
|
|
3
|
+
import { TreeSnapshot } from '../snapshots/snapshot_builder.js';
|
|
4
|
+
import { INITIAL_LEAF, TreeBase } from '../tree_base.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Merkle tree implementation that uses a LevelDB database to store the tree.
|
|
8
|
+
*/
|
|
9
|
+
export class SparseTree extends TreeBase implements UpdateOnlyTree {
|
|
10
|
+
#snapshotBuilder = new FullTreeSnapshotBuilder(this.store, this);
|
|
11
|
+
/**
|
|
12
|
+
* Updates a leaf in the tree.
|
|
13
|
+
* @param leaf - New contents of the leaf.
|
|
14
|
+
* @param index - Index of the leaf to be updated.
|
|
15
|
+
*/
|
|
16
|
+
public updateLeaf(leaf: Buffer, index: bigint): Promise<void> {
|
|
17
|
+
if (index > this.maxIndex) {
|
|
18
|
+
throw Error(`Index out of bounds. Index ${index}, max index: ${this.maxIndex}.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const insertingZeroElement = leaf.equals(INITIAL_LEAF);
|
|
22
|
+
const originallyZeroElement = this.getLeafValue(index, true)?.equals(INITIAL_LEAF);
|
|
23
|
+
if (insertingZeroElement && originallyZeroElement) {
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
this.addLeafToCacheAndHashToRoot(leaf, index);
|
|
27
|
+
if (insertingZeroElement) {
|
|
28
|
+
// Deleting element (originally non-zero and new value is zero)
|
|
29
|
+
this.cachedSize = (this.cachedSize ?? this.size) - 1n;
|
|
30
|
+
} else if (originallyZeroElement) {
|
|
31
|
+
// Inserting new element (originally zero and new value is non-zero)
|
|
32
|
+
this.cachedSize = (this.cachedSize ?? this.size) + 1n;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public snapshot(block: number): Promise<TreeSnapshot> {
|
|
39
|
+
return this.#snapshotBuilder.snapshot(block);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public getSnapshot(block: number): Promise<TreeSnapshot> {
|
|
43
|
+
return this.#snapshotBuilder.getSnapshot(block);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public findLeafIndex(_value: Buffer, _includeUncommitted: boolean): bigint | undefined {
|
|
47
|
+
throw new Error('Finding leaf index is not supported for sparse trees');
|
|
48
|
+
}
|
|
49
|
+
}
|