diff --git a/src/psbt.js b/src/psbt.js index d7a5c9a..5719586 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -226,7 +226,7 @@ class Psbt { range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } - finalizeInput(inputIndex) { + finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) { const input = utils_1.checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, @@ -234,14 +234,11 @@ class Psbt { this.__CACHE, ); if (!script) throw new Error(`No script found for input #${inputIndex}`); - const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); checkPartialSigSighashes(input); - const { finalScriptSig, finalScriptWitness } = getFinalScripts( + const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( + inputIndex, + input, script, - scriptType, - input.partialSig, isSegwit, isP2SH, isP2WSH, @@ -772,7 +769,20 @@ function getTxCacheValue(key, name, inputs, c) { if (key === '__FEE_RATE') return c.__FEE_RATE; else if (key === '__FEE') return c.__FEE; } -function getFinalScripts( +function getFinalScripts(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) { + const scriptType = classifyScript(script); + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); + return prepareFinalScripts( + script, + scriptType, + input.partialSig, + isSegwit, + isP2SH, + isP2WSH, + ); +} +function prepareFinalScripts( script, scriptType, partialSig, diff --git a/test/integration/csv.spec.ts b/test/integration/csv.spec.ts index d6f99c7..6f11a90 100644 --- a/test/integration/csv.spec.ts +++ b/test/integration/csv.spec.ts @@ -1,9 +1,11 @@ import * as assert from 'assert'; +import { PsbtInput } from 'bip174/src/lib/interfaces'; import { before, describe, it } from 'mocha'; import * as bitcoin from '../..'; import { regtestUtils } from './_regtest'; const regtest = regtestUtils.network; const bip68 = require('bip68'); +const varuint = require('varuint-bitcoin'); function toOutputScript(address: string): Buffer { return bitcoin.address.toOutputScript(address, regtest); @@ -129,33 +131,28 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { // fund the P2SH(CSV) address const unspent = await regtestUtils.faucet(p2sh.address!, 1e5); + const utx = await regtestUtils.fetch(unspent.txId); + // for non segwit inputs, you must pass the full transaction buffer + const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex'); - const tx = new bitcoin.Transaction(); - tx.version = 2; - tx.addInput(idToHash(unspent.txId), unspent.vout, sequence); - tx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 7e4); - - // {Alice's signature} OP_TRUE - const signatureHash = tx.hashForSignature( - 0, - p2sh.redeem!.output!, - hashType, - ); - const redeemScriptSig = bitcoin.payments.p2sh({ - network: regtest, - redeem: { - network: regtest, - output: p2sh.redeem!.output, - input: bitcoin.script.compile([ - bitcoin.script.signature.encode( - alice.sign(signatureHash), - hashType, - ), - bitcoin.opcodes.OP_TRUE, - ]), - }, - }).input; - tx.setInputScript(0, redeemScriptSig!); + // This is an example of using the finalizeInput second parameter to + // define how you finalize the inputs, allowing for any type of script. + const tx = new bitcoin.Psbt({ network: regtest }) + .setVersion(2) + .addInput({ + hash: unspent.txId, + index: unspent.vout, + sequence, + redeemScript: p2sh.redeem!.output!, + nonWitnessUtxo, + }) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 7e4, + }) + .signInput(0, alice) + .finalizeInput(0, csvGetFinalScripts) // See csvGetFinalScripts below + .extractTransaction(); // TODO: test that it failures _prior_ to expiry, unfortunately, race conditions when run concurrently // ... @@ -430,3 +427,88 @@ describe('bitcoinjs-lib (transactions w/ CSV)', () => { }, ); }); + +// This function is used to finalize a CSV transaction using PSBT. +// See first test above. +function csvGetFinalScripts( + inputIndex: number, + input: PsbtInput, + script: Buffer, + isSegwit: boolean, + isP2SH: boolean, + isP2WSH: boolean, +): { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +} { + // Step 1: Check to make sure the meaningful script matches what you expect. + const decompiled = bitcoin.script.decompile(script); + // Checking if first OP is OP_IF... should do better check in production! + // You may even want to check the public keys in the script against a + // whitelist depending on the circumstances!!! + // You also want to check the contents of the input to see if you have enough + // info to actually construct the scriptSig and Witnesses. + if (!decompiled || decompiled[0] !== bitcoin.opcodes.OP_IF) { + throw new Error(`Can not finalize input #${inputIndex}`); + } + + // Step 2: Create final scripts + let payment: bitcoin.Payment = { + network: regtest, + output: script, + // This logic should be more strict and make sure the pubkeys in the + // meaningful script are the ones signing in the PSBT etc. + input: bitcoin.script.compile([ + input.partialSig![0].signature, + bitcoin.opcodes.OP_TRUE, + ]), + }; + if (isP2WSH && isSegwit) + payment = bitcoin.payments.p2wsh({ + network: regtest, + redeem: payment, + }); + if (isP2SH) + payment = bitcoin.payments.p2sh({ + network: regtest, + redeem: payment, + }); + + function witnessStackToScriptWitness(witness: Buffer[]): Buffer { + let buffer = Buffer.allocUnsafe(0); + + function writeSlice(slice: Buffer): void { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + + function writeVarInt(i: number): void { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + + function writeVarSlice(slice: Buffer): void { + writeVarInt(slice.length); + writeSlice(slice); + } + + function writeVector(vector: Buffer[]): void { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + + writeVector(witness); + + return buffer; + } + + return { + finalScriptSig: payment.input, + finalScriptWitness: + payment.witness && payment.witness.length > 0 + ? witnessStackToScriptWitness(payment.witness) + : undefined, + }; +} diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index dfed415..54fe380 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -274,7 +274,10 @@ export class Psbt { return this; } - finalizeInput(inputIndex: number): this { + finalizeInput( + inputIndex: number, + finalScriptsFunc: FinalScriptsFunc = getFinalScripts, + ): this { const input = checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, @@ -283,16 +286,12 @@ export class Psbt { ); if (!script) throw new Error(`No script found for input #${inputIndex}`); - const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); - checkPartialSigSighashes(input); - const { finalScriptSig, finalScriptWitness } = getFinalScripts( + const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( + inputIndex, + input, script, - scriptType, - input.partialSig!, isSegwit, isP2SH, isP2WSH, @@ -991,7 +990,49 @@ function getTxCacheValue( else if (key === '__FEE') return c.__FEE!; } +/** + * This function must do two things: + * 1. Check if the `input` can be finalized. If it can not be finalized, throw. + * ie. `Can not finalize input #${inputIndex}` + * 2. Create the finalScriptSig and finalScriptWitness Buffers. + */ +type FinalScriptsFunc = ( + inputIndex: number, // Which input is it? + 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, // Is it P2WSH? +) => { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +}; + function getFinalScripts( + inputIndex: number, + input: PsbtInput, + script: Buffer, + isSegwit: boolean, + isP2SH: boolean, + isP2WSH: boolean, +): { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +} { + const scriptType = classifyScript(script); + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); + return prepareFinalScripts( + script, + scriptType, + input.partialSig!, + isSegwit, + isP2SH, + isP2WSH, + ); +} + +function prepareFinalScripts( script: Buffer, scriptType: string, partialSig: PartialSig[], diff --git a/types/psbt.d.ts b/types/psbt.d.ts index b1bacea..44eb4d8 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -58,7 +58,7 @@ export declare class Psbt { getFeeRate(): number; getFee(): number; finalizeAllInputs(): this; - finalizeInput(inputIndex: number): this; + finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; validateSignaturesOfAllInputs(): boolean; validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean; signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; @@ -124,4 +124,19 @@ interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; } +/** + * This function must do two things: + * 1. Check if the `input` can be finalized. If it can not be finalized, throw. + * ie. `Can not finalize input #${inputIndex}` + * 2. Create the finalScriptSig and finalScriptWitness Buffers. + */ +declare type FinalScriptsFunc = (inputIndex: number, // Which input is it? +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) => { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +}; export {};