UNPKG

@scure/btc-signer

Version:

Audited & minimal library for Bitcoin. Handle transactions, Schnorr, Taproot, UTXO & PSBT

1,074 lines (1,073 loc) 55.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Transaction = exports.SigHash = exports.SignatureHash = exports.def = exports.Decimal = exports.DEFAULT_SEQUENCE = exports.DEFAULT_LOCKTIME = exports.DEFAULT_VERSION = exports.PRECISION = exports.toVsize = void 0; exports.cloneDeep = cloneDeep; exports.inputBeforeSign = inputBeforeSign; exports.getPrevOut = getPrevOut; exports.normalizeInput = normalizeInput; exports.getInputType = getInputType; exports.PSBTCombine = PSBTCombine; exports.bip32Path = bip32Path; const base_1 = require("@scure/base"); const P = require("micro-packed"); const payment_ts_1 = require("./payment.js"); const psbt = require("./psbt.js"); const script_ts_1 = require("./script.js"); const u = require("./utils.js"); const utils_ts_1 = require("./utils.js"); const EMPTY32 = new Uint8Array(32); const EMPTY_OUTPUT = { amount: 0xffffffffffffffffn, script: P.EMPTY, }; const toVsize = (weight) => Math.ceil(weight / 4); exports.toVsize = toVsize; exports.PRECISION = 8; exports.DEFAULT_VERSION = 2; exports.DEFAULT_LOCKTIME = 0; exports.DEFAULT_SEQUENCE = 4294967295; exports.Decimal = P.coders.decimal(exports.PRECISION); // Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc) const def = (value, def) => (value === undefined ? def : value); exports.def = def; function cloneDeep(obj) { if (Array.isArray(obj)) return obj.map((i) => cloneDeep(i)); // slice of nodejs Buffer doesn't copy else if ((0, utils_ts_1.isBytes)(obj)) return Uint8Array.from(obj); // immutable else if (['number', 'bigint', 'boolean', 'string', 'undefined'].includes(typeof obj)) return obj; // null is object else if (obj === null) return obj; // should be last, so it won't catch other types else if (typeof obj === 'object') { return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)])); } throw new Error(`cloneDeep: unknown type=${obj} (${typeof obj})`); } /** * Internal, exported only for backwards-compat. Use `SigHash` instead. * @deprecated */ var SignatureHash; (function (SignatureHash) { SignatureHash[SignatureHash["DEFAULT"] = 0] = "DEFAULT"; SignatureHash[SignatureHash["ALL"] = 1] = "ALL"; SignatureHash[SignatureHash["NONE"] = 2] = "NONE"; SignatureHash[SignatureHash["SINGLE"] = 3] = "SINGLE"; SignatureHash[SignatureHash["ANYONECANPAY"] = 128] = "ANYONECANPAY"; })(SignatureHash || (exports.SignatureHash = SignatureHash = {})); var SigHash; (function (SigHash) { SigHash[SigHash["DEFAULT"] = 0] = "DEFAULT"; SigHash[SigHash["ALL"] = 1] = "ALL"; SigHash[SigHash["NONE"] = 2] = "NONE"; SigHash[SigHash["SINGLE"] = 3] = "SINGLE"; SigHash[SigHash["DEFAULT_ANYONECANPAY"] = 128] = "DEFAULT_ANYONECANPAY"; SigHash[SigHash["ALL_ANYONECANPAY"] = 129] = "ALL_ANYONECANPAY"; SigHash[SigHash["NONE_ANYONECANPAY"] = 130] = "NONE_ANYONECANPAY"; SigHash[SigHash["SINGLE_ANYONECANPAY"] = 131] = "SINGLE_ANYONECANPAY"; })(SigHash || (exports.SigHash = SigHash = {})); function getTaprootKeys(privKey, pubKey, internalKey, merkleRoot = P.EMPTY) { if ((0, utils_ts_1.equalBytes)(internalKey, pubKey)) { privKey = u.taprootTweakPrivKey(privKey, merkleRoot); pubKey = u.pubSchnorr(privKey); } return { privKey, pubKey }; } // Force check amount/script function outputBeforeSign(i) { if (i.script === undefined || i.amount === undefined) throw new Error('Transaction/output: script and amount required'); return { script: i.script, amount: i.amount }; } // Force check index/txid/sequence function inputBeforeSign(i) { if (i.txid === undefined || i.index === undefined) throw new Error('Transaction/input: txid and index required'); return { txid: i.txid, index: i.index, sequence: (0, exports.def)(i.sequence, exports.DEFAULT_SEQUENCE), finalScriptSig: (0, exports.def)(i.finalScriptSig, P.EMPTY), }; } function cleanFinalInput(i) { for (const _k in i) { const k = _k; if (!psbt.PSBTInputFinalKeys.includes(k)) delete i[k]; } } // (TxHash, Idx) const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE }); function validateSigHash(s) { if (typeof s !== 'number' || typeof SigHash[s] !== 'string') throw new Error(`Invalid SigHash=${s}`); return s; } function unpackSighash(hashType) { const masked = hashType & 0b0011111; return { isAny: !!(hashType & SignatureHash.ANYONECANPAY), isNone: masked === SignatureHash.NONE, isSingle: masked === SignatureHash.SINGLE, }; } function validateOpts(opts) { if (opts !== undefined && {}.toString.call(opts) !== '[object Object]') throw new Error(`Wrong object type for transaction options: ${opts}`); const _opts = { ...opts, // Defaults version: (0, exports.def)(opts.version, exports.DEFAULT_VERSION), lockTime: (0, exports.def)(opts.lockTime, 0), PSBTVersion: (0, exports.def)(opts.PSBTVersion, 0), }; if (typeof _opts.allowUnknowInput !== 'undefined') opts.allowUnknownInputs = _opts.allowUnknowInput; if (typeof _opts.allowUnknowOutput !== 'undefined') opts.allowUnknownOutputs = _opts.allowUnknowOutput; if (typeof _opts.lockTime !== 'number') throw new Error('Transaction lock time should be number'); P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime // There is no PSBT v1, and any new version will probably have fields which we don't know how to parse, which // can lead to constructing broken transactions if (_opts.PSBTVersion !== 0 && _opts.PSBTVersion !== 2) throw new Error(`Unknown PSBT version ${_opts.PSBTVersion}`); // Flags for (const k of [ 'allowUnknownVersion', 'allowUnknownOutputs', 'allowUnknownInputs', 'disableScriptCheck', 'bip174jsCompat', 'allowLegacyWitnessUtxo', 'lowR', ]) { const v = _opts[k]; if (v === undefined) continue; // optional if (typeof v !== 'boolean') throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`); } // 0 and -1 happens in tests if (_opts.allowUnknownVersion ? typeof _opts.version === 'number' : ![-1, 0, 1, 2, 3].includes(_opts.version)) throw new Error(`Unknown version: ${_opts.version}`); if (_opts.customScripts !== undefined) { const cs = _opts.customScripts; if (!Array.isArray(cs)) { throw new Error(`wrong custom scripts type (expected array): customScripts=${cs} (${typeof cs})`); } for (const s of cs) { if (typeof s.encode !== 'function' || typeof s.decode !== 'function') throw new Error(`wrong script=${s} (${typeof s})`); if (s.finalizeTaproot !== undefined && typeof s.finalizeTaproot !== 'function') throw new Error(`wrong script=${s} (${typeof s})`); } } return Object.freeze(_opts); } // NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point! function validateInput(i) { if (i.nonWitnessUtxo && i.index !== undefined) { const last = i.nonWitnessUtxo.outputs.length - 1; if (i.index > last) throw new Error(`validateInput: index(${i.index}) not in nonWitnessUtxo`); const prevOut = i.nonWitnessUtxo.outputs[i.index]; if (i.witnessUtxo && (!(0, utils_ts_1.equalBytes)(i.witnessUtxo.script, prevOut.script) || i.witnessUtxo.amount !== prevOut.amount)) throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo'); if (i.txid) { const outputs = i.nonWitnessUtxo.outputs; if (outputs.length - 1 < i.index) throw new Error('nonWitnessUtxo: incorect output index'); // At this point, we are using previous tx output to create new input. // Script safety checks are unnecessary: // - User has no control over previous tx. If somebody send money in same tx // as unspendable output, we still want user able to spend money // - We still want some checks to notify user about possible errors early // in case user wants to use wrong input by mistake // - Worst case: tx will be rejected by nodes. Still better than disallowing user // to spend real input, no matter how broken it looks const tx = Transaction.fromRaw(script_ts_1.RawTx.encode(i.nonWitnessUtxo), { allowUnknownOutputs: true, disableScriptCheck: true, allowUnknownInputs: true, }); const txid = base_1.hex.encode(i.txid); // PSBTv2 vectors have non-final tx in inputs if (tx.isFinal && tx.id !== txid) throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`); } } return i; } // Normalizes input function getPrevOut(input) { if (input.nonWitnessUtxo) { if (input.index === undefined) throw new Error('Unknown input index'); return input.nonWitnessUtxo.outputs[input.index]; } else if (input.witnessUtxo) return input.witnessUtxo; else throw new Error('Cannot find previous output info'); } function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, allowUnknown = false) { let { nonWitnessUtxo, txid } = i; // String support for common fields. We usually prefer Uint8Array to avoid errors // like hex looking string accidentally passed, however, in case of nonWitnessUtxo // it is better to expect string, since constructing this complex object will be // difficult for user if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = base_1.hex.decode(nonWitnessUtxo); if ((0, utils_ts_1.isBytes)(nonWitnessUtxo)) nonWitnessUtxo = script_ts_1.RawTx.decode(nonWitnessUtxo); if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined) nonWitnessUtxo = cur?.nonWitnessUtxo; if (typeof txid === 'string') txid = base_1.hex.decode(txid); // TODO: if we have nonWitnessUtxo, we can extract txId from here if (txid === undefined) txid = cur?.txid; let res = { ...cur, ...i, nonWitnessUtxo, txid }; if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo; if (res.sequence === undefined) res.sequence = exports.DEFAULT_SEQUENCE; if (res.tapMerkleRoot === null) delete res.tapMerkleRoot; res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown); psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point let prevOut; if (res.nonWitnessUtxo && res.index !== undefined) prevOut = res.nonWitnessUtxo.outputs[res.index]; else if (res.witnessUtxo) prevOut = res.witnessUtxo; if (prevOut && !disableScriptCheck) (0, payment_ts_1.checkScript)(prevOut && prevOut.script, res.redeemScript, res.witnessScript); return res; } function getInputType(input, allowLegacyWitnessUtxo = false) { let txType = 'legacy'; let defaultSighash = SignatureHash.ALL; const prevOut = getPrevOut(input); const first = payment_ts_1.OutScript.decode(prevOut.script); let type = first.type; let cur = first; const stack = [first]; if (first.type === 'tr') { defaultSighash = SignatureHash.DEFAULT; return { txType: 'taproot', type: 'tr', last: first, lastScript: prevOut.script, defaultSighash, sighash: input.sighashType || defaultSighash, }; } else { if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit'; if (first.type === 'sh') { if (!input.redeemScript) throw new Error('inputType: sh without redeemScript'); let child = payment_ts_1.OutScript.decode(input.redeemScript); if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit'; stack.push(child); cur = child; type += `-${child.type}`; } // wsh can be inside sh if (cur.type === 'wsh') { if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript'); let child = payment_ts_1.OutScript.decode(input.witnessScript); if (child.type === 'wsh') txType = 'segwit'; stack.push(child); cur = child; type += `-${child.type}`; } const last = stack[stack.length - 1]; if (last.type === 'sh' || last.type === 'wsh') throw new Error('inputType: sh/wsh cannot be terminal type'); const lastScript = payment_ts_1.OutScript.encode(last); const res = { type, txType, last, lastScript, defaultSighash, sighash: input.sighashType || defaultSighash, }; if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) { throw new Error(`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`); } return res; } } class Transaction { constructor(opts = {}) { this.global = {}; this.inputs = []; // use getInput() this.outputs = []; // use getOutput() const _opts = (this.opts = validateOpts(opts)); // Merge with global structure of PSBTv2 if (_opts.lockTime !== exports.DEFAULT_LOCKTIME) this.global.fallbackLocktime = _opts.lockTime; this.global.txVersion = _opts.version; } // Import static fromRaw(raw, opts = {}) { const parsed = script_ts_1.RawTx.decode(raw); const tx = new Transaction({ ...opts, version: parsed.version, lockTime: parsed.lockTime }); for (const o of parsed.outputs) tx.addOutput(o); tx.outputs = parsed.outputs; tx.inputs = parsed.inputs; if (parsed.witnesses) { for (let i = 0; i < parsed.witnesses.length; i++) tx.inputs[i].finalScriptWitness = parsed.witnesses[i]; } return tx; } // PSBT static fromPSBT(psbt_, opts = {}) { let parsed; try { parsed = psbt.RawPSBTV0.decode(psbt_); } catch (e0) { try { parsed = psbt.RawPSBTV2.decode(psbt_); } catch (e2) { // Throw error for v0 parsing, since it popular, otherwise it would be shadowed by v2 error throw e0; } } const PSBTVersion = parsed.global.version || 0; if (PSBTVersion !== 0 && PSBTVersion !== 2) throw new Error(`Wrong PSBT version=${PSBTVersion}`); const unsigned = parsed.global.unsignedTx; const version = PSBTVersion === 0 ? unsigned?.version : parsed.global.txVersion; const lockTime = PSBTVersion === 0 ? unsigned?.lockTime : parsed.global.fallbackLocktime; const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion }); // We need slice here, because otherwise const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount; tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => validateInput({ finalScriptSig: P.EMPTY, ...parsed.global.unsignedTx?.inputs[j], ...i, })); const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount; tx.outputs = parsed.outputs.slice(0, outputCount).map((i, j) => ({ ...i, ...parsed.global.unsignedTx?.outputs[j], })); tx.global = { ...parsed.global, txVersion: version }; // just in case proprietary/unknown fields if (lockTime !== exports.DEFAULT_LOCKTIME) tx.global.fallbackLocktime = lockTime; return tx; } toPSBT(PSBTVersion = this.opts.PSBTVersion) { if (PSBTVersion !== 0 && PSBTVersion !== 2) throw new Error(`Wrong PSBT version=${PSBTVersion}`); // if (PSBTVersion === 0 && this.inputs.length === 0) { // throw new Error( // 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.' // ); // } const inputs = this.inputs.map((i) => validateInput(psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, i))); for (const inp of inputs) { // Don't serialize empty fields if (inp.partialSig && !inp.partialSig.length) delete inp.partialSig; if (inp.finalScriptSig && !inp.finalScriptSig.length) delete inp.finalScriptSig; if (inp.finalScriptWitness && !inp.finalScriptWitness.length) delete inp.finalScriptWitness; } const outputs = this.outputs.map((i) => psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i)); const global = { ...this.global }; if (PSBTVersion === 0) { /* - Bitcoin raw transaction expects to have at least 1 input because it uses case with zero inputs as marker for SegWit - this means we cannot serialize raw tx with zero inputs since it will be parsed as SegWit tx - Parsing of PSBTv0 depends on unsignedTx (it looks for input count here) - BIP-174 requires old serialization format (without witnesses) inside global, which solves this */ global.unsignedTx = script_ts_1.RawOldTx.decode(script_ts_1.RawOldTx.encode({ version: this.version, lockTime: this.lockTime, inputs: this.inputs.map(inputBeforeSign).map((i) => ({ ...i, finalScriptSig: P.EMPTY, })), outputs: this.outputs.map(outputBeforeSign), })); delete global.fallbackLocktime; delete global.txVersion; } else { global.version = PSBTVersion; global.txVersion = this.version; global.inputCount = this.inputs.length; global.outputCount = this.outputs.length; if (global.fallbackLocktime && global.fallbackLocktime === exports.DEFAULT_LOCKTIME) delete global.fallbackLocktime; } if (this.opts.bip174jsCompat) { if (!inputs.length) inputs.push({}); if (!outputs.length) outputs.push({}); } return (PSBTVersion === 0 ? psbt.RawPSBTV0 : psbt.RawPSBTV2).encode({ global, inputs, outputs, }); } // BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time) get lockTime() { let height = exports.DEFAULT_LOCKTIME; let heightCnt = 0; let time = exports.DEFAULT_LOCKTIME; let timeCnt = 0; for (const i of this.inputs) { if (i.requiredHeightLocktime) { height = Math.max(height, i.requiredHeightLocktime); heightCnt++; } if (i.requiredTimeLocktime) { time = Math.max(time, i.requiredTimeLocktime); timeCnt++; } } if (heightCnt && heightCnt >= timeCnt) return height; if (time !== exports.DEFAULT_LOCKTIME) return time; return this.global.fallbackLocktime || exports.DEFAULT_LOCKTIME; } get version() { // Should be not possible if (this.global.txVersion === undefined) throw new Error('No global.txVersion'); return this.global.txVersion; } inputStatus(idx) { this.checkInputIdx(idx); const input = this.inputs[idx]; // Finalized if (input.finalScriptSig && input.finalScriptSig.length) return 'finalized'; if (input.finalScriptWitness && input.finalScriptWitness.length) return 'finalized'; // Signed taproot if (input.tapKeySig) return 'signed'; if (input.tapScriptSig && input.tapScriptSig.length) return 'signed'; // Signed if (input.partialSig && input.partialSig.length) return 'signed'; return 'unsigned'; } // Cannot replace unpackSighash, tests rely on very generic implemenetation with signing inputs outside of range // We will lose some vectors -> smaller test coverage of preimages (very important!) inputSighash(idx) { this.checkInputIdx(idx); const inputSighash = this.inputs[idx].sighashType; const sighash = inputSighash === undefined ? SignatureHash.DEFAULT : inputSighash; // ALL or DEFAULT -- everything signed // NONE -- all inputs + no outputs // SINGLE -- all inputs + output with same index // ALL + ANYONE -- specific input + all outputs // NONE + ANYONE -- specific input + no outputs // SINGLE -- specific inputs + output with same index const sigOutputs = sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11; const sigInputs = sighash & SignatureHash.ANYONECANPAY; return { sigInputs, sigOutputs }; } // Very nice for debug purposes, but slow. If there is too much inputs/outputs to add, will be quadratic. // Some cache will be nice, but there chance to have bugs with cache invalidation signStatus() { // if addInput or addOutput is not possible, then all inputs or outputs are signed let addInput = true, addOutput = true; let inputs = [], outputs = []; for (let idx = 0; idx < this.inputs.length; idx++) { const status = this.inputStatus(idx); // Unsigned input doesn't affect anything if (status === 'unsigned') continue; const { sigInputs, sigOutputs } = this.inputSighash(idx); // Input type if (sigInputs === SignatureHash.ANYONECANPAY) inputs.push(idx); else addInput = false; // Output type if (sigOutputs === SignatureHash.ALL) addOutput = false; else if (sigOutputs === SignatureHash.SINGLE) outputs.push(idx); else if (sigOutputs === SignatureHash.NONE) { // Doesn't affect any outputs at all } else throw new Error(`Wrong signature hash output type: ${sigOutputs}`); } return { addInput, addOutput, inputs, outputs }; } get isFinal() { for (let idx = 0; idx < this.inputs.length; idx++) if (this.inputStatus(idx) !== 'finalized') return false; return true; } // Info utils get hasWitnesses() { let out = false; for (const i of this.inputs) if (i.finalScriptWitness && i.finalScriptWitness.length) out = true; return out; } // https://en.bitcoin.it/wiki/Weight_units get weight() { if (!this.isFinal) throw new Error('Transaction is not finalized'); let out = 32; // Outputs const outputs = this.outputs.map(outputBeforeSign); out += 4 * script_ts_1.CompactSizeLen.encode(this.outputs.length).length; for (const o of outputs) out += 32 + 4 * script_ts_1.VarBytes.encode(o.script).length; // Inputs if (this.hasWitnesses) out += 2; out += 4 * script_ts_1.CompactSizeLen.encode(this.inputs.length).length; for (const i of this.inputs) { out += 160 + 4 * script_ts_1.VarBytes.encode(i.finalScriptSig || P.EMPTY).length; if (this.hasWitnesses && i.finalScriptWitness) out += script_ts_1.RawWitness.encode(i.finalScriptWitness).length; } return out; } get vsize() { return (0, exports.toVsize)(this.weight); } toBytes(withScriptSig = false, withWitness = false) { return script_ts_1.RawTx.encode({ version: this.version, lockTime: this.lockTime, inputs: this.inputs.map(inputBeforeSign).map((i) => ({ ...i, finalScriptSig: (withScriptSig && i.finalScriptSig) || P.EMPTY, })), outputs: this.outputs.map(outputBeforeSign), witnesses: this.inputs.map((i) => i.finalScriptWitness || []), segwitFlag: withWitness && this.hasWitnesses, }); } get unsignedTx() { return this.toBytes(false, false); } get hex() { return base_1.hex.encode(this.toBytes(true, this.hasWitnesses)); } get hash() { if (!this.isFinal) throw new Error('Transaction is not finalized'); return base_1.hex.encode(u.sha256x2(this.toBytes(true))); } get id() { if (!this.isFinal) throw new Error('Transaction is not finalized'); return base_1.hex.encode(u.sha256x2(this.toBytes(true)).reverse()); } // Input stuff checkInputIdx(idx) { if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length) throw new Error(`Wrong input index=${idx}`); } getInput(idx) { this.checkInputIdx(idx); return cloneDeep(this.inputs[idx]); } get inputsLength() { return this.inputs.length; } // Modification addInput(input, _ignoreSignStatus = false) { if (!_ignoreSignStatus && !this.signStatus().addInput) throw new Error('Tx has signed inputs, cannot add new one'); this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck)); return this.inputs.length - 1; } updateInput(idx, input, _ignoreSignStatus = false) { this.checkInputIdx(idx); let allowedFields = undefined; if (!_ignoreSignStatus) { const status = this.signStatus(); if (!status.addInput || status.inputs.includes(idx)) allowedFields = psbt.PSBTInputUnsignedKeys; } this.inputs[idx] = normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown); } // Output stuff checkOutputIdx(idx) { if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.outputs.length) throw new Error(`Wrong output index=${idx}`); } getOutput(idx) { this.checkOutputIdx(idx); return cloneDeep(this.outputs[idx]); } getOutputAddress(idx, network = utils_ts_1.NETWORK) { const out = this.getOutput(idx); if (!out.script) return; return (0, payment_ts_1.Address)(network).encode(payment_ts_1.OutScript.decode(out.script)); } get outputsLength() { return this.outputs.length; } normalizeOutput(o, cur, allowedFields) { let { amount, script } = o; if (amount === undefined) amount = cur?.amount; if (typeof amount !== 'bigint') throw new Error(`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`); if (typeof script === 'string') script = base_1.hex.decode(script); if (script === undefined) script = cur?.script; let res = { ...cur, ...o, amount, script }; if (res.amount === undefined) delete res.amount; res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.allowUnknown); psbt.PSBTOutputCoder.encode(res); if (res.script && !this.opts.allowUnknownOutputs && payment_ts_1.OutScript.decode(res.script).type === 'unknown') { throw new Error('Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'); } if (!this.opts.disableScriptCheck) (0, payment_ts_1.checkScript)(res.script, res.redeemScript, res.witnessScript); return res; } addOutput(o, _ignoreSignStatus = false) { if (!_ignoreSignStatus && !this.signStatus().addOutput) throw new Error('Tx has signed outputs, cannot add new one'); this.outputs.push(this.normalizeOutput(o)); return this.outputs.length - 1; } updateOutput(idx, output, _ignoreSignStatus = false) { this.checkOutputIdx(idx); let allowedFields = undefined; if (!_ignoreSignStatus) { const status = this.signStatus(); if (!status.addOutput || status.outputs.includes(idx)) allowedFields = psbt.PSBTOutputUnsignedKeys; } this.outputs[idx] = this.normalizeOutput(output, this.outputs[idx], allowedFields); } addOutputAddress(address, amount, network = utils_ts_1.NETWORK) { return this.addOutput({ script: payment_ts_1.OutScript.encode((0, payment_ts_1.Address)(network).decode(address)), amount }); } // Utils get fee() { let res = 0n; for (const i of this.inputs) { const prevOut = getPrevOut(i); if (!prevOut) throw new Error('Empty input amount'); res += prevOut.amount; } const outputs = this.outputs.map(outputBeforeSign); for (const o of outputs) res -= o.amount; return res; } // Signing // Based on https://github.com/bitcoin/bitcoin/blob/5871b5b5ab57a0caf9b7514eb162c491c83281d5/test/functional/test_framework/script.py#L624 // There is optimization opportunity to re-use hashes for multiple inputs for witness v0/v1, // but we are trying to be less complicated for audit purpose for now. preimageLegacy(idx, prevOutScript, hashType) { const { isAny, isNone, isSingle } = unpackSighash(hashType); if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`); if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length) return P.U256BE.encode(1n); prevOutScript = script_ts_1.Script.encode(script_ts_1.Script.decode(prevOutScript).filter((i) => i !== 'CODESEPARATOR')); let inputs = this.inputs .map(inputBeforeSign) .map((input, inputIdx) => ({ ...input, finalScriptSig: inputIdx === idx ? prevOutScript : P.EMPTY, })); if (isAny) inputs = [inputs[idx]]; else if (isNone || isSingle) { inputs = inputs.map((input, inputIdx) => ({ ...input, sequence: inputIdx === idx ? input.sequence : 0, })); } let outputs = this.outputs.map(outputBeforeSign); if (isNone) outputs = []; else if (isSingle) { outputs = outputs.slice(0, idx).fill(EMPTY_OUTPUT).concat([outputs[idx]]); } const tmpTx = script_ts_1.RawTx.encode({ lockTime: this.lockTime, version: this.version, segwitFlag: false, inputs, outputs, }); return u.sha256x2(tmpTx, P.I32LE.encode(hashType)); } preimageWitnessV0(idx, prevOutScript, hashType, amount) { const { isAny, isNone, isSingle } = unpackSighash(hashType); let inputHash = EMPTY32; let sequenceHash = EMPTY32; let outputHash = EMPTY32; const inputs = this.inputs.map(inputBeforeSign); const outputs = this.outputs.map(outputBeforeSign); if (!isAny) inputHash = u.sha256x2(...inputs.map(TxHashIdx.encode)); if (!isAny && !isSingle && !isNone) sequenceHash = u.sha256x2(...inputs.map((i) => P.U32LE.encode(i.sequence))); if (!isSingle && !isNone) { outputHash = u.sha256x2(...outputs.map(script_ts_1.RawOutput.encode)); } else if (isSingle && idx < outputs.length) outputHash = u.sha256x2(script_ts_1.RawOutput.encode(outputs[idx])); const input = inputs[idx]; return u.sha256x2(P.I32LE.encode(this.version), inputHash, sequenceHash, P.bytes(32, true).encode(input.txid), P.U32LE.encode(input.index), script_ts_1.VarBytes.encode(prevOutScript), P.U64LE.encode(amount), P.U32LE.encode(input.sequence), outputHash, P.U32LE.encode(this.lockTime), P.U32LE.encode(hashType)); } preimageWitnessV1(idx, prevOutScript, hashType, amount, codeSeparator = -1, leafScript, leafVer = 0xc0, annex) { if (!Array.isArray(amount) || this.inputs.length !== amount.length) throw new Error(`Invalid amounts array=${amount}`); if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length) throw new Error(`Invalid prevOutScript array=${prevOutScript}`); const out = [ P.U8.encode(0), P.U8.encode(hashType), // U8 sigHash P.I32LE.encode(this.version), P.U32LE.encode(this.lockTime), ]; const outType = hashType === SignatureHash.DEFAULT ? SignatureHash.ALL : hashType & 0b11; const inType = hashType & SignatureHash.ANYONECANPAY; const inputs = this.inputs.map(inputBeforeSign); const outputs = this.outputs.map(outputBeforeSign); if (inType !== SignatureHash.ANYONECANPAY) { out.push(...[ inputs.map(TxHashIdx.encode), amount.map(P.U64LE.encode), prevOutScript.map(script_ts_1.VarBytes.encode), inputs.map((i) => P.U32LE.encode(i.sequence)), ].map((i) => u.sha256((0, utils_ts_1.concatBytes)(...i)))); } if (outType === SignatureHash.ALL) { out.push(u.sha256((0, utils_ts_1.concatBytes)(...outputs.map(script_ts_1.RawOutput.encode)))); } const spendType = (annex ? 1 : 0) | (leafScript ? 2 : 0); out.push(new Uint8Array([spendType])); if (inType === SignatureHash.ANYONECANPAY) { const inp = inputs[idx]; out.push(TxHashIdx.encode(inp), P.U64LE.encode(amount[idx]), script_ts_1.VarBytes.encode(prevOutScript[idx]), P.U32LE.encode(inp.sequence)); } else out.push(P.U32LE.encode(idx)); if (spendType & 1) out.push(u.sha256(script_ts_1.VarBytes.encode(annex || P.EMPTY))); if (outType === SignatureHash.SINGLE) out.push(idx < outputs.length ? u.sha256(script_ts_1.RawOutput.encode(outputs[idx])) : EMPTY32); if (leafScript) out.push((0, payment_ts_1.tapLeafHash)(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator)); return u.tagSchnorr('TapSighash', ...out); } // Signer can be privateKey OR instance of bip32 HD stuff signIdx(privateKey, idx, allowedSighash, _auxRand) { this.checkInputIdx(idx); const input = this.inputs[idx]; const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo); // Handle BIP32 HDKey if (!(0, utils_ts_1.isBytes)(privateKey)) { if (!input.bip32Derivation || !input.bip32Derivation.length) throw new Error('bip32Derivation: empty'); const signers = input.bip32Derivation .filter((i) => i[1].fingerprint == privateKey.fingerprint) .map(([pubKey, { path }]) => { let s = privateKey; for (const i of path) s = s.deriveChild(i); if (!(0, utils_ts_1.equalBytes)(s.publicKey, pubKey)) throw new Error('bip32Derivation: wrong pubKey'); if (!s.privateKey) throw new Error('bip32Derivation: no privateKey'); return s; }); if (!signers.length) throw new Error(`bip32Derivation: no items with fingerprint=${privateKey.fingerprint}`); let signed = false; for (const s of signers) if (this.signIdx(s.privateKey, idx)) signed = true; return signed; } // Sighash checks // Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour. if (!allowedSighash) allowedSighash = [inputType.defaultSighash]; else allowedSighash.forEach(validateSigHash); const sighash = inputType.sighash; if (!allowedSighash.includes(sighash)) { throw new Error(`Input with not allowed sigHash=${sighash}. Allowed: ${allowedSighash.join(', ')}`); } // It is possible to sign these inputs for legacy/segwit v0 (but no taproot!), // however this was because of bug in bitcoin-core, which remains here because of consensus. // If this is absolutely neccessary for your case, please open issue. // We disable it to avoid complicated workflow where SINGLE will block adding new outputs const { sigOutputs } = this.inputSighash(idx); if (sigOutputs === SignatureHash.SINGLE && idx >= this.outputs.length) { throw new Error(`Input with sighash SINGLE, but there is no output with corresponding index=${idx}`); } // Actual signing // Taproot const prevOut = getPrevOut(input); if (inputType.txType === 'taproot') { const prevOuts = this.inputs.map(getPrevOut); const prevOutScript = prevOuts.map((i) => i.script); const amount = prevOuts.map((i) => i.amount); let signed = false; let schnorrPub = u.pubSchnorr(privateKey); let merkleRoot = input.tapMerkleRoot || P.EMPTY; if (input.tapInternalKey) { // internal + tweak = tweaked key // if internal key == current public key, we need to tweak private key, // otherwise sign as is. bitcoinjs implementation always wants tweaked // priv key to be provided const { pubKey, privKey } = getTaprootKeys(privateKey, schnorrPub, input.tapInternalKey, merkleRoot); const [taprootPubKey, _] = u.taprootTweakPubkey(input.tapInternalKey, merkleRoot); if ((0, utils_ts_1.equalBytes)(taprootPubKey, pubKey)) { const hash = this.preimageWitnessV1(idx, prevOutScript, sighash, amount); const sig = (0, utils_ts_1.concatBytes)(u.signSchnorr(hash, privKey, _auxRand), sighash !== SignatureHash.DEFAULT ? new Uint8Array([sighash]) : P.EMPTY); this.updateInput(idx, { tapKeySig: sig }, true); signed = true; } } if (input.tapLeafScript) { input.tapScriptSig = input.tapScriptSig || []; for (const [_, _script] of input.tapLeafScript) { const script = _script.subarray(0, -1); const scriptDecoded = script_ts_1.Script.decode(script); const ver = _script[_script.length - 1]; const hash = (0, payment_ts_1.tapLeafHash)(script, ver); // NOTE: no need to tweak internal key here, since we don't support nested p2tr const pos = scriptDecoded.findIndex((i) => (0, utils_ts_1.isBytes)(i) && (0, utils_ts_1.equalBytes)(i, schnorrPub)); // Skip if there is no public key in tapLeafScript if (pos === -1) continue; const msg = this.preimageWitnessV1(idx, prevOutScript, sighash, amount, undefined, script, ver); const sig = (0, utils_ts_1.concatBytes)(u.signSchnorr(msg, privateKey, _auxRand), sighash !== SignatureHash.DEFAULT ? new Uint8Array([sighash]) : P.EMPTY); this.updateInput(idx, { tapScriptSig: [[{ pubKey: schnorrPub, leafHash: hash }, sig]] }, true); signed = true; } } if (!signed) throw new Error('No taproot scripts signed'); return true; } else { // only compressed keys are supported for now const pubKey = u.pubECDSA(privateKey); // TODO: replace with explicit checks // Check if script has public key or its has inside let hasPubkey = false; const pubKeyHash = u.hash160(pubKey); for (const i of script_ts_1.Script.decode(inputType.lastScript)) { if ((0, utils_ts_1.isBytes)(i) && ((0, utils_ts_1.equalBytes)(i, pubKey) || (0, utils_ts_1.equalBytes)(i, pubKeyHash))) hasPubkey = true; } if (!hasPubkey) throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`); let hash; if (inputType.txType === 'legacy') { hash = this.preimageLegacy(idx, inputType.lastScript, sighash); } else if (inputType.txType === 'segwit') { let script = inputType.lastScript; // If wpkh OR sh-wpkh, wsh-wpkh is impossible, so looks ok if (inputType.last.type === 'wpkh') script = payment_ts_1.OutScript.encode({ type: 'pkh', hash: inputType.last.hash }); hash = this.preimageWitnessV0(idx, script, sighash, prevOut.amount); } else throw new Error(`Transaction/sign: unknown tx type: ${inputType.txType}`); const sig = u.signECDSA(hash, privateKey, this.opts.lowR); this.updateInput(idx, { partialSig: [[pubKey, (0, utils_ts_1.concatBytes)(sig, new Uint8Array([sighash]))]], }, true); } return true; } // This is bad API. Will work if user creates and signs tx, but if // there is some complex workflow with exchanging PSBT and signing them, // then it is better to validate which output user signs. How could a better API look like? // Example: user adds input, sends to another party, then signs received input (mixer etc), // another user can add different input for same key and user will sign it. // Even worse: another user can add bip32 derivation, and spend money from different address. // Better api: signIdx sign(privateKey, allowedSighash, _auxRand) { let num = 0; for (let i = 0; i < this.inputs.length; i++) { try { if (this.signIdx(privateKey, i, allowedSighash, _auxRand)) num++; } catch (e) { } } if (!num) throw new Error('No inputs signed'); return num; } finalizeIdx(idx) { this.checkInputIdx(idx); if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount'); const input = this.inputs[idx]; const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo); // Taproot finalize if (inputType.txType === 'taproot') { if (input.tapKeySig) input.finalScriptWitness = [input.tapKeySig]; else if (input.tapLeafScript && input.tapScriptSig) { // Sort leafs by control block length. const leafs = input.tapLeafScript.sort((a, b) => psbt.TaprootControlBlock.encode(a[0]).length - psbt.TaprootControlBlock.encode(b[0]).length); for (const [cb, _script] of leafs) { // Last byte is version const script = _script.slice(0, -1); const ver = _script[_script.length - 1]; const outScript = payment_ts_1.OutScript.decode(script); const hash = (0, payment_ts_1.tapLeafHash)(script, ver); const scriptSig = input.tapScriptSig.filter((i) => (0, utils_ts_1.equalBytes)(i[0].leafHash, hash)); let signatures = []; if (outScript.type === 'tr_ms') { const m = outScript.m; const pubkeys = outScript.pubkeys; let added = 0; for (const pub of pubkeys) { const sigIdx = scriptSig.findIndex((i) => (0, utils_ts_1.equalBytes)(i[0].pubKey, pub)); // Should have exact amount of signatures (more -- will fail) if (added === m || sigIdx === -1) { signatures.push(P.EMPTY); continue; } signatures.push(scriptSig[sigIdx][1]); added++; } // Should be exact same as m if (added !== m) continue; } else if (outScript.type === 'tr_ns') { for (const pub of outScript.pubkeys) { const sigIdx = scriptSig.findIndex((i) => (0, utils_ts_1.equalBytes)(i[0].pubKey, pub)); if (sigIdx === -1) continue; signatures.push(scriptSig[sigIdx][1]); } if (signatures.length !== outScript.pubkeys.length) continue; } else if (outScript.type === 'unknown' && this.opts.allowUnknownInputs) { // Trying our best to sign what we can const scriptDecoded = script_ts_1.Script.decode(script); signatures = scriptSig .map(([{ pubKey }, signature]) => { const pos = scriptDecoded.findIndex((i) => (0, utils_ts_1.isBytes)(i) && (0, utils_ts_1.equalBytes)(i, pubKey)); if (pos === -1) throw new Error('finalize/taproot: cannot find position of pubkey in script'); return { signature, pos }; }) // Reverse order (because witness is stack and we take last element first from it) .sort((a, b) => a.pos - b.pos) .map((i) => i.signature); if (!signatures.length) continue; } else { const custom = this.opts.customScripts; if (custom) { for (const c of custom) { if (!c.finalizeTaproot) continue; const scriptDecoded = script_ts_1.Script.decode(script); const csEncoded = c.encode(scriptDecoded); if (csEncoded === undefined) continue; const finalized = c.finalizeTaproot(script, csEncoded, scriptSig); if (!finalized) continue; input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb)); input.finalScriptSig = P.EMPTY; cleanFinalInput(input); return; } } throw new Error('Finalize: Unknown tapLeafScript'); } // Witness is stack, so last element will be used first input.finalScriptWitness = signatures .reverse() .concat([script, psbt.TaprootControlBlock.encode(cb)]); break; } if (!input.finalScriptWitness) throw new Error('finalize/taproot: empty witness'); } else throw new Error('finalize/taproot: unknown input'); input.finalScriptSig = P.EMPTY; cleanFinalInput(input); return; } if (!input.partialSig || !input.partialSig.length) throw new Error('Not enough partial sign'); let inputScript = P.EMPTY; let witness = []; // TODO: move input scripts closer to payments/output scripts // Multisig if (inputType.last.type === 'ms') { const m = inputType.last.m; const pubkeys = inputType.last.pubkeys; let signatures = []; // partial: [pubkey, sign] for (const pub of pubkeys) { const sign = input.partialSig.find((s) => (0, utils_ts_1.equalBytes)(pub, s[0])); if (!sign) continue; signatures.push(sign[1]); } signatures = signatures.slice(0, m); if (signatures.length !== m) { throw new Error(`Multisig: wrong signatures count, m=${m} n=${pubkeys.length} signatures=${signatures.length}`);