From 361ea7c098489545253ba1824202c41a27d22596 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 27 Apr 2020 17:10:11 +0900 Subject: [PATCH 01/15] Add inputHasPubkey and outputHasPubkey methods --- src/psbt.js | 100 +++++++++++++++++++++++++++++--- test/psbt.spec.ts | 141 +++++++++++++++++++++++++++++++++++++++++++++- ts_src/psbt.ts | 127 +++++++++++++++++++++++++++++++++++++---- types/psbt.d.ts | 2 + 4 files changed, 349 insertions(+), 21 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 134be87..48fb798 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -277,6 +277,14 @@ class Psbt { this.data.clearFinalizedInput(inputIndex); return this; } + inputHasPubkey(inputIndex, pubkey) { + const input = utils_1.checkForInput(this.data.inputs, inputIndex); + return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); + } + outputHasPubkey(outputIndex, pubkey) { + const output = utils_1.checkForOutput(this.data.outputs, outputIndex); + return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE); + } validateSignaturesOfAllInputs() { utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => @@ -653,6 +661,7 @@ const isP2PK = isPaymentFactory(payments.p2pk); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); +const isP2SHScript = isPaymentFactory(payments.p2sh); function check32Bit(num) { if ( typeof num !== 'number' || @@ -723,14 +732,7 @@ function checkPartialSigSighashes(input) { }); } function checkScriptForPubkey(pubkey, script, action) { - 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) { + if (!pubkeyInScript(pubkey, script)) { throw new Error( `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, ); @@ -1219,6 +1221,88 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { } return c[inputIndex]; } +function pubkeyInInput(pubkey, input, inputIndex, cache) { + let script; + if (input.witnessUtxo !== undefined) { + script = input.witnessUtxo.script; + } else if (input.nonWitnessUtxo !== undefined) { + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); + script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + } else { + throw new Error("Can't find pubkey in input without Utxo data"); + } + const meaningfulScript = checkScripts( + script, + input.redeemScript, + input.witnessScript, + ); + return pubkeyInScript(pubkey, meaningfulScript); +} +function pubkeyInOutput(pubkey, output, outputIndex, cache) { + const script = cache.__TX.outs[outputIndex].script; + const meaningfulScript = checkScripts( + script, + output.redeemScript, + output.witnessScript, + ); + return pubkeyInScript(pubkey, meaningfulScript); +} +function checkScripts(script, redeemScript, witnessScript) { + let fail = false; + if (isP2SHScript(script)) { + if (redeemScript === undefined) { + fail = true; + } else if (isP2WSHScript(redeemScript)) { + if (witnessScript === undefined) { + fail = true; + } else { + fail = !payments + .p2sh({ + redeem: payments.p2wsh({ + redeem: { output: witnessScript }, + }), + }) + .output.equals(script); + if (!fail) return witnessScript; + } + } else { + fail = !payments + .p2sh({ + redeem: { output: redeemScript }, + }) + .output.equals(script); + if (!fail) return redeemScript; + } + } else if (isP2WSHScript(script)) { + if (witnessScript === undefined) { + fail = true; + } else { + fail = !payments + .p2wsh({ + redeem: { output: witnessScript }, + }) + .output.equals(script); + if (!fail) return witnessScript; + } + } + if (fail) { + throw new Error('Incomplete script information'); + } + return script; +} +function pubkeyInScript(pubkey, script) { + const pubkeyHash = crypto_1.hash160(pubkey); + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + return decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); +} function classifyScript(script) { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index e0eba81..9400a5e 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; -import { bip32, ECPair, networks as NETWORKS, Psbt } from '..'; +import { bip32, ECPair, networks as NETWORKS, Psbt, payments } from '..'; import * as preFixtures from './fixtures/psbt.json'; @@ -542,6 +542,143 @@ describe(`Psbt`, () => { }); }); + describe('inputHasPubkey', () => { + it('should throw', () => { + const psbt = new Psbt(); + psbt.addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + }); + + assert.throws(() => { + psbt.inputHasPubkey(0, Buffer.from([])); + }, new RegExp("Can't find pubkey in input without Utxo data")); + + psbt.updateInput(0, { + witnessUtxo: { + value: 1337, + script: payments.p2sh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!, + }, + }); + + assert.throws(() => { + psbt.inputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + delete psbt.data.inputs[0].witnessUtxo; + + psbt.updateInput(0, { + witnessUtxo: { + value: 1337, + script: payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!, + }, + }); + + assert.throws(() => { + psbt.inputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + delete psbt.data.inputs[0].witnessUtxo; + + psbt.updateInput(0, { + witnessUtxo: { + value: 1337, + script: payments.p2sh({ + redeem: payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }), + }).output!, + }, + redeemScript: payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!, + }); + + assert.throws(() => { + psbt.inputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + psbt.updateInput(0, { + witnessScript: Buffer.from([0x51]), + }); + + assert.doesNotThrow(() => { + psbt.inputHasPubkey(0, Buffer.from([0x51])); + }); + }); + }); + + describe('outputHasPubkey', () => { + it('should throw', () => { + const psbt = new Psbt(); + psbt + .addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + }) + .addOutput({ + script: payments.p2sh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!, + value: 1337, + }); + + assert.throws(() => { + psbt.outputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + (psbt as any).__CACHE.__TX.outs[0].script = payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!; + + assert.throws(() => { + psbt.outputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + (psbt as any).__CACHE.__TX.outs[0].script = payments.p2sh({ + redeem: payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }), + }).output!; + + psbt.updateOutput(0, { + redeemScript: payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!, + }); + + assert.throws(() => { + psbt.outputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + delete psbt.data.outputs[0].redeemScript; + + psbt.updateOutput(0, { + witnessScript: Buffer.from([0x51]), + }); + + assert.throws(() => { + psbt.outputHasPubkey(0, Buffer.from([])); + }, new RegExp('Incomplete script information')); + + psbt.updateOutput(0, { + redeemScript: payments.p2wsh({ + redeem: { output: Buffer.from([0x51]) }, + }).output!, + }); + + assert.doesNotThrow(() => { + psbt.outputHasPubkey(0, Buffer.from([0x51])); + }); + }); + }); + describe('clone', () => { it('Should clone a psbt exactly with no reference', () => { const f = fixtures.clone; @@ -643,6 +780,8 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.setVersion(3); }, new RegExp('Can not modify transaction, signatures exist.')); + assert.strictEqual(psbt.inputHasPubkey(0, alice.publicKey), true); + assert.strictEqual(psbt.outputHasPubkey(0, alice.publicKey), false); assert.strictEqual( psbt.extractTransaction().toHex(), '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 89491b9..301596a 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -13,7 +13,7 @@ import { TransactionInput, TransactionOutput, } from 'bip174/src/lib/interfaces'; -import { checkForInput } from 'bip174/src/lib/utils'; +import { checkForInput, checkForOutput } from 'bip174/src/lib/utils'; import { fromOutputScript, toOutputScript } from './address'; import { cloneBuffer, reverseBuffer } from './bufferutils'; import { hash160 } from './crypto'; @@ -340,6 +340,16 @@ export class Psbt { return this; } + inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean { + const input = checkForInput(this.data.inputs, inputIndex); + return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); + } + + outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean { + const output = checkForOutput(this.data.outputs, outputIndex); + return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE); + } + validateSignaturesOfAllInputs(): boolean { checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => @@ -849,6 +859,7 @@ const isP2PK = isPaymentFactory(payments.p2pk); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); +const isP2SHScript = isPaymentFactory(payments.p2sh); function check32Bit(num: number): void { if ( @@ -927,17 +938,7 @@ function checkScriptForPubkey( script: Buffer, action: string, ): 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) { + if (!pubkeyInScript(pubkey, script)) { throw new Error( `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, ); @@ -1560,6 +1561,108 @@ function nonWitnessUtxoTxFromCache( return c[inputIndex]; } +function pubkeyInInput( + pubkey: Buffer, + input: PsbtInput, + inputIndex: number, + cache: PsbtCache, +): boolean { + let script: Buffer; + if (input.witnessUtxo !== undefined) { + script = input.witnessUtxo.script; + } else if (input.nonWitnessUtxo !== undefined) { + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); + script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + } else { + throw new Error("Can't find pubkey in input without Utxo data"); + } + const meaningfulScript = checkScripts( + script, + input.redeemScript, + input.witnessScript, + ); + return pubkeyInScript(pubkey, meaningfulScript); +} + +function pubkeyInOutput( + pubkey: Buffer, + output: PsbtOutput, + outputIndex: number, + cache: PsbtCache, +): boolean { + const script = cache.__TX.outs[outputIndex].script; + const meaningfulScript = checkScripts( + script, + output.redeemScript, + output.witnessScript, + ); + return pubkeyInScript(pubkey, meaningfulScript); +} + +function checkScripts( + script: Buffer, + redeemScript?: Buffer, + witnessScript?: Buffer, +): Buffer { + let fail = false; + if (isP2SHScript(script)) { + if (redeemScript === undefined) { + fail = true; + } else if (isP2WSHScript(redeemScript)) { + if (witnessScript === undefined) { + fail = true; + } else { + fail = !payments + .p2sh({ + redeem: payments.p2wsh({ + redeem: { output: witnessScript }, + }), + }) + .output!.equals(script); + if (!fail) return witnessScript; + } + } else { + fail = !payments + .p2sh({ + redeem: { output: redeemScript }, + }) + .output!.equals(script); + if (!fail) return redeemScript; + } + } else if (isP2WSHScript(script)) { + if (witnessScript === undefined) { + fail = true; + } else { + fail = !payments + .p2wsh({ + redeem: { output: witnessScript }, + }) + .output!.equals(script); + if (!fail) return witnessScript; + } + } + if (fail) { + throw new Error('Incomplete script information'); + } + return script; +} + +function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { + const pubkeyHash = hash160(pubkey); + + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + + return decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); +} + function classifyScript(script: Buffer): string { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 0a898d8..c47ce74 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -63,6 +63,8 @@ export declare class Psbt { getFee(): number; finalizeAllInputs(): this; finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; + inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean; + outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean; validateSignaturesOfAllInputs(): boolean; validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean; signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; From de0bbf51e59e74dad62d4a15f4913e6bd6a9f3c6 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 27 Apr 2020 16:46:07 +0700 Subject: [PATCH 02/15] Export PSBT getter types --- ts_src/index.ts | 2 +- ts_src/psbt.ts | 13 ++++++++++--- types/index.d.ts | 2 +- types/psbt.d.ts | 14 ++++++++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/ts_src/index.ts b/ts_src/index.ts index 505407f..b9aa49c 100644 --- a/ts_src/index.ts +++ b/ts_src/index.ts @@ -9,7 +9,7 @@ import * as script from './script'; export { ECPair, address, bip32, crypto, networks, payments, script }; export { Block } from './block'; -export { Psbt } from './psbt'; +export { Psbt, PsbtTxInput, PsbtTxOutput } from './psbt'; export { OPS as opcodes } from './script'; export { Transaction } from './transaction'; export { TransactionBuilder } from './transaction_builder'; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 301596a..31d15c7 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -11,7 +11,6 @@ import { Transaction as ITransaction, TransactionFromBuffer, TransactionInput, - TransactionOutput, } from 'bip174/src/lib/interfaces'; import { checkForInput, checkForOutput } from 'bip174/src/lib/utils'; import { fromOutputScript, toOutputScript } from './address'; @@ -27,6 +26,14 @@ import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; +export interface PsbtTxInput extends TransactionInput { + hash: Buffer; +} + +export interface PsbtTxOutput extends Output { + address: string; +} + /** * These are the default arguments for a Psbt instance. */ @@ -146,7 +153,7 @@ export class Psbt { this.setLocktime(locktime); } - get txInputs(): TransactionInput[] { + get txInputs(): PsbtTxInput[] { return this.__CACHE.__TX.ins.map(input => ({ hash: cloneBuffer(input.hash), index: input.index, @@ -154,7 +161,7 @@ export class Psbt { })); } - get txOutputs(): TransactionOutput[] { + get txOutputs(): PsbtTxOutput[] { return this.__CACHE.__TX.outs.map(output => ({ script: cloneBuffer(output.script), value: output.value, diff --git a/types/index.d.ts b/types/index.d.ts index 68da119..c8f2a00 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,7 +7,7 @@ import * as payments from './payments'; import * as script from './script'; export { ECPair, address, bip32, crypto, networks, payments, script }; export { Block } from './block'; -export { Psbt } from './psbt'; +export { Psbt, PsbtTxInput, PsbtTxOutput } from './psbt'; export { OPS as opcodes } from './script'; export { Transaction } from './transaction'; export { TransactionBuilder } from './transaction_builder'; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index c47ce74..127ef0f 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,8 +1,14 @@ import { Psbt as PsbtBase } from 'bip174'; -import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput, TransactionOutput } 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'; +import { Output, Transaction } from './transaction'; +export interface PsbtTxInput extends TransactionInput { + hash: Buffer; +} +export interface PsbtTxOutput extends Output { + address: string; +} /** * Psbt class can parse and generate a PSBT binary based off of the BIP174. * There are 6 roles that this class fulfills. (Explained in BIP174) @@ -46,8 +52,8 @@ export declare class Psbt { readonly inputCount: number; version: number; locktime: number; - readonly txInputs: TransactionInput[]; - readonly txOutputs: TransactionOutput[]; + readonly txInputs: PsbtTxInput[]; + readonly txOutputs: PsbtTxOutput[]; combine(...those: Psbt[]): this; clone(): Psbt; setMaximumFeeRate(satoshiPerByte: number): void; From 9fd13f3a43c99273644c0ba205794864d2749f71 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 27 Apr 2020 17:18:05 +0700 Subject: [PATCH 03/15] Fix lint error --- test/psbt.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index 9400a5e..f755ba7 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; -import { bip32, ECPair, networks as NETWORKS, Psbt, payments } from '..'; +import { bip32, ECPair, networks as NETWORKS, payments, Psbt } from '..'; import * as preFixtures from './fixtures/psbt.json'; From e9382ebea26875dea19ce3888cd2aaf37e5fc783 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 27 Apr 2020 20:37:32 +0700 Subject: [PATCH 04/15] Fix horrific bug! --- src/bufferutils.js | 2 +- ts_src/bufferutils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bufferutils.js b/src/bufferutils.js index 87645c6..2ee3542 100644 --- a/src/bufferutils.js +++ b/src/bufferutils.js @@ -44,7 +44,7 @@ exports.reverseBuffer = reverseBuffer; function cloneBuffer(buffer) { const clone = Buffer.alloc(buffer.length); buffer.copy(clone); - return buffer; + return clone; } exports.cloneBuffer = cloneBuffer; /** diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index 087162f..2025f88 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -51,7 +51,7 @@ export function reverseBuffer(buffer: Buffer): Buffer { export function cloneBuffer(buffer: Buffer): Buffer { const clone = Buffer.alloc(buffer.length); buffer.copy(clone); - return buffer; + return clone; } /** From e3bf997d64dea29efcdd24f9d0441dfb844d6285 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 27 Apr 2020 20:38:04 +0700 Subject: [PATCH 05/15] Improve test coverage --- test/psbt.spec.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index f755ba7..de83fe3 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -846,4 +846,71 @@ describe(`Psbt`, () => { assert.ok((psbt as any).data.inputs[index].nonWitnessUtxo.equals(value)); }); }); + + describe('Transaction properties', () => { + it('.version is exposed and is settable', () => { + const psbt = new Psbt(); + + assert.strictEqual(psbt.version, 2); + assert.strictEqual(psbt.version, (psbt as any).__CACHE.__TX.version); + + psbt.version = 1; + assert.strictEqual(psbt.version, 1); + assert.strictEqual(psbt.version, (psbt as any).__CACHE.__TX.version); + }); + + it('.locktime is exposed and is settable', () => { + const psbt = new Psbt(); + + assert.strictEqual(psbt.locktime, 0); + assert.strictEqual(psbt.locktime, (psbt as any).__CACHE.__TX.locktime); + + psbt.locktime = 123; + assert.strictEqual(psbt.locktime, 123); + assert.strictEqual(psbt.locktime, (psbt as any).__CACHE.__TX.locktime); + }); + + it('.txInputs is exposed as a readonly clone', () => { + const psbt = new Psbt(); + const hash = Buffer.alloc(32); + const index = 0; + psbt.addInput({ hash, index }); + + const input = psbt.txInputs[0]; + const internalInput = (psbt as any).__CACHE.__TX.ins[0]; + + assert.ok(input.hash.equals(internalInput.hash)); + assert.strictEqual(input.index, internalInput.index); + assert.strictEqual(input.sequence, internalInput.sequence); + + input.hash[0] = 123; + input.index = 123; + input.sequence = 123; + + assert.ok(!input.hash.equals(internalInput.hash)); + assert.notEqual(input.index, internalInput.index); + assert.notEqual(input.sequence, internalInput.sequence); + }); + + it('.txOutputs is exposed as a readonly clone', () => { + const psbt = new Psbt(); + const address = '1LukeQU5jwebXbMLDVydeH4vFSobRV9rkj'; + const value = 100000; + psbt.addOutput({ address, value }); + + const output = psbt.txOutputs[0]; + const internalInput = (psbt as any).__CACHE.__TX.outs[0]; + + assert.strictEqual(output.address, address); + + assert.ok(output.script.equals(internalInput.script)); + assert.strictEqual(output.value, internalInput.value); + + output.script[0] = 123; + output.value = 123; + + assert.ok(!output.script.equals(internalInput.script)); + assert.notEqual(output.value, internalInput.value); + }); + }); }); From 97074f8a649e9b2733d6c24ac9e18b9b09517211 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 28 Apr 2020 14:41:48 +0900 Subject: [PATCH 06/15] Refactor getMeaningfulScript --- src/psbt.js | 80 +++++++++++++++++++++------------------------ test/psbt.spec.ts | 14 ++++---- ts_src/psbt.ts | 83 +++++++++++++++++++++++------------------------ 3 files changed, 84 insertions(+), 93 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 48fb798..91e0438 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1235,7 +1235,7 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { } else { throw new Error("Can't find pubkey in input without Utxo data"); } - const meaningfulScript = checkScripts( + const meaningfulScript = getMeaningfulScript( script, input.redeemScript, input.witnessScript, @@ -1244,55 +1244,49 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { } function pubkeyInOutput(pubkey, output, outputIndex, cache) { const script = cache.__TX.outs[outputIndex].script; - const meaningfulScript = checkScripts( + const meaningfulScript = getMeaningfulScript( script, output.redeemScript, output.witnessScript, ); return pubkeyInScript(pubkey, meaningfulScript); } -function checkScripts(script, redeemScript, witnessScript) { - let fail = false; - if (isP2SHScript(script)) { - if (redeemScript === undefined) { - fail = true; - } else if (isP2WSHScript(redeemScript)) { - if (witnessScript === undefined) { - fail = true; - } else { - fail = !payments - .p2sh({ - redeem: payments.p2wsh({ - redeem: { output: witnessScript }, - }), - }) - .output.equals(script); - if (!fail) return witnessScript; - } - } else { - fail = !payments - .p2sh({ - redeem: { output: redeemScript }, - }) - .output.equals(script); - if (!fail) return redeemScript; - } - } else if (isP2WSHScript(script)) { - if (witnessScript === undefined) { - fail = true; - } else { - fail = !payments - .p2wsh({ - redeem: { output: witnessScript }, - }) - .output.equals(script); - if (!fail) return witnessScript; - } +function getMeaningfulScript(script, redeemScript, witnessScript) { + const { p2sh, p2wsh } = payments; + const isP2SH = isP2SHScript(script); + const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); + const isP2WSH = isP2WSHScript(script); + if (isP2SH && redeemScript === undefined) + throw new Error('scriptPubkey is P2SH but redeemScript missing'); + if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) + throw new Error( + 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', + ); + let payment; + let meaningfulScript; + if (isP2SHP2WSH) { + meaningfulScript = witnessScript; + payment = p2sh({ redeem: p2wsh({ redeem: { output: meaningfulScript } }) }); + if (!payment.redeem.output.equals(redeemScript)) + throw new Error('P2SHP2WSH witnessScript and redeemScript do not match'); + if (!payment.output.equals(script)) + throw new Error( + 'P2SHP2WSH witnessScript+redeemScript and scriptPubkey do not match', + ); + } else if (isP2WSH) { + meaningfulScript = witnessScript; + payment = p2wsh({ redeem: { output: meaningfulScript } }); + if (!payment.output.equals(script)) + throw new Error('P2WSH witnessScript and scriptPubkey do not match'); + } else if (isP2SH) { + meaningfulScript = redeemScript; + payment = p2sh({ redeem: { output: meaningfulScript } }); + if (!payment.output.equals(script)) + throw new Error('P2SH redeemScript and scriptPubkey do not match'); + } else { + meaningfulScript = script; } - if (fail) { - throw new Error('Incomplete script information'); - } - return script; + return meaningfulScript; } function pubkeyInScript(pubkey, script) { const pubkeyHash = crypto_1.hash160(pubkey); diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index de83fe3..ff2131b 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -566,7 +566,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.inputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey is P2SH but redeemScript missing')); delete psbt.data.inputs[0].witnessUtxo; @@ -581,7 +581,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.inputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing')); delete psbt.data.inputs[0].witnessUtxo; @@ -601,7 +601,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.inputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing')); psbt.updateInput(0, { witnessScript: Buffer.from([0x51]), @@ -631,7 +631,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.outputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey is P2SH but redeemScript missing')); (psbt as any).__CACHE.__TX.outs[0].script = payments.p2wsh({ redeem: { output: Buffer.from([0x51]) }, @@ -639,7 +639,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.outputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing')); (psbt as any).__CACHE.__TX.outs[0].script = payments.p2sh({ redeem: payments.p2wsh({ @@ -655,7 +655,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.outputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey or redeemScript is P2WSH but witnessScript missing')); delete psbt.data.outputs[0].redeemScript; @@ -665,7 +665,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.outputHasPubkey(0, Buffer.from([])); - }, new RegExp('Incomplete script information')); + }, new RegExp('scriptPubkey is P2SH but redeemScript missing')); psbt.updateOutput(0, { redeemScript: payments.p2wsh({ diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 31d15c7..7749827 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1587,7 +1587,7 @@ function pubkeyInInput( } else { throw new Error("Can't find pubkey in input without Utxo data"); } - const meaningfulScript = checkScripts( + const meaningfulScript = getMeaningfulScript( script, input.redeemScript, input.witnessScript, @@ -1602,7 +1602,7 @@ function pubkeyInOutput( cache: PsbtCache, ): boolean { const script = cache.__TX.outs[outputIndex].script; - const meaningfulScript = checkScripts( + const meaningfulScript = getMeaningfulScript( script, output.redeemScript, output.witnessScript, @@ -1610,52 +1610,49 @@ function pubkeyInOutput( return pubkeyInScript(pubkey, meaningfulScript); } -function checkScripts( +function getMeaningfulScript( script: Buffer, redeemScript?: Buffer, witnessScript?: Buffer, ): Buffer { - let fail = false; - if (isP2SHScript(script)) { - if (redeemScript === undefined) { - fail = true; - } else if (isP2WSHScript(redeemScript)) { - if (witnessScript === undefined) { - fail = true; - } else { - fail = !payments - .p2sh({ - redeem: payments.p2wsh({ - redeem: { output: witnessScript }, - }), - }) - .output!.equals(script); - if (!fail) return witnessScript; - } - } else { - fail = !payments - .p2sh({ - redeem: { output: redeemScript }, - }) - .output!.equals(script); - if (!fail) return redeemScript; - } - } else if (isP2WSHScript(script)) { - if (witnessScript === undefined) { - fail = true; - } else { - fail = !payments - .p2wsh({ - redeem: { output: witnessScript }, - }) - .output!.equals(script); - if (!fail) return witnessScript; - } + const { p2sh, p2wsh } = payments; + const isP2SH = isP2SHScript(script); + const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); + const isP2WSH = isP2WSHScript(script); + + if (isP2SH && redeemScript === undefined) + throw new Error('scriptPubkey is P2SH but redeemScript missing'); + if ((isP2WSH || isP2SHP2WSH) && witnessScript === undefined) + throw new Error( + 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', + ); + + let payment: payments.Payment; + let meaningfulScript: Buffer; + + if (isP2SHP2WSH) { + meaningfulScript = witnessScript!; + payment = p2sh({ redeem: p2wsh({ redeem: { output: meaningfulScript } }) }); + if (!payment.redeem!.output!.equals(redeemScript!)) + throw new Error('P2SHP2WSH witnessScript and redeemScript do not match'); + if (!payment.output!.equals(script!)) + throw new Error( + 'P2SHP2WSH witnessScript+redeemScript and scriptPubkey do not match', + ); + } else if (isP2WSH) { + meaningfulScript = witnessScript!; + payment = p2wsh({ redeem: { output: meaningfulScript } }); + if (!payment.output!.equals(script!)) + throw new Error('P2WSH witnessScript and scriptPubkey do not match'); + } else if (isP2SH) { + meaningfulScript = redeemScript!; + payment = p2sh({ redeem: { output: meaningfulScript } }); + if (!payment.output!.equals(script!)) + throw new Error('P2SH redeemScript and scriptPubkey do not match'); + } else { + meaningfulScript = script; } - if (fail) { - throw new Error('Incomplete script information'); - } - return script; + return meaningfulScript; } function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { From 7d09fe5dcb8c8835570ff9341bc48c21d975307a Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 28 Apr 2020 18:50:00 +0900 Subject: [PATCH 07/15] Refactor Psbt logic --- src/psbt.js | 164 ++++++++++++++++++++--------------------------- ts_src/psbt.ts | 169 +++++++++++++++++++++---------------------------- 2 files changed, 143 insertions(+), 190 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 91e0438..d240350 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -764,13 +764,13 @@ function checkTxInputCache(cache, input) { cache.__TX_IN_CACHE[key] = 1; } function scriptCheckerFactory(payment, paymentScriptName) { - return (inputIndex, scriptPubKey, redeemScript) => { + return (inputIndex, scriptPubKey, redeemScript, ioType) => { const redeemScriptOutput = payment({ redeem: { output: redeemScript }, }).output; if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( - `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + `${paymentScriptName} for ${ioType} #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); } }; @@ -877,7 +877,7 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { ); } let hash; - let script; + let prevout; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, @@ -893,83 +893,51 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { ); } const prevoutIndex = unsignedTx.ins[inputIndex].index; - const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript(inputIndex, prevout.script, input.redeemScript); - script = input.redeemScript; - } else { - script = prevout.script; - } - if (isP2WSHScript(script)) { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - input.witnessScript, - prevout.value, - sighashType, - ); - script = input.witnessScript; - } else if (isP2WPKH(script)) { - // P2WPKH uses the P2PKH template for prevoutScript when signing - const signingScript = payments.p2pkh({ hash: script.slice(2) }).output; - hash = unsignedTx.hashForWitnessV0( - inputIndex, - signingScript, - prevout.value, - sighashType, - ); - } else { - hash = unsignedTx.hashForSignature(inputIndex, script, sighashType); - } + prevout = nonWitnessUtxoTx.outs[prevoutIndex]; } else if (input.witnessUtxo) { - let _script; // so we don't shadow the `let script` above - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript( - inputIndex, - input.witnessUtxo.script, - input.redeemScript, - ); - _script = input.redeemScript; - } else { - _script = input.witnessUtxo.script; - } - if (isP2WPKH(_script)) { - // P2WPKH uses the P2PKH template for prevoutScript when signing - const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output; - hash = unsignedTx.hashForWitnessV0( - inputIndex, - signingScript, - input.witnessUtxo.value, - sighashType, - ); - script = _script; - } else if (isP2WSHScript(_script)) { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, _script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - input.witnessScript, - input.witnessUtxo.value, - sighashType, - ); - // want to make sure the script we return is the actual meaningful script - script = input.witnessScript; - } else { - throw new Error( - `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + - `${_script.toString('hex')}`, - ); - } + prevout = input.witnessUtxo; } else { throw new Error('Need a Utxo input item for signing'); } + const { meaningfulScript, type } = getMeaningfulScript( + prevout.script, + inputIndex, + 'input', + input.redeemScript, + input.witnessScript, + ); + if (['p2shp2wsh', 'p2wsh'].indexOf(type) >= 0) { + hash = unsignedTx.hashForWitnessV0( + inputIndex, + meaningfulScript, + prevout.value, + sighashType, + ); + } else if (isP2WPKH(meaningfulScript)) { + // P2WPKH uses the P2PKH template for prevoutScript when signing + const signingScript = payments.p2pkh({ hash: meaningfulScript.slice(2) }) + .output; + hash = unsignedTx.hashForWitnessV0( + inputIndex, + signingScript, + prevout.value, + sighashType, + ); + } else { + // non-segwit + if (input.nonWitnessUtxo === undefined) + throw new Error( + `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + + `${meaningfulScript.toString('hex')}`, + ); + hash = unsignedTx.hashForSignature( + inputIndex, + meaningfulScript, + sighashType, + ); + } return { - script, + script: meaningfulScript, sighashType, hash, }; @@ -1235,8 +1203,10 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { } else { throw new Error("Can't find pubkey in input without Utxo data"); } - const meaningfulScript = getMeaningfulScript( + const { meaningfulScript } = getMeaningfulScript( script, + inputIndex, + 'input', input.redeemScript, input.witnessScript, ); @@ -1244,15 +1214,22 @@ function pubkeyInInput(pubkey, input, inputIndex, cache) { } function pubkeyInOutput(pubkey, output, outputIndex, cache) { const script = cache.__TX.outs[outputIndex].script; - const meaningfulScript = getMeaningfulScript( + const { meaningfulScript } = getMeaningfulScript( script, + outputIndex, + 'output', output.redeemScript, output.witnessScript, ); return pubkeyInScript(pubkey, meaningfulScript); } -function getMeaningfulScript(script, redeemScript, witnessScript) { - const { p2sh, p2wsh } = payments; +function getMeaningfulScript( + script, + index, + ioType, + redeemScript, + witnessScript, +) { const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); @@ -1262,31 +1239,30 @@ function getMeaningfulScript(script, redeemScript, witnessScript) { throw new Error( 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', ); - let payment; let meaningfulScript; if (isP2SHP2WSH) { meaningfulScript = witnessScript; - payment = p2sh({ redeem: p2wsh({ redeem: { output: meaningfulScript } }) }); - if (!payment.redeem.output.equals(redeemScript)) - throw new Error('P2SHP2WSH witnessScript and redeemScript do not match'); - if (!payment.output.equals(script)) - throw new Error( - 'P2SHP2WSH witnessScript+redeemScript and scriptPubkey do not match', - ); + checkRedeemScript(index, script, redeemScript, ioType); + checkWitnessScript(index, redeemScript, witnessScript, ioType); } else if (isP2WSH) { meaningfulScript = witnessScript; - payment = p2wsh({ redeem: { output: meaningfulScript } }); - if (!payment.output.equals(script)) - throw new Error('P2WSH witnessScript and scriptPubkey do not match'); + checkWitnessScript(index, script, witnessScript, ioType); } else if (isP2SH) { meaningfulScript = redeemScript; - payment = p2sh({ redeem: { output: meaningfulScript } }); - if (!payment.output.equals(script)) - throw new Error('P2SH redeemScript and scriptPubkey do not match'); + checkRedeemScript(index, script, redeemScript, ioType); } else { meaningfulScript = script; } - return meaningfulScript; + return { + meaningfulScript, + type: isP2SHP2WSH + ? 'p2shp2wsh' + : isP2SH + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', + }; } function pubkeyInScript(pubkey, script) { const pubkeyHash = crypto_1.hash160(pubkey); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 7749827..13e1286 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -984,11 +984,12 @@ function checkTxInputCache( function scriptCheckerFactory( payment: any, paymentScriptName: string, -): (idx: number, spk: Buffer, rs: Buffer) => void { +): (idx: number, spk: Buffer, rs: Buffer, ioType: 'input' | 'output') => void { return ( inputIndex: number, scriptPubKey: Buffer, redeemScript: Buffer, + ioType: 'input' | 'output', ): void => { const redeemScriptOutput = payment({ redeem: { output: redeemScript }, @@ -996,7 +997,7 @@ function scriptCheckerFactory( if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( - `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + `${paymentScriptName} for ${ioType} #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); } }; @@ -1158,7 +1159,7 @@ function getHashForSig( ); } let hash: Buffer; - let script: Buffer; + let prevout: Output; if (input.nonWitnessUtxo) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( @@ -1178,85 +1179,54 @@ function getHashForSig( } const prevoutIndex = unsignedTx.ins[inputIndex].index; - const prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output; - - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript(inputIndex, prevout.script, input.redeemScript); - script = input.redeemScript; - } else { - script = prevout.script; - } - - if (isP2WSHScript(script)) { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - input.witnessScript, - prevout.value, - sighashType, - ); - script = input.witnessScript; - } else if (isP2WPKH(script)) { - // P2WPKH uses the P2PKH template for prevoutScript when signing - const signingScript = payments.p2pkh({ hash: script.slice(2) }).output!; - hash = unsignedTx.hashForWitnessV0( - inputIndex, - signingScript, - prevout.value, - sighashType, - ); - } else { - hash = unsignedTx.hashForSignature(inputIndex, script, sighashType); - } + prevout = nonWitnessUtxoTx.outs[prevoutIndex] as Output; } else if (input.witnessUtxo) { - let _script: Buffer; // so we don't shadow the `let script` above - if (input.redeemScript) { - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - checkRedeemScript( - inputIndex, - input.witnessUtxo.script, - input.redeemScript, - ); - _script = input.redeemScript; - } else { - _script = input.witnessUtxo.script; - } - if (isP2WPKH(_script)) { - // P2WPKH uses the P2PKH template for prevoutScript when signing - const signingScript = payments.p2pkh({ hash: _script.slice(2) }).output!; - hash = unsignedTx.hashForWitnessV0( - inputIndex, - signingScript, - input.witnessUtxo.value, - sighashType, - ); - script = _script; - } else if (isP2WSHScript(_script)) { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, _script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - input.witnessScript, - input.witnessUtxo.value, - sighashType, - ); - // want to make sure the script we return is the actual meaningful script - script = input.witnessScript; - } else { - throw new Error( - `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + - `${_script.toString('hex')}`, - ); - } + prevout = input.witnessUtxo; } else { throw new Error('Need a Utxo input item for signing'); } + + const { meaningfulScript, type } = getMeaningfulScript( + prevout.script, + inputIndex, + 'input', + input.redeemScript, + input.witnessScript, + ); + + if (['p2shp2wsh', 'p2wsh'].indexOf(type) >= 0) { + hash = unsignedTx.hashForWitnessV0( + inputIndex, + meaningfulScript, + prevout.value, + sighashType, + ); + } else if (isP2WPKH(meaningfulScript)) { + // P2WPKH uses the P2PKH template for prevoutScript when signing + const signingScript = payments.p2pkh({ hash: meaningfulScript.slice(2) }) + .output!; + hash = unsignedTx.hashForWitnessV0( + inputIndex, + signingScript, + prevout.value, + sighashType, + ); + } else { + // non-segwit + if (input.nonWitnessUtxo === undefined) + throw new Error( + `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + + `${meaningfulScript.toString('hex')}`, + ); + hash = unsignedTx.hashForSignature( + inputIndex, + meaningfulScript, + sighashType, + ); + } + return { - script, + script: meaningfulScript, sighashType, hash, }; @@ -1587,8 +1557,10 @@ function pubkeyInInput( } else { throw new Error("Can't find pubkey in input without Utxo data"); } - const meaningfulScript = getMeaningfulScript( + const { meaningfulScript } = getMeaningfulScript( script, + inputIndex, + 'input', input.redeemScript, input.witnessScript, ); @@ -1602,8 +1574,10 @@ function pubkeyInOutput( cache: PsbtCache, ): boolean { const script = cache.__TX.outs[outputIndex].script; - const meaningfulScript = getMeaningfulScript( + const { meaningfulScript } = getMeaningfulScript( script, + outputIndex, + 'output', output.redeemScript, output.witnessScript, ); @@ -1612,10 +1586,14 @@ function pubkeyInOutput( function getMeaningfulScript( script: Buffer, + index: number, + ioType: 'input' | 'output', redeemScript?: Buffer, witnessScript?: Buffer, -): Buffer { - const { p2sh, p2wsh } = payments; +): { + meaningfulScript: Buffer; + type: 'p2sh' | 'p2wsh' | 'p2shp2wsh' | 'raw'; +} { const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); const isP2WSH = isP2WSHScript(script); @@ -1627,32 +1605,31 @@ function getMeaningfulScript( 'scriptPubkey or redeemScript is P2WSH but witnessScript missing', ); - let payment: payments.Payment; let meaningfulScript: Buffer; if (isP2SHP2WSH) { meaningfulScript = witnessScript!; - payment = p2sh({ redeem: p2wsh({ redeem: { output: meaningfulScript } }) }); - if (!payment.redeem!.output!.equals(redeemScript!)) - throw new Error('P2SHP2WSH witnessScript and redeemScript do not match'); - if (!payment.output!.equals(script!)) - throw new Error( - 'P2SHP2WSH witnessScript+redeemScript and scriptPubkey do not match', - ); + checkRedeemScript(index, script, redeemScript!, ioType); + checkWitnessScript(index, redeemScript!, witnessScript!, ioType); } else if (isP2WSH) { meaningfulScript = witnessScript!; - payment = p2wsh({ redeem: { output: meaningfulScript } }); - if (!payment.output!.equals(script!)) - throw new Error('P2WSH witnessScript and scriptPubkey do not match'); + checkWitnessScript(index, script, witnessScript!, ioType); } else if (isP2SH) { meaningfulScript = redeemScript!; - payment = p2sh({ redeem: { output: meaningfulScript } }); - if (!payment.output!.equals(script!)) - throw new Error('P2SH redeemScript and scriptPubkey do not match'); + checkRedeemScript(index, script, redeemScript!, ioType); } else { meaningfulScript = script; } - return meaningfulScript; + return { + meaningfulScript, + type: isP2SHP2WSH + ? 'p2shp2wsh' + : isP2SH + ? 'p2sh' + : isP2WSH + ? 'p2wsh' + : 'raw', + }; } function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { From 0c52803ba1fe5ae8dd3b84ad50bd84b0878d17de Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 28 Apr 2020 18:52:43 +0900 Subject: [PATCH 08/15] Add discouraged unsafe nonsegwit signing --- src/psbt.js | 35 +++++++++++++++++++++++++++++++++-- ts_src/psbt.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index d240350..5958b1e 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -69,6 +69,14 @@ class Psbt { __NON_WITNESS_UTXO_BUF_CACHE: [], __TX_IN_CACHE: {}, __TX: this.data.globalMap.unsignedTx.tx, + // Old TransactionBuilder behavior was to not confirm input values + // before signing. Even though we highly encourage people to get + // the full parent transaction to verify values, the ability to + // sign non-segwit inputs without the full transaction was often + // requested. So the only way to activate is to use @ts-ignore. + // We will disable exporting the Psbt when unsafe sign is active. + // because it is not BIP174 compliant. + __UNSAFE_SIGN_NONSEGWIT: false, }; if (this.data.inputs.length === 0) this.setVersion(2); // Make data hidden when enumerating @@ -313,6 +321,7 @@ class Psbt { inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), this.__CACHE, + true, ) : { hash: hashCache, script: scriptCache }; sighashCache = sig.hashType; @@ -513,12 +522,15 @@ class Psbt { }); } toBuffer() { + checkCache(this.__CACHE); return this.data.toBuffer(); } toHex() { + checkCache(this.__CACHE); return this.data.toHex(); } toBase64() { + checkCache(this.__CACHE); return this.data.toBase64(); } updateGlobal(updateData) { @@ -626,6 +638,11 @@ function canFinalize(input, script, scriptType) { return false; } } +function checkCache(cache) { + if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) { + throw new Error('Not BIP174 compliant, can not export'); + } +} function hasSigs(neededSigs, partialSig, pubkeys) { if (!partialSig) return false; let sigs; @@ -857,6 +874,7 @@ function getHashAndSighashType( inputIndex, input, cache, + false, sighashTypes, ); checkScriptForPubkey(pubkey, script, 'sign'); @@ -865,7 +883,7 @@ function getHashAndSighashType( sighashType, }; } -function getHashForSig(inputIndex, input, cache, sighashTypes) { +function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { const unsignedTx = cache.__TX; const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; @@ -925,11 +943,24 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { ); } else { // non-segwit - if (input.nonWitnessUtxo === undefined) + if ( + input.nonWitnessUtxo === undefined && + cache.__UNSAFE_SIGN_NONSEGWIT === false + ) throw new Error( `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + `${meaningfulScript.toString('hex')}`, ); + if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false) + console.warn( + 'Warning: Signing non-segwit inputs without the full parent transaction ' + + 'means there is a chance that a miner could feed you incorrect information ' + + 'to trick you into paying large fees. This behavior is the same as the old ' + + 'TransactionBuilder class when signing non-segwit scripts. You are not ' + + 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + + 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + + '*********************', + ); hash = unsignedTx.hashForSignature( inputIndex, meaningfulScript, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 13e1286..23bdc1d 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -115,6 +115,14 @@ export class Psbt { __NON_WITNESS_UTXO_BUF_CACHE: [], __TX_IN_CACHE: {}, __TX: (this.data.globalMap.unsignedTx as PsbtTransaction).tx, + // Old TransactionBuilder behavior was to not confirm input values + // before signing. Even though we highly encourage people to get + // the full parent transaction to verify values, the ability to + // sign non-segwit inputs without the full transaction was often + // requested. So the only way to activate is to use @ts-ignore. + // We will disable exporting the Psbt when unsafe sign is active. + // because it is not BIP174 compliant. + __UNSAFE_SIGN_NONSEGWIT: false, }; if (this.data.inputs.length === 0) this.setVersion(2); @@ -386,6 +394,7 @@ export class Psbt { inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), this.__CACHE, + true, ) : { hash: hashCache!, script: scriptCache! }; sighashCache = sig.hashType; @@ -619,14 +628,17 @@ export class Psbt { } toBuffer(): Buffer { + checkCache(this.__CACHE); return this.data.toBuffer(); } toHex(): string { + checkCache(this.__CACHE); return this.data.toHex(); } toBase64(): string { + checkCache(this.__CACHE); return this.data.toBase64(); } @@ -681,6 +693,7 @@ interface PsbtCache { __FEE_RATE?: number; __FEE?: number; __EXTRACTED_TX?: Transaction; + __UNSAFE_SIGN_NONSEGWIT: boolean; } interface PsbtOptsOptional { @@ -825,6 +838,12 @@ function canFinalize( } } +function checkCache(cache: PsbtCache): void { + if (cache.__UNSAFE_SIGN_NONSEGWIT !== false) { + throw new Error('Not BIP174 compliant, can not export'); + } +} + function hasSigs( neededSigs: number, partialSig?: any[], @@ -1130,6 +1149,7 @@ function getHashAndSighashType( inputIndex, input, cache, + false, sighashTypes, ); checkScriptForPubkey(pubkey, script, 'sign'); @@ -1143,6 +1163,7 @@ function getHashForSig( inputIndex: number, input: PsbtInput, cache: PsbtCache, + forValidate: boolean, sighashTypes?: number[], ): { script: Buffer; @@ -1213,11 +1234,24 @@ function getHashForSig( ); } else { // non-segwit - if (input.nonWitnessUtxo === undefined) + if ( + input.nonWitnessUtxo === undefined && + cache.__UNSAFE_SIGN_NONSEGWIT === false + ) throw new Error( `Input #${inputIndex} has witnessUtxo but non-segwit script: ` + `${meaningfulScript.toString('hex')}`, ); + if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false) + console.warn( + 'Warning: Signing non-segwit inputs without the full parent transaction ' + + 'means there is a chance that a miner could feed you incorrect information ' + + 'to trick you into paying large fees. This behavior is the same as the old ' + + 'TransactionBuilder class when signing non-segwit scripts. You are not ' + + 'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' + + 'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' + + '*********************', + ); hash = unsignedTx.hashForSignature( inputIndex, meaningfulScript, From d02483473b26f9fd9f24fe11bd1c31d1bb9bc90d Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 28 Apr 2020 19:58:05 +0700 Subject: [PATCH 09/15] allocUnsafe for faster buffer cloning It's safe to do this because we immediately overwrite the entire buffer. No need to zero out first. --- src/bufferutils.js | 2 +- ts_src/bufferutils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bufferutils.js b/src/bufferutils.js index 2ee3542..a68fd31 100644 --- a/src/bufferutils.js +++ b/src/bufferutils.js @@ -42,7 +42,7 @@ function reverseBuffer(buffer) { } exports.reverseBuffer = reverseBuffer; function cloneBuffer(buffer) { - const clone = Buffer.alloc(buffer.length); + const clone = Buffer.allocUnsafe(buffer.length); buffer.copy(clone); return clone; } diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index 2025f88..9005f2a 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -49,7 +49,7 @@ export function reverseBuffer(buffer: Buffer): Buffer { } export function cloneBuffer(buffer: Buffer): Buffer { - const clone = Buffer.alloc(buffer.length); + const clone = Buffer.allocUnsafe(buffer.length); buffer.copy(clone); return clone; } From c9f399e509c5375580e9c196e3c2c5eeb59aa053 Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 29 Apr 2020 11:05:33 +0900 Subject: [PATCH 10/15] Add getInputType --- src/psbt.js | 37 ++++++++++++++--- test/fixtures/psbt.json | 18 +++++++++ test/psbt.spec.ts | 89 +++++++++++++++++++++++++++++++++++++++++ ts_src/psbt.ts | 79 +++++++++++++++++++++++++++++++----- types/psbt.d.ts | 2 + 5 files changed, 208 insertions(+), 17 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 5958b1e..7cab795 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -189,6 +189,7 @@ class Psbt { ); } checkInputsForPartialSig(this.data.inputs, 'addInput'); + if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); const c = this.__CACHE; this.data.addInput(inputData); const txIn = c.__TX.ins[c.__TX.ins.length - 1]; @@ -285,6 +286,20 @@ class Psbt { this.data.clearFinalizedInput(inputIndex); return this; } + getInputType(inputIndex) { + const input = utils_1.checkForInput(this.data.inputs, inputIndex); + const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); + const result = getMeaningfulScript( + script, + inputIndex, + 'input', + input.redeemScript, + input.witnessScript, + ); + const type = result.type === 'raw' ? '' : result.type + '-'; + const mainType = classifyScript(result.meaningfulScript); + return type + mainType; + } inputHasPubkey(inputIndex, pubkey) { const input = utils_1.checkForInput(this.data.inputs, inputIndex); return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); @@ -538,6 +553,7 @@ class Psbt { return this; } updateInput(inputIndex, updateData) { + if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); this.data.updateInput(inputIndex, updateData); if (updateData.nonWitnessUtxo) { addNonWitnessTxCache( @@ -924,7 +940,7 @@ function getHashForSig(inputIndex, input, cache, forValidate, sighashTypes) { input.redeemScript, input.witnessScript, ); - if (['p2shp2wsh', 'p2wsh'].indexOf(type) >= 0) { + if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( inputIndex, meaningfulScript, @@ -1220,20 +1236,22 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { } return c[inputIndex]; } -function pubkeyInInput(pubkey, input, inputIndex, cache) { - let script; +function getScriptFromUtxo(inputIndex, input, cache) { if (input.witnessUtxo !== undefined) { - script = input.witnessUtxo.script; + return input.witnessUtxo.script; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); - script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; } else { throw new Error("Can't find pubkey in input without Utxo data"); } +} +function pubkeyInInput(pubkey, input, inputIndex, cache) { + const script = getScriptFromUtxo(inputIndex, input, cache); const { meaningfulScript } = getMeaningfulScript( script, inputIndex, @@ -1275,9 +1293,11 @@ function getMeaningfulScript( meaningfulScript = witnessScript; checkRedeemScript(index, script, redeemScript, ioType); checkWitnessScript(index, redeemScript, witnessScript, ioType); + checkInvalidP2WSH(meaningfulScript); } else if (isP2WSH) { meaningfulScript = witnessScript; checkWitnessScript(index, script, witnessScript, ioType); + checkInvalidP2WSH(meaningfulScript); } else if (isP2SH) { meaningfulScript = redeemScript; checkRedeemScript(index, script, redeemScript, ioType); @@ -1287,7 +1307,7 @@ function getMeaningfulScript( return { meaningfulScript, type: isP2SHP2WSH - ? 'p2shp2wsh' + ? 'p2sh-p2wsh' : isP2SH ? 'p2sh' : isP2WSH @@ -1295,6 +1315,11 @@ function getMeaningfulScript( : 'raw', }; } +function checkInvalidP2WSH(script) { + if (isP2WPKH(script) || isP2SHScript(script)) { + throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); + } +} function pubkeyInScript(pubkey, script) { const pubkeyHash = crypto_1.hash160(pubkey); const decompiled = bscript.decompile(script); diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index e3062e8..0e51d57 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -313,6 +313,24 @@ }, "exception": "Invalid arguments for Psbt\\.addInput\\. Requires single object with at least \\[hash\\] and \\[index\\]" }, + { + "description": "checks for invalid p2wsh witnessScript", + "inputData": { + "hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')", + "index": 0, + "witnessScript": "Buffer.from('0014000102030405060708090a0b0c0d0e0f00010203', 'hex')" + }, + "exception": "P2WPKH or P2SH can not be contained within P2WSH" + }, + { + "description": "checks for invalid p2wsh witnessScript", + "inputData": { + "hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')", + "index": 0, + "witnessScript": "Buffer.from('a914000102030405060708090a0b0c0d0e0f0001020387', 'hex')" + }, + "exception": "P2WPKH or P2SH can not be contained within P2WSH" + }, { "description": "should be equal", "inputData": { diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index ff2131b..5e88fe0 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -542,6 +542,95 @@ describe(`Psbt`, () => { }); }); + describe('getInputType', () => { + const { publicKey } = ECPair.makeRandom(); + const p2wpkhPub = (pubkey: Buffer): Buffer => + payments.p2wpkh({ + pubkey, + }).output!; + const p2pkhPub = (pubkey: Buffer): Buffer => + payments.p2pkh({ + pubkey, + }).output!; + const p2shOut = (output: Buffer): Buffer => + payments.p2sh({ + redeem: { output }, + }).output!; + const p2wshOut = (output: Buffer): Buffer => + payments.p2wsh({ + redeem: { output }, + }).output!; + const p2shp2wshOut = (output: Buffer): Buffer => p2shOut(p2wshOut(output)); + const noOuter = (output: Buffer): Buffer => output; + + function getInputTypeTest({ + innerScript, + outerScript, + redeemGetter, + witnessGetter, + expectedType, + }: any): void { + const psbt = new Psbt(); + psbt.addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + witnessUtxo: { + script: outerScript(innerScript(publicKey)), + value: 2e3, + }, + ...(redeemGetter ? { redeemScript: redeemGetter(publicKey) } : {}), + ...(witnessGetter ? { witnessScript: witnessGetter(publicKey) } : {}), + }); + const type = psbt.getInputType(0); + assert.strictEqual(type, expectedType, 'incorrect input type'); + } + [ + { + innerScript: p2pkhPub, + outerScript: noOuter, + redeemGetter: null, + witnessGetter: null, + expectedType: 'pubkeyhash', + }, + { + innerScript: p2wpkhPub, + outerScript: noOuter, + redeemGetter: null, + witnessGetter: null, + expectedType: 'witnesspubkeyhash', + }, + { + innerScript: p2pkhPub, + outerScript: p2shOut, + redeemGetter: p2pkhPub, + witnessGetter: null, + expectedType: 'p2sh-pubkeyhash', + }, + { + innerScript: p2wpkhPub, + outerScript: p2shOut, + redeemGetter: p2wpkhPub, + witnessGetter: null, + expectedType: 'p2sh-witnesspubkeyhash', + }, + { + innerScript: p2pkhPub, + outerScript: p2wshOut, + redeemGetter: null, + witnessGetter: p2pkhPub, + expectedType: 'p2wsh-pubkeyhash', + }, + { + innerScript: p2pkhPub, + outerScript: p2shp2wshOut, + redeemGetter: (pk: Buffer): Buffer => p2wshOut(p2pkhPub(pk)), + witnessGetter: p2pkhPub, + expectedType: 'p2sh-p2wsh-pubkeyhash', + }, + ].forEach(getInputTypeTest); + }); + describe('inputHasPubkey', () => { it('should throw', () => { const psbt = new Psbt(); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 23bdc1d..cb14fc5 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -242,6 +242,7 @@ export class Psbt { ); } checkInputsForPartialSig(this.data.inputs, 'addInput'); + if (inputData.witnessScript) checkInvalidP2WSH(inputData.witnessScript); const c = this.__CACHE; this.data.addInput(inputData); const txIn = c.__TX.ins[c.__TX.ins.length - 1]; @@ -355,6 +356,21 @@ export class Psbt { return this; } + getInputType(inputIndex: number): AllScriptType { + const input = checkForInput(this.data.inputs, inputIndex); + const script = getScriptFromUtxo(inputIndex, input, this.__CACHE); + const result = getMeaningfulScript( + script, + inputIndex, + 'input', + input.redeemScript, + input.witnessScript, + ); + const type = result.type === 'raw' ? '' : result.type + '-'; + const mainType = classifyScript(result.meaningfulScript); + return (type + mainType) as AllScriptType; + } + inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean { const input = checkForInput(this.data.inputs, inputIndex); return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); @@ -648,6 +664,7 @@ export class Psbt { } updateInput(inputIndex: number, updateData: PsbtInputUpdate): this { + if (updateData.witnessScript) checkInvalidP2WSH(updateData.witnessScript); this.data.updateInput(inputIndex, updateData); if (updateData.nonWitnessUtxo) { addNonWitnessTxCache( @@ -1215,7 +1232,7 @@ function getHashForSig( input.witnessScript, ); - if (['p2shp2wsh', 'p2wsh'].indexOf(type) >= 0) { + if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) { hash = unsignedTx.hashForWitnessV0( inputIndex, meaningfulScript, @@ -1572,25 +1589,32 @@ function nonWitnessUtxoTxFromCache( return c[inputIndex]; } -function pubkeyInInput( - pubkey: Buffer, - input: PsbtInput, +function getScriptFromUtxo( inputIndex: number, + input: PsbtInput, cache: PsbtCache, -): boolean { - let script: Buffer; +): Buffer { if (input.witnessUtxo !== undefined) { - script = input.witnessUtxo.script; + return input.witnessUtxo.script; } else if (input.nonWitnessUtxo !== undefined) { const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( cache, input, inputIndex, ); - script = nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; + return nonWitnessUtxoTx.outs[cache.__TX.ins[inputIndex].index].script; } else { throw new Error("Can't find pubkey in input without Utxo data"); } +} + +function pubkeyInInput( + pubkey: Buffer, + input: PsbtInput, + inputIndex: number, + cache: PsbtCache, +): boolean { + const script = getScriptFromUtxo(inputIndex, input, cache); const { meaningfulScript } = getMeaningfulScript( script, inputIndex, @@ -1626,7 +1650,7 @@ function getMeaningfulScript( witnessScript?: Buffer, ): { meaningfulScript: Buffer; - type: 'p2sh' | 'p2wsh' | 'p2shp2wsh' | 'raw'; + type: 'p2sh' | 'p2wsh' | 'p2sh-p2wsh' | 'raw'; } { const isP2SH = isP2SHScript(script); const isP2SHP2WSH = isP2SH && redeemScript && isP2WSHScript(redeemScript); @@ -1645,9 +1669,11 @@ function getMeaningfulScript( meaningfulScript = witnessScript!; checkRedeemScript(index, script, redeemScript!, ioType); checkWitnessScript(index, redeemScript!, witnessScript!, ioType); + checkInvalidP2WSH(meaningfulScript); } else if (isP2WSH) { meaningfulScript = witnessScript!; checkWitnessScript(index, script, witnessScript!, ioType); + checkInvalidP2WSH(meaningfulScript); } else if (isP2SH) { meaningfulScript = redeemScript!; checkRedeemScript(index, script, redeemScript!, ioType); @@ -1657,7 +1683,7 @@ function getMeaningfulScript( return { meaningfulScript, type: isP2SHP2WSH - ? 'p2shp2wsh' + ? 'p2sh-p2wsh' : isP2SH ? 'p2sh' : isP2WSH @@ -1666,6 +1692,12 @@ function getMeaningfulScript( }; } +function checkInvalidP2WSH(script: Buffer): void { + if (isP2WPKH(script) || isP2SHScript(script)) { + throw new Error('P2WPKH or P2SH can not be contained within P2WSH'); + } +} + function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { const pubkeyHash = hash160(pubkey); @@ -1678,7 +1710,32 @@ function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean { }); } -function classifyScript(script: Buffer): string { +type AllScriptType = + | 'witnesspubkeyhash' + | 'pubkeyhash' + | 'multisig' + | 'pubkey' + | 'nonstandard' + | 'p2sh-witnesspubkeyhash' + | 'p2sh-pubkeyhash' + | 'p2sh-multisig' + | 'p2sh-pubkey' + | 'p2sh-nonstandard' + | 'p2wsh-pubkeyhash' + | 'p2wsh-multisig' + | 'p2wsh-pubkey' + | 'p2wsh-nonstandard' + | 'p2sh-p2wsh-pubkeyhash' + | 'p2sh-p2wsh-multisig' + | 'p2sh-p2wsh-pubkey' + | 'p2sh-p2wsh-nonstandard'; +type ScriptType = + | 'witnesspubkeyhash' + | 'pubkeyhash' + | 'multisig' + | 'pubkey' + | 'nonstandard'; +function classifyScript(script: Buffer): ScriptType { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 127ef0f..4d1c099 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -69,6 +69,7 @@ export declare class Psbt { getFee(): number; finalizeAllInputs(): this; finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; + getInputType(inputIndex: number): AllScriptType; inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean; outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean; validateSignaturesOfAllInputs(): boolean; @@ -151,4 +152,5 @@ isP2WSH: boolean) => { finalScriptSig: Buffer | undefined; finalScriptWitness: Buffer | undefined; }; +declare type AllScriptType = 'witnesspubkeyhash' | 'pubkeyhash' | 'multisig' | 'pubkey' | 'nonstandard' | 'p2sh-witnesspubkeyhash' | 'p2sh-pubkeyhash' | 'p2sh-multisig' | 'p2sh-pubkey' | 'p2sh-nonstandard' | 'p2wsh-pubkeyhash' | 'p2wsh-multisig' | 'p2wsh-pubkey' | 'p2wsh-nonstandard' | 'p2sh-p2wsh-pubkeyhash' | 'p2sh-p2wsh-multisig' | 'p2sh-p2wsh-pubkey' | 'p2sh-p2wsh-nonstandard'; export {}; From 5d19abfb85641887060804a2a396eacd6fea9117 Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 29 Apr 2020 13:32:57 +0900 Subject: [PATCH 11/15] Add ability to get redeemScript|witnessScript from finalized scripts --- src/psbt.js | 35 +++++++++++++++++++++++++++++++++-- test/psbt.spec.ts | 34 ++++++++++++++++++++++------------ ts_src/psbt.ts | 43 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 7cab795..b80951f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -293,8 +293,9 @@ class Psbt { script, inputIndex, 'input', - input.redeemScript, - input.witnessScript, + input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), + input.witnessScript || + redeemFromFinalWitnessScript(input.finalScriptWitness), ); const type = result.type === 'raw' ? '' : result.type + '-'; const mainType = classifyScript(result.meaningfulScript); @@ -1272,6 +1273,36 @@ function pubkeyInOutput(pubkey, output, outputIndex, cache) { ); return pubkeyInScript(pubkey, meaningfulScript); } +function redeemFromFinalScriptSig(finalScript) { + if (!finalScript) return; + const decomp = bscript.decompile(finalScript); + if (!decomp) return; + const lastItem = decomp[decomp.length - 1]; + if ( + !Buffer.isBuffer(lastItem) || + isPubkeyLike(lastItem) || + isSigLike(lastItem) + ) + return; + const sDecomp = bscript.decompile(lastItem); + if (!sDecomp) return; + return lastItem; +} +function redeemFromFinalWitnessScript(finalScript) { + if (!finalScript) return; + const decomp = scriptWitnessToWitnessStack(finalScript); + const lastItem = decomp[decomp.length - 1]; + if (isPubkeyLike(lastItem)) return; + const sDecomp = bscript.decompile(lastItem); + if (!sDecomp) return; + return lastItem; +} +function isPubkeyLike(buf) { + return buf.length === 33 && bscript.isCanonicalPubKey(buf); +} +function isSigLike(buf) { + return bscript.isCanonicalScriptSignature(buf); +} function getMeaningfulScript( script, index, diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index 5e88fe0..a5a2214 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -543,7 +543,8 @@ describe(`Psbt`, () => { }); describe('getInputType', () => { - const { publicKey } = ECPair.makeRandom(); + const key = ECPair.makeRandom(); + const { publicKey } = key; const p2wpkhPub = (pubkey: Buffer): Buffer => payments.p2wpkh({ pubkey, @@ -569,19 +570,26 @@ describe(`Psbt`, () => { redeemGetter, witnessGetter, expectedType, + finalize, }: any): void { const psbt = new Psbt(); - psbt.addInput({ - hash: - '0000000000000000000000000000000000000000000000000000000000000000', - index: 0, - witnessUtxo: { - script: outerScript(innerScript(publicKey)), - value: 2e3, - }, - ...(redeemGetter ? { redeemScript: redeemGetter(publicKey) } : {}), - ...(witnessGetter ? { witnessScript: witnessGetter(publicKey) } : {}), - }); + psbt + .addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + witnessUtxo: { + script: outerScript(innerScript(publicKey)), + value: 2e3, + }, + ...(redeemGetter ? { redeemScript: redeemGetter(publicKey) } : {}), + ...(witnessGetter ? { witnessScript: witnessGetter(publicKey) } : {}), + }) + .addOutput({ + script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d'), + value: 1800, + }); + if (finalize) psbt.signInput(0, key).finalizeInput(0); const type = psbt.getInputType(0); assert.strictEqual(type, expectedType, 'incorrect input type'); } @@ -613,6 +621,7 @@ describe(`Psbt`, () => { redeemGetter: p2wpkhPub, witnessGetter: null, expectedType: 'p2sh-witnesspubkeyhash', + finalize: true, }, { innerScript: p2pkhPub, @@ -620,6 +629,7 @@ describe(`Psbt`, () => { redeemGetter: null, witnessGetter: p2pkhPub, expectedType: 'p2wsh-pubkeyhash', + finalize: true, }, { innerScript: p2pkhPub, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index cb14fc5..39d3a4c 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -363,8 +363,9 @@ export class Psbt { script, inputIndex, 'input', - input.redeemScript, - input.witnessScript, + input.redeemScript || redeemFromFinalScriptSig(input.finalScriptSig), + input.witnessScript || + redeemFromFinalWitnessScript(input.finalScriptWitness), ); const type = result.type === 'raw' ? '' : result.type + '-'; const mainType = classifyScript(result.meaningfulScript); @@ -1642,6 +1643,44 @@ function pubkeyInOutput( return pubkeyInScript(pubkey, meaningfulScript); } +function redeemFromFinalScriptSig( + finalScript: Buffer | undefined, +): Buffer | undefined { + if (!finalScript) return; + const decomp = bscript.decompile(finalScript); + if (!decomp) return; + const lastItem = decomp[decomp.length - 1]; + if ( + !Buffer.isBuffer(lastItem) || + isPubkeyLike(lastItem) || + isSigLike(lastItem) + ) + return; + const sDecomp = bscript.decompile(lastItem); + if (!sDecomp) return; + return lastItem; +} + +function redeemFromFinalWitnessScript( + finalScript: Buffer | undefined, +): Buffer | undefined { + if (!finalScript) return; + const decomp = scriptWitnessToWitnessStack(finalScript); + const lastItem = decomp[decomp.length - 1]; + if (isPubkeyLike(lastItem)) return; + const sDecomp = bscript.decompile(lastItem); + if (!sDecomp) return; + return lastItem; +} + +function isPubkeyLike(buf: Buffer): boolean { + return buf.length === 33 && bscript.isCanonicalPubKey(buf); +} + +function isSigLike(buf: Buffer): boolean { + return bscript.isCanonicalScriptSignature(buf); +} + function getMeaningfulScript( script: Buffer, index: number, From f87a20caa7828f6f8c29049c73efd369ff81c57b Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 29 Apr 2020 14:39:50 +0900 Subject: [PATCH 12/15] Add hasHDKey --- src/psbt.js | 21 ++++++++++++++++++ test/psbt.spec.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++ ts_src/psbt.ts | 27 +++++++++++++++++++++++ types/psbt.d.ts | 2 ++ 4 files changed, 105 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index b80951f..693bfc3 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -305,10 +305,24 @@ class Psbt { const input = utils_1.checkForInput(this.data.inputs, inputIndex); return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); } + inputHasHDKey(inputIndex, root) { + const input = utils_1.checkForInput(this.data.inputs, inputIndex); + const derivationIsMine = bip32DerivationIsMine(root); + return ( + !!input.bip32Derivation && input.bip32Derivation.some(derivationIsMine) + ); + } outputHasPubkey(outputIndex, pubkey) { const output = utils_1.checkForOutput(this.data.outputs, outputIndex); return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE); } + outputHasHDKey(outputIndex, root) { + const output = utils_1.checkForOutput(this.data.outputs, outputIndex); + const derivationIsMine = bip32DerivationIsMine(root); + return ( + !!output.bip32Derivation && output.bip32Derivation.some(derivationIsMine) + ); + } validateSignaturesOfAllInputs() { utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => @@ -696,6 +710,13 @@ const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); +function bip32DerivationIsMine(root) { + return d => { + if (!d.masterFingerprint.equals(root.fingerprint)) return false; + if (!root.derivePath(d.path).publicKey.equals(d.pubkey)) return false; + return true; + }; +} function check32Bit(num) { if ( typeof num !== 'number' || diff --git a/test/psbt.spec.ts b/test/psbt.spec.ts index a5a2214..c33e9cf 100644 --- a/test/psbt.spec.ts +++ b/test/psbt.spec.ts @@ -1,4 +1,5 @@ import * as assert from 'assert'; +import * as crypto from 'crypto'; import { describe, it } from 'mocha'; import { bip32, ECPair, networks as NETWORKS, payments, Psbt } from '..'; @@ -641,6 +642,29 @@ describe(`Psbt`, () => { ].forEach(getInputTypeTest); }); + describe('inputHasHDKey', () => { + it('should return true if HD key is present', () => { + const root = bip32.fromSeed(crypto.randomBytes(32)); + const root2 = bip32.fromSeed(crypto.randomBytes(32)); + const path = "m/0'/0"; + const psbt = new Psbt(); + psbt.addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + bip32Derivation: [ + { + masterFingerprint: root.fingerprint, + path, + pubkey: root.derivePath(path).publicKey, + }, + ], + }); + assert.strictEqual(psbt.inputHasHDKey(0, root), true); + assert.strictEqual(psbt.inputHasHDKey(0, root2), false); + }); + }); + describe('inputHasPubkey', () => { it('should throw', () => { const psbt = new Psbt(); @@ -712,6 +736,37 @@ describe(`Psbt`, () => { }); }); + describe('outputHasHDKey', () => { + it('should return true if HD key is present', () => { + const root = bip32.fromSeed(crypto.randomBytes(32)); + const root2 = bip32.fromSeed(crypto.randomBytes(32)); + const path = "m/0'/0"; + const psbt = new Psbt(); + psbt + .addInput({ + hash: + '0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + }) + .addOutput({ + script: Buffer.from( + '0014000102030405060708090a0b0c0d0e0f00010203', + 'hex', + ), + value: 2000, + bip32Derivation: [ + { + masterFingerprint: root.fingerprint, + path, + pubkey: root.derivePath(path).publicKey, + }, + ], + }); + assert.strictEqual(psbt.outputHasHDKey(0, root), true); + assert.strictEqual(psbt.outputHasHDKey(0, root2), false); + }); + }); + describe('outputHasPubkey', () => { it('should throw', () => { const psbt = new Psbt(); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 39d3a4c..8f06e15 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,6 +1,7 @@ import { Psbt as PsbtBase } from 'bip174'; import * as varuint from 'bip174/src/lib/converter/varint'; import { + Bip32Derivation, KeyValue, PartialSig, PsbtGlobalUpdate, @@ -377,11 +378,27 @@ export class Psbt { return pubkeyInInput(pubkey, input, inputIndex, this.__CACHE); } + inputHasHDKey(inputIndex: number, root: HDSigner): boolean { + const input = checkForInput(this.data.inputs, inputIndex); + const derivationIsMine = bip32DerivationIsMine(root); + return ( + !!input.bip32Derivation && input.bip32Derivation.some(derivationIsMine) + ); + } + outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean { const output = checkForOutput(this.data.outputs, outputIndex); return pubkeyInOutput(pubkey, output, outputIndex, this.__CACHE); } + outputHasHDKey(outputIndex: number, root: HDSigner): boolean { + const output = checkForOutput(this.data.outputs, outputIndex); + const derivationIsMine = bip32DerivationIsMine(root); + return ( + !!output.bip32Derivation && output.bip32Derivation.some(derivationIsMine) + ); + } + validateSignaturesOfAllInputs(): boolean { checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => @@ -905,6 +922,16 @@ const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2WSHScript = isPaymentFactory(payments.p2wsh); const isP2SHScript = isPaymentFactory(payments.p2sh); +function bip32DerivationIsMine( + root: HDSigner, +): (d: Bip32Derivation) => boolean { + return (d: Bip32Derivation): boolean => { + if (!d.masterFingerprint.equals(root.fingerprint)) return false; + if (!root.derivePath(d.path).publicKey.equals(d.pubkey)) return false; + return true; + }; +} + function check32Bit(num: number): void { if ( typeof num !== 'number' || diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 4d1c099..eb239dc 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -71,7 +71,9 @@ export declare class Psbt { finalizeInput(inputIndex: number, finalScriptsFunc?: FinalScriptsFunc): this; getInputType(inputIndex: number): AllScriptType; inputHasPubkey(inputIndex: number, pubkey: Buffer): boolean; + inputHasHDKey(inputIndex: number, root: HDSigner): boolean; outputHasPubkey(outputIndex: number, pubkey: Buffer): boolean; + outputHasHDKey(outputIndex: number, root: HDSigner): boolean; validateSignaturesOfAllInputs(): boolean; validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean; signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; From 3a54c738176a04ad8cc5e08944cb07bb171a87de Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 12 Sep 2020 00:19:21 +0900 Subject: [PATCH 13/15] Update bip174 dep --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9a67fe..5b53bb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -367,9 +367,9 @@ } }, "bip174": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-1.0.1.tgz", - "integrity": "sha512-Mq2aFs1TdMfxBpYPg7uzjhsiXbAtoVq44TNjEWtvuZBiBgc3m7+n55orYMtTAxdg7jWbL4DtH0MKocJER4xERQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", + "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==" }, "bip32": { "version": "2.0.4", @@ -1344,7 +1344,8 @@ "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true }, "lodash.flattendeep": { "version": "4.4.0", diff --git a/package.json b/package.json index 5b765c6..b45735b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ ], "dependencies": { "bech32": "^1.1.2", - "bip174": "^1.0.1", + "bip174": "^2.0.1", "bip32": "^2.0.4", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", From 5e3442b74be523f511bcee362c184d91d5f44331 Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 12 Sep 2020 00:35:57 +0900 Subject: [PATCH 14/15] Fix txOutputs --- ts_src/psbt.ts | 6 +++--- types/psbt.d.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index d4d886f..c55e6bc 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -32,8 +32,8 @@ export interface PsbtTxInput extends TransactionInput { hash: Buffer; } -export interface PsbtTxOutput extends Output { - address: string; +export interface PsbtTxOutput extends TransactionOutput { + address: string | undefined; } /** @@ -171,7 +171,7 @@ export class Psbt { })); } - get txOutputs(): TransactionOutput[] { + get txOutputs(): PsbtTxOutput[] { return this.__CACHE.__TX.outs.map(output => { let address; try { diff --git a/types/psbt.d.ts b/types/psbt.d.ts index eb239dc..022a95d 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,13 +1,13 @@ import { Psbt as PsbtBase } from 'bip174'; -import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput } from 'bip174/src/lib/interfaces'; +import { KeyValue, PsbtGlobalUpdate, PsbtInput, PsbtInputUpdate, PsbtOutput, PsbtOutputUpdate, TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; -import { Output, Transaction } from './transaction'; +import { Transaction } from './transaction'; export interface PsbtTxInput extends TransactionInput { hash: Buffer; } -export interface PsbtTxOutput extends Output { - address: string; +export interface PsbtTxOutput extends TransactionOutput { + address: string | undefined; } /** * Psbt class can parse and generate a PSBT binary based off of the BIP174. From 7aaef308e0b1c9dddd7ba8049780e59ecb4f6f8c Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 12 Sep 2020 00:49:05 +0900 Subject: [PATCH 15/15] 5.2.0 --- CHANGELOG.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9eae7..a4e1eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 5.2.0 +__changed__ +- Updated PSBT to allow for witnessUtxo and nonWitnessUtxo simultaneously (Re: segwit psbt bug) (#1563) + +__added__ +- PSBT methods `getInputType`, `inputHasPubkey`, `inputHasHDKey`, `outputHasPubkey`, `outputHasHDKey` (#1563) + # 5.1.10 __fixed__ - Fixed psbt.signInputAsync (and consequentially all Async signing methods) not handling rejection of keypair.sign properly (#1582) diff --git a/package-lock.json b/package-lock.json index 5b53bb7..7278818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "5.1.10", + "version": "5.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b45735b..0f92b75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "5.1.10", + "version": "5.2.0", "description": "Client-side Bitcoin JavaScript library", "main": "./src/index.js", "types": "./types/index.d.ts",