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

764 lines (669 loc) 25 kB
/** * @fileoverview Sighash calculation for Bitcoin transactions * @description Implements BIP143 (SegWit) and BIP341 (Taproot) sighash algorithms * @version 1.0.0 * @author yfbsei * @license ISC */ import { createHash } from 'node:crypto'; /** * Sighash types * @constant {Object} */ const SIGHASH = { ALL: 0x01, NONE: 0x02, SINGLE: 0x03, ANYONECANPAY: 0x80, DEFAULT: 0x00, // Taproot only ALL_ANYONECANPAY: 0x81, NONE_ANYONECANPAY: 0x82, SINGLE_ANYONECANPAY: 0x83 }; /** * Custom error class for sighash operations * @class SighashError * @extends Error */ class SighashError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'SighashError'; this.code = code; this.details = details; } } /** * SHA256 hash helper * @param {Buffer} data - Data to hash * @returns {Buffer} 32-byte hash */ function sha256(data) { return createHash('sha256').update(data).digest(); } /** * Double SHA256 hash * @param {Buffer} data - Data to hash * @returns {Buffer} 32-byte hash */ function hash256(data) { return sha256(sha256(data)); } /** * Tagged hash for Taproot (BIP340) * @param {string} tag - Hash tag * @param {Buffer} data - Data to hash * @returns {Buffer} 32-byte tagged hash */ function taggedHash(tag, data) { const tagHash = sha256(Buffer.from(tag, 'utf8')); return sha256(Buffer.concat([tagHash, tagHash, data])); } /** * Encode a variable-length integer * @param {number|bigint} n - Number to encode * @returns {Buffer} VarInt encoded */ function encodeVarInt(n) { const num = typeof n === 'bigint' ? Number(n) : n; if (num < 0xfd) { return Buffer.from([num]); } else if (num <= 0xffff) { const buf = Buffer.alloc(3); buf[0] = 0xfd; buf.writeUInt16LE(num, 1); return buf; } else if (num <= 0xffffffff) { const buf = Buffer.alloc(5); buf[0] = 0xfe; buf.writeUInt32LE(num, 1); return buf; } else { const buf = Buffer.alloc(9); buf[0] = 0xff; buf.writeBigUInt64LE(BigInt(num), 1); return buf; } } /** * Create scriptCode for P2WPKH sighash * @param {Buffer} pubkeyHash - 20-byte public key hash * @returns {Buffer} scriptCode */ function createP2WPKHScriptCode(pubkeyHash) { if (pubkeyHash.length !== 20) { throw new SighashError('Invalid pubkey hash length', 'INVALID_PUBKEY_HASH'); } // OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG return Buffer.concat([ Buffer.from([0x19, 0x76, 0xa9, 0x14]), pubkeyHash, Buffer.from([0x88, 0xac]) ]); } /** * BIP143 - SegWit Sighash Calculator * @class BIP143 */ class BIP143 { /** * Calculate hashPrevouts * @param {Array} inputs - Transaction inputs * @param {number} sighashType - Sighash type * @returns {Buffer} 32-byte hash */ static hashPrevouts(inputs, sighashType) { if (sighashType & SIGHASH.ANYONECANPAY) { return Buffer.alloc(32, 0); } const data = []; for (const input of inputs) { const txid = typeof input.txid === 'string' ? Buffer.from(input.txid, 'hex').reverse() : Buffer.from(input.txid).reverse(); const vout = Buffer.alloc(4); vout.writeUInt32LE(input.vout, 0); data.push(txid, vout); } return hash256(Buffer.concat(data)); } /** * Calculate hashSequence * @param {Array} inputs - Transaction inputs * @param {number} sighashType - Sighash type * @returns {Buffer} 32-byte hash */ static hashSequence(inputs, sighashType) { const baseType = sighashType & 0x1f; if ((sighashType & SIGHASH.ANYONECANPAY) || baseType === SIGHASH.SINGLE || baseType === SIGHASH.NONE) { return Buffer.alloc(32, 0); } const data = []; for (const input of inputs) { const seq = Buffer.alloc(4); seq.writeUInt32LE(input.sequence ?? 0xffffffff, 0); data.push(seq); } return hash256(Buffer.concat(data)); } /** * Calculate hashOutputs * @param {Array} outputs - Transaction outputs * @param {number} inputIndex - Current input index * @param {number} sighashType - Sighash type * @returns {Buffer} 32-byte hash */ static hashOutputs(outputs, inputIndex, sighashType) { const baseType = sighashType & 0x1f; if (baseType === SIGHASH.NONE) { return Buffer.alloc(32, 0); } if (baseType === SIGHASH.SINGLE) { if (inputIndex >= outputs.length) { return Buffer.alloc(32, 0); } const output = outputs[inputIndex]; return hash256(this._serializeOutput(output)); } // SIGHASH_ALL const data = outputs.map(o => this._serializeOutput(o)); return hash256(Buffer.concat(data)); } /** * Serialize a single output * @private */ static _serializeOutput(output) { const value = Buffer.alloc(8); value.writeBigUInt64LE(BigInt(output.value), 0); const script = Buffer.isBuffer(output.scriptPubKey) ? output.scriptPubKey : Buffer.from(output.scriptPubKey, 'hex'); return Buffer.concat([value, encodeVarInt(script.length), script]); } /** * Calculate BIP143 sighash for SegWit inputs * @param {Object} tx - Transaction object * @param {number} inputIndex - Input being signed * @param {Buffer} scriptCode - Script code for input * @param {number} value - Input value in satoshis * @param {number} [sighashType=0x01] - Sighash type * @returns {Buffer} 32-byte sighash */ static calculate(tx, inputIndex, scriptCode, value, sighashType = SIGHASH.ALL) { if (inputIndex < 0 || inputIndex >= tx.inputs.length) { throw new SighashError('Invalid input index', 'INVALID_INDEX'); } const input = tx.inputs[inputIndex]; // 1. nVersion (4 bytes) const version = Buffer.alloc(4); version.writeInt32LE(tx.version ?? 2, 0); // 2. hashPrevouts (32 bytes) const prevouts = this.hashPrevouts(tx.inputs, sighashType); // 3. hashSequence (32 bytes) const sequence = this.hashSequence(tx.inputs, sighashType); // 4. outpoint (36 bytes) const txid = typeof input.txid === 'string' ? Buffer.from(input.txid, 'hex').reverse() : Buffer.from(input.txid).reverse(); const vout = Buffer.alloc(4); vout.writeUInt32LE(input.vout, 0); const outpoint = Buffer.concat([txid, vout]); // 5. scriptCode (with length prefix already included) const scriptCodeData = Buffer.isBuffer(scriptCode) ? scriptCode : Buffer.from(scriptCode, 'hex'); // 6. value (8 bytes) const valueBuf = Buffer.alloc(8); valueBuf.writeBigUInt64LE(BigInt(value), 0); // 7. nSequence (4 bytes) const nSequence = Buffer.alloc(4); nSequence.writeUInt32LE(input.sequence ?? 0xffffffff, 0); // 8. hashOutputs (32 bytes) const outputs = this.hashOutputs(tx.outputs, inputIndex, sighashType); // 9. nLocktime (4 bytes) const locktime = Buffer.alloc(4); locktime.writeUInt32LE(tx.locktime ?? 0, 0); // 10. sighash type (4 bytes) const sigType = Buffer.alloc(4); sigType.writeUInt32LE(sighashType, 0); // Concatenate all and double SHA256 const preimage = Buffer.concat([ version, prevouts, sequence, outpoint, scriptCodeData, valueBuf, nSequence, outputs, locktime, sigType ]); return hash256(preimage); } /** * Calculate sighash for P2WPKH input * @param {Object} tx - Transaction object * @param {number} inputIndex - Input index * @param {Buffer} pubkeyHash - 20-byte public key hash * @param {number} value - Input value in satoshis * @param {number} [sighashType=0x01] - Sighash type * @returns {Buffer} 32-byte sighash */ static forP2WPKH(tx, inputIndex, pubkeyHash, value, sighashType = SIGHASH.ALL) { const scriptCode = createP2WPKHScriptCode(pubkeyHash); return this.calculate(tx, inputIndex, scriptCode, value, sighashType); } } /** * BIP341 - Taproot Sighash Calculator * @class BIP341 */ class BIP341 { /** * Calculate sha_prevouts for Taproot * @param {Array} inputs - Transaction inputs * @returns {Buffer} 32-byte hash */ static shaPrevouts(inputs) { const data = []; for (const input of inputs) { const txid = typeof input.txid === 'string' ? Buffer.from(input.txid, 'hex').reverse() : Buffer.from(input.txid).reverse(); const vout = Buffer.alloc(4); vout.writeUInt32LE(input.vout, 0); data.push(txid, vout); } return sha256(Buffer.concat(data)); } /** * Calculate sha_amounts for Taproot * @param {Array} prevouts - Previous outputs with values * @returns {Buffer} 32-byte hash */ static shaAmounts(prevouts) { const data = []; for (const prevout of prevouts) { const value = Buffer.alloc(8); value.writeBigUInt64LE(BigInt(prevout.value), 0); data.push(value); } return sha256(Buffer.concat(data)); } /** * Calculate sha_scriptpubkeys for Taproot * @param {Array} prevouts - Previous outputs with scriptPubKeys * @returns {Buffer} 32-byte hash */ static shaScriptPubkeys(prevouts) { const data = []; for (const prevout of prevouts) { const script = Buffer.isBuffer(prevout.scriptPubKey) ? prevout.scriptPubKey : Buffer.from(prevout.scriptPubKey, 'hex'); data.push(encodeVarInt(script.length), script); } return sha256(Buffer.concat(data)); } /** * Calculate sha_sequences for Taproot * @param {Array} inputs - Transaction inputs * @returns {Buffer} 32-byte hash */ static shaSequences(inputs) { const data = []; for (const input of inputs) { const seq = Buffer.alloc(4); seq.writeUInt32LE(input.sequence ?? 0xffffffff, 0); data.push(seq); } return sha256(Buffer.concat(data)); } /** * Calculate sha_outputs for Taproot * @param {Array} outputs - Transaction outputs * @returns {Buffer} 32-byte hash */ static shaOutputs(outputs) { const data = []; for (const output of outputs) { const value = Buffer.alloc(8); value.writeBigUInt64LE(BigInt(output.value), 0); const script = Buffer.isBuffer(output.scriptPubKey) ? output.scriptPubKey : Buffer.from(output.scriptPubKey, 'hex'); data.push(value, encodeVarInt(script.length), script); } return sha256(Buffer.concat(data)); } /** * Calculate BIP341 sighash for Taproot key-path spending * @param {Object} tx - Transaction object * @param {number} inputIndex - Input being signed * @param {Array} prevouts - Previous outputs (with value and scriptPubKey) * @param {number} [sighashType=0x00] - Sighash type (DEFAULT=0x00) * @param {Buffer} [annex=null] - Optional annex * @returns {Buffer} 32-byte sighash */ static calculate(tx, inputIndex, prevouts, sighashType = SIGHASH.DEFAULT, annex = null) { if (inputIndex < 0 || inputIndex >= tx.inputs.length) { throw new SighashError('Invalid input index', 'INVALID_INDEX'); } if (prevouts.length !== tx.inputs.length) { throw new SighashError('Prevouts count must match inputs count', 'PREVOUT_MISMATCH'); } const input = tx.inputs[inputIndex]; const parts = []; // Epoch (1 byte) parts.push(Buffer.from([0x00])); // Hash type (1 byte) parts.push(Buffer.from([sighashType])); // nVersion (4 bytes) const version = Buffer.alloc(4); version.writeInt32LE(tx.version ?? 2, 0); parts.push(version); // nLockTime (4 bytes) const locktime = Buffer.alloc(4); locktime.writeUInt32LE(tx.locktime ?? 0, 0); parts.push(locktime); const baseType = sighashType & 0x1f; const anyoneCanPay = !!(sighashType & SIGHASH.ANYONECANPAY); // If not ANYONECANPAY, include aggregated data if (!anyoneCanPay) { parts.push(this.shaPrevouts(tx.inputs)); parts.push(this.shaAmounts(prevouts)); parts.push(this.shaScriptPubkeys(prevouts)); parts.push(this.shaSequences(tx.inputs)); } // If SIGHASH_ALL (or DEFAULT), include all outputs if (baseType === SIGHASH.ALL || baseType === SIGHASH.DEFAULT) { parts.push(this.shaOutputs(tx.outputs)); } // Spend type (1 byte): 0 = key path, 1 = script path (+ annex flags) let spendType = 0x00; if (annex) { spendType |= 0x01; } parts.push(Buffer.from([spendType])); // If ANYONECANPAY, include input-specific data if (anyoneCanPay) { // outpoint const txid = typeof input.txid === 'string' ? Buffer.from(input.txid, 'hex').reverse() : Buffer.from(input.txid).reverse(); const vout = Buffer.alloc(4); vout.writeUInt32LE(input.vout, 0); parts.push(txid, vout); // amount const value = Buffer.alloc(8); value.writeBigUInt64LE(BigInt(prevouts[inputIndex].value), 0); parts.push(value); // scriptPubKey const script = Buffer.isBuffer(prevouts[inputIndex].scriptPubKey) ? prevouts[inputIndex].scriptPubKey : Buffer.from(prevouts[inputIndex].scriptPubKey, 'hex'); parts.push(encodeVarInt(script.length), script); // sequence const seq = Buffer.alloc(4); seq.writeUInt32LE(input.sequence ?? 0xffffffff, 0); parts.push(seq); } else { // input index (4 bytes) const idx = Buffer.alloc(4); idx.writeUInt32LE(inputIndex, 0); parts.push(idx); } // Annex hash if present if (annex) { parts.push(sha256(Buffer.concat([encodeVarInt(annex.length), annex]))); } // If SIGHASH_SINGLE, include single output if (baseType === SIGHASH.SINGLE) { if (inputIndex >= tx.outputs.length) { throw new SighashError('No output for SIGHASH_SINGLE', 'NO_OUTPUT'); } const output = tx.outputs[inputIndex]; const value = Buffer.alloc(8); value.writeBigUInt64LE(BigInt(output.value), 0); const script = Buffer.isBuffer(output.scriptPubKey) ? output.scriptPubKey : Buffer.from(output.scriptPubKey, 'hex'); parts.push(sha256(Buffer.concat([value, encodeVarInt(script.length), script]))); } // Create tagged hash return taggedHash('TapSighash', Buffer.concat(parts)); } /** * Calculate sighash for Taproot script-path spending * @param {Object} tx - Transaction object * @param {number} inputIndex - Input index * @param {Array} prevouts - Previous outputs * @param {Buffer} tapLeafHash - Leaf hash (tapleaf_hash) * @param {Buffer} keyVersion - Key version (0x00 for internal key) * @param {number} [sighashType=0x00] - Sighash type * @param {Buffer} [annex=null] - Optional annex * @returns {Buffer} 32-byte sighash */ static forScriptPath(tx, inputIndex, prevouts, tapLeafHash, keyVersion = Buffer.from([0x00]), sighashType = SIGHASH.DEFAULT, annex = null) { // For script path, we append additional data to the key-path sighash preimage const keyPathSighash = this.calculate(tx, inputIndex, prevouts, sighashType, annex); // ext_flag = 1 for script path // This is a simplified implementation - full script path needs leaf data const extData = Buffer.concat([ tapLeafHash, keyVersion, Buffer.from([0xff, 0xff, 0xff, 0xff]) // codesep_pos = -1 ]); return taggedHash('TapSighash', Buffer.concat([ Buffer.from([0x00, sighashType]), // epoch + hash_type keyPathSighash.slice(2), // Skip epoch and hash_type from key-path Buffer.from([0x01]), // ext_flag = 1 extData ])); } } /** * Legacy sighash calculator (P2PKH) * @class LegacySighash */ class LegacySighash { /** * Calculate legacy sighash for P2PKH inputs * @param {Object} tx - Transaction object * @param {number} inputIndex - Input being signed * @param {Buffer} subscript - Subscript (scriptPubKey of UTXO) * @param {number} [sighashType=0x01] - Sighash type * @returns {Buffer} 32-byte sighash */ static calculate(tx, inputIndex, subscript, sighashType = SIGHASH.ALL) { if (inputIndex < 0 || inputIndex >= tx.inputs.length) { throw new SighashError('Invalid input index', 'INVALID_INDEX'); } // Clone transaction const txCopy = { version: tx.version ?? 1, inputs: tx.inputs.map((input, i) => ({ txid: input.txid, vout: input.vout, scriptSig: i === inputIndex ? subscript : Buffer.alloc(0), sequence: input.sequence ?? 0xffffffff })), outputs: [...tx.outputs], locktime: tx.locktime ?? 0 }; const baseType = sighashType & 0x1f; // Handle SIGHASH_NONE if (baseType === SIGHASH.NONE) { txCopy.outputs = []; for (let i = 0; i < txCopy.inputs.length; i++) { if (i !== inputIndex) { txCopy.inputs[i].sequence = 0; } } } // Handle SIGHASH_SINGLE if (baseType === SIGHASH.SINGLE) { if (inputIndex >= tx.outputs.length) { // Bitcoin Core bug: return hash of 1 const result = Buffer.alloc(32, 0); result[0] = 1; return result; } txCopy.outputs = tx.outputs.slice(0, inputIndex + 1).map((out, i) => { if (i < inputIndex) { return { value: -1, scriptPubKey: Buffer.alloc(0) }; } return out; }); for (let i = 0; i < txCopy.inputs.length; i++) { if (i !== inputIndex) { txCopy.inputs[i].sequence = 0; } } } // Handle ANYONECANPAY if (sighashType & SIGHASH.ANYONECANPAY) { txCopy.inputs = [txCopy.inputs[inputIndex]]; } // Serialize and hash const serialized = this._serialize(txCopy); const sigHashBuf = Buffer.alloc(4); sigHashBuf.writeUInt32LE(sighashType, 0); return hash256(Buffer.concat([serialized, sigHashBuf])); } /** * Serialize transaction for legacy sighash * @private */ static _serialize(tx) { const parts = []; // Version const version = Buffer.alloc(4); version.writeInt32LE(tx.version, 0); parts.push(version); // Inputs parts.push(encodeVarInt(tx.inputs.length)); for (const input of tx.inputs) { const txid = typeof input.txid === 'string' ? Buffer.from(input.txid, 'hex').reverse() : Buffer.from(input.txid).reverse(); parts.push(txid); const vout = Buffer.alloc(4); vout.writeUInt32LE(input.vout, 0); parts.push(vout); const script = Buffer.isBuffer(input.scriptSig) ? input.scriptSig : Buffer.from(input.scriptSig || '', 'hex'); parts.push(encodeVarInt(script.length)); parts.push(script); const seq = Buffer.alloc(4); seq.writeUInt32LE(input.sequence, 0); parts.push(seq); } // Outputs parts.push(encodeVarInt(tx.outputs.length)); for (const output of tx.outputs) { const value = Buffer.alloc(8); if (output.value === -1) { value.writeBigInt64LE(BigInt(-1), 0); } else { value.writeBigUInt64LE(BigInt(output.value), 0); } parts.push(value); const script = Buffer.isBuffer(output.scriptPubKey) ? output.scriptPubKey : Buffer.from(output.scriptPubKey || '', 'hex'); parts.push(encodeVarInt(script.length)); parts.push(script); } // Locktime const locktime = Buffer.alloc(4); locktime.writeUInt32LE(tx.locktime, 0); parts.push(locktime); return Buffer.concat(parts); } } /** * Unified sighash calculator * @class SighashCalculator */ class SighashCalculator { /** * Calculate sighash based on input type * @param {Object} tx - Transaction object * @param {number} inputIndex - Input index * @param {Object} prevout - Previous output info * @param {number} [sighashType=0x01] - Sighash type * @param {Object} [options={}] - Additional options * @returns {Buffer} 32-byte sighash */ static calculate(tx, inputIndex, prevout, sighashType = SIGHASH.ALL, options = {}) { const inputType = prevout.type || this._detectType(prevout); switch (inputType) { case 'p2tr': return BIP341.calculate( tx, inputIndex, options.prevouts || [prevout], sighashType === SIGHASH.ALL ? SIGHASH.DEFAULT : sighashType, options.annex ); case 'p2wpkh': return BIP143.forP2WPKH( tx, inputIndex, prevout.pubkeyHash || prevout.program, prevout.value, sighashType ); case 'p2pkh': default: return LegacySighash.calculate( tx, inputIndex, prevout.scriptPubKey, sighashType ); } } /** * Detect input type from prevout * @private */ static _detectType(prevout) { if (!prevout.scriptPubKey) return 'p2pkh'; const script = Buffer.isBuffer(prevout.scriptPubKey) ? prevout.scriptPubKey : Buffer.from(prevout.scriptPubKey, 'hex'); // P2TR: OP_1 <32-byte key> if (script.length === 34 && script[0] === 0x51 && script[1] === 0x20) { return 'p2tr'; } // P2WPKH: OP_0 <20-byte hash> if (script.length === 22 && script[0] === 0x00 && script[1] === 0x14) { return 'p2wpkh'; } // P2WSH: OP_0 <32-byte hash> if (script.length === 34 && script[0] === 0x00 && script[1] === 0x20) { return 'p2wsh'; } // Default to P2PKH return 'p2pkh'; } } export { SIGHASH, SighashError, SighashCalculator, BIP143, BIP341, LegacySighash, createP2WPKHScriptCode, sha256, hash256, taggedHash, encodeVarInt }; export default SighashCalculator;