UNPKG

merkletreejs

Version:
455 lines (454 loc) 16.5 kB
/// <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; }