merkle-tree-lib
Version:
Merkle Tree implementation with BIP340 tagged hash support.
139 lines (127 loc) • 5.36 kB
text/typescript
import * as crypto from 'crypto';
/**
* MerkleTree class: builds a binary Merkle tree using tagged hashing (BIP-0340).
* It can compute the Merkle root and generate proofs for given leaves.
*/
export class MerkleTree {
private leafHashes: Buffer[];
private levels: Buffer[][]; // levels[0] = leaf level (after any padding), levels[H] = [root]
public root: Buffer = Buffer.alloc(0); // Initialize to avoid TS error
private tagHashLeaf: Buffer;
private tagHashBranch: Buffer;
/**
* Initialize the Merkle tree with an array of data (strings) and hashing tags.
* @param leavesData - Array of strings representing leaf data.
* @param leafTag - Tag for hashing leaves (context-specific tag). Defaults to "Bitcoin_Transaction".
* @param branchTag - Tag for hashing internal nodes. Defaults to the same as leafTag.
*/
constructor(leavesData: string[], leafTag: string = "Bitcoin_Transaction", branchTag: string = "Bitcoin_Transaction") {
this.tagHashLeaf = this.computeTagHash(leafTag);
this.tagHashBranch = this.computeTagHash(branchTag);
// Compute all leaf hashes
this.leafHashes = leavesData.map(data => {
const msg = Buffer.from(data, 'utf8');
return this.taggedHash(this.tagHashLeaf, msg);
});
// Build Merkle tree levels
this.levels = [];
this.buildTree();
}
/** Compute SHA256 hash of the tag (as UTF-8), used for tagged hashing. */
private computeTagHash(tag: string): Buffer {
return crypto.createHash('sha256').update(tag, 'utf8').digest();
}
/**
* Compute a tagged hash: SHA256( tagHash || tagHash || message ).
* @param tagHash - Buffer of 32-byte hash of the tag.
* @param msg - Message bytes to hash.
* @returns 32-byte SHA256 digest.
*/
private taggedHash(tagHash: Buffer, msg: Buffer): Buffer {
// Use two tagHash copies as prefix, then the message
return crypto.createHash('sha256')
.update(tagHash)
.update(tagHash)
.update(msg)
.digest();
}
/** Build the Merkle tree levels and root from the leaf hashes. */
private buildTree(): void {
let levelNodes = this.leafHashes.slice(); // copy leaf hashes
const n = levelNodes.length;
if (n === 0) {
// No leaves: define root as empty buffer
this.root = Buffer.alloc(0);
this.levels = [];
return;
}
// If there's only one leaf, that leaf's hash will be the root (no pairing needed).
// Otherwise, build the tree by hashing pairs of nodes up to the root.
let levelIndex = 0;
while (levelNodes.length > 1) {
// If odd number of nodes, duplicate the last node to make it even
if (levelNodes.length % 2 === 1) {
levelNodes.push(levelNodes[levelNodes.length - 1]);
}
// Store the current level (after padding)
this.levels[levelIndex] = levelNodes;
// Compute parent level
const nextLevel: Buffer[] = [];
for (let i = 0; i < levelNodes.length; i += 2) {
const left = levelNodes[i];
const right = levelNodes[i + 1];
// Branch hash using the branch tag
const parentHash = this.taggedHash(this.tagHashBranch, Buffer.concat([left, right]));
nextLevel.push(parentHash);
}
levelNodes = nextLevel;
levelIndex++;
}
// The loop ends when levelNodes has a single element (root)
this.levels[levelIndex] = levelNodes;
this.root = levelNodes[0];
}
/**
* Get the Merkle root as a hex string.
* @returns Hexadecimal string of the Merkle root.
*/
public getHexRoot(): string {
return this.root.toString('hex');
}
/**
* Generate the Merkle proof for the leaf at the given index.
* @param index - Index of the target leaf in the original input array.
* @returns An array of proof elements, each a tuple [siblingHash (Buffer), side (0 or 1)].
* side = 0 indicates the sibling is the left node, side = 1 indicates the sibling is the right node.
*/
public getProof(index: number): { siblingHash: Buffer, side: 0 | 1 }[] {
const proof: { siblingHash: Buffer, side: 0 | 1 }[] = [];
if (index < 0 || index >= this.leafHashes.length) {
return proof; // index out of range, return empty proof
}
// Traverse from leaf level up to root (levels[0] to levels[h-1], where levels[h] is root)
let currentIndex = index;
for (let level = 0; level < this.levels.length - 1; level++) {
const levelNodes = this.levels[level];
const isEvenIndex = (currentIndex % 2 === 0);
let siblingIndex: number;
let side: 0 | 1;
if (isEvenIndex) {
// Current node is a left child, sibling is to the right
siblingIndex = currentIndex + 1;
// In case of a duplicated last node, siblingIndex may equal currentIndex
// (but our building algorithm pads such that sibling always exists)
side = 1; // sibling is on the right side
} else {
// Current node is a right child, sibling is to the left
siblingIndex = currentIndex - 1;
side = 0; // sibling is on the left side
}
const siblingHash = levelNodes[siblingIndex];
proof.push({ siblingHash, side });
// Move to the parent index for next level
currentIndex = Math.floor(currentIndex / 2);
}
return proof;
}
}