@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.
Files changed (87) hide show
  1. package/dest/config/env_var.d.ts +1 -1
  2. package/dest/config/env_var.d.ts.map +1 -1
  3. package/dest/config/network_name.d.ts +1 -1
  4. package/dest/config/network_name.d.ts.map +1 -1
  5. package/dest/config/network_name.js +6 -2
  6. package/dest/crypto/aes128/index.d.ts.map +1 -1
  7. package/dest/crypto/aes128/index.js +23 -6
  8. package/dest/crypto/ecdsa/index.d.ts.map +1 -1
  9. package/dest/crypto/ecdsa/index.js +66 -48
  10. package/dest/crypto/grumpkin/index.d.ts.map +1 -1
  11. package/dest/crypto/grumpkin/index.js +64 -43
  12. package/dest/crypto/keys/index.js +9 -4
  13. package/dest/crypto/pedersen/pedersen.wasm.d.ts.map +1 -1
  14. package/dest/crypto/pedersen/pedersen.wasm.js +29 -13
  15. package/dest/crypto/poseidon/index.d.ts.map +1 -1
  16. package/dest/crypto/poseidon/index.js +42 -17
  17. package/dest/crypto/schnorr/index.d.ts.map +1 -1
  18. package/dest/crypto/schnorr/index.js +35 -37
  19. package/dest/crypto/secp256k1/index.d.ts.map +1 -1
  20. package/dest/crypto/secp256k1/index.js +29 -18
  21. package/dest/crypto/secp256k1-signer/utils.d.ts +8 -0
  22. package/dest/crypto/secp256k1-signer/utils.d.ts.map +1 -1
  23. package/dest/crypto/secp256k1-signer/utils.js +14 -0
  24. package/dest/crypto/sync/index.js +3 -1
  25. package/dest/crypto/sync/pedersen/index.d.ts.map +1 -1
  26. package/dest/crypto/sync/pedersen/index.js +17 -10
  27. package/dest/crypto/sync/poseidon/index.d.ts.map +1 -1
  28. package/dest/crypto/sync/poseidon/index.js +27 -12
  29. package/dest/fields/bls12_point.d.ts +7 -7
  30. package/dest/fields/bls12_point.js +7 -7
  31. package/dest/fields/fields.d.ts.map +1 -1
  32. package/dest/fields/fields.js +9 -10
  33. package/dest/index.d.ts +1 -0
  34. package/dest/index.d.ts.map +1 -1
  35. package/dest/index.js +1 -0
  36. package/dest/json-rpc/client/safe_json_rpc_client.d.ts.map +1 -1
  37. package/dest/json-rpc/client/safe_json_rpc_client.js +9 -0
  38. package/dest/log/pino-logger.d.ts.map +1 -1
  39. package/dest/log/pino-logger.js +0 -1
  40. package/dest/profiler/index.d.ts +2 -0
  41. package/dest/profiler/index.d.ts.map +1 -0
  42. package/dest/profiler/index.js +1 -0
  43. package/dest/profiler/profiler.d.ts +8 -0
  44. package/dest/profiler/profiler.d.ts.map +1 -0
  45. package/dest/profiler/profiler.js +97 -0
  46. package/dest/testing/formatting.d.ts +4 -0
  47. package/dest/testing/formatting.d.ts.map +1 -0
  48. package/dest/testing/formatting.js +3 -0
  49. package/dest/testing/index.d.ts +1 -0
  50. package/dest/testing/index.d.ts.map +1 -1
  51. package/dest/testing/index.js +1 -0
  52. package/dest/trees/unbalanced_merkle_tree.d.ts +0 -1
  53. package/dest/trees/unbalanced_merkle_tree.d.ts.map +1 -1
  54. package/dest/trees/unbalanced_merkle_tree.js +1 -1
  55. package/dest/trees/unbalanced_merkle_tree_calculator.d.ts +25 -22
  56. package/dest/trees/unbalanced_merkle_tree_calculator.d.ts.map +1 -1
  57. package/dest/trees/unbalanced_merkle_tree_calculator.js +124 -94
  58. package/dest/trees/unbalanced_tree_store.d.ts +1 -0
  59. package/dest/trees/unbalanced_tree_store.d.ts.map +1 -1
  60. package/dest/trees/unbalanced_tree_store.js +6 -0
  61. package/package.json +4 -3
  62. package/src/config/env_var.ts +2 -1
  63. package/src/config/network_name.ts +14 -3
  64. package/src/crypto/aes128/index.ts +19 -10
  65. package/src/crypto/ecdsa/index.ts +40 -37
  66. package/src/crypto/grumpkin/index.ts +29 -31
  67. package/src/crypto/keys/index.ts +5 -5
  68. package/src/crypto/pedersen/pedersen.wasm.ts +22 -18
  69. package/src/crypto/poseidon/index.ts +32 -24
  70. package/src/crypto/schnorr/index.ts +20 -17
  71. package/src/crypto/secp256k1/index.ts +15 -11
  72. package/src/crypto/secp256k1-signer/utils.ts +16 -0
  73. package/src/crypto/sync/index.ts +1 -1
  74. package/src/crypto/sync/pedersen/index.ts +16 -15
  75. package/src/crypto/sync/poseidon/index.ts +27 -22
  76. package/src/fields/bls12_point.ts +7 -7
  77. package/src/fields/fields.ts +5 -6
  78. package/src/index.ts +1 -0
  79. package/src/json-rpc/client/safe_json_rpc_client.ts +9 -0
  80. package/src/log/pino-logger.ts +0 -1
  81. package/src/profiler/index.ts +1 -0
  82. package/src/profiler/profiler.ts +125 -0
  83. package/src/testing/formatting.ts +3 -0
  84. package/src/testing/index.ts +1 -0
  85. package/src/trees/unbalanced_merkle_tree.ts +1 -1
  86. package/src/trees/unbalanced_merkle_tree_calculator.ts +140 -92
  87. package/src/trees/unbalanced_tree_store.ts +5 -1
