UNPKG

merkle-tree-lib

Version:

Merkle Tree implementation with BIP340 tagged hash support.

272 lines (232 loc) 8.49 kB
import { HashStrategy } from '../hash/HashStrategy'; import { MerkleProof, MerkleProofElement, ProofDirection } from '../proof/MerkleProof'; import { TaggedSha256Strategy } from '../hash/strategies/TaggedSha256Strategy'; /** * Default tag constants for hashing */ export const DEFAULT_TAGS = { LEAF: 'Bitcoin_Transaction', BRANCH: 'Bitcoin_Transaction' }; /** * MerkleTree - Implementation of a Merkle tree data structure * * A Merkle tree is a binary tree of hashes where leaf nodes contain * hashes of data blocks, and non-leaf nodes contain hashes of their children. */ export class MerkleTree { /** Original data leaves */ private readonly leaves: string[]; /** Hash of the leaves */ private readonly leafHashes: Buffer[]; /** Tree structure - array of arrays, each inner array is a level in the tree */ private readonly levels: Buffer[][]; /** Strategy for hashing leaf nodes */ private readonly leafHashStrategy: HashStrategy; /** Strategy for hashing branch nodes */ private readonly branchHashStrategy: HashStrategy; /** * Create a new Merkle tree * * @param data - Array of data elements to include in the tree * @param leafHashStrategy - Strategy to use for hashing leaves (default: TaggedSha256 with 'MERKLE_LEAF' tag) * @param branchHashStrategy - Strategy to use for hashing branches (default: TaggedSha256 with 'MERKLE_BRANCH' tag) */ constructor( data: string[], leafHashStrategy: HashStrategy = new TaggedSha256Strategy(DEFAULT_TAGS.LEAF), branchHashStrategy: HashStrategy = new TaggedSha256Strategy(DEFAULT_TAGS.BRANCH) ) { if (!data || data.length === 0) { throw new Error('Cannot create a Merkle tree with no data'); } this.leaves = [...data]; // Store original data this.leafHashStrategy = leafHashStrategy; this.branchHashStrategy = branchHashStrategy; // Hash the leaves this.leafHashes = data.map(item => this.leafHashStrategy.hash(item)); // Build the tree this.levels = this.buildTree(this.leafHashes); } /** * Build the Merkle tree from leaf hashes * * @param leafHashes - Array of leaf node hashes * @returns 2D array representing tree levels */ private buildTree(leafHashes: Buffer[]): Buffer[][] { const levels: Buffer[][] = []; // Add leaf hashes as the first level levels.push([...leafHashes]); // Build subsequent levels until we reach the root while (levels[levels.length - 1].length > 1) { const currentLevel = levels[levels.length - 1]; const nextLevel: Buffer[] = []; // Process pairs in the current level to build the next level up for (let i = 0; i < currentLevel.length; i += 2) { // If this is an odd end node with no pair, propagate it up if (i + 1 === currentLevel.length) { nextLevel.push(currentLevel[i]); continue; } // Combine the pair of nodes and hash them const combined = Buffer.concat([currentLevel[i], currentLevel[i + 1]]); const parentHash = this.branchHashStrategy.hash(combined); nextLevel.push(parentHash); } // Add the new level to our levels array levels.push(nextLevel); } return levels; } /** * Get the Merkle root hash * * @returns The root hash as a Buffer */ public getRoot(): Buffer { // Root is the only element in the last level return this.levels[this.levels.length - 1][0]; } /** * Get the Merkle root hash as a hex string * * @returns The root hash as a hex string */ public getRootHex(): string { return this.getRoot().toString('hex'); } /** * Generate a Merkle proof for a specific leaf * * @param index - Index of the leaf in the original data array * @returns A MerkleProof object * @throws Error if the index is out of bounds */ public generateProof(index: number): MerkleProof { if (index < 0 || index >= this.leaves.length) { throw new Error(`Leaf index out of range: ${index}`); } const elements: MerkleProofElement[] = []; let currentIndex = index; // Collect sibling hashes for each level up to the root for (let level = 0; level < this.levels.length - 1; level++) { const levelNodes = this.levels[level]; // Calculate sibling index (if even, sibling is right; if odd, sibling is left) const isRightNode = currentIndex % 2 === 1; const siblingIndex = isRightNode ? currentIndex - 1 : currentIndex + 1; // Only add sibling if it exists at this level if (siblingIndex < levelNodes.length) { elements.push({ siblingHash: levelNodes[siblingIndex], direction: isRightNode ? ProofDirection.LEFT : ProofDirection.RIGHT }); } // Move up to the parent node index for the next level currentIndex = Math.floor(currentIndex / 2); } // Create the Merkle proof with the original data and collected elements return new MerkleProof( this.leaves[index], index, elements, this.getRoot() ); } /** * Get the number of leaves in the tree * * @returns The count of leaf nodes */ public getLeafCount(): number { return this.leaves.length; } /** * Get the original data leaf at the specified index * * @param index - Index in the original data array * @returns The original data string * @throws Error if the index is out of bounds */ public getLeaf(index: number): string { if (index < 0 || index >= this.leaves.length) { throw new Error(`Leaf index out of range: ${index}`); } return this.leaves[index]; } /** * Get the hash of the leaf at the specified index * * @param index - Index in the original data array * @returns The leaf hash as a Buffer * @throws Error if the index is out of bounds */ public getLeafHash(index: number): Buffer { if (index < 0 || index >= this.leafHashes.length) { throw new Error(`Leaf index out of range: ${index}`); } return this.leafHashes[index]; } /** * Check if the tree contains a specific leaf value * * @param data - The leaf data to check * @returns The index of the leaf if found, -1 otherwise */ public findLeaf(data: string): number { return this.leaves.findIndex(leaf => leaf === data); } /** * Export the tree structure for visualization or debugging * * @returns 2D array of hex strings representing the tree */ public exportTree(): string[][] { return this.levels.map(level => level.map(hash => hash.toString('hex')) ); } /** * Update a leaf and recompute affected tree nodes * * @param index - Index of the leaf to update * @param newData - New data for the leaf * @returns The new root hash * @throws Error if the index is out of bounds */ public updateLeaf(index: number, newData: string): Buffer { if (index < 0 || index >= this.leaves.length) { throw new Error(`Leaf index out of range: ${index}`); } // Update the leaf data this.leaves[index] = newData; // Rehash the leaf this.leafHashes[index] = this.leafHashStrategy.hash(newData); this.levels[0][index] = this.leafHashes[index]; // Recompute affected nodes let currentIndex = index; for (let level = 0; level < this.levels.length - 1; level++) { // Move up to the parent index const parentLevel = level + 1; const parentIndex = Math.floor(currentIndex / 2); // Get the pair of children (current node and its sibling) const levelNodes = this.levels[level]; const isRightChild = currentIndex % 2 === 1; const siblingIndex = isRightChild ? currentIndex - 1 : currentIndex + 1; // If there's no sibling (odd end node), just propagate up if (siblingIndex >= levelNodes.length) { this.levels[parentLevel][parentIndex] = levelNodes[currentIndex]; } else { // Otherwise compute the new parent hash from the pair const first = levelNodes[isRightChild ? siblingIndex : currentIndex]; const second = levelNodes[isRightChild ? currentIndex : siblingIndex]; const combined = Buffer.concat([first, second]); this.levels[parentLevel][parentIndex] = this.branchHashStrategy.hash(combined); } // Update the current index for the next level currentIndex = parentIndex; } // Return the new root return this.getRoot(); } }