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

370 lines (317 loc) 12.1 kB
/** * @fileoverview Witness Builder for SegWit and Taproot transactions * @description Build witness stacks for P2WPKH, P2WSH, P2TR key-path and script-path * @version 1.0.0 * @author yfbsei * @license ISC */ import { createHash } from 'node:crypto'; /** * Custom error class for witness operations * @class WitnessError * @extends Error */ class WitnessError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'WitnessError'; this.code = code; this.details = details; } } /** * Tagged hash for Taproot * @param {string} tag - Hash tag * @param {Buffer} data - Data to hash * @returns {Buffer} 32-byte hash */ function taggedHash(tag, data) { const tagHash = createHash('sha256').update(tag).digest(); return createHash('sha256').update(Buffer.concat([tagHash, tagHash, data])).digest(); } /** * Encode a variable-length integer * @param {number} n - Number to encode * @returns {Buffer} VarInt bytes */ function encodeVarInt(n) { if (n < 0xfd) { return Buffer.from([n]); } else if (n <= 0xffff) { const buf = Buffer.alloc(3); buf[0] = 0xfd; buf.writeUInt16LE(n, 1); return buf; } else if (n <= 0xffffffff) { const buf = Buffer.alloc(5); buf[0] = 0xfe; buf.writeUInt32LE(n, 1); return buf; } else { const buf = Buffer.alloc(9); buf[0] = 0xff; buf.writeBigUInt64LE(BigInt(n), 1); return buf; } } /** * Witness Builder class * @class WitnessBuilder */ class WitnessBuilder { /** * Build P2WPKH witness stack * @param {Buffer} signature - DER signature with sighash type appended * @param {Buffer} publicKey - Compressed public key (33 bytes) * @returns {Array<Buffer>} Witness stack [signature, publicKey] */ static buildP2WPKH(signature, publicKey) { if (!Buffer.isBuffer(signature)) { signature = Buffer.from(signature, 'hex'); } if (!Buffer.isBuffer(publicKey)) { publicKey = Buffer.from(publicKey, 'hex'); } if (publicKey.length !== 33) { throw new WitnessError('P2WPKH requires 33-byte compressed public key', 'INVALID_PUBKEY'); } return [signature, publicKey]; } /** * Build P2WSH witness stack * @param {Array<Buffer>} stackItems - Items to push (signatures, etc.) * @param {Buffer} witnessScript - The witness script * @returns {Array<Buffer>} Witness stack [...items, witnessScript] */ static buildP2WSH(stackItems, witnessScript) { if (!Array.isArray(stackItems)) { throw new WitnessError('Stack items must be an array', 'INVALID_STACK'); } const witness = stackItems.map(item => Buffer.isBuffer(item) ? item : Buffer.from(item, 'hex') ); const script = Buffer.isBuffer(witnessScript) ? witnessScript : Buffer.from(witnessScript, 'hex'); witness.push(script); return witness; } /** * Build P2WSH multisig witness stack * @param {Array<Buffer>} signatures - DER signatures with sighash types * @param {Buffer} redeemScript - Multisig redeem script * @returns {Array<Buffer>} Witness stack [OP_0, ...signatures, redeemScript] */ static buildP2WSHMultisig(signatures, redeemScript) { // OP_0 for CHECKMULTISIG bug const witness = [Buffer.alloc(0)]; for (const sig of signatures) { witness.push(Buffer.isBuffer(sig) ? sig : Buffer.from(sig, 'hex')); } witness.push(Buffer.isBuffer(redeemScript) ? redeemScript : Buffer.from(redeemScript, 'hex')); return witness; } /** * Build P2TR key-path witness stack * @param {Buffer} schnorrSignature - 64 or 65 byte Schnorr signature * @returns {Array<Buffer>} Witness stack [signature] */ static buildP2TRKeyPath(schnorrSignature) { if (!Buffer.isBuffer(schnorrSignature)) { schnorrSignature = Buffer.from(schnorrSignature, 'hex'); } if (schnorrSignature.length !== 64 && schnorrSignature.length !== 65) { throw new WitnessError( 'P2TR key-path requires 64 or 65 byte Schnorr signature', 'INVALID_SIGNATURE' ); } return [schnorrSignature]; } /** * Build P2TR script-path witness stack * @param {Array<Buffer>} stackItems - Script input items * @param {Buffer} tapscript - The tapscript being executed * @param {Buffer} controlBlock - The control block * @returns {Array<Buffer>} Witness stack [...items, tapscript, controlBlock] */ static buildP2TRScriptPath(stackItems, tapscript, controlBlock) { const witness = []; // Add stack items for (const item of stackItems) { witness.push(Buffer.isBuffer(item) ? item : Buffer.from(item, 'hex')); } // Add tapscript witness.push(Buffer.isBuffer(tapscript) ? tapscript : Buffer.from(tapscript, 'hex')); // Add control block witness.push(Buffer.isBuffer(controlBlock) ? controlBlock : Buffer.from(controlBlock, 'hex')); return witness; } /** * Build control block for Taproot script-path spending * @param {Buffer} internalPubkey - 32-byte x-only internal public key * @param {number} leafVersion - Leaf version (default 0xc0) * @param {Array<Buffer>} merklePath - Merkle path from leaf to root * @returns {Buffer} Control block */ static buildControlBlock(internalPubkey, leafVersion = 0xc0, merklePath = []) { if (!Buffer.isBuffer(internalPubkey)) { internalPubkey = Buffer.from(internalPubkey, 'hex'); } if (internalPubkey.length !== 32) { throw new WitnessError('Internal pubkey must be 32 bytes', 'INVALID_PUBKEY'); } // First byte: leaf version with parity bit const parts = [Buffer.from([leafVersion]), internalPubkey]; // Add merkle path for (const node of merklePath) { const nodeBuf = Buffer.isBuffer(node) ? node : Buffer.from(node, 'hex'); if (nodeBuf.length !== 32) { throw new WitnessError('Merkle path nodes must be 32 bytes', 'INVALID_MERKLE_NODE'); } parts.push(nodeBuf); } return Buffer.concat(parts); } /** * Calculate tapleaf hash * @param {Buffer} script - Tapscript * @param {number} leafVersion - Leaf version (default 0xc0) * @returns {Buffer} 32-byte tapleaf hash */ static calculateTapleafHash(script, leafVersion = 0xc0) { if (!Buffer.isBuffer(script)) { script = Buffer.from(script, 'hex'); } const data = Buffer.concat([ Buffer.from([leafVersion]), encodeVarInt(script.length), script ]); return taggedHash('TapLeaf', data); } /** * Calculate tapbranch hash * @param {Buffer} left - Left child hash * @param {Buffer} right - Right child hash * @returns {Buffer} 32-byte tapbranch hash */ static calculateTapbranchHash(left, right) { // Sort lexicographically if (Buffer.compare(left, right) > 0) { [left, right] = [right, left]; } return taggedHash('TapBranch', Buffer.concat([left, right])); } /** * Calculate taptweak hash * @param {Buffer} internalPubkey - 32-byte x-only internal public key * @param {Buffer} [merkleRoot=null] - Optional 32-byte merkle root * @returns {Buffer} 32-byte tweak */ static calculateTaptweak(internalPubkey, merkleRoot = null) { if (!Buffer.isBuffer(internalPubkey)) { internalPubkey = Buffer.from(internalPubkey, 'hex'); } const data = merkleRoot ? Buffer.concat([internalPubkey, merkleRoot]) : internalPubkey; return taggedHash('TapTweak', data); } /** * Serialize witness stack to bytes * @param {Array<Buffer>} witnessStack - Witness items * @returns {Buffer} Serialized witness */ static serialize(witnessStack) { const parts = [encodeVarInt(witnessStack.length)]; for (const item of witnessStack) { const buf = Buffer.isBuffer(item) ? item : Buffer.from(item, 'hex'); parts.push(encodeVarInt(buf.length)); parts.push(buf); } return Buffer.concat(parts); } /** * Parse serialized witness data * @param {Buffer} data - Serialized witness * @returns {Array<Buffer>} Witness stack */ static parse(data) { const result = []; let offset = 0; // Read item count const countResult = this._readVarInt(data, offset); const count = countResult.value; offset = countResult.offset; for (let i = 0; i < count; i++) { const lenResult = this._readVarInt(data, offset); const len = lenResult.value; offset = lenResult.offset; result.push(data.slice(offset, offset + len)); offset += len; } return result; } /** * Read variable-length integer * @private */ static _readVarInt(data, offset) { const first = data[offset]; if (first < 0xfd) { return { value: first, offset: offset + 1 }; } else if (first === 0xfd) { return { value: data.readUInt16LE(offset + 1), offset: offset + 3 }; } else if (first === 0xfe) { return { value: data.readUInt32LE(offset + 1), offset: offset + 5 }; } else { return { value: Number(data.readBigUInt64LE(offset + 1)), offset: offset + 9 }; } } /** * Validate witness stack for a given output type * @param {Array<Buffer>} witnessStack - Witness to validate * @param {string} outputType - Output type (p2wpkh, p2wsh, p2tr) * @returns {Object} Validation result */ static validate(witnessStack, outputType) { if (!Array.isArray(witnessStack) || witnessStack.length === 0) { return { valid: false, error: 'Empty witness stack' }; } switch (outputType) { case 'p2wpkh': if (witnessStack.length !== 2) { return { valid: false, error: 'P2WPKH requires exactly 2 items' }; } if (witnessStack[1].length !== 33) { return { valid: false, error: 'P2WPKH requires 33-byte compressed pubkey' }; } return { valid: true }; case 'p2wsh': if (witnessStack.length < 1) { return { valid: false, error: 'P2WSH requires at least witness script' }; } return { valid: true }; case 'p2tr': // Key-path: 1 item (signature) // Script-path: 2+ items (inputs + script + control block) if (witnessStack.length === 1) { const sig = witnessStack[0]; if (sig.length !== 64 && sig.length !== 65) { return { valid: false, error: 'P2TR key-path requires 64/65 byte signature' }; } } return { valid: true }; default: return { valid: false, error: `Unknown output type: ${outputType}` }; } } } export { WitnessBuilder, WitnessError, taggedHash, encodeVarInt }; export default WitnessBuilder;