From b8ec94627eb636a40fcf40e88100fe09f6b310fd Mon Sep 17 00:00:00 2001 From: Saravanan Mani Date: Fri, 9 Dec 2022 17:51:01 +0530 Subject: [PATCH] feat(psbt): enable psbt finalizer to finalize partially signed psbt tx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently PSBT doesn’t support to extract partially signed tx from psbt This change enables to do that in a hacky way and against PSBT spec. Test suite is to be done. Ex use case - one of the cosigner doesn’t support psbt. TICKET: BG-58455 --- src/payments/index.d.ts | 1 + src/payments/p2ms.js | 30 ++++++++++++++++++-- src/psbt.d.ts | 5 ++-- src/psbt.js | 61 ++++++++++++++++++++++++++++++++-------- ts_src/payments/index.ts | 1 + ts_src/payments/p2ms.ts | 30 ++++++++++++++++++-- ts_src/psbt.ts | 46 +++++++++++++++++++++++++----- 7 files changed, 150 insertions(+), 24 deletions(-) diff --git a/src/payments/index.d.ts b/src/payments/index.d.ts index c2c619834..28e2bd76a 100644 --- a/src/payments/index.d.ts +++ b/src/payments/index.d.ts @@ -41,6 +41,7 @@ export declare type PaymentFunction = () => Payment; export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; + minRequiredSigCount?: number; eccLib?: TinySecp256k1Interface; } export declare type StackElement = Buffer | number; diff --git a/src/payments/p2ms.js b/src/payments/p2ms.js index 0b7e72d9a..8cf329628 100644 --- a/src/payments/p2ms.js +++ b/src/payments/p2ms.js @@ -23,6 +23,12 @@ function p2ms(a, opts) { !a.signatures ) throw new TypeError('Not enough data'); + const minRequiredSigCount = + opts !== undefined ? opts.minRequiredSigCount : undefined; + if (minRequiredSigCount !== undefined && minRequiredSigCount < 1) + throw new Error( + `minRequiredSigCount=${minRequiredSigCount} is less than minimum value 1`, + ); opts = Object.assign({ validate: true }, opts || {}); function isAcceptableSignature(x) { return ( @@ -127,7 +133,15 @@ function p2ms(a, opts) { if (o.n < o.m) throw new TypeError('Pubkey count cannot be less than m'); } if (a.signatures) { - if (a.signatures.length < o.m) + let m = o.m; + if (o.m !== undefined && minRequiredSigCount !== undefined) { + if (o.m < minRequiredSigCount) + throw new TypeError( + `minRequiredSigCount=${minRequiredSigCount} is more than m=${o.m}`, + ); + m = minRequiredSigCount; + } + if (a.signatures.length < m) throw new TypeError('Not enough signatures provided'); if (a.signatures.length > o.m) throw new TypeError('Too many signatures provided'); @@ -141,7 +155,19 @@ function p2ms(a, opts) { throw new TypeError('Input has invalid signature(s)'); if (a.signatures && !stacksEqual(a.signatures, o.signatures)) throw new TypeError('Signature mismatch'); - if (a.m !== undefined && a.m !== a.signatures.length) + if ( + a.m !== undefined && + minRequiredSigCount !== undefined && + a.m < minRequiredSigCount + ) + throw new TypeError( + `minRequiredSigCount=${minRequiredSigCount} is more than m=${a.m}`, + ); + if ( + minRequiredSigCount === undefined && + a.m !== undefined && + a.m !== a.signatures.length + ) throw new TypeError('Signature count mismatch'); } } diff --git a/src/psbt.d.ts b/src/psbt.d.ts index cb3bda457..c88587e05 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -81,7 +81,7 @@ export declare class Psbt { getFeeRate(): number; getFee(): bigint; finalizeAllInputs(): this; - finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; + finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc, minRequiredSigCount?: number): this; getInputType(inputIndex: number): AllScriptType; inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean; inputHasHDKey(inputIndex: number, root: HDSigner): boolean; @@ -193,7 +193,8 @@ input: PsbtInput, // The PSBT input contents script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.) isSegwit: boolean, // Is it segwit? isP2SH: boolean, // Is it P2SH? -isP2WSH: boolean) => { +isP2WSH: boolean, // Is it P2WSH? +minRequiredSigCount?: number) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; }; diff --git a/src/psbt.js b/src/psbt.js index 280b0c3f6..7f2833305 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -273,7 +273,11 @@ class Psbt { range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } - finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) { + finalizeInput( + inputIndex, + finalScriptsFunc = getFinalScripts, + minRequiredSigCount, + ) { const input = (0, utils_1.checkForInput)(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, @@ -289,6 +293,7 @@ class Psbt { isSegwit, isP2SH, isP2WSH, + minRequiredSigCount, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); if (finalScriptWitness) @@ -678,7 +683,7 @@ class PsbtTransaction { } } exports.PsbtTransaction = PsbtTransaction; -function canFinalize(input, script, scriptType) { +function canFinalize(input, script, scriptType, minRequiredSigCount) { switch (scriptType) { case 'pubkey': case 'pubkeyhash': @@ -686,7 +691,17 @@ function canFinalize(input, script, scriptType) { return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m, input.partialSig, p2ms.pubkeys); + let m = p2ms.m; + if (p2ms.m !== undefined && minRequiredSigCount !== undefined) { + if (p2ms.m < minRequiredSigCount) + throw new Error( + `minRequiredSigCount=${minRequiredSigCount} is more than m=${ + p2ms.m + }`, + ); + m = minRequiredSigCount; + } + return hasSigs(m, input.partialSig, p2ms.pubkeys); default: return false; } @@ -876,9 +891,21 @@ function getTxCacheValue(key, name, inputs, c) { if (key === '__FEE_RATE') return c.__FEE_RATE; else if (key === '__FEE') return c.__FEE; } -function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { +function getFinalScripts( + inputIndex, + input, + script, + isSegwit, + isP2SH, + isP2WSH, + minRequiredSigCount, +) { + if (minRequiredSigCount !== undefined && minRequiredSigCount < 1) + throw new Error( + `minRequiredSigCount=${minRequiredSigCount} is less than minimum value 1`, + ); const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) + if (!canFinalize(input, script, scriptType, minRequiredSigCount)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( script, @@ -887,6 +914,7 @@ function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { isSegwit, isP2SH, isP2WSH, + minRequiredSigCount, ); } function prepareFinalScripts( @@ -896,11 +924,17 @@ function prepareFinalScripts( isSegwit, isP2SH, isP2WSH, + minRequiredSigCount, ) { let finalScriptSig; let finalScriptWitness; // Wow, the payments API is very handy - const payment = getPayment(script, scriptType, partialSig); + const payment = getPayment( + script, + scriptType, + partialSig, + minRequiredSigCount, + ); const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); if (isSegwit) { @@ -1036,15 +1070,20 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { hash, }; } -function getPayment(script, scriptType, partialSig) { +function getPayment(script, scriptType, partialSig, minRequiredSigCount) { let payment; switch (scriptType) { case 'multisig': const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); + payment = payments.p2ms( + { + output: script, + signatures: sigs, + }, + { + minRequiredSigCount, + }, + ); break; case 'pubkey': payment = payments.p2pk({ diff --git a/ts_src/payments/index.ts b/ts_src/payments/index.ts index 2f0d4563a..058cd1479 100644 --- a/ts_src/payments/index.ts +++ b/ts_src/payments/index.ts @@ -44,6 +44,7 @@ export type PaymentFunction = () => Payment; export interface PaymentOpts { validate?: boolean; allowIncomplete?: boolean; + minRequiredSigCount?: number; eccLib?: TinySecp256k1Interface; } diff --git a/ts_src/payments/p2ms.ts b/ts_src/payments/p2ms.ts index eaa144069..a628e238f 100644 --- a/ts_src/payments/p2ms.ts +++ b/ts_src/payments/p2ms.ts @@ -25,6 +25,12 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { !a.signatures ) throw new TypeError('Not enough data'); + const minRequiredSigCount = + opts !== undefined ? opts.minRequiredSigCount : undefined; + if (minRequiredSigCount !== undefined && minRequiredSigCount < 1) + throw new Error( + `minRequiredSigCount=${minRequiredSigCount} is less than minimum value 1`, + ); opts = Object.assign({ validate: true }, opts || {}); function isAcceptableSignature(x: Buffer | number): boolean { @@ -136,7 +142,15 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { } if (a.signatures) { - if (a.signatures.length < o.m!) + let m = o.m!; + if (o.m !== undefined && minRequiredSigCount !== undefined) { + if (o.m < minRequiredSigCount) + throw new TypeError( + `minRequiredSigCount=${minRequiredSigCount} is more than m=${o.m}`, + ); + m = minRequiredSigCount; + } + if (a.signatures.length < m) throw new TypeError('Not enough signatures provided'); if (a.signatures.length > o.m!) throw new TypeError('Too many signatures provided'); @@ -152,7 +166,19 @@ export function p2ms(a: Payment, opts?: PaymentOpts): Payment { if (a.signatures && !stacksEqual(a.signatures, o.signatures!)) throw new TypeError('Signature mismatch'); - if (a.m !== undefined && a.m !== a.signatures!.length) + if ( + a.m !== undefined && + minRequiredSigCount !== undefined && + a.m < minRequiredSigCount + ) + throw new TypeError( + `minRequiredSigCount=${minRequiredSigCount} is more than m=${a.m}`, + ); + if ( + minRequiredSigCount === undefined && + a.m !== undefined && + a.m !== a.signatures!.length + ) throw new TypeError('Signature count mismatch'); } } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 1a27d9162..de3c1751d 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -360,6 +360,7 @@ export class Psbt { finalizeInput( inputIndex: number, finalScriptsFunc: FinalScriptsFunc = getFinalScripts, + minRequiredSigCount?: number, ): this { const input = checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( @@ -378,6 +379,7 @@ export class Psbt { isSegwit, isP2SH, isP2WSH, + minRequiredSigCount, ); if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); @@ -917,6 +919,7 @@ function canFinalize( input: PsbtInput, script: Buffer, scriptType: string, + minRequiredSigCount?: number, ): boolean { switch (scriptType) { case 'pubkey': @@ -925,7 +928,17 @@ function canFinalize( return hasSigs(1, input.partialSig); case 'multisig': const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m!, input.partialSig, p2ms.pubkeys); + let m = p2ms.m!; + if (p2ms.m !== undefined && minRequiredSigCount !== undefined) { + if (p2ms.m < minRequiredSigCount) + throw new Error( + `minRequiredSigCount=${minRequiredSigCount} is more than m=${ + p2ms.m + }`, + ); + m = minRequiredSigCount; + } + return hasSigs(m, input.partialSig, p2ms.pubkeys); default: return false; } @@ -1169,6 +1182,7 @@ type FinalScriptsFunc = ( isSegwit: boolean, // Is it segwit? isP2SH: boolean, // Is it P2SH? isP2WSH: boolean, // Is it P2WSH? + minRequiredSigCount?: number, ) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; @@ -1181,12 +1195,17 @@ function getFinalScripts( isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean, + minRequiredSigCount?: number, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; } { + if (minRequiredSigCount !== undefined && minRequiredSigCount < 1) + throw new Error( + `minRequiredSigCount=${minRequiredSigCount} is less than minimum value 1`, + ); const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) + if (!canFinalize(input, script, scriptType, minRequiredSigCount)) throw new Error(`Can not finalize input #${inputIndex}`); return prepareFinalScripts( script, @@ -1195,6 +1214,7 @@ function getFinalScripts( isSegwit, isP2SH, isP2WSH, + minRequiredSigCount, ); } @@ -1205,6 +1225,7 @@ function prepareFinalScripts( isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean, + minRequiredSigCount?: number, ): { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; @@ -1213,7 +1234,12 @@ function prepareFinalScripts( let finalScriptWitness: Buffer | undefined; // Wow, the payments API is very handy - const payment: payments.Payment = getPayment(script, scriptType, partialSig); + const payment: payments.Payment = getPayment( + script, + scriptType, + partialSig, + minRequiredSigCount, + ); const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); @@ -1376,15 +1402,21 @@ function getPayment( script: Buffer, scriptType: string, partialSig: PartialSig[], + minRequiredSigCount?: number, ): payments.Payment { let payment: payments.Payment; switch (scriptType) { case 'multisig': const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); + payment = payments.p2ms( + { + output: script, + signatures: sigs, + }, + { + minRequiredSigCount, + }, + ); break; case 'pubkey': payment = payments.p2pk({