@aztec/merkle-tree 0.0.0-test.0
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/README.md +41 -0
- package/dest/hasher_with_stats.d.ts +22 -0
- package/dest/hasher_with_stats.d.ts.map +1 -0
- package/dest/hasher_with_stats.js +40 -0
- package/dest/index.d.ts +20 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +19 -0
- package/dest/interfaces/append_only_tree.d.ts +16 -0
- package/dest/interfaces/append_only_tree.d.ts.map +1 -0
- package/dest/interfaces/append_only_tree.js +3 -0
- package/dest/interfaces/indexed_tree.d.ts +69 -0
- package/dest/interfaces/indexed_tree.d.ts.map +1 -0
- package/dest/interfaces/indexed_tree.js +3 -0
- package/dest/interfaces/merkle_tree.d.ts +64 -0
- package/dest/interfaces/merkle_tree.d.ts.map +1 -0
- package/dest/interfaces/merkle_tree.js +3 -0
- package/dest/interfaces/update_only_tree.d.ts +17 -0
- package/dest/interfaces/update_only_tree.d.ts.map +1 -0
- package/dest/interfaces/update_only_tree.js +3 -0
- package/dest/load_tree.d.ts +16 -0
- package/dest/load_tree.d.ts.map +1 -0
- package/dest/load_tree.js +13 -0
- package/dest/new_tree.d.ts +16 -0
- package/dest/new_tree.d.ts.map +1 -0
- package/dest/new_tree.js +14 -0
- package/dest/pedersen.d.ts +13 -0
- package/dest/pedersen.d.ts.map +1 -0
- package/dest/pedersen.js +24 -0
- package/dest/poseidon.d.ts +13 -0
- package/dest/poseidon.d.ts.map +1 -0
- package/dest/poseidon.js +24 -0
- package/dest/sha_256.d.ts +22 -0
- package/dest/sha_256.d.ts.map +1 -0
- package/dest/sha_256.js +44 -0
- package/dest/snapshots/append_only_snapshot.d.ts +32 -0
- package/dest/snapshots/append_only_snapshot.d.ts.map +1 -0
- package/dest/snapshots/append_only_snapshot.js +216 -0
- package/dest/snapshots/base_full_snapshot.d.ts +66 -0
- package/dest/snapshots/base_full_snapshot.d.ts.map +1 -0
- package/dest/snapshots/base_full_snapshot.js +201 -0
- package/dest/snapshots/full_snapshot.d.ts +27 -0
- package/dest/snapshots/full_snapshot.d.ts.map +1 -0
- package/dest/snapshots/full_snapshot.js +23 -0
- package/dest/snapshots/indexed_tree_snapshot.d.ts +16 -0
- package/dest/snapshots/indexed_tree_snapshot.d.ts.map +1 -0
- package/dest/snapshots/indexed_tree_snapshot.js +79 -0
- package/dest/snapshots/snapshot_builder.d.ts +84 -0
- package/dest/snapshots/snapshot_builder.d.ts.map +1 -0
- package/dest/snapshots/snapshot_builder.js +1 -0
- package/dest/snapshots/snapshot_builder_test_suite.d.ts +6 -0
- package/dest/snapshots/snapshot_builder_test_suite.d.ts.map +1 -0
- package/dest/snapshots/snapshot_builder_test_suite.js +167 -0
- package/dest/sparse_tree/sparse_tree.d.ts +21 -0
- package/dest/sparse_tree/sparse_tree.d.ts.map +1 -0
- package/dest/sparse_tree/sparse_tree.js +44 -0
- package/dest/standard_indexed_tree/standard_indexed_tree.d.ts +291 -0
- package/dest/standard_indexed_tree/standard_indexed_tree.d.ts.map +1 -0
- package/dest/standard_indexed_tree/standard_indexed_tree.js +478 -0
- package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.d.ts +25 -0
- package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.d.ts.map +1 -0
- package/dest/standard_indexed_tree/test/standard_indexed_tree_with_append.js +56 -0
- package/dest/standard_tree/standard_tree.d.ts +23 -0
- package/dest/standard_tree/standard_tree.d.ts.map +1 -0
- package/dest/standard_tree/standard_tree.js +47 -0
- package/dest/tree_base.d.ts +158 -0
- package/dest/tree_base.d.ts.map +1 -0
- package/dest/tree_base.js +290 -0
- package/dest/unbalanced_tree.d.ts +109 -0
- package/dest/unbalanced_tree.d.ts.map +1 -0
- package/dest/unbalanced_tree.js +208 -0
- package/package.json +85 -0
- package/src/hasher_with_stats.ts +51 -0
- package/src/index.ts +19 -0
- package/src/interfaces/append_only_tree.ts +17 -0
- package/src/interfaces/indexed_tree.ts +83 -0
- package/src/interfaces/merkle_tree.ts +70 -0
- package/src/interfaces/update_only_tree.ts +18 -0
- package/src/load_tree.ts +33 -0
- package/src/new_tree.ts +29 -0
- package/src/pedersen.ts +27 -0
- package/src/poseidon.ts +27 -0
- package/src/sha_256.ts +49 -0
- package/src/snapshots/append_only_snapshot.ts +278 -0
- package/src/snapshots/base_full_snapshot.ts +222 -0
- package/src/snapshots/full_snapshot.ts +33 -0
- package/src/snapshots/indexed_tree_snapshot.ts +108 -0
- package/src/snapshots/snapshot_builder.ts +92 -0
- package/src/snapshots/snapshot_builder_test_suite.ts +226 -0
- package/src/sparse_tree/sparse_tree.ts +56 -0
- package/src/standard_indexed_tree/standard_indexed_tree.ts +640 -0
- package/src/standard_indexed_tree/test/standard_indexed_tree_with_append.ts +81 -0
- package/src/standard_tree/standard_tree.ts +60 -0
- package/src/tree_base.ts +359 -0
- package/src/unbalanced_tree.ts +239 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { StandardIndexedTree } from '../standard_indexed_tree.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A testing utility which is here to store the original implementation of StandardIndexedTree.appendLeaves method
|
|
5
|
+
* that was replaced by the more efficient batchInsert method. We keep the original implementation around as it useful
|
|
6
|
+
* for testing that the more complex batchInsert method works correctly.
|
|
7
|
+
*/
|
|
8
|
+
export class StandardIndexedTreeWithAppend extends StandardIndexedTree {
|
|
9
|
+
/**
|
|
10
|
+
* Appends the given leaves to the tree.
|
|
11
|
+
* @param leaves - The leaves to append.
|
|
12
|
+
* @returns Empty promise.
|
|
13
|
+
* @remarks This method is inefficient and is here mostly for testing. Use batchInsert instead.
|
|
14
|
+
*/
|
|
15
|
+
public override appendLeaves(leaves: Buffer[]): Promise<void> {
|
|
16
|
+
for (const leaf of leaves) {
|
|
17
|
+
this.appendLeaf(leaf);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private appendEmptyLeaf() {
|
|
24
|
+
const newSize = (this.cachedSize ?? this.size) + 1n;
|
|
25
|
+
if (newSize - 1n > this.maxIndex) {
|
|
26
|
+
throw Error(`Can't append beyond max index. Max index: ${this.maxIndex}`);
|
|
27
|
+
}
|
|
28
|
+
this.cachedSize = newSize;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Appends the given leaf to the tree.
|
|
33
|
+
* @param leaf - The leaf to append.
|
|
34
|
+
* @returns Empty promise.
|
|
35
|
+
*/
|
|
36
|
+
private appendLeaf(leaf: Buffer): void {
|
|
37
|
+
const newLeaf = this.leafFactory.fromBuffer(leaf);
|
|
38
|
+
|
|
39
|
+
// Special case when appending zero
|
|
40
|
+
if (newLeaf.getKey() === 0n) {
|
|
41
|
+
this.appendEmptyLeaf();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const lowLeafIndex = this.findIndexOfPreviousKey(newLeaf.getKey(), true);
|
|
46
|
+
if (lowLeafIndex === undefined) {
|
|
47
|
+
throw new Error(`Previous leaf not found!`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isUpdate = lowLeafIndex.alreadyPresent;
|
|
51
|
+
const lowLeafPreimage = this.getLatestLeafPreimageCopy(lowLeafIndex.index, true)!;
|
|
52
|
+
const currentSize = this.getNumLeaves(true);
|
|
53
|
+
|
|
54
|
+
if (isUpdate) {
|
|
55
|
+
const newLowLeaf = lowLeafPreimage.asLeaf().updateTo(newLeaf);
|
|
56
|
+
const newLowLeafPreimage = this.leafPreimageFactory.fromLeaf(
|
|
57
|
+
newLowLeaf,
|
|
58
|
+
lowLeafPreimage.getNextKey(),
|
|
59
|
+
lowLeafPreimage.getNextIndex(),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
this.updateLeaf(newLowLeafPreimage, BigInt(lowLeafIndex.index));
|
|
63
|
+
this.appendEmptyLeaf();
|
|
64
|
+
} else {
|
|
65
|
+
const newLeafPreimage = this.leafPreimageFactory.fromLeaf(
|
|
66
|
+
newLeaf,
|
|
67
|
+
lowLeafPreimage.getNextKey(),
|
|
68
|
+
lowLeafPreimage.getNextIndex(),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// insert a new leaf at the highest index and update the values of our previous leaf copy
|
|
72
|
+
const newLowLeafPreimage = this.leafPreimageFactory.fromLeaf(
|
|
73
|
+
lowLeafPreimage.asLeaf(),
|
|
74
|
+
newLeaf.getKey(),
|
|
75
|
+
BigInt(currentSize),
|
|
76
|
+
);
|
|
77
|
+
this.updateLeaf(newLowLeafPreimage, BigInt(lowLeafIndex.index));
|
|
78
|
+
this.updateLeaf(newLeafPreimage, currentSize);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Bufferable, serializeToBuffer } from '@aztec/foundation/serialize';
|
|
2
|
+
import { Timer } from '@aztec/foundation/timer';
|
|
3
|
+
import type { TreeInsertionStats } from '@aztec/stdlib/stats';
|
|
4
|
+
|
|
5
|
+
import type { AppendOnlyTree } from '../interfaces/append_only_tree.js';
|
|
6
|
+
import { AppendOnlySnapshotBuilder } from '../snapshots/append_only_snapshot.js';
|
|
7
|
+
import type { TreeSnapshot } from '../snapshots/snapshot_builder.js';
|
|
8
|
+
import { TreeBase } from '../tree_base.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A Merkle tree implementation that uses a LevelDB database to store the tree.
|
|
12
|
+
*/
|
|
13
|
+
export class StandardTree<T extends Bufferable = Buffer> extends TreeBase<T> implements AppendOnlyTree<T> {
|
|
14
|
+
#snapshotBuilder = new AppendOnlySnapshotBuilder(this.store, this, this.hasher, this.deserializer);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Appends the given leaves to the tree.
|
|
18
|
+
* @param leaves - The leaves to append.
|
|
19
|
+
* @returns Empty promise.
|
|
20
|
+
*/
|
|
21
|
+
public override appendLeaves(leaves: T[]): Promise<void> {
|
|
22
|
+
this.hasher.reset();
|
|
23
|
+
const timer = new Timer();
|
|
24
|
+
super.appendLeaves(leaves);
|
|
25
|
+
this.log.debug(`Inserted ${leaves.length} leaves into ${this.getName()} tree`, {
|
|
26
|
+
eventName: 'tree-insertion',
|
|
27
|
+
duration: timer.ms(),
|
|
28
|
+
batchSize: leaves.length,
|
|
29
|
+
treeName: this.getName(),
|
|
30
|
+
treeDepth: this.getDepth(),
|
|
31
|
+
treeType: 'append-only',
|
|
32
|
+
...this.hasher.stats(),
|
|
33
|
+
} satisfies TreeInsertionStats);
|
|
34
|
+
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public snapshot(blockNumber: number): Promise<TreeSnapshot<T>> {
|
|
39
|
+
return this.#snapshotBuilder.snapshot(blockNumber);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public getSnapshot(blockNumber: number): Promise<TreeSnapshot<T>> {
|
|
43
|
+
return this.#snapshotBuilder.getSnapshot(blockNumber);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public findLeafIndex(value: T, includeUncommitted: boolean): bigint | undefined {
|
|
47
|
+
return this.findLeafIndexAfter(value, 0n, includeUncommitted);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public findLeafIndexAfter(value: T, startIndex: bigint, includeUncommitted: boolean): bigint | undefined {
|
|
51
|
+
const buffer = serializeToBuffer(value);
|
|
52
|
+
for (let i = startIndex; i < this.getNumLeaves(includeUncommitted); i++) {
|
|
53
|
+
const currentValue = this.getLeafValue(i, includeUncommitted);
|
|
54
|
+
if (currentValue && serializeToBuffer(currentValue).equals(buffer)) {
|
|
55
|
+
return i;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/tree_base.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { toBigIntLE, toBufferLE } from '@aztec/foundation/bigint-buffer';
|
|
2
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { type Bufferable, type FromBuffer, serializeToBuffer } from '@aztec/foundation/serialize';
|
|
4
|
+
import { type Hasher, SiblingPath } from '@aztec/foundation/trees';
|
|
5
|
+
import type { AztecKVStore, AztecMap, AztecSingleton } from '@aztec/kv-store';
|
|
6
|
+
|
|
7
|
+
import { HasherWithStats } from './hasher_with_stats.js';
|
|
8
|
+
import type { MerkleTree } from './interfaces/merkle_tree.js';
|
|
9
|
+
|
|
10
|
+
const MAX_DEPTH = 254;
|
|
11
|
+
|
|
12
|
+
const indexToKeyHash = (name: string, level: number, index: bigint) => `${name}:${level}:${index}`;
|
|
13
|
+
const encodeMeta = (root: Buffer, depth: number, size: bigint) => {
|
|
14
|
+
const data = Buffer.alloc(36);
|
|
15
|
+
root.copy(data);
|
|
16
|
+
data.writeUInt32LE(depth, 32);
|
|
17
|
+
return Buffer.concat([data, toBufferLE(size, 32)]);
|
|
18
|
+
};
|
|
19
|
+
const decodeMeta = (meta: Buffer) => {
|
|
20
|
+
const root = meta.subarray(0, 32);
|
|
21
|
+
const depth = meta.readUInt32LE(32);
|
|
22
|
+
const size = toBigIntLE(meta.subarray(36));
|
|
23
|
+
return {
|
|
24
|
+
root,
|
|
25
|
+
depth,
|
|
26
|
+
size,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const openTreeMetaSingleton = (store: AztecKVStore, treeName: string): AztecSingleton<Buffer> =>
|
|
31
|
+
store.openSingleton(`merkle_tree_${treeName}_meta`);
|
|
32
|
+
|
|
33
|
+
export const getTreeMeta = (store: AztecKVStore, treeName: string) => {
|
|
34
|
+
const singleton = openTreeMetaSingleton(store, treeName);
|
|
35
|
+
const val = singleton.get();
|
|
36
|
+
if (!val) {
|
|
37
|
+
throw new Error();
|
|
38
|
+
}
|
|
39
|
+
return decodeMeta(val);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const INITIAL_LEAF = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A Merkle tree implementation that uses a LevelDB database to store the tree.
|
|
46
|
+
*/
|
|
47
|
+
export abstract class TreeBase<T extends Bufferable> implements MerkleTree<T> {
|
|
48
|
+
protected readonly maxIndex: bigint;
|
|
49
|
+
protected cachedSize?: bigint;
|
|
50
|
+
private root!: Buffer;
|
|
51
|
+
private zeroHashes: Buffer[] = [];
|
|
52
|
+
private cache: { [key: string]: Buffer } = {};
|
|
53
|
+
protected log: Logger;
|
|
54
|
+
protected hasher: HasherWithStats;
|
|
55
|
+
|
|
56
|
+
private nodes: AztecMap<string, Buffer>;
|
|
57
|
+
private meta: AztecSingleton<Buffer>;
|
|
58
|
+
|
|
59
|
+
public constructor(
|
|
60
|
+
protected store: AztecKVStore,
|
|
61
|
+
hasher: Hasher,
|
|
62
|
+
private name: string,
|
|
63
|
+
private depth: number,
|
|
64
|
+
protected size: bigint = 0n,
|
|
65
|
+
protected deserializer: FromBuffer<T>,
|
|
66
|
+
root?: Buffer,
|
|
67
|
+
) {
|
|
68
|
+
if (!(depth >= 1 && depth <= MAX_DEPTH)) {
|
|
69
|
+
throw Error('Invalid depth');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.hasher = new HasherWithStats(hasher);
|
|
73
|
+
this.nodes = store.openMap('merkle_tree_' + name);
|
|
74
|
+
this.meta = openTreeMetaSingleton(store, name);
|
|
75
|
+
|
|
76
|
+
// Compute the zero values at each layer.
|
|
77
|
+
let current = INITIAL_LEAF;
|
|
78
|
+
for (let i = depth - 1; i >= 0; --i) {
|
|
79
|
+
this.zeroHashes[i] = current;
|
|
80
|
+
current = hasher.hash(current, current);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.root = root ? root : current;
|
|
84
|
+
this.maxIndex = 2n ** BigInt(depth) - 1n;
|
|
85
|
+
|
|
86
|
+
this.log = createLogger(`merkle-tree:${name.toLowerCase()}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the root of the tree.
|
|
91
|
+
* @param includeUncommitted - If true, root incorporating uncommitted changes is returned.
|
|
92
|
+
* @returns The root of the tree.
|
|
93
|
+
*/
|
|
94
|
+
public getRoot(includeUncommitted: boolean): Buffer {
|
|
95
|
+
return !includeUncommitted ? this.root : this.cache[indexToKeyHash(this.name, 0, 0n)] ?? this.root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the number of leaves in the tree.
|
|
100
|
+
* @param includeUncommitted - If true, the returned number of leaves includes uncommitted changes.
|
|
101
|
+
* @returns The number of leaves in the tree.
|
|
102
|
+
*/
|
|
103
|
+
public getNumLeaves(includeUncommitted: boolean) {
|
|
104
|
+
return !includeUncommitted ? this.size : this.cachedSize ?? this.size;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns the name of the tree.
|
|
109
|
+
* @returns The name of the tree.
|
|
110
|
+
*/
|
|
111
|
+
public getName(): string {
|
|
112
|
+
return this.name;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the depth of the tree.
|
|
117
|
+
* @returns The depth of the tree.
|
|
118
|
+
*/
|
|
119
|
+
public getDepth(): number {
|
|
120
|
+
return this.depth;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns a sibling path for the element at the given index.
|
|
125
|
+
* @param index - The index of the element.
|
|
126
|
+
* @param includeUncommitted - Indicates whether to get a sibling path incorporating uncommitted changes.
|
|
127
|
+
* @returns A sibling path for the element at the given index.
|
|
128
|
+
* Note: The sibling path is an array of sibling hashes, with the lowest hash (leaf hash) first, and the highest hash last.
|
|
129
|
+
*/
|
|
130
|
+
public getSiblingPath<N extends number>(index: bigint, includeUncommitted: boolean): Promise<SiblingPath<N>> {
|
|
131
|
+
const path: Buffer[] = [];
|
|
132
|
+
let level = this.depth;
|
|
133
|
+
while (level > 0) {
|
|
134
|
+
const isRight = index & 0x01n;
|
|
135
|
+
const sibling = this.getLatestValueAtIndex(level, isRight ? index - 1n : index + 1n, includeUncommitted);
|
|
136
|
+
path.push(sibling);
|
|
137
|
+
level -= 1;
|
|
138
|
+
index >>= 1n;
|
|
139
|
+
}
|
|
140
|
+
return Promise.resolve(new SiblingPath<N>(this.depth as N, path));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Commits the changes to the database.
|
|
145
|
+
* @returns Empty promise.
|
|
146
|
+
*/
|
|
147
|
+
public commit(): Promise<void> {
|
|
148
|
+
return this.store.transaction(() => {
|
|
149
|
+
const keys = Object.getOwnPropertyNames(this.cache);
|
|
150
|
+
for (const key of keys) {
|
|
151
|
+
void this.nodes.set(key, this.cache[key]);
|
|
152
|
+
}
|
|
153
|
+
this.size = this.getNumLeaves(true);
|
|
154
|
+
this.root = this.getRoot(true);
|
|
155
|
+
|
|
156
|
+
this.clearCache();
|
|
157
|
+
|
|
158
|
+
void this.writeMeta();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Rolls back the not-yet-committed changes.
|
|
164
|
+
* @returns Empty promise.
|
|
165
|
+
*/
|
|
166
|
+
public rollback(): Promise<void> {
|
|
167
|
+
this.clearCache();
|
|
168
|
+
return Promise.resolve();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gets the value at the given index.
|
|
173
|
+
* @param index - The index of the leaf.
|
|
174
|
+
* @param includeUncommitted - Indicates whether to include uncommitted changes.
|
|
175
|
+
* @returns Leaf value at the given index or undefined.
|
|
176
|
+
*/
|
|
177
|
+
public getLeafValue(index: bigint, includeUncommitted: boolean): T | undefined {
|
|
178
|
+
const buf = this.getLatestValueAtIndex(this.depth, index, includeUncommitted);
|
|
179
|
+
if (buf) {
|
|
180
|
+
return this.deserializer.fromBuffer(buf);
|
|
181
|
+
} else {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets the value at the given index.
|
|
188
|
+
* @param index - The index of the leaf.
|
|
189
|
+
* @param includeUncommitted - Indicates whether to include uncommitted changes.
|
|
190
|
+
* @returns Leaf value at the given index or undefined.
|
|
191
|
+
*/
|
|
192
|
+
public getLeafBuffer(index: bigint, includeUncommitted: boolean): Buffer | undefined {
|
|
193
|
+
return this.getLatestValueAtIndex(this.depth, index, includeUncommitted);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public getNode(level: number, index: bigint): Buffer | undefined {
|
|
197
|
+
if (level < 0 || level > this.depth) {
|
|
198
|
+
throw Error('Invalid level: ' + level);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (index < 0 || index >= 2n ** BigInt(level)) {
|
|
202
|
+
throw Error('Invalid index: ' + index);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return this.dbGet(indexToKeyHash(this.name, level, index));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public getZeroHash(level: number): Buffer {
|
|
209
|
+
if (level <= 0 || level > this.depth) {
|
|
210
|
+
throw new Error('Invalid level');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return this.zeroHashes[level - 1];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clears the cache.
|
|
218
|
+
*/
|
|
219
|
+
private clearCache() {
|
|
220
|
+
this.cache = {};
|
|
221
|
+
this.cachedSize = undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Adds a leaf and all the hashes above it to the cache.
|
|
226
|
+
* @param leaf - Leaf to add to cache.
|
|
227
|
+
* @param index - Index of the leaf (used to derive the cache key).
|
|
228
|
+
*/
|
|
229
|
+
protected addLeafToCacheAndHashToRoot(leaf: Buffer, index: bigint) {
|
|
230
|
+
const key = indexToKeyHash(this.name, this.depth, index);
|
|
231
|
+
let current = leaf;
|
|
232
|
+
this.cache[key] = current;
|
|
233
|
+
let level = this.depth;
|
|
234
|
+
while (level > 0) {
|
|
235
|
+
const isRight = index & 0x01n;
|
|
236
|
+
const sibling = this.getLatestValueAtIndex(level, isRight ? index - 1n : index + 1n, true);
|
|
237
|
+
const lhs = isRight ? sibling : current;
|
|
238
|
+
const rhs = isRight ? current : sibling;
|
|
239
|
+
current = this.hasher.hash(lhs, rhs);
|
|
240
|
+
level -= 1;
|
|
241
|
+
index >>= 1n;
|
|
242
|
+
const cacheKey = indexToKeyHash(this.name, level, index);
|
|
243
|
+
this.cache[cacheKey] = current;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Returns the latest value at the given index.
|
|
249
|
+
* @param level - The level of the tree.
|
|
250
|
+
* @param index - The index of the element.
|
|
251
|
+
* @param includeUncommitted - Indicates, whether to get include uncommitted changes.
|
|
252
|
+
* @returns The latest value at the given index.
|
|
253
|
+
* Note: If the value is not in the cache, it will be fetched from the database.
|
|
254
|
+
*/
|
|
255
|
+
private getLatestValueAtIndex(level: number, index: bigint, includeUncommitted: boolean): Buffer {
|
|
256
|
+
const key = indexToKeyHash(this.name, level, index);
|
|
257
|
+
if (includeUncommitted && this.cache[key] !== undefined) {
|
|
258
|
+
return this.cache[key];
|
|
259
|
+
}
|
|
260
|
+
const committed = this.dbGet(key);
|
|
261
|
+
if (committed !== undefined) {
|
|
262
|
+
return committed;
|
|
263
|
+
}
|
|
264
|
+
return this.zeroHashes[level - 1];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Gets a value from db by key.
|
|
269
|
+
* @param key - The key to by which to get the value.
|
|
270
|
+
* @returns A value from the db based on the key.
|
|
271
|
+
*/
|
|
272
|
+
private dbGet(key: string): Buffer | undefined {
|
|
273
|
+
return this.nodes.get(key);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Initializes the tree.
|
|
278
|
+
* @param prefilledSize - A number of leaves that are prefilled with values.
|
|
279
|
+
* @returns Empty promise.
|
|
280
|
+
*/
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
282
|
+
public async init(prefilledSize: number): Promise<void> {
|
|
283
|
+
// prefilledSize is used only by Indexed Tree.
|
|
284
|
+
await this.writeMeta();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Writes meta data to the provided batch.
|
|
289
|
+
* @param batch - The batch to which to write the meta data.
|
|
290
|
+
*/
|
|
291
|
+
protected writeMeta() {
|
|
292
|
+
const data = encodeMeta(this.getRoot(true), this.depth, this.getNumLeaves(true));
|
|
293
|
+
return this.meta.set(data);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Appends the given leaves to the tree.
|
|
298
|
+
* @param leaves - The leaves to append.
|
|
299
|
+
* @returns Empty promise.
|
|
300
|
+
*
|
|
301
|
+
* @remarks The batch insertion algorithm works as follows:
|
|
302
|
+
* 1. Insert all the leaves,
|
|
303
|
+
* 2. start iterating over levels from the bottom up,
|
|
304
|
+
* 3. on each level iterate over all the affected nodes (i.e. nodes whose preimages have changed),
|
|
305
|
+
* 4. fetch the preimage, hash it and insert the updated value.
|
|
306
|
+
* @remarks This algorithm is optimal when it comes to the number of hashing operations. It might not be optimal when
|
|
307
|
+
* it comes to the number of database reads, but that should be irrelevant given that most of the time
|
|
308
|
+
* `getLatestValueAtIndex` will return a value from cache (because at least one of the 2 children was
|
|
309
|
+
* touched in previous iteration).
|
|
310
|
+
*/
|
|
311
|
+
protected appendLeaves(leaves: T[]): void {
|
|
312
|
+
const numLeaves = this.getNumLeaves(true);
|
|
313
|
+
if (numLeaves + BigInt(leaves.length) - 1n > this.maxIndex) {
|
|
314
|
+
throw Error(`Can't append beyond max index. Max index: ${this.maxIndex}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 1. Insert all the leaves
|
|
318
|
+
let firstIndex = numLeaves;
|
|
319
|
+
let level = this.depth;
|
|
320
|
+
for (let i = 0; i < leaves.length; i++) {
|
|
321
|
+
const cacheKey = indexToKeyHash(this.name, level, firstIndex + BigInt(i));
|
|
322
|
+
this.cache[cacheKey] = serializeToBuffer(leaves[i]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let lastIndex = firstIndex + BigInt(leaves.length);
|
|
326
|
+
// 2. Iterate over all the levels from the bottom up
|
|
327
|
+
while (level > 0) {
|
|
328
|
+
firstIndex >>= 1n;
|
|
329
|
+
lastIndex >>= 1n;
|
|
330
|
+
// 3.Iterate over all the affected nodes at this level and update them
|
|
331
|
+
for (let index = firstIndex; index <= lastIndex; index++) {
|
|
332
|
+
const lhs = this.getLatestValueAtIndex(level, index * 2n, true);
|
|
333
|
+
const rhs = this.getLatestValueAtIndex(level, index * 2n + 1n, true);
|
|
334
|
+
const cacheKey = indexToKeyHash(this.name, level - 1, index);
|
|
335
|
+
this.cache[cacheKey] = this.hasher.hash(lhs, rhs);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
level -= 1;
|
|
339
|
+
}
|
|
340
|
+
this.cachedSize = numLeaves + BigInt(leaves.length);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Returns the index of a leaf given its value, or undefined if no leaf with that value is found.
|
|
345
|
+
* @param value - The leaf value to look for.
|
|
346
|
+
* @param includeUncommitted - Indicates whether to include uncommitted data.
|
|
347
|
+
* @returns The index of the first leaf found with a given value (undefined if not found).
|
|
348
|
+
*/
|
|
349
|
+
abstract findLeafIndex(value: T, includeUncommitted: boolean): bigint | undefined;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Returns the first index containing a leaf value after `startIndex`.
|
|
353
|
+
* @param leaf - The leaf value to look for.
|
|
354
|
+
* @param startIndex - The index to start searching from (used when skipping nullified messages)
|
|
355
|
+
* @param includeUncommitted - Indicates whether to include uncommitted data.
|
|
356
|
+
* @returns The index of the first leaf found with a given value (undefined if not found).
|
|
357
|
+
*/
|
|
358
|
+
abstract findLeafIndexAfter(leaf: T, startIndex: bigint, includeUncommitted: boolean): bigint | undefined;
|
|
359
|
+
}
|