UNPKG

@bitgo/utxo-lib

Version:

Client-side Bitcoin JavaScript library

1,017 lines 170 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UtxoPsbt = void 0; const assert_1 = require("assert"); const bip174_1 = require("bip174"); const utils_1 = require("bip174/src/lib/utils"); const bufferutils_1 = require("bitcoinjs-lib/src/bufferutils"); const bs58check = require("bs58check"); const proprietaryKeyVal_1 = require("bip174/src/lib/proprietaryKeyVal"); const secp256k1_1 = require("@bitgo/secp256k1"); const __1 = require(".."); const UtxoTransaction_1 = require("./UtxoTransaction"); const Unspent_1 = require("./Unspent"); const scriptTypes_1 = require("./psbt/scriptTypes"); const fromHalfSigned_1 = require("./psbt/fromHalfSigned"); const outputScripts_1 = require("./outputScripts"); const parseInput_1 = require("./parseInput"); const Musig2_1 = require("./Musig2"); const types_1 = require("./types"); const taproot_1 = require("../taproot"); const PsbtUtil_1 = require("./PsbtUtil"); function defaultSighashTypes() { return [__1.Transaction.SIGHASH_DEFAULT, __1.Transaction.SIGHASH_ALL]; } function addForkIdToSighashesIfNeeded(network, sighashTypes) { switch ((0, __1.getMainnet)(network)) { case __1.networks.bitcoincash: case __1.networks.bitcoinsv: case __1.networks.bitcoingold: case __1.networks.ecash: return [...sighashTypes, ...sighashTypes.map((s) => s | UtxoTransaction_1.UtxoTransaction.SIGHASH_FORKID)]; default: return sighashTypes; } } function toSignatureParams(network, v) { if (Array.isArray(v)) return toSignatureParams(network, { sighashTypes: v }); const defaultSignatureParams = { deterministic: false, sighashTypes: defaultSighashTypes() }; const ret = { ...defaultSignatureParams, ...v }; ret.sighashTypes = addForkIdToSighashesIfNeeded(network, ret.sighashTypes); return ret; } /** * @param a * @param b * @returns true if the two public keys are equal ignoring the y coordinate. */ function equalPublicKeyIgnoreY(a, b) { return (0, outputScripts_1.toXOnlyPublicKey)(a).equals((0, outputScripts_1.toXOnlyPublicKey)(b)); } // TODO: upstream does `checkInputsForPartialSigs` before doing things like // `setVersion`. Our inputs could have tapscriptsigs (or in future tapkeysigs) // and not fail that check. Do we want to do anything about that? class UtxoPsbt extends __1.Psbt { constructor() { super(...arguments); this.nonceStore = new Musig2_1.Musig2NonceStore(); } static transactionFromBuffer(buffer, network) { return UtxoTransaction_1.UtxoTransaction.fromBuffer(buffer, false, 'bigint', network); } static createPsbt(opts, data) { return new UtxoPsbt(opts, data || new bip174_1.Psbt(new __1.PsbtTransaction({ tx: new UtxoTransaction_1.UtxoTransaction(opts.network) }))); } static fromBuffer(buffer, opts) { const transactionFromBuffer = (buffer) => { const tx = this.transactionFromBuffer(buffer, opts.network); return new __1.PsbtTransaction({ tx }); }; const psbtBase = bip174_1.Psbt.fromBuffer(buffer, transactionFromBuffer, { bip32PathsAbsolute: opts.bip32PathsAbsolute, }); const psbt = this.createPsbt(opts, psbtBase); // Upstream checks for duplicate inputs here, but it seems to be of dubious value. return psbt; } static fromHex(data, opts) { return this.fromBuffer(Buffer.from(data, 'hex'), opts); } /** * @param parent - Parent key. Matched with `bip32Derivations` using `fingerprint` property. * @param bip32Derivations - possible derivations for input or output * @param ignoreY - when true, ignore the y coordinate when matching public keys * @return derived bip32 node if matching derivation is found, undefined if none is found * @throws Error if more than one match is found */ static deriveKeyPair(parent, bip32Derivations, { ignoreY }) { const matchingDerivations = bip32Derivations.filter((bipDv) => { return bipDv.masterFingerprint.equals(parent.fingerprint); }); if (!matchingDerivations.length) { // No fingerprint match return undefined; } if (matchingDerivations.length !== 1) { throw new Error(`more than one matching derivation for fingerprint ${parent.fingerprint.toString('hex')}: ${matchingDerivations.length}`); } const [derivation] = matchingDerivations; const node = parent.derivePath(derivation.path); if (!node.publicKey.equals(derivation.pubkey)) { if (!ignoreY || !equalPublicKeyIgnoreY(node.publicKey, derivation.pubkey)) { throw new Error('pubkey did not match bip32Derivation'); } } return node; } static deriveKeyPairForInput(bip32, input) { return input.tapBip32Derivation?.length ? UtxoPsbt.deriveKeyPair(bip32, input.tapBip32Derivation, { ignoreY: true })?.publicKey : input.bip32Derivation?.length ? UtxoPsbt.deriveKeyPair(bip32, input.bip32Derivation, { ignoreY: false })?.publicKey : bip32?.publicKey; } get network() { return this.tx.network; } toHex() { return this.toBuffer().toString('hex'); } /** * It is expensive to attempt to compute every output address using psbt.txOutputs[outputIndex] * to then just get the script. Here, we are doing the same thing as what txOutputs() does in * bitcoinjs-lib, but without iterating over each output. * @param outputIndex * @returns output script at the given index */ getOutputScript(outputIndex) { return this.__CACHE.__TX.outs[outputIndex].script; } getNonWitnessPreviousTxids() { const txInputs = this.txInputs; // These are somewhat costly to extract const txidSet = new Set(); this.data.inputs.forEach((input, index) => { if (!input.witnessUtxo) { throw new Error('Must have witness UTXO for all inputs'); } if (!(0, scriptTypes_1.isSegwit)(input.witnessUtxo.script, input.redeemScript)) { txidSet.add((0, Unspent_1.getOutputIdForInput)(txInputs[index]).txid); } }); return [...txidSet]; } addNonWitnessUtxos(txBufs) { const txInputs = this.txInputs; // These are somewhat costly to extract this.data.inputs.forEach((input, index) => { if (!input.witnessUtxo) { throw new Error('Must have witness UTXO for all inputs'); } if (!(0, scriptTypes_1.isSegwit)(input.witnessUtxo.script, input.redeemScript)) { const { txid } = (0, Unspent_1.getOutputIdForInput)(txInputs[index]); if (txBufs[txid] === undefined) { throw new Error('Not all required previous transactions provided'); } this.updateInput(index, { nonWitnessUtxo: txBufs[txid] }); } }); return this; } static fromTransaction(transaction, prevOutputs) { if (prevOutputs.length !== transaction.ins.length) { throw new Error(`Transaction has ${transaction.ins.length} inputs, but ${prevOutputs.length} previous outputs provided`); } const clonedTransaction = transaction.clone(); const updates = (0, fromHalfSigned_1.unsign)(clonedTransaction, prevOutputs); const psbtBase = new bip174_1.Psbt(new __1.PsbtTransaction({ tx: clonedTransaction })); clonedTransaction.ins.forEach(() => psbtBase.inputs.push({ unknownKeyVals: [] })); clonedTransaction.outs.forEach(() => psbtBase.outputs.push({ unknownKeyVals: [] })); const psbt = this.createPsbt({ network: transaction.network }, psbtBase); updates.forEach((update, index) => { psbt.updateInput(index, update); psbt.updateInput(index, { witnessUtxo: { script: prevOutputs[index].script, value: prevOutputs[index].value } }); }); return psbt; } getUnsignedTx() { return this.tx.clone(); } static newTransaction(network) { return new UtxoTransaction_1.UtxoTransaction(network); } get tx() { return this.data.globalMap.unsignedTx.tx; } checkForSignatures(propName) { this.data.inputs.forEach((input) => { if (input.tapScriptSig?.length || input.tapKeySig || input.partialSig?.length) { throw new Error(`Cannot modify ${propName ?? 'transaction'} - signatures exist.`); } }); } /** * @returns true if the input at inputIndex is a taproot key path. * Checks for presence of minimum required key path input fields and absence of any script path only input fields. */ isTaprootKeyPathInput(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); return (!!input.tapInternalKey && !!input.tapMerkleRoot && !(input.tapLeafScript?.length || input.tapScriptSig?.length || input.tapBip32Derivation?.some((v) => v.leafHashes.length))); } /** * @returns true if the input at inputIndex is a taproot script path. * Checks for presence of minimum required script path input fields and absence of any key path only input fields. */ isTaprootScriptPathInput(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); return (!!input.tapLeafScript?.length && !(this.getProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER, subtype: PsbtUtil_1.ProprietaryKeySubtype.MUSIG2_PARTICIPANT_PUB_KEYS, }).length || this.getProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER, subtype: PsbtUtil_1.ProprietaryKeySubtype.MUSIG2_PUB_NONCE, }).length || this.getProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER, subtype: PsbtUtil_1.ProprietaryKeySubtype.MUSIG2_PARTIAL_SIG, }).length)); } /** * @returns true if the input at inputIndex is a taproot */ isTaprootInput(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const isP2TR = (script) => { try { (0, taproot_1.getTaprootOutputKey)(script); return true; } catch (e) { return false; } }; return !!(input.tapInternalKey || input.tapMerkleRoot || input.tapLeafScript?.length || input.tapBip32Derivation?.length || input.tapScriptSig?.length || this.getProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER, subtype: PsbtUtil_1.ProprietaryKeySubtype.MUSIG2_PARTICIPANT_PUB_KEYS, }).length || this.getProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER, subtype: PsbtUtil_1.ProprietaryKeySubtype.MUSIG2_PUB_NONCE, }).length || this.getProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER, subtype: PsbtUtil_1.ProprietaryKeySubtype.MUSIG2_PARTIAL_SIG, }).length || (input.witnessUtxo && isP2TR(input.witnessUtxo.script))); } isMultisigTaprootScript(script) { try { (0, parseInput_1.parsePubScript2Of3)(script, 'taprootScriptPathSpend'); return true; } catch (e) { return false; } } /** * Mostly copied from bitcoinjs-lib/ts_src/psbt.ts */ finalizeAllInputs() { (0, utils_1.checkForInput)(this.data.inputs, 0); // making sure we have at least one this.data.inputs.map((input, idx) => { if (input.tapLeafScript?.length) { return this.isMultisigTaprootScript(input.tapLeafScript[0].script) ? this.finalizeTaprootInput(idx) : this.finalizeTapInputWithSingleLeafScriptAndSignature(idx); } else if (this.isTaprootKeyPathInput(idx)) { return this.finalizeTaprootMusig2Input(idx); } return this.finalizeInput(idx); }); return this; } finalizeTaprootInput(inputIndex) { const sanitizeSignature = (sig) => { const sighashType = sig.length === 64 ? __1.Transaction.SIGHASH_DEFAULT : sig.readUInt8(sig.length - 1); const inputSighashType = input.sighashType === undefined ? __1.Transaction.SIGHASH_DEFAULT : input.sighashType; (0, assert_1.ok)(sighashType === inputSighashType, 'signature sighash does not match input sighash type'); // TODO BTC-663 This should be fixed in platform. This is just a workaround fix. return sighashType === __1.Transaction.SIGHASH_DEFAULT && sig.length === 65 ? sig.slice(0, 64) : sig; }; const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); // witness = control-block script first-sig second-sig if (input.tapLeafScript?.length !== 1) { throw new Error('Only one leaf script supported for finalizing'); } const { controlBlock, script } = input.tapLeafScript[0]; const witness = [script, controlBlock]; const [pubkey1, pubkey2] = (0, parseInput_1.parsePubScript2Of3)(script, 'taprootScriptPathSpend').publicKeys; for (const pk of [pubkey1, pubkey2]) { const sig = input.tapScriptSig?.find(({ pubkey }) => equalPublicKeyIgnoreY(pk, pubkey)); if (!sig) { throw new Error('Could not find signatures in Script Sig.'); } witness.unshift(sanitizeSignature(sig.signature)); } const witnessLength = witness.reduce((s, b) => s + b.length + bufferutils_1.varuint.encodingLength(b.length), 1); const bufferWriter = bufferutils_1.BufferWriter.withCapacity(witnessLength); bufferWriter.writeVector(witness); const finalScriptWitness = bufferWriter.end(); this.data.updateInput(inputIndex, { finalScriptWitness }); this.data.clearFinalizedInput(inputIndex); return this; } /** * Finalizes a taproot musig2 input by aggregating all partial sigs. * IMPORTANT: Always call validate* function before finalizing. */ finalizeTaprootMusig2Input(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const partialSigs = (0, Musig2_1.parsePsbtMusig2PartialSigs)(input); if (partialSigs?.length !== 2) { throw new Error(`invalid number of partial signatures ${partialSigs ? partialSigs.length : 0} to finalize`); } const { partialSigs: pSigs, sigHashType } = (0, Musig2_1.getSigHashTypeFromSigs)(partialSigs); const { sessionKey } = this.getMusig2SessionKey(inputIndex, sigHashType); const aggSig = (0, Musig2_1.musig2AggregateSigs)(pSigs.map((pSig) => pSig.partialSig), sessionKey); const sig = sigHashType === __1.Transaction.SIGHASH_DEFAULT ? aggSig : Buffer.concat([aggSig, Buffer.of(sigHashType)]); // single signature with 64/65 bytes size is script witness for key path spend const bufferWriter = bufferutils_1.BufferWriter.withCapacity(1 + bufferutils_1.varuint.encodingLength(sig.length) + sig.length); bufferWriter.writeVector([sig]); const finalScriptWitness = bufferWriter.end(); this.data.updateInput(inputIndex, { finalScriptWitness }); this.data.clearFinalizedInput(inputIndex); // deleting only BitGo proprietary key values. this.deleteProprietaryKeyVals(inputIndex, { identifier: PsbtUtil_1.PSBT_PROPRIETARY_IDENTIFIER }); return this; } finalizeTapInputWithSingleLeafScriptAndSignature(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); if (input.tapLeafScript?.length !== 1) { throw new Error('Only one leaf script supported for finalizing'); } if (input.tapScriptSig?.length !== 1) { throw new Error('Could not find signatures in Script Sig.'); } const { controlBlock, script } = input.tapLeafScript[0]; const witness = [input.tapScriptSig[0].signature, script, controlBlock]; const witnessLength = witness.reduce((s, b) => s + b.length + bufferutils_1.varuint.encodingLength(b.length), 1); const bufferWriter = bufferutils_1.BufferWriter.withCapacity(witnessLength); bufferWriter.writeVector(witness); const finalScriptWitness = bufferWriter.end(); this.data.updateInput(inputIndex, { finalScriptWitness }); this.data.clearFinalizedInput(inputIndex); return this; } /** * Mostly copied from bitcoinjs-lib/ts_src/psbt.ts * * Unlike the function it overrides, this does not take a validator. In BitGo * context, we know how we want to validate so we just hard code the right * validator. */ validateSignaturesOfAllInputs() { (0, utils_1.checkForInput)(this.data.inputs, 0); // making sure we have at least one const results = this.data.inputs.map((input, idx) => { return this.validateSignaturesOfInputCommon(idx); }); return results.reduce((final, res) => res && final, true); } /** * @returns true iff any matching valid signature is found for a derived pub key from given HD key pair. */ validateSignaturesOfInputHD(inputIndex, hdKeyPair) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const pubKey = UtxoPsbt.deriveKeyPairForInput(hdKeyPair, input); if (!pubKey) { throw new Error('can not derive from HD key pair'); } return this.validateSignaturesOfInputCommon(inputIndex, pubKey); } /** * @returns true iff any valid signature(s) are found from bip32 data of PSBT or for given pub key. */ validateSignaturesOfInputCommon(inputIndex, pubkey) { try { if (this.isTaprootScriptPathInput(inputIndex)) { return this.validateTaprootSignaturesOfInput(inputIndex, pubkey); } else if (this.isTaprootKeyPathInput(inputIndex)) { return this.validateTaprootMusig2SignaturesOfInput(inputIndex, pubkey); } return this.validateSignaturesOfInput(inputIndex, (p, m, s) => __1.ecc.verify(m, p, s, true), pubkey); } catch (err) { // Not an elegant solution. Might need upstream changes like custom error types. if (err.message === 'No signatures for this pubkey') { return false; } throw err; } } getMusig2SessionKey(inputIndex, sigHashType) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); if (!input.tapInternalKey || !input.tapMerkleRoot) { throw new Error('both tapInternalKey and tapMerkleRoot are required'); } const participants = this.getMusig2Participants(inputIndex, input.tapInternalKey, input.tapMerkleRoot); const nonces = this.getMusig2Nonces(inputIndex, participants); const { hash } = this.getTaprootHashForSig(inputIndex, [sigHashType]); const sessionKey = (0, Musig2_1.createMusig2SigningSession)({ pubNonces: [nonces[0].pubNonce, nonces[1].pubNonce], pubKeys: participants.participantPubKeys, txHash: hash, internalPubKey: input.tapInternalKey, tapTreeRoot: input.tapMerkleRoot, }); return { participants, nonces, hash, sessionKey }; } /** * @returns true for following cases. * If valid musig2 partial signatures exists for both 2 keys, it will also verify aggregated sig * for aggregated tweaked key (output key), otherwise only verifies partial sig. * If pubkey is passed in input, it will check sig only for that pubkey, * if no sig exits for such key, throws error. * For invalid state of input data, it will throw errors. */ validateTaprootMusig2SignaturesOfInput(inputIndex, pubkey) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const partialSigs = (0, Musig2_1.parsePsbtMusig2PartialSigs)(input); if (!partialSigs) { throw new Error(`No signatures to validate`); } let myPartialSigs = partialSigs; if (pubkey) { myPartialSigs = partialSigs.filter((kv) => equalPublicKeyIgnoreY(kv.participantPubKey, pubkey)); if (myPartialSigs?.length < 1) { throw new Error('No signatures for this pubkey'); } } const { partialSigs: mySigs, sigHashType } = (0, Musig2_1.getSigHashTypeFromSigs)(myPartialSigs); const { participants, nonces, hash, sessionKey } = this.getMusig2SessionKey(inputIndex, sigHashType); const results = mySigs.map((mySig) => { const myNonce = nonces.find((kv) => equalPublicKeyIgnoreY(kv.participantPubKey, mySig.participantPubKey)); if (!myNonce) { throw new Error('Found no pub nonce for pubkey'); } return (0, Musig2_1.musig2PartialSigVerify)(mySig.partialSig, mySig.participantPubKey, myNonce.pubNonce, sessionKey); }); // For valid single sig or 1 or 2 failure sigs, no need to validate aggregated sig. So skip. const result = results.every((res) => res); if (!result || mySigs.length < 2) { return result; } const aggSig = (0, Musig2_1.musig2AggregateSigs)(mySigs.map((mySig) => mySig.partialSig), sessionKey); return __1.ecc.verifySchnorr(hash, participants.tapOutputKey, aggSig); } validateTaprootSignaturesOfInput(inputIndex, pubkey) { const input = this.data.inputs[inputIndex]; const tapSigs = (input || {}).tapScriptSig; if (!input || !tapSigs || tapSigs.length < 1) { throw new Error('No signatures to validate'); } let mySigs; if (pubkey) { mySigs = tapSigs.filter((sig) => equalPublicKeyIgnoreY(sig.pubkey, pubkey)); if (mySigs.length < 1) { throw new Error('No signatures for this pubkey'); } } else { mySigs = tapSigs; } const results = []; (0, assert_1.ok)(input.tapLeafScript?.length === 1, `single tapLeafScript is expected. Got ${input.tapLeafScript?.length}`); const [tapLeafScript] = input.tapLeafScript; const pubKeys = this.isMultisigTaprootScript(tapLeafScript.script) ? (0, parseInput_1.parsePubScript2Of3)(tapLeafScript.script, 'taprootScriptPathSpend').publicKeys : undefined; for (const pSig of mySigs) { const { signature, leafHash, pubkey } = pSig; if (pubKeys) { (0, assert_1.ok)(pubKeys.find((pk) => pubkey.equals(pk)), 'public key not found in tap leaf script'); } let sigHashType; let sig; if (signature.length === 65) { sigHashType = signature[64]; sig = signature.slice(0, 64); } else { sigHashType = __1.Transaction.SIGHASH_DEFAULT; sig = signature; } const { hash } = this.getTaprootHashForSig(inputIndex, [sigHashType], leafHash); results.push(__1.ecc.verifySchnorr(hash, pubkey, sig)); } return results.every((res) => res); } /** * @param inputIndex * @param rootNodes optional input root bip32 nodes to verify with. If it is not provided, globalXpub will be used. * @return array of boolean values. True when corresponding index in `publicKeys` has signed the transaction. * If no signature in the tx or no public key matching signature, the validation is considered as false. * If rootNodes are not explicitly passed in, the return array will be unordered. * Use getSortedRootNodes() instead if ordering is important. */ getSignatureValidationArray(inputIndex, { rootNodes }) { if (!rootNodes && (!this.data.globalMap.globalXpub?.length || !(0, types_1.isTriple)(this.data.globalMap.globalXpub))) { throw new Error('Cannot get signature validation array without 3 global xpubs'); } const bip32s = rootNodes ? rootNodes : this.data.globalMap.globalXpub?.map((xpub) => (0, secp256k1_1.BIP32Factory)(__1.ecc).fromBase58(bs58check.encode(xpub.extendedPubkey))); if (!bip32s) { throw new Error('either globalMap or rootNodes is required'); } const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); if (!(0, PsbtUtil_1.getPsbtInputSignatureCount)(input)) { return [false, false, false]; } return bip32s.map((bip32) => { const pubKey = UtxoPsbt.deriveKeyPairForInput(bip32, input); if (!pubKey) { return false; } try { return this.validateSignaturesOfInputCommon(inputIndex, pubKey); } catch (err) { // Not an elegant solution. Might need upstream changes like custom error types. if (err.message === 'No signatures for this pubkey') { return false; } throw err; } }); } /** * Mostly copied from bitcoinjs-lib/ts_src/psbt.ts */ signAllInputsHD(hdKeyPair, params) { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } const { sighashTypes, deterministic } = toSignatureParams(this.network, params); const results = []; for (let i = 0; i < this.data.inputs.length; i++) { try { this.signInputHD(i, hdKeyPair, { sighashTypes, deterministic }); results.push(true); } catch (err) { results.push(false); } } if (results.every((v) => !v)) { throw new Error('No inputs were signed'); } return this; } /** * Mostly copied from bitcoinjs-lib/ts_src/psbt.ts:signInputHD */ signTaprootInputHD(inputIndex, hdKeyPair, { sighashTypes = [__1.Transaction.SIGHASH_DEFAULT, __1.Transaction.SIGHASH_ALL], deterministic = false } = {}) { if (!this.isTaprootInput(inputIndex)) { throw new Error('not a taproot input'); } if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); if (!input.tapBip32Derivation || input.tapBip32Derivation.length === 0) { throw new Error('Need tapBip32Derivation to sign Taproot with HD'); } const myDerivations = input.tapBip32Derivation .map((bipDv) => { if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { return bipDv; } }) .filter((v) => !!v); if (myDerivations.length === 0) { throw new Error('Need one tapBip32Derivation masterFingerprint to match the HDSigner fingerprint'); } function getDerivedNode(bipDv) { const node = hdKeyPair.derivePath(bipDv.path); if (!equalPublicKeyIgnoreY(bipDv.pubkey, node.publicKey)) { throw new Error('pubkey did not match tapBip32Derivation'); } return node; } if (input.tapLeafScript?.length) { const signers = myDerivations.map((bipDv) => { const signer = getDerivedNode(bipDv); if (!('signSchnorr' in signer)) { throw new Error('signSchnorr function is required to sign p2tr'); } return { signer, leafHashes: bipDv.leafHashes }; }); signers.forEach(({ signer, leafHashes }) => this.signTaprootInput(inputIndex, signer, leafHashes, sighashTypes)); } else if (input.tapInternalKey?.length) { const signers = myDerivations.map((bipDv) => { const signer = getDerivedNode(bipDv); if (!('privateKey' in signer) || !signer.privateKey) { throw new Error('privateKey is required to sign p2tr musig2'); } return signer; }); signers.forEach((signer) => this.signTaprootMusig2Input(inputIndex, signer, { sighashTypes, deterministic })); } return this; } signInput(inputIndex, keyPair, sighashTypes) { const { sighashTypes: sighashForNetwork } = toSignatureParams(this.network, sighashTypes); return super.signInput(inputIndex, keyPair, sighashForNetwork); } signInputHD(inputIndex, hdKeyPair, params) { const { sighashTypes, deterministic } = toSignatureParams(this.network, params); if (this.isTaprootInput(inputIndex)) { return this.signTaprootInputHD(inputIndex, hdKeyPair, { sighashTypes, deterministic }); } else { return super.signInputHD(inputIndex, hdKeyPair, sighashTypes); } } getMusig2Participants(inputIndex, tapInternalKey, tapMerkleRoot) { const participantsKeyValData = (0, Musig2_1.parsePsbtMusig2Participants)(this.data.inputs[inputIndex]); if (!participantsKeyValData) { throw new Error(`Found 0 matching participant key value instead of 1`); } (0, Musig2_1.assertPsbtMusig2Participants)(participantsKeyValData, tapInternalKey, tapMerkleRoot); return participantsKeyValData; } getMusig2Nonces(inputIndex, participantsKeyValData) { const noncesKeyValsData = (0, Musig2_1.parsePsbtMusig2Nonces)(this.data.inputs[inputIndex]); if (!noncesKeyValsData || !(0, types_1.isTuple)(noncesKeyValsData)) { throw new Error(`Found ${noncesKeyValsData?.length ? noncesKeyValsData.length : 0} matching nonce key value instead of 2`); } (0, Musig2_1.assertPsbtMusig2Nonces)(noncesKeyValsData, participantsKeyValData); return noncesKeyValsData; } /** * Signs p2tr musig2 key path input with 2 aggregated keys. * * Note: Only can sign deterministically as the cosigner * @param inputIndex * @param signer - XY public key and private key are required * @param sighashTypes * @param deterministic If true, sign the musig input deterministically */ signTaprootMusig2Input(inputIndex, signer, { sighashTypes = [__1.Transaction.SIGHASH_DEFAULT, __1.Transaction.SIGHASH_ALL], deterministic = false } = {}) { if (!this.isTaprootKeyPathInput(inputIndex)) { throw new Error('not a taproot musig2 input'); } const input = this.data.inputs[inputIndex]; if (!input.tapInternalKey || !input.tapMerkleRoot) { throw new Error('missing required input data'); } // Retrieve and check that we have two participant nonces const participants = this.getMusig2Participants(inputIndex, input.tapInternalKey, input.tapMerkleRoot); const { tapOutputKey, participantPubKeys } = participants; const signerPubKey = participantPubKeys.find((pubKey) => equalPublicKeyIgnoreY(pubKey, signer.publicKey)); if (!signerPubKey) { throw new Error('signer pub key should match one of participant pub keys'); } const nonces = this.getMusig2Nonces(inputIndex, participants); const { hash, sighashType } = this.getTaprootHashForSig(inputIndex, sighashTypes); let partialSig; if (deterministic) { if (!equalPublicKeyIgnoreY(signerPubKey, participantPubKeys[1])) { throw new Error('can only add a deterministic signature on the cosigner'); } const firstSignerNonce = nonces.find((n) => equalPublicKeyIgnoreY(n.participantPubKey, participantPubKeys[0])); if (!firstSignerNonce) { throw new Error('could not find the user nonce'); } partialSig = (0, Musig2_1.musig2DeterministicSign)({ privateKey: signer.privateKey, otherNonce: firstSignerNonce.pubNonce, publicKeys: participantPubKeys, internalPubKey: input.tapInternalKey, tapTreeRoot: input.tapMerkleRoot, hash, }).sig; } else { const sessionKey = (0, Musig2_1.createMusig2SigningSession)({ pubNonces: [nonces[0].pubNonce, nonces[1].pubNonce], pubKeys: participantPubKeys, txHash: hash, internalPubKey: input.tapInternalKey, tapTreeRoot: input.tapMerkleRoot, }); const signerNonce = nonces.find((kv) => equalPublicKeyIgnoreY(kv.participantPubKey, signerPubKey)); if (!signerNonce) { throw new Error('pubNonce is missing. retry signing process'); } partialSig = (0, Musig2_1.musig2PartialSign)(signer.privateKey, signerNonce.pubNonce, sessionKey, this.nonceStore); } if (sighashType !== __1.Transaction.SIGHASH_DEFAULT) { partialSig = Buffer.concat([partialSig, Buffer.of(sighashType)]); } const sig = (0, Musig2_1.encodePsbtMusig2PartialSig)({ participantPubKey: signerPubKey, tapOutputKey, partialSig: partialSig, }); this.addProprietaryKeyValToInput(inputIndex, sig); return this; } signTaprootInput(inputIndex, signer, leafHashes, sighashTypes = [__1.Transaction.SIGHASH_DEFAULT, __1.Transaction.SIGHASH_ALL]) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); // Figure out if this is script path or not, if not, tweak the private key if (!input.tapLeafScript?.length) { throw new Error('tapLeafScript is required for p2tr script path'); } const pubkey = (0, outputScripts_1.toXOnlyPublicKey)(signer.publicKey); if (input.tapLeafScript.length !== 1) { throw new Error('Only one leaf script supported for signing'); } const [tapLeafScript] = input.tapLeafScript; if (this.isMultisigTaprootScript(tapLeafScript.script)) { const pubKeys = (0, parseInput_1.parsePubScript2Of3)(tapLeafScript.script, 'taprootScriptPathSpend').publicKeys; (0, assert_1.ok)(pubKeys.find((pk) => pubkey.equals(pk)), 'public key not found in tap leaf script'); } const parsedControlBlock = __1.taproot.parseControlBlock(__1.ecc, tapLeafScript.controlBlock); const { leafVersion } = parsedControlBlock; if (leafVersion !== tapLeafScript.leafVersion) { throw new Error('Tap script leaf version mismatch with control block'); } const leafHash = __1.taproot.getTapleafHash(__1.ecc, parsedControlBlock, tapLeafScript.script); if (!leafHashes.find((l) => l.equals(leafHash))) { throw new Error(`Signer cannot sign for leaf hash ${leafHash.toString('hex')}`); } const { hash, sighashType } = this.getTaprootHashForSig(inputIndex, sighashTypes, leafHash); let signature = signer.signSchnorr(hash); if (sighashType !== __1.Transaction.SIGHASH_DEFAULT) { signature = Buffer.concat([signature, Buffer.of(sighashType)]); } this.data.updateInput(inputIndex, { tapScriptSig: [ { pubkey, signature, leafHash, }, ], }); return this; } getTaprootOutputScript(inputIndex) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); if (input.tapLeafScript?.length) { return __1.taproot.createTaprootOutputScript({ controlBlock: input.tapLeafScript[0].controlBlock, leafScript: input.tapLeafScript[0].script, }); } else if (input.tapInternalKey && input.tapMerkleRoot) { return __1.taproot.createTaprootOutputScript({ internalPubKey: input.tapInternalKey, taptreeRoot: input.tapMerkleRoot, }); } throw new Error('not a taproot input'); } getTaprootHashForSig(inputIndex, sighashTypes, leafHash) { if (!this.isTaprootInput(inputIndex)) { throw new Error('not a taproot input'); } const sighashType = this.data.inputs[inputIndex].sighashType || __1.Transaction.SIGHASH_DEFAULT; if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { throw new Error(`Sighash type is not allowed. Retry the sign method passing the ` + `sighashTypes array of whitelisted types. Sighash type: ${sighashType}`); } const txInputs = this.txInputs; // These are somewhat costly to extract const prevoutScripts = []; const prevoutValues = []; this.data.inputs.forEach((input, i) => { let prevout; if (input.nonWitnessUtxo) { // TODO: This could be costly, either cache it here, or find a way to share with super const nonWitnessUtxoTx = this.constructor.transactionFromBuffer(input.nonWitnessUtxo, this.tx.network); const prevoutHash = txInputs[i].hash; const utxoHash = nonWitnessUtxoTx.getHash(); // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout if (!prevoutHash.equals(utxoHash)) { throw new Error(`Non-witness UTXO hash for input #${i} doesn't match the hash specified in the prevout`); } const prevoutIndex = txInputs[i].index; prevout = nonWitnessUtxoTx.outs[prevoutIndex]; } else if (input.witnessUtxo) { prevout = input.witnessUtxo; } else { throw new Error('Need a Utxo input item for signing'); } prevoutScripts.push(prevout.script); prevoutValues.push(prevout.value); }); const outputScript = this.getTaprootOutputScript(inputIndex); if (!outputScript.equals(prevoutScripts[inputIndex])) { throw new Error(`Witness script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`); } const hash = this.tx.hashForWitnessV1(inputIndex, prevoutScripts, prevoutValues, sighashType, leafHash); return { hash, sighashType }; } /** * @deprecated Please use the new method addProprietaryKeyVals(psbt, entry, index, keyValueData) * * Adds proprietary key value pair to PSBT input. * Default identifierEncoding is utf-8 for identifier. */ addProprietaryKeyValToInput(inputIndex, keyValueData) { return this.addUnknownKeyValToInput(inputIndex, { key: (0, proprietaryKeyVal_1.encodeProprietaryKey)(keyValueData.key), value: keyValueData.value, }); } /** * @deprecated Please use the new method addProprietaryKeyValuesToUnknownKeyValues(psbt, entry, index, keyValueData) * or updateProprietaryKeyValuesToUnknownKeyValues(keyValueData) * * Adds or updates (if exists) proprietary key value pair to PSBT input. * Default identifierEncoding is utf-8 for identifier. */ addOrUpdateProprietaryKeyValToInput(inputIndex, keyValueData) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const key = (0, proprietaryKeyVal_1.encodeProprietaryKey)(keyValueData.key); const { value } = keyValueData; if (input.unknownKeyVals?.length) { const ukvIndex = input.unknownKeyVals.findIndex((ukv) => ukv.key.equals(key)); if (ukvIndex > -1) { input.unknownKeyVals[ukvIndex] = { key, value }; return this; } } this.addUnknownKeyValToInput(inputIndex, { key, value, }); return this; } /** * @deprecated Please use getProprietaryKeyValuesFromUnknownKeyValues(psbtField, keySearch). The new method * allows for the retrieval of either input or output proprietary key values. * * To search any data from proprietary key value against keydata in the inputs. * Default identifierEncoding is utf-8 for identifier. */ getProprietaryKeyVals(inputIndex, keySearch) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); return (0, PsbtUtil_1.getPsbtInputProprietaryKeyVals)(input, keySearch); } /** * @deprecated Please use deleteProprietaryKeyValues(type, index, keysToDelete?). The new method * allows for the deletion of either input or output proprietary key values. * * To delete any data from proprietary key value in PSBT input. * Default identifierEncoding is utf-8 for identifier. */ deleteProprietaryKeyVals(inputIndex, keysToDelete) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); if (!input.unknownKeyVals?.length) { return this; } if (keysToDelete && keysToDelete.subtype === undefined && Buffer.isBuffer(keysToDelete.keydata)) { throw new Error('invalid proprietary key search filter combination. subtype is required'); } input.unknownKeyVals = input.unknownKeyVals.filter((keyValue, i) => { const key = (0, proprietaryKeyVal_1.decodeProprietaryKey)(keyValue.key); return !(keysToDelete === undefined || (keysToDelete.identifier === key.identifier && (keysToDelete.subtype === undefined || (keysToDelete.subtype === key.subtype && (!Buffer.isBuffer(keysToDelete.keydata) || keysToDelete.keydata.equals(key.keydata)))))); }); return this; } createMusig2NonceForInput(inputIndex, keyPair, keyType, params = { deterministic: false }) { const input = this.data.inputs[inputIndex]; if (!input.tapInternalKey) { throw new Error('tapInternalKey is required to create nonce'); } if (!input.tapMerkleRoot) { throw new Error('tapMerkleRoot is required to create nonce'); } const getDerivedKeyPair = () => { if (!input.tapBip32Derivation?.length) { throw new Error('tapBip32Derivation is required to create nonce'); } const derived = UtxoPsbt.deriveKeyPair(keyPair, input.tapBip32Derivation, { ignoreY: true }); if (!derived) { throw new Error('No bip32Derivation masterFingerprint matched the HD keyPair fingerprint'); } return derived; }; const derivedKeyPair = keyType === 'root' ? getDerivedKeyPair() : keyPair; if (!derivedKeyPair.privateKey) { throw new Error('privateKey is required to create nonce'); } const participants = (0, Musig2_1.parsePsbtMusig2Participants)(input); if (!participants) { throw new Error(`Found 0 matching participant key value instead of 1`); } (0, Musig2_1.assertPsbtMusig2Participants)(participants, input.tapInternalKey, input.tapMerkleRoot); const { tapOutputKey, participantPubKeys } = participants; const participantPubKey = participantPubKeys.find((pubKey) => equalPublicKeyIgnoreY(pubKey, derivedKeyPair.publicKey)); if (!Buffer.isBuffer(participantPubKey)) { throw new Error('participant plain pub key should match one bip32Derivation plain pub key'); } const { hash } = this.getTaprootHashForSig(inputIndex); let pubNonce; if (params.deterministic) { if (params.sessionId) { throw new Error('Cannot add extra entropy when generating a deterministic nonce'); } // There must be only 2 participant pubKeys if it got to this point if (!equalPublicKeyIgnoreY(participantPubKey, participantPubKeys[1])) { throw new Error(`Only the cosigner's nonce can be set deterministically`); } const nonces = (0, Musig2_1.parsePsbtMusig2Nonces)(input); if (!nonces) { throw new Error(`No nonces found on input #${inputIndex}`); } if (nonces.length > 2) { throw new Error(`Cannot have more than 2 nonces`); } const firstSignerNonce = nonces.find((kv) => equalPublicKeyIgnoreY(kv.participantPubKey, participantPubKeys[0])); if (!firstSignerNonce) { throw new Error('signer nonce must be set if cosigner nonce is to be derived deterministically'); } pubNonce = (0, Musig2_1.createMusig2DeterministicNonce)({ privateKey: derivedKeyPair.privateKey, otherNonce: firstSignerNonce.pubNonce, publicKeys: participantPubKeys, internalPubKey: input.tapInternalKey, tapTreeRoot: input.tapMerkleRoot, hash, }); } else { pubNonce = Buffer.from(this.nonceStore.createMusig2Nonce(derivedKeyPair.privateKey, participantPubKey, tapOutputKey, hash, params.sessionId)); } return { tapOutputKey, participantPubKey, pubNonce }; } setMusig2NoncesInner(keyPair, keyType, inputIndex, params = { deterministic: false }) { if (keyPair.isNeutered()) { throw new Error('private key is required to generate nonce'); } if (Buffer.isBuffer(params.sessionId) && params.sessionId.length !== 32) { throw new Error(`Invalid sessionId size ${params.sessionId.length}`); } const inputIndexes = inputIndex === undefined ? [...Array(this.inputCount).keys()] : [inputIndex]; inputIndexes.forEach((index) => { if (!this.isTaprootKeyPathInput(index)) { return; } const nonce = this.createMusig2NonceForInput(index, keyPair, keyType, params); this.addOrUpdateProprietaryKeyValToInput(index, (0, Musig2_1.encodePsbtMusig2PubNonce)(nonce)); }); return this; } /** * Generates and sets MuSig2 nonce to taproot key path input at inputIndex. * If input is not a taproot key path, no action. * * @param inputIndex input index * @param keyPair derived key pair * @param sessionId Optional extra entropy. If provided it must either be a counter unique to this secret key, * (converted to an array of 32 bytes), or 32 uniformly random bytes. * @param deterministic If true, set the cosigner nonce deterministically */ setInputMusig2Nonce(inputIndex, derivedKeyPair, params = { deterministic: false }) { return this.setMusig2NoncesInner(derivedKeyPair, 'derived', inputIndex, params); } /** * Generates and sets MuSig2 nonce to taproot key path input at inputIndex. * If input is not a taproot key path, no action. * * @param inputIndex input index * @param keyPair HD root key pair * @param sessionId Optional extra entropy. If provided it must either be a counter unique to this secret key, * (converted to an array of 32 bytes), or 32 uniformly random bytes. * @param deterministic If true, set the cosigner nonce deterministically */ setInputMusig2NonceHD(inputIndex, keyPair, params = { deterministic: false }) { (0, utils_1.checkForInput)(this.data.inputs, inputIndex); return this.setMusig2NoncesInner(keyPair, 'root', inputIndex, params); } /** * Generates and sets MuSig2 nonce to all taproot key path inputs. Other inputs will be skipped. * * @param inputIndex input index * @param keyPair derived key pair * @param sessionId Optional extra entropy. If provided it must either be a counter unique to this secret key, * (converted to an array of 32 bytes), or 32 uniformly random bytes. */ setAllInputsMusig2Nonce(keyPair, params = { deterministic: false }) { return this.setMusig2NoncesInner(keyPair, 'derived', undefined, params); } /** * Generates and sets MuSig2 nonce to all taproot key path inputs. Other inputs will be skipped. * * @param inputIndex input index * @param keyPair HD root key pair * @param sessionId Optional extra entropy. If provided it must either be a counter unique to this secret key, * (converted to an array of 32 bytes), or 32 uniformly random bytes. */ setAllInputsMusig2NonceHD(keyPair, params = { deterministic: false }) { return this.setMusig2NoncesInner(keyPair, 'root', undefined, params); } clone() { return UtxoPsbt.fro