From 22682fc2c335bc75f7f8aa8a926f2d2da0a129af Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 10 Oct 2019 11:01:54 +0900 Subject: [PATCH 1/3] Allow custom implementations of finalizers --- src/psbt.js | 19 ++++++++++++++---- ts_src/psbt.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++---- types/psbt.d.ts | 15 ++++++++++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 57a159c..3326995 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -204,7 +204,18 @@ class Psbt { range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } - finalizeInput(inputIndex) { + finalizeInput( + inputIndex, + { + classifyScript: classifyScriptF, + canFinalize: canFinalizeF, + getFinalScripts: getFinalScriptsF, + } = { + classifyScript, + canFinalize, + getFinalScripts, + }, + ) { const input = utils_1.checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, @@ -212,11 +223,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)) + const scriptType = classifyScriptF(script); + if (!canFinalizeF(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); checkPartialSigSighashes(input); - const { finalScriptSig, finalScriptWitness } = getFinalScripts( + const { finalScriptSig, finalScriptWitness } = getFinalScriptsF( script, scriptType, input.partialSig, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index d35fd4c..6fa6f43 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -251,7 +251,18 @@ export class Psbt { return this; } - finalizeInput(inputIndex: number): this { + finalizeInput( + inputIndex: number, + { + classifyScript: classifyScriptF, + canFinalize: canFinalizeF, + getFinalScripts: getFinalScriptsF, + }: IFinalizeFuncs = { + classifyScript, + canFinalize, + getFinalScripts, + }, + ): this { const input = checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, @@ -260,13 +271,13 @@ export class Psbt { ); if (!script) throw new Error(`No script found for input #${inputIndex}`); - const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) + const scriptType = classifyScriptF(script); + if (!canFinalizeF(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); checkPartialSigSighashes(input); - const { finalScriptSig, finalScriptWitness } = getFinalScripts( + const { finalScriptSig, finalScriptWitness } = getFinalScriptsF( script, scriptType, input.partialSig!, @@ -735,6 +746,39 @@ class PsbtTransaction implements ITransaction { } } +// This interface is added to allow for custom scripts to be finalized with PSBT. +interface IFinalizeFuncs { + classifyScript: FinalizeFuncClassifyScript; + canFinalize: FinalizeFuncCanFinalize; + getFinalScripts: FinalizeFuncGetFinalScripts; +} + +// Takes the meaningful script (redeemScript for P2SH and witnessScript for P2WSH) +// and returns a string to classify the script. +type FinalizeFuncClassifyScript = (script: Buffer) => string; +// Takes the Psbt data for the input and the meaningful script and its type name. +// returns true if we can finalize the input +type FinalizeFuncCanFinalize = ( + input: PsbtInput, + script: Buffer, + scriptType: string, +) => boolean; +// Takes the meaningful script, its type name, all the signatures from this input, +// and 3 booleans to tell you if it is segwit, P2SH, and P2WSH. +// it returns finalScriptSig and finalScriptWitness to be placed in the Psbt. +// if one is not needed, it should be undefined. (In TypeScript, it must be declared but undefined.) +type FinalizeFuncGetFinalScripts = ( + script: Buffer, + scriptType: string, + partialSig: PartialSig[], + isSegwit: boolean, + isP2SH: boolean, + isP2WSH: boolean, +) => { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +}; + function canFinalize( input: PsbtInput, script: Buffer, diff --git a/types/psbt.d.ts b/types/psbt.d.ts index b1bacea..516d72f 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,5 +1,5 @@ import { Psbt as PsbtBase } from 'bip174'; -import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput } from 'bip174/src/lib/interfaces'; +import { KeyValue, PartialSig, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; @@ -58,7 +58,7 @@ export declare class Psbt { getFeeRate(): number; getFee(): number; finalizeAllInputs(): this; - finalizeInput(inputIndex: number): this; + finalizeInput(inputIndex: number, { classifyScript: classifyScriptF, canFinalize: canFinalizeF, getFinalScripts: getFinalScriptsF, }?: IFinalizeFuncs): this; validateSignaturesOfAllInputs(): boolean; validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean; signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; @@ -124,4 +124,15 @@ interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; } +interface IFinalizeFuncs { + classifyScript: FinalizeFuncClassifyScript; + canFinalize: FinalizeFuncCanFinalize; + getFinalScripts: FinalizeFuncGetFinalScripts; +} +declare type FinalizeFuncClassifyScript = (script: Buffer) => string; +declare type FinalizeFuncCanFinalize = (input: PsbtInput, script: Buffer, scriptType: string) => boolean; +declare type FinalizeFuncGetFinalScripts = (script: Buffer, scriptType: string, partialSig: PartialSig[], isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean) => { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +}; export {}; From 4b5a519bfee242229b901a88a25c8a23d68c1d7a Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 28 Oct 2019 11:40:56 +0900 Subject: [PATCH 2/3] Use single func instead of 3 --- src/psbt.js | 37 ++++++++++--------- ts_src/psbt.ts | 95 ++++++++++++++++++++++++------------------------- types/psbt.d.ts | 24 +++++++------ 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 3326995..e28cd49 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -204,18 +204,7 @@ class Psbt { range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } - finalizeInput( - inputIndex, - { - classifyScript: classifyScriptF, - canFinalize: canFinalizeF, - getFinalScripts: getFinalScriptsF, - } = { - classifyScript, - canFinalize, - getFinalScripts, - }, - ) { + finalizeInput(inputIndex, finalScriptsFunc = getFinalScripts) { const input = utils_1.checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, @@ -223,14 +212,11 @@ class Psbt { this.__CACHE, ); if (!script) throw new Error(`No script found for input #${inputIndex}`); - const scriptType = classifyScriptF(script); - if (!canFinalizeF(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); checkPartialSigSighashes(input); - const { finalScriptSig, finalScriptWitness } = getFinalScriptsF( + const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( + inputIndex, + input, script, - scriptType, - input.partialSig, isSegwit, isP2SH, isP2WSH, @@ -749,7 +735,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/ts_src/psbt.ts b/ts_src/psbt.ts index 6fa6f43..15a6ce8 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -253,15 +253,7 @@ export class Psbt { finalizeInput( inputIndex: number, - { - classifyScript: classifyScriptF, - canFinalize: canFinalizeF, - getFinalScripts: getFinalScriptsF, - }: IFinalizeFuncs = { - classifyScript, - canFinalize, - getFinalScripts, - }, + finalScriptsFunc: FinalScriptsFunc = getFinalScripts, ): this { const input = checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( @@ -271,16 +263,12 @@ export class Psbt { ); if (!script) throw new Error(`No script found for input #${inputIndex}`); - const scriptType = classifyScriptF(script); - if (!canFinalizeF(input, script, scriptType)) - throw new Error(`Can not finalize input #${inputIndex}`); - checkPartialSigSighashes(input); - const { finalScriptSig, finalScriptWitness } = getFinalScriptsF( + const { finalScriptSig, finalScriptWitness } = finalScriptsFunc( + inputIndex, + input, script, - scriptType, - input.partialSig!, isSegwit, isP2SH, isP2WSH, @@ -746,39 +734,6 @@ class PsbtTransaction implements ITransaction { } } -// This interface is added to allow for custom scripts to be finalized with PSBT. -interface IFinalizeFuncs { - classifyScript: FinalizeFuncClassifyScript; - canFinalize: FinalizeFuncCanFinalize; - getFinalScripts: FinalizeFuncGetFinalScripts; -} - -// Takes the meaningful script (redeemScript for P2SH and witnessScript for P2WSH) -// and returns a string to classify the script. -type FinalizeFuncClassifyScript = (script: Buffer) => string; -// Takes the Psbt data for the input and the meaningful script and its type name. -// returns true if we can finalize the input -type FinalizeFuncCanFinalize = ( - input: PsbtInput, - script: Buffer, - scriptType: string, -) => boolean; -// Takes the meaningful script, its type name, all the signatures from this input, -// and 3 booleans to tell you if it is segwit, P2SH, and P2WSH. -// it returns finalScriptSig and finalScriptWitness to be placed in the Psbt. -// if one is not needed, it should be undefined. (In TypeScript, it must be declared but undefined.) -type FinalizeFuncGetFinalScripts = ( - script: Buffer, - scriptType: string, - partialSig: PartialSig[], - isSegwit: boolean, - isP2SH: boolean, - isP2WSH: boolean, -) => { - finalScriptSig: Buffer | undefined; - finalScriptWitness: Buffer | undefined; -}; - function canFinalize( input: PsbtInput, script: Buffer, @@ -996,7 +951,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 516d72f..44eb4d8 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,5 +1,5 @@ import { Psbt as PsbtBase } from 'bip174'; -import { KeyValue, PartialSig, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput } from 'bip174/src/lib/interfaces'; +import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; @@ -58,7 +58,7 @@ export declare class Psbt { getFeeRate(): number; getFee(): number; finalizeAllInputs(): this; - finalizeInput(inputIndex: number, { classifyScript: classifyScriptF, canFinalize: canFinalizeF, getFinalScripts: getFinalScriptsF, }?: IFinalizeFuncs): this; + finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; validateSignaturesOfAllInputs(): boolean; validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean; signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; @@ -124,14 +124,18 @@ interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; } -interface IFinalizeFuncs { - classifyScript: FinalizeFuncClassifyScript; - canFinalize: FinalizeFuncCanFinalize; - getFinalScripts: FinalizeFuncGetFinalScripts; -} -declare type FinalizeFuncClassifyScript = (script: Buffer) => string; -declare type FinalizeFuncCanFinalize = (input: PsbtInput, script: Buffer, scriptType: string) => boolean; -declare type FinalizeFuncGetFinalScripts = (script: Buffer, scriptType: string, partialSig: PartialSig[], isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean) => { +/** + * 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; }; From f2224473237f9066c93421aa2317f03fab0efeee Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 28 Oct 2019 12:27:35 +0900 Subject: [PATCH 3/3] Add CSV example for custom finalizer --- test/integration/csv.spec.ts | 134 ++++++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 26 deletions(-) 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, + }; +}