From c9f399e509c5375580e9c196e3c2c5eeb59aa053 Mon Sep 17 00:00:00 2001 From: junderw <junderwood@bitcoinbank.co.jp> Date: Wed, 29 Apr 2020 11:05:33 +0900 Subject: [PATCH] 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 {};