@osmura/merkletreejs 0.6.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.
@@ -0,0 +1,829 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnifiedBinaryTree = exports.InternalNode = exports.StemNode = exports.chunkifyCode = exports.getTreeKeyForCodeChunk = exports.getTreeKeyForStorageSlot = exports.getTreeKeyForCodeHash = exports.getTreeKeyForBasicData = exports.getTreeKey = exports.treeHash = exports.oldStyleAddressToAddress32 = exports.push32 = exports.push1 = exports.pushOffset = exports.MAIN_STORAGE_OFFSET = exports.STEM_SUBTREE_WIDTH = exports.CODE_OFFSET = exports.HEADER_STORAGE_OFFSET = exports.CODE_HASH_LEAF_KEY = exports.BASIC_DATA_LEAF_KEY = void 0;
4
+ const buffer_1 = require("buffer");
5
+ const Base_1 = require("./Base");
6
+ // -----------------------------------------------------------------------------
7
+ // Constants
8
+ // -----------------------------------------------------------------------------
9
+ /**
10
+ * Constants used for key derivation and tree organization.
11
+ * These define the structure and layout of the binary tree.
12
+ */
13
+ // Leaf key types
14
+ exports.BASIC_DATA_LEAF_KEY = 0; // Used for account basic data (nonce, balance, etc.)
15
+ exports.CODE_HASH_LEAF_KEY = 1; // Used for contract code hash
16
+ // Storage layout offsets
17
+ exports.HEADER_STORAGE_OFFSET = 64; // Start of header storage slots
18
+ exports.CODE_OFFSET = 128; // Start of code chunks
19
+ exports.STEM_SUBTREE_WIDTH = 256; // Width of each stem subtree (8 bits)
20
+ exports.MAIN_STORAGE_OFFSET = 256; // Start of main storage slots
21
+ // EVM PUSH instruction constants
22
+ exports.pushOffset = 95; // Base offset for PUSH instructions
23
+ exports.push1 = exports.pushOffset + 1; // PUSH1 opcode (0x60)
24
+ exports.push32 = exports.pushOffset + 32; // PUSH32 opcode (0x7F)
25
+ // -----------------------------------------------------------------------------
26
+ // Utility Functions for Key Derivation and Code Chunkification
27
+ // -----------------------------------------------------------------------------
28
+ /**
29
+ * Converts a 20-byte Ethereum address to a 32-byte address by left-padding with zeros.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const addr20 = Buffer.from('1234567890123456789012345678901234567890', 'hex')
34
+ * const addr32 = oldStyleAddressToAddress32(addr20)
35
+ * // addr32 = 0x000000000000123456789012345678901234567890 (32 bytes)
36
+ * ```
37
+ */
38
+ function oldStyleAddressToAddress32(address) {
39
+ if (address.length !== 20) {
40
+ throw new Error('Address must be 20 bytes.');
41
+ }
42
+ return buffer_1.Buffer.concat([buffer_1.Buffer.alloc(12, 0), address]);
43
+ }
44
+ exports.oldStyleAddressToAddress32 = oldStyleAddressToAddress32;
45
+ /**
46
+ * Applies a hash function to input data with proper buffering.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const input = Buffer.from('Hello World')
51
+ * const hashFn = (data) => blake3.hash(data)
52
+ * const hash = treeHash(input, hashFn)
53
+ * // hash = 32-byte BLAKE3 hash of 'Hello World'
54
+ * ```
55
+ */
56
+ function treeHash(input, hashFn) {
57
+ return treeHashFn(hashFn)(input);
58
+ }
59
+ exports.treeHash = treeHash;
60
+ function treeHashFn(hashFn) {
61
+ return Base_1.Base.bufferifyFn(hashFn);
62
+ }
63
+ /**
64
+ * Derives a tree key from an address and indices using a hash function.
65
+ * Used to generate unique keys for different parts of the tree structure.
66
+ * The resulting key is composed of a 31-byte stem (derived from address and treeIndex)
67
+ * and a 1-byte subIndex.
68
+ *
69
+ * @param address - A 32-byte address to derive the key from
70
+ * @param treeIndex - Primary index used to derive different trees for the same address
71
+ * @param subIndex - Secondary index used to derive different keys within the same tree
72
+ * @param hashFn - Hash function to use for key derivation
73
+ * @returns A 32-byte key that uniquely identifies this storage slot
74
+ * @throws Error if address is not 32 bytes
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const addr32 = oldStyleAddressToAddress32(address)
79
+ * const treeKey = getTreeKey(addr32, 0, 1, blake3.hash)
80
+ * // Returns a unique key for this address's tree at index 0, subIndex 1
81
+ * ```
82
+ */
83
+ function getTreeKey(address, treeIndex, subIndex, hashFn) {
84
+ // Validate address length
85
+ if (address.length !== 32) {
86
+ throw new Error('Address must be 32 bytes.');
87
+ }
88
+ // Get the tree-specific hash function
89
+ const treeHash = treeHashFn(hashFn);
90
+ // Create a buffer to store the tree index
91
+ const indexBuffer = buffer_1.Buffer.alloc(32, 0);
92
+ indexBuffer.writeUInt32LE(treeIndex, 0);
93
+ // Generate the stem by:
94
+ // 1. Concatenating address and index buffer
95
+ // 2. Hashing the result
96
+ // 3. Taking first 31 bytes
97
+ const stem = treeHash(buffer_1.Buffer.concat([address, indexBuffer]), hashFn).subarray(0, 31);
98
+ // Combine the stem with the subIndex to create the final 32-byte key
99
+ return buffer_1.Buffer.concat([stem, buffer_1.Buffer.from([subIndex])]);
100
+ }
101
+ exports.getTreeKey = getTreeKey;
102
+ /**
103
+ * Derives a key for storing an account's basic data (nonce, balance, etc.).
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const addr32 = oldStyleAddressToAddress32(address)
108
+ * const basicDataKey = getTreeKeyForBasicData(addr32, hashFn)
109
+ * tree.insert(basicDataKey, accountData)
110
+ * ```
111
+ */
112
+ function getTreeKeyForBasicData(address, hashFn) {
113
+ return getTreeKey(address, 0, exports.BASIC_DATA_LEAF_KEY, hashFn);
114
+ }
115
+ exports.getTreeKeyForBasicData = getTreeKeyForBasicData;
116
+ /**
117
+ * Derives a key for storing a contract's code hash.
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * const addr32 = oldStyleAddressToAddress32(contractAddress)
122
+ * const codeHashKey = getTreeKeyForCodeHash(addr32, hashFn)
123
+ * tree.insert(codeHashKey, codeHash)
124
+ * ```
125
+ */
126
+ function getTreeKeyForCodeHash(address, hashFn) {
127
+ return getTreeKey(address, 0, exports.CODE_HASH_LEAF_KEY, hashFn);
128
+ }
129
+ exports.getTreeKeyForCodeHash = getTreeKeyForCodeHash;
130
+ /**
131
+ * Derives a tree key for a storage slot in a contract's storage.
132
+ * Handles two types of storage:
133
+ * 1. Header storage (slots 0-63): Used for contract metadata and special storage
134
+ * 2. Main storage (slots 256+): Used for regular contract storage
135
+ *
136
+ * The storage layout is:
137
+ * - Header storage: slots [0, 63] mapped to positions [64, 127]
138
+ * - Main storage: slots [256+] mapped to positions [384+]
139
+ * This creates gaps in the tree to allow for future extensions.
140
+ *
141
+ * @param address - The 32-byte contract address
142
+ * @param storageKey - The storage slot number to access
143
+ * @param hashFn - Hash function to use for key derivation
144
+ * @returns A 32-byte key that uniquely identifies this storage slot
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const addr32 = oldStyleAddressToAddress32(contractAddress)
149
+ * // Get key for a header storage slot (0-63)
150
+ * const headerKey = getTreeKeyForStorageSlot(addr32, 5, blake3.hash)
151
+ * // Get key for a main storage slot (256+)
152
+ * const mainKey = getTreeKeyForStorageSlot(addr32, 300, blake3.hash)
153
+ * ```
154
+ */
155
+ function getTreeKeyForStorageSlot(address, storageKey, hashFn) {
156
+ let pos;
157
+ // If storage key is in header range (0-63), map it to positions 64-127
158
+ if (storageKey < exports.CODE_OFFSET - exports.HEADER_STORAGE_OFFSET) {
159
+ pos = exports.HEADER_STORAGE_OFFSET + storageKey;
160
+ }
161
+ else {
162
+ // Otherwise, map it to main storage starting at position 384
163
+ pos = exports.MAIN_STORAGE_OFFSET + storageKey;
164
+ }
165
+ // Convert the position to tree coordinates:
166
+ // - treeIndex: Which subtree to use (pos / 256)
167
+ // - subIndex: Which leaf in the subtree (pos % 256)
168
+ return getTreeKey(address, Math.floor(pos / exports.STEM_SUBTREE_WIDTH), pos % exports.STEM_SUBTREE_WIDTH, hashFn);
169
+ }
170
+ exports.getTreeKeyForStorageSlot = getTreeKeyForStorageSlot;
171
+ /**
172
+ * Derives a key for storing a chunk of contract code.
173
+ * Used when contract code is split into 32-byte chunks.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const addr32 = oldStyleAddressToAddress32(contractAddress)
178
+ * const chunks = chunkifyCode(contractCode)
179
+ * chunks.forEach((chunk, i) => {
180
+ * const key = getTreeKeyForCodeChunk(addr32, i, hashFn)
181
+ * tree.insert(key, chunk)
182
+ * })
183
+ * ```
184
+ */
185
+ function getTreeKeyForCodeChunk(address, chunkId, hashFn) {
186
+ const pos = exports.CODE_OFFSET + chunkId;
187
+ return getTreeKey(address, Math.floor(pos / exports.STEM_SUBTREE_WIDTH), pos % exports.STEM_SUBTREE_WIDTH, hashFn);
188
+ }
189
+ exports.getTreeKeyForCodeChunk = getTreeKeyForCodeChunk;
190
+ /**
191
+ * Splits EVM bytecode into 31-byte chunks with metadata.
192
+ * Each chunk is prefixed with a byte indicating the number of bytes
193
+ * that are part of PUSH data in the next chunk.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const code = Buffer.from('6001600201', 'hex') // PUSH1 01 PUSH1 02 ADD
198
+ * const chunks = chunkifyCode(code)
199
+ * // chunks[0] = [0x01, 0x60, 0x01, 0x60, 0x02, 0x01, 0x00...] (32 bytes)
200
+ * ```
201
+ */
202
+ function chunkifyCode(code) {
203
+ // If code length is not divisible by 31, pad it with zeros
204
+ // This ensures all chunks (except last) are exactly 31 bytes
205
+ const remainder = code.length % 31;
206
+ if (remainder !== 0) {
207
+ code = buffer_1.Buffer.concat([code, buffer_1.Buffer.alloc(31 - remainder, 0)]);
208
+ }
209
+ // Create array to track how many bytes of PUSH data follow each position
210
+ // Size is code.length + 32 to handle edge cases where PUSH data crosses chunk boundaries
211
+ const bytesToExecData = new Array(code.length + 32).fill(0);
212
+ // Iterate through the bytecode to identify PUSH operations and their data
213
+ let pos = 0;
214
+ while (pos < code.length) {
215
+ const opcode = code[pos];
216
+ let pushdataBytes = 0;
217
+ // Check if opcode is a PUSH operation (0x60 to 0x7F)
218
+ if (opcode >= exports.push1 && opcode <= exports.push32) {
219
+ // Calculate number of bytes to push (PUSH1 = 1 byte, PUSH2 = 2 bytes, etc.)
220
+ pushdataBytes = opcode - exports.pushOffset;
221
+ }
222
+ pos += 1; // Move past the opcode
223
+ // For each byte of PUSH data, store how many remaining PUSH bytes follow
224
+ // This helps identify which bytes are executable vs PUSH data when chunking
225
+ for (let x = 0; x < pushdataBytes; x++) {
226
+ bytesToExecData[pos + x] = pushdataBytes - x;
227
+ }
228
+ pos += pushdataBytes; // Skip over the PUSH data bytes
229
+ }
230
+ // Split the code into 32-byte chunks (1 prefix byte + 31 code bytes)
231
+ const chunks = [];
232
+ for (let start = 0; start < code.length; start += 31) {
233
+ // First byte of chunk indicates how many PUSH data bytes are at start of next chunk
234
+ const prefix = Math.min(bytesToExecData[start], 31);
235
+ // Create a new chunk by combining:
236
+ // 1. Single prefix byte indicating PUSH data count
237
+ // 2. 31 bytes of code starting at current position
238
+ const chunk = buffer_1.Buffer.concat([
239
+ buffer_1.Buffer.from([prefix]),
240
+ code.slice(start, start + 31)
241
+ ]);
242
+ chunks.push(chunk);
243
+ }
244
+ return chunks;
245
+ }
246
+ exports.chunkifyCode = chunkifyCode;
247
+ /**
248
+ * Leaf node in the binary tree that stores actual values.
249
+ * Contains a 31-byte stem and an array of 256 possible values.
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * const stem = Buffer.alloc(31, 0)
254
+ * const node = new StemNode(stem)
255
+ * node.setValue(0, Buffer.alloc(32).fill(1)) // Set value at index 0
256
+ * ```
257
+ */
258
+ class StemNode {
259
+ /**
260
+ * Creates a new StemNode with the given stem.
261
+ *
262
+ * @param stem - The 31-byte stem for this node.
263
+ */
264
+ constructor(stem) {
265
+ this.nodeType = 'stem';
266
+ if (stem.length !== 31) {
267
+ throw new Error('Stem must be 31 bytes.');
268
+ }
269
+ this.stem = stem;
270
+ this.values = new Array(256).fill(null);
271
+ }
272
+ /**
273
+ * Sets the value at the given index.
274
+ *
275
+ * @param index - The index to set the value at.
276
+ * @param value - The 32-byte value to set.
277
+ */
278
+ setValue(index, value) {
279
+ if (value.length !== 32) {
280
+ throw new Error('Value must be 32 bytes.');
281
+ }
282
+ this.values[index] = value;
283
+ }
284
+ }
285
+ exports.StemNode = StemNode;
286
+ /**
287
+ * Internal node in the binary tree with left and right children.
288
+ * Used to create the tree structure based on key bit patterns.
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * const node = new InternalNode()
293
+ * node.left = new StemNode(Buffer.alloc(31, 0))
294
+ * node.right = new StemNode(Buffer.alloc(31, 1))
295
+ * ```
296
+ */
297
+ class InternalNode {
298
+ constructor() {
299
+ this.left = null;
300
+ this.right = null;
301
+ this.nodeType = 'internal';
302
+ }
303
+ }
304
+ exports.InternalNode = InternalNode;
305
+ /**
306
+ * Main binary tree implementation that stores key-value pairs.
307
+ * Uses a configurable hash function and supports various operations.
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const tree = new BinaryTree(blake3.hash)
312
+ * tree.insert(key, value)
313
+ * const root = tree.merkelize()
314
+ * const serialized = tree.serialize()
315
+ * ```
316
+ */
317
+ class UnifiedBinaryTree {
318
+ /**
319
+ * Creates a new BinaryTree instance with the given hash function.
320
+ *
321
+ * @param hashFn - The hash function to use for key derivation.
322
+ */
323
+ constructor(hashFn) {
324
+ this.root = null;
325
+ this.hashFn = Base_1.Base.bufferifyFn(hashFn);
326
+ }
327
+ /**
328
+ * Inserts a key-value pair into the binary tree.
329
+ * The key is split into two parts:
330
+ * - stem (first 31 bytes): Determines the path in the tree
331
+ * - subIndex (last byte): Determines the position within a leaf node
332
+ *
333
+ * If this is the first insertion, creates a new leaf node.
334
+ * Otherwise, recursively traverses or builds the tree structure.
335
+ *
336
+ * @param key - A 32-byte key that determines where to store the value
337
+ * @param value - A 32-byte value to store
338
+ * @throws Error if key or value is not exactly 32 bytes
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * const tree = new BinaryTree(hashFn)
343
+ * const key = getTreeKey(address, 0, 1, hashFn)
344
+ * const value = Buffer.alloc(32).fill(1)
345
+ * tree.insert(key, value)
346
+ * ```
347
+ */
348
+ insert(key, value) {
349
+ // Validate input lengths
350
+ if (key.length !== 32) {
351
+ throw new Error('Key must be 32 bytes.');
352
+ }
353
+ if (value.length !== 32) {
354
+ throw new Error('Value must be 32 bytes.');
355
+ }
356
+ // Split key into stem (path) and subIndex (leaf position)
357
+ const stem = key.slice(0, 31);
358
+ const subIndex = key[31];
359
+ // If tree is empty, create first leaf node
360
+ if (this.root === null) {
361
+ this.root = new StemNode(stem);
362
+ this.root.setValue(subIndex, value);
363
+ return;
364
+ }
365
+ // Otherwise, recursively insert into existing tree
366
+ // Starting at depth 0 (root level)
367
+ this.root = this.insertRecursive(this.root, stem, subIndex, value, 0);
368
+ }
369
+ /**
370
+ * Recursively inserts a key-value pair into the tree.
371
+ * This method handles three cases:
372
+ * 1. Empty node: Creates a new leaf node
373
+ * 2. Stem node: Either updates value or splits into internal node
374
+ * 3. Internal node: Recursively traverses left or right based on stem bits
375
+ *
376
+ * @param node - Current node in traversal (null if empty)
377
+ * @param stem - The 31-byte path component of the key
378
+ * @param subIndex - The leaf position component of the key
379
+ * @param value - The 32-byte value to store
380
+ * @param depth - Current depth in the tree (max 247 to prevent hash collisions)
381
+ * @returns The new or updated node
382
+ * @throws Error if tree depth exceeds 247 levels
383
+ */
384
+ insertRecursive(node, stem, subIndex, value, depth) {
385
+ // Prevent deep recursion that could lead to hash collisions
386
+ if (depth >= 248) {
387
+ throw new Error('Depth must be less than 248.');
388
+ }
389
+ // Case 1: Empty node - create new leaf
390
+ if (node === null) {
391
+ const newNode = new StemNode(stem);
392
+ newNode.setValue(subIndex, value);
393
+ return newNode;
394
+ }
395
+ // Convert stem to bit array for path decisions
396
+ const stemBits = this.bytesToBits(stem);
397
+ // Case 2: Reached a leaf node (StemNode)
398
+ if (node instanceof StemNode) {
399
+ // If stems match, just update the value
400
+ if (node.stem.equals(stem)) {
401
+ node.setValue(subIndex, value);
402
+ return node;
403
+ }
404
+ // If stems differ, need to split this leaf node
405
+ const existingStemBits = this.bytesToBits(node.stem);
406
+ return this.splitLeaf(node, stemBits, existingStemBits, subIndex, value, depth);
407
+ }
408
+ else { // Case 3: Internal node - traverse left or right
409
+ // Use current depth's bit to decide path (0 = left, 1 = right)
410
+ const bit = stemBits[depth];
411
+ if (bit === 0) {
412
+ node.left = this.insertRecursive(node.left, stem, subIndex, value, depth + 1);
413
+ }
414
+ else {
415
+ node.right = this.insertRecursive(node.right, stem, subIndex, value, depth + 1);
416
+ }
417
+ return node;
418
+ }
419
+ }
420
+ /**
421
+ * Converts a byte array to an array of individual bits.
422
+ * Each byte is converted to 8 bits, maintaining the most-significant-bit first order.
423
+ * Used for making path decisions in the binary tree based on stem bytes.
424
+ *
425
+ * @param data - Buffer containing bytes to convert
426
+ * @returns Array of bits (0s and 1s) in MSB-first order
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const bytes = Buffer.from([0xA5]) // Binary: 10100101
431
+ * const bits = bytesToBits(bytes)
432
+ * // bits = [1,0,1,0,0,1,0,1]
433
+ * // ^ MSB LSB ^
434
+ * ```
435
+ *
436
+ * Process for each byte:
437
+ * 1. Right shift by (7-i) positions to get desired bit to LSB
438
+ * 2. AND with 1 to isolate that bit
439
+ * 3. Push result (0 or 1) to output array
440
+ */
441
+ bytesToBits(data) {
442
+ const bits = [];
443
+ // Process each byte in the input buffer
444
+ for (const byte of data) {
445
+ // Extract each bit from the byte, MSB first
446
+ for (let i = 0; i < 8; i++) {
447
+ // Right shift to position + mask to get bit value
448
+ // i=0: shift 7 (10100101 -> 00000001)
449
+ // i=1: shift 6 (10100101 -> 00000000)
450
+ // i=2: shift 5 (10100101 -> 00000001)
451
+ // etc.
452
+ bits.push((byte >> (7 - i)) & 1);
453
+ }
454
+ }
455
+ return bits;
456
+ }
457
+ /**
458
+ * Converts an array of bits back into a Buffer of bytes.
459
+ * This is the inverse operation of bytesToBits.
460
+ * Processes bits in groups of 8, maintaining MSB-first order.
461
+ *
462
+ * @param bits - Array of 0s and 1s to convert to bytes
463
+ * @returns Buffer containing the reconstructed bytes
464
+ * @throws Error if the number of bits is not divisible by 8
465
+ *
466
+ * @example
467
+ * ```typescript
468
+ * const bits = [1,0,1,0,0,1,0,1] // Represents binary 10100101
469
+ * const bytes = bitsToBytes(bits)
470
+ * // bytes = Buffer.from([0xA5])
471
+ * ```
472
+ *
473
+ * Process for each byte:
474
+ * 1. Take 8 bits at a time
475
+ * 2. For each bit:
476
+ * - Shift it left to its correct position (7-j positions)
477
+ * - OR it with the accumulating byte value
478
+ * 3. Add completed byte to array
479
+ */
480
+ bitsToBytes(bits) {
481
+ // Ensure we have complete bytes (groups of 8 bits)
482
+ if (bits.length % 8 !== 0) {
483
+ throw new Error('Number of bits must be a multiple of 8.');
484
+ }
485
+ const bytes = [];
486
+ // Process bits in groups of 8
487
+ for (let i = 0; i < bits.length; i += 8) {
488
+ let byte = 0;
489
+ // Build each byte bit by bit
490
+ for (let j = 0; j < 8; j++) {
491
+ // Left shift each bit to its position and OR with current byte
492
+ // j=0: bit goes to position 7 (MSB)
493
+ // j=1: bit goes to position 6
494
+ // j=2: bit goes to position 5
495
+ // etc.
496
+ byte |= bits[i + j] << (7 - j);
497
+ }
498
+ bytes.push(byte);
499
+ }
500
+ return buffer_1.Buffer.from(bytes);
501
+ }
502
+ /**
503
+ * Applies the hash function to the given data with special handling for null values.
504
+ * Used primarily for Merkle tree calculations and node hashing.
505
+ *
506
+ * Special cases:
507
+ * - null input -> returns 32-byte zero buffer
508
+ * - 64-byte zero buffer -> returns 32-byte zero buffer
509
+ * This handling ensures consistent treatment of empty/uninitialized nodes.
510
+ *
511
+ * @param data - Buffer to hash, must be either 32 or 64 bytes, or null
512
+ * @returns A 32-byte hash of the data, or zero32 for empty cases
513
+ * @throws Error if data length is not 32 or 64 bytes
514
+ *
515
+ * @example
516
+ * ```typescript
517
+ * // Regular hashing
518
+ * const hash1 = hashData(nodeBuffer) // Returns hash of data
519
+ *
520
+ * // Empty cases - all return 32 zeros
521
+ * const hash2 = hashData(null)
522
+ * const hash3 = hashData(Buffer.alloc(64, 0))
523
+ * ```
524
+ */
525
+ hashData(data) {
526
+ // Pre-allocate zero buffers for comparison and return values
527
+ const zero64 = buffer_1.Buffer.alloc(64, 0); // Used to detect empty 64-byte input
528
+ const zero32 = buffer_1.Buffer.alloc(32, 0); // Returned for empty/zero cases
529
+ // Return zero32 for either null input or a 64-byte zero buffer
530
+ // This treats empty nodes consistently in the tree
531
+ if (data === null || data.equals(zero64)) {
532
+ return zero32;
533
+ }
534
+ // Validate input size - must be either a single node (32 bytes)
535
+ // or a pair of nodes being combined (64 bytes)
536
+ if (data.length !== 32 && data.length !== 64) {
537
+ throw new Error('Data must be 32 or 64 bytes.');
538
+ }
539
+ // Apply the configured hash function to valid data
540
+ return this.hashFn(data);
541
+ }
542
+ /**
543
+ * Computes the Merkle root of the entire tree.
544
+ * The Merkle root is a single 32-byte hash that uniquely represents the entire tree state.
545
+ *
546
+ * The computation follows these rules:
547
+ * 1. For Internal nodes: hash(leftChild || rightChild)
548
+ * 2. For Stem nodes: hash(stem || 0x00 || merkleOfValues)
549
+ * 3. For empty nodes: return 32 bytes of zeros
550
+ *
551
+ * @returns A 32-byte Buffer containing the Merkle root
552
+ *
553
+ * @example
554
+ * ```typescript
555
+ * const tree = new BinaryTree(hashFn)
556
+ * tree.insert(key1, value1)
557
+ * tree.insert(key2, value2)
558
+ * const root = tree.merkelize()
559
+ * // root now contains a 32-byte hash representing the entire tree
560
+ * ```
561
+ */
562
+ merkelize() {
563
+ /**
564
+ * Recursive helper function to compute the Merkle root of a subtree
565
+ * @param node - Root of the subtree to compute hash for
566
+ * @returns 32-byte Buffer containing the node's Merkle hash
567
+ */
568
+ const computeMerkle = (node) => {
569
+ const zero32 = buffer_1.Buffer.alloc(32, 0);
570
+ // Base case: empty node returns zero hash
571
+ if (node === null) {
572
+ return zero32;
573
+ }
574
+ // Case 1: Internal node
575
+ if (node instanceof InternalNode) {
576
+ // Recursively compute hashes of left and right children
577
+ const leftHash = computeMerkle(node.left);
578
+ const rightHash = computeMerkle(node.right);
579
+ // Combine and hash the children
580
+ return this.hashData(buffer_1.Buffer.concat([leftHash, rightHash]));
581
+ }
582
+ // Case 2: Stem node (leaf)
583
+ // First compute Merkle tree of the 256 values in this node
584
+ const level = node.values.map(val => this.hashData(val));
585
+ // Build a balanced binary tree from the value hashes
586
+ // Each iteration combines pairs of hashes until only root remains
587
+ while (level.length > 1) {
588
+ const newLevel = [];
589
+ for (let i = 0; i < level.length; i += 2) {
590
+ // Combine each pair of hashes
591
+ newLevel.push(this.hashData(buffer_1.Buffer.concat([level[i], level[i + 1]])));
592
+ }
593
+ // Replace old level with new level
594
+ level.splice(0, level.length, ...newLevel);
595
+ }
596
+ // Final stem node hash combines:
597
+ // 1. The stem (31 bytes)
598
+ // 2. A zero byte (1 byte)
599
+ // 3. The Merkle root of values (32 bytes)
600
+ return this.hashData(buffer_1.Buffer.concat([
601
+ node.stem,
602
+ buffer_1.Buffer.from([0]),
603
+ level[0] // 32-byte value root
604
+ ]));
605
+ };
606
+ // Start computation from root
607
+ return computeMerkle(this.root);
608
+ }
609
+ // -------------------------------------------------------------
610
+ // New Features
611
+ // -------------------------------------------------------------
612
+ /**
613
+ * Incrementally updates the value for an existing key.
614
+ * For our implementation, update is the same as insert.
615
+ *
616
+ * @param key - A 32-byte key.
617
+ * @param value - A 32-byte value.
618
+ */
619
+ update(key, value) {
620
+ // Simply re-insert; our insert() method will update an existing key.
621
+ this.insert(key, value);
622
+ }
623
+ /**
624
+ * Performs a batch insertion of key-value pairs.
625
+ *
626
+ * @param entries - An array of objects with 'key' and 'value' properties.
627
+ */
628
+ insertBatch(entries) {
629
+ for (const { key, value } of entries) {
630
+ this.insert(key, value);
631
+ }
632
+ }
633
+ /**
634
+ * Serializes the entire tree structure into a JSON Buffer.
635
+ * Converts the tree into a format that can be stored or transmitted,
636
+ * preserving the complete structure and all values.
637
+ *
638
+ * The serialized format for each node type is:
639
+ * 1. Stem Node:
640
+ * ```json
641
+ * {
642
+ * "nodeType": "stem",
643
+ * "stem": "hex string of 31 bytes",
644
+ * "values": ["hex string or null", ...] // 256 entries
645
+ * }
646
+ * ```
647
+ * 2. Internal Node:
648
+ * ```json
649
+ * {
650
+ * "nodeType": "internal",
651
+ * "left": <node or null>,
652
+ * "right": <node or null>
653
+ * }
654
+ * ```
655
+ *
656
+ * @returns Buffer containing the JSON string representation of the tree
657
+ *
658
+ * @example
659
+ * ```typescript
660
+ * const tree = new BinaryTree(hashFn)
661
+ * tree.insert(key, value)
662
+ * const serialized = tree.serialize()
663
+ * // Save to file or transmit
664
+ * const newTree = UnifiedBinaryTree.deserialize(serialized, hashFn)
665
+ * ```
666
+ */
667
+ serialize() {
668
+ /**
669
+ * Helper function to recursively serialize each node in the tree
670
+ * Converts Buffer data to hex strings for JSON compatibility
671
+ *
672
+ * @param node - The node to serialize
673
+ * @returns JSON-compatible object representation of the node
674
+ */
675
+ function serializeNode(node) {
676
+ // Handle empty nodes
677
+ if (!node)
678
+ return null;
679
+ // Case 1: Stem (leaf) node
680
+ if (node instanceof StemNode) {
681
+ return {
682
+ nodeType: 'stem',
683
+ stem: node.stem.toString('hex'),
684
+ values: node.values.map(val => // Convert 256 values to hex
685
+ (val ? val.toString('hex') : null)) // Preserve null values
686
+ };
687
+ }
688
+ else { // Case 2: Internal node
689
+ return {
690
+ nodeType: 'internal',
691
+ left: serializeNode(node.left),
692
+ right: serializeNode(node.right) // Recursively serialize right subtree
693
+ };
694
+ }
695
+ }
696
+ // Wrap the serialized tree in a root object and convert to Buffer
697
+ const obj = { root: serializeNode(this.root) };
698
+ return buffer_1.Buffer.from(JSON.stringify(obj), 'utf8');
699
+ }
700
+ /**
701
+ * Reconstructs a BinaryTree from its serialized form.
702
+ * This is the inverse operation of serialize().
703
+ *
704
+ * Expected input format:
705
+ * ```json
706
+ * {
707
+ * "root": {
708
+ * "nodeType": "internal"|"stem",
709
+ * // For stem nodes:
710
+ * "stem": "hex string",
711
+ * "values": ["hex string"|null, ...],
712
+ * // For internal nodes:
713
+ * "left": <node|null>,
714
+ * "right": <node|null>
715
+ * }
716
+ * }
717
+ * ```
718
+ *
719
+ * @param data - Buffer containing the JSON serialized tree
720
+ * @param hashFn - Hash function to use for the reconstructed tree
721
+ * @returns A new BinaryTree instance with the deserialized structure
722
+ * @throws Error if JSON parsing fails or format is invalid
723
+ *
724
+ * @example
725
+ * ```typescript
726
+ * const serialized = existingTree.serialize()
727
+ * const newTree = UnifiedBinaryTree.deserialize(serialized, hashFn)
728
+ * // newTree is now identical to existingTree
729
+ * ```
730
+ */
731
+ static deserialize(data, hashFn) {
732
+ // Parse the JSON string from the buffer
733
+ const json = JSON.parse(data.toString('utf8'));
734
+ /**
735
+ * Helper function to recursively deserialize nodes
736
+ * Converts hex strings back to Buffers and reconstructs the tree structure
737
+ *
738
+ * @param obj - JSON object representing a node
739
+ * @returns Reconstructed BinaryTreeNode or null
740
+ */
741
+ function deserializeNode(obj) {
742
+ // Handle null nodes
743
+ if (obj === null)
744
+ return null;
745
+ // Case 1: Reconstruct stem (leaf) node
746
+ if (obj.nodeType === 'stem') {
747
+ // Convert hex stem back to Buffer
748
+ const node = new StemNode(buffer_1.Buffer.from(obj.stem, 'hex'));
749
+ // Convert hex values back to Buffers, preserving nulls
750
+ node.values = obj.values.map((v) => (v !== null ? buffer_1.Buffer.from(v, 'hex') : null));
751
+ return node;
752
+ }
753
+ else if (obj.nodeType === 'internal') { // Case 2: Reconstruct internal node
754
+ const node = new InternalNode();
755
+ // Recursively deserialize left and right subtrees
756
+ node.left = deserializeNode(obj.left);
757
+ node.right = deserializeNode(obj.right);
758
+ return node;
759
+ }
760
+ // Invalid node type
761
+ return null;
762
+ }
763
+ // Create new tree with provided hash function
764
+ const tree = new UnifiedBinaryTree(hashFn);
765
+ // Deserialize and set the root node
766
+ tree.root = deserializeNode(json.root);
767
+ return tree;
768
+ }
769
+ /**
770
+ * Splits a leaf node when inserting a new key with a different stem.
771
+ * This method handles two cases:
772
+ * 1. Matching bits at current depth: Continue splitting recursively
773
+ * 2. Different bits at current depth: Create new internal node and arrange leaves
774
+ *
775
+ * The process ensures that keys with different stems are properly distributed
776
+ * in the tree based on their binary representation.
777
+ *
778
+ * @param leaf - The existing leaf node to split
779
+ * @param stemBits - Binary representation of the new stem
780
+ * @param existingStemBits - Binary representation of the existing stem
781
+ * @param subIndex - Position within leaf node for new value
782
+ * @param value - Value to store at the new position
783
+ * @param depth - Current depth in the tree
784
+ * @returns A new internal node containing both the existing and new data
785
+ *
786
+ * Example:
787
+ * If stems differ at bit 3:
788
+ * - New stem: [1,0,1,0,...]
789
+ * - Existing stem: [1,0,1,1,...]
790
+ * ^ split here
791
+ * Creates an internal node with the leaf nodes arranged based on bit 3
792
+ */
793
+ splitLeaf(leaf, stemBits, existingStemBits, subIndex, value, depth) {
794
+ // Case 1: Bits match at current depth, need to go deeper
795
+ if (stemBits[depth] === existingStemBits[depth]) {
796
+ const newInternal = new InternalNode();
797
+ const bit = stemBits[depth];
798
+ // Continue splitting recursively in the matching direction
799
+ if (bit === 0) {
800
+ newInternal.left = this.splitLeaf(leaf, stemBits, existingStemBits, subIndex, value, depth + 1);
801
+ }
802
+ else {
803
+ newInternal.right = this.splitLeaf(leaf, stemBits, existingStemBits, subIndex, value, depth + 1);
804
+ }
805
+ return newInternal;
806
+ }
807
+ else { // Case 2: Bits differ at current depth, create split point
808
+ const newInternal = new InternalNode();
809
+ const bit = stemBits[depth];
810
+ // Create new leaf node for the new stem
811
+ const newStem = this.bitsToBytes(stemBits);
812
+ const newNode = new StemNode(newStem);
813
+ newNode.setValue(subIndex, value);
814
+ // Arrange nodes based on their bits at current depth
815
+ // bit = 0: new node goes left, existing goes right
816
+ // bit = 1: new node goes right, existing goes left
817
+ if (bit === 0) {
818
+ newInternal.left = newNode;
819
+ newInternal.right = leaf;
820
+ }
821
+ else {
822
+ newInternal.right = newNode;
823
+ newInternal.left = leaf;
824
+ }
825
+ return newInternal;
826
+ }
827
+ }
828
+ }
829
+ exports.UnifiedBinaryTree = UnifiedBinaryTree;