@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.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/Base.d.ts +212 -0
- package/dist/Base.js +379 -0
- package/dist/IncrementalMerkleTree.d.ts +36 -0
- package/dist/IncrementalMerkleTree.js +252 -0
- package/dist/MerkleMountainRange.d.ts +95 -0
- package/dist/MerkleMountainRange.js +436 -0
- package/dist/MerkleRadixTree.d.ts +33 -0
- package/dist/MerkleRadixTree.js +152 -0
- package/dist/MerkleSumTree.d.ts +37 -0
- package/dist/MerkleSumTree.js +138 -0
- package/dist/MerkleTree.d.ts +570 -0
- package/dist/MerkleTree.js +1326 -0
- package/dist/UnifiedBinaryTree.d.ts +454 -0
- package/dist/UnifiedBinaryTree.js +829 -0
- package/dist/functional.d.ts +255 -0
- package/dist/functional.js +360 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +30 -0
- package/merkletree.js +1 -0
- package/package.json +155 -0
|
@@ -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;
|