merkletreejs
Version:
Construct Merkle Trees and verify proofs
455 lines (454 loc) • 16.5 kB
TypeScript
/// <reference types="node" />
/**
* Type aliases for fixed-length byte sequences used throughout the codebase.
* These help maintain type safety and clarify the expected byte lengths.
*/
export declare type Address = Buffer;
export declare type Address32 = Buffer;
export declare type Bytes32 = Buffer;
/**
* Constants used for key derivation and tree organization.
* These define the structure and layout of the binary tree.
*/
export declare const BASIC_DATA_LEAF_KEY = 0;
export declare const CODE_HASH_LEAF_KEY = 1;
export declare const HEADER_STORAGE_OFFSET = 64;
export declare const CODE_OFFSET = 128;
export declare const STEM_SUBTREE_WIDTH = 256;
export declare const MAIN_STORAGE_OFFSET = 256;
export declare const pushOffset = 95;
export declare const push1: number;
export declare const push32: number;
/** Function type for hash operations */
export declare type HashFunction = (data: any) => any;
/**
* 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)
* ```
*/
export declare function oldStyleAddressToAddress32(address: Address): Address32;
/**
* 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'
* ```
*/
export declare function treeHash(input: Buffer, hashFn: HashFunction): Bytes32;
/**
* 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
* ```
*/
export declare function getTreeKey(address: Address32, treeIndex: number, subIndex: number, hashFn: HashFunction): Address32;
/**
* 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)
* ```
*/
export declare function getTreeKeyForBasicData(address: Address32, hashFn: HashFunction): Address32;
/**
* 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)
* ```
*/
export declare function getTreeKeyForCodeHash(address: Address32, hashFn: HashFunction): Address32;
/**
* 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)
* ```
*/
export declare function getTreeKeyForStorageSlot(address: Address32, storageKey: number, hashFn: HashFunction): Address32;
/**
* 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)
* })
* ```
*/
export declare function getTreeKeyForCodeChunk(address: Address32, chunkId: number, hashFn: HashFunction): Address32;
/**
* 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)
* ```
*/
export declare function chunkifyCode(code: Buffer): Bytes32[];
/**
* Node types in the binary tree.
* - StemNode: Leaf node containing up to 256 values
* - InternalNode: Internal node with left and right children
*/
export declare type BinaryTreeNode = StemNode | InternalNode;
/**
* 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
* ```
*/
export declare class StemNode {
stem: Buffer;
values: Array<Buffer | null>;
nodeType: 'stem';
/**
* Creates a new StemNode with the given stem.
*
* @param stem - The 31-byte stem for this node.
*/
constructor(stem: Buffer);
/**
* 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: number, value: Buffer): void;
}
/**
* 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))
* ```
*/
export declare class InternalNode {
left: BinaryTreeNode | null;
right: BinaryTreeNode | null;
nodeType: 'internal';
}
/**
* 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()
* ```
*/
export declare class UnifiedBinaryTree {
root: BinaryTreeNode | null;
hashFn: HashFunction;
/**
* Creates a new BinaryTree instance with the given hash function.
*
* @param hashFn - The hash function to use for key derivation.
*/
constructor(hashFn: HashFunction);
/**
* 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: Buffer, value: Buffer): void;
/**
* 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
*/
private insertRecursive;
/**
* 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
*/
private bytesToBits;
/**
* 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
*/
private bitsToBytes;
/**
* 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))
* ```
*/
private hashData;
/**
* 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(): Buffer;
/**
* 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: Buffer, value: Buffer): void;
/**
* Performs a batch insertion of key-value pairs.
*
* @param entries - An array of objects with 'key' and 'value' properties.
*/
insertBatch(entries: {
key: Buffer;
value: Buffer;
}[]): void;
/**
* 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(): Buffer;
/**
* 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: Buffer, hashFn: HashFunction): UnifiedBinaryTree;
/**
* 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
*/
private splitLeaf;
}