j-bitcoin
Version:
Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features
198 lines (164 loc) • 5.15 kB
JavaScript
/**
* @fileoverview Taproot merkle tree implementation for script path spending
* @version 1.0.0
* @author yfbsei
* @license ISC
*/
import { createHash } from 'node:crypto';
class MerkleTreeError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'MerkleTreeError';
this.code = code;
this.details = details;
}
}
const MERKLE_CONSTANTS = {
HASH_SIZE: 32,
MAX_TREE_DEPTH: 128,
MAX_SCRIPT_SIZE: 10000,
MAX_LEAVES: 2 ** 16,
TAPLEAF_TAG: 'TapLeaf',
TAPBRANCH_TAG: 'TapBranch',
TAPTWEAK_TAG: 'TapTweak',
DEFAULT_LEAF_VERSION: 0xc0
};
class TaggedHash {
static create(tag, data) {
const tagHash = createHash('sha256').update(tag).digest();
const taggedData = Buffer.concat([tagHash, tagHash, data]);
return createHash('sha256').update(taggedData).digest();
}
static createTapLeaf(leafVersion, script) {
const data = Buffer.concat([
Buffer.from([leafVersion]),
Buffer.from([script.length]),
script
]);
return this.create(MERKLE_CONSTANTS.TAPLEAF_TAG, data);
}
static createTapBranch(left, right) {
const ordered = Buffer.compare(left, right) < 0
? Buffer.concat([left, right])
: Buffer.concat([right, left]);
return this.create(MERKLE_CONSTANTS.TAPBRANCH_TAG, ordered);
}
static createTapTweak(internalKey, merkleRoot = null) {
const data = merkleRoot
? Buffer.concat([internalKey, merkleRoot])
: internalKey;
return this.create(MERKLE_CONSTANTS.TAPTWEAK_TAG, data);
}
}
class TaprootMerkleTree {
constructor(scripts, leafVersion = MERKLE_CONSTANTS.DEFAULT_LEAF_VERSION) {
if (!Array.isArray(scripts) || scripts.length === 0) {
throw new MerkleTreeError('Scripts array is required', 'INVALID_SCRIPTS');
}
if (scripts.length > MERKLE_CONSTANTS.MAX_LEAVES) {
throw new MerkleTreeError('Too many scripts', 'TOO_MANY_LEAVES');
}
this.leafVersion = leafVersion;
this.scripts = scripts;
this.leaves = [];
this.tree = [];
this._buildTree();
}
_buildTree() {
this.leaves = this.scripts.map((script, index) => ({
script: Buffer.isBuffer(script) ? script : Buffer.from(script),
leafVersion: this.leafVersion,
hash: TaggedHash.createTapLeaf(
this.leafVersion,
Buffer.isBuffer(script) ? script : Buffer.from(script)
),
index
}));
if (this.leaves.length === 1) {
this.tree = [[this.leaves[0].hash]];
this.root = this.leaves[0].hash;
return;
}
let currentLevel = this.leaves.map(leaf => leaf.hash);
this.tree = [currentLevel];
while (currentLevel.length > 1) {
const nextLevel = [];
for (let i = 0; i < currentLevel.length; i += 2) {
if (i + 1 < currentLevel.length) {
const branch = TaggedHash.createTapBranch(currentLevel[i], currentLevel[i + 1]);
nextLevel.push(branch);
} else {
nextLevel.push(currentLevel[i]);
}
}
this.tree.push(nextLevel);
currentLevel = nextLevel;
}
this.root = currentLevel[0];
}
getRoot() {
return this.root;
}
getMerklePath(leafIndex) {
if (leafIndex < 0 || leafIndex >= this.leaves.length) {
throw new MerkleTreeError('Invalid leaf index', 'INVALID_INDEX');
}
const path = [];
let index = leafIndex;
for (let level = 0; level < this.tree.length - 1; level++) {
const currentLevel = this.tree[level];
const siblingIndex = index % 2 === 0 ? index + 1 : index - 1;
if (siblingIndex < currentLevel.length) {
path.push({
hash: currentLevel[siblingIndex],
position: index % 2 === 0 ? 'right' : 'left'
});
}
index = Math.floor(index / 2);
}
return {
leafIndex,
leafHash: this.leaves[leafIndex].hash,
hashes: path.map(p => p.hash),
positions: path.map(p => p.position)
};
}
verifyInclusion(leafIndex, leafHash) {
const path = this.getMerklePath(leafIndex);
let computedHash = leafHash;
for (let i = 0; i < path.hashes.length; i++) {
computedHash = TaggedHash.createTapBranch(computedHash, path.hashes[i]);
}
return computedHash.equals(this.root);
}
getLeaf(index) {
if (index < 0 || index >= this.leaves.length) {
throw new MerkleTreeError('Invalid leaf index', 'INVALID_INDEX');
}
return { ...this.leaves[index] };
}
getLeafCount() {
return this.leaves.length;
}
getTreeDepth() {
return this.tree.length;
}
toJSON() {
return {
root: this.root.toString('hex'),
leafVersion: this.leafVersion,
leafCount: this.leaves.length,
depth: this.tree.length,
leaves: this.leaves.map(leaf => ({
hash: leaf.hash.toString('hex'),
index: leaf.index
}))
};
}
}
export {
TaprootMerkleTree,
TaggedHash,
MerkleTreeError,
MERKLE_CONSTANTS
};