@bitgo/utxo-lib
Version:
Client-side Bitcoin JavaScript library
1,017 lines • 170 kB
JavaScript
"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