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;