diff --git a/src/psbt.js b/src/psbt.js index 529f023..3bb1b3f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -5,6 +5,7 @@ const utils_1 = require('bip174/src/lib/utils'); const address_1 = require('./address'); const bufferutils_1 = require('./bufferutils'); const crypto_1 = require('./crypto'); +const ecpair_1 = require('./ecpair'); const networks_1 = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); @@ -304,6 +305,39 @@ class Psbt extends bip174_1.Psbt { this.clearFinalizedInput(inputIndex); return true; } + validateSignatures(inputIndex, pubkey) { + const input = this.inputs[inputIndex]; + const partialSig = (input || {}).partialSig; + if (!input || !partialSig || partialSig.length < 1) + throw new Error('No signatures to validate'); + const mySigs = pubkey + ? partialSig.filter(sig => sig.pubkey.equals(pubkey)) + : partialSig; + if (mySigs.length < 1) throw new Error('No signatures for this pubkey'); + const results = []; + let hashCache; + let scriptCache; + let sighashCache; + for (const pSig of mySigs) { + const sig = bscript.signature.decode(pSig.signature); + const { hash, script } = + sighashCache !== sig.hashType + ? getHashForSig( + inputIndex, + Object.assign({}, input, { sighashType: sig.hashType }), + this.__TX, + this.__CACHE, + ) + : { hash: hashCache, script: scriptCache }; + sighashCache = sig.hashType; + hashCache = hash; + scriptCache = script; + checkScriptForPubkey(pSig.pubkey, script, 'verify'); + const keypair = ecpair_1.fromPublicKey(pSig.pubkey); + results.push(keypair.verify(hash, sig.signature)); + } + return results.every(res => res === true); + } signInput(inputIndex, keyPair) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -391,7 +425,7 @@ function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx, cache) { unsignedTx, cache, ); - checkScriptForPubkey(pubkey, script); + checkScriptForPubkey(pubkey, script, 'sign'); return { hash, sighashType, @@ -494,7 +528,7 @@ function canFinalize(input, script, scriptType) { return false; } } -function checkScriptForPubkey(pubkey, script) { +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'); @@ -504,7 +538,7 @@ function checkScriptForPubkey(pubkey, script) { }); if (!hasKey) { throw new Error( - `Can not sign for this input with the key ${pubkey.toString('hex')}`, + `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, ); } } diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index e48e335..9fb76f7 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -7,6 +7,65 @@ const regtest = regtestUtils.network // See bottom of file for some helper functions used to make the payment objects needed. describe('bitcoinjs-lib (transactions with psbt)', () => { + it('can create a 1-to-1 Transaction', () => { + const alice = bitcoin.ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr') + const psbt = new bitcoin.Psbt() + psbt.setVersion(2) // These are defaults. This line is not needed. + psbt.setLocktime(0) // These are defaults. This line is not needed. + psbt.addInput({ + // if hash is string, txid, if hash is Buffer, is reversed compared to txid + hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e', + index: 0, + sequence: 0xffffffff, // These are defaults. This line is not needed. + + // non-segwit inputs now require passing the whole previous tx as Buffer + nonWitnessUtxo: Buffer.from( + '0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' + + '452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' + + 'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' + + '9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' + + '631e5e1e66009ce3710ceea5b1ad13ffffffff01' + + // value in satoshis (Int64LE) = 0x015f90 = 90000 + '905f010000000000' + + // scriptPubkey length + '19' + + // scriptPubkey + '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' + + // locktime + '00000000', + 'hex', + ), + + // // If this input was segwit, instead of nonWitnessUtxo, you would add + // // a witnessUtxo as follows. The scriptPubkey and the value only are needed. + // witnessUtxo: { + // script: Buffer.from( + // '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac', + // 'hex', + // ), + // value: 90000, + // }, + + // Not featured here: redeemScript. A Buffer of the redeemScript + }) + psbt.addOutput({ + address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', + value: 80000 + }) + psbt.signInput(0, alice) + psbt.validateSignatures(0) + psbt.finalizeAllInputs() + assert.strictEqual( + psbt.extractTransaction().toHex(), + '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + + 'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' + + 'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' + + '9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' + + 'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' + + '08a22724efa6f6a07b0ec4c79aa88ac00000000', + ) + }) + it('can create (and broadcast via 3PBP) a typical Transaction', async () => { // these are { payment: Payment; keys: ECPair[] } const alice1 = createPayment('p2pkh') @@ -64,6 +123,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // final1.combine(final2) would give the exact same result psbt.combine(final1, final2) + // Finalizer wants to check all signatures are valid before finalizing. + // If the finalizer wants to check for specific pubkeys, the second arg + // can be passed. See the first multisig example below. + assert.strictEqual(psbt.validateSignatures(0), true) + assert.strictEqual(psbt.validateSignatures(1), true) + // This step it new. Since we separate the signing operation and // the creation of the scriptSig and witness stack, we are able to psbt.finalizeAllInputs() @@ -94,6 +159,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, alice1.keys[0]) + assert.strictEqual(psbt.validateSignatures(0), true) psbt.finalizeAllInputs() // build and broadcast to the RegTest network @@ -122,6 +188,11 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { .signInput(0, multisig.keys[0]) .signInput(0, multisig.keys[2]) + assert.strictEqual(psbt.validateSignatures(0), true) + assert.strictEqual(psbt.validateSignatures(0, multisig.keys[0].publicKey), true) + assert.throws(() => { + psbt.validateSignatures(0, multisig.keys[3].publicKey) + }, new RegExp('No signatures for this pubkey')) psbt.finalizeAllInputs() const tx = psbt.extractTransaction() @@ -158,6 +229,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, p2sh.keys[0]) + assert.strictEqual(psbt.validateSignatures(0), true) psbt.finalizeAllInputs() const tx = psbt.extractTransaction() @@ -196,6 +268,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, p2wpkh.keys[0]) + assert.strictEqual(psbt.validateSignatures(0), true) psbt.finalizeAllInputs() const tx = psbt.extractTransaction() @@ -232,6 +305,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, p2wsh.keys[0]) + assert.strictEqual(psbt.validateSignatures(0), true) psbt.finalizeAllInputs() const tx = psbt.extractTransaction() @@ -271,6 +345,11 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { .signInput(0, p2sh.keys[2]) .signInput(0, p2sh.keys[3]) + assert.strictEqual(psbt.validateSignatures(0), true) + assert.strictEqual(psbt.validateSignatures(0, p2sh.keys[3].publicKey), true) + assert.throws(() => { + psbt.validateSignatures(0, p2sh.keys[1].publicKey) + }, new RegExp('No signatures for this pubkey')) psbt.finalizeAllInputs() const tx = psbt.extractTransaction() @@ -287,7 +366,8 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) }) -function createPayment(_type) { +function createPayment(_type, network) { + network = network || regtest const splitType = _type.split('-').reverse(); const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; const keys = []; @@ -297,11 +377,11 @@ function createPayment(_type) { m = parseInt(match[1]) let n = parseInt(match[2]) while (n > 1) { - keys.push(bitcoin.ECPair.makeRandom({ network: regtest })); + keys.push(bitcoin.ECPair.makeRandom({ network })); n-- } } - keys.push(bitcoin.ECPair.makeRandom({ network: regtest })); + keys.push(bitcoin.ECPair.makeRandom({ network })); let payment; splitType.forEach(type => { @@ -309,17 +389,17 @@ function createPayment(_type) { payment = bitcoin.payments.p2ms({ m, pubkeys: keys.map(key => key.publicKey).sort(), - network: regtest, + network, }); } else if (['p2sh', 'p2wsh'].indexOf(type) > -1) { payment = bitcoin.payments[type]({ redeem: payment, - network: regtest, + network, }); } else { payment = bitcoin.payments[type]({ pubkey: keys[0].publicKey, - network: regtest, + network, }); } }); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 9072033..6ccef08 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -10,7 +10,11 @@ import { checkForInput } from 'bip174/src/lib/utils'; import { toOutputScript } from './address'; import { reverseBuffer } from './bufferutils'; import { hash160 } from './crypto'; -import { Signer, SignerAsync } from './ecpair'; +import { + fromPublicKey as ecPairFromPublicKey, + Signer, + SignerAsync, +} from './ecpair'; import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; @@ -367,6 +371,40 @@ export class Psbt extends PsbtBase { return true; } + validateSignatures(inputIndex: number, pubkey?: Buffer): boolean { + const input = this.inputs[inputIndex]; + const partialSig = (input || {}).partialSig; + if (!input || !partialSig || partialSig.length < 1) + throw new Error('No signatures to validate'); + const mySigs = pubkey + ? partialSig.filter(sig => sig.pubkey.equals(pubkey)) + : partialSig; + if (mySigs.length < 1) throw new Error('No signatures for this pubkey'); + const results: boolean[] = []; + let hashCache: Buffer; + let scriptCache: Buffer; + let sighashCache: number; + for (const pSig of mySigs) { + const sig = bscript.signature.decode(pSig.signature); + const { hash, script } = + sighashCache! !== sig.hashType + ? getHashForSig( + inputIndex, + Object.assign({}, input, { sighashType: sig.hashType }), + this.__TX, + this.__CACHE, + ) + : { hash: hashCache!, script: scriptCache! }; + sighashCache = sig.hashType; + hashCache = hash; + scriptCache = script; + checkScriptForPubkey(pSig.pubkey, script, 'verify'); + const keypair = ecPairFromPublicKey(pSig.pubkey); + results.push(keypair.verify(hash, sig.signature)); + } + return results.every(res => res === true); + } + signInput(inputIndex: number, keyPair: Signer): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -507,7 +545,7 @@ function getHashAndSighashType( unsignedTx, cache, ); - checkScriptForPubkey(pubkey, script); + checkScriptForPubkey(pubkey, script, 'sign'); return { hash, sighashType, @@ -628,7 +666,11 @@ function canFinalize( } } -function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void { +function checkScriptForPubkey( + pubkey: Buffer, + script: Buffer, + action: string, +): void { const pubkeyHash = hash160(pubkey); const decompiled = bscript.decompile(script); @@ -641,7 +683,7 @@ function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void { if (!hasKey) { throw new Error( - `Can not sign for this input with the key ${pubkey.toString('hex')}`, + `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, ); } } diff --git a/types/psbt.d.ts b/types/psbt.d.ts index a708c21..1f9ee7b 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -29,6 +29,7 @@ export declare class Psbt extends PsbtBase { inputResults: boolean[]; }; finalizeInput(inputIndex: number): boolean; + validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; signInput(inputIndex: number, keyPair: Signer): this; signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise; }