@aztec/foundation 2.1.0-rc.9 → 3.0.0-devnet.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.
- package/dest/config/env_var.d.ts +1 -1
- package/dest/config/env_var.d.ts.map +1 -1
- package/dest/config/network_name.d.ts +1 -1
- package/dest/config/network_name.d.ts.map +1 -1
- package/dest/config/network_name.js +6 -2
- package/dest/crypto/aes128/index.d.ts.map +1 -1
- package/dest/crypto/aes128/index.js +23 -6
- package/dest/crypto/ecdsa/index.d.ts.map +1 -1
- package/dest/crypto/ecdsa/index.js +66 -48
- package/dest/crypto/grumpkin/index.d.ts.map +1 -1
- package/dest/crypto/grumpkin/index.js +64 -43
- package/dest/crypto/keys/index.js +9 -4
- package/dest/crypto/pedersen/pedersen.wasm.d.ts.map +1 -1
- package/dest/crypto/pedersen/pedersen.wasm.js +29 -13
- package/dest/crypto/poseidon/index.d.ts.map +1 -1
- package/dest/crypto/poseidon/index.js +42 -17
- package/dest/crypto/schnorr/index.d.ts.map +1 -1
- package/dest/crypto/schnorr/index.js +35 -37
- package/dest/crypto/secp256k1/index.d.ts.map +1 -1
- package/dest/crypto/secp256k1/index.js +29 -18
- package/dest/crypto/secp256k1-signer/utils.d.ts +8 -0
- package/dest/crypto/secp256k1-signer/utils.d.ts.map +1 -1
- package/dest/crypto/secp256k1-signer/utils.js +14 -0
- package/dest/crypto/sync/index.js +3 -1
- package/dest/crypto/sync/pedersen/index.d.ts.map +1 -1
- package/dest/crypto/sync/pedersen/index.js +17 -10
- package/dest/crypto/sync/poseidon/index.d.ts.map +1 -1
- package/dest/crypto/sync/poseidon/index.js +27 -12
- package/dest/fields/bls12_point.d.ts +7 -7
- package/dest/fields/bls12_point.js +7 -7
- package/dest/fields/fields.d.ts.map +1 -1
- package/dest/fields/fields.js +9 -10
- package/dest/index.d.ts +1 -0
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -0
- package/dest/json-rpc/client/safe_json_rpc_client.d.ts.map +1 -1
- package/dest/json-rpc/client/safe_json_rpc_client.js +9 -0
- package/dest/log/pino-logger.d.ts.map +1 -1
- package/dest/log/pino-logger.js +0 -1
- package/dest/profiler/index.d.ts +2 -0
- package/dest/profiler/index.d.ts.map +1 -0
- package/dest/profiler/index.js +1 -0
- package/dest/profiler/profiler.d.ts +8 -0
- package/dest/profiler/profiler.d.ts.map +1 -0
- package/dest/profiler/profiler.js +97 -0
- package/dest/testing/formatting.d.ts +4 -0
- package/dest/testing/formatting.d.ts.map +1 -0
- package/dest/testing/formatting.js +3 -0
- package/dest/testing/index.d.ts +1 -0
- package/dest/testing/index.d.ts.map +1 -1
- package/dest/testing/index.js +1 -0
- package/dest/trees/unbalanced_merkle_tree.d.ts +0 -1
- package/dest/trees/unbalanced_merkle_tree.d.ts.map +1 -1
- package/dest/trees/unbalanced_merkle_tree.js +1 -1
- package/dest/trees/unbalanced_merkle_tree_calculator.d.ts +25 -22
- package/dest/trees/unbalanced_merkle_tree_calculator.d.ts.map +1 -1
- package/dest/trees/unbalanced_merkle_tree_calculator.js +124 -94
- package/dest/trees/unbalanced_tree_store.d.ts +1 -0
- package/dest/trees/unbalanced_tree_store.d.ts.map +1 -1
- package/dest/trees/unbalanced_tree_store.js +6 -0
- package/package.json +4 -3
- package/src/config/env_var.ts +2 -1
- package/src/config/network_name.ts +14 -3
- package/src/crypto/aes128/index.ts +19 -10
- package/src/crypto/ecdsa/index.ts +40 -37
- package/src/crypto/grumpkin/index.ts +29 -31
- package/src/crypto/keys/index.ts +5 -5
- package/src/crypto/pedersen/pedersen.wasm.ts +22 -18
- package/src/crypto/poseidon/index.ts +32 -24
- package/src/crypto/schnorr/index.ts +20 -17
- package/src/crypto/secp256k1/index.ts +15 -11
- package/src/crypto/secp256k1-signer/utils.ts +16 -0
- package/src/crypto/sync/index.ts +1 -1
- package/src/crypto/sync/pedersen/index.ts +16 -15
- package/src/crypto/sync/poseidon/index.ts +27 -22
- package/src/fields/bls12_point.ts +7 -7
- package/src/fields/fields.ts +5 -6
- package/src/index.ts +1 -0
- package/src/json-rpc/client/safe_json_rpc_client.ts +9 -0
- package/src/log/pino-logger.ts +0 -1
- package/src/profiler/index.ts +1 -0
- package/src/profiler/profiler.ts +125 -0
- package/src/testing/formatting.ts +3 -0
- package/src/testing/index.ts +1 -0
- package/src/trees/unbalanced_merkle_tree.ts +1 -1
- package/src/trees/unbalanced_merkle_tree_calculator.ts +140 -92
- package/src/trees/unbalanced_tree_store.ts +5 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BarretenbergSync
|
|
1
|
+
import { BarretenbergSync } from '@aztec/bb.js';
|
|
2
2
|
|
|
3
3
|
import { Fr } from '../../../fields/fields.js';
|
|
4
4
|
import { type Fieldable, serializeToFields } from '../../../serialize/serialize.js';
|
|
@@ -10,10 +10,11 @@ import { type Fieldable, serializeToFields } from '../../../serialize/serialize.
|
|
|
10
10
|
*/
|
|
11
11
|
export function poseidon2Hash(input: Fieldable[]): Fr {
|
|
12
12
|
const inputFields = serializeToFields(input);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const api = BarretenbergSync.getSingleton();
|
|
14
|
+
const response = api.poseidon2Hash({
|
|
15
|
+
inputs: inputFields.map(i => i.toBuffer()),
|
|
16
|
+
});
|
|
17
|
+
return Fr.fromBuffer(Buffer.from(response.hash));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -26,18 +27,20 @@ export function poseidon2HashWithSeparator(input: Fieldable[], separator: number
|
|
|
26
27
|
const inputFields = serializeToFields(input);
|
|
27
28
|
inputFields.unshift(new Fr(separator));
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const api = BarretenbergSync.getSingleton();
|
|
31
|
+
const response = api.poseidon2Hash({
|
|
32
|
+
inputs: inputFields.map(i => i.toBuffer()),
|
|
33
|
+
});
|
|
34
|
+
return Fr.fromBuffer(Buffer.from(response.hash));
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function poseidon2HashAccumulate(input: Fieldable[]): Fr {
|
|
36
38
|
const inputFields = serializeToFields(input);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const api = BarretenbergSync.getSingleton();
|
|
40
|
+
const response = api.poseidon2HashAccumulate({
|
|
41
|
+
inputs: inputFields.map(i => i.toBuffer()),
|
|
42
|
+
});
|
|
43
|
+
return Fr.fromBuffer(Buffer.from(response.hash));
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -49,12 +52,13 @@ export function poseidon2Permutation(input: Fieldable[]): Fr[] {
|
|
|
49
52
|
const inputFields = serializeToFields(input);
|
|
50
53
|
// We'd like this assertion but it's not possible to use it in the browser.
|
|
51
54
|
// assert(input.length === 4, 'Input state must be of size 4');
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
const api = BarretenbergSync.getSingleton();
|
|
56
|
+
const response = api.poseidon2Permutation({
|
|
57
|
+
inputs: inputFields.map(i => i.toBuffer()),
|
|
58
|
+
});
|
|
55
59
|
// We'd like this assertion but it's not possible to use it in the browser.
|
|
56
|
-
// assert(
|
|
57
|
-
return
|
|
60
|
+
// assert(response.outputs.length === 4, 'Output state must be of size 4');
|
|
61
|
+
return response.outputs.map(o => Fr.fromBuffer(Buffer.from(o)));
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export function poseidon2HashBytes(input: Buffer): Fr {
|
|
@@ -68,9 +72,10 @@ export function poseidon2HashBytes(input: Buffer): Fr {
|
|
|
68
72
|
inputFields.push(Fr.fromBuffer(fieldBytes));
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const api = BarretenbergSync.getSingleton();
|
|
76
|
+
const response = api.poseidon2Hash({
|
|
77
|
+
inputs: inputFields.map(i => i.toBuffer()),
|
|
78
|
+
});
|
|
74
79
|
|
|
75
|
-
return Fr.fromBuffer(Buffer.from(
|
|
80
|
+
return Fr.fromBuffer(Buffer.from(response.hash));
|
|
76
81
|
}
|
|
@@ -158,13 +158,13 @@ export class BLS12Point {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
|
-
* Converts a Point to two BN254 Fr elements by storing its compressed form as:
|
|
162
|
-
*
|
|
163
|
-
* |
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* |
|
|
167
|
-
*
|
|
161
|
+
* Converts a Point to two BN254 Fr elements by storing its compressed form (48 bytes) as:
|
|
162
|
+
* +-------------------+------------------------+
|
|
163
|
+
* | 31 bytes | 17 bytes |
|
|
164
|
+
* +-------------------+------------------------+
|
|
165
|
+
* | Field Element 1 | Field Element 2 |
|
|
166
|
+
* | [0][bytes 0-30] | [0...0][bytes 31-47] |
|
|
167
|
+
* +-------------------+------------------------+
|
|
168
168
|
* Used in the rollup circuits to store blob commitments in the native field type. See blob.ts.
|
|
169
169
|
* @param point - A BLS12Point instance.
|
|
170
170
|
* @returns The point fields.
|
package/src/fields/fields.ts
CHANGED
|
@@ -321,15 +321,14 @@ export class Fr extends BaseField {
|
|
|
321
321
|
* @returns A square root of the field element (null if it does not exist).
|
|
322
322
|
*/
|
|
323
323
|
async sqrt(): Promise<Fr | null> {
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
if (!isSqrt) {
|
|
324
|
+
await BarretenbergSync.initSingleton({ wasmPath: process.env.BB_WASM_PATH });
|
|
325
|
+
const api = BarretenbergSync.getSingleton();
|
|
326
|
+
const response = api.bn254FrSqrt({ input: this.toBuffer() });
|
|
327
|
+
if (!response.isSquareRoot) {
|
|
329
328
|
// Field element is not a quadratic residue mod p so it has no square root.
|
|
330
329
|
return null;
|
|
331
330
|
}
|
|
332
|
-
return
|
|
331
|
+
return Fr.fromBuffer(Buffer.from(response.value));
|
|
333
332
|
}
|
|
334
333
|
|
|
335
334
|
toJSON() {
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * as trees from './trees/index.js';
|
|
|
24
24
|
export * as types from './types/index.js';
|
|
25
25
|
export * as url from './url/index.js';
|
|
26
26
|
export * as testing from './testing/index.js';
|
|
27
|
+
export * as profiler from './profiler/index.js';
|
|
27
28
|
export * as config from './config/index.js';
|
|
28
29
|
export * as buffer from './buffer/index.js';
|
|
29
30
|
export * as ethSignature from './eth-signature/index.js';
|
|
@@ -178,6 +178,15 @@ export function createSafeJsonRpcClient<T extends object>(
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
} catch (err) {
|
|
181
|
+
// Re-throw ComponentsVersionsError immediately without converting to JSON-RPC error
|
|
182
|
+
// This ensures version mismatch errors are surfaced to the user instead of being hidden
|
|
183
|
+
if (err && typeof err === 'object' && 'name' in err && err.name === 'ComponentsVersionsError') {
|
|
184
|
+
// Reject all pending requests with the version error
|
|
185
|
+
for (let i = 0; i < rpcCalls.length; i++) {
|
|
186
|
+
rpcCalls[i].deferred.reject(err);
|
|
187
|
+
}
|
|
188
|
+
return; // Return early, the promises are already rejected
|
|
189
|
+
}
|
|
181
190
|
log.warn(`Failed to fetch from the remote server`, err);
|
|
182
191
|
for (let i = 0; i < rpcCalls.length; i++) {
|
|
183
192
|
const { request, deferred } = rpcCalls[i];
|
package/src/log/pino-logger.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './profiler.js';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
|
|
6
|
+
interface Span {
|
|
7
|
+
label: string;
|
|
8
|
+
start: number;
|
|
9
|
+
dur: number;
|
|
10
|
+
count: number;
|
|
11
|
+
children: Span[];
|
|
12
|
+
parent: Span | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProfileData {
|
|
16
|
+
spans: SerializedSpan[];
|
|
17
|
+
timestamp: string;
|
|
18
|
+
totalTime: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SerializedSpan {
|
|
22
|
+
label: string;
|
|
23
|
+
dur: number;
|
|
24
|
+
count: number;
|
|
25
|
+
children: SerializedSpan[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const als = new AsyncLocalStorage<Span>();
|
|
29
|
+
const roots: Span[] = [];
|
|
30
|
+
|
|
31
|
+
function reset(): void {
|
|
32
|
+
roots.length = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Strip out circular references (parent) and unused fields (start) for JSON serialization
|
|
36
|
+
function serializeSpans(spans: Span[]): SerializedSpan[] {
|
|
37
|
+
return spans.map(span => ({
|
|
38
|
+
label: span.label,
|
|
39
|
+
dur: span.dur,
|
|
40
|
+
count: span.count,
|
|
41
|
+
children: serializeSpans(span.children),
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let i = 0;
|
|
46
|
+
function save(): void {
|
|
47
|
+
if (roots.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Find max single execution time across all spans (dur/count since dur is accumulated)
|
|
52
|
+
const findMaxSingleDuration = (spans: Span[]): number => {
|
|
53
|
+
let max = 0;
|
|
54
|
+
for (const span of spans) {
|
|
55
|
+
const singleDur = span.dur / span.count;
|
|
56
|
+
max = Math.max(max, singleDur);
|
|
57
|
+
if (span.children.length > 0) {
|
|
58
|
+
max = Math.max(max, findMaxSingleDuration(span.children));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return max;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const profileData: ProfileData = {
|
|
65
|
+
spans: serializeSpans(roots),
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
totalTime: findMaxSingleDuration(roots),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const profilePath = path.join(process.cwd(), `profile-${i++}.json`);
|
|
71
|
+
process.stdout.write(`Writing profile data to ${profilePath}\n`);
|
|
72
|
+
fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Hook into Jest to save after each test
|
|
76
|
+
if (typeof afterEach === 'function') {
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
save();
|
|
79
|
+
reset();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Also save on process exit for non-Jest environments
|
|
84
|
+
process.on('exit', () => {
|
|
85
|
+
save();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Wrapper for async functions to maintain context properly
|
|
89
|
+
async function runAsync<ReturnType>(label: string, fn: () => Promise<ReturnType>): Promise<ReturnType> {
|
|
90
|
+
const parent = als.getStore();
|
|
91
|
+
|
|
92
|
+
// Check if we already have a span with this label in the current context
|
|
93
|
+
let existingSpan: Span | undefined;
|
|
94
|
+
if (parent) {
|
|
95
|
+
existingSpan = parent.children.find(c => c.label === label);
|
|
96
|
+
} else {
|
|
97
|
+
existingSpan = roots.find(r => r.label === label);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let span: Span;
|
|
101
|
+
if (existingSpan) {
|
|
102
|
+
// Reuse existing span and increment count
|
|
103
|
+
span = existingSpan;
|
|
104
|
+
span.count++;
|
|
105
|
+
} else {
|
|
106
|
+
// Create new span
|
|
107
|
+
span = { label, start: performance.now(), dur: 0, count: 1, children: [], parent };
|
|
108
|
+
if (parent) {
|
|
109
|
+
parent.children.push(span);
|
|
110
|
+
} else {
|
|
111
|
+
roots.push(span);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const startTime = performance.now();
|
|
116
|
+
const result: ReturnType = await als.run(span, fn);
|
|
117
|
+
const elapsed = performance.now() - startTime;
|
|
118
|
+
|
|
119
|
+
// Add to total duration (for averaging)
|
|
120
|
+
span.dur += elapsed;
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const profiler = { reset, runAsync };
|
package/src/testing/index.ts
CHANGED
|
@@ -63,7 +63,7 @@ function getMaxBalancedSubtreeDepth(numLeaves: number) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/// Get the maximum depth of an unbalanced tree that can be created with the given number of leaves.
|
|
66
|
-
|
|
66
|
+
function getMaxUnbalancedTreeDepth(numLeaves: number) {
|
|
67
67
|
return Math.ceil(Math.log2(numLeaves));
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -1,37 +1,53 @@
|
|
|
1
|
-
import { type Bufferable, serializeToBuffer } from '@aztec/foundation/serialize';
|
|
2
|
-
import type { AsyncHasher } from '@aztec/foundation/trees';
|
|
3
|
-
import { SiblingPath } from '@aztec/foundation/trees';
|
|
4
|
-
|
|
5
1
|
import { sha256Trunc } from '../crypto/index.js';
|
|
2
|
+
import type { Hasher } from './hasher.js';
|
|
3
|
+
import { SiblingPath } from './sibling_path.js';
|
|
4
|
+
import { type TreeNodeLocation, UnbalancedTreeStore } from './unbalanced_tree_store.js';
|
|
5
|
+
|
|
6
|
+
export function computeCompressedUnbalancedMerkleTreeRoot(
|
|
7
|
+
leaves: Buffer[],
|
|
8
|
+
valueToCompress = Buffer.alloc(32),
|
|
9
|
+
hasher?: Hasher['hash'],
|
|
10
|
+
): Buffer {
|
|
11
|
+
const calculator = UnbalancedMerkleTreeCalculator.create(leaves, valueToCompress, hasher);
|
|
12
|
+
return calculator.getRoot();
|
|
13
|
+
}
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
interface TreeNode {
|
|
16
|
+
value: Buffer;
|
|
17
|
+
leafIndex?: number;
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
/**
|
|
10
21
|
* An ephemeral unbalanced Merkle tree implementation.
|
|
11
22
|
* Follows the rollup implementation which greedily hashes pairs of nodes up the tree.
|
|
12
23
|
* Remaining rightmost nodes are shifted up until they can be paired.
|
|
24
|
+
* The values that match the `valueToCompress` are skipped and the sibling of the compressed leaf are shifted up until
|
|
25
|
+
* they can be paired.
|
|
13
26
|
* If there is only one leaf, the root is the leaf.
|
|
14
27
|
*/
|
|
15
28
|
export class UnbalancedMerkleTreeCalculator {
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
// This map stores value -> index and depth, since we have variable depth
|
|
19
|
-
private valueCache: { [key: string]: string } = {};
|
|
20
|
-
protected size: bigint = 0n;
|
|
21
|
-
|
|
22
|
-
root: Buffer = Buffer.alloc(32);
|
|
29
|
+
private store: UnbalancedTreeStore<TreeNode>;
|
|
30
|
+
private leafLocations: TreeNodeLocation[] = [];
|
|
23
31
|
|
|
24
32
|
public constructor(
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
|
|
33
|
+
private readonly leaves: Buffer[],
|
|
34
|
+
private readonly valueToCompress: Buffer,
|
|
35
|
+
private readonly hasher: Hasher['hash'],
|
|
36
|
+
) {
|
|
37
|
+
if (leaves.length === 0) {
|
|
38
|
+
throw Error('Cannot create a compressed unbalanced tree with 0 leaves.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.store = new UnbalancedTreeStore(leaves.length);
|
|
42
|
+
this.buildTree();
|
|
43
|
+
}
|
|
28
44
|
|
|
29
45
|
static create(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
leaves: Buffer[],
|
|
47
|
+
valueToCompress = Buffer.alloc(0),
|
|
48
|
+
hasher = (left: Buffer, right: Buffer) => sha256Trunc(Buffer.concat([left, right])) as Buffer<ArrayBuffer>,
|
|
33
49
|
) {
|
|
34
|
-
return new UnbalancedMerkleTreeCalculator(
|
|
50
|
+
return new UnbalancedMerkleTreeCalculator(leaves, valueToCompress, hasher);
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
/**
|
|
@@ -39,108 +55,140 @@ export class UnbalancedMerkleTreeCalculator {
|
|
|
39
55
|
* @returns The root of the tree.
|
|
40
56
|
*/
|
|
41
57
|
public getRoot(): Buffer {
|
|
42
|
-
return this.
|
|
58
|
+
return this.store.getRoot()!.value;
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
/**
|
|
46
|
-
* Returns a sibling path for the element
|
|
62
|
+
* Returns a sibling path for the element.
|
|
47
63
|
* @param value - The value of the element.
|
|
48
64
|
* @returns A sibling path for the element.
|
|
49
65
|
* Note: The sibling path is an array of sibling hashes, with the lowest hash (leaf hash) first, and the highest hash last.
|
|
50
66
|
*/
|
|
51
|
-
public getSiblingPath<N extends number>(value:
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
public getSiblingPath<N extends number>(value: Buffer): SiblingPath<N> {
|
|
68
|
+
const leafIndex = this.leaves.findIndex(leaf => leaf.equals(value));
|
|
69
|
+
if (leafIndex === -1) {
|
|
70
|
+
throw Error(`Leaf value ${value.toString('hex')} not found in tree.`);
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
const [depth, _index] = this.valueCache[serializeToBuffer(value).toString('hex')].split(':');
|
|
58
|
-
let level = parseInt(depth, 10);
|
|
59
|
-
let index = BigInt(_index);
|
|
60
|
-
while (level > 0) {
|
|
61
|
-
const isRight = index & 0x01n;
|
|
62
|
-
const key = indexToKeyHash(level, isRight ? index - 1n : index + 1n);
|
|
63
|
-
const sibling = this.cache[key];
|
|
64
|
-
path.push(sibling);
|
|
65
|
-
level -= 1;
|
|
66
|
-
index >>= 1n;
|
|
67
|
-
}
|
|
68
|
-
return Promise.resolve(new SiblingPath<N>(parseInt(depth, 10) as N, path));
|
|
73
|
+
return this.getSiblingPathByLeafIndex(leafIndex);
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
/**
|
|
72
|
-
*
|
|
73
|
-
* @param
|
|
74
|
-
* @returns
|
|
77
|
+
* Returns a sibling path for the leaf at the given index.
|
|
78
|
+
* @param leafIndex - The index of the leaf.
|
|
79
|
+
* @returns A sibling path for the leaf.
|
|
75
80
|
*/
|
|
76
|
-
public
|
|
77
|
-
if (this.
|
|
78
|
-
throw Error(`
|
|
81
|
+
public getSiblingPathByLeafIndex<N extends number>(leafIndex: number): SiblingPath<N> {
|
|
82
|
+
if (leafIndex >= this.leaves.length) {
|
|
83
|
+
throw Error(`Leaf index ${leafIndex} out of bounds. Tree has ${this.leaves.length} leaves.`);
|
|
79
84
|
}
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
|
|
86
|
+
const leaf = this.leaves[leafIndex];
|
|
87
|
+
if (leaf.equals(this.valueToCompress)) {
|
|
88
|
+
throw Error(`Leaf at index ${leafIndex} has been compressed.`);
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
const path: Buffer[] = [];
|
|
92
|
+
let location = this.leafLocations[leafIndex];
|
|
93
|
+
while (location.level > 0) {
|
|
94
|
+
const sibling = this.store.getSibling(location)!;
|
|
95
|
+
path.push(sibling.value);
|
|
96
|
+
location = this.store.getParentLocation(location);
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
|
|
99
|
+
return new SiblingPath<N>(path.length as N, path);
|
|
100
|
+
}
|
|
91
101
|
|
|
92
|
-
|
|
102
|
+
public getLeafLocation(leafIndex: number) {
|
|
103
|
+
return this.leafLocations[leafIndex];
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @param leaves - The leaves
|
|
98
|
-
* @returns Resulting root of the tree.
|
|
107
|
+
* Adds leaves and nodes to the store. Updates the leafLocations.
|
|
108
|
+
* @param leaves - The leaves of the tree.
|
|
99
109
|
*/
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for (let i =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
110
|
+
private buildTree() {
|
|
111
|
+
this.leafLocations = this.leaves.map((value, i) => this.store.setLeaf(i, { value, leafIndex: i }));
|
|
112
|
+
|
|
113
|
+
// Start with the leaves that are not compressed.
|
|
114
|
+
let toProcess = this.leafLocations.filter((_, i) => !this.leaves[i].equals(this.valueToCompress));
|
|
115
|
+
if (!toProcess.length) {
|
|
116
|
+
// All leaves are compressed. Set 0 to the root.
|
|
117
|
+
this.store.setNode({ level: 0, index: 0 }, { value: Buffer.alloc(32) });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const level = toProcess[0].level;
|
|
122
|
+
for (let i = level; i > 0; i--) {
|
|
123
|
+
const toProcessNext = [];
|
|
124
|
+
for (const location of toProcess) {
|
|
125
|
+
if (location.level !== i) {
|
|
126
|
+
toProcessNext.push(location);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parentLocation = this.store.getParentLocation(location);
|
|
131
|
+
if (this.store.getNode(parentLocation)) {
|
|
132
|
+
// Parent has been updated by its (left) sibling.
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sibling = this.store.getSibling(location);
|
|
137
|
+
// If sibling is undefined, all its children are compressed.
|
|
138
|
+
const shouldShiftUp = !sibling || sibling.value.equals(this.valueToCompress);
|
|
139
|
+
if (shouldShiftUp) {
|
|
140
|
+
// The node becomes the parent if the sibling is a compressed leaf.
|
|
141
|
+
const isLeaf = this.shiftNodeUp(location, parentLocation);
|
|
142
|
+
if (!isLeaf) {
|
|
143
|
+
this.shiftChildrenUp(location);
|
|
144
|
+
}
|
|
126
145
|
} else {
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
// Hash the value with the (right) sibling and update the parent node.
|
|
147
|
+
const node = this.store.getNode(location)!;
|
|
148
|
+
const parentValue = this.hasher(node.value, sibling.value);
|
|
149
|
+
this.store.setNode(parentLocation, { value: parentValue });
|
|
130
150
|
}
|
|
151
|
+
|
|
152
|
+
// Add the parent location to be processed next.
|
|
153
|
+
toProcessNext.push(parentLocation);
|
|
131
154
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
nextLayer = [];
|
|
155
|
+
|
|
156
|
+
toProcess = toProcessNext;
|
|
135
157
|
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private shiftNodeUp(fromLocation: TreeNodeLocation, toLocation: TreeNodeLocation): boolean {
|
|
161
|
+
const node = this.store.getNode(fromLocation)!;
|
|
136
162
|
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
this.store.setNode(toLocation, node);
|
|
164
|
+
|
|
165
|
+
const isLeaf = node.leafIndex !== undefined;
|
|
166
|
+
if (isLeaf) {
|
|
167
|
+
// Update the location if the node is a leaf.
|
|
168
|
+
this.leafLocations[node.leafIndex!] = toLocation;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return isLeaf;
|
|
139
172
|
}
|
|
140
173
|
|
|
141
|
-
private
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
174
|
+
private shiftChildrenUp(parent: TreeNodeLocation) {
|
|
175
|
+
const [left, right] = this.store.getChildLocations(parent);
|
|
176
|
+
|
|
177
|
+
const level = parent.level;
|
|
178
|
+
const groupSize = 2 ** level;
|
|
179
|
+
const computeNewLocation = (index: number) => ({
|
|
180
|
+
level,
|
|
181
|
+
index: Math.floor(index / (groupSize * 2)) * groupSize + (index % groupSize),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const isLeftLeaf = this.shiftNodeUp(left, computeNewLocation(left.index));
|
|
185
|
+
const isRightLeaf = this.shiftNodeUp(right, computeNewLocation(right.index));
|
|
186
|
+
|
|
187
|
+
if (!isLeftLeaf) {
|
|
188
|
+
this.shiftChildrenUp(left);
|
|
189
|
+
}
|
|
190
|
+
if (!isRightLeaf) {
|
|
191
|
+
this.shiftChildrenUp(right);
|
|
192
|
+
}
|
|
145
193
|
}
|
|
146
194
|
}
|
|
@@ -68,7 +68,7 @@ export class UnbalancedTreeStore<T> {
|
|
|
68
68
|
return [left, right];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
getLeaf(leafIndex: number) {
|
|
71
|
+
getLeaf(leafIndex: number): T | undefined {
|
|
72
72
|
const { level, indexAtLevel } = findLeafLevelAndIndex(this.#numLeaves, leafIndex);
|
|
73
73
|
const location = {
|
|
74
74
|
level,
|
|
@@ -81,6 +81,10 @@ export class UnbalancedTreeStore<T> {
|
|
|
81
81
|
return this.#nodeMapping.get(this.#getKey(location))?.value;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
getRoot(): T | undefined {
|
|
85
|
+
return this.getNode({ level: 0, index: 0 });
|
|
86
|
+
}
|
|
87
|
+
|
|
84
88
|
getParent(location: TreeNodeLocation): T | undefined {
|
|
85
89
|
const parentLocation = this.getParentLocation(location);
|
|
86
90
|
return this.getNode(parentLocation);
|