From 813b84f91f0294c89fc11cdb66aefd80e9adb28a Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 3 Jul 2019 15:13:36 +0900 Subject: [PATCH] Finalize and extract done --- src/psbt.js | 217 +++++++++++++++++++++++++++++++-------- ts_src/psbt.ts | 268 +++++++++++++++++++++++++++++++++++++++--------- types/psbt.d.ts | 8 +- 3 files changed, 400 insertions(+), 93 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index e03969d..59d898c 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -6,6 +6,7 @@ const crypto_1 = require('./crypto'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { // protected __TX: Transaction; constructor(network) { @@ -23,56 +24,75 @@ class Psbt extends bip174_1.Psbt { // } // }); } - canFinalize(inputIndex) { + extractTransaction() { + if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + const tx = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + this.inputs.forEach((input, idx) => { + if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; + if (input.finalScriptWitness) { + const decompiled = bscript.decompile(input.finalScriptWitness); + if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); + } + }); + return tx; + } + finalizeAllInputs() { + const inputResults = range(this.inputs.length).map(idx => + this.finalizeInput(idx), + ); + const result = inputResults.every(val => val === true); + return { + result, + inputResults, + }; + } + finalizeInput(inputIndex) { const input = utils_1.checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( + const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, this.globalMap.unsignedTx, ); if (!script) return false; const scriptType = classifyScript(script); - const hasSigs = (neededSigs, partialSig) => { - if (!partialSig) return false; - if (partialSig.length > neededSigs) - throw new Error('Too many signatures'); - return partialSig.length === neededSigs; - }; - switch (scriptType) { - case 'pubkey': - return hasSigs(1, input.partialSig); - case 'pubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m, input.partialSig); - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - default: - return false; + if (!canFinalize(input, script, scriptType)) return false; + let finalScriptSig; + let finalScriptWitness; + // Wow, the payments API is very handy + const payment = getPayment(script, scriptType, input.partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness); + } + if (p2sh) { + finalScriptSig = bscript.compile([p2sh.redeem.output]); + } + } else { + finalScriptSig = payment.input; } + if (finalScriptSig) + this.addFinalScriptSigToInput(inputIndex, finalScriptSig); + if (finalScriptWitness) + this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + if (!finalScriptSig && !finalScriptWitness) return false; + this.clearFinalizedInput(inputIndex); + return true; } signInput(inputIndex, keyPair) { - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); + const input = utils_1.checkForInput(this.inputs, inputIndex); + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); const { hash, sighashType, script } = getHashForSig( inputIndex, input, this.globalMap.unsignedTx, ); const pubkey = keyPair.publicKey; - const pubkeyHash = crypto_1.hash160(keyPair.publicKey); - const decompiled = bscript.decompile(script); - if (decompiled === null) throw new Error('Unknown script error'); - const hasKey = decompiled.some(element => { - if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); - }); - if (!hasKey) { - throw new Error( - `Can not sign for this input with the key ${pubkey.toString('hex')}`, - ); - } + checkScriptForPubkey(pubkey, script); const partialSig = { pubkey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), @@ -81,6 +101,77 @@ class Psbt extends bip174_1.Psbt { } } exports.Psbt = Psbt; +// +// +// +// +// Helper functions +// +// +// +// +function isFinalized(input) { + return !!input.finalScriptSig || !!input.finalScriptWitness; +} +function getPayment(script, scriptType, partialSig) { + let payment; + switch (scriptType) { + case 'multisig': + payment = payments.p2ms({ + output: script, + signatures: partialSig.map(ps => ps.signature), + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment; +} +function canFinalize(input, script, scriptType) { + 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); + default: + return false; + } +} +function checkScriptForPubkey(pubkey, script) { + const pubkeyHash = crypto_1.hash160(pubkey); + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + const hasKey = decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); + if (!hasKey) { + throw new Error( + `Can not sign for this input with the key ${pubkey.toString('hex')}`, + ); + } +} const getHashForSig = (inputIndex, input, txBuf) => { const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf); const sighashType = @@ -202,29 +293,67 @@ const classifyScript = script => { return 'nonstandard'; }; function getScriptFromInput(inputIndex, input, _unsignedTx) { - let script; + const res = { + script: null, + isSegwit: false, + isP2SH: false, + isP2WSH: false, + }; if (input.nonWitnessUtxo) { if (input.redeemScript) { - script = input.redeemScript; + res.isP2SH = true; + res.script = input.redeemScript; } else { const unsignedTx = transaction_1.Transaction.fromBuffer(_unsignedTx); const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( input.nonWitnessUtxo, ); const prevoutIndex = unsignedTx.ins[inputIndex].index; - script = nonWitnessUtxoTx.outs[prevoutIndex].script; + res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } } else if (input.witnessUtxo) { + res.isSegwit = true; + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript; if (input.witnessScript) { - script = input.witnessScript; + res.script = input.witnessScript; } else if (input.redeemScript) { - script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output; + res.script = payments.p2pkh({ + hash: input.redeemScript.slice(2), + }).output; } else { - script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) - .output; + res.script = payments.p2pkh({ + hash: input.witnessUtxo.script.slice(2), + }).output; } - } else { - return; } - return script; + return res; } +const hasSigs = (neededSigs, partialSig) => { + if (!partialSig) return false; + if (partialSig.length > neededSigs) throw new Error('Too many signatures'); + return partialSig.length === neededSigs; +}; +function witnessStackToScriptWitness(witness) { + let buffer = Buffer.allocUnsafe(0); + function writeSlice(slice) { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + function writeVarInt(i) { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + function writeVarSlice(slice) { + writeVarInt(slice.length); + writeSlice(slice); + } + function writeVector(vector) { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + writeVector(witness); + return buffer; +} +const range = n => [...Array(n).keys()]; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 194a1f7..273a1c3 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,5 @@ import { Psbt as PsbtBase } from 'bip174'; -import { PsbtInput } from 'bip174/src/lib/interfaces'; +import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; import { hash160 } from './crypto'; import { Signer } from './ecpair'; @@ -7,6 +7,7 @@ import { Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Transaction } from './transaction'; +const varuint = require('varuint-bitcoin'); export class Psbt extends PsbtBase { // protected __TX: Transaction; @@ -25,41 +26,84 @@ export class Psbt extends PsbtBase { // }); } - canFinalize(inputIndex: number): boolean { + extractTransaction(): Transaction { + if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + const tx = Transaction.fromBuffer(this.globalMap.unsignedTx!); + this.inputs.forEach((input, idx) => { + if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; + if (input.finalScriptWitness) { + const decompiled = bscript.decompile(input.finalScriptWitness); + if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); + } + }); + return tx; + } + + finalizeAllInputs(): { + result: boolean; + inputResults: boolean[]; + } { + const inputResults = range(this.inputs.length).map(idx => + this.finalizeInput(idx), + ); + const result = inputResults.every(val => val === true); + return { + result, + inputResults, + }; + } + + finalizeInput(inputIndex: number): boolean { const input = checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( + const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, this.globalMap.unsignedTx!, ); if (!script) return false; + const scriptType = classifyScript(script); + if (!canFinalize(input, script, scriptType)) return false; - const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { - if (!partialSig) return false; - if (partialSig.length > neededSigs) - throw new Error('Too many signatures'); - return partialSig.length === neededSigs; - }; + let finalScriptSig: Buffer | undefined; + let finalScriptWitness: Buffer | undefined; - switch (scriptType) { - case 'pubkey': - return hasSigs(1, input.partialSig); - case 'pubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m!, input.partialSig); - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - default: - return false; + // Wow, the payments API is very handy + const payment: payments.Payment = getPayment( + script, + scriptType, + input.partialSig!, + ); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness!); + } + if (p2sh) { + finalScriptSig = bscript.compile([p2sh.redeem!.output!]); + } + } else { + finalScriptSig = payment.input; } + + if (finalScriptSig) + this.addFinalScriptSigToInput(inputIndex, finalScriptSig); + if (finalScriptWitness) + this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + if (!finalScriptSig && !finalScriptWitness) return false; + + this.clearFinalizedInput(inputIndex); + return true; } signInput(inputIndex: number, keyPair: Signer): Psbt { - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); + const input = checkForInput(this.inputs, inputIndex); + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); const { hash, sighashType, script } = getHashForSig( inputIndex, input, @@ -67,21 +111,8 @@ export class Psbt extends PsbtBase { ); const pubkey = keyPair.publicKey; - const pubkeyHash = hash160(keyPair.publicKey); - const decompiled = bscript.decompile(script); - if (decompiled === null) throw new Error('Unknown script error'); - - const hasKey = decompiled.some(element => { - if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); - }); - - if (!hasKey) { - throw new Error( - `Can not sign for this input with the key ${pubkey.toString('hex')}`, - ); - } + checkScriptForPubkey(pubkey, script); const partialSig = { pubkey, @@ -92,6 +123,93 @@ export class Psbt extends PsbtBase { } } +// +// +// +// +// Helper functions +// +// +// +// + +function isFinalized(input: PsbtInput): boolean { + return !!input.finalScriptSig || !!input.finalScriptWitness; +} + +function getPayment( + script: Buffer, + scriptType: string, + partialSig: PartialSig[], +): payments.Payment { + let payment: payments.Payment; + switch (scriptType) { + case 'multisig': + payment = payments.p2ms({ + output: script, + signatures: partialSig.map(ps => ps.signature), + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment!; +} + +function canFinalize( + input: PsbtInput, + script: Buffer, + scriptType: string, +): boolean { + 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); + default: + return false; + } +} + +function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void { + const pubkeyHash = hash160(pubkey); + + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + + const hasKey = decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); + + if (!hasKey) { + throw new Error( + `Can not sign for this input with the key ${pubkey.toString('hex')}`, + ); + } +} + interface HashForSigData { script: Buffer; hash: Buffer; @@ -239,32 +357,86 @@ const classifyScript = (script: Buffer): string => { return 'nonstandard'; }; +interface GetScriptReturn { + script: Buffer | null; + isSegwit: boolean; + isP2SH: boolean; + isP2WSH: boolean; +} function getScriptFromInput( inputIndex: number, input: PsbtInput, _unsignedTx: Buffer, -): Buffer | undefined { - let script: Buffer; +): GetScriptReturn { + const res: GetScriptReturn = { + script: null, + isSegwit: false, + isP2SH: false, + isP2WSH: false, + }; if (input.nonWitnessUtxo) { if (input.redeemScript) { - script = input.redeemScript; + res.isP2SH = true; + res.script = input.redeemScript; } else { const unsignedTx = Transaction.fromBuffer(_unsignedTx); const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); const prevoutIndex = unsignedTx.ins[inputIndex].index; - script = nonWitnessUtxoTx.outs[prevoutIndex].script; + res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } } else if (input.witnessUtxo) { + res.isSegwit = true; + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript; if (input.witnessScript) { - script = input.witnessScript; + res.script = input.witnessScript; } else if (input.redeemScript) { - script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output!; + res.script = payments.p2pkh({ + hash: input.redeemScript.slice(2), + }).output!; } else { - script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) - .output!; + res.script = payments.p2pkh({ + hash: input.witnessUtxo.script.slice(2), + }).output!; } - } else { - return; } - return script; + return res; } + +const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { + if (!partialSig) return false; + if (partialSig.length > neededSigs) throw new Error('Too many signatures'); + return partialSig.length === neededSigs; +}; + +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; +} + +const range = (n: number): number[] => [...Array(n).keys()]; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index fda7e6b..f5b6430 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,9 +1,15 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; import { Network } from './networks'; +import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { network?: Network | undefined; constructor(network?: Network | undefined); - canFinalize(inputIndex: number): boolean; + extractTransaction(): Transaction; + finalizeAllInputs(): { + result: boolean; + inputResults: boolean[]; + }; + finalizeInput(inputIndex: number): boolean; signInput(inputIndex: number, keyPair: Signer): Psbt; }