@@ -1,4 +1,4 @@
1
- import { BarretenbergSync, Fr as FrBarretenberg } from '@aztec/bb.js';
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 hash = BarretenbergSync.getSingleton().poseidon2Hash(
14
- inputFields.map(i => new FrBarretenberg(i.toBuffer())), // TODO(#4189): remove this stupid conversion
15
- );
16
- return Fr.fromBuffer(Buffer.from(hash.toBuffer()));
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 hash = BarretenbergSync.getSingleton().poseidon2Hash(
30
- inputFields.map(i => new FrBarretenberg(i.toBuffer())), // TODO(#4189): remove this stupid conversion
31
- );
32
- return Fr.fromBuffer(Buffer.from(hash.toBuffer()));
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 result = BarretenbergSync.getSingleton().poseidon2HashAccumulate(
38
- inputFields.map(i => new FrBarretenberg(i.toBuffer())),
39
- );
40
- return Fr.fromBuffer(Buffer.from(result.toBuffer()));
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 res = BarretenbergSync.getSingleton().poseidon2Permutation(
53
- inputFields.map(i => new FrBarretenberg(i.toBuffer())),
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(res.length === 4, 'Output state must be of size 4');
57
- return res.map(o => Fr.fromBuffer(Buffer.from(o.toBuffer())));
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 res = BarretenbergSync.getSingleton().poseidon2Hash(
72
- inputFields.map(i => new FrBarretenberg(i.toBuffer())), // TODO(#4189): remove this stupid conversion
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(res.toBuffer()));
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
- * | Field Element 1 | Field Element 2 |
164
- * | [bytes 0-31] | [bytes 32-47] |
165
- * +------------------+------------------+
166
- * | 32 bytes | 16 bytes |
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.
@@ -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
- const api = await BarretenbergSync.initSingleton(process.env.BB_WASM_PATH);
325
- const wasm = api.getWasm();
326
- const [buf] = wasm.callWasmExport('bn254_fr_sqrt', [this.toBuffer()], [Fr.SIZE_IN_BYTES + 1]);
327
- const isSqrt = buf[0] === 1;
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 new Fr(Buffer.from(buf.slice(1)));
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];
@@ -111,7 +111,6 @@ const redactedPaths = [
111
111
  // bot keys
112
112
  'l1PrivateKey',
113
113
  'senderPrivateKey',
114
- 'recipientEncryptionSecret',
115
114
  // blob sink
116
115
  'l1ConsensusHostApiKeys',
117
116
  // sensitive options used in the CLI
@@ -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 };
@@ -0,0 +1,3 @@
1
+ export function toInlineStrArray(arr: { toString: () => string }[]): string {
2
+ return `[${arr.map(f => f.toString()).join(',')}]`;
3
+ }
@@ -1,3 +1,4 @@
1
+ export * from './formatting.js';
1
2
  export * from './snapshot_serializer.js';
2
3
  export * from './port_allocator.js';
3
4
  export * from './test_data.js';
@@ -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
- export function getMaxUnbalancedTreeDepth(numLeaves: number) {
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
- const indexToKeyHash = (level: number, index: bigint) => `${level}:${index}`;
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
- // This map stores index and depth -> value
17
- private cache: { [key: string]: Buffer } = {};
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 maxDepth: number,
26
- private hasher: AsyncHasher['hash'],
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
- height: number,
31
- hasher = (left: Buffer, right: Buffer) =>
32
- Promise.resolve(sha256Trunc(Buffer.concat([left, right])) as Buffer<ArrayBuffer>),
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(height, hasher);
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.root;
58
+ return this.store.getRoot()!.value;
43
59
  }
44
60
 
45
61
  /**
46
- * Returns a sibling path for the element at the given index.
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: Bufferable): Promise<SiblingPath<N>> {
52
- if (this.size === 1n) {
53
- return Promise.resolve(new SiblingPath<N>(0 as N, []));
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
- const path: Buffer[] = [];
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
- * Appends the given leaves to the tree.
73
- * @param leaves - The leaves to append.
74
- * @returns Empty promise.
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 async appendLeaves(leaves: Buffer[]): Promise<void> {
77
- if (this.size != BigInt(0)) {
78
- throw Error(`Can't re-append to an unbalanced tree. Current has ${this.size} leaves.`);
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
- if (leaves.length === 0) {
81
- throw Error(`Can't append 0 leaves to an unbalanced tree.`);
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
- if (leaves.length === 1) {
85
- this.root = leaves[0];
86
- } else {
87
- this.root = await this.batchInsert(leaves);
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
- this.size = BigInt(leaves.length);
99
+ return new SiblingPath<N>(path.length as N, path);
100
+ }
91
101
 
92
- return Promise.resolve();
102
+ public getLeafLocation(leafIndex: number) {
103
+ return this.leafLocations[leafIndex];
93
104
  }
94
105
 
95
106
  /**
96
- * Calculates root while adding leaves and nodes to the cache.
97
- * @param leaves - The leaves to append.
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 async batchInsert(_leaves: Buffer[]): Promise<Buffer> {
101
- // If we have an even number of leaves, hash them all in pairs
102
- // Otherwise, store the final leaf to be shifted up to the next odd sized level
103
- let [layerWidth, nodeToShift] =
104
- _leaves.length & 1
105
- ? [_leaves.length - 1, serializeToBuffer(_leaves[_leaves.length - 1])]
106
- : [_leaves.length, Buffer.alloc(0)];
107
- // Allocate this layer's leaves and init the next layer up
108
- let thisLayer = _leaves.slice(0, layerWidth).map(l => serializeToBuffer(l));
109
- let nextLayer = [];
110
- // Store the bottom level leaves
111
- thisLayer.forEach((leaf, i) => this.storeNode(leaf, this.maxDepth, BigInt(i)));
112
- for (let i = 0; i < this.maxDepth; i++) {
113
- for (let j = 0; j < layerWidth; j += 2) {
114
- // Store the hash of each pair one layer up
115
- nextLayer[j / 2] = await this.hasher(serializeToBuffer(thisLayer[j]), serializeToBuffer(thisLayer[j + 1]));
116
- this.storeNode(nextLayer[j / 2], this.maxDepth - i - 1, BigInt(j >> 1));
117
- }
118
- layerWidth /= 2;
119
- if (layerWidth & 1) {
120
- if (nodeToShift.length) {
121
- // If the next layer has odd length, and we have a node that needs to be shifted up, add it here
122
- nextLayer.push(serializeToBuffer(nodeToShift));
123
- this.storeNode(nodeToShift, this.maxDepth - i - 1, BigInt((layerWidth * 2) >> 1));
124
- layerWidth += 1;
125
- nodeToShift = Buffer.alloc(0);
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
- // If we don't have a node waiting to be shifted, store the next layer's final node to be shifted
128
- layerWidth -= 1;
129
- nodeToShift = nextLayer[layerWidth];
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
- // reset the layers
133
- thisLayer = nextLayer;
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
- // return the root
138
- return thisLayer[0];
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 storeNode(value: Buffer, depth: number, index: bigint) {
142
- const key = indexToKeyHash(depth, index);
143
- this.cache[key] = value;
144
- this.valueCache[value.toString('hex')] = key;
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);