Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(psbt): enable psbt finalizer to finalize partially signed psbt tx #49

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/payments/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 28 additions & 2 deletions src/payments/p2ms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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');
Expand All @@ -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');
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/psbt.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
Expand Down
61 changes: 50 additions & 11 deletions src/psbt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -289,6 +293,7 @@ class Psbt {
isSegwit,
isP2SH,
isP2WSH,
minRequiredSigCount,
);
if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig });
if (finalScriptWitness)
Expand Down Expand Up @@ -678,15 +683,25 @@ class PsbtTransaction {
}
}
exports.PsbtTransaction = PsbtTransaction;
function canFinalize(input, script, scriptType) {
function canFinalize(input, script, scriptType, minRequiredSigCount) {
switch (scriptType) {
case 'pubkey':
case 'pubkeyhash':
case 'witnesspubkeyhash':
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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -887,6 +914,7 @@ function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) {
isSegwit,
isP2SH,
isP2WSH,
minRequiredSigCount,
);
}
function prepareFinalScripts(
Expand All @@ -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) {
Expand Down Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions ts_src/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type PaymentFunction = () => Payment;
export interface PaymentOpts {
validate?: boolean;
allowIncomplete?: boolean;
minRequiredSigCount?: number;
eccLib?: TinySecp256k1Interface;
}

Expand Down
30 changes: 28 additions & 2 deletions ts_src/payments/p2ms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
Expand All @@ -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');
}
}
Expand Down
46 changes: 39 additions & 7 deletions ts_src/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -378,6 +379,7 @@ export class Psbt {
isSegwit,
isP2SH,
isP2WSH,
minRequiredSigCount,
);

if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig });
Expand Down Expand Up @@ -917,6 +919,7 @@ function canFinalize(
input: PsbtInput,
script: Buffer,
scriptType: string,
minRequiredSigCount?: number,
): boolean {
switch (scriptType) {
case 'pubkey':
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -1195,6 +1214,7 @@ function getFinalScripts(
isSegwit,
isP2SH,
isP2WSH,
minRequiredSigCount,
);
}

Expand All @@ -1205,6 +1225,7 @@ function prepareFinalScripts(
isSegwit: boolean,
isP2SH: boolean,
isP2WSH: boolean,
minRequiredSigCount?: number,
): {
finalScriptSig: Buffer | undefined;
finalScriptWitness: Buffer | undefined;
Expand All @@ -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 });

Expand Down Expand Up @@ -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({
Expand Down