UNPKG

merkletreejs

Version:
830 lines (829 loc) 33 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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; const buffer_1 = require("buffer"); const Base_1 = require("./Base"); // ----------------------------------------------------------------------------- // Constants // ----------------------------------------------------------------------------- /** * Constants used for key derivation and tree organization. * These define the structure and layout of the binary tree. */ // Leaf key types exports.BASIC_DATA_LEAF_KEY = 0; // Used for account basic data (nonce, balance, etc.) exports.CODE_HASH_LEAF_KEY = 1; // Used for contract code hash // Storage layout offsets exports.HEADER_STORAGE_OFFSET = 64; // Start of header storage slots exports.CODE_OFFSET = 128; // Start of code chunks exports.STEM_SUBTREE_WIDTH = 256; // Width of each stem subtree (8 bits) exports.MAIN_STORAGE_OFFSET = 256; // Start of main storage slots // EVM PUSH instruction constants exports.pushOffset = 95; // Base offset for PUSH instructions exports.push1 = exports.pushOffset + 1; // PUSH1 opcode (0x60) exports.push32 = exports.pushOffset + 32; // PUSH32 opcode (0x7F) // ----------------------------------------------------------------------------- // Utility Functions for Key Derivation and Code Chunkification // ----------------------------------------------------------------------------- /** * Converts a 20-byte Ethereum address to a 32-byte address by left-padding with zeros. * * @example * ```typescript * const addr20 = Buffer.from('1234567890123456789012345678901234567890', 'hex') * const addr32 = oldStyleAddressToAddress32(addr20) * // addr32 = 0x000000000000123456789012345678901234567890 (32 bytes) * ``` */ function oldStyleAddressToAddress32(address) { if (address.length !== 20) { throw new Error('Address must be 20 bytes.'); } return buffer_1.Buffer.concat([buffer_1.Buffer.alloc(12, 0), address]); } exports.oldStyleAddressToAddress32 = oldStyleAddressToAddress32; /** * Applies a hash function to input data with proper buffering. * * @example * ```typescript * const input = Buffer.from('Hello World') * const hashFn = (data) => blake3.hash(data) * const hash = treeHash(input, hashFn) * // hash = 32-byte BLAKE3 hash of 'Hello World' * ``` */ function treeHash(input, hashFn) { return treeHashFn(hashFn)(input); } exports.treeHash = treeHash; function treeHashFn(hashFn) { return Base_1.Base.bufferifyFn(hashFn); } /** * Derives a tree key from an address and indices using a hash function. * Used to generate unique keys for different parts of the tree structure. * The resulting key is composed of a 31-byte stem (derived from address and treeIndex) * and a 1-byte subIndex. * * @param address - A 32-byte address to derive the key from * @param treeIndex - Primary index used to derive different trees for the same address * @param subIndex - Secondary index used to derive different keys within the same tree * @param hashFn - Hash function to use for key derivation * @returns A 32-byte key that uniquely identifies this storage slot * @throws Error if address is not 32 bytes * * @example * ```typescript * const addr32 = oldStyleAddressToAddress32(address) * const treeKey = getTreeKey(addr32, 0, 1, blake3.hash) * // Returns a unique key for this address's tree at index 0, subIndex 1 * ``` */ function getTreeKey(address, treeIndex, subIndex, hashFn) { // Validate address length if (address.length !== 32) { throw new Error('Address must be 32 bytes.'); } // Get the tree-specific hash function const treeHash = treeHashFn(hashFn); // Create a buffer to store the tree index const indexBuffer = buffer_1.Buffer.alloc(32, 0); indexBuffer.writeUInt32LE(treeIndex, 0); // Generate the stem by: // 1. Concatenating address and index buffer // 2. Hashing the result // 3. Taking first 31 bytes const stem = treeHash(buffer_1.Buffer.concat([address, indexBuffer]), hashFn).subarray(0, 31); // Combine the stem with the subIndex to create the final 32-byte key return buffer_1.Buffer.concat([stem, buffer_1.Buffer.from([subIndex])]); } exports.getTreeKey = getTreeKey; /** * Derives a key for storing an account's basic data (nonce, balance, etc.). * * @example * ```typescript * const addr32 = oldStyleAddressToAddress32(address) * const basicDataKey = getTreeKeyForBasicData(addr32, hashFn) * tree.insert(basicDataKey, accountData) * ``` */ function getTreeKeyForBasicData(address, hashFn) { return getTreeKey(address, 0, exports.BASIC_DATA_LEAF_KEY, hashFn); } exports.getTreeKeyForBasicData = getTreeKeyForBasicData; /** * Derives a key for storing a contract's code hash. * * @example * ```typescript * const addr32 = oldStyleAddressToAddress32(contractAddress) * const codeHashKey = getTreeKeyForCodeHash(addr32, hashFn) * tree.insert(codeHashKey, codeHash) * ``` */ function getTreeKeyForCodeHash(address, hashFn) { return getTreeKey(address, 0, exports.CODE_HASH_LEAF_KEY, hashFn); } exports.getTreeKeyForCodeHash = getTreeKeyForCodeHash; /** * Derives a tree key for a storage slot in a contract's storage. * Handles two types of storage: * 1. Header storage (slots 0-63): Used for contract metadata and special storage * 2. Main storage (slots 256+): Used for regular contract storage * * The storage layout is: * - Header storage: slots [0, 63] mapped to positions [64, 127] * - Main storage: slots [256+] mapped to positions [384+] * This creates gaps in the tree to allow for future extensions. * * @param address - The 32-byte contract address * @param storageKey - The storage slot number to access * @param hashFn - Hash function to use for key derivation * @returns A 32-byte key that uniquely identifies this storage slot * * @example * ```typescript * const addr32 = oldStyleAddressToAddress32(contractAddress) * // Get key for a header storage slot (0-63) * const headerKey = getTreeKeyForStorageSlot(addr32, 5, blake3.hash) * // Get key for a main storage slot (256+) * const mainKey = getTreeKeyForStorageSlot(addr32, 300, blake3.hash) * ``` */ function getTreeKeyForStorageSlot(address, storageKey, hashFn) { let pos; // If storage key is in header range (0-63), map it to positions 64-127 if (storageKey < exports.CODE_OFFSET - exports.HEADER_STORAGE_OFFSET) { pos = exports.HEADER_STORAGE_OFFSET + storageKey; } else { // Otherwise, map it to main storage starting at position 384 pos = exports.MAIN_STORAGE_OFFSET + storageKey; } // Convert the position to tree coordinates: // - treeIndex: Which subtree to use (pos / 256) // - subIndex: Which leaf in the subtree (pos % 256) return getTreeKey(address, Math.floor(pos / exports.STEM_SUBTREE_WIDTH), pos % exports.STEM_SUBTREE_WIDTH, hashFn); } exports.getTreeKeyForStorageSlot = getTreeKeyForStorageSlot; /** * Derives a key for storing a chunk of contract code. * Used when contract code is split into 32-byte chunks. * * @example * ```typescript * const addr32 = oldStyleAddressToAddress32(contractAddress) * const chunks = chunkifyCode(contractCode) * chunks.forEach((chunk, i) => { * const key = getTreeKeyForCodeChunk(addr32, i, hashFn) * tree.insert(key, chunk) * }) * ``` */ function getTreeKeyForCodeChunk(address, chunkId, hashFn) { const pos = exports.CODE_OFFSET + chunkId; return getTreeKey(address, Math.floor(pos / exports.STEM_SUBTREE_WIDTH), pos % exports.STEM_SUBTREE_WIDTH, hashFn); } exports.getTreeKeyForCodeChunk = getTreeKeyForCodeChunk; /** * Splits EVM bytecode into 31-byte chunks with metadata. * Each chunk is prefixed with a byte indicating the number of bytes * that are part of PUSH data in the next chunk. * * @example * ```typescript * const code = Buffer.from('6001600201', 'hex') // PUSH1 01 PUSH1 02 ADD * const chunks = chunkifyCode(code) * // chunks[0] = [0x01, 0x60, 0x01, 0x60, 0x02, 0x01, 0x00...] (32 bytes) * ``` */ function chunkifyCode(code) { // If code length is not divisible by 31, pad it with zeros // This ensures all chunks (except last) are exactly 31 bytes const remainder = code.length % 31; if (remainder !== 0) { code = buffer_1.Buffer.concat([code, buffer_1.Buffer.alloc(31 - remainder, 0)]); } // Create array to track how many bytes of PUSH data follow each position // Size is code.length + 32 to handle edge cases where PUSH data crosses chunk boundaries const bytesToExecData = new Array(code.length + 32).fill(0); // Iterate through the bytecode to identify PUSH operations and their data let pos = 0; while (pos < code.length) { const opcode = code[pos]; let pushdataBytes = 0; // Check if opcode is a PUSH operation (0x60 to 0x7F) if (opcode >= exports.push1 && opcode <= exports.push32) { // Calculate number of bytes to push (PUSH1 = 1 byte, PUSH2 = 2 bytes, etc.) pushdataBytes = opcode - exports.pushOffset; } pos += 1; // Move past the opcode // For each byte of PUSH data, store how many remaining PUSH bytes follow // This helps identify which bytes are executable vs PUSH data when chunking for (let x = 0; x < pushdataBytes; x++) { bytesToExecData[pos + x] = pushdataBytes - x; } pos += pushdataBytes; // Skip over the PUSH data bytes } // Split the code into 32-byte chunks (1 prefix byte + 31 code bytes) const chunks = []; for (let start = 0; start < code.length; start += 31) { // First byte of chunk indicates how many PUSH data bytes are at start of next chunk const prefix = Math.min(bytesToExecData[start], 31); // Create a new chunk by combining: // 1. Single prefix byte indicating PUSH data count // 2. 31 bytes of code starting at current position const chunk = buffer_1.Buffer.concat([ buffer_1.Buffer.from([prefix]), code.slice(start, start + 31) ]); chunks.push(chunk); } return chunks; } exports.chunkifyCode = chunkifyCode; /** * Leaf node in the binary tree that stores actual values. * Contains a 31-byte stem and an array of 256 possible values. * * @example * ```typescript * const stem = Buffer.alloc(31, 0) * const node = new StemNode(stem) * node.setValue(0, Buffer.alloc(32).fill(1)) // Set value at index 0 * ``` */ class StemNode { /** * Creates a new StemNode with the given stem. * * @param stem - The 31-byte stem for this node. */ constructor(stem) { this.nodeType = 'stem'; if (stem.length !== 31) { throw new Error('Stem must be 31 bytes.'); } this.stem = stem; this.values = new Array(256).fill(null); } /** * Sets the value at the given index. * * @param index - The index to set the value at. * @param value - The 32-byte value to set. */ setValue(index, value) { if (value.length !== 32) { throw new Error('Value must be 32 bytes.'); } this.values[index] = value; } } exports.StemNode = StemNode; /** * Internal node in the binary tree with left and right children. * Used to create the tree structure based on key bit patterns. * * @example * ```typescript * const node = new InternalNode() * node.left = new StemNode(Buffer.alloc(31, 0)) * node.right = new StemNode(Buffer.alloc(31, 1)) * ``` */ class InternalNode { constructor() { this.left = null; this.right = null; this.nodeType = 'internal'; } } exports.InternalNode = InternalNode; /** * Main binary tree implementation that stores key-value pairs. * Uses a configurable hash function and supports various operations. * * @example * ```typescript * const tree = new BinaryTree(blake3.hash) * tree.insert(key, value) * const root = tree.merkelize() * const serialized = tree.serialize() * ``` */ class UnifiedBinaryTree { /** * Creates a new BinaryTree instance with the given hash function. * * @param hashFn - The hash function to use for key derivation. */ constructor(hashFn) { this.root = null; this.hashFn = Base_1.Base.bufferifyFn(hashFn); } /** * Inserts a key-value pair into the binary tree. * The key is split into two parts: * - stem (first 31 bytes): Determines the path in the tree * - subIndex (last byte): Determines the position within a leaf node * * If this is the first insertion, creates a new leaf node. * Otherwise, recursively traverses or builds the tree structure. * * @param key - A 32-byte key that determines where to store the value * @param value - A 32-byte value to store * @throws Error if key or value is not exactly 32 bytes * * @example * ```typescript * const tree = new BinaryTree(hashFn) * const key = getTreeKey(address, 0, 1, hashFn) * const value = Buffer.alloc(32).fill(1) * tree.insert(key, value) * ``` */ insert(key, value) { // Validate input lengths if (key.length !== 32) { throw new Error('Key must be 32 bytes.'); } if (value.length !== 32) { throw new Error('Value must be 32 bytes.'); } // Split key into stem (path) and subIndex (leaf position) const stem = key.slice(0, 31); const subIndex = key[31]; // If tree is empty, create first leaf node if (this.root === null) { this.root = new StemNode(stem); this.root.setValue(subIndex, value); return; } // Otherwise, recursively insert into existing tree // Starting at depth 0 (root level) this.root = this.insertRecursive(this.root, stem, subIndex, value, 0); } /** * Recursively inserts a key-value pair into the tree. * This method handles three cases: * 1. Empty node: Creates a new leaf node * 2. Stem node: Either updates value or splits into internal node * 3. Internal node: Recursively traverses left or right based on stem bits * * @param node - Current node in traversal (null if empty) * @param stem - The 31-byte path component of the key * @param subIndex - The leaf position component of the key * @param value - The 32-byte value to store * @param depth - Current depth in the tree (max 247 to prevent hash collisions) * @returns The new or updated node * @throws Error if tree depth exceeds 247 levels */ insertRecursive(node, stem, subIndex, value, depth) { // Prevent deep recursion that could lead to hash collisions if (depth >= 248) { throw new Error('Depth must be less than 248.'); } // Case 1: Empty node - create new leaf if (node === null) { const newNode = new StemNode(stem); newNode.setValue(subIndex, value); return newNode; } // Convert stem to bit array for path decisions const stemBits = this.bytesToBits(stem); // Case 2: Reached a leaf node (StemNode) if (node instanceof StemNode) { // If stems match, just update the value if (node.stem.equals(stem)) { node.setValue(subIndex, value); return node; } // If stems differ, need to split this leaf node const existingStemBits = this.bytesToBits(node.stem); return this.splitLeaf(node, stemBits, existingStemBits, subIndex, value, depth); } else { // Case 3: Internal node - traverse left or right // Use current depth's bit to decide path (0 = left, 1 = right) const bit = stemBits[depth]; if (bit === 0) { node.left = this.insertRecursive(node.left, stem, subIndex, value, depth + 1); } else { node.right = this.insertRecursive(node.right, stem, subIndex, value, depth + 1); } return node; } } /** * Converts a byte array to an array of individual bits. * Each byte is converted to 8 bits, maintaining the most-significant-bit first order. * Used for making path decisions in the binary tree based on stem bytes. * * @param data - Buffer containing bytes to convert * @returns Array of bits (0s and 1s) in MSB-first order * * @example * ```typescript * const bytes = Buffer.from([0xA5]) // Binary: 10100101 * const bits = bytesToBits(bytes) * // bits = [1,0,1,0,0,1,0,1] * // ^ MSB LSB ^ * ``` * * Process for each byte: * 1. Right shift by (7-i) positions to get desired bit to LSB * 2. AND with 1 to isolate that bit * 3. Push result (0 or 1) to output array */ bytesToBits(data) { const bits = []; // Process each byte in the input buffer for (const byte of data) { // Extract each bit from the byte, MSB first for (let i = 0; i < 8; i++) { // Right shift to position + mask to get bit value // i=0: shift 7 (10100101 -> 00000001) // i=1: shift 6 (10100101 -> 00000000) // i=2: shift 5 (10100101 -> 00000001) // etc. bits.push((byte >> (7 - i)) & 1); } } return bits; } /** * Converts an array of bits back into a Buffer of bytes. * This is the inverse operation of bytesToBits. * Processes bits in groups of 8, maintaining MSB-first order. * * @param bits - Array of 0s and 1s to convert to bytes * @returns Buffer containing the reconstructed bytes * @throws Error if the number of bits is not divisible by 8 * * @example * ```typescript * const bits = [1,0,1,0,0,1,0,1] // Represents binary 10100101 * const bytes = bitsToBytes(bits) * // bytes = Buffer.from([0xA5]) * ``` * * Process for each byte: * 1. Take 8 bits at a time * 2. For each bit: * - Shift it left to its correct position (7-j positions) * - OR it with the accumulating byte value * 3. Add completed byte to array */ bitsToBytes(bits) { // Ensure we have complete bytes (groups of 8 bits) if (bits.length % 8 !== 0) { throw new Error('Number of bits must be a multiple of 8.'); } const bytes = []; // Process bits in groups of 8 for (let i = 0; i < bits.length; i += 8) { let byte = 0; // Build each byte bit by bit for (let j = 0; j < 8; j++) { // Left shift each bit to its position and OR with current byte // j=0: bit goes to position 7 (MSB) // j=1: bit goes to position 6 // j=2: bit goes to position 5 // etc. byte |= bits[i + j] << (7 - j); } bytes.push(byte); } return buffer_1.Buffer.from(bytes); } /** * Applies the hash function to the given data with special handling for null values. * Used primarily for Merkle tree calculations and node hashing. * * Special cases: * - null input -> returns 32-byte zero buffer * - 64-byte zero buffer -> returns 32-byte zero buffer * This handling ensures consistent treatment of empty/uninitialized nodes. * * @param data - Buffer to hash, must be either 32 or 64 bytes, or null * @returns A 32-byte hash of the data, or zero32 for empty cases * @throws Error if data length is not 32 or 64 bytes * * @example * ```typescript * // Regular hashing * const hash1 = hashData(nodeBuffer) // Returns hash of data * * // Empty cases - all return 32 zeros * const hash2 = hashData(null) * const hash3 = hashData(Buffer.alloc(64, 0)) * ``` */ hashData(data) { // Pre-allocate zero buffers for comparison and return values const zero64 = buffer_1.Buffer.alloc(64, 0); // Used to detect empty 64-byte input const zero32 = buffer_1.Buffer.alloc(32, 0); // Returned for empty/zero cases // Return zero32 for either null input or a 64-byte zero buffer // This treats empty nodes consistently in the tree if (data === null || data.equals(zero64)) { return zero32; } // Validate input size - must be either a single node (32 bytes) // or a pair of nodes being combined (64 bytes) if (data.length !== 32 && data.length !== 64) { throw new Error('Data must be 32 or 64 bytes.'); } // Apply the configured hash function to valid data return this.hashFn(data); } /** * Computes the Merkle root of the entire tree. * The Merkle root is a single 32-byte hash that uniquely represents the entire tree state. * * The computation follows these rules: * 1. For Internal nodes: hash(leftChild || rightChild) * 2. For Stem nodes: hash(stem || 0x00 || merkleOfValues) * 3. For empty nodes: return 32 bytes of zeros * * @returns A 32-byte Buffer containing the Merkle root * * @example * ```typescript * const tree = new BinaryTree(hashFn) * tree.insert(key1, value1) * tree.insert(key2, value2) * const root = tree.merkelize() * // root now contains a 32-byte hash representing the entire tree * ``` */ merkelize() { /** * Recursive helper function to compute the Merkle root of a subtree * @param node - Root of the subtree to compute hash for * @returns 32-byte Buffer containing the node's Merkle hash */ const computeMerkle = (node) => { const zero32 = buffer_1.Buffer.alloc(32, 0); // Base case: empty node returns zero hash if (node === null) { return zero32; } // Case 1: Internal node if (node instanceof InternalNode) { // Recursively compute hashes of left and right children const leftHash = computeMerkle(node.left); const rightHash = computeMerkle(node.right); // Combine and hash the children return this.hashData(buffer_1.Buffer.concat([leftHash, rightHash])); } // Case 2: Stem node (leaf) // First compute Merkle tree of the 256 values in this node const level = node.values.map(val => this.hashData(val)); // Build a balanced binary tree from the value hashes // Each iteration combines pairs of hashes until only root remains while (level.length > 1) { const newLevel = []; for (let i = 0; i < level.length; i += 2) { // Combine each pair of hashes newLevel.push(this.hashData(buffer_1.Buffer.concat([level[i], level[i + 1]]))); } // Replace old level with new level level.splice(0, level.length, ...newLevel); } // Final stem node hash combines: // 1. The stem (31 bytes) // 2. A zero byte (1 byte) // 3. The Merkle root of values (32 bytes) return this.hashData(buffer_1.Buffer.concat([ node.stem, buffer_1.Buffer.from([0]), level[0] // 32-byte value root ])); }; // Start computation from root return computeMerkle(this.root); } // ------------------------------------------------------------- // New Features // ------------------------------------------------------------- /** * Incrementally updates the value for an existing key. * For our implementation, update is the same as insert. * * @param key - A 32-byte key. * @param value - A 32-byte value. */ update(key, value) { // Simply re-insert; our insert() method will update an existing key. this.insert(key, value); } /** * Performs a batch insertion of key-value pairs. * * @param entries - An array of objects with 'key' and 'value' properties. */ insertBatch(entries) { for (const { key, value } of entries) { this.insert(key, value); } } /** * Serializes the entire tree structure into a JSON Buffer. * Converts the tree into a format that can be stored or transmitted, * preserving the complete structure and all values. * * The serialized format for each node type is: * 1. Stem Node: * ```json * { * "nodeType": "stem", * "stem": "hex string of 31 bytes", * "values": ["hex string or null", ...] // 256 entries * } * ``` * 2. Internal Node: * ```json * { * "nodeType": "internal", * "left": <node or null>, * "right": <node or null> * } * ``` * * @returns Buffer containing the JSON string representation of the tree * * @example * ```typescript * const tree = new BinaryTree(hashFn) * tree.insert(key, value) * const serialized = tree.serialize() * // Save to file or transmit * const newTree = UnifiedBinaryTree.deserialize(serialized, hashFn) * ``` */ serialize() { /** * Helper function to recursively serialize each node in the tree * Converts Buffer data to hex strings for JSON compatibility * * @param node - The node to serialize * @returns JSON-compatible object representation of the node */ function serializeNode(node) { // Handle empty nodes if (!node) return null; // Case 1: Stem (leaf) node if (node instanceof StemNode) { return { nodeType: 'stem', stem: node.stem.toString('hex'), values: node.values.map(val => // Convert 256 values to hex (val ? val.toString('hex') : null)) // Preserve null values }; } else { // Case 2: Internal node return { nodeType: 'internal', left: serializeNode(node.left), right: serializeNode(node.right) // Recursively serialize right subtree }; } } // Wrap the serialized tree in a root object and convert to Buffer const obj = { root: serializeNode(this.root) }; return buffer_1.Buffer.from(JSON.stringify(obj), 'utf8'); } /** * Reconstructs a BinaryTree from its serialized form. * This is the inverse operation of serialize(). * * Expected input format: * ```json * { * "root": { * "nodeType": "internal"|"stem", * // For stem nodes: * "stem": "hex string", * "values": ["hex string"|null, ...], * // For internal nodes: * "left": <node|null>, * "right": <node|null> * } * } * ``` * * @param data - Buffer containing the JSON serialized tree * @param hashFn - Hash function to use for the reconstructed tree * @returns A new BinaryTree instance with the deserialized structure * @throws Error if JSON parsing fails or format is invalid * * @example * ```typescript * const serialized = existingTree.serialize() * const newTree = UnifiedBinaryTree.deserialize(serialized, hashFn) * // newTree is now identical to existingTree * ``` */ static deserialize(data, hashFn) { // Parse the JSON string from the buffer const json = JSON.parse(data.toString('utf8')); /** * Helper function to recursively deserialize nodes * Converts hex strings back to Buffers and reconstructs the tree structure * * @param obj - JSON object representing a node * @returns Reconstructed BinaryTreeNode or null */ function deserializeNode(obj) { // Handle null nodes if (obj === null) return null; // Case 1: Reconstruct stem (leaf) node if (obj.nodeType === 'stem') { // Convert hex stem back to Buffer const node = new StemNode(buffer_1.Buffer.from(obj.stem, 'hex')); // Convert hex values back to Buffers, preserving nulls node.values = obj.values.map((v) => (v !== null ? buffer_1.Buffer.from(v, 'hex') : null)); return node; } else if (obj.nodeType === 'internal') { // Case 2: Reconstruct internal node const node = new InternalNode(); // Recursively deserialize left and right subtrees node.left = deserializeNode(obj.left); node.right = deserializeNode(obj.right); return node; } // Invalid node type return null; } // Create new tree with provided hash function const tree = new UnifiedBinaryTree(hashFn); // Deserialize and set the root node tree.root = deserializeNode(json.root); return tree; } /** * Splits a leaf node when inserting a new key with a different stem. * This method handles two cases: * 1. Matching bits at current depth: Continue splitting recursively * 2. Different bits at current depth: Create new internal node and arrange leaves * * The process ensures that keys with different stems are properly distributed * in the tree based on their binary representation. * * @param leaf - The existing leaf node to split * @param stemBits - Binary representation of the new stem * @param existingStemBits - Binary representation of the existing stem * @param subIndex - Position within leaf node for new value * @param value - Value to store at the new position * @param depth - Current depth in the tree * @returns A new internal node containing both the existing and new data * * Example: * If stems differ at bit 3: * - New stem: [1,0,1,0,...] * - Existing stem: [1,0,1,1,...] * ^ split here * Creates an internal node with the leaf nodes arranged based on bit 3 */ splitLeaf(leaf, stemBits, existingStemBits, subIndex, value, depth) { // Case 1: Bits match at current depth, need to go deeper if (stemBits[depth] === existingStemBits[depth]) { const newInternal = new InternalNode(); const bit = stemBits[depth]; // Continue splitting recursively in the matching direction if (bit === 0) { newInternal.left = this.splitLeaf(leaf, stemBits, existingStemBits, subIndex, value, depth + 1); } else { newInternal.right = this.splitLeaf(leaf, stemBits, existingStemBits, subIndex, value, depth + 1); } return newInternal; } else { // Case 2: Bits differ at current depth, create split point const newInternal = new InternalNode(); const bit = stemBits[depth]; // Create new leaf node for the new stem const newStem = this.bitsToBytes(stemBits); const newNode = new StemNode(newStem); newNode.setValue(subIndex, value); // Arrange nodes based on their bits at current depth // bit = 0: new node goes left, existing goes right // bit = 1: new node goes right, existing goes left if (bit === 0) { newInternal.left = newNode; newInternal.right = leaf; } else { newInternal.right = newNode; newInternal.left = leaf; } return newInternal; } } } exports.UnifiedBinaryTree = UnifiedBinaryTree;