UNPKG

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

190 lines (153 loc) 5.79 kB
/** * @fileoverview Taproot control block implementation following BIP341 * @version 1.0.0 * @author yfbsei * @license ISC */ import { secp256k1 } from '@noble/curves/secp256k1'; import { TaprootMerkleTree, TaggedHash, MERKLE_CONSTANTS } from './merkle-tree.js'; import { CRYPTO_CONSTANTS } from '../constants.js'; class ControlBlockError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'ControlBlockError'; this.code = code; this.details = details; } } const CONTROL_BLOCK_CONSTANTS = { BASE_SIZE: 33, HASH_SIZE: 32, MIN_SIZE: 33, MAX_SIZE: 33 + (32 * 128), LEAF_VERSION_MASK: 0xfe, PARITY_MASK: 0x01, DEFAULT_LEAF_VERSION: 0xc0, MAX_MERKLE_DEPTH: 128 }; class TaprootControlBlock { constructor() { this.validationCache = new Map(); } parseControlBlock(controlBlock) { if (!Buffer.isBuffer(controlBlock)) { throw new ControlBlockError('Control block must be a Buffer', 'INVALID_TYPE'); } if (controlBlock.length < CONTROL_BLOCK_CONSTANTS.MIN_SIZE) { throw new ControlBlockError('Control block too small', 'TOO_SMALL'); } if (controlBlock.length > CONTROL_BLOCK_CONSTANTS.MAX_SIZE) { throw new ControlBlockError('Control block too large', 'TOO_LARGE'); } const remainingSize = controlBlock.length - CONTROL_BLOCK_CONSTANTS.BASE_SIZE; if (remainingSize % CONTROL_BLOCK_CONSTANTS.HASH_SIZE !== 0) { throw new ControlBlockError('Invalid control block size', 'INVALID_SIZE'); } const leafVersionAndParity = controlBlock[0]; const leafVersion = leafVersionAndParity & CONTROL_BLOCK_CONSTANTS.LEAF_VERSION_MASK; const parity = leafVersionAndParity & CONTROL_BLOCK_CONSTANTS.PARITY_MASK; const internalKey = controlBlock.slice(1, 33); const merklePathData = controlBlock.slice(33); const merkleDepth = merklePathData.length / CONTROL_BLOCK_CONSTANTS.HASH_SIZE; const merklePath = []; for (let i = 0; i < merkleDepth; i++) { const start = i * CONTROL_BLOCK_CONSTANTS.HASH_SIZE; const end = start + CONTROL_BLOCK_CONSTANTS.HASH_SIZE; merklePath.push(merklePathData.slice(start, end)); } return { leafVersion, parity, internalKey, merklePath, merkleDepth, isValid: true }; } verifyScriptInclusion(script, controlBlock, expectedOutputKey) { const parsed = this.parseControlBlock(controlBlock); const { leafVersion, parity, internalKey, merklePath } = parsed; const tapLeafHash = TaggedHash.createTapLeaf(leafVersion, script); let computedRoot = tapLeafHash; for (const siblingHash of merklePath) { computedRoot = TaggedHash.createTapBranch(computedRoot, siblingHash); } if (merklePath.length === 0) { computedRoot = tapLeafHash; } const tapTweak = this.computeTapTweak(internalKey, computedRoot); const expectedKey = this.tweakInternalKey(internalKey, tapTweak, parity); const isValid = expectedKey.equals(expectedOutputKey); return { isValid, computedRoot, expectedOutputKey: expectedKey, metrics: { merkleDepth: merklePath.length, leafHash: tapLeafHash, tapTweak } }; } generateControlBlock(merkleTree, leafIndex, internalKey, outputKeyParity) { if (!(merkleTree instanceof TaprootMerkleTree)) { throw new ControlBlockError('Invalid merkle tree', 'INVALID_TREE'); } if (!Buffer.isBuffer(internalKey) || internalKey.length !== 32) { throw new ControlBlockError('Internal key must be 32 bytes', 'INVALID_KEY'); } const leaf = merkleTree.getLeaf(leafIndex); const merklePath = merkleTree.getMerklePath(leafIndex); const firstByte = (leaf.leafVersion & CONTROL_BLOCK_CONSTANTS.LEAF_VERSION_MASK) | (outputKeyParity & CONTROL_BLOCK_CONSTANTS.PARITY_MASK); const parts = [ Buffer.from([firstByte]), internalKey, ...merklePath.hashes ]; return Buffer.concat(parts); } computeTapTweak(internalKey, merkleRoot = null) { return TaggedHash.createTapTweak(internalKey, merkleRoot); } tweakInternalKey(internalKey, tapTweak, expectedParity = null) { const internalKeyBigInt = BigInt('0x' + internalKey.toString('hex')); const tweakBigInt = BigInt('0x' + tapTweak.toString('hex')); const point = secp256k1.ProjectivePoint.fromHex( Buffer.concat([Buffer.from([0x02]), internalKey]) ); const tweakPoint = secp256k1.ProjectivePoint.BASE.multiply(tweakBigInt); const outputPoint = point.add(tweakPoint); const outputKey = Buffer.from(outputPoint.toRawBytes(true)).slice(1); if (expectedParity !== null) { const actualParity = outputPoint.toRawBytes(true)[0] === 0x03 ? 1 : 0; if (actualParity !== expectedParity) { const negatedPoint = outputPoint.negate(); return Buffer.from(negatedPoint.toRawBytes(true)).slice(1); } } return outputKey; } validateAgainstTree(controlBlock, merkleTree, leafIndex) { try { const parsed = this.parseControlBlock(controlBlock); const expectedPath = merkleTree.getMerklePath(leafIndex); if (parsed.merklePath.length !== expectedPath.hashes.length) { return false; } for (let i = 0; i < parsed.merklePath.length; i++) { if (!parsed.merklePath[i].equals(expectedPath.hashes[i])) { return false; } } return true; } catch { return false; } } } export { TaprootControlBlock, ControlBlockError, CONTROL_BLOCK_CONSTANTS };