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
JavaScript
/**
* @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;