Skip to content

Commit f67aab3

Browse files
authored
Merge pull request #1491 from bitcoinjs/finalizerCallback
Allow custom implementations of finalizers
2 parents 32bc404 + f222447 commit f67aab3

File tree

4 files changed

+192
-44
lines changed

4 files changed

+192
-44
lines changed

src/psbt.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,22 +226,19 @@ class Psbt {
226226
range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx));
227227
return this;
228228
}
229-
finalizeInput(inputIndex) {
229+
finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) {
230230
const input = utils_1.checkForInput(this.data.inputs, inputIndex);
231231
const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput(
232232
inputIndex,
233233
input,
234234
this.__CACHE,
235235
);
236236
if (!script) throw new Error(`No script found for input #${inputIndex}`);
237-
const scriptType = classifyScript(script);
238-
if (!canFinalize(input, script, scriptType))
239-
throw new Error(`Can not finalize input #${inputIndex}`);
240237
checkPartialSigSighashes(input);
241-
const { finalScriptSig, finalScriptWitness } = getFinalScripts(
238+
const { finalScriptSig, finalScriptWitness } = finalScriptsFunc(
239+
inputIndex,
240+
input,
242241
script,
243-
scriptType,
244-
input.partialSig,
245242
isSegwit,
246243
isP2SH,
247244
isP2WSH,
@@ -772,7 +769,20 @@ function getTxCacheValue(key, name, inputs, c) {
772769
if (key === '__FEE_RATE') return c.__FEE_RATE;
773770
else if (key === '__FEE') return c.__FEE;
774771
}
775-
function getFinalScripts(
772+
function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) {
773+
const scriptType = classifyScript(script);
774+
if (!canFinalize(input, script, scriptType))
775+
throw new Error(`Can not finalize input #${inputIndex}`);
776+
return prepareFinalScripts(
777+
script,
778+
scriptType,
779+
input.partialSig,
780+
isSegwit,
781+
isP2SH,
782+
isP2WSH,
783+
);
784+
}
785+
function prepareFinalScripts(
776786
script,
777787
scriptType,
778788
partialSig,

test/integration/csv.spec.ts

Lines changed: 109 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as assert from 'assert';
2+
import { PsbtInput } from 'bip174/src/lib/interfaces';
23
import { before, describe, it } from 'mocha';
34
import * as bitcoin from '../..';
45
import { regtestUtils } from './_regtest';
56
const regtest = regtestUtils.network;
67
const bip68 = require('bip68');
8+
const varuint = require('varuint-bitcoin');
79

810
function toOutputScript(address: string): Buffer {
911
return bitcoin.address.toOutputScript(address, regtest);
@@ -129,33 +131,28 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
129131

130132
// fund the P2SH(CSV) address
131133
const unspent = await regtestUtils.faucet(p2sh.address!, 1e5);
132-
133-
const tx = new bitcoin.Transaction();
134-
tx.version = 2;
135-
tx.addInput(idToHash(unspent.txId), unspent.vout, sequence);
136-
tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4);
137-
138-
// {Alice's signature} OP_TRUE
139-
const signatureHash = tx.hashForSignature(
140-
0,
141-
p2sh.redeem!.output!,
142-
hashType,
143-
);
144-
const redeemScriptSig = bitcoin.payments.p2sh({
145-
network: regtest,
146-
redeem: {
147-
network: regtest,
148-
output: p2sh.redeem!.output,
149-
input: bitcoin.script.compile([
150-
bitcoin.script.signature.encode(
151-
alice.sign(signatureHash),
152-
hashType,
153-
),
154-
bitcoin.opcodes.OP_TRUE,
155-
]),
156-
},
157-
}).input;
158-
tx.setInputScript(0, redeemScriptSig!);
134+
const utx = await regtestUtils.fetch(unspent.txId);
135+
// for non segwit inputs, you must pass the full transaction buffer
136+
const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex');
137+
138+
// This is an example of using the finalizeInput second parameter to
139+
// define how you finalize the inputs, allowing for any type of script.
140+
const tx = new bitcoin.Psbt({ network: regtest })
141+
.setVersion(2)
142+
.addInput({
143+
hash: unspent.txId,
144+
index: unspent.vout,
145+
sequence,
146+
redeemScript: p2sh.redeem!.output!,
147+
nonWitnessUtxo,
148+
})
149+
.addOutput({
150+
address: regtestUtils.RANDOM_ADDRESS,
151+
value: 7e4,
152+
})
153+
.signInput(0, alice)
154+
.finalizeInput(0, csvGetFinalScripts) // See csvGetFinalScripts below
155+
.extractTransaction();
159156

160157
// TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently
161158
// ...
@@ -430,3 +427,88 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => {
430427
},
431428
);
432429
});
430+
431+
// This function is used to finalize a CSV transaction using PSBT.
432+
// See first test above.
433+
function csvGetFinalScripts(
434+
inputIndex: number,
435+
input: PsbtInput,
436+
script: Buffer,
437+
isSegwit: boolean,
438+
isP2SH: boolean,
439+
isP2WSH: boolean,
440+
): {
441+
finalScriptSig: Buffer | undefined;
442+
finalScriptWitness: Buffer | undefined;
443+
} {
444+
// Step 1: Check to make sure the meaningful script matches what you expect.
445+
const decompiled = bitcoin.script.decompile(script);
446+
// Checking if first OP is OP_IF... should do better check in production!
447+
// You may even want to check the public keys in the script against a
448+
// whitelist depending on the circumstances!!!
449+
// You also want to check the contents of the input to see if you have enough
450+
// info to actually construct the scriptSig and Witnesses.
451+
if (!decompiled || decompiled[0] !== bitcoin.opcodes.OP_IF) {
452+
throw new Error(`Can not finalize input #${inputIndex}`);
453+
}
454+
455+
// Step 2: Create final scripts
456+
let payment: bitcoin.Payment = {
457+
network: regtest,
458+
output: script,
459+
// This logic should be more strict and make sure the pubkeys in the
460+
// meaningful script are the ones signing in the PSBT etc.
461+
input: bitcoin.script.compile([
462+
input.partialSig![0].signature,
463+
bitcoin.opcodes.OP_TRUE,
464+
]),
465+
};
466+
if (isP2WSH && isSegwit)
467+
payment = bitcoin.payments.p2wsh({
468+
network: regtest,
469+
redeem: payment,
470+
});
471+
if (isP2SH)
472+
payment = bitcoin.payments.p2sh({
473+
network: regtest,
474+
redeem: payment,
475+
});
476+
477+
function witnessStackToScriptWitness(witness: Buffer[]): Buffer {
478+
let buffer = Buffer.allocUnsafe(0);
479+
480+
function writeSlice(slice: Buffer): void {
481+
buffer = Buffer.concat([buffer, Buffer.from(slice)]);
482+
}
483+
484+
function writeVarInt(i: number): void {
485+
const currentLen = buffer.length;
486+
const varintLen = varuint.encodingLength(i);
487+
488+
buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
489+
varuint.encode(i, buffer, currentLen);
490+
}
491+
492+
function writeVarSlice(slice: Buffer): void {
493+
writeVarInt(slice.length);
494+
writeSlice(slice);
495+
}
496+
497+
function writeVector(vector: Buffer[]): void {
498+
writeVarInt(vector.length);
499+
vector.forEach(writeVarSlice);
500+
}
501+
502+
writeVector(witness);
503+
504+
return buffer;
505+
}
506+
507+
return {
508+
finalScriptSig: payment.input,
509+
finalScriptWitness:
510+
payment.witness && payment.witness.length > 0
511+
? witnessStackToScriptWitness(payment.witness)
512+
: undefined,
513+
};
514+
}

ts_src/psbt.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,10 @@ export class Psbt {
274274
return this;
275275
}
276276

277-
finalizeInput(inputIndex: number): this {
277+
finalizeInput(
278+
inputIndex: number,
279+
finalScriptsFunc: FinalScriptsFunc = getFinalScripts,
280+
): this {
278281
const input = checkForInput(this.data.inputs, inputIndex);
279282
const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput(
280283
inputIndex,
@@ -283,16 +286,12 @@ export class Psbt {
283286
);
284287
if (!script) throw new Error(`No script found for input #${inputIndex}`);
285288

286-
const scriptType = classifyScript(script);
287-
if (!canFinalize(input, script, scriptType))
288-
throw new Error(`Can not finalize input #${inputIndex}`);
289-
290289
checkPartialSigSighashes(input);
291290

292-
const { finalScriptSig, finalScriptWitness } = getFinalScripts(
291+
const { finalScriptSig, finalScriptWitness } = finalScriptsFunc(
292+
inputIndex,
293+
input,
293294
script,
294-
scriptType,
295-
input.partialSig!,
296295
isSegwit,
297296
isP2SH,
298297
isP2WSH,
@@ -991,7 +990,49 @@ function getTxCacheValue(
991990
else if (key === '__FEE') return c.__FEE!;
992991
}
993992

993+
/**
994+
* This function must do two things:
995+
* 1. Check if the `input` can be finalized. If it can not be finalized, throw.
996+
* ie. `Can not finalize input #${inputIndex}`
997+
* 2. Create the finalScriptSig and finalScriptWitness Buffers.
998+
*/
999+
type FinalScriptsFunc = (
1000+
inputIndex: number, // Which input is it?
1001+
input: PsbtInput, // The PSBT input contents
1002+
script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.)
1003+
isSegwit: boolean, // Is it segwit?
1004+
isP2SH: boolean, // Is it P2SH?
1005+
isP2WSH: boolean, // Is it P2WSH?
1006+
) => {
1007+
finalScriptSig: Buffer | undefined;
1008+
finalScriptWitness: Buffer | undefined;
1009+
};
1010+
9941011
function getFinalScripts(
1012+
inputIndex: number,
1013+
input: PsbtInput,
1014+
script: Buffer,
1015+
isSegwit: boolean,
1016+
isP2SH: boolean,
1017+
isP2WSH: boolean,
1018+
): {
1019+
finalScriptSig: Buffer | undefined;
1020+
finalScriptWitness: Buffer | undefined;
1021+
} {
1022+
const scriptType = classifyScript(script);
1023+
if (!canFinalize(input, script, scriptType))
1024+
throw new Error(`Can not finalize input #${inputIndex}`);
1025+
return prepareFinalScripts(
1026+
script,
1027+
scriptType,
1028+
input.partialSig!,
1029+
isSegwit,
1030+
isP2SH,
1031+
isP2WSH,
1032+
);
1033+
}
1034+
1035+
function prepareFinalScripts(
9951036
script: Buffer,
9961037
scriptType: string,
9971038
partialSig: PartialSig[],

types/psbt.d.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export declare class Psbt {
5858
getFeeRate(): number;
5959
getFee(): number;
6060
finalizeAllInputs(): this;
61-
finalizeInput(inputIndex: number): this;
61+
finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this;
6262
validateSignaturesOfAllInputs(): boolean;
6363
validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean;
6464
signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this;
@@ -124,4 +124,19 @@ interface HDSignerAsync extends HDSignerBase {
124124
derivePath(path: string): HDSignerAsync;
125125
sign(hash: Buffer): Promise<Buffer>;
126126
}
127+
/**
128+
* This function must do two things:
129+
* 1. Check if the `input` can be finalized. If it can not be finalized, throw.
130+
* ie. `Can not finalize input #${inputIndex}`
131+
* 2. Create the finalScriptSig and finalScriptWitness Buffers.
132+
*/
133+
declare type FinalScriptsFunc = (inputIndex: number, // Which input is it?
134+
input: PsbtInput, // The PSBT input contents
135+
script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.)
136+
isSegwit: boolean, // Is it segwit?
137+
isP2SH: boolean, // Is it P2SH?
138+
isP2WSH: boolean) => {
139+
finalScriptSig: Buffer | undefined;
140+
finalScriptWitness: Buffer | undefined;
141+
};
127142
export {};

0 commit comments

Comments
 (0)