From 62f174902127ad9ccf30414b52e2dc23c588526f Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 24 Jun 2019 17:09:48 +0700 Subject: [PATCH 001/111] Add BIP174 dependency --- package-lock.json | 5 +++++ package.json | 1 + 2 files changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index d769ef5..30a6172 100644 --- a/package-lock.json +++ b/package-lock.json @@ -199,6 +199,11 @@ "file-uri-to-path": "1.0.0" } }, + "bip174": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.4.tgz", + "integrity": "sha512-KirRr35qCNih51thdf6klTevFTtm/cIw2iqumRgxtqFjNREFG78fFylfCkMM9aQdlYfSoj3sczxgXham0jZVnw==" + }, "bip32": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.3.tgz", diff --git a/package.json b/package.json index b8ec5f3..fd5f5cf 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", + "bip174": "0.0.4", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", From 6a5e395ebd27eb3ae08953cae4c78ee4ba76c41f Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Mon, 24 Jun 2019 17:20:15 +0700 Subject: [PATCH 002/111] Extend BIP174 PSBT base class --- src/index.js | 2 ++ src/psbt.js | 9 +++++++++ ts_src/index.ts | 1 + ts_src/psbt.ts | 7 +++++++ types/index.d.ts | 1 + types/psbt.d.ts | 4 ++++ 6 files changed, 24 insertions(+) create mode 100644 src/psbt.js create mode 100644 ts_src/psbt.ts create mode 100644 types/psbt.d.ts diff --git a/src/index.js b/src/index.js index 499380e..bc71f02 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,8 @@ const script = require('./script'); exports.script = script; var block_1 = require('./block'); exports.Block = block_1.Block; +var psbt_1 = require('./psbt'); +exports.Psbt = psbt_1.Psbt; var script_1 = require('./script'); exports.opcodes = script_1.OPS; var transaction_1 = require('./transaction'); diff --git a/src/psbt.js b/src/psbt.js new file mode 100644 index 0000000..70b60e2 --- /dev/null +++ b/src/psbt.js @@ -0,0 +1,9 @@ +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); +const bip174_1 = require('bip174'); +class Psbt extends bip174_1.Psbt { + constructor() { + super(); + } +} +exports.Psbt = Psbt; diff --git a/ts_src/index.ts b/ts_src/index.ts index 4f2d498..58c39c7 100644 --- a/ts_src/index.ts +++ b/ts_src/index.ts @@ -9,6 +9,7 @@ import * as script from './script'; export { ECPair, address, bip32, crypto, networks, payments, script }; export { Block } from './block'; +export { Psbt } 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 new file mode 100644 index 0000000..08d2670 --- /dev/null +++ b/ts_src/psbt.ts @@ -0,0 +1,7 @@ +import { Psbt as PsbtBase } from 'bip174'; + +export class Psbt extends PsbtBase { + constructor() { + super(); + } +} diff --git a/types/index.d.ts b/types/index.d.ts index 93d72e4..fc5a932 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,6 +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 { 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 new file mode 100644 index 0000000..880ebd2 --- /dev/null +++ b/types/psbt.d.ts @@ -0,0 +1,4 @@ +import { Psbt as PsbtBase } from 'bip174'; +export declare class Psbt extends PsbtBase { + constructor(); +} From 2ed89cdc68b83b286c5d216b4257a8a31f56281f Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 25 Jun 2019 17:47:26 +0700 Subject: [PATCH 003/111] Update BIP174 package --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30a6172..ac0507b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.4.tgz", - "integrity": "sha512-KirRr35qCNih51thdf6klTevFTtm/cIw2iqumRgxtqFjNREFG78fFylfCkMM9aQdlYfSoj3sczxgXham0jZVnw==" + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.5.tgz", + "integrity": "sha512-NNt0e9pz7h8EhC+pNAcB8G0Ca/Lei42YnAtPMewpcuLzRJGgaJO4vgtBpeQHH/f3fWlabZwSh/3tyEHwFNXlRw==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index fd5f5cf..6527baf 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.4", + "bip174": "0.0.5", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", From 6ed635d7b41b976f2d88dc7492e857c8adda0543 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 25 Jun 2019 18:22:00 +0700 Subject: [PATCH 004/111] Flesh out signInput interface --- src/psbt.js | 12 ++++++++++++ ts_src/psbt.ts | 18 ++++++++++++++++++ types/psbt.d.ts | 2 ++ 3 files changed, 32 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index 70b60e2..887c814 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -5,5 +5,17 @@ class Psbt extends bip174_1.Psbt { constructor() { super(); } + signInput(inputIndex, keyPair) { + // TODO: Implement BIP174 pre-sign checks: + // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer + // TODO: Get hash to sign + const hash = Buffer.alloc(32); + const partialSig = { + pubkey: keyPair.publicKey, + signature: keyPair.sign(hash), + }; + this.addPartialSigToInput(inputIndex, partialSig); + return this; + } } exports.Psbt = Psbt; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 08d2670..8fec780 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,7 +1,25 @@ import { Psbt as PsbtBase } from 'bip174'; +import { Signer } from './ecpair'; export class Psbt extends PsbtBase { constructor() { super(); } + + signInput(inputIndex: number, keyPair: Signer): Psbt { + // TODO: Implement BIP174 pre-sign checks: + // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer + + // TODO: Get hash to sign + const hash = Buffer.alloc(32); + + const partialSig = { + pubkey: keyPair.publicKey, + signature: keyPair.sign(hash), + }; + + this.addPartialSigToInput(inputIndex, partialSig); + + return this; + } } diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 880ebd2..a58b982 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,4 +1,6 @@ import { Psbt as PsbtBase } from 'bip174'; +import { Signer } from './ecpair'; export declare class Psbt extends PsbtBase { constructor(); + signInput(inputIndex: number, keyPair: Signer): Psbt; } From ff3caa02fe23a949bad78b23c29a25d5da04d2e1 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 26 Jun 2019 16:29:00 +0700 Subject: [PATCH 005/111] Add BIP174 pseudo code for signing checks --- src/psbt.js | 21 +++++++++++++++++++++ ts_src/psbt.ts | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index 887c814..5bdb176 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -8,6 +8,27 @@ class Psbt extends bip174_1.Psbt { signInput(inputIndex, keyPair) { // TODO: Implement BIP174 pre-sign checks: // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer + // + // if non_witness_utxo.exists: + // assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash) + // if redeemScript.exists: + // assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript)) + // sign_non_witness(redeemScript) + // else: + // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey) + // else if witness_utxo.exists: + // if redeemScript.exists: + // assert(witness_utxo.scriptPubKey == P2SH(redeemScript)) + // script = redeemScript + // else: + // script = witness_utxo.scriptPubKey + // if IsP2WPKH(script): + // sign_witness(P2PKH(script[2:22])) + // else if IsP2WSH(script): + // assert(script == P2WSH(witnessScript)) + // sign_witness(witnessScript) + // else: + // assert False // TODO: Get hash to sign const hash = Buffer.alloc(32); const partialSig = { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 8fec780..73308a2 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -9,6 +9,27 @@ export class Psbt extends PsbtBase { signInput(inputIndex: number, keyPair: Signer): Psbt { // TODO: Implement BIP174 pre-sign checks: // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer + // + // if non_witness_utxo.exists: + // assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash) + // if redeemScript.exists: + // assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript)) + // sign_non_witness(redeemScript) + // else: + // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey) + // else if witness_utxo.exists: + // if redeemScript.exists: + // assert(witness_utxo.scriptPubKey == P2SH(redeemScript)) + // script = redeemScript + // else: + // script = witness_utxo.scriptPubKey + // if IsP2WPKH(script): + // sign_witness(P2PKH(script[2:22])) + // else if IsP2WSH(script): + // assert(script == P2WSH(witnessScript)) + // sign_witness(witnessScript) + // else: + // assert False // TODO: Get hash to sign const hash = Buffer.alloc(32); From 98dff9a47e810292b17284a3797cfee437e994d1 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 26 Jun 2019 17:55:02 +0700 Subject: [PATCH 006/111] Check Non-witness UTXO hash when signing PSBT input --- src/psbt.js | 19 +++++++++++++++++++ ts_src/psbt.ts | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index 5bdb176..5d40ab4 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1,6 +1,7 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); +const transaction_1 = require('./transaction'); class Psbt extends bip174_1.Psbt { constructor() { super(); @@ -29,6 +30,24 @@ class Psbt extends bip174_1.Psbt { // sign_witness(witnessScript) // else: // assert False + const input = this.inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (input.nonWitnessUtxo) { + const unsignedTx = transaction_1.Transaction.fromBuffer( + this.globalMap.unsignedTx, + ); + const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( + input.nonWitnessUtxo, + ); + const inputHash = unsignedTx.ins[inputIndex].hash; + const utxoHash = nonWitnessUtxoTx.getHash(); + if (Buffer.compare(inputHash, utxoHash) !== 0) { + throw new Error( + `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, + ); + } + } // TODO: Get hash to sign const hash = Buffer.alloc(32); const partialSig = { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 73308a2..9b0e9ce 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,6 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; +import { Transaction } from './transaction'; export class Psbt extends PsbtBase { constructor() { @@ -31,6 +32,24 @@ export class Psbt extends PsbtBase { // else: // assert False + const input = this.inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (input.nonWitnessUtxo) { + const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); + const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); + + const inputHash = unsignedTx.ins[inputIndex].hash; + const utxoHash = nonWitnessUtxoTx.getHash(); + + if (Buffer.compare(inputHash, utxoHash) !== 0) { + throw new Error( + `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, + ); + } + } + // TODO: Get hash to sign const hash = Buffer.alloc(32); From 2dcac556015e736e215c293e7af71a17a1a0a88a Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 26 Jun 2019 18:20:49 +0700 Subject: [PATCH 007/111] Add simple tests for non-witness UTXO check --- test/psbt.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test/psbt.js diff --git a/test/psbt.js b/test/psbt.js new file mode 100644 index 0000000..9422d6e --- /dev/null +++ b/test/psbt.js @@ -0,0 +1,43 @@ +const { describe, it, beforeEach } = require('mocha') +const assert = require('assert') + +const ECPair = require('../src/ecpair') +const Psbt = require('..').Psbt + +// For now, just hardcoding some test values is fine +// const fixtures = require('./fixtures/psbt') + +describe(`Psbt`, () => { + // constants + const keyPair = ECPair.fromPrivateKey(Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex')) + + describe('signInput', () => { + it('throws if non-witness UTXO hash doesn\'t match the hash specified in the prevout', () => { + const inputUtxo = '0200000001c1602ba68c8c241450a78b61dbfde272989181d07537b1e70d31b7db939557f2000000006a473044022029872b97579850c87658e431bb9df4a3f3e41590777529a55e25eb11eccafef50220511700aa1ea2c2cd499251f99014f22c5af63a00c76fd24da650014a0a8199e901210264187d9ee773aa333ac223678478b1df3ea268178fc9447e0a60c443eddaa749fdffffff01995d0000000000001976a914759d6677091e973b9e9d99f19c68fbf43e3f05f988acc8d30800' + const inputHash = 'd2d00ff71b1d1920d3a7717274b720f3d8230d9f38ec8f3e6867a207f2a40092' + const inputIndex = 0 + + const psbt = new Psbt() + psbt.addInput({hash: inputHash, index: inputIndex}) + psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) + + assert.throws(() => { + psbt.signInput(inputIndex, keyPair) + }) + }) + + it('does not throw if non-witness UTXO hash matches the hash specified in the prevout', () => { + const inputUtxo = '01000000027dddcf79a2e541030bc753871d1c9d4dc163e4d6bd5aefae4bd84de64e16a652000000008b483045022100d3a2c3b58ae0f0b551711aa8949f478724428efa03f3179c3a50dc2c9ace46aa02201b2da84a21429a10af187731c882fc1f727e7b89573e07f0192e9e3de79fabf00141040e3a759c33b03e1af8e5d86fb447a40eff244c847a4f8274276db490054e8be076f8801ddc9c5246ee86b6f33cfe38e8b7e57ab9db390eb3ec1ec6ae9eeea113fdffffff4fef6d7f3c1e5d0bea733b2fd644fa456cdf73f21eb7e8866a2721d79266e9e8010000008a4730440220284a2989d45c48a6c8a556b30b3467eaf6abf866ec75c4d9f0e3872074f62c070220686ad82869c1669e6be162c4a34e4c617a971766f2b9688789eb2f498fe5eb6b0141040e3a759c33b03e1af8e5d86fb447a40eff244c847a4f8274276db490054e8be076f8801ddc9c5246ee86b6f33cfe38e8b7e57ab9db390eb3ec1ec6ae9eeea113fdffffff0224c70d00000000001976a914da6473ed373e08f46dd8003fca7ba72fbe9c555e88ac9cb00e00000000001976a91449707992598f85a31aa6715af70fe507610b6f8b88ac11c00800' + const inputHash = 'd2d00ff71b1d1920d3a7717274b720f3d8230d9f38ec8f3e6867a207f2a40092' + const inputIndex = 0 + + const psbt = new Psbt() + psbt.addInput({hash: inputHash, index: inputIndex}) + psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) + + assert.doesNotThrow(() => { + psbt.signInput(inputIndex, keyPair) + }) + }) + }) +}) From 5fd18d806f8dbce87b176cd2a3f869a82dc07990 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 27 Jun 2019 18:19:15 +0700 Subject: [PATCH 008/111] Check redeem script matches when signing PSBT input --- src/psbt.js | 20 +++++++++++++++++--- ts_src/psbt.ts | 23 ++++++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 5d40ab4..6e72daa 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1,6 +1,7 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); +const payments = require('./payments'); const transaction_1 = require('./transaction'); class Psbt extends bip174_1.Psbt { constructor() { @@ -32,7 +33,6 @@ class Psbt extends bip174_1.Psbt { // assert False const input = this.inputs[inputIndex]; if (input === undefined) throw new Error(`No input #${inputIndex}`); - // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout if (input.nonWitnessUtxo) { const unsignedTx = transaction_1.Transaction.fromBuffer( this.globalMap.unsignedTx, @@ -40,13 +40,27 @@ class Psbt extends bip174_1.Psbt { const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( input.nonWitnessUtxo, ); - const inputHash = unsignedTx.ins[inputIndex].hash; + const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); - if (Buffer.compare(inputHash, utxoHash) !== 0) { + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (Buffer.compare(prevoutHash, utxoHash) !== 0) { throw new Error( `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, ); } + if (input.redeemScript) { + const prevoutIndex = unsignedTx.ins[inputIndex].index; + const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + const redeemScriptOutput = payments.p2sh({ + redeem: { output: input.redeemScript }, + }).output; + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + if (Buffer.compare(prevout.script, redeemScriptOutput) !== 0) { + throw new Error( + `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } + } } // TODO: Get hash to sign const hash = Buffer.alloc(32); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 9b0e9ce..8f40881 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,6 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; +import * as payments from './payments'; import { Transaction } from './transaction'; export class Psbt extends PsbtBase { @@ -35,19 +36,35 @@ export class Psbt extends PsbtBase { const input = this.inputs[inputIndex]; if (input === undefined) throw new Error(`No input #${inputIndex}`); - // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout if (input.nonWitnessUtxo) { const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); - const inputHash = unsignedTx.ins[inputIndex].hash; + const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); - if (Buffer.compare(inputHash, utxoHash) !== 0) { + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (Buffer.compare(prevoutHash, utxoHash) !== 0) { throw new Error( `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, ); } + + if (input.redeemScript) { + const prevoutIndex = unsignedTx.ins[inputIndex].index; + const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + + const redeemScriptOutput = payments.p2sh({ + redeem: { output: input.redeemScript }, + }).output as Buffer; + + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + if (Buffer.compare(prevout.script, redeemScriptOutput) !== 0) { + throw new Error( + `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } + } } // TODO: Get hash to sign From 64dc6543be759f36c0c14e9191213f04c6940b70 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 27 Jun 2019 18:20:19 +0700 Subject: [PATCH 009/111] Add simple tests for redeem script check --- test/psbt.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/psbt.js b/test/psbt.js index 9422d6e..f814921 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -39,5 +39,37 @@ describe(`Psbt`, () => { psbt.signInput(inputIndex, keyPair) }) }) + + it('throws if redeem script does not match the scriptPubKey in the prevout', () => { + const inputUtxo = '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000' + const inputRedeemScript = '00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903' + const inputHash = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858' + const inputIndex = 0 + + const psbt = new Psbt() + psbt.addInput({hash: inputHash, index: inputIndex}) + psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) + psbt.addRedeemScriptToInput(inputIndex, Buffer.from(inputRedeemScript, 'hex')) + + assert.throws(() => { + psbt.signInput(inputIndex, keyPair) + }) + }) + + it('does not throw if redeem script matches the scriptPubKey in the prevout', () => { + const inputUtxo = '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000' + const inputRedeemScript = '5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae' + const inputHash = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858' + const inputIndex = 0 + + const psbt = new Psbt() + psbt.addInput({hash: inputHash, index: inputIndex}) + psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) + psbt.addRedeemScriptToInput(inputIndex, Buffer.from(inputRedeemScript, 'hex')) + + assert.doesNotThrow(() => { + psbt.signInput(inputIndex, keyPair) + }) + }) }) }) From 1afac399b14a1d1e40974c5bee597c06687cfff6 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 16:31:29 +0700 Subject: [PATCH 010/111] Update BIP174 package to fix inheritance issues --- package-lock.json | 6 +++--- package.json | 2 +- src/psbt.js | 10 ++++++++-- ts_src/psbt.ts | 10 ++++++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac0507b..d409322 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.5.tgz", - "integrity": "sha512-NNt0e9pz7h8EhC+pNAcB8G0Ca/Lei42YnAtPMewpcuLzRJGgaJO4vgtBpeQHH/f3fWlabZwSh/3tyEHwFNXlRw==" + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.8.tgz", + "integrity": "sha512-xWPzmlCvLoOWTlXk1wG7+TyOfaN8xX07IieuG4ug5su3igC9s4Lsdq+IEEMo+YHDQ4hPPAX9LYio6aEIAA+Zrg==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 6527baf..b964697 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.5", + "bip174": "0.0.8", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", diff --git a/src/psbt.js b/src/psbt.js index 6e72daa..9f29525 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -68,8 +68,14 @@ class Psbt extends bip174_1.Psbt { pubkey: keyPair.publicKey, signature: keyPair.sign(hash), }; - this.addPartialSigToInput(inputIndex, partialSig); - return this; + // Just hardcode this for now to satisfy the stricter sig type checks + partialSig.signature = Buffer.from( + '304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6' + + '771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa9' + + '4b99bdf86151db9a9a01', + 'hex', + ); + return this.addPartialSigToInput(inputIndex, partialSig); } } exports.Psbt = Psbt; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 8f40881..75a90d7 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -75,8 +75,14 @@ export class Psbt extends PsbtBase { signature: keyPair.sign(hash), }; - this.addPartialSigToInput(inputIndex, partialSig); + // Just hardcode this for now to satisfy the stricter sig type checks + partialSig.signature = Buffer.from( + '304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6' + + '771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa9' + + '4b99bdf86151db9a9a01', + 'hex', + ); - return this; + return this.addPartialSigToInput(inputIndex, partialSig); } } From 3a82486fb5667ee14bc6d830e745a1151163d202 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 16:55:00 +0700 Subject: [PATCH 011/111] Loop over PSBT tests from fixtures --- test/fixtures/psbt.json | 30 +++++++++++++++++ test/psbt.js | 71 +++++++---------------------------------- 2 files changed, 42 insertions(+), 59 deletions(-) create mode 100644 test/fixtures/psbt.json diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json new file mode 100644 index 0000000..9268ed9 --- /dev/null +++ b/test/fixtures/psbt.json @@ -0,0 +1,30 @@ +{ + "signInput": { + "checks": [ + { + "description": "checks non-witness UTXO matches the hash specified in the prevout", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAAZIApPIHomdoPo/sOJ8NI9jzILd0cnGn0yAZHRv3D9DSAAAAAAD/////AAAAAAAAAQD9tQEBAAAAAn3dz3mi5UEDC8dThx0cnU3BY+TWvVrvrkvYTeZOFqZSAAAAAItIMEUCIQDTosO1iuDwtVFxGqiUn0eHJEKO+gPzF5w6UNwsms5GqgIgGy2oSiFCmhCvGHcxyIL8H3J+e4lXPgfwGS6ePeefq/ABQQQOOnWcM7A+Gvjl2G+0R6QO/yRMhHpPgnQnbbSQBU6L4Hb4gB3cnFJG7oa28zz+OOi35Xq52zkOs+wexq6e7qET/f///0/vbX88Hl0L6nM7L9ZE+kVs33PyHrfohmonIdeSZunoAQAAAIpHMEQCIChKKYnUXEimyKVWsws0Z+r2q/hm7HXE2fDjhyB09iwHAiBoatgoacFmnmvhYsSjTkxhepcXZvK5aIeJ6y9Jj+XrawFBBA46dZwzsD4a+OXYb7RHpA7/JEyEek+CdCdttJAFTovgdviAHdycUkbuhrbzPP446LflernbOQ6z7B7Grp7uoRP9////AiTHDQAAAAAAGXapFNpkc+03Pgj0bdgAP8p7py++nFVeiKycsA4AAAAAABl2qRRJcHmSWY+FoxqmcVr3D+UHYQtvi4isEcAIAAAA", + "inputToCheck": 0 + }, + "shouldThrow": { + "errorMessage": "Non-witness UTXO hash for input #0 doesn't match the hash specified in the prevout", + "psbt": "cHNidP8BADMBAAAAAZIApPIHomdoPo/sOJ8NI9jzILd0cnGn0yAZHRv3D9DSAAAAAAD/////AAAAAAAAAQC/AgAAAAHBYCumjIwkFFCni2Hb/eJymJGB0HU3secNMbfbk5VX8gAAAABqRzBEAiAphyuXV5hQyHZY5DG7nfSj8+QVkHd1KaVeJesR7Mr+9QIgURcAqh6iws1JklH5kBTyLFr2OgDHb9JNplABSgqBmekBIQJkGH2e53OqMzrCI2eEeLHfPqJoF4/JRH4KYMRD7dqnSf3///8BmV0AAAAAAAAZdqkUdZ1mdwkelzuenZnxnGj79D4/BfmIrMjTCAAAAA==", + "inputToCheck": 0 + } + }, + { + "description": "checks redeem script matches the scriptPubKey in the prevout", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAAVjoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////AAAAAAAAAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuAAA=", + "inputToCheck": 0 + }, + "shouldThrow": { + "errorMessage": "Redeem script for input #0 doesn't match the scriptPubKey in the prevout", + "psbt": "cHNidP8BADMBAAAAAVjoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////AAAAAAAAAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMAAA==", + "inputToCheck": 0 + } + } + ] + } +} diff --git a/test/psbt.js b/test/psbt.js index f814921..1d8ee90 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -4,71 +4,24 @@ const assert = require('assert') const ECPair = require('../src/ecpair') const Psbt = require('..').Psbt -// For now, just hardcoding some test values is fine -// const fixtures = require('./fixtures/psbt') +const fixtures = require('./fixtures/psbt') describe(`Psbt`, () => { // constants const keyPair = ECPair.fromPrivateKey(Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex')) describe('signInput', () => { - it('throws if non-witness UTXO hash doesn\'t match the hash specified in the prevout', () => { - const inputUtxo = '0200000001c1602ba68c8c241450a78b61dbfde272989181d07537b1e70d31b7db939557f2000000006a473044022029872b97579850c87658e431bb9df4a3f3e41590777529a55e25eb11eccafef50220511700aa1ea2c2cd499251f99014f22c5af63a00c76fd24da650014a0a8199e901210264187d9ee773aa333ac223678478b1df3ea268178fc9447e0a60c443eddaa749fdffffff01995d0000000000001976a914759d6677091e973b9e9d99f19c68fbf43e3f05f988acc8d30800' - const inputHash = 'd2d00ff71b1d1920d3a7717274b720f3d8230d9f38ec8f3e6867a207f2a40092' - const inputIndex = 0 - - const psbt = new Psbt() - psbt.addInput({hash: inputHash, index: inputIndex}) - psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) - - assert.throws(() => { - psbt.signInput(inputIndex, keyPair) - }) - }) - - it('does not throw if non-witness UTXO hash matches the hash specified in the prevout', () => { - const inputUtxo = '01000000027dddcf79a2e541030bc753871d1c9d4dc163e4d6bd5aefae4bd84de64e16a652000000008b483045022100d3a2c3b58ae0f0b551711aa8949f478724428efa03f3179c3a50dc2c9ace46aa02201b2da84a21429a10af187731c882fc1f727e7b89573e07f0192e9e3de79fabf00141040e3a759c33b03e1af8e5d86fb447a40eff244c847a4f8274276db490054e8be076f8801ddc9c5246ee86b6f33cfe38e8b7e57ab9db390eb3ec1ec6ae9eeea113fdffffff4fef6d7f3c1e5d0bea733b2fd644fa456cdf73f21eb7e8866a2721d79266e9e8010000008a4730440220284a2989d45c48a6c8a556b30b3467eaf6abf866ec75c4d9f0e3872074f62c070220686ad82869c1669e6be162c4a34e4c617a971766f2b9688789eb2f498fe5eb6b0141040e3a759c33b03e1af8e5d86fb447a40eff244c847a4f8274276db490054e8be076f8801ddc9c5246ee86b6f33cfe38e8b7e57ab9db390eb3ec1ec6ae9eeea113fdffffff0224c70d00000000001976a914da6473ed373e08f46dd8003fca7ba72fbe9c555e88ac9cb00e00000000001976a91449707992598f85a31aa6715af70fe507610b6f8b88ac11c00800' - const inputHash = 'd2d00ff71b1d1920d3a7717274b720f3d8230d9f38ec8f3e6867a207f2a40092' - const inputIndex = 0 - - const psbt = new Psbt() - psbt.addInput({hash: inputHash, index: inputIndex}) - psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) - - assert.doesNotThrow(() => { - psbt.signInput(inputIndex, keyPair) - }) - }) - - it('throws if redeem script does not match the scriptPubKey in the prevout', () => { - const inputUtxo = '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000' - const inputRedeemScript = '00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903' - const inputHash = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858' - const inputIndex = 0 - - const psbt = new Psbt() - psbt.addInput({hash: inputHash, index: inputIndex}) - psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) - psbt.addRedeemScriptToInput(inputIndex, Buffer.from(inputRedeemScript, 'hex')) - - assert.throws(() => { - psbt.signInput(inputIndex, keyPair) - }) - }) - - it('does not throw if redeem script matches the scriptPubKey in the prevout', () => { - const inputUtxo = '0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000' - const inputRedeemScript = '5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae' - const inputHash = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858' - const inputIndex = 0 - - const psbt = new Psbt() - psbt.addInput({hash: inputHash, index: inputIndex}) - psbt.addNonWitnessUtxoToInput(inputIndex, Buffer.from(inputUtxo, 'hex')) - psbt.addRedeemScriptToInput(inputIndex, Buffer.from(inputRedeemScript, 'hex')) - - assert.doesNotThrow(() => { - psbt.signInput(inputIndex, keyPair) + fixtures.signInput.checks.forEach(f => { + it(f.description, () => { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotThrow(() => { + psbtThatShouldsign.signInput(f.shouldSign.inputToCheck, keyPair) + }) + + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.throws(() => { + psbtThatShouldThrow.signInput(f.shouldThrow.inputToCheck, keyPair) + }, {message: f.shouldThrow.errorMessage}) }) }) }) From 6562ee96a48f49d80f4b66ade10b6b17c982f8c7 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 17:17:18 +0700 Subject: [PATCH 012/111] Remove redundant import from test --- test/psbt.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/psbt.js b/test/psbt.js index 1d8ee90..3bf873d 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -1,4 +1,4 @@ -const { describe, it, beforeEach } = require('mocha') +const { describe, it } = require('mocha') const assert = require('assert') const ECPair = require('../src/ecpair') From 08627e65a3faeade0b81ab2487379de773bdb6d0 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 18:14:37 +0700 Subject: [PATCH 013/111] Check redeem script matches witness utxo when signing PSBT input --- src/psbt.js | 14 ++++++++++++++ ts_src/psbt.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index 9f29525..4dc72ff 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -61,6 +61,20 @@ class Psbt extends bip174_1.Psbt { ); } } + } else if (input.witnessUtxo) { + if (input.redeemScript) { + const redeemScriptOutput = payments.p2sh({ + redeem: { output: input.redeemScript }, + }).output; + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + if ( + Buffer.compare(input.witnessUtxo.script, redeemScriptOutput) !== 0 + ) { + throw new Error( + `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } + } } // TODO: Get hash to sign const hash = Buffer.alloc(32); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 75a90d7..ab1fd7b 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -65,6 +65,21 @@ export class Psbt extends PsbtBase { ); } } + } else if (input.witnessUtxo) { + if (input.redeemScript) { + const redeemScriptOutput = payments.p2sh({ + redeem: { output: input.redeemScript }, + }).output as Buffer; + + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + if ( + Buffer.compare(input.witnessUtxo.script, redeemScriptOutput) !== 0 + ) { + throw new Error( + `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } + } } // TODO: Get hash to sign From 10b3aff4fde3ecd74bdad6a04e88dabdc151beac Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 18:15:01 +0700 Subject: [PATCH 014/111] Test redeem script witness utxo check --- test/fixtures/psbt.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 9268ed9..1b72874 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -14,7 +14,7 @@ } }, { - "description": "checks redeem script matches the scriptPubKey in the prevout", + "description": "checks redeem script matches the scriptPubKey in a non-witness prevout", "shouldSign": { "psbt": "cHNidP8BADMBAAAAAVjoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////AAAAAAAAAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuAAA=", "inputToCheck": 0 @@ -24,6 +24,18 @@ "psbt": "cHNidP8BADMBAAAAAVjoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////AAAAAAAAAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMAAA==", "inputToCheck": 0 } + }, + { + "description": "checks redeem script matches the scriptPubKey in a witness prevout", + "shouldSign": { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", + "inputToCheck": 1 + }, + "shouldThrow": { + "errorMessage": "Redeem script for input #1 doesn't match the scriptPubKey in the prevout", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", + "inputToCheck": 1 + } } ] } From 95b4a2806d6d433bd0d766d0be7c0183d28541cd Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 18:21:32 +0700 Subject: [PATCH 015/111] Improve code re-use for redeem script checks --- src/psbt.js | 37 +++++++++++++++++-------------------- ts_src/psbt.ts | 46 +++++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 4dc72ff..c68afb0 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -3,6 +3,17 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const payments = require('./payments'); const transaction_1 = require('./transaction'); +const checkRedeemScript = (inputIndex, scriptPubKey, redeemScript) => { + const redeemScriptOutput = payments.p2sh({ + redeem: { output: redeemScript }, + }).output; + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + if (Buffer.compare(scriptPubKey, redeemScriptOutput) !== 0) { + throw new Error( + `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } +}; class Psbt extends bip174_1.Psbt { constructor() { super(); @@ -51,29 +62,15 @@ class Psbt extends bip174_1.Psbt { if (input.redeemScript) { const prevoutIndex = unsignedTx.ins[inputIndex].index; const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; - const redeemScriptOutput = payments.p2sh({ - redeem: { output: input.redeemScript }, - }).output; - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - if (Buffer.compare(prevout.script, redeemScriptOutput) !== 0) { - throw new Error( - `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, - ); - } + checkRedeemScript(inputIndex, prevout.script, input.redeemScript); } } else if (input.witnessUtxo) { if (input.redeemScript) { - const redeemScriptOutput = payments.p2sh({ - redeem: { output: input.redeemScript }, - }).output; - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - if ( - Buffer.compare(input.witnessUtxo.script, redeemScriptOutput) !== 0 - ) { - throw new Error( - `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, - ); - } + checkRedeemScript( + inputIndex, + input.witnessUtxo.script, + input.redeemScript, + ); } } // TODO: Get hash to sign diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index ab1fd7b..70e04c9 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -3,6 +3,23 @@ import { Signer } from './ecpair'; import * as payments from './payments'; import { Transaction } from './transaction'; +const checkRedeemScript = ( + inputIndex: number, + scriptPubKey: Buffer, + redeemScript: Buffer, +): void => { + const redeemScriptOutput = payments.p2sh({ + redeem: { output: redeemScript }, + }).output as Buffer; + + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript + if (Buffer.compare(scriptPubKey, redeemScriptOutput) !== 0) { + throw new Error( + `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } +}; + export class Psbt extends PsbtBase { constructor() { super(); @@ -53,32 +70,15 @@ export class Psbt extends PsbtBase { if (input.redeemScript) { const prevoutIndex = unsignedTx.ins[inputIndex].index; const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; - - const redeemScriptOutput = payments.p2sh({ - redeem: { output: input.redeemScript }, - }).output as Buffer; - - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - if (Buffer.compare(prevout.script, redeemScriptOutput) !== 0) { - throw new Error( - `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, - ); - } + checkRedeemScript(inputIndex, prevout.script, input.redeemScript); } } else if (input.witnessUtxo) { if (input.redeemScript) { - const redeemScriptOutput = payments.p2sh({ - redeem: { output: input.redeemScript }, - }).output as Buffer; - - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - if ( - Buffer.compare(input.witnessUtxo.script, redeemScriptOutput) !== 0 - ) { - throw new Error( - `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, - ); - } + checkRedeemScript( + inputIndex, + input.witnessUtxo.script, + input.redeemScript, + ); } } From f961724c73b97e1d503d5311a64d8d228e1476bd Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 18:26:42 +0700 Subject: [PATCH 016/111] Prefer buf1.equals(buf2) over Buffer.compare(buf1, buf2) !== 0 --- src/psbt.js | 4 ++-- ts_src/psbt.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index c68afb0..55e6ac9 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -8,7 +8,7 @@ const checkRedeemScript = (inputIndex, scriptPubKey, redeemScript) => { redeem: { output: redeemScript }, }).output; // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - if (Buffer.compare(scriptPubKey, redeemScriptOutput) !== 0) { + if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); @@ -54,7 +54,7 @@ class Psbt extends bip174_1.Psbt { const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout - if (Buffer.compare(prevoutHash, utxoHash) !== 0) { + if (!prevoutHash.equals(utxoHash)) { throw new Error( `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, ); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 70e04c9..7e5d13c 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -13,7 +13,7 @@ const checkRedeemScript = ( }).output as Buffer; // If a redeemScript is provided, the scriptPubKey must be for that redeemScript - if (Buffer.compare(scriptPubKey, redeemScriptOutput) !== 0) { + if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); @@ -61,7 +61,7 @@ export class Psbt extends PsbtBase { const utxoHash = nonWitnessUtxoTx.getHash(); // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout - if (Buffer.compare(prevoutHash, utxoHash) !== 0) { + if (!prevoutHash.equals(utxoHash)) { throw new Error( `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, ); From 18e7c9de80db6f5d335574f33ef39dedab82d0e3 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Fri, 28 Jun 2019 18:28:28 +0700 Subject: [PATCH 017/111] Move comments to main check logic --- src/psbt.js | 3 ++- ts_src/psbt.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 55e6ac9..2513fb9 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -7,7 +7,6 @@ const checkRedeemScript = (inputIndex, scriptPubKey, redeemScript) => { const redeemScriptOutput = payments.p2sh({ redeem: { output: redeemScript }, }).output; - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, @@ -62,10 +61,12 @@ class Psbt extends bip174_1.Psbt { if (input.redeemScript) { const prevoutIndex = unsignedTx.ins[inputIndex].index; const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript(inputIndex, prevout.script, input.redeemScript); } } else if (input.witnessUtxo) { if (input.redeemScript) { + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript( inputIndex, input.witnessUtxo.script, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 7e5d13c..167f15f 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -12,7 +12,6 @@ const checkRedeemScript = ( redeem: { output: redeemScript }, }).output as Buffer; - // If a redeemScript is provided, the scriptPubKey must be for that redeemScript if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, @@ -70,10 +69,13 @@ export class Psbt extends PsbtBase { if (input.redeemScript) { const prevoutIndex = unsignedTx.ins[inputIndex].index; const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript(inputIndex, prevout.script, input.redeemScript); } } else if (input.witnessUtxo) { if (input.redeemScript) { + // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript( inputIndex, input.witnessUtxo.script, From 667ffb58eb14aafef64b1816f9bf3e2adf610c50 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 1 Jul 2019 18:01:46 +0900 Subject: [PATCH 018/111] Use signature encode --- src/psbt.js | 13 +++++-------- ts_src/psbt.ts | 14 +++++--------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 2513fb9..7bfa439 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const payments = require('./payments'); +const script = require('./script'); const transaction_1 = require('./transaction'); const checkRedeemScript = (inputIndex, scriptPubKey, redeemScript) => { const redeemScriptOutput = payments.p2sh({ @@ -78,15 +79,11 @@ class Psbt extends bip174_1.Psbt { const hash = Buffer.alloc(32); const partialSig = { pubkey: keyPair.publicKey, - signature: keyPair.sign(hash), + signature: script.signature.encode( + keyPair.sign(hash), + input.sighashType || 0x01, + ), }; - // Just hardcode this for now to satisfy the stricter sig type checks - partialSig.signature = Buffer.from( - '304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6' + - '771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa9' + - '4b99bdf86151db9a9a01', - 'hex', - ); return this.addPartialSigToInput(inputIndex, partialSig); } } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 167f15f..63bc057 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,6 +1,7 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; import * as payments from './payments'; +import * as script from './script'; import { Transaction } from './transaction'; const checkRedeemScript = ( @@ -89,17 +90,12 @@ export class Psbt extends PsbtBase { const partialSig = { pubkey: keyPair.publicKey, - signature: keyPair.sign(hash), + signature: script.signature.encode( + keyPair.sign(hash), + input.sighashType || 0x01, + ), }; - // Just hardcode this for now to satisfy the stricter sig type checks - partialSig.signature = Buffer.from( - '304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6' + - '771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa9' + - '4b99bdf86151db9a9a01', - 'hex', - ); - return this.addPartialSigToInput(inputIndex, partialSig); } } From f87b66eb24850fbca2a8626e8473d4ece0b22ae1 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 1 Jul 2019 18:55:18 +0900 Subject: [PATCH 019/111] Finish sign --- src/psbt.js | 81 +++++++++++++++++++++++++++++++++++++++--------- ts_src/psbt.ts | 84 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 135 insertions(+), 30 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 7bfa439..9a9ad7e 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,18 +2,35 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const payments = require('./payments'); -const script = require('./script'); +const bscript = require('./script'); const transaction_1 = require('./transaction'); -const checkRedeemScript = (inputIndex, scriptPubKey, redeemScript) => { - const redeemScriptOutput = payments.p2sh({ +const scriptCheckerFactory = (payment, paymentScriptName) => ( + inputIndex, + scriptPubKey, + redeemScript, +) => { + const redeemScriptOutput = payment({ redeem: { output: redeemScript }, }).output; if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( - `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); } }; +const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); +const checkWitnessScript = scriptCheckerFactory( + payments.p2wsh, + 'Witness script', +); +const isP2WPKH = script => { + try { + payments.p2wpkh({ output: script }); + return true; + } catch (err) { + return false; + } +}; class Psbt extends bip174_1.Psbt { constructor() { super(); @@ -44,10 +61,12 @@ class Psbt extends bip174_1.Psbt { // assert False const input = this.inputs[inputIndex]; if (input === undefined) throw new Error(`No input #${inputIndex}`); + const unsignedTx = transaction_1.Transaction.fromBuffer( + this.globalMap.unsignedTx, + ); + const sighashType = input.sighashType || 0x01; + let hash; if (input.nonWitnessUtxo) { - const unsignedTx = transaction_1.Transaction.fromBuffer( - this.globalMap.unsignedTx, - ); const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( input.nonWitnessUtxo, ); @@ -59,13 +78,25 @@ class Psbt extends bip174_1.Psbt { `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, ); } + const prevoutIndex = unsignedTx.ins[inputIndex].index; + const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; if (input.redeemScript) { - const prevoutIndex = unsignedTx.ins[inputIndex].index; - const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript(inputIndex, prevout.script, input.redeemScript); + hash = unsignedTx.hashForSignature( + inputIndex, + input.redeemScript, + sighashType, + ); + } else { + hash = unsignedTx.hashForSignature( + inputIndex, + prevout.script, + sighashType, + ); } } else if (input.witnessUtxo) { + let script; if (input.redeemScript) { // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript( @@ -73,16 +104,36 @@ class Psbt extends bip174_1.Psbt { 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, + ); + } else { + if (!input.witnessScript) + throw new Error('Segwit input needs witnessScript if not P2WPKH'); + checkWitnessScript(inputIndex, script, input.witnessScript); + hash = unsignedTx.hashForWitnessV0( + inputIndex, + script, + input.witnessUtxo.value, + sighashType, + ); + } + } else { + throw new Error('Need a Utxo input item for signing'); } - // TODO: Get hash to sign - const hash = Buffer.alloc(32); const partialSig = { pubkey: keyPair.publicKey, - signature: script.signature.encode( - keyPair.sign(hash), - input.sighashType || 0x01, - ), + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }; return this.addPartialSigToInput(inputIndex, partialSig); } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 63bc057..5ec9ef1 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,25 +1,45 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; import * as payments from './payments'; -import * as script from './script'; +import * as bscript from './script'; import { Transaction } from './transaction'; -const checkRedeemScript = ( +type ScriptCheckerFunction = (idx: number, spk: Buffer, rs: Buffer) => void; + +const scriptCheckerFactory = ( + payment: any, + paymentScriptName: string, +): ScriptCheckerFunction => ( inputIndex: number, scriptPubKey: Buffer, redeemScript: Buffer, ): void => { - const redeemScriptOutput = payments.p2sh({ + const redeemScriptOutput = payment({ redeem: { output: redeemScript }, }).output as Buffer; if (!scriptPubKey.equals(redeemScriptOutput)) { throw new Error( - `Redeem script for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, ); } }; +const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); +const checkWitnessScript = scriptCheckerFactory( + payments.p2wsh, + 'Witness script', +); + +const isP2WPKH = (script: Buffer): boolean => { + try { + payments.p2wpkh({ output: script }); + return true; + } catch (err) { + return false; + } +}; + export class Psbt extends PsbtBase { constructor() { super(); @@ -53,8 +73,11 @@ export class Psbt extends PsbtBase { const input = this.inputs[inputIndex]; if (input === undefined) throw new Error(`No input #${inputIndex}`); + const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); + const sighashType = input.sighashType || 0x01; + let hash: Buffer; + if (input.nonWitnessUtxo) { - const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); const prevoutHash = unsignedTx.ins[inputIndex].hash; @@ -67,14 +90,26 @@ export class Psbt extends PsbtBase { ); } - if (input.redeemScript) { - const prevoutIndex = unsignedTx.ins[inputIndex].index; - const prevout = nonWitnessUtxoTx.outs[prevoutIndex]; + 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); + hash = unsignedTx.hashForSignature( + inputIndex, + input.redeemScript, + sighashType, + ); + } else { + hash = unsignedTx.hashForSignature( + inputIndex, + prevout.script, + sighashType, + ); } } else if (input.witnessUtxo) { + let script: Buffer; if (input.redeemScript) { // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript( @@ -82,18 +117,37 @@ export class Psbt extends PsbtBase { 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, + ); + } else { + if (!input.witnessScript) + throw new Error('Segwit input needs witnessScript if not P2WPKH'); + checkWitnessScript(inputIndex, script, input.witnessScript); + hash = unsignedTx.hashForWitnessV0( + inputIndex, + script, + input.witnessUtxo.value, + sighashType, + ); + } + } else { + throw new Error('Need a Utxo input item for signing'); } - // TODO: Get hash to sign - const hash = Buffer.alloc(32); - const partialSig = { pubkey: keyPair.publicKey, - signature: script.signature.encode( - keyPair.sign(hash), - input.sighashType || 0x01, - ), + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }; return this.addPartialSigToInput(inputIndex, partialSig); From f72c915ff150188575c0766ee4ab6dbc622bfed0 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 1 Jul 2019 19:57:35 +0900 Subject: [PATCH 020/111] Start towards finalizing inputs --- package-lock.json | 6 ++--- package.json | 2 +- src/psbt.js | 60 ++++++++++++++++++++++++++++++++++++++++--- ts_src/psbt.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++--- types/psbt.d.ts | 5 +++- 5 files changed, 125 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index d409322..4195bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.8.tgz", - "integrity": "sha512-xWPzmlCvLoOWTlXk1wG7+TyOfaN8xX07IieuG4ug5su3igC9s4Lsdq+IEEMo+YHDQ4hPPAX9LYio6aEIAA+Zrg==" + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.10.tgz", + "integrity": "sha512-gFtSEMayg7HPKGnIQcEx5CqD/qHWuMlxLJ/+VV4k4Q2mcA0rY040JbNpFuCGVI6rJYv211f0NA7nkU4xkPX4nQ==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index b964697..1346928 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.8", + "bip174": "0.0.10", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", diff --git a/src/psbt.js b/src/psbt.js index 9a9ad7e..9d00e41 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1,6 +1,8 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); +const utils_1 = require('bip174/src/lib/utils'); +const classify = require('./classify'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); @@ -23,17 +25,67 @@ const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); -const isP2WPKH = script => { +const isPayment = (script, payment) => { try { - payments.p2wpkh({ output: script }); + payment({ output: script }); return true; } catch (err) { return false; } }; +function getScriptFromInput(inputIndex, input, _unsignedTx) { + let script; + if (input.nonWitnessUtxo) { + if (input.redeemScript) { + script = input.redeemScript; + } else { + const unsignedTx = transaction_1.Transaction.fromBuffer(_unsignedTx); + const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( + input.nonWitnessUtxo, + ); + const prevoutIndex = unsignedTx.ins[inputIndex].index; + script = nonWitnessUtxoTx.outs[prevoutIndex].script; + } + } else if (input.witnessUtxo) { + if (input.witnessScript) { + script = input.witnessScript; + } else if (input.redeemScript) { + script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output; + } else { + script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) + .output; + } + } else { + return; + } + return script; +} class Psbt extends bip174_1.Psbt { - constructor() { + constructor(network) { super(); + this.network = network; + } + canFinalize(inputIndex) { + const input = utils_1.checkForInput(this.inputs, inputIndex); + const script = getScriptFromInput( + inputIndex, + input, + this.globalMap.unsignedTx, + ); + if (!script) return false; + const scriptType = classify.output(script); + switch (scriptType) { + case 'pubkey': + return false; + case 'pubkeyhash': + return false; + case 'multisig': + return false; + case 'witnesspubkeyhash': + return false; + default: + return false; + } } signInput(inputIndex, keyPair) { // TODO: Implement BIP174 pre-sign checks: @@ -108,7 +160,7 @@ class Psbt extends bip174_1.Psbt { } else { script = input.witnessUtxo.script; } - if (isP2WPKH(script)) { + if (isPayment(script, payments.p2wpkh)) { // P2WPKH uses the P2PKH template for prevoutScript when signing const signingScript = payments.p2pkh({ hash: script.slice(2) }).output; hash = unsignedTx.hashForWitnessV0( diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 5ec9ef1..2f0b6ea 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,9 @@ import { Psbt as PsbtBase } from 'bip174'; +import { PsbtInput } from 'bip174/src/lib/interfaces'; +import { checkForInput } from 'bip174/src/lib/utils'; +import * as classify from './classify'; import { Signer } from './ecpair'; +import { Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Transaction } from './transaction'; @@ -31,20 +35,73 @@ const checkWitnessScript = scriptCheckerFactory( 'Witness script', ); -const isP2WPKH = (script: Buffer): boolean => { +const isPayment = (script: Buffer, payment: any): boolean => { try { - payments.p2wpkh({ output: script }); + payment({ output: script }); return true; } catch (err) { return false; } }; +function getScriptFromInput( + inputIndex: number, + input: PsbtInput, + _unsignedTx: Buffer, +): Buffer | undefined { + let script: Buffer; + if (input.nonWitnessUtxo) { + if (input.redeemScript) { + script = input.redeemScript; + } else { + const unsignedTx = Transaction.fromBuffer(_unsignedTx); + const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); + const prevoutIndex = unsignedTx.ins[inputIndex].index; + script = nonWitnessUtxoTx.outs[prevoutIndex].script; + } + } else if (input.witnessUtxo) { + if (input.witnessScript) { + script = input.witnessScript; + } else if (input.redeemScript) { + script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output!; + } else { + script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) + .output!; + } + } else { + return; + } + return script; +} + export class Psbt extends PsbtBase { - constructor() { + constructor(public network?: Network) { super(); } + canFinalize(inputIndex: number): boolean { + const input = checkForInput(this.inputs, inputIndex); + const script = getScriptFromInput( + inputIndex, + input, + this.globalMap.unsignedTx!, + ); + if (!script) return false; + const scriptType = classify.output(script); + switch (scriptType) { + case 'pubkey': + return false; + case 'pubkeyhash': + return false; + case 'multisig': + return false; + case 'witnesspubkeyhash': + return false; + default: + return false; + } + } + signInput(inputIndex: number, keyPair: Signer): Psbt { // TODO: Implement BIP174 pre-sign checks: // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer @@ -121,7 +178,7 @@ export class Psbt extends PsbtBase { } else { script = input.witnessUtxo.script; } - if (isP2WPKH(script)) { + if (isPayment(script, payments.p2wpkh)) { // P2WPKH uses the P2PKH template for prevoutScript when signing const signingScript = payments.p2pkh({ hash: script.slice(2) }).output!; hash = unsignedTx.hashForWitnessV0( diff --git a/types/psbt.d.ts b/types/psbt.d.ts index a58b982..fda7e6b 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,6 +1,9 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; +import { Network } from './networks'; export declare class Psbt extends PsbtBase { - constructor(); + network?: Network | undefined; + constructor(network?: Network | undefined); + canFinalize(inputIndex: number): boolean; signInput(inputIndex: number, keyPair: Signer): Psbt; } From f28e9cef71d457366e97423ca254e8c6da28dcda Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 2 Jul 2019 15:03:24 +0900 Subject: [PATCH 021/111] Refactor - Clean up sign - Get the meaningful script - Search for pubkey and prevent sign if can't find self - Tests failed, so comment out for now --- src/psbt.js | 289 +++++++++++++++++++++++--------------------- ts_src/psbt.ts | 318 ++++++++++++++++++++++++++++--------------------- 2 files changed, 335 insertions(+), 272 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 9d00e41..fa4ba9f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,10 +2,153 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const utils_1 = require('bip174/src/lib/utils'); -const classify = require('./classify'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +class Psbt extends bip174_1.Psbt { + constructor(network) { + super(); + this.network = network; + } + canFinalize(inputIndex) { + const input = utils_1.checkForInput(this.inputs, inputIndex); + const script = getScriptFromInput( + inputIndex, + input, + this.globalMap.unsignedTx, + ); + if (!script) return false; + const scriptType = classifyScript(script); + // TODO: for each type + switch (scriptType) { + case 'pubkey': + return false; + case 'pubkeyhash': + return false; + case 'multisig': + return false; + case 'witnesspubkeyhash': + return false; + default: + return false; + } + } + signInput(inputIndex, keyPair) { + const input = this.inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + const { hash, sighashType } = getHashForSig( + inputIndex, + input, + this.globalMap.unsignedTx, + ); + const pubkey = keyPair.publicKey; + // // TODO: throw error when the pubkey or pubkey hash is not found anywhere + // // in the script + // const pubkeyHash = hash160(keyPair.publicKey); + // + // const decompiled = bscript.decompile(script); + // if (decompiled === null) throw new Error('Unknown script error'); + // + // const hasKey = decompiled.some(element => { + // if (typeof element === 'number') return false; + // return element.equals(pubkey) || element.equals(pubkeyHash); + // }); + // + // if (!hasKey) { + // throw new Error( + // `Can not sign for this input with the key ${pubkey.toString('hex')}`, + // ); + // } + const partialSig = { + pubkey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }; + return this.addPartialSigToInput(inputIndex, partialSig); + } +} +exports.Psbt = Psbt; +const getHashForSig = (inputIndex, input, txBuf) => { + const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf); + const sighashType = + input.sighashType || transaction_1.Transaction.SIGHASH_ALL; + let hash; + let script; + if (input.nonWitnessUtxo) { + const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( + input.nonWitnessUtxo, + ); + const prevoutHash = unsignedTx.ins[inputIndex].hash; + const utxoHash = nonWitnessUtxoTx.getHash(); + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (!prevoutHash.equals(utxoHash)) { + throw new Error( + `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, + ); + } + 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; + hash = unsignedTx.hashForSignature( + inputIndex, + input.redeemScript, + sighashType, + ); + } else { + script = prevout.script; + hash = unsignedTx.hashForSignature( + inputIndex, + prevout.script, + sighashType, + ); + } + } 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 (!input.witnessScript) + throw new Error('Segwit input needs witnessScript if not P2WPKH'); + checkWitnessScript(inputIndex, _script, input.witnessScript); + hash = unsignedTx.hashForWitnessV0( + inputIndex, + _script, + input.witnessUtxo.value, + sighashType, + ); + // want to make sure the script we return is the actual meaningful script + script = input.witnessScript; + } + } else { + throw new Error('Need a Utxo input item for signing'); + } + return { + script, + sighashType, + hash, + }; +}; const scriptCheckerFactory = (payment, paymentScriptName) => ( inputIndex, scriptPubKey, @@ -25,7 +168,7 @@ const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); -const isPayment = (script, payment) => { +const isPaymentFactory = payment => script => { try { payment({ output: script }); return true; @@ -33,6 +176,17 @@ const isPayment = (script, payment) => { return false; } }; +const isP2WPKH = isPaymentFactory(payments.p2wpkh); +const isP2PKH = isPaymentFactory(payments.p2pkh); +const isP2MS = isPaymentFactory(payments.p2ms); +const isP2PK = isPaymentFactory(payments.p2pk); +const classifyScript = script => { + if (isP2WPKH(script)) return 'witnesspubkeyhash'; + if (isP2PKH(script)) return 'pubkeyhash'; + if (isP2MS(script)) return 'multisig'; + if (isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +}; function getScriptFromInput(inputIndex, input, _unsignedTx) { let script; if (input.nonWitnessUtxo) { @@ -60,134 +214,3 @@ function getScriptFromInput(inputIndex, input, _unsignedTx) { } return script; } -class Psbt extends bip174_1.Psbt { - constructor(network) { - super(); - this.network = network; - } - canFinalize(inputIndex) { - const input = utils_1.checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( - inputIndex, - input, - this.globalMap.unsignedTx, - ); - if (!script) return false; - const scriptType = classify.output(script); - switch (scriptType) { - case 'pubkey': - return false; - case 'pubkeyhash': - return false; - case 'multisig': - return false; - case 'witnesspubkeyhash': - return false; - default: - return false; - } - } - signInput(inputIndex, keyPair) { - // TODO: Implement BIP174 pre-sign checks: - // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer - // - // if non_witness_utxo.exists: - // assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash) - // if redeemScript.exists: - // assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript)) - // sign_non_witness(redeemScript) - // else: - // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey) - // else if witness_utxo.exists: - // if redeemScript.exists: - // assert(witness_utxo.scriptPubKey == P2SH(redeemScript)) - // script = redeemScript - // else: - // script = witness_utxo.scriptPubKey - // if IsP2WPKH(script): - // sign_witness(P2PKH(script[2:22])) - // else if IsP2WSH(script): - // assert(script == P2WSH(witnessScript)) - // sign_witness(witnessScript) - // else: - // assert False - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); - const unsignedTx = transaction_1.Transaction.fromBuffer( - this.globalMap.unsignedTx, - ); - const sighashType = input.sighashType || 0x01; - let hash; - if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( - input.nonWitnessUtxo, - ); - const prevoutHash = unsignedTx.ins[inputIndex].hash; - const utxoHash = nonWitnessUtxoTx.getHash(); - // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout - if (!prevoutHash.equals(utxoHash)) { - throw new Error( - `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, - ); - } - 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); - hash = unsignedTx.hashForSignature( - inputIndex, - input.redeemScript, - sighashType, - ); - } else { - hash = unsignedTx.hashForSignature( - inputIndex, - prevout.script, - sighashType, - ); - } - } else if (input.witnessUtxo) { - let script; - 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 (isPayment(script, payments.p2wpkh)) { - // 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, - ); - } else { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - script, - input.witnessUtxo.value, - sighashType, - ); - } - } else { - throw new Error('Need a Utxo input item for signing'); - } - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }; - return this.addPartialSigToInput(inputIndex, partialSig); - } -} -exports.Psbt = Psbt; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 2f0b6ea..63cc318 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,13 +1,174 @@ import { Psbt as PsbtBase } from 'bip174'; import { PsbtInput } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; -import * as classify from './classify'; +// import { hash160 } from './crypto'; // TODO: used in pubkey check import { Signer } from './ecpair'; import { Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Transaction } from './transaction'; +export class Psbt extends PsbtBase { + constructor(public network?: Network) { + super(); + } + + canFinalize(inputIndex: number): boolean { + const input = checkForInput(this.inputs, inputIndex); + const script = getScriptFromInput( + inputIndex, + input, + this.globalMap.unsignedTx!, + ); + if (!script) return false; + const scriptType = classifyScript(script); + // TODO: for each type + switch (scriptType) { + case 'pubkey': + return false; + case 'pubkeyhash': + return false; + case 'multisig': + return false; + case 'witnesspubkeyhash': + return false; + default: + return false; + } + } + + signInput(inputIndex: number, keyPair: Signer): Psbt { + const input = this.inputs[inputIndex]; + if (input === undefined) throw new Error(`No input #${inputIndex}`); + const { + hash, + sighashType, + // script, // TODO: use for pubkey check below + } = getHashForSig(inputIndex, input, this.globalMap.unsignedTx!); + + const pubkey = keyPair.publicKey; + // // TODO: throw error when the pubkey or pubkey hash is not found anywhere + // // in the script + // const pubkeyHash = hash160(keyPair.publicKey); + // + // const decompiled = bscript.decompile(script); + // if (decompiled === null) throw new Error('Unknown script error'); + // + // const hasKey = decompiled.some(element => { + // if (typeof element === 'number') return false; + // return element.equals(pubkey) || element.equals(pubkeyHash); + // }); + // + // if (!hasKey) { + // throw new Error( + // `Can not sign for this input with the key ${pubkey.toString('hex')}`, + // ); + // } + + const partialSig = { + pubkey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }; + + return this.addPartialSigToInput(inputIndex, partialSig); + } +} + +interface HashForSigData { + script: Buffer; + hash: Buffer; + sighashType: number; +} + +const getHashForSig = ( + inputIndex: number, + input: PsbtInput, + txBuf: Buffer, +): HashForSigData => { + const unsignedTx = Transaction.fromBuffer(txBuf); + const sighashType = input.sighashType || Transaction.SIGHASH_ALL; + let hash: Buffer; + let script: Buffer; + + if (input.nonWitnessUtxo) { + const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); + + const prevoutHash = unsignedTx.ins[inputIndex].hash; + const utxoHash = nonWitnessUtxoTx.getHash(); + + // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout + if (!prevoutHash.equals(utxoHash)) { + throw new Error( + `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, + ); + } + + 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; + hash = unsignedTx.hashForSignature( + inputIndex, + input.redeemScript, + sighashType, + ); + } else { + script = prevout.script; + hash = unsignedTx.hashForSignature( + inputIndex, + prevout.script, + sighashType, + ); + } + } 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 (!input.witnessScript) + throw new Error('Segwit input needs witnessScript if not P2WPKH'); + checkWitnessScript(inputIndex, _script, input.witnessScript); + hash = unsignedTx.hashForWitnessV0( + inputIndex, + _script, + input.witnessUtxo.value, + sighashType, + ); + // want to make sure the script we return is the actual meaningful script + script = input.witnessScript; + } + } else { + throw new Error('Need a Utxo input item for signing'); + } + return { + script, + sighashType, + hash, + }; +}; + type ScriptCheckerFunction = (idx: number, spk: Buffer, rs: Buffer) => void; const scriptCheckerFactory = ( @@ -35,7 +196,11 @@ const checkWitnessScript = scriptCheckerFactory( 'Witness script', ); -const isPayment = (script: Buffer, payment: any): boolean => { +type isPaymentFunction = (script: Buffer) => boolean; + +const isPaymentFactory = (payment: any): isPaymentFunction => ( + script: Buffer, +): boolean => { try { payment({ output: script }); return true; @@ -43,6 +208,18 @@ const isPayment = (script: Buffer, payment: any): boolean => { return false; } }; +const isP2WPKH = isPaymentFactory(payments.p2wpkh); +const isP2PKH = isPaymentFactory(payments.p2pkh); +const isP2MS = isPaymentFactory(payments.p2ms); +const isP2PK = isPaymentFactory(payments.p2pk); + +const classifyScript = (script: Buffer): string => { + if (isP2WPKH(script)) return 'witnesspubkeyhash'; + if (isP2PKH(script)) return 'pubkeyhash'; + if (isP2MS(script)) return 'multisig'; + if (isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +}; function getScriptFromInput( inputIndex: number, @@ -73,140 +250,3 @@ function getScriptFromInput( } return script; } - -export class Psbt extends PsbtBase { - constructor(public network?: Network) { - super(); - } - - canFinalize(inputIndex: number): boolean { - const input = checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( - inputIndex, - input, - this.globalMap.unsignedTx!, - ); - if (!script) return false; - const scriptType = classify.output(script); - switch (scriptType) { - case 'pubkey': - return false; - case 'pubkeyhash': - return false; - case 'multisig': - return false; - case 'witnesspubkeyhash': - return false; - default: - return false; - } - } - - signInput(inputIndex: number, keyPair: Signer): Psbt { - // TODO: Implement BIP174 pre-sign checks: - // https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#signer - // - // if non_witness_utxo.exists: - // assert(sha256d(non_witness_utxo) == psbt.tx.innput[i].prevout.hash) - // if redeemScript.exists: - // assert(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey == P2SH(redeemScript)) - // sign_non_witness(redeemScript) - // else: - // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey) - // else if witness_utxo.exists: - // if redeemScript.exists: - // assert(witness_utxo.scriptPubKey == P2SH(redeemScript)) - // script = redeemScript - // else: - // script = witness_utxo.scriptPubKey - // if IsP2WPKH(script): - // sign_witness(P2PKH(script[2:22])) - // else if IsP2WSH(script): - // assert(script == P2WSH(witnessScript)) - // sign_witness(witnessScript) - // else: - // assert False - - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); - - const unsignedTx = Transaction.fromBuffer(this.globalMap.unsignedTx!); - const sighashType = input.sighashType || 0x01; - let hash: Buffer; - - if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); - - const prevoutHash = unsignedTx.ins[inputIndex].hash; - const utxoHash = nonWitnessUtxoTx.getHash(); - - // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout - if (!prevoutHash.equals(utxoHash)) { - throw new Error( - `Non-witness UTXO hash for input #${inputIndex} doesn't match the hash specified in the prevout`, - ); - } - - 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); - hash = unsignedTx.hashForSignature( - inputIndex, - input.redeemScript, - sighashType, - ); - } else { - hash = unsignedTx.hashForSignature( - inputIndex, - prevout.script, - sighashType, - ); - } - } else if (input.witnessUtxo) { - let script: Buffer; - 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 (isPayment(script, payments.p2wpkh)) { - // 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, - ); - } else { - if (!input.witnessScript) - throw new Error('Segwit input needs witnessScript if not P2WPKH'); - checkWitnessScript(inputIndex, script, input.witnessScript); - hash = unsignedTx.hashForWitnessV0( - inputIndex, - script, - input.witnessUtxo.value, - sighashType, - ); - } - } else { - throw new Error('Need a Utxo input item for signing'); - } - - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }; - - return this.addPartialSigToInput(inputIndex, partialSig); - } -} From 4644e9d2ebc65fe7822070c6ed3bbe2821e5a9e7 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 2 Jul 2019 15:18:00 +0900 Subject: [PATCH 022/111] Finish canFinalize --- src/psbt.js | 16 +++++++++++----- ts_src/psbt.ts | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index fa4ba9f..cf1edca 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -19,16 +19,22 @@ class Psbt extends bip174_1.Psbt { ); if (!script) return false; const scriptType = classifyScript(script); - // TODO: for each type + const hasSigs = (neededSigs, partialSig) => { + if (!partialSig) return false; + if (partialSig.length > neededSigs) + throw new Error('Too many signatures'); + return partialSig.length === neededSigs; + }; switch (scriptType) { case 'pubkey': - return false; + return hasSigs(1, input.partialSig); case 'pubkeyhash': - return false; + return hasSigs(1, input.partialSig); case 'multisig': - return false; + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m, input.partialSig); case 'witnesspubkeyhash': - return false; + return hasSigs(1, input.partialSig); default: return false; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 63cc318..b1190aa 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -22,16 +22,24 @@ export class Psbt extends PsbtBase { ); if (!script) return false; const scriptType = classifyScript(script); - // TODO: for each type + + const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { + if (!partialSig) return false; + if (partialSig.length > neededSigs) + throw new Error('Too many signatures'); + return partialSig.length === neededSigs; + }; + switch (scriptType) { case 'pubkey': - return false; + return hasSigs(1, input.partialSig); case 'pubkeyhash': - return false; + return hasSigs(1, input.partialSig); case 'multisig': - return false; + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m!, input.partialSig); case 'witnesspubkeyhash': - return false; + return hasSigs(1, input.partialSig); default: return false; } From 354d67a31aaca6c04dc352d361371810ed8ddfa1 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 2 Jul 2019 15:35:23 +0900 Subject: [PATCH 023/111] Just some ideas, TODO mostly. --- src/psbt.js | 12 ++++++++++++ ts_src/psbt.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index cf1edca..651aeb2 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -6,9 +6,21 @@ const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); class Psbt extends bip174_1.Psbt { + // protected __TX: Transaction; constructor(network) { super(); this.network = network; + // // TODO: figure out a way to use a Transaction Object instead of a Buffer + // // TODO: Caching, since .toBuffer() calls every time we get is lame. + // this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); + // delete this.globalMap.unsignedTx; + // Object.defineProperty(this.globalMap, 'unsignedTx', { + // enumerable: true, + // writable: false, + // get(): Buffer { + // return this.__TX.toBuffer(); + // } + // }); } canFinalize(inputIndex) { const input = utils_1.checkForInput(this.inputs, inputIndex); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index b1190aa..cf60f91 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -9,8 +9,20 @@ import * as bscript from './script'; import { Transaction } from './transaction'; export class Psbt extends PsbtBase { + // protected __TX: Transaction; constructor(public network?: Network) { super(); + // // TODO: figure out a way to use a Transaction Object instead of a Buffer + // // TODO: Caching, since .toBuffer() calls every time we get is lame. + // this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); + // delete this.globalMap.unsignedTx; + // Object.defineProperty(this.globalMap, 'unsignedTx', { + // enumerable: true, + // writable: false, + // get(): Buffer { + // return this.__TX.toBuffer(); + // } + // }); } canFinalize(inputIndex: number): boolean { From 7ff40cebc4abb81b83cb8c0a287e066d8d64e094 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 2 Jul 2019 18:15:30 +0700 Subject: [PATCH 024/111] Recreate test case PSBTs and try and sign them with the valid key --- test/fixtures/psbt.json | 32 +++++++++++++++++++------------- test/psbt.js | 13 ++++++++----- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 1b72874..4aaec98 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -4,37 +4,43 @@ { "description": "checks non-witness UTXO matches the hash specified in the prevout", "shouldSign": { - "psbt": "cHNidP8BADMBAAAAAZIApPIHomdoPo/sOJ8NI9jzILd0cnGn0yAZHRv3D9DSAAAAAAD/////AAAAAAAAAQD9tQEBAAAAAn3dz3mi5UEDC8dThx0cnU3BY+TWvVrvrkvYTeZOFqZSAAAAAItIMEUCIQDTosO1iuDwtVFxGqiUn0eHJEKO+gPzF5w6UNwsms5GqgIgGy2oSiFCmhCvGHcxyIL8H3J+e4lXPgfwGS6ePeefq/ABQQQOOnWcM7A+Gvjl2G+0R6QO/yRMhHpPgnQnbbSQBU6L4Hb4gB3cnFJG7oa28zz+OOi35Xq52zkOs+wexq6e7qET/f///0/vbX88Hl0L6nM7L9ZE+kVs33PyHrfohmonIdeSZunoAQAAAIpHMEQCIChKKYnUXEimyKVWsws0Z+r2q/hm7HXE2fDjhyB09iwHAiBoatgoacFmnmvhYsSjTkxhepcXZvK5aIeJ6y9Jj+XrawFBBA46dZwzsD4a+OXYb7RHpA7/JEyEek+CdCdttJAFTovgdviAHdycUkbuhrbzPP446LflernbOQ6z7B7Grp7uoRP9////AiTHDQAAAAAAGXapFNpkc+03Pgj0bdgAP8p7py++nFVeiKycsA4AAAAAABl2qRRJcHmSWY+FoxqmcVr3D+UHYQtvi4isEcAIAAAA", - "inputToCheck": 0 + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" }, "shouldThrow": { "errorMessage": "Non-witness UTXO hash for input #0 doesn't match the hash specified in the prevout", - "psbt": "cHNidP8BADMBAAAAAZIApPIHomdoPo/sOJ8NI9jzILd0cnGn0yAZHRv3D9DSAAAAAAD/////AAAAAAAAAQC/AgAAAAHBYCumjIwkFFCni2Hb/eJymJGB0HU3secNMbfbk5VX8gAAAABqRzBEAiAphyuXV5hQyHZY5DG7nfSj8+QVkHd1KaVeJesR7Mr+9QIgURcAqh6iws1JklH5kBTyLFr2OgDHb9JNplABSgqBmekBIQJkGH2e53OqMzrCI2eEeLHfPqJoF4/JRH4KYMRD7dqnSf3///8BmV0AAAAAAAAZdqkUdZ1mdwkelzuenZnxnGj79D4/BfmIrMjTCAAAAA==", - "inputToCheck": 0 + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQD4AQAAAAABAbD7u8z1SxTjfvhwmkQQvdbbWA+n3GKBBmGecSIAaM5jBQAAABcWABS0SqIhdn2LbW4TAJc3GVh7SnD/eP////8CNg8AAAAAAAAWABSmNm8WWVF+wq+QAeRo9d763jEXhRAnAAAAAAAAGXapFNpkc+03Pgj0bdgAP8p7py++nFVeiKwCRzBEAiB/u0BLwdeerqWf0JH33wwMv8Nn3sKblFvj+CntdC4B9gIgKVVHBH1c9ewnzkuyW6dnz1YARujBJnle1eBNSBAJD9IBIQOmYxHmd2Yz53FpC9+nv+pKdM+5OyEAW3BAN2cccQ0LkgAAAAAAAA==", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } }, { "description": "checks redeem script matches the scriptPubKey in a non-witness prevout", "shouldSign": { - "psbt": "cHNidP8BADMBAAAAAVjoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////AAAAAAAAAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuAAA=", - "inputToCheck": 0 + "psbt": "cHNidP8BADMBAAAAAR2dq8JwBaxnbWHZGw0HdxuUGFcg6dvx3pgjWMm+Pzf2AAAAAAD/////AAAAAAAAAQC9AgAAAAH//////////////////////////////////////////wAAAABqRzBEAiAf7N+IK1uuxTxvEOoVGabNsiT7jMlfSDCd0VYxv+sQTQIgQVYM7ig9TIx1LzrX2RXgw2zW2fMKuRs/bT9eZx6jmYwBIQJpKKFOB6PrPJhRAtaQ+cHHryY5QYIi5dxZtkMwCtuFYf////8BAOH1BQAAAAAXqRRdh8wk5NRiF7VGQ4Zb4i8Vl1YFMocAAAAAAQRpUiECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWEhAgHGF3SgP82qhvqMptNluTLHhLtzDsmc0pNWEDETNj/rIQIFIl+T3Z90vBFGN8uYJHCrUO4DvrOGVWkVDsBeEzBUi1OuAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" }, "shouldThrow": { "errorMessage": "Redeem script for input #0 doesn't match the scriptPubKey in the prevout", - "psbt": "cHNidP8BADMBAAAAAVjoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////AAAAAAAAAQC7AgAAAAGq1zkxAYvSX4SuQAtohIvgnbcG6sKsGCmLq+5xq2VviwAAAABIRzBEAiBY9vx8ajPhsxVI1IHIJsAVvTATWq1CzWd5Datm0q0kOwIgShztJgTGc1tjk+W0FpHdeLAPDFlC+591GFb6qTgVfboB/v///wKA8PoCAAAAABepFA+5RjQhaWuCyDOvJBx4wX3b3kk0h9DyCicBAAAAF6kUKcp0+KCPgZmUKBhcl7XYUuQGP2GHZQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMAAA==", - "inputToCheck": 0 + "psbt": "cHNidP8BADMBAAAAAR2dq8JwBaxnbWHZGw0HdxuUGFcg6dvx3pgjWMm+Pzf2AAAAAAD/////AAAAAAAAAQC9AgAAAAH//////////////////////////////////////////wAAAABqRzBEAiAf7N+IK1uuxTxvEOoVGabNsiT7jMlfSDCd0VYxv+sQTQIgQVYM7ig9TIx1LzrX2RXgw2zW2fMKuRs/bT9eZx6jmYwBIQJpKKFOB6PrPJhRAtaQ+cHHryY5QYIi5dxZtkMwCtuFYf////8BAOH1BQAAAAAXqRRdh8wk5NRiF7VGQ4Zb4i8Vl1YFMocAAAAAAQRpUiEDGMZFrWWJBIIu33FdV9Q+Zit0fcoBOdgS7ooA2h2QlbAhAuAzQeDZh730hBbfTPzlaXJgCh2Jyui/ufS0k8wqJ55FIQKMg6lgEnyRnGIZ90eP4MmuRdT3EcO4+irJEm5yTCiko1OuAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } }, { "description": "checks redeem script matches the scriptPubKey in a witness prevout", "shouldSign": { - "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", - "inputToCheck": 1 + "psbt": "cHNidP8BADMBAAAAAYaq+PdOUY2PnV9kZKa82XlqrPByOqwH2TRg2+LQdqs2AAAAAAD/////AAAAAAAAAQEgAOH1BQAAAAAXqRSTNeWHqa9INvSnQ120SZeJc+6JSocBBBYAFC8spHIOpiw9giaEPd5RGkMYvXRHAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" }, "shouldThrow": { - "errorMessage": "Redeem script for input #1 doesn't match the scriptPubKey in the prevout", - "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", - "inputToCheck": 1 + "errorMessage": "Redeem script for input #0 doesn't match the scriptPubKey in the prevout", + "psbt": "cHNidP8BADMBAAAAAYaq+PdOUY2PnV9kZKa82XlqrPByOqwH2TRg2+LQdqs2AAAAAAD/////AAAAAAAAAQEgAOH1BQAAAAAXqRSTNeWHqa9INvSnQ120SZeJc+6JSocBBBYAFA3zpl6FMnlgCviVJgbcnBj01iLgAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } } ] diff --git a/test/psbt.js b/test/psbt.js index 3bf873d..8584bd6 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -7,20 +7,23 @@ const Psbt = require('..').Psbt const fixtures = require('./fixtures/psbt') describe(`Psbt`, () => { - // constants - const keyPair = ECPair.fromPrivateKey(Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex')) - describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) assert.doesNotThrow(() => { - psbtThatShouldsign.signInput(f.shouldSign.inputToCheck, keyPair) + psbtThatShouldsign.signInput( + f.shouldSign.inputToCheck, + ECPair.fromWIF(f.shouldSign.WIF), + ) }) const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) assert.throws(() => { - psbtThatShouldThrow.signInput(f.shouldThrow.inputToCheck, keyPair) + psbtThatShouldThrow.signInput( + f.shouldThrow.inputToCheck, + ECPair.fromWIF(f.shouldThrow.WIF), + ) }, {message: f.shouldThrow.errorMessage}) }) }) From 8d74bebe044c76c4229e424ebf10dc35feb45a06 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 2 Jul 2019 18:17:37 +0700 Subject: [PATCH 025/111] Throw error when signing with a privkey that doesn't match the pubkey --- src/psbt.js | 32 ++++++++++++++------------------ ts_src/psbt.ts | 44 +++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 651aeb2..e03969d 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const utils_1 = require('bip174/src/lib/utils'); +const crypto_1 = require('./crypto'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); @@ -54,29 +55,24 @@ class Psbt extends bip174_1.Psbt { signInput(inputIndex, keyPair) { const input = this.inputs[inputIndex]; if (input === undefined) throw new Error(`No input #${inputIndex}`); - const { hash, sighashType } = getHashForSig( + const { hash, sighashType, script } = getHashForSig( inputIndex, input, this.globalMap.unsignedTx, ); const pubkey = keyPair.publicKey; - // // TODO: throw error when the pubkey or pubkey hash is not found anywhere - // // in the script - // const pubkeyHash = hash160(keyPair.publicKey); - // - // const decompiled = bscript.decompile(script); - // if (decompiled === null) throw new Error('Unknown script error'); - // - // const hasKey = decompiled.some(element => { - // if (typeof element === 'number') return false; - // return element.equals(pubkey) || element.equals(pubkeyHash); - // }); - // - // if (!hasKey) { - // throw new Error( - // `Can not sign for this input with the key ${pubkey.toString('hex')}`, - // ); - // } + const pubkeyHash = crypto_1.hash160(keyPair.publicKey); + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + const hasKey = decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); + if (!hasKey) { + throw new Error( + `Can not sign for this input with the key ${pubkey.toString('hex')}`, + ); + } const partialSig = { pubkey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index cf60f91..194a1f7 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,7 +1,7 @@ import { Psbt as PsbtBase } from 'bip174'; import { PsbtInput } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; -// import { hash160 } from './crypto'; // TODO: used in pubkey check +import { hash160 } from './crypto'; import { Signer } from './ecpair'; import { Network } from './networks'; import * as payments from './payments'; @@ -60,30 +60,28 @@ export class Psbt extends PsbtBase { signInput(inputIndex: number, keyPair: Signer): Psbt { const input = this.inputs[inputIndex]; if (input === undefined) throw new Error(`No input #${inputIndex}`); - const { - hash, - sighashType, - // script, // TODO: use for pubkey check below - } = getHashForSig(inputIndex, input, this.globalMap.unsignedTx!); + const { hash, sighashType, script } = getHashForSig( + inputIndex, + input, + this.globalMap.unsignedTx!, + ); const pubkey = keyPair.publicKey; - // // TODO: throw error when the pubkey or pubkey hash is not found anywhere - // // in the script - // const pubkeyHash = hash160(keyPair.publicKey); - // - // const decompiled = bscript.decompile(script); - // if (decompiled === null) throw new Error('Unknown script error'); - // - // const hasKey = decompiled.some(element => { - // if (typeof element === 'number') return false; - // return element.equals(pubkey) || element.equals(pubkeyHash); - // }); - // - // if (!hasKey) { - // throw new Error( - // `Can not sign for this input with the key ${pubkey.toString('hex')}`, - // ); - // } + const pubkeyHash = hash160(keyPair.publicKey); + + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + + const hasKey = decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); + + if (!hasKey) { + throw new Error( + `Can not sign for this input with the key ${pubkey.toString('hex')}`, + ); + } const partialSig = { pubkey, From 658ea845b12a9212567b7bdb262594c009510117 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 2 Jul 2019 18:20:55 +0700 Subject: [PATCH 026/111] Test matching privkey check --- test/fixtures/psbt.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 4aaec98..696cdcc 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -1,6 +1,20 @@ { "signInput": { "checks": [ + { + "description": "checks privkey matches the input it's signing", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + }, + "shouldThrow": { + "errorMessage": "Can not sign for this input with the key 02e717fee6be913148f9fd676c0876b7e4574118542c6758b4a9fb9f38f171842b", + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 0, + "WIF": "Kz4mjzErKCH5eQ97RXNQd3Wv7WsLA83BjynfQk4N7BB8J5xuUjAv" + } + }, { "description": "checks non-witness UTXO matches the hash specified in the prevout", "shouldSign": { From b8789c5d13be55d136eb24904fa285ccfcaa8668 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 2 Jul 2019 18:29:14 +0700 Subject: [PATCH 027/111] Test input exists check --- test/fixtures/psbt.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 696cdcc..5e008a4 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -1,6 +1,20 @@ { "signInput": { "checks": [ + { + "description": "checks the input exists", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + }, + "shouldThrow": { + "errorMessage": "No input #1", + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 1, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + } + }, { "description": "checks privkey matches the input it's signing", "shouldSign": { From 343297a3597141a25ef69035f8c20f66f8ae18e1 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 2 Jul 2019 18:31:46 +0700 Subject: [PATCH 028/111] Test error if UTXO doesn't exist --- test/fixtures/psbt.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 5e008a4..371a8b7 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -15,6 +15,20 @@ "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } }, + { + "description": "checks a UTXO value exists for the input", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + }, + "shouldThrow": { + "errorMessage": "Need a Utxo input item for signing", + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAAA=", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + } + }, { "description": "checks privkey matches the input it's signing", "shouldSign": { From 813b84f91f0294c89fc11cdb66aefd80e9adb28a Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 3 Jul 2019 15:13:36 +0900 Subject: [PATCH 029/111] Finalize and extract done --- src/psbt.js | 217 +++++++++++++++++++++++++++++++-------- ts_src/psbt.ts | 268 +++++++++++++++++++++++++++++++++++++++--------- types/psbt.d.ts | 8 +- 3 files changed, 400 insertions(+), 93 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index e03969d..59d898c 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -6,6 +6,7 @@ const crypto_1 = require('./crypto'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { // protected __TX: Transaction; constructor(network) { @@ -23,56 +24,75 @@ class Psbt extends bip174_1.Psbt { // } // }); } - canFinalize(inputIndex) { + extractTransaction() { + if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + const tx = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + this.inputs.forEach((input, idx) => { + if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; + if (input.finalScriptWitness) { + const decompiled = bscript.decompile(input.finalScriptWitness); + if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); + } + }); + return tx; + } + finalizeAllInputs() { + const inputResults = range(this.inputs.length).map(idx => + this.finalizeInput(idx), + ); + const result = inputResults.every(val => val === true); + return { + result, + inputResults, + }; + } + finalizeInput(inputIndex) { const input = utils_1.checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( + const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, this.globalMap.unsignedTx, ); if (!script) return false; const scriptType = classifyScript(script); - const hasSigs = (neededSigs, partialSig) => { - if (!partialSig) return false; - if (partialSig.length > neededSigs) - throw new Error('Too many signatures'); - return partialSig.length === neededSigs; - }; - switch (scriptType) { - case 'pubkey': - return hasSigs(1, input.partialSig); - case 'pubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m, input.partialSig); - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - default: - return false; + if (!canFinalize(input, script, scriptType)) return false; + let finalScriptSig; + let finalScriptWitness; + // Wow, the payments API is very handy + const payment = getPayment(script, scriptType, input.partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness); + } + if (p2sh) { + finalScriptSig = bscript.compile([p2sh.redeem.output]); + } + } else { + finalScriptSig = payment.input; } + if (finalScriptSig) + this.addFinalScriptSigToInput(inputIndex, finalScriptSig); + if (finalScriptWitness) + this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + if (!finalScriptSig && !finalScriptWitness) return false; + this.clearFinalizedInput(inputIndex); + return true; } signInput(inputIndex, keyPair) { - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); + const input = utils_1.checkForInput(this.inputs, inputIndex); + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); const { hash, sighashType, script } = getHashForSig( inputIndex, input, this.globalMap.unsignedTx, ); const pubkey = keyPair.publicKey; - const pubkeyHash = crypto_1.hash160(keyPair.publicKey); - const decompiled = bscript.decompile(script); - if (decompiled === null) throw new Error('Unknown script error'); - const hasKey = decompiled.some(element => { - if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); - }); - if (!hasKey) { - throw new Error( - `Can not sign for this input with the key ${pubkey.toString('hex')}`, - ); - } + checkScriptForPubkey(pubkey, script); const partialSig = { pubkey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), @@ -81,6 +101,77 @@ class Psbt extends bip174_1.Psbt { } } exports.Psbt = Psbt; +// +// +// +// +// Helper functions +// +// +// +// +function isFinalized(input) { + return !!input.finalScriptSig || !!input.finalScriptWitness; +} +function getPayment(script, scriptType, partialSig) { + let payment; + switch (scriptType) { + case 'multisig': + payment = payments.p2ms({ + output: script, + signatures: partialSig.map(ps => ps.signature), + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment; +} +function canFinalize(input, script, scriptType) { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m, input.partialSig); + default: + return false; + } +} +function checkScriptForPubkey(pubkey, script) { + const pubkeyHash = crypto_1.hash160(pubkey); + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + const hasKey = decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); + if (!hasKey) { + throw new Error( + `Can not sign for this input with the key ${pubkey.toString('hex')}`, + ); + } +} const getHashForSig = (inputIndex, input, txBuf) => { const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf); const sighashType = @@ -202,29 +293,67 @@ const classifyScript = script => { return 'nonstandard'; }; function getScriptFromInput(inputIndex, input, _unsignedTx) { - let script; + const res = { + script: null, + isSegwit: false, + isP2SH: false, + isP2WSH: false, + }; if (input.nonWitnessUtxo) { if (input.redeemScript) { - script = input.redeemScript; + res.isP2SH = true; + res.script = input.redeemScript; } else { const unsignedTx = transaction_1.Transaction.fromBuffer(_unsignedTx); const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( input.nonWitnessUtxo, ); const prevoutIndex = unsignedTx.ins[inputIndex].index; - script = nonWitnessUtxoTx.outs[prevoutIndex].script; + res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } } else if (input.witnessUtxo) { + res.isSegwit = true; + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript; if (input.witnessScript) { - script = input.witnessScript; + res.script = input.witnessScript; } else if (input.redeemScript) { - script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output; + res.script = payments.p2pkh({ + hash: input.redeemScript.slice(2), + }).output; } else { - script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) - .output; + res.script = payments.p2pkh({ + hash: input.witnessUtxo.script.slice(2), + }).output; } - } else { - return; } - return script; + return res; } +const hasSigs = (neededSigs, partialSig) => { + if (!partialSig) return false; + if (partialSig.length > neededSigs) throw new Error('Too many signatures'); + return partialSig.length === neededSigs; +}; +function witnessStackToScriptWitness(witness) { + let buffer = Buffer.allocUnsafe(0); + function writeSlice(slice) { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + function writeVarInt(i) { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + function writeVarSlice(slice) { + writeVarInt(slice.length); + writeSlice(slice); + } + function writeVector(vector) { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + writeVector(witness); + return buffer; +} +const range = n => [...Array(n).keys()]; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 194a1f7..273a1c3 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,5 @@ import { Psbt as PsbtBase } from 'bip174'; -import { PsbtInput } from 'bip174/src/lib/interfaces'; +import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; import { hash160 } from './crypto'; import { Signer } from './ecpair'; @@ -7,6 +7,7 @@ import { Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Transaction } from './transaction'; +const varuint = require('varuint-bitcoin'); export class Psbt extends PsbtBase { // protected __TX: Transaction; @@ -25,41 +26,84 @@ export class Psbt extends PsbtBase { // }); } - canFinalize(inputIndex: number): boolean { + extractTransaction(): Transaction { + if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + const tx = Transaction.fromBuffer(this.globalMap.unsignedTx!); + this.inputs.forEach((input, idx) => { + if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; + if (input.finalScriptWitness) { + const decompiled = bscript.decompile(input.finalScriptWitness); + if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); + } + }); + return tx; + } + + finalizeAllInputs(): { + result: boolean; + inputResults: boolean[]; + } { + const inputResults = range(this.inputs.length).map(idx => + this.finalizeInput(idx), + ); + const result = inputResults.every(val => val === true); + return { + result, + inputResults, + }; + } + + finalizeInput(inputIndex: number): boolean { const input = checkForInput(this.inputs, inputIndex); - const script = getScriptFromInput( + const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, this.globalMap.unsignedTx!, ); if (!script) return false; + const scriptType = classifyScript(script); + if (!canFinalize(input, script, scriptType)) return false; - const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { - if (!partialSig) return false; - if (partialSig.length > neededSigs) - throw new Error('Too many signatures'); - return partialSig.length === neededSigs; - }; + let finalScriptSig: Buffer | undefined; + let finalScriptWitness: Buffer | undefined; - switch (scriptType) { - case 'pubkey': - return hasSigs(1, input.partialSig); - case 'pubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m!, input.partialSig); - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - default: - return false; + // Wow, the payments API is very handy + const payment: payments.Payment = getPayment( + script, + scriptType, + input.partialSig!, + ); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness!); + } + if (p2sh) { + finalScriptSig = bscript.compile([p2sh.redeem!.output!]); + } + } else { + finalScriptSig = payment.input; } + + if (finalScriptSig) + this.addFinalScriptSigToInput(inputIndex, finalScriptSig); + if (finalScriptWitness) + this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + if (!finalScriptSig && !finalScriptWitness) return false; + + this.clearFinalizedInput(inputIndex); + return true; } signInput(inputIndex: number, keyPair: Signer): Psbt { - const input = this.inputs[inputIndex]; - if (input === undefined) throw new Error(`No input #${inputIndex}`); + const input = checkForInput(this.inputs, inputIndex); + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); const { hash, sighashType, script } = getHashForSig( inputIndex, input, @@ -67,21 +111,8 @@ export class Psbt extends PsbtBase { ); const pubkey = keyPair.publicKey; - const pubkeyHash = hash160(keyPair.publicKey); - const decompiled = bscript.decompile(script); - if (decompiled === null) throw new Error('Unknown script error'); - - const hasKey = decompiled.some(element => { - if (typeof element === 'number') return false; - return element.equals(pubkey) || element.equals(pubkeyHash); - }); - - if (!hasKey) { - throw new Error( - `Can not sign for this input with the key ${pubkey.toString('hex')}`, - ); - } + checkScriptForPubkey(pubkey, script); const partialSig = { pubkey, @@ -92,6 +123,93 @@ export class Psbt extends PsbtBase { } } +// +// +// +// +// Helper functions +// +// +// +// + +function isFinalized(input: PsbtInput): boolean { + return !!input.finalScriptSig || !!input.finalScriptWitness; +} + +function getPayment( + script: Buffer, + scriptType: string, + partialSig: PartialSig[], +): payments.Payment { + let payment: payments.Payment; + switch (scriptType) { + case 'multisig': + payment = payments.p2ms({ + output: script, + signatures: partialSig.map(ps => ps.signature), + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + } + return payment!; +} + +function canFinalize( + input: PsbtInput, + script: Buffer, + scriptType: string, +): boolean { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m!, input.partialSig); + default: + return false; + } +} + +function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void { + const pubkeyHash = hash160(pubkey); + + const decompiled = bscript.decompile(script); + if (decompiled === null) throw new Error('Unknown script error'); + + const hasKey = decompiled.some(element => { + if (typeof element === 'number') return false; + return element.equals(pubkey) || element.equals(pubkeyHash); + }); + + if (!hasKey) { + throw new Error( + `Can not sign for this input with the key ${pubkey.toString('hex')}`, + ); + } +} + interface HashForSigData { script: Buffer; hash: Buffer; @@ -239,32 +357,86 @@ const classifyScript = (script: Buffer): string => { return 'nonstandard'; }; +interface GetScriptReturn { + script: Buffer | null; + isSegwit: boolean; + isP2SH: boolean; + isP2WSH: boolean; +} function getScriptFromInput( inputIndex: number, input: PsbtInput, _unsignedTx: Buffer, -): Buffer | undefined { - let script: Buffer; +): GetScriptReturn { + const res: GetScriptReturn = { + script: null, + isSegwit: false, + isP2SH: false, + isP2WSH: false, + }; if (input.nonWitnessUtxo) { if (input.redeemScript) { - script = input.redeemScript; + res.isP2SH = true; + res.script = input.redeemScript; } else { const unsignedTx = Transaction.fromBuffer(_unsignedTx); const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); const prevoutIndex = unsignedTx.ins[inputIndex].index; - script = nonWitnessUtxoTx.outs[prevoutIndex].script; + res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } } else if (input.witnessUtxo) { + res.isSegwit = true; + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript; if (input.witnessScript) { - script = input.witnessScript; + res.script = input.witnessScript; } else if (input.redeemScript) { - script = payments.p2pkh({ hash: input.redeemScript.slice(2) }).output!; + res.script = payments.p2pkh({ + hash: input.redeemScript.slice(2), + }).output!; } else { - script = payments.p2pkh({ hash: input.witnessUtxo.script.slice(2) }) - .output!; + res.script = payments.p2pkh({ + hash: input.witnessUtxo.script.slice(2), + }).output!; } - } else { - return; } - return script; + return res; } + +const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { + if (!partialSig) return false; + if (partialSig.length > neededSigs) throw new Error('Too many signatures'); + return partialSig.length === neededSigs; +}; + +function witnessStackToScriptWitness(witness: Buffer[]): Buffer { + let buffer = Buffer.allocUnsafe(0); + + function writeSlice(slice: Buffer): void { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + + function writeVarInt(i: number): void { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + + function writeVarSlice(slice: Buffer): void { + writeVarInt(slice.length); + writeSlice(slice); + } + + function writeVector(vector: Buffer[]): void { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + + writeVector(witness); + + return buffer; +} + +const range = (n: number): number[] => [...Array(n).keys()]; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index fda7e6b..f5b6430 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,9 +1,15 @@ import { Psbt as PsbtBase } from 'bip174'; import { Signer } from './ecpair'; import { Network } from './networks'; +import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { network?: Network | undefined; constructor(network?: Network | undefined); - canFinalize(inputIndex: number): boolean; + extractTransaction(): Transaction; + finalizeAllInputs(): { + result: boolean; + inputResults: boolean[]; + }; + finalizeInput(inputIndex: number): boolean; signInput(inputIndex: number, keyPair: Signer): Psbt; } From 77dde89acc047607ed9a30506379bc80fdba3642 Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 3 Jul 2019 15:34:18 +0900 Subject: [PATCH 030/111] Add async signing method --- src/psbt.js | 94 ++++++++++++++++++++++++++++---------- ts_src/psbt.ts | 117 ++++++++++++++++++++++++++++++++++++------------ tsconfig.json | 2 +- types/psbt.d.ts | 3 +- 4 files changed, 161 insertions(+), 55 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 59d898c..ae60861 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -56,24 +56,14 @@ class Psbt extends bip174_1.Psbt { if (!script) return false; const scriptType = classifyScript(script); if (!canFinalize(input, script, scriptType)) return false; - let finalScriptSig; - let finalScriptWitness; - // Wow, the payments API is very handy - const payment = getPayment(script, scriptType, input.partialSig); - const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); - const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); - if (isSegwit) { - if (p2wsh) { - finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); - } else { - finalScriptWitness = witnessStackToScriptWitness(payment.witness); - } - if (p2sh) { - finalScriptSig = bscript.compile([p2sh.redeem.output]); - } - } else { - finalScriptSig = payment.input; - } + const { finalScriptSig, finalScriptWitness } = getFinalScripts( + script, + scriptType, + input.partialSig, + isSegwit, + isP2SH, + isP2WSH, + ); if (finalScriptSig) this.addFinalScriptSigToInput(inputIndex, finalScriptSig); if (finalScriptWitness) @@ -83,22 +73,38 @@ class Psbt extends bip174_1.Psbt { return true; } signInput(inputIndex, keyPair) { - const input = utils_1.checkForInput(this.inputs, inputIndex); if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash, sighashType, script } = getHashForSig( + const { hash, sighashType } = getHashAndSighashType( + this.inputs, inputIndex, - input, + keyPair.publicKey, this.globalMap.unsignedTx, ); - const pubkey = keyPair.publicKey; - checkScriptForPubkey(pubkey, script); const partialSig = { - pubkey, + pubkey: keyPair.publicKey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }; return this.addPartialSigToInput(inputIndex, partialSig); } + async signInputAsync(inputIndex, keyPair) { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + const { hash, sighashType } = getHashAndSighashType( + this.inputs, + inputIndex, + keyPair.publicKey, + this.globalMap.unsignedTx, + ); + const partialSig = { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode( + await keyPair.sign(hash), + sighashType, + ), + }; + this.addPartialSigToInput(inputIndex, partialSig); + } } exports.Psbt = Psbt; // @@ -113,6 +119,46 @@ exports.Psbt = Psbt; function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } +function getHashAndSighashType(inputs, inputIndex, pubkey, txBuf) { + const input = utils_1.checkForInput(inputs, inputIndex); + const { hash, sighashType, script } = getHashForSig(inputIndex, input, txBuf); + checkScriptForPubkey(pubkey, script); + return { + hash, + sighashType, + }; +} +function getFinalScripts( + script, + scriptType, + partialSig, + isSegwit, + isP2SH, + isP2WSH, +) { + let finalScriptSig; + let finalScriptWitness; + // Wow, the payments API is very handy + const payment = getPayment(script, scriptType, partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness); + } + if (p2sh) { + finalScriptSig = bscript.compile([p2sh.redeem.output]); + } + } else { + finalScriptSig = payment.input; + } + return { + finalScriptSig, + finalScriptWitness, + }; +} function getPayment(script, scriptType, partialSig) { let payment; switch (scriptType) { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 273a1c3..d9a059c 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -2,7 +2,7 @@ import { Psbt as PsbtBase } from 'bip174'; import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; import { hash160 } from './crypto'; -import { Signer } from './ecpair'; +import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; @@ -65,30 +65,14 @@ export class Psbt extends PsbtBase { const scriptType = classifyScript(script); if (!canFinalize(input, script, scriptType)) return false; - let finalScriptSig: Buffer | undefined; - let finalScriptWitness: Buffer | undefined; - - // Wow, the payments API is very handy - const payment: payments.Payment = getPayment( + const { finalScriptSig, finalScriptWitness } = getFinalScripts( script, scriptType, input.partialSig!, + isSegwit, + isP2SH, + isP2WSH, ); - const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); - const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); - - if (isSegwit) { - if (p2wsh) { - finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); - } else { - finalScriptWitness = witnessStackToScriptWitness(payment.witness!); - } - if (p2sh) { - finalScriptSig = bscript.compile([p2sh.redeem!.output!]); - } - } else { - finalScriptSig = payment.input; - } if (finalScriptSig) this.addFinalScriptSigToInput(inputIndex, finalScriptSig); @@ -101,26 +85,46 @@ export class Psbt extends PsbtBase { } signInput(inputIndex: number, keyPair: Signer): Psbt { - const input = checkForInput(this.inputs, inputIndex); if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); - const { hash, sighashType, script } = getHashForSig( + const { hash, sighashType } = getHashAndSighashType( + this.inputs, inputIndex, - input, + keyPair.publicKey, this.globalMap.unsignedTx!, ); - const pubkey = keyPair.publicKey; - - checkScriptForPubkey(pubkey, script); - const partialSig = { - pubkey, + pubkey: keyPair.publicKey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }; return this.addPartialSigToInput(inputIndex, partialSig); } + + async signInputAsync( + inputIndex: number, + keyPair: SignerAsync, + ): Promise { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + const { hash, sighashType } = getHashAndSighashType( + this.inputs, + inputIndex, + keyPair.publicKey, + this.globalMap.unsignedTx!, + ); + + const partialSig = { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode( + await keyPair.sign(hash), + sighashType, + ), + }; + + this.addPartialSigToInput(inputIndex, partialSig); + } } // @@ -137,6 +141,61 @@ function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } +function getHashAndSighashType( + inputs: PsbtInput[], + inputIndex: number, + pubkey: Buffer, + txBuf: Buffer, +): { + hash: Buffer; + sighashType: number; +} { + const input = checkForInput(inputs, inputIndex); + const { hash, sighashType, script } = getHashForSig(inputIndex, input, txBuf); + checkScriptForPubkey(pubkey, script); + return { + hash, + sighashType, + }; +} + +function getFinalScripts( + script: Buffer, + scriptType: string, + partialSig: PartialSig[], + isSegwit: boolean, + isP2SH: boolean, + isP2WSH: boolean, +): { + finalScriptSig: Buffer | undefined; + finalScriptWitness: Buffer | undefined; +} { + let finalScriptSig: Buffer | undefined; + let finalScriptWitness: Buffer | undefined; + + // Wow, the payments API is very handy + const payment: payments.Payment = getPayment(script, scriptType, partialSig); + const p2wsh = !isP2WSH ? null : payments.p2wsh({ redeem: payment }); + const p2sh = !isP2SH ? null : payments.p2sh({ redeem: p2wsh || payment }); + + if (isSegwit) { + if (p2wsh) { + finalScriptWitness = witnessStackToScriptWitness(p2wsh.witness!); + } else { + finalScriptWitness = witnessStackToScriptWitness(payment.witness!); + } + if (p2sh) { + finalScriptSig = bscript.compile([p2sh.redeem!.output!]); + } + } else { + finalScriptSig = payment.input; + } + return { + finalScriptSig, + finalScriptWitness, + }; +} + function getPayment( script: Buffer, scriptType: string, diff --git a/tsconfig.json b/tsconfig.json index f770a45..1de632d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2017", "module": "commonjs", "outDir": "./src", "declaration": true, diff --git a/types/psbt.d.ts b/types/psbt.d.ts index f5b6430..26ae0a7 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,5 +1,5 @@ import { Psbt as PsbtBase } from 'bip174'; -import { Signer } from './ecpair'; +import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { @@ -12,4 +12,5 @@ export declare class Psbt extends PsbtBase { }; finalizeInput(inputIndex: number): boolean; signInput(inputIndex: number, keyPair: Signer): Psbt; + signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise; } From 1c8fc6978015ae86f6950eccfdffb68f5d73d5a1 Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 3 Jul 2019 15:48:56 +0900 Subject: [PATCH 031/111] Stick with ES2015 for now --- src/psbt.js | 17 ++++++++--------- ts_src/psbt.ts | 20 ++++++++------------ tsconfig.json | 2 +- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index ae60861..1ed7354 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -87,7 +87,7 @@ class Psbt extends bip174_1.Psbt { }; return this.addPartialSigToInput(inputIndex, partialSig); } - async signInputAsync(inputIndex, keyPair) { + signInputAsync(inputIndex, keyPair) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( @@ -96,14 +96,13 @@ class Psbt extends bip174_1.Psbt { keyPair.publicKey, this.globalMap.unsignedTx, ); - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode( - await keyPair.sign(hash), - sighashType, - ), - }; - this.addPartialSigToInput(inputIndex, partialSig); + return keyPair.sign(hash).then(signature => { + const partialSig = { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }; + this.addPartialSigToInput(inputIndex, partialSig); + }); } } exports.Psbt = Psbt; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index d9a059c..680ea92 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -102,10 +102,7 @@ export class Psbt extends PsbtBase { return this.addPartialSigToInput(inputIndex, partialSig); } - async signInputAsync( - inputIndex: number, - keyPair: SignerAsync, - ): Promise { + signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( @@ -115,15 +112,14 @@ export class Psbt extends PsbtBase { this.globalMap.unsignedTx!, ); - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode( - await keyPair.sign(hash), - sighashType, - ), - }; + return keyPair.sign(hash).then(signature => { + const partialSig = { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }; - this.addPartialSigToInput(inputIndex, partialSig); + this.addPartialSigToInput(inputIndex, partialSig); + }); } } diff --git a/tsconfig.json b/tsconfig.json index 1de632d..f770a45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2015", "module": "commonjs", "outDir": "./src", "declaration": true, From 48fc75c4f016f199c734e7433a9622027e3f81ad Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 3 Jul 2019 18:42:31 +0900 Subject: [PATCH 032/111] Fix p2sh and p2wsh not working --- src/psbt.js | 60 ++++++++++++++++++++++++++++++++++++++------ ts_src/psbt.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 1ed7354..3cbd899 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -30,8 +30,9 @@ class Psbt extends bip174_1.Psbt { this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (input.finalScriptWitness) { - const decompiled = bscript.decompile(input.finalScriptWitness); - if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); + tx.ins[idx].witness = scriptWitnessToWitnessStack( + input.finalScriptWitness, + ); } }); return tx; @@ -148,23 +149,44 @@ function getFinalScripts( finalScriptWitness = witnessStackToScriptWitness(payment.witness); } if (p2sh) { - finalScriptSig = bscript.compile([p2sh.redeem.output]); + finalScriptSig = p2sh.input; } } else { - finalScriptSig = payment.input; + if (p2sh) { + finalScriptSig = p2sh.input; + } else { + finalScriptSig = payment.input; + } } return { finalScriptSig, finalScriptWitness, }; } +function getSortedSigs(script, partialSig) { + const p2ms = payments.p2ms({ output: script }); + // for each pubkey in order of p2ms script + return p2ms.pubkeys + .map(pk => { + // filter partialSig array by pubkey being equal + return ( + partialSig.filter(ps => { + return ps.pubkey.equals(pk); + })[0] || {} + ).signature; + // Any pubkey without a match will return undefined + // this last filter removes all the undefined items in the array. + }) + .filter(v => !!v); +} function getPayment(script, scriptType, partialSig) { let payment; switch (scriptType) { case 'multisig': + const sigs = getSortedSigs(script, partialSig); payment = payments.p2ms({ output: script, - signatures: partialSig.map(ps => ps.signature), + signatures: sigs, }); break; case 'pubkey': @@ -283,7 +305,7 @@ const getHashForSig = (inputIndex, input, txBuf) => { checkWitnessScript(inputIndex, _script, input.witnessScript); hash = unsignedTx.hashForWitnessV0( inputIndex, - _script, + input.witnessScript, input.witnessUtxo.value, sighashType, ); @@ -363,11 +385,11 @@ function getScriptFromInput(inputIndex, input, _unsignedTx) { if (input.witnessScript) { res.script = input.witnessScript; } else if (input.redeemScript) { - res.script = payments.p2pkh({ + res.script = payments.p2wpkh({ hash: input.redeemScript.slice(2), }).output; } else { - res.script = payments.p2pkh({ + res.script = payments.p2wpkh({ hash: input.witnessUtxo.script.slice(2), }).output; } @@ -401,4 +423,26 @@ function witnessStackToScriptWitness(witness) { writeVector(witness); return buffer; } +function scriptWitnessToWitnessStack(buffer) { + let offset = 0; + function readSlice(n) { + offset += n; + return buffer.slice(offset - n, offset); + } + function readVarInt() { + const vi = varuint.decode(buffer, offset); + offset += varuint.decode.bytes; + return vi; + } + function readVarSlice() { + return readSlice(readVarInt()); + } + function readVector() { + const count = readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + return readVector(); +} const range = n => [...Array(n).keys()]; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 680ea92..7c60a1d 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -32,8 +32,9 @@ export class Psbt extends PsbtBase { this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (input.finalScriptWitness) { - const decompiled = bscript.decompile(input.finalScriptWitness); - if (decompiled) tx.ins[idx].witness = bscript.toStack(decompiled); + tx.ins[idx].witness = scriptWitnessToWitnessStack( + input.finalScriptWitness, + ); } }); return tx; @@ -181,10 +182,14 @@ function getFinalScripts( finalScriptWitness = witnessStackToScriptWitness(payment.witness!); } if (p2sh) { - finalScriptSig = bscript.compile([p2sh.redeem!.output!]); + finalScriptSig = p2sh.input; } } else { - finalScriptSig = payment.input; + if (p2sh) { + finalScriptSig = p2sh.input; + } else { + finalScriptSig = payment.input; + } } return { finalScriptSig, @@ -192,6 +197,23 @@ function getFinalScripts( }; } +function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] { + const p2ms = payments.p2ms({ output: script }); + // for each pubkey in order of p2ms script + return p2ms + .pubkeys!.map(pk => { + // filter partialSig array by pubkey being equal + return ( + partialSig.filter(ps => { + return ps.pubkey.equals(pk); + })[0] || {} + ).signature; + // Any pubkey without a match will return undefined + // this last filter removes all the undefined items in the array. + }) + .filter(v => !!v); +} + function getPayment( script: Buffer, scriptType: string, @@ -200,9 +222,10 @@ function getPayment( let payment: payments.Payment; switch (scriptType) { case 'multisig': + const sigs = getSortedSigs(script, partialSig); payment = payments.p2ms({ output: script, - signatures: partialSig.map(ps => ps.signature), + signatures: sigs, }); break; case 'pubkey': @@ -343,7 +366,7 @@ const getHashForSig = ( checkWitnessScript(inputIndex, _script, input.witnessScript); hash = unsignedTx.hashForWitnessV0( inputIndex, - _script, + input.witnessScript, input.witnessUtxo.value, sighashType, ); @@ -446,11 +469,11 @@ function getScriptFromInput( if (input.witnessScript) { res.script = input.witnessScript; } else if (input.redeemScript) { - res.script = payments.p2pkh({ + res.script = payments.p2wpkh({ hash: input.redeemScript.slice(2), }).output!; } else { - res.script = payments.p2pkh({ + res.script = payments.p2wpkh({ hash: input.witnessUtxo.script.slice(2), }).output!; } @@ -494,4 +517,32 @@ function witnessStackToScriptWitness(witness: Buffer[]): Buffer { return buffer; } +function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { + let offset = 0; + + function readSlice(n: number): Buffer { + offset += n; + return buffer.slice(offset - n, offset); + } + + function readVarInt(): number { + const vi = varuint.decode(buffer, offset); + offset += varuint.decode.bytes; + return vi; + } + + function readVarSlice(): Buffer { + return readSlice(readVarInt()); + } + + function readVector(): Buffer[] { + const count = readVarInt(); + const vector: Buffer[] = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + + return readVector(); +} + const range = (n: number): number[] => [...Array(n).keys()]; From 1fc2e146ea461feb26fb3872f02b26dd91ff7e18 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 3 Jul 2019 16:40:37 +0700 Subject: [PATCH 033/111] Test BIP174 invalid test cases --- test/fixtures/psbt.json | 76 +++++++++++++++++++++++++++++++++++++++++ test/psbt.js | 10 ++++++ 2 files changed, 86 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 371a8b7..b2104e8 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -1,4 +1,80 @@ { + "bip174": { + "invalid": [ + { + "errorMessage": "Format Error: Invalid Magic Number", + "psbt": "AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA==" + }, + { + "errorMessage": "Format Error: Unexpected End of PSBT", + "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==" + }, + { + "errorMessage": "Format Error: Transaction ScriptSigs are not empty", + "psbt": "cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=" + }, + { + "errorMessage": "Format Error: Only one UNSIGNED_TX allowed", + "psbt": "cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==" + }, + { + "errorMessage": "Format Error: Keys must be unique for each input: input index 0 key 00", + "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA" + }, + { + "errorMessage": "Format Error: Invalid global key: 0001", + "psbt": "cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" + }, + { + "errorMessage": "Format Error: Invalid input key: 0100", + "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" + }, + { + "errorMessage": "Format Error: invalid pubkey in key 0x0203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd", + "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==" + }, + { + "errorMessage": "Format Error: Invalid input key: 0400", + "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" + }, + { + "errorMessage": "Format Error: Invalid input key: 0500", + "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" + }, + { + "errorMessage": "Format Error: invalid pubkey in key 0x0603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd", + "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==" + }, + { + "errorMessage": "Format Error: Invalid input key: 0000", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + }, + { + "errorMessage": "Format Error: Invalid input key: 0700", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + }, + { + "errorMessage": "Format Error: Invalid input key: 0800", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + }, + { + "errorMessage": "Format Error: invalid pubkey in key 0x0203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca587", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + }, + { + "errorMessage": "Format Error: Invalid input key: 0300", + "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" + }, + { + "errorMessage": "Format Error: Invalid output key: 0000", + "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" + }, + { + "errorMessage": "Format Error: Unexpected End of PSBT", + "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" + } + ] + }, "signInput": { "checks": [ { diff --git a/test/psbt.js b/test/psbt.js index 8584bd6..b26e5cb 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -7,6 +7,16 @@ const Psbt = require('..').Psbt const fixtures = require('./fixtures/psbt') describe(`Psbt`, () => { + describe('BIP174 Test Vectors', () => { + fixtures.bip174.invalid.forEach(f => { + it(`Invalid: "${f.errorMessage}"`, () => { + assert.throws(() => { + Psbt.fromBase64(f.psbt) + }, {message: f.errorMessage}) + }) + }) + }) + describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { From c24a6e1ad31ebcf06f286912e2a7f7939d71b65f Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 3 Jul 2019 17:01:47 +0700 Subject: [PATCH 034/111] Include test case number in test output --- test/psbt.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/psbt.js b/test/psbt.js index b26e5cb..c75bce8 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -8,8 +8,8 @@ const fixtures = require('./fixtures/psbt') describe(`Psbt`, () => { describe('BIP174 Test Vectors', () => { - fixtures.bip174.invalid.forEach(f => { - it(`Invalid: "${f.errorMessage}"`, () => { + fixtures.bip174.invalid.forEach((f, i) => { + it(`Invalid #${i + 1}: "${f.errorMessage}"`, () => { assert.throws(() => { Psbt.fromBase64(f.psbt) }, {message: f.errorMessage}) From 2662e46987313e88b17d1f1e1d2590511204294c Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 3 Jul 2019 17:02:02 +0700 Subject: [PATCH 035/111] Test BIP174 valid test cases --- test/fixtures/psbt.json | 8 ++++++++ test/psbt.js | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index b2104e8..451fe91 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -73,6 +73,14 @@ "errorMessage": "Format Error: Unexpected End of PSBT", "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" } + ], + "valid": [ + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA", + "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==", + "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", + "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=", + "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index c75bce8..6ae59cd 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -15,6 +15,14 @@ describe(`Psbt`, () => { }, {message: f.errorMessage}) }) }) + + fixtures.bip174.valid.forEach((psbt, i) => { + it(`Valid #${i + 1}`, () => { + assert.doesNotThrow(() => { + Psbt.fromBase64(psbt) + }) + }) + }) }) describe('signInput', () => { From 336c76bfda711b9171110abcbc6d933e94d114ab Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 3 Jul 2019 17:20:27 +0700 Subject: [PATCH 036/111] Add descriptions to invalid test cases from BIP174 spec --- test/fixtures/psbt.json | 18 ++++++++++++++++++ test/psbt.js | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 451fe91..a758eb2 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -2,74 +2,92 @@ "bip174": { "invalid": [ { + "description": "Network transaction, not PSBT format", "errorMessage": "Format Error: Invalid Magic Number", "psbt": "AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA==" }, { + "description": "PSBT missing outputs", "errorMessage": "Format Error: Unexpected End of PSBT", "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==" }, { + "description": "PSBT where one input has a filled scriptSig in the unsigned tx", "errorMessage": "Format Error: Transaction ScriptSigs are not empty", "psbt": "cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=" }, { + "description": "PSBT where inputs and outputs are provided but without an unsigned tx", "errorMessage": "Format Error: Only one UNSIGNED_TX allowed", "psbt": "cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==" }, { + "description": "PSBT with duplicate keys in an input", "errorMessage": "Format Error: Keys must be unique for each input: input index 0 key 00", "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA" }, { + "description": "PSBT With invalid global transaction typed key", "errorMessage": "Format Error: Invalid global key: 0001", "psbt": "cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" }, { + "description": "PSBT With invalid input witness utxo typed key", "errorMessage": "Format Error: Invalid input key: 0100", "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" }, { + "description": "PSBT With invalid pubkey length for input partial signature typed key", "errorMessage": "Format Error: invalid pubkey in key 0x0203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd", "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==" }, { + "description": "PSBT With invalid redeemscript typed key", "errorMessage": "Format Error: Invalid input key: 0400", "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" }, { + "description": "PSBT With invalid witnessscript typed key", "errorMessage": "Format Error: Invalid input key: 0500", "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA" }, { + "description": "PSBT With invalid bip32 typed key", "errorMessage": "Format Error: invalid pubkey in key 0x0603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd", "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==" }, { + "description": "PSBT With invalid non-witness utxo typed key", "errorMessage": "Format Error: Invalid input key: 0000", "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" }, { + "description": "PSBT With invalid final scriptsig typed key", "errorMessage": "Format Error: Invalid input key: 0700", "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" }, { + "description": "PSBT With invalid final script witness typed key", "errorMessage": "Format Error: Invalid input key: 0800", "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" }, { + "description": "PSBT With invalid pubkey in output BIP 32 derivation paths typed key", "errorMessage": "Format Error: invalid pubkey in key 0x0203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca587", "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" }, { + "description": "PSBT With invalid input sighash type typed key", "errorMessage": "Format Error: Invalid input key: 0300", "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" }, { + "description": "PSBT With invalid output redeemScript typed key", "errorMessage": "Format Error: Invalid output key: 0000", "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" }, { + "description": "PSBT With invalid output witnessScript typed key", "errorMessage": "Format Error: Unexpected End of PSBT", "psbt": "cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A" } diff --git a/test/psbt.js b/test/psbt.js index 6ae59cd..f0d8846 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -8,8 +8,8 @@ const fixtures = require('./fixtures/psbt') describe(`Psbt`, () => { describe('BIP174 Test Vectors', () => { - fixtures.bip174.invalid.forEach((f, i) => { - it(`Invalid #${i + 1}: "${f.errorMessage}"`, () => { + fixtures.bip174.invalid.forEach(f => { + it(`Invalid: ${f.description}`, () => { assert.throws(() => { Psbt.fromBase64(f.psbt) }, {message: f.errorMessage}) From 54e2e55ef73247b6b489430dd15e015cd7312620 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 3 Jul 2019 17:24:43 +0700 Subject: [PATCH 037/111] Add descriptions to valid test cases from BIP174 spec --- test/fixtures/psbt.json | 30 ++++++++++++++++++++++++------ test/psbt.js | 6 +++--- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index a758eb2..3eaed01 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -93,12 +93,30 @@ } ], "valid": [ - "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA", - "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA", - "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==", - "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", - "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=", - "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" + { + "description": "PSBT with one P2PKH input. Outputs are empty", + "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA" + }, + { + "description": "PSBT with one P2PKH input and one P2SH-P2WPKH input. First input is signed and finalized. Outputs are empty", + "psbt": "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA" + }, + { + "description": "PSBT with one P2PKH input which has a non-final scriptSig and has a sighash type specified. Outputs are empty", + "psbt": "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==" + }, + { + "description": "PSBT with one P2PKH input and one P2SH-P2WPKH input both with non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available. Outputs filled.", + "psbt": "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=" + }, + { + "description": "PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript, witnessScript, and keypaths are available. Contains one signature.", + "psbt": "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=" + }, + { + "description": "PSBT with unknown types in the inputs.", + "psbt": "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index f0d8846..ea9131f 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -16,10 +16,10 @@ describe(`Psbt`, () => { }) }) - fixtures.bip174.valid.forEach((psbt, i) => { - it(`Valid #${i + 1}`, () => { + fixtures.bip174.valid.forEach(f => { + it(`Valid: ${f.description}`, () => { assert.doesNotThrow(() => { - Psbt.fromBase64(psbt) + Psbt.fromBase64(f.psbt) }) }) }) From a876698d15be214a5a6424446e553151473887a2 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 3 Jul 2019 17:38:09 +0700 Subject: [PATCH 038/111] Test BIP174 signer check test cases --- test/fixtures/psbt.json | 26 ++++++++++++++++++++++++++ test/psbt.js | 10 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 3eaed01..d57894a 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -117,6 +117,32 @@ "description": "PSBT with unknown types in the inputs.", "psbt": "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=" } + ], + "failSignChecks": [ + { + "description": "A Witness UTXO is provided for a non-witness input", + "errorMessage": "Segwit input needs witnessScript if not P2WPKH", + "psbt": "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEBItPf9QUAAAAAGXapFNSO0xELlAFMsRS9Mtb00GbcdCVriKwAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", + "inputToCheck": 0 + }, + { + "description": "redeemScript with non-witness UTXO does not match the scriptPubKey", + "errorMessage": "Redeem script for input #0 doesn't match the scriptPubKey in the prevout", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq8iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=", + "inputToCheck": 0 + }, + { + "description": "redeemScript with witness UTXO does not match the scriptPubKey", + "errorMessage": "Redeem script for input #1 doesn't match the scriptPubKey in the prevout", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQABBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=", + "inputToCheck": 1 + }, + { + "description": "witnessScript with witness UTXO does not match the redeemScript", + "errorMessage": "Witness script for input #1 doesn't match the scriptPubKey in the prevout", + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSrSIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=", + "inputToCheck": 1 + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index ea9131f..e99e592 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -23,6 +23,16 @@ describe(`Psbt`, () => { }) }) }) + + fixtures.bip174.failSignChecks.forEach(f => { + const keyPair = ECPair.makeRandom() + it(`Fails Signer checks: ${f.description}`, () => { + const psbt = Psbt.fromBase64(f.psbt) + assert.throws(() => { + psbt.signInput(f.inputToCheck, keyPair) + }, {message: f.errorMessage}) + }) + }) }) describe('signInput', () => { From df9008bae70981109201d4a565bb6a47de406c53 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 10:57:23 +0900 Subject: [PATCH 039/111] Update bip174 --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4195bf2..0eb81d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.10.tgz", - "integrity": "sha512-gFtSEMayg7HPKGnIQcEx5CqD/qHWuMlxLJ/+VV4k4Q2mcA0rY040JbNpFuCGVI6rJYv211f0NA7nkU4xkPX4nQ==" + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.11.tgz", + "integrity": "sha512-/WuRQOwgT0cGKDrNROOhOHpdtXyeuBJMqEgsBUdlCeFRLXIA8g5pHzbFmVm/v+OcYdenHfwzW/hOEf1xJOI27Q==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 1346928..54d94f4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.10", + "bip174": "0.0.11", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", From 2b8e8001bc6d8ebbafa83067119e4b229f5d8ef9 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 11:26:23 +0900 Subject: [PATCH 040/111] Support Addresses for outputs --- src/psbt.js | 28 ++++++++++++++++------------ ts_src/psbt.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++--- types/psbt.d.ts | 12 ++++++++++-- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 3cbd899..d47bdb2 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -2,16 +2,17 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const utils_1 = require('bip174/src/lib/utils'); +const address_1 = require('./address'); const crypto_1 = require('./crypto'); +const networks_1 = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { - // protected __TX: Transaction; - constructor(network) { + constructor(opts = {}) { super(); - this.network = network; + this.opts = Object.assign({}, DEFAULT_OPTS, opts); // // TODO: figure out a way to use a Transaction Object instead of a Buffer // // TODO: Caching, since .toBuffer() calls every time we get is lame. // this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); @@ -24,6 +25,15 @@ class Psbt extends bip174_1.Psbt { // } // }); } + addOutput(outputData, allowNoInput = false, transactionOutputAdder) { + const { address } = outputData; + if (typeof address === 'string') { + const { network } = this.opts; + const script = address_1.toOutputScript(address, network); + outputData = Object.assign(outputData, { script }); + } + return super.addOutput(outputData, allowNoInput, transactionOutputAdder); + } extractTransaction() { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); const tx = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); @@ -107,15 +117,9 @@ class Psbt extends bip174_1.Psbt { } } exports.Psbt = Psbt; -// -// -// -// -// Helper functions -// -// -// -// +const DEFAULT_OPTS = { + network: networks_1.bitcoin, +}; function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 7c60a1d..eff576b 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,9 +1,14 @@ import { Psbt as PsbtBase } from 'bip174'; -import { PartialSig, PsbtInput } from 'bip174/src/lib/interfaces'; +import { + PartialSig, + PsbtInput, + TransactionOutput, +} from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; +import { toOutputScript } from './address'; import { hash160 } from './crypto'; import { Signer, SignerAsync } from './ecpair'; -import { Network } from './networks'; +import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Transaction } from './transaction'; @@ -11,8 +16,10 @@ const varuint = require('varuint-bitcoin'); export class Psbt extends PsbtBase { // protected __TX: Transaction; - constructor(public network?: Network) { + private opts: PsbtOpts; + constructor(opts: PsbtOptsOptional = {}) { super(); + this.opts = Object.assign({}, DEFAULT_OPTS, opts); // // TODO: figure out a way to use a Transaction Object instead of a Buffer // // TODO: Caching, since .toBuffer() calls every time we get is lame. // this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); @@ -26,6 +33,29 @@ export class Psbt extends PsbtBase { // }); } + addOutput(outputData: TransactionOutput, allowNoInput?: boolean): this; + addOutput( + outputData: T, + allowNoInput?: boolean, + transactionOutputAdder?: (output: T, txBuffer: Buffer) => Buffer, + ): this; + addOutput( + outputData: T | TransactionOutput, + allowNoInput: boolean = false, + transactionOutputAdder?: ( + output: T | TransactionOutput, + txBuffer: Buffer, + ) => Buffer, + ): this { + const { address } = outputData as any; + if (typeof address === 'string') { + const { network } = this.opts; + const script = toOutputScript(address, network); + outputData = Object.assign(outputData, { script }); + } + return super.addOutput(outputData, allowNoInput, transactionOutputAdder); + } + extractTransaction(): Transaction { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); const tx = Transaction.fromBuffer(this.globalMap.unsignedTx!); @@ -134,6 +164,18 @@ export class Psbt extends PsbtBase { // // +interface PsbtOptsOptional { + network?: Network; +} + +interface PsbtOpts { + network: Network; +} + +const DEFAULT_OPTS = { + network: btcNetwork, +}; + function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 26ae0a7..6275ee3 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,10 +1,14 @@ +/// import { Psbt as PsbtBase } from 'bip174'; +import { TransactionOutput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { - network?: Network | undefined; - constructor(network?: Network | undefined); + private opts; + constructor(opts?: PsbtOptsOptional); + addOutput(outputData: TransactionOutput, allowNoInput?: boolean): this; + addOutput(outputData: T, allowNoInput?: boolean, transactionOutputAdder?: (output: T, txBuffer: Buffer) => Buffer): this; extractTransaction(): Transaction; finalizeAllInputs(): { result: boolean; @@ -14,3 +18,7 @@ export declare class Psbt extends PsbtBase { signInput(inputIndex: number, keyPair: Signer): Psbt; signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise; } +interface PsbtOptsOptional { + network?: Network; +} +export {}; From b28c96d228ccfa9aadca3b53fd96b4a6e8bac4bd Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 12:03:48 +0900 Subject: [PATCH 041/111] Set to version 2 by default --- package-lock.json | 6 +++--- package.json | 2 +- src/psbt.js | 1 + ts_src/psbt.ts | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0eb81d0..a6aa1a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.11.tgz", - "integrity": "sha512-/WuRQOwgT0cGKDrNROOhOHpdtXyeuBJMqEgsBUdlCeFRLXIA8g5pHzbFmVm/v+OcYdenHfwzW/hOEf1xJOI27Q==" + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.12.tgz", + "integrity": "sha512-rYVuFTSCROv8iI07BhEddap+iXiFz2aiYEUSQR8rqv7JKWbMO2QQqCJMvr2E0df8wu5ySysd/CupIP4eG4ukqQ==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 54d94f4..93d2188 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.11", + "bip174": "0.0.12", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", diff --git a/src/psbt.js b/src/psbt.js index d47bdb2..0157e9f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -12,6 +12,7 @@ const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { constructor(opts = {}) { super(); + this.setVersion(2); this.opts = Object.assign({}, DEFAULT_OPTS, opts); // // TODO: figure out a way to use a Transaction Object instead of a Buffer // // TODO: Caching, since .toBuffer() calls every time we get is lame. diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index eff576b..0752df2 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -19,6 +19,7 @@ export class Psbt extends PsbtBase { private opts: PsbtOpts; constructor(opts: PsbtOptsOptional = {}) { super(); + this.setVersion(2); this.opts = Object.assign({}, DEFAULT_OPTS, opts); // // TODO: figure out a way to use a Transaction Object instead of a Buffer // // TODO: Caching, since .toBuffer() calls every time we get is lame. From f7e726a8ebecb3e0cafe573f2b95258c12d3e4b7 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 13:33:08 +0900 Subject: [PATCH 042/111] Add TX cache and addInput addOutput --- package-lock.json | 6 +-- package.json | 2 +- src/psbt.js | 91 +++++++++++++++++++++++++++++----- ts_src/psbt.ts | 121 +++++++++++++++++++++++++++++++++++----------- types/psbt.d.ts | 9 ++-- 5 files changed, 181 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6aa1a4..768c79f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.12.tgz", - "integrity": "sha512-rYVuFTSCROv8iI07BhEddap+iXiFz2aiYEUSQR8rqv7JKWbMO2QQqCJMvr2E0df8wu5ySysd/CupIP4eG4ukqQ==" + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.13.tgz", + "integrity": "sha512-jWP7Lb27Nmbk6gaZKhJZOyk5LqRWs9z+R2xzgu3W8/iZXIIP2kcR6fh5lNg7GGOiWUaqanWC9rjrDVrBVbXKww==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 93d2188..1fadfcb 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.12", + "bip174": "0.0.13", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", diff --git a/src/psbt.js b/src/psbt.js index 0157e9f..df1d491 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); const utils_1 = require('bip174/src/lib/utils'); const address_1 = require('./address'); +const bufferutils_1 = require('./bufferutils'); const crypto_1 = require('./crypto'); const networks_1 = require('./networks'); const payments = require('./payments'); @@ -12,28 +13,92 @@ const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { constructor(opts = {}) { super(); + // set defaults this.setVersion(2); this.opts = Object.assign({}, DEFAULT_OPTS, opts); - // // TODO: figure out a way to use a Transaction Object instead of a Buffer - // // TODO: Caching, since .toBuffer() calls every time we get is lame. - // this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); - // delete this.globalMap.unsignedTx; - // Object.defineProperty(this.globalMap, 'unsignedTx', { - // enumerable: true, - // writable: false, - // get(): Buffer { - // return this.__TX.toBuffer(); - // } - // }); + this.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + // set cache + const self = this; + delete this.globalMap.unsignedTx; + Object.defineProperty(this.globalMap, 'unsignedTx', { + enumerable: true, + get() { + if (self.__TX_BUF_CACHE !== undefined) { + return self.__TX_BUF_CACHE; + } else { + self.__TX_BUF_CACHE = self.__TX.toBuffer(); + return self.__TX_BUF_CACHE; + } + }, + set(data) { + self.__TX_BUF_CACHE = data; + }, + }); + // Make data hidden when enumerating + const dpew = (obj, attr, enumerable, writable) => + Object.defineProperty(obj, attr, { + enumerable, + writable, + }); + dpew(this, '__TX', false, false); + dpew(this, '__TX_BUF_CACHE', false, true); + dpew(this, 'opts', false, true); } - addOutput(outputData, allowNoInput = false, transactionOutputAdder) { + addInput(inputData) { + const self = this; + const inputAdder = (_inputData, txBuf) => { + if ( + !txBuf || + _inputData.hash === undefined || + _inputData.index === undefined || + (!Buffer.isBuffer(_inputData.hash) && + typeof _inputData.hash !== 'string') || + typeof _inputData.index !== 'number' + ) { + throw new Error('Error adding input.'); + } + const prevHash = Buffer.isBuffer(_inputData.hash) + ? _inputData.hash + : bufferutils_1.reverseBuffer(Buffer.from(_inputData.hash, 'hex')); + self.__TX.ins.push({ + hash: prevHash, + index: _inputData.index, + script: Buffer.alloc(0), + sequence: + _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, + witness: [], + }); + console.log(self.__TX); + return self.__TX.toBuffer(); + }; + return super.addInput(inputData, inputAdder); + } + addOutput(outputData) { const { address } = outputData; if (typeof address === 'string') { const { network } = this.opts; const script = address_1.toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } - return super.addOutput(outputData, allowNoInput, transactionOutputAdder); + const self = this; + const outputAdder = (_outputData, txBuf) => { + if ( + !txBuf || + _outputData.script === undefined || + _outputData.value === undefined || + !Buffer.isBuffer(_outputData.script) || + typeof _outputData.value !== 'number' + ) { + throw new Error('Error adding output.'); + } + self.__TX.outs.push({ + script: _outputData.script, + value: _outputData.value, + }); + console.log(self.__TX); + return self.__TX.toBuffer(); + }; + return super.addOutput(outputData, true, outputAdder); } extractTransaction() { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0752df2..58988f2 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -2,10 +2,12 @@ import { Psbt as PsbtBase } from 'bip174'; import { PartialSig, PsbtInput, + TransactionInput, TransactionOutput, } from 'bip174/src/lib/interfaces'; 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 { bitcoin as btcNetwork, Network } from './networks'; @@ -15,46 +17,111 @@ import { Transaction } from './transaction'; const varuint = require('varuint-bitcoin'); export class Psbt extends PsbtBase { - // protected __TX: Transaction; + private __TX: Transaction; + private __TX_BUF_CACHE?: Buffer; private opts: PsbtOpts; constructor(opts: PsbtOptsOptional = {}) { super(); + // set defaults this.setVersion(2); this.opts = Object.assign({}, DEFAULT_OPTS, opts); - // // TODO: figure out a way to use a Transaction Object instead of a Buffer - // // TODO: Caching, since .toBuffer() calls every time we get is lame. - // this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); - // delete this.globalMap.unsignedTx; - // Object.defineProperty(this.globalMap, 'unsignedTx', { - // enumerable: true, - // writable: false, - // get(): Buffer { - // return this.__TX.toBuffer(); - // } - // }); + this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); + + // set cache + const self = this; + delete this.globalMap.unsignedTx; + Object.defineProperty(this.globalMap, 'unsignedTx', { + enumerable: true, + get(): Buffer { + if (self.__TX_BUF_CACHE !== undefined) { + return self.__TX_BUF_CACHE; + } else { + self.__TX_BUF_CACHE = self.__TX.toBuffer(); + return self.__TX_BUF_CACHE; + } + }, + set(data: Buffer): void { + self.__TX_BUF_CACHE = data; + }, + }); + + // Make data hidden when enumerating + const dpew = ( + obj: any, + attr: string, + enumerable: boolean, + writable: boolean, + ): any => + Object.defineProperty(obj, attr, { + enumerable, + writable, + }); + dpew(this, '__TX', false, false); + dpew(this, '__TX_BUF_CACHE', false, true); + dpew(this, 'opts', false, true); } - addOutput(outputData: TransactionOutput, allowNoInput?: boolean): this; - addOutput( - outputData: T, - allowNoInput?: boolean, - transactionOutputAdder?: (output: T, txBuffer: Buffer) => Buffer, - ): this; - addOutput( - outputData: T | TransactionOutput, - allowNoInput: boolean = false, - transactionOutputAdder?: ( - output: T | TransactionOutput, - txBuffer: Buffer, - ) => Buffer, - ): this { + addInput(inputData: TransactionInput): this { + const self = this; + const inputAdder = ( + _inputData: TransactionInput, + txBuf: Buffer, + ): Buffer => { + if ( + !txBuf || + (_inputData as any).hash === undefined || + (_inputData as any).index === undefined || + (!Buffer.isBuffer((_inputData as any).hash) && + typeof (_inputData as any).hash !== 'string') || + typeof (_inputData as any).index !== 'number' + ) { + throw new Error('Error adding input.'); + } + const prevHash = Buffer.isBuffer(_inputData.hash) + ? _inputData.hash + : reverseBuffer(Buffer.from(_inputData.hash, 'hex')); + self.__TX.ins.push({ + hash: prevHash, + index: _inputData.index, + script: Buffer.alloc(0), + sequence: _inputData.sequence || Transaction.DEFAULT_SEQUENCE, + witness: [], + }); + console.log(self.__TX); + return self.__TX.toBuffer(); + }; + return super.addInput(inputData, inputAdder); + } + + addOutput(outputData: TransactionOutput): this { const { address } = outputData as any; if (typeof address === 'string') { const { network } = this.opts; const script = toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } - return super.addOutput(outputData, allowNoInput, transactionOutputAdder); + const self = this; + const outputAdder = ( + _outputData: TransactionOutput, + txBuf: Buffer, + ): Buffer => { + if ( + !txBuf || + (_outputData as any).script === undefined || + (_outputData as any).value === undefined || + !Buffer.isBuffer((_outputData as any).script) || + typeof (_outputData as any).value !== 'number' + ) { + throw new Error('Error adding output.'); + } + self.__TX.outs.push({ + script: (_outputData as any).script!, + value: _outputData.value, + }); + console.log(self.__TX); + return self.__TX.toBuffer(); + }; + return super.addOutput(outputData, true, outputAdder); } extractTransaction(): Transaction { diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 6275ee3..a530cd0 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,14 +1,15 @@ -/// import { Psbt as PsbtBase } from 'bip174'; -import { TransactionOutput } from 'bip174/src/lib/interfaces'; +import { TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { + private __TX; + private __TX_BUF_CACHE?; private opts; constructor(opts?: PsbtOptsOptional); - addOutput(outputData: TransactionOutput, allowNoInput?: boolean): this; - addOutput(outputData: T, allowNoInput?: boolean, transactionOutputAdder?: (output: T, txBuffer: Buffer) => Buffer): this; + addInput(inputData: TransactionInput): this; + addOutput(outputData: TransactionOutput): this; extractTransaction(): Transaction; finalizeAllInputs(): { result: boolean; From 539c88596a4ae9f4ee4fac0a6adb3db6bb60708a Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 13:42:34 +0900 Subject: [PATCH 043/111] Add version and locktime setters --- src/psbt.js | 12 +++++++++++- ts_src/psbt.ts | 14 +++++++++++++- types/psbt.d.ts | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index df1d491..ec91bd9 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -14,9 +14,9 @@ class Psbt extends bip174_1.Psbt { constructor(opts = {}) { super(); // set defaults - this.setVersion(2); this.opts = Object.assign({}, DEFAULT_OPTS, opts); this.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + this.setVersion(2); // set cache const self = this; delete this.globalMap.unsignedTx; @@ -44,6 +44,16 @@ class Psbt extends bip174_1.Psbt { dpew(this, '__TX_BUF_CACHE', false, true); dpew(this, 'opts', false, true); } + setVersion(version) { + this.__TX.version = version; + this.__TX_BUF_CACHE = undefined; + return this; + } + setLocktime(locktime) { + this.__TX.locktime = locktime; + this.__TX_BUF_CACHE = undefined; + return this; + } addInput(inputData) { const self = this; const inputAdder = (_inputData, txBuf) => { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 58988f2..46e9487 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -23,9 +23,9 @@ export class Psbt extends PsbtBase { constructor(opts: PsbtOptsOptional = {}) { super(); // set defaults - this.setVersion(2); this.opts = Object.assign({}, DEFAULT_OPTS, opts); this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); + this.setVersion(2); // set cache const self = this; @@ -61,6 +61,18 @@ export class Psbt extends PsbtBase { dpew(this, 'opts', false, true); } + setVersion(version: number): this { + this.__TX.version = version; + this.__TX_BUF_CACHE = undefined; + return this; + } + + setLocktime(locktime: number): this { + this.__TX.locktime = locktime; + this.__TX_BUF_CACHE = undefined; + return this; + } + addInput(inputData: TransactionInput): this { const self = this; const inputAdder = ( diff --git a/types/psbt.d.ts b/types/psbt.d.ts index a530cd0..213c31b 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -8,6 +8,8 @@ export declare class Psbt extends PsbtBase { private __TX_BUF_CACHE?; private opts; constructor(opts?: PsbtOptsOptional); + setVersion(version: number): this; + setLocktime(locktime: number): this; addInput(inputData: TransactionInput): this; addOutput(outputData: TransactionOutput): this; extractTransaction(): Transaction; From b98761a28329897ed400f8686bf87fa5c8489851 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 13:52:48 +0900 Subject: [PATCH 044/111] Promise fixes for async --- src/psbt.js | 31 +++++++++++++++++-------------- ts_src/psbt.ts | 37 +++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index ec91bd9..b9ed061 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -175,20 +175,23 @@ class Psbt extends bip174_1.Psbt { return this.addPartialSigToInput(inputIndex, partialSig); } signInputAsync(inputIndex, keyPair) { - if (!keyPair || !keyPair.publicKey) - throw new Error('Need Signer to sign input'); - const { hash, sighashType } = getHashAndSighashType( - this.inputs, - inputIndex, - keyPair.publicKey, - this.globalMap.unsignedTx, - ); - return keyPair.sign(hash).then(signature => { - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(signature, sighashType), - }; - this.addPartialSigToInput(inputIndex, partialSig); + return new Promise((resolve, reject) => { + if (!keyPair || !keyPair.publicKey) + return reject(new Error('Need Signer to sign input')); + const { hash, sighashType } = getHashAndSighashType( + this.inputs, + inputIndex, + keyPair.publicKey, + this.globalMap.unsignedTx, + ); + Promise.resolve(keyPair.sign(hash)).then(signature => { + const partialSig = { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }; + this.addPartialSigToInput(inputIndex, partialSig); + resolve(); + }); }); } } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 46e9487..2c36130 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -214,23 +214,28 @@ export class Psbt extends PsbtBase { } signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise { - if (!keyPair || !keyPair.publicKey) - throw new Error('Need Signer to sign input'); - const { hash, sighashType } = getHashAndSighashType( - this.inputs, - inputIndex, - keyPair.publicKey, - this.globalMap.unsignedTx!, + return new Promise( + (resolve, reject): void => { + if (!keyPair || !keyPair.publicKey) + return reject(new Error('Need Signer to sign input')); + const { hash, sighashType } = getHashAndSighashType( + this.inputs, + inputIndex, + keyPair.publicKey, + this.globalMap.unsignedTx!, + ); + + Promise.resolve(keyPair.sign(hash)).then(signature => { + const partialSig = { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }; + + this.addPartialSigToInput(inputIndex, partialSig); + resolve(); + }); + }, ); - - return keyPair.sign(hash).then(signature => { - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(signature, sighashType), - }; - - this.addPartialSigToInput(inputIndex, partialSig); - }); } } From 5b5daf84dd7deeffa3f933fa03a29001fcc6270d Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 14:33:36 +0900 Subject: [PATCH 045/111] Remove unnecessary extra Transaction Buffer parsing --- src/psbt.js | 57 ++++++++++++++++++++++++++++++++-------- ts_src/psbt.ts | 70 ++++++++++++++++++++++++++++++++++++++++--------- types/psbt.d.ts | 5 +++- 3 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index b9ed061..4a6306f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -11,6 +11,39 @@ const bscript = require('./script'); const transaction_1 = require('./transaction'); const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { + static fromTransaction(txBuf) { + const tx = transaction_1.Transaction.fromBuffer(txBuf); + const psbt = new this(); + psbt.__TX = tx; + let inputCount = tx.ins.length; + let outputCount = tx.outs.length; + while (inputCount > 0) { + psbt.inputs.push({ + keyVals: [], + }); + inputCount--; + } + while (outputCount > 0) { + psbt.outputs.push({ + keyVals: [], + }); + outputCount--; + } + return psbt; + } + static fromBuffer(buffer) { + let tx; + const txCountGetter = txBuf => { + tx = transaction_1.Transaction.fromBuffer(txBuf); + return { + inputCount: tx.ins.length, + outputCount: tx.outs.length, + }; + }; + const psbt = super.fromBuffer(buffer, txCountGetter); + psbt.__TX = tx; + return psbt; + } constructor(opts = {}) { super(); // set defaults @@ -40,7 +73,7 @@ class Psbt extends bip174_1.Psbt { enumerable, writable, }); - dpew(this, '__TX', false, false); + dpew(this, '__TX', false, true); dpew(this, '__TX_BUF_CACHE', false, true); dpew(this, 'opts', false, true); } @@ -112,7 +145,7 @@ class Psbt extends bip174_1.Psbt { } extractTransaction() { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); - const tx = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + const tx = this.__TX.clone(); this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (input.finalScriptWitness) { @@ -138,7 +171,7 @@ class Psbt extends bip174_1.Psbt { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, - this.globalMap.unsignedTx, + this.__TX, ); if (!script) return false; const scriptType = classifyScript(script); @@ -166,7 +199,7 @@ class Psbt extends bip174_1.Psbt { this.inputs, inputIndex, keyPair.publicKey, - this.globalMap.unsignedTx, + this.__TX, ); const partialSig = { pubkey: keyPair.publicKey, @@ -182,7 +215,7 @@ class Psbt extends bip174_1.Psbt { this.inputs, inputIndex, keyPair.publicKey, - this.globalMap.unsignedTx, + this.__TX, ); Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = { @@ -202,9 +235,13 @@ const DEFAULT_OPTS = { function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function getHashAndSighashType(inputs, inputIndex, pubkey, txBuf) { +function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx) { const input = utils_1.checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig(inputIndex, input, txBuf); + const { hash, sighashType, script } = getHashForSig( + inputIndex, + input, + unsignedTx, + ); checkScriptForPubkey(pubkey, script); return { hash, @@ -322,8 +359,7 @@ function checkScriptForPubkey(pubkey, script) { ); } } -const getHashForSig = (inputIndex, input, txBuf) => { - const unsignedTx = transaction_1.Transaction.fromBuffer(txBuf); +const getHashForSig = (inputIndex, input, unsignedTx) => { const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; let hash; @@ -442,7 +478,7 @@ const classifyScript = script => { if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; }; -function getScriptFromInput(inputIndex, input, _unsignedTx) { +function getScriptFromInput(inputIndex, input, unsignedTx) { const res = { script: null, isSegwit: false, @@ -454,7 +490,6 @@ function getScriptFromInput(inputIndex, input, _unsignedTx) { res.isP2SH = true; res.script = input.redeemScript; } else { - const unsignedTx = transaction_1.Transaction.fromBuffer(_unsignedTx); const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( input.nonWitnessUtxo, ); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 2c36130..0aee319 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -17,6 +17,50 @@ import { Transaction } from './transaction'; const varuint = require('varuint-bitcoin'); export class Psbt extends PsbtBase { + static fromTransaction( + this: T, + txBuf: Buffer, + ): InstanceType { + const tx = Transaction.fromBuffer(txBuf); + const psbt = new this() as Psbt; + psbt.__TX = tx; + let inputCount = tx.ins.length; + let outputCount = tx.outs.length; + while (inputCount > 0) { + psbt.inputs.push({ + keyVals: [], + }); + inputCount--; + } + while (outputCount > 0) { + psbt.outputs.push({ + keyVals: [], + }); + outputCount--; + } + return psbt as InstanceType; + } + static fromBuffer( + this: T, + buffer: Buffer, + ): InstanceType { + let tx: Transaction | undefined; + const txCountGetter = ( + txBuf: Buffer, + ): { + inputCount: number; + outputCount: number; + } => { + tx = Transaction.fromBuffer(txBuf); + return { + inputCount: tx.ins.length, + outputCount: tx.outs.length, + }; + }; + const psbt = super.fromBuffer(buffer, txCountGetter) as Psbt; + psbt.__TX = tx!; + return psbt as InstanceType; + } private __TX: Transaction; private __TX_BUF_CACHE?: Buffer; private opts: PsbtOpts; @@ -56,7 +100,7 @@ export class Psbt extends PsbtBase { enumerable, writable, }); - dpew(this, '__TX', false, false); + dpew(this, '__TX', false, true); dpew(this, '__TX_BUF_CACHE', false, true); dpew(this, 'opts', false, true); } @@ -138,7 +182,7 @@ export class Psbt extends PsbtBase { extractTransaction(): Transaction { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); - const tx = Transaction.fromBuffer(this.globalMap.unsignedTx!); + const tx = this.__TX.clone(); this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (input.finalScriptWitness) { @@ -169,7 +213,7 @@ export class Psbt extends PsbtBase { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, - this.globalMap.unsignedTx!, + this.__TX, ); if (!script) return false; @@ -195,14 +239,14 @@ export class Psbt extends PsbtBase { return true; } - signInput(inputIndex: number, keyPair: Signer): Psbt { + signInput(inputIndex: number, keyPair: Signer): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( this.inputs, inputIndex, keyPair.publicKey, - this.globalMap.unsignedTx!, + this.__TX, ); const partialSig = { @@ -222,7 +266,7 @@ export class Psbt extends PsbtBase { this.inputs, inputIndex, keyPair.publicKey, - this.globalMap.unsignedTx!, + this.__TX, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -269,13 +313,17 @@ function getHashAndSighashType( inputs: PsbtInput[], inputIndex: number, pubkey: Buffer, - txBuf: Buffer, + unsignedTx: Transaction, ): { hash: Buffer; sighashType: number; } { const input = checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig(inputIndex, input, txBuf); + const { hash, sighashType, script } = getHashForSig( + inputIndex, + input, + unsignedTx, + ); checkScriptForPubkey(pubkey, script); return { hash, @@ -424,9 +472,8 @@ interface HashForSigData { const getHashForSig = ( inputIndex: number, input: PsbtInput, - txBuf: Buffer, + unsignedTx: Transaction, ): HashForSigData => { - const unsignedTx = Transaction.fromBuffer(txBuf); const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; let script: Buffer; @@ -571,7 +618,7 @@ interface GetScriptReturn { function getScriptFromInput( inputIndex: number, input: PsbtInput, - _unsignedTx: Buffer, + unsignedTx: Transaction, ): GetScriptReturn { const res: GetScriptReturn = { script: null, @@ -584,7 +631,6 @@ function getScriptFromInput( res.isP2SH = true; res.script = input.redeemScript; } else { - const unsignedTx = Transaction.fromBuffer(_unsignedTx); const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 213c31b..a203854 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,9 +1,12 @@ +/// import { Psbt as PsbtBase } from 'bip174'; import { TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { + static fromTransaction(this: T, txBuf: Buffer): InstanceType; + static fromBuffer(this: T, buffer: Buffer): InstanceType; private __TX; private __TX_BUF_CACHE?; private opts; @@ -18,7 +21,7 @@ export declare class Psbt extends PsbtBase { inputResults: boolean[]; }; finalizeInput(inputIndex: number): boolean; - signInput(inputIndex: number, keyPair: Signer): Psbt; + signInput(inputIndex: number, keyPair: Signer): this; signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise; } interface PsbtOptsOptional { From 3e7f490093303417e069a8a629103a774be97fad Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 14:45:50 +0900 Subject: [PATCH 046/111] Check for input empty on parse --- src/psbt.js | 14 ++++++++++++++ ts_src/psbt.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index 4a6306f..a85a23a 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -13,6 +13,7 @@ const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { static fromTransaction(txBuf) { const tx = transaction_1.Transaction.fromBuffer(txBuf); + checkTxEmpty(tx); const psbt = new this(); psbt.__TX = tx; let inputCount = tx.ins.length; @@ -35,6 +36,7 @@ class Psbt extends bip174_1.Psbt { let tx; const txCountGetter = txBuf => { tx = transaction_1.Transaction.fromBuffer(txBuf); + checkTxEmpty(tx); return { inputCount: tx.ins.length, outputCount: tx.outs.length, @@ -564,3 +566,15 @@ function scriptWitnessToWitnessStack(buffer) { return readVector(); } const range = n => [...Array(n).keys()]; +function checkTxEmpty(tx) { + const isEmpty = tx.ins.every( + input => + input.script && + input.script.length === 0 && + input.witness && + input.witness.length === 0, + ); + if (!isEmpty) { + throw new Error('Format Error: Transaction ScriptSigs are not empty'); + } +} diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0aee319..14f0c08 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -22,6 +22,7 @@ export class Psbt extends PsbtBase { txBuf: Buffer, ): InstanceType { const tx = Transaction.fromBuffer(txBuf); + checkTxEmpty(tx); const psbt = new this() as Psbt; psbt.__TX = tx; let inputCount = tx.ins.length; @@ -52,6 +53,7 @@ export class Psbt extends PsbtBase { outputCount: number; } => { tx = Transaction.fromBuffer(txBuf); + checkTxEmpty(tx); return { inputCount: tx.ins.length, outputCount: tx.outs.length, @@ -719,3 +721,16 @@ function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { } const range = (n: number): number[] => [...Array(n).keys()]; + +function checkTxEmpty(tx: Transaction): void { + const isEmpty = tx.ins.every( + input => + input.script && + input.script.length === 0 && + input.witness && + input.witness.length === 0, + ); + if (!isEmpty) { + throw new Error('Format Error: Transaction ScriptSigs are not empty'); + } +} From 45bd5b47516dad0a1f1ba6d37fba9cf1218f2a8f Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 4 Jul 2019 17:35:39 +0900 Subject: [PATCH 047/111] Check for signatures, add setSequence --- src/psbt.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++ ts_src/psbt.ts | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ types/psbt.d.ts | 1 + 3 files changed, 124 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index a85a23a..eaa8d62 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -80,16 +80,31 @@ class Psbt extends bip174_1.Psbt { dpew(this, 'opts', false, true); } setVersion(version) { + check32Bit(version); + checkInputsForPartialSig(this.inputs, 'setVersion'); this.__TX.version = version; this.__TX_BUF_CACHE = undefined; return this; } setLocktime(locktime) { + check32Bit(locktime); + checkInputsForPartialSig(this.inputs, 'setLocktime'); this.__TX.locktime = locktime; this.__TX_BUF_CACHE = undefined; return this; } + setSequence(inputIndex, sequence) { + check32Bit(sequence); + checkInputsForPartialSig(this.inputs, 'setSequence'); + if (this.__TX.ins.length <= inputIndex) { + throw new Error('Input index too high'); + } + this.__TX.ins[inputIndex].sequence = sequence; + this.__TX_BUF_CACHE = undefined; + return this; + } addInput(inputData) { + checkInputsForPartialSig(this.inputs, 'addInput'); const self = this; const inputAdder = (_inputData, txBuf) => { if ( @@ -119,6 +134,7 @@ class Psbt extends bip174_1.Psbt { return super.addInput(inputData, inputAdder); } addOutput(outputData) { + checkInputsForPartialSig(this.inputs, 'addOutput'); const { address } = outputData; if (typeof address === 'string') { const { network } = this.opts; @@ -578,3 +594,47 @@ function checkTxEmpty(tx) { throw new Error('Format Error: Transaction ScriptSigs are not empty'); } } +function checkInputsForPartialSig(inputs, action) { + inputs.forEach(input => { + let throws = false; + if ((input.partialSig || []).length > 0) { + if (input.sighashType !== undefined) { + const whitelist = []; + const isAnyoneCanPay = + input.sighashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + if (!isAnyoneCanPay && action === 'addInput') { + throws = true; + } + const hashType = input.sighashType & 0x1f; + switch (hashType) { + case transaction_1.Transaction.SIGHASH_ALL: + break; + case transaction_1.Transaction.SIGHASH_SINGLE: + case transaction_1.Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setSequence'); + break; + } + if (whitelist.indexOf(action) === -1) { + throws = true; + } + } else { + throws = true; + } + } + if (throws) { + throw new Error('Can not modify transaction, signatures exist.'); + } + }); +} +function check32Bit(num) { + if ( + typeof num !== 'number' || + num !== Math.floor(num) || + num > 0xffffffff || + num < 0 + ) { + throw new Error('Invalid 32 bit integer'); + } +} diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 14f0c08..0b019a3 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -108,18 +108,34 @@ export class Psbt extends PsbtBase { } setVersion(version: number): this { + check32Bit(version); + checkInputsForPartialSig(this.inputs, 'setVersion'); this.__TX.version = version; this.__TX_BUF_CACHE = undefined; return this; } setLocktime(locktime: number): this { + check32Bit(locktime); + checkInputsForPartialSig(this.inputs, 'setLocktime'); this.__TX.locktime = locktime; this.__TX_BUF_CACHE = undefined; return this; } + setSequence(inputIndex: number, sequence: number): this { + check32Bit(sequence); + checkInputsForPartialSig(this.inputs, 'setSequence'); + if (this.__TX.ins.length <= inputIndex) { + throw new Error('Input index too high'); + } + this.__TX.ins[inputIndex].sequence = sequence; + this.__TX_BUF_CACHE = undefined; + return this; + } + addInput(inputData: TransactionInput): this { + checkInputsForPartialSig(this.inputs, 'addInput'); const self = this; const inputAdder = ( _inputData: TransactionInput, @@ -152,6 +168,7 @@ export class Psbt extends PsbtBase { } addOutput(outputData: TransactionOutput): this { + checkInputsForPartialSig(this.inputs, 'addOutput'); const { address } = outputData as any; if (typeof address === 'string') { const { network } = this.opts; @@ -734,3 +751,49 @@ function checkTxEmpty(tx: Transaction): void { throw new Error('Format Error: Transaction ScriptSigs are not empty'); } } + +function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { + inputs.forEach(input => { + let throws = false; + if ((input.partialSig || []).length > 0) { + if (input.sighashType !== undefined) { + const whitelist: string[] = []; + const isAnyoneCanPay = + input.sighashType & Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + if (!isAnyoneCanPay && action === 'addInput') { + throws = true; + } + const hashType = input.sighashType & 0x1f; + switch (hashType) { + case Transaction.SIGHASH_ALL: + break; + case Transaction.SIGHASH_SINGLE: + case Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setSequence'); + break; + } + if (whitelist.indexOf(action) === -1) { + throws = true; + } + } else { + throws = true; + } + } + if (throws) { + throw new Error('Can not modify transaction, signatures exist.'); + } + }); +} + +function check32Bit(num: number): void { + if ( + typeof num !== 'number' || + num !== Math.floor(num) || + num > 0xffffffff || + num < 0 + ) { + throw new Error('Invalid 32 bit integer'); + } +} diff --git a/types/psbt.d.ts b/types/psbt.d.ts index a203854..1aa6f28 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -13,6 +13,7 @@ export declare class Psbt extends PsbtBase { constructor(opts?: PsbtOptsOptional); setVersion(version: number): this; setLocktime(locktime: number): this; + setSequence(inputIndex: number, sequence: number): this; addInput(inputData: TransactionInput): this; addOutput(outputData: TransactionOutput): this; extractTransaction(): Transaction; From 2501fc92bcd8f2467e715c36b1be554186b35cee Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 13:47:18 +0700 Subject: [PATCH 048/111] Test BIP174 creator check test cases --- test/fixtures/psbt.json | 25 +++++++++++++++++++++++++ test/psbt.js | 14 ++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index d57894a..49d4f1f 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -143,6 +143,31 @@ "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSrSIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=", "inputToCheck": 1 } + ], + "creator": [ + { + "inputs": [ + { + "hash": "75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858", + "index": 0 + }, + { + "hash": "1dea7cd05979072a3578cab271c02244ea8a090bbb46aa680a65ecd027048d83", + "index": 1 + } + ], + "outputs": [ + { + "script": "0014d85c2b71d0060b09c9886aeb815e50991dda124d", + "value": 149990000 + }, + { + "script": "001400aea9a2e5f0f876a588df5546e8742d1d87008f", + "value": 100000000 + } + ], + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index e99e592..98f407b 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -33,6 +33,20 @@ describe(`Psbt`, () => { }, {message: f.errorMessage}) }) }) + + fixtures.bip174.creator.forEach(f => { + it('Creates expected PSBT', () => { + const psbt = new Psbt() + for (const input of f.inputs) { + psbt.addInput(input) + } + for (const output of f.outputs) { + const script = Buffer.from(output.script, 'hex'); + psbt.addOutput({...output, script}) + } + assert.strictEqual(psbt.toBase64(), f.result) + }) + }) }) describe('signInput', () => { From 275618ed43e1eb533c37fdbc2967cf1bfc916b93 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 13:52:19 +0700 Subject: [PATCH 049/111] Remove console.log --- src/psbt.js | 2 -- ts_src/psbt.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index eaa8d62..b2bbeda 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -128,7 +128,6 @@ class Psbt extends bip174_1.Psbt { _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, witness: [], }); - console.log(self.__TX); return self.__TX.toBuffer(); }; return super.addInput(inputData, inputAdder); @@ -156,7 +155,6 @@ class Psbt extends bip174_1.Psbt { script: _outputData.script, value: _outputData.value, }); - console.log(self.__TX); return self.__TX.toBuffer(); }; return super.addOutput(outputData, true, outputAdder); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0b019a3..fea9a67 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -161,7 +161,6 @@ export class Psbt extends PsbtBase { sequence: _inputData.sequence || Transaction.DEFAULT_SEQUENCE, witness: [], }); - console.log(self.__TX); return self.__TX.toBuffer(); }; return super.addInput(inputData, inputAdder); @@ -193,7 +192,6 @@ export class Psbt extends PsbtBase { script: (_outputData as any).script!, value: _outputData.value, }); - console.log(self.__TX); return self.__TX.toBuffer(); }; return super.addOutput(outputData, true, outputAdder); From a32d1c3eac4e4fa6e2a3ddfcfa59008b98e206e6 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 14:53:51 +0700 Subject: [PATCH 050/111] Test BIP174 updater check test cases --- test/fixtures/psbt.json | 76 +++++++++++++++++++++++++++++++++++++++++ test/psbt.js | 51 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 49d4f1f..ab9a470 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -168,6 +168,82 @@ ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" } + ], + "updater": [ + { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=", + "inputData": [ + { + "nonWitnessUtxo": "0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000", + "redeemScript": "5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae", + "bip32Derivation": [ + { + "masterFingerprint": "d90c6a4f", + "pubkey": "029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f", + "path": "m/0'/0'/0'" + }, + { + "masterFingerprint": "d90c6a4f", + "pubkey": "02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7", + "path": "m/0'/0'/1'" + } + ] + }, + { + "witnessUtxo": { + "script": "a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887", + "value": 200000000 + }, + "redeemScript": "00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903", + "witnessScript": "522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae", + "bip32Derivation": [ + { + "masterFingerprint": "d90c6a4f", + "pubkey": "023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73", + "path": "m/0'/0'/3'" + }, + { + "masterFingerprint": "d90c6a4f", + "pubkey": "03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc", + "path": "m/0'/0'/2'" + } + ] + } + ], + "outputData": [ + { + "bip32Derivation": [ + { + "masterFingerprint": "d90c6a4f", + "pubkey": "03a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58771", + "path": "m/0'/0'/4'" + } + ] + }, + { + "bip32Derivation": [ + { + "masterFingerprint": "d90c6a4f", + "pubkey": "027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b50051096", + "path": "m/0'/0'/5'" + } + ] + } + ], + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + }, + { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", + "inputData": [ + { + "sighashType": 1 + }, + { + "sighashType": 1 + } + ], + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index 98f407b..cd80d73 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -6,6 +6,27 @@ const Psbt = require('..').Psbt const fixtures = require('./fixtures/psbt') +const upperCaseFirstLetter = str => str.replace(/^./, s => s.toUpperCase()) + +const b = hex => Buffer.from(hex, 'hex'); + +const initBuffers = (attr, data) => { + if ([ + 'nonWitnessUtxo', + 'redeemScript', + 'witnessScript' + ].includes(attr)) { + data = b(data) + } else if (attr === 'bip32Derivation') { + data.masterFingerprint = b(data.masterFingerprint) + data.pubkey = b(data.pubkey) + } else if (attr === 'witnessUtxo') { + data.script = b(data.script) + } + + return data +}; + describe(`Psbt`, () => { describe('BIP174 Test Vectors', () => { fixtures.bip174.invalid.forEach(f => { @@ -47,6 +68,36 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.toBase64(), f.result) }) }) + + fixtures.bip174.updater.forEach(f => { + it('Updates PSBT to the expected result', () => { + const psbt = Psbt.fromBase64(f.psbt) + + for (const inputOrOutput of ['input', 'output']) { + const fixtureData = f[`${inputOrOutput}Data`] + if (fixtureData) { + for (const [i, data] of fixtureData.entries()) { + const attrs = Object.keys(data) + for (const attr of attrs) { + const upperAttr = upperCaseFirstLetter(attr) + let adder = psbt[`add${upperAttr}To${upperCaseFirstLetter(inputOrOutput)}`] + if (adder !== undefined) { + adder = adder.bind(psbt) + const arg = data[attr] + if (Array.isArray(arg)) { + arg.forEach(a => adder(i, initBuffers(attr, a))) + } else { + adder(i, initBuffers(attr, arg)) + } + } + } + } + } + } + + assert.strictEqual(psbt.toBase64(), f.result) + }) + }) }) describe('signInput', () => { From 30815e9e8fe5a9a74129800013a1193848b854b0 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 15:32:16 +0700 Subject: [PATCH 051/111] Test BIP174 signer test cases --- test/fixtures/psbt.json | 30 ++++++++++++++++++++++++++++++ test/psbt.js | 14 ++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index ab9a470..3e65598 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -244,6 +244,36 @@ ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" } + ], + "signer": [ + { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", + "keys": [ + { + "inputToSign": 0, + "WIF": "cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr" + }, + { + "inputToSign": 1, + "WIF": "cR6SXDoyfQrcp4piaiHE97Rsgta9mNhGTen9XeonVgwsh4iSgw6d" + } + ], + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEBAwQBAAAAAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + }, + { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", + "keys": [ + { + "inputToSign": 0, + "WIF": "cT7J9YpCwY3AVRFSjN6ukeEeWY6mhpbJPxRaDaP5QTdygQRxP9Au" + }, + { + "inputToSign": 1, + "WIF": "cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE" + } + ], + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index cd80d73..00f50ba 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -3,6 +3,7 @@ const assert = require('assert') const ECPair = require('../src/ecpair') const Psbt = require('..').Psbt +const NETWORKS = require('../src/networks') const fixtures = require('./fixtures/psbt') @@ -100,6 +101,19 @@ describe(`Psbt`, () => { }) }) + fixtures.bip174.signer.forEach(f => { + it('Signs PSBT to the expected result', () => { + const psbt = Psbt.fromBase64(f.psbt) + + f.keys.forEach(({inputToSign, WIF}) => { + const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet); + psbt.signInput(inputToSign, keyPair); + }) + + assert.strictEqual(psbt.toBase64(), f.result) + }) + }) + describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { From 4e55ab0f2057b796441f938efbfcd78f02028ef8 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 16:11:19 +0700 Subject: [PATCH 052/111] Test BIP174 combiner test cases --- test/fixtures/psbt.json | 9 +++++++++ test/psbt.js | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 3e65598..5928ff6 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -274,6 +274,15 @@ ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" } + ], + "combiner": [ + { + "psbts": [ + "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEBAwQBAAAAAQQiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEFR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuIgYCOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnMQ2QxqTwAAAIAAAACAAwAAgCIGAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcENkMak8AAACAAAAAgAIAAIAAIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", + "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" + ], + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index 00f50ba..f417079 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -114,6 +114,20 @@ describe(`Psbt`, () => { }) }) + fixtures.bip174.combiner.forEach(f => { + it('Combines two PSBTs to the expected result', () => { + const psbts = f.psbts.map(psbt => Psbt.fromBase64(psbt)) + + psbts[0].combine(psbts[1]) + + // Produces a different Base64 string due to implemetation specific key-value ordering. + // That means this test will fail: + // assert.strictEqual(psbts[0].toBase64(), f.result) + // However, if we compare the actual PSBT properties we can see they are logically identical: + assert.deepStrictEqual(psbts[0], Psbt.fromBase64(f.result)) + }) + }) + describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { From a80155dbdbd1ceff2466fb2054792ecc327a1f3a Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 16:16:19 +0700 Subject: [PATCH 053/111] Test BIP174 finalizer test cases --- test/fixtures/psbt.json | 6 ++++++ test/psbt.js | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 5928ff6..5344894 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -283,6 +283,12 @@ ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" } + ], + "finalizer": [ + { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index f417079..213afc5 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -128,6 +128,16 @@ describe(`Psbt`, () => { }) }) + fixtures.bip174.finalizer.forEach(f => { + it('Finalizes inputs and gives the expected PSBT', () => { + const psbt = Psbt.fromBase64(f.psbt) + + psbt.finalizeAllInputs() + + assert.strictEqual(psbt.toBase64(), f.result) + }) + }) + describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { From 35cf120c33b64a57c396a6a1132a8d9b825d2078 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 16:17:50 +0700 Subject: [PATCH 054/111] Add extra combiner test case --- test/fixtures/psbt.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 5344894..37416cf 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -282,6 +282,13 @@ "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=" ], "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + }, + { + "psbts": [ + "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAKDwECAwQFBgcICQ8BAgMEBQYHCAkKCwwNDg8ACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAoPAQIDBAUGBwgJDwECAwQFBgcICQoLDA0ODwA=", + "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAKDwECAwQFBgcIEA8BAgMEBQYHCAkKCwwNDg8ACg8BAgMEBQYHCBAPAQIDBAUGBwgJCgsMDQ4PAAoPAQIDBAUGBwgQDwECAwQFBgcICQoLDA0ODwA=" + ], + "result": "cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAKDwECAwQFBgcICQ8BAgMEBQYHCAkKCwwNDg8KDwECAwQFBgcIEA8BAgMEBQYHCAkKCwwNDg8ACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PCg8BAgMEBQYHCBAPAQIDBAUGBwgJCgsMDQ4PAAoPAQIDBAUGBwgJDwECAwQFBgcICQoLDA0ODwoPAQIDBAUGBwgQDwECAwQFBgcICQoLDA0ODwA=" } ], "finalizer": [ From e3efdbdb99e904aba5d888dedced11ead9fddd1f Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 16:23:55 +0700 Subject: [PATCH 055/111] Test BIP174 extractor test cases --- test/fixtures/psbt.json | 6 ++++++ test/psbt.js | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 37416cf..7242b03 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -296,6 +296,12 @@ "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" } + ], + "extractor": [ + { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==", + "transaction": "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" + } ] }, "signInput": { diff --git a/test/psbt.js b/test/psbt.js index 213afc5..5d59a88 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -138,6 +138,16 @@ describe(`Psbt`, () => { }) }) + fixtures.bip174.extractor.forEach(f => { + it('Extracts the expected transaction from a PSBT', () => { + const psbt = Psbt.fromBase64(f.psbt) + + const transaction = psbt.extractTransaction().toHex() + + assert.strictEqual(transaction, f.transaction) + }) + }) + describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { From dc23b8cce0b9a59e6f54046c32f8fe323dda363a Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 17:00:01 +0700 Subject: [PATCH 056/111] Test fromTransaction --- test/fixtures/psbt.json | 8 +++++++- test/psbt.js | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 7242b03..00877f9 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -391,5 +391,11 @@ } } ] - } + }, + "fromTransaction": [ + { + "transaction": "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000", + "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" + } + ] } diff --git a/test/psbt.js b/test/psbt.js index 5d59a88..290c5e5 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -169,4 +169,13 @@ describe(`Psbt`, () => { }) }) }) + + describe('fromTransaction', () => { + fixtures.fromTransaction.forEach(f => { + it('Creates the expected PSBT from a transaction buffer', () => { + const psbt = Psbt.fromTransaction(Buffer.from(f.transaction, 'hex')) + assert.strictEqual(psbt.toBase64(), f.result) + }) + }) + }) }) From 09a6c37430c76eb90a583f6e164dc6f98e7daee3 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 17:00:20 +0700 Subject: [PATCH 057/111] Test setVersion --- test/psbt.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/psbt.js b/test/psbt.js index 290c5e5..0f9a2f8 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -178,4 +178,14 @@ describe(`Psbt`, () => { }) }) }) + + describe('setVersion', () => { + it('Sets the version value of the unsigned transaction', () => { + const psbt = new Psbt() + + assert.strictEqual(psbt.extractTransaction().version, 2) + psbt.setVersion(1) + assert.strictEqual(psbt.extractTransaction().version, 1) + }) + }) }) From 871e5877117de551affeca88f2cbab84f3d3b240 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 17:00:34 +0700 Subject: [PATCH 058/111] Test setLocktime --- test/psbt.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/psbt.js b/test/psbt.js index 0f9a2f8..d790b63 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -188,4 +188,14 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.extractTransaction().version, 1) }) }) + + describe('setLocktime', () => { + it('Sets the nLockTime value of the unsigned transaction', () => { + const psbt = new Psbt() + + assert.strictEqual(psbt.extractTransaction().locktime, 0) + psbt.setLocktime(1) + assert.strictEqual(psbt.extractTransaction().locktime, 1) + }) + }) }) From ba5f336e02785560166d3812908264189cca765c Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 4 Jul 2019 17:20:16 +0700 Subject: [PATCH 059/111] Test setSequence --- test/psbt.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/psbt.js b/test/psbt.js index d790b63..67f3611 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -198,4 +198,30 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.extractTransaction().locktime, 1) }) }) + + describe('setSequence', () => { + it('Sets the sequence number for a given input', () => { + const psbt = new Psbt() + psbt.addInput({ + hash: '0000000000000000000000000000000000000000000000000000000000000000', + index: 0 + }); + + assert.strictEqual(psbt.__TX.ins[0].sequence, 0xffffffff) + psbt.setSequence(0, 0) + assert.strictEqual(psbt.__TX.ins[0].sequence, 0) + }) + + it('throws if input index is too high', () => { + const psbt = new Psbt() + psbt.addInput({ + hash: '0000000000000000000000000000000000000000000000000000000000000000', + index: 0 + }); + + assert.throws(() => { + psbt.setSequence(1, 0) + }, {message: 'Input index too high'}) + }) + }) }) From 14eeb309df25c59ebf01f05078036425c33b9a65 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 12:28:04 +0900 Subject: [PATCH 060/111] Add fee checking before extract --- src/psbt.js | 204 ++++++++++++++++++++++++++++++++++++++---------- ts_src/psbt.ts | 156 +++++++++++++++++++++++++++++++++++- types/psbt.d.ts | 12 ++- 3 files changed, 324 insertions(+), 48 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index b2bbeda..272c716 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -11,6 +11,45 @@ const bscript = require('./script'); const transaction_1 = require('./transaction'); const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { + constructor(opts = {}) { + super(); + this.__NON_WITNESS_UTXO_TX_CACHE = []; + this.__NON_WITNESS_UTXO_BUF_CACHE = []; + // set defaults + this.opts = Object.assign({}, DEFAULT_OPTS, opts); + this.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + this.setVersion(2); + // set cache + const self = this; + delete this.globalMap.unsignedTx; + Object.defineProperty(this.globalMap, 'unsignedTx', { + enumerable: true, + get() { + if (self.__TX_BUF_CACHE !== undefined) { + return self.__TX_BUF_CACHE; + } else { + self.__TX_BUF_CACHE = self.__TX.toBuffer(); + return self.__TX_BUF_CACHE; + } + }, + set(data) { + self.__TX_BUF_CACHE = data; + }, + }); + // Make data hidden when enumerating + const dpew = (obj, attr, enumerable, writable) => + Object.defineProperty(obj, attr, { + enumerable, + writable, + }); + dpew(this, '__TX', false, true); + dpew(this, '__EXTRACTED_TX', false, true); + dpew(this, '__FEE_RATE', false, true); + dpew(this, '__TX_BUF_CACHE', false, true); + dpew(this, '__NON_WITNESS_UTXO_TX_CACHE', false, true); + dpew(this, '__NON_WITNESS_UTXO_BUF_CACHE', false, true); + dpew(this, 'opts', false, true); + } static fromTransaction(txBuf) { const tx = transaction_1.Transaction.fromBuffer(txBuf); checkTxEmpty(tx); @@ -46,44 +85,16 @@ class Psbt extends bip174_1.Psbt { psbt.__TX = tx; return psbt; } - constructor(opts = {}) { - super(); - // set defaults - this.opts = Object.assign({}, DEFAULT_OPTS, opts); - this.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); - this.setVersion(2); - // set cache - const self = this; - delete this.globalMap.unsignedTx; - Object.defineProperty(this.globalMap, 'unsignedTx', { - enumerable: true, - get() { - if (self.__TX_BUF_CACHE !== undefined) { - return self.__TX_BUF_CACHE; - } else { - self.__TX_BUF_CACHE = self.__TX.toBuffer(); - return self.__TX_BUF_CACHE; - } - }, - set(data) { - self.__TX_BUF_CACHE = data; - }, - }); - // Make data hidden when enumerating - const dpew = (obj, attr, enumerable, writable) => - Object.defineProperty(obj, attr, { - enumerable, - writable, - }); - dpew(this, '__TX', false, true); - dpew(this, '__TX_BUF_CACHE', false, true); - dpew(this, 'opts', false, true); + setMaximumFeeRate(satoshiPerByte) { + check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw + this.opts.maximumFeeRate = satoshiPerByte; } setVersion(version) { check32Bit(version); checkInputsForPartialSig(this.inputs, 'setVersion'); this.__TX.version = version; this.__TX_BUF_CACHE = undefined; + this.__EXTRACTED_TX = undefined; return this; } setLocktime(locktime) { @@ -91,6 +102,7 @@ class Psbt extends bip174_1.Psbt { checkInputsForPartialSig(this.inputs, 'setLocktime'); this.__TX.locktime = locktime; this.__TX_BUF_CACHE = undefined; + this.__EXTRACTED_TX = undefined; return this; } setSequence(inputIndex, sequence) { @@ -101,6 +113,7 @@ class Psbt extends bip174_1.Psbt { } this.__TX.ins[inputIndex].sequence = sequence; this.__TX_BUF_CACHE = undefined; + this.__EXTRACTED_TX = undefined; return this; } addInput(inputData) { @@ -159,8 +172,29 @@ class Psbt extends bip174_1.Psbt { }; return super.addOutput(outputData, true, outputAdder); } - extractTransaction() { + addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { + super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); + const input = this.inputs[inputIndex]; + addNonWitnessTxCache(this, input, inputIndex); + return this; + } + extractTransaction(disableFeeCheck) { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + if (!disableFeeCheck) { + const feeRate = this.__FEE_RATE || this.getFeeRate(); + const vsize = this.__EXTRACTED_TX.virtualSize(); + const satoshis = feeRate * vsize; + if (feeRate >= this.opts.maximumFeeRate) { + throw new Error( + `Warning: You are paying around ${satoshis / 1e8} in fees, which ` + + `is ${feeRate} satoshi per byte for a transaction with a VSize of ` + + `${vsize} bytes (segwit counted as 0.25 byte per byte)\n` + + `Use setMaximumFeeRate method to raise your threshold, or pass ` + + `true to the first arg of extractTransaction.`, + ); + } + } + if (this.__EXTRACTED_TX) return this.__EXTRACTED_TX; const tx = this.__TX.clone(); this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; @@ -170,8 +204,51 @@ class Psbt extends bip174_1.Psbt { ); } }); + this.__EXTRACTED_TX = tx; return tx; } + getFeeRate() { + if (!this.inputs.every(isFinalized)) + throw new Error('PSBT must be finalized to calculate fee rate'); + if (this.__FEE_RATE) return this.__FEE_RATE; + let tx; + let inputAmount = 0; + let mustFinalize = true; + if (this.__EXTRACTED_TX) { + tx = this.__EXTRACTED_TX; + mustFinalize = false; + } else { + tx = this.__TX.clone(); + } + this.inputs.forEach((input, idx) => { + if (mustFinalize && input.finalScriptSig) + tx.ins[idx].script = input.finalScriptSig; + if (mustFinalize && input.finalScriptWitness) { + tx.ins[idx].witness = scriptWitnessToWitnessStack( + input.finalScriptWitness, + ); + } + if (input.witnessUtxo) { + inputAmount += input.witnessUtxo.value; + } else if (input.nonWitnessUtxo) { + // @ts-ignore + if (!this.__NON_WITNESS_UTXO_TX_CACHE[idx]) { + addNonWitnessTxCache(this, input, idx); + } + const vout = this.__TX.ins[idx].index; + const out = this.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout]; + inputAmount += out.value; + } else { + throw new Error('Missing input value: index #' + idx); + } + }); + this.__EXTRACTED_TX = tx; + const outputAmount = tx.outs.reduce((total, o) => total + o.value, 0); + const fee = inputAmount - outputAmount; + const bytes = tx.virtualSize(); + this.__FEE_RATE = Math.floor(fee / bytes); + return this.__FEE_RATE; + } finalizeAllInputs() { const inputResults = range(this.inputs.length).map(idx => this.finalizeInput(idx), @@ -188,6 +265,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, input, this.__TX, + this, ); if (!script) return false; const scriptType = classifyScript(script); @@ -216,6 +294,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__TX, + this, ); const partialSig = { pubkey: keyPair.publicKey, @@ -232,6 +311,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__TX, + this, ); Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = { @@ -247,16 +327,50 @@ class Psbt extends bip174_1.Psbt { exports.Psbt = Psbt; const DEFAULT_OPTS = { network: networks_1.bitcoin, + maximumFeeRate: 5000, }; +function addNonWitnessTxCache(psbt, input, inputIndex) { + // @ts-ignore + psbt.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; + const tx = transaction_1.Transaction.fromBuffer(input.nonWitnessUtxo); + // @ts-ignore + psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; + const self = psbt; + const selfIndex = inputIndex; + delete input.nonWitnessUtxo; + Object.defineProperty(input, 'nonWitnessUtxo', { + enumerable: true, + get() { + // @ts-ignore + if (self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] !== undefined) { + // @ts-ignore + return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + } else { + // @ts-ignore + self.__NON_WITNESS_UTXO_BUF_CACHE[ + selfIndex + // @ts-ignore + ] = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex].toBuffer(); + // @ts-ignore + return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + } + }, + set(data) { + // @ts-ignore + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; + }, + }); +} function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx) { +function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx, psbt) { const input = utils_1.checkForInput(inputs, inputIndex); const { hash, sighashType, script } = getHashForSig( inputIndex, input, unsignedTx, + psbt, ); checkScriptForPubkey(pubkey, script); return { @@ -375,15 +489,18 @@ function checkScriptForPubkey(pubkey, script) { ); } } -const getHashForSig = (inputIndex, input, unsignedTx) => { +const getHashForSig = (inputIndex, input, unsignedTx, psbt) => { const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; let hash; let script; if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( - input.nonWitnessUtxo, - ); + // @ts-ignore + if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(psbt, input, inputIndex); + } + // @ts-ignore + const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout @@ -494,7 +611,7 @@ const classifyScript = script => { if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; }; -function getScriptFromInput(inputIndex, input, unsignedTx) { +function getScriptFromInput(inputIndex, input, unsignedTx, psbt) { const res = { script: null, isSegwit: false, @@ -506,9 +623,12 @@ function getScriptFromInput(inputIndex, input, unsignedTx) { res.isP2SH = true; res.script = input.redeemScript; } else { - const nonWitnessUtxoTx = transaction_1.Transaction.fromBuffer( - input.nonWitnessUtxo, - ); + // @ts-ignore + if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(psbt, input, inputIndex); + } + // @ts-ignore + const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index fea9a67..bd6aeb4 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,5 +1,6 @@ import { Psbt as PsbtBase } from 'bip174'; import { + NonWitnessUtxo, PartialSig, PsbtInput, TransactionInput, @@ -13,7 +14,7 @@ import { Signer, SignerAsync } from './ecpair'; import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; -import { Transaction } from './transaction'; +import { Output, Transaction } from './transaction'; const varuint = require('varuint-bitcoin'); export class Psbt extends PsbtBase { @@ -65,6 +66,10 @@ export class Psbt extends PsbtBase { } private __TX: Transaction; private __TX_BUF_CACHE?: Buffer; + private __FEE_RATE?: number; + private __EXTRACTED_TX?: Transaction; + private __NON_WITNESS_UTXO_TX_CACHE: Transaction[] = []; + private __NON_WITNESS_UTXO_BUF_CACHE: Buffer[] = []; private opts: PsbtOpts; constructor(opts: PsbtOptsOptional = {}) { super(); @@ -103,15 +108,25 @@ export class Psbt extends PsbtBase { writable, }); dpew(this, '__TX', false, true); + dpew(this, '__EXTRACTED_TX', false, true); + dpew(this, '__FEE_RATE', false, true); dpew(this, '__TX_BUF_CACHE', false, true); + dpew(this, '__NON_WITNESS_UTXO_TX_CACHE', false, true); + dpew(this, '__NON_WITNESS_UTXO_BUF_CACHE', false, true); dpew(this, 'opts', false, true); } + setMaximumFeeRate(satoshiPerByte: number): void { + check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw + this.opts.maximumFeeRate = satoshiPerByte; + } + setVersion(version: number): this { check32Bit(version); checkInputsForPartialSig(this.inputs, 'setVersion'); this.__TX.version = version; this.__TX_BUF_CACHE = undefined; + this.__EXTRACTED_TX = undefined; return this; } @@ -120,6 +135,7 @@ export class Psbt extends PsbtBase { checkInputsForPartialSig(this.inputs, 'setLocktime'); this.__TX.locktime = locktime; this.__TX_BUF_CACHE = undefined; + this.__EXTRACTED_TX = undefined; return this; } @@ -131,6 +147,7 @@ export class Psbt extends PsbtBase { } this.__TX.ins[inputIndex].sequence = sequence; this.__TX_BUF_CACHE = undefined; + this.__EXTRACTED_TX = undefined; return this; } @@ -197,8 +214,33 @@ export class Psbt extends PsbtBase { return super.addOutput(outputData, true, outputAdder); } - extractTransaction(): Transaction { + addNonWitnessUtxoToInput( + inputIndex: number, + nonWitnessUtxo: NonWitnessUtxo, + ): this { + super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); + const input = this.inputs[inputIndex]; + addNonWitnessTxCache(this, input, inputIndex); + return this; + } + + extractTransaction(disableFeeCheck?: boolean): Transaction { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + if (!disableFeeCheck) { + const feeRate = this.__FEE_RATE || this.getFeeRate(); + const vsize = this.__EXTRACTED_TX!.virtualSize(); + const satoshis = feeRate * vsize; + if (feeRate >= this.opts.maximumFeeRate) { + throw new Error( + `Warning: You are paying around ${satoshis / 1e8} in fees, which ` + + `is ${feeRate} satoshi per byte for a transaction with a VSize of ` + + `${vsize} bytes (segwit counted as 0.25 byte per byte)\n` + + `Use setMaximumFeeRate method to raise your threshold, or pass ` + + `true to the first arg of extractTransaction.`, + ); + } + } + if (this.__EXTRACTED_TX) return this.__EXTRACTED_TX; const tx = this.__TX.clone(); this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; @@ -208,9 +250,56 @@ export class Psbt extends PsbtBase { ); } }); + this.__EXTRACTED_TX = tx; return tx; } + getFeeRate(): number { + if (!this.inputs.every(isFinalized)) + throw new Error('PSBT must be finalized to calculate fee rate'); + if (this.__FEE_RATE) return this.__FEE_RATE; + let tx: Transaction; + let inputAmount = 0; + let mustFinalize = true; + if (this.__EXTRACTED_TX) { + tx = this.__EXTRACTED_TX; + mustFinalize = false; + } else { + tx = this.__TX.clone(); + } + this.inputs.forEach((input, idx) => { + if (mustFinalize && input.finalScriptSig) + tx.ins[idx].script = input.finalScriptSig; + if (mustFinalize && input.finalScriptWitness) { + tx.ins[idx].witness = scriptWitnessToWitnessStack( + input.finalScriptWitness, + ); + } + if (input.witnessUtxo) { + inputAmount += input.witnessUtxo.value; + } else if (input.nonWitnessUtxo) { + // @ts-ignore + if (!this.__NON_WITNESS_UTXO_TX_CACHE[idx]) { + addNonWitnessTxCache(this, input, idx); + } + const vout = this.__TX.ins[idx].index; + const out = this.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout] as Output; + inputAmount += out.value; + } else { + throw new Error('Missing input value: index #' + idx); + } + }); + this.__EXTRACTED_TX = tx; + const outputAmount = (tx.outs as Output[]).reduce( + (total, o) => total + o.value, + 0, + ); + const fee = inputAmount - outputAmount; + const bytes = tx.virtualSize(); + this.__FEE_RATE = Math.floor(fee / bytes); + return this.__FEE_RATE; + } + finalizeAllInputs(): { result: boolean; inputResults: boolean[]; @@ -231,6 +320,7 @@ export class Psbt extends PsbtBase { inputIndex, input, this.__TX, + this, ); if (!script) return false; @@ -264,6 +354,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__TX, + this, ); const partialSig = { @@ -284,6 +375,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__TX, + this, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -312,16 +404,58 @@ export class Psbt extends PsbtBase { interface PsbtOptsOptional { network?: Network; + maximumFeeRate?: number; } interface PsbtOpts { network: Network; + maximumFeeRate: number; } const DEFAULT_OPTS = { network: btcNetwork, + maximumFeeRate: 5000, // satoshi per byte }; +function addNonWitnessTxCache( + psbt: Psbt, + input: PsbtInput, + inputIndex: number, +): void { + // @ts-ignore + psbt.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!; + + const tx = Transaction.fromBuffer(input.nonWitnessUtxo!); + // @ts-ignore + psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; + + const self = psbt; + const selfIndex = inputIndex; + delete input.nonWitnessUtxo; + Object.defineProperty(input, 'nonWitnessUtxo', { + enumerable: true, + get(): Buffer { + // @ts-ignore + if (self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] !== undefined) { + // @ts-ignore + return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + } else { + // @ts-ignore + self.__NON_WITNESS_UTXO_BUF_CACHE[ + selfIndex + // @ts-ignore + ] = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex].toBuffer(); + // @ts-ignore + return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + } + }, + set(data: Buffer): void { + // @ts-ignore + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; + }, + }); +} + function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } @@ -331,6 +465,7 @@ function getHashAndSighashType( inputIndex: number, pubkey: Buffer, unsignedTx: Transaction, + psbt: Psbt, ): { hash: Buffer; sighashType: number; @@ -340,6 +475,7 @@ function getHashAndSighashType( inputIndex, input, unsignedTx, + psbt, ); checkScriptForPubkey(pubkey, script); return { @@ -490,13 +626,19 @@ const getHashForSig = ( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, + psbt: Psbt, ): HashForSigData => { const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; let script: Buffer; if (input.nonWitnessUtxo) { - const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); + // @ts-ignore + if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(psbt, input, inputIndex); + } + // @ts-ignore + const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); @@ -636,6 +778,7 @@ function getScriptFromInput( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, + psbt: Psbt, ): GetScriptReturn { const res: GetScriptReturn = { script: null, @@ -648,7 +791,12 @@ function getScriptFromInput( res.isP2SH = true; res.script = input.redeemScript; } else { - const nonWitnessUtxoTx = Transaction.fromBuffer(input.nonWitnessUtxo); + // @ts-ignore + if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(psbt, input, inputIndex); + } + // @ts-ignore + const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 1aa6f28..af234d8 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,6 +1,6 @@ /// import { Psbt as PsbtBase } from 'bip174'; -import { TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; +import { NonWitnessUtxo, TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; @@ -9,14 +9,21 @@ export declare class Psbt extends PsbtBase { static fromBuffer(this: T, buffer: Buffer): InstanceType; private __TX; private __TX_BUF_CACHE?; + private __FEE_RATE?; + private __EXTRACTED_TX?; + private __NON_WITNESS_UTXO_TX_CACHE; + private __NON_WITNESS_UTXO_BUF_CACHE; private opts; constructor(opts?: PsbtOptsOptional); + setMaximumFeeRate(satoshiPerByte: number): void; setVersion(version: number): this; setLocktime(locktime: number): this; setSequence(inputIndex: number, sequence: number): this; addInput(inputData: TransactionInput): this; addOutput(outputData: TransactionOutput): this; - extractTransaction(): Transaction; + addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; + extractTransaction(disableFeeCheck?: boolean): Transaction; + getFeeRate(): number; finalizeAllInputs(): { result: boolean; inputResults: boolean[]; @@ -27,5 +34,6 @@ export declare class Psbt extends PsbtBase { } interface PsbtOptsOptional { network?: Network; + maximumFeeRate?: number; } export {}; From 51133c8051458b9d9089aa0374be6fb20999b40d Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 12:51:13 +0900 Subject: [PATCH 061/111] Add type instance check tests --- src/psbt.js | 10 ++++++++-- test/psbt.js | 27 ++++++++++++++++++++++++++- ts_src/psbt.ts | 10 ++++++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 272c716..ba42917 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -143,7 +143,10 @@ class Psbt extends bip174_1.Psbt { }); return self.__TX.toBuffer(); }; - return super.addInput(inputData, inputAdder); + super.addInput(inputData, inputAdder); + this.__FEE_RATE = undefined; + this.__EXTRACTED_TX = undefined; + return this; } addOutput(outputData) { checkInputsForPartialSig(this.inputs, 'addOutput'); @@ -170,7 +173,10 @@ class Psbt extends bip174_1.Psbt { }); return self.__TX.toBuffer(); }; - return super.addOutput(outputData, true, outputAdder); + super.addOutput(outputData, true, outputAdder); + this.__FEE_RATE = undefined; + this.__EXTRACTED_TX = undefined; + return this; } addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); diff --git a/test/psbt.js b/test/psbt.js index 67f3611..d65872c 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -158,7 +158,7 @@ describe(`Psbt`, () => { ECPair.fromWIF(f.shouldSign.WIF), ) }) - + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) assert.throws(() => { psbtThatShouldThrow.signInput( @@ -224,4 +224,29 @@ describe(`Psbt`, () => { }, {message: 'Input index too high'}) }) }) + + describe('Method return types', () => { + it('fromTransaction returns Psbt type (not base class)', () => { + const psbt = Psbt.fromTransaction(Buffer.from([2,0,0,0,0,0,0,0,0,0])); + assert.strictEqual(psbt instanceof Psbt, true); + assert.ok(psbt.__TX); + }) + it('fromBuffer returns Psbt type (not base class)', () => { + const psbt = Psbt.fromBuffer(Buffer.from( + '70736274ff01000a01000000000000000000000000', 'hex' //cHNidP8BAAoBAAAAAAAAAAAAAAAA + )); + assert.strictEqual(psbt instanceof Psbt, true); + assert.ok(psbt.__TX); + }) + it('fromBase64 returns Psbt type (not base class)', () => { + const psbt = Psbt.fromBase64('cHNidP8BAAoBAAAAAAAAAAAAAAAA'); + assert.strictEqual(psbt instanceof Psbt, true); + assert.ok(psbt.__TX); + }) + it('fromHex returns Psbt type (not base class)', () => { + const psbt = Psbt.fromHex('70736274ff01000a01000000000000000000000000'); + assert.strictEqual(psbt instanceof Psbt, true); + assert.ok(psbt.__TX); + }) + }) }) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index bd6aeb4..73ffe33 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -180,7 +180,10 @@ export class Psbt extends PsbtBase { }); return self.__TX.toBuffer(); }; - return super.addInput(inputData, inputAdder); + super.addInput(inputData, inputAdder); + this.__FEE_RATE = undefined; + this.__EXTRACTED_TX = undefined; + return this; } addOutput(outputData: TransactionOutput): this { @@ -211,7 +214,10 @@ export class Psbt extends PsbtBase { }); return self.__TX.toBuffer(); }; - return super.addOutput(outputData, true, outputAdder); + super.addOutput(outputData, true, outputAdder); + this.__FEE_RATE = undefined; + this.__EXTRACTED_TX = undefined; + return this; } addNonWitnessUtxoToInput( From 93e1661c6c7c1557203e6ff3e379b8f128fd0ea6 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 14:30:08 +0900 Subject: [PATCH 062/111] Remove need for ts-ignore --- src/psbt.js | 65 +++++++++++++++++++-------------------------- ts_src/psbt.ts | 70 ++++++++++++++++++++++--------------------------- types/psbt.d.ts | 3 +-- 3 files changed, 60 insertions(+), 78 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index ba42917..aa2adf2 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -13,8 +13,10 @@ const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { constructor(opts = {}) { super(); - this.__NON_WITNESS_UTXO_TX_CACHE = []; - this.__NON_WITNESS_UTXO_BUF_CACHE = []; + this.__NON_WITNESS_UTXO_CACHE = { + __NON_WITNESS_UTXO_TX_CACHE: [], + __NON_WITNESS_UTXO_BUF_CACHE: [], + }; // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); this.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); @@ -46,8 +48,7 @@ class Psbt extends bip174_1.Psbt { dpew(this, '__EXTRACTED_TX', false, true); dpew(this, '__FEE_RATE', false, true); dpew(this, '__TX_BUF_CACHE', false, true); - dpew(this, '__NON_WITNESS_UTXO_TX_CACHE', false, true); - dpew(this, '__NON_WITNESS_UTXO_BUF_CACHE', false, true); + dpew(this, '__NON_WITNESS_UTXO_CACHE', false, true); dpew(this, 'opts', false, true); } static fromTransaction(txBuf) { @@ -181,7 +182,7 @@ class Psbt extends bip174_1.Psbt { addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); const input = this.inputs[inputIndex]; - addNonWitnessTxCache(this, input, inputIndex); + addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, inputIndex); return this; } extractTransaction(disableFeeCheck) { @@ -237,12 +238,12 @@ class Psbt extends bip174_1.Psbt { if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { - // @ts-ignore - if (!this.__NON_WITNESS_UTXO_TX_CACHE[idx]) { - addNonWitnessTxCache(this, input, idx); + const cache = this.__NON_WITNESS_UTXO_CACHE; + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[idx]) { + addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, idx); } const vout = this.__TX.ins[idx].index; - const out = this.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout]; + const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout]; inputAmount += out.value; } else { throw new Error('Missing input value: index #' + idx); @@ -271,7 +272,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, input, this.__TX, - this, + this.__NON_WITNESS_UTXO_CACHE, ); if (!script) return false; const scriptType = classifyScript(script); @@ -300,7 +301,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__TX, - this, + this.__NON_WITNESS_UTXO_CACHE, ); const partialSig = { pubkey: keyPair.publicKey, @@ -317,7 +318,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__TX, - this, + this.__NON_WITNESS_UTXO_CACHE, ); Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = { @@ -335,34 +336,26 @@ const DEFAULT_OPTS = { network: networks_1.bitcoin, maximumFeeRate: 5000, }; -function addNonWitnessTxCache(psbt, input, inputIndex) { - // @ts-ignore - psbt.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; +function addNonWitnessTxCache(cache, input, inputIndex) { + cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; const tx = transaction_1.Transaction.fromBuffer(input.nonWitnessUtxo); - // @ts-ignore - psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; - const self = psbt; + cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; + const self = cache; const selfIndex = inputIndex; delete input.nonWitnessUtxo; Object.defineProperty(input, 'nonWitnessUtxo', { enumerable: true, get() { - // @ts-ignore if (self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] !== undefined) { - // @ts-ignore return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; } else { - // @ts-ignore self.__NON_WITNESS_UTXO_BUF_CACHE[ selfIndex - // @ts-ignore ] = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex].toBuffer(); - // @ts-ignore return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; } }, set(data) { - // @ts-ignore self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; }, }); @@ -370,13 +363,13 @@ function addNonWitnessTxCache(psbt, input, inputIndex) { function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx, psbt) { +function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx, cache) { const input = utils_1.checkForInput(inputs, inputIndex); const { hash, sighashType, script } = getHashForSig( inputIndex, input, unsignedTx, - psbt, + cache, ); checkScriptForPubkey(pubkey, script); return { @@ -495,18 +488,16 @@ function checkScriptForPubkey(pubkey, script) { ); } } -const getHashForSig = (inputIndex, input, unsignedTx, psbt) => { +const getHashForSig = (inputIndex, input, unsignedTx, cache) => { const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; let hash; let script; if (input.nonWitnessUtxo) { - // @ts-ignore - if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(psbt, input, inputIndex); + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); } - // @ts-ignore - const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout @@ -617,7 +608,7 @@ const classifyScript = script => { if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; }; -function getScriptFromInput(inputIndex, input, unsignedTx, psbt) { +function getScriptFromInput(inputIndex, input, unsignedTx, cache) { const res = { script: null, isSegwit: false, @@ -629,12 +620,10 @@ function getScriptFromInput(inputIndex, input, unsignedTx, psbt) { res.isP2SH = true; res.script = input.redeemScript; } else { - // @ts-ignore - if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(psbt, input, inputIndex); + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); } - // @ts-ignore - const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 73ffe33..e6c96f2 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -64,12 +64,14 @@ export class Psbt extends PsbtBase { psbt.__TX = tx!; return psbt as InstanceType; } + private __NON_WITNESS_UTXO_CACHE = { + __NON_WITNESS_UTXO_TX_CACHE: [] as Transaction[], + __NON_WITNESS_UTXO_BUF_CACHE: [] as Buffer[], + }; private __TX: Transaction; private __TX_BUF_CACHE?: Buffer; private __FEE_RATE?: number; private __EXTRACTED_TX?: Transaction; - private __NON_WITNESS_UTXO_TX_CACHE: Transaction[] = []; - private __NON_WITNESS_UTXO_BUF_CACHE: Buffer[] = []; private opts: PsbtOpts; constructor(opts: PsbtOptsOptional = {}) { super(); @@ -111,8 +113,7 @@ export class Psbt extends PsbtBase { dpew(this, '__EXTRACTED_TX', false, true); dpew(this, '__FEE_RATE', false, true); dpew(this, '__TX_BUF_CACHE', false, true); - dpew(this, '__NON_WITNESS_UTXO_TX_CACHE', false, true); - dpew(this, '__NON_WITNESS_UTXO_BUF_CACHE', false, true); + dpew(this, '__NON_WITNESS_UTXO_CACHE', false, true); dpew(this, 'opts', false, true); } @@ -226,7 +227,7 @@ export class Psbt extends PsbtBase { ): this { super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); const input = this.inputs[inputIndex]; - addNonWitnessTxCache(this, input, inputIndex); + addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, inputIndex); return this; } @@ -284,12 +285,12 @@ export class Psbt extends PsbtBase { if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { - // @ts-ignore - if (!this.__NON_WITNESS_UTXO_TX_CACHE[idx]) { - addNonWitnessTxCache(this, input, idx); + const cache = this.__NON_WITNESS_UTXO_CACHE; + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[idx]) { + addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, idx); } const vout = this.__TX.ins[idx].index; - const out = this.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout] as Output; + const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout] as Output; inputAmount += out.value; } else { throw new Error('Missing input value: index #' + idx); @@ -326,7 +327,7 @@ export class Psbt extends PsbtBase { inputIndex, input, this.__TX, - this, + this.__NON_WITNESS_UTXO_CACHE, ); if (!script) return false; @@ -360,7 +361,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__TX, - this, + this.__NON_WITNESS_UTXO_CACHE, ); const partialSig = { @@ -381,7 +382,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__TX, - this, + this.__NON_WITNESS_UTXO_CACHE, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -408,6 +409,11 @@ export class Psbt extends PsbtBase { // // +interface NonWitnessUtxoCache { + __NON_WITNESS_UTXO_TX_CACHE: Transaction[]; + __NON_WITNESS_UTXO_BUF_CACHE: Buffer[]; +} + interface PsbtOptsOptional { network?: Network; maximumFeeRate?: number; @@ -424,39 +430,31 @@ const DEFAULT_OPTS = { }; function addNonWitnessTxCache( - psbt: Psbt, + cache: NonWitnessUtxoCache, input: PsbtInput, inputIndex: number, ): void { - // @ts-ignore - psbt.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!; + cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!; const tx = Transaction.fromBuffer(input.nonWitnessUtxo!); - // @ts-ignore - psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; + cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; - const self = psbt; + const self = cache; const selfIndex = inputIndex; delete input.nonWitnessUtxo; Object.defineProperty(input, 'nonWitnessUtxo', { enumerable: true, get(): Buffer { - // @ts-ignore if (self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] !== undefined) { - // @ts-ignore return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; } else { - // @ts-ignore self.__NON_WITNESS_UTXO_BUF_CACHE[ selfIndex - // @ts-ignore ] = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex].toBuffer(); - // @ts-ignore return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; } }, set(data: Buffer): void { - // @ts-ignore self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; }, }); @@ -471,7 +469,7 @@ function getHashAndSighashType( inputIndex: number, pubkey: Buffer, unsignedTx: Transaction, - psbt: Psbt, + cache: NonWitnessUtxoCache, ): { hash: Buffer; sighashType: number; @@ -481,7 +479,7 @@ function getHashAndSighashType( inputIndex, input, unsignedTx, - psbt, + cache, ); checkScriptForPubkey(pubkey, script); return { @@ -632,19 +630,17 @@ const getHashForSig = ( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, - psbt: Psbt, + cache: NonWitnessUtxoCache, ): HashForSigData => { const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; let script: Buffer; if (input.nonWitnessUtxo) { - // @ts-ignore - if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(psbt, input, inputIndex); + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); } - // @ts-ignore - const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); @@ -784,7 +780,7 @@ function getScriptFromInput( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, - psbt: Psbt, + cache: NonWitnessUtxoCache, ): GetScriptReturn { const res: GetScriptReturn = { script: null, @@ -797,12 +793,10 @@ function getScriptFromInput( res.isP2SH = true; res.script = input.redeemScript; } else { - // @ts-ignore - if (!psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(psbt, input, inputIndex); + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); } - // @ts-ignore - const nonWitnessUtxoTx = psbt.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } diff --git a/types/psbt.d.ts b/types/psbt.d.ts index af234d8..fae4fff 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -7,12 +7,11 @@ import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { static fromTransaction(this: T, txBuf: Buffer): InstanceType; static fromBuffer(this: T, buffer: Buffer): InstanceType; + private __NON_WITNESS_UTXO_CACHE; private __TX; private __TX_BUF_CACHE?; private __FEE_RATE?; private __EXTRACTED_TX?; - private __NON_WITNESS_UTXO_TX_CACHE; - private __NON_WITNESS_UTXO_BUF_CACHE; private opts; constructor(opts?: PsbtOptsOptional); setMaximumFeeRate(satoshiPerByte: number): void; From 8d52ce1668bc1ee73d56cdfc4bbf2b5e23fa653c Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 16:42:13 +0900 Subject: [PATCH 063/111] Add some tests and an input duplicate checker --- src/psbt.js | 54 ++++++++++++------ test/fixtures/psbt.json | 31 +++++++++++ test/psbt.js | 118 ++++++++++++++++++++++++++++++++++++++-- ts_src/psbt.ts | 58 +++++++++++++++----- types/psbt.d.ts | 3 +- 5 files changed, 228 insertions(+), 36 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index aa2adf2..e797949 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -13,9 +13,10 @@ const varuint = require('varuint-bitcoin'); class Psbt extends bip174_1.Psbt { constructor(opts = {}) { super(); - this.__NON_WITNESS_UTXO_CACHE = { + this.__CACHE = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], + __TX_IN_CACHE: {}, }; // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); @@ -48,7 +49,7 @@ class Psbt extends bip174_1.Psbt { dpew(this, '__EXTRACTED_TX', false, true); dpew(this, '__FEE_RATE', false, true); dpew(this, '__TX_BUF_CACHE', false, true); - dpew(this, '__NON_WITNESS_UTXO_CACHE', false, true); + dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } static fromTransaction(txBuf) { @@ -56,6 +57,7 @@ class Psbt extends bip174_1.Psbt { checkTxEmpty(tx); const psbt = new this(); psbt.__TX = tx; + checkTxForDupeIns(tx, psbt.__CACHE); let inputCount = tx.ins.length; let outputCount = tx.outs.length; while (inputCount > 0) { @@ -84,8 +86,12 @@ class Psbt extends bip174_1.Psbt { }; const psbt = super.fromBuffer(buffer, txCountGetter); psbt.__TX = tx; + checkTxForDupeIns(tx, psbt.__CACHE); return psbt; } + get inputCount() { + return this.inputs.length; + } setMaximumFeeRate(satoshiPerByte) { check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw this.opts.maximumFeeRate = satoshiPerByte; @@ -134,14 +140,17 @@ class Psbt extends bip174_1.Psbt { const prevHash = Buffer.isBuffer(_inputData.hash) ? _inputData.hash : bufferutils_1.reverseBuffer(Buffer.from(_inputData.hash, 'hex')); - self.__TX.ins.push({ - hash: prevHash, - index: _inputData.index, - script: Buffer.alloc(0), - sequence: - _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, - witness: [], - }); + // Check if input already exists in cache. + const input = { hash: prevHash, index: _inputData.index }; + checkTxInputCache(self.__CACHE, input); + self.__TX.ins.push( + Object.assign({}, input, { + script: Buffer.alloc(0), + sequence: + _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, + witness: [], + }), + ); return self.__TX.toBuffer(); }; super.addInput(inputData, inputAdder); @@ -182,7 +191,7 @@ class Psbt extends bip174_1.Psbt { addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); const input = this.inputs[inputIndex]; - addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, inputIndex); + addNonWitnessTxCache(this.__CACHE, input, inputIndex); return this; } extractTransaction(disableFeeCheck) { @@ -238,9 +247,9 @@ class Psbt extends bip174_1.Psbt { if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { - const cache = this.__NON_WITNESS_UTXO_CACHE; + const cache = this.__CACHE; if (!cache.__NON_WITNESS_UTXO_TX_CACHE[idx]) { - addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, idx); + addNonWitnessTxCache(this.__CACHE, input, idx); } const vout = this.__TX.ins[idx].index; const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout]; @@ -272,7 +281,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, input, this.__TX, - this.__NON_WITNESS_UTXO_CACHE, + this.__CACHE, ); if (!script) return false; const scriptType = classifyScript(script); @@ -301,7 +310,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__TX, - this.__NON_WITNESS_UTXO_CACHE, + this.__CACHE, ); const partialSig = { pubkey: keyPair.publicKey, @@ -318,7 +327,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__TX, - this.__NON_WITNESS_UTXO_CACHE, + this.__CACHE, ); Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = { @@ -360,6 +369,19 @@ function addNonWitnessTxCache(cache, input, inputIndex) { }, }); } +function checkTxForDupeIns(tx, cache) { + tx.ins.forEach(input => { + checkTxInputCache(cache, input); + }); +} +function checkTxInputCache(cache, input) { + const key = + bufferutils_1.reverseBuffer(Buffer.from(input.hash)).toString('hex') + + ':' + + input.index; + if (cache.__TX_IN_CACHE[key]) throw new Error('Duplicate input detected.'); + cache.__TX_IN_CACHE[key] = 1; +} function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 00877f9..59a74cd 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -304,6 +304,37 @@ } ] }, + "addInput": { + "checks": [ + { + "description": "checks for hash and index", + "inputData": { + "hash": 42 + }, + "exception": "Error adding input." + }, + { + "description": "checks for hash and index", + "inputData": { + "hash": "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", + "index": 2 + }, + "equals": "cHNidP8BADMCAAAAAQABAgMEBQYHCAkKCwwNDg8AAQIDBAUGBwgJCgsMDQ4PAgAAAAD/////AAAAAAAAAAA=" + } + ] + }, + "addOutput": { + "checks": [ + { + "description": "checks for hash and index", + "outputData": { + "address": "1P2NFEBp32V2arRwZNww6tgXEV58FG94mr", + "value": "xyz" + }, + "exception": "Error adding output." + } + ] + }, "signInput": { "checks": [ { diff --git a/test/psbt.js b/test/psbt.js index d65872c..9a3ac92 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -21,8 +21,16 @@ const initBuffers = (attr, data) => { } else if (attr === 'bip32Derivation') { data.masterFingerprint = b(data.masterFingerprint) data.pubkey = b(data.pubkey) - } else if (attr === 'witnessUtxo') { + } else if (attr === 'witnessUtxo') { data.script = b(data.script) + } else if (attr === 'hash') { + if ( + typeof data === 'string' && + data.match(/^[0-9a-f]*$/i) && + data.length % 2 === 0 + ) { + data = b(data) + } } return data @@ -140,11 +148,55 @@ describe(`Psbt`, () => { fixtures.bip174.extractor.forEach(f => { it('Extracts the expected transaction from a PSBT', () => { - const psbt = Psbt.fromBase64(f.psbt) + const psbt1 = Psbt.fromBase64(f.psbt) + const transaction1 = psbt1.extractTransaction(true).toHex() - const transaction = psbt.extractTransaction().toHex() + const psbt2 = Psbt.fromBase64(f.psbt) + const transaction2 = psbt2.extractTransaction().toHex() - assert.strictEqual(transaction, f.transaction) + assert.strictEqual(transaction1, transaction2) + assert.strictEqual(transaction1, f.transaction) + + const psbt3 = Psbt.fromBase64(f.psbt) + delete psbt3.inputs[0].finalScriptSig + delete psbt3.inputs[0].finalScriptWitness + assert.throws(() => { + psbt3.extractTransaction() + }, new RegExp('Not finalized')) + + const psbt4 = Psbt.fromBase64(f.psbt) + psbt4.setMaximumFeeRate(1) + assert.throws(() => { + psbt4.extractTransaction() + }, new RegExp('Warning: You are paying around [\\d.]+ in fees')) + + const psbt5 = Psbt.fromBase64(f.psbt) + psbt5.extractTransaction(true) + const fr1 = psbt5.getFeeRate() + const fr2 = psbt5.getFeeRate() + assert.strictEqual(fr1, fr2) + }) + }) + + describe('signInputAsync', () => { + fixtures.signInput.checks.forEach(f => { + it(f.description, async () => { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotReject(async () => { + await psbtThatShouldsign.signInputAsync( + f.shouldSign.inputToCheck, + ECPair.fromWIF(f.shouldSign.WIF), + ) + }) + + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.rejects(async () => { + await psbtThatShouldThrow.signInputAsync( + f.shouldThrow.inputToCheck, + ECPair.fromWIF(f.shouldThrow.WIF), + ) + }, {message: f.shouldThrow.errorMessage}) + }) }) }) @@ -179,6 +231,54 @@ describe(`Psbt`, () => { }) }) + describe('addInput', () => { + fixtures.addInput.checks.forEach(f => { + for (const attr of Object.keys(f.inputData)) { + f.inputData[attr] = initBuffers(attr, f.inputData[attr]) + } + it(f.description, () => { + const psbt = new Psbt() + + if (f.exception) { + assert.throws(() => { + psbt.addInput(f.inputData) + }, new RegExp(f.exception)) + } else { + assert.doesNotThrow(() => { + psbt.addInput(f.inputData) + if (f.equals) { + assert.strictEqual(psbt.toBase64(), f.equals) + } else { + console.log(psbt.toBase64()) + } + }) + } + }) + }) + }) + + describe('addOutput', () => { + fixtures.addOutput.checks.forEach(f => { + for (const attr of Object.keys(f.outputData)) { + f.outputData[attr] = initBuffers(attr, f.outputData[attr]) + } + it(f.description, () => { + const psbt = new Psbt() + + if (f.exception) { + assert.throws(() => { + psbt.addOutput(f.outputData) + }, new RegExp(f.exception)) + } else { + assert.doesNotThrow(() => { + psbt.addOutput(f.outputData) + console.log(psbt.toBase64()) + }) + } + }) + }) + }) + describe('setVersion', () => { it('Sets the version value of the unsigned transaction', () => { const psbt = new Psbt() @@ -225,6 +325,16 @@ describe(`Psbt`, () => { }) }) + describe('setMaximumFeeRate', () => { + it('Sets the maximumFeeRate value', () => { + const psbt = new Psbt() + + assert.strictEqual(psbt.opts.maximumFeeRate, 5000) + psbt.setMaximumFeeRate(6000) + assert.strictEqual(psbt.opts.maximumFeeRate, 6000) + }) + }) + describe('Method return types', () => { it('fromTransaction returns Psbt type (not base class)', () => { const psbt = Psbt.fromTransaction(Buffer.from([2,0,0,0,0,0,0,0,0,0])); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index e6c96f2..24a088e 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -26,6 +26,7 @@ export class Psbt extends PsbtBase { checkTxEmpty(tx); const psbt = new this() as Psbt; psbt.__TX = tx; + checkTxForDupeIns(tx, psbt.__CACHE); let inputCount = tx.ins.length; let outputCount = tx.outs.length; while (inputCount > 0) { @@ -62,11 +63,13 @@ export class Psbt extends PsbtBase { }; const psbt = super.fromBuffer(buffer, txCountGetter) as Psbt; psbt.__TX = tx!; + checkTxForDupeIns(tx!, psbt.__CACHE); return psbt as InstanceType; } - private __NON_WITNESS_UTXO_CACHE = { + private __CACHE = { __NON_WITNESS_UTXO_TX_CACHE: [] as Transaction[], __NON_WITNESS_UTXO_BUF_CACHE: [] as Buffer[], + __TX_IN_CACHE: {} as { [index: string]: number }, }; private __TX: Transaction; private __TX_BUF_CACHE?: Buffer; @@ -113,10 +116,14 @@ export class Psbt extends PsbtBase { dpew(this, '__EXTRACTED_TX', false, true); dpew(this, '__FEE_RATE', false, true); dpew(this, '__TX_BUF_CACHE', false, true); - dpew(this, '__NON_WITNESS_UTXO_CACHE', false, true); + dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } + get inputCount(): number { + return this.inputs.length; + } + setMaximumFeeRate(satoshiPerByte: number): void { check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw this.opts.maximumFeeRate = satoshiPerByte; @@ -172,9 +179,13 @@ export class Psbt extends PsbtBase { const prevHash = Buffer.isBuffer(_inputData.hash) ? _inputData.hash : reverseBuffer(Buffer.from(_inputData.hash, 'hex')); + + // Check if input already exists in cache. + const input = { hash: prevHash, index: _inputData.index }; + checkTxInputCache(self.__CACHE, input); + self.__TX.ins.push({ - hash: prevHash, - index: _inputData.index, + ...input, script: Buffer.alloc(0), sequence: _inputData.sequence || Transaction.DEFAULT_SEQUENCE, witness: [], @@ -227,7 +238,7 @@ export class Psbt extends PsbtBase { ): this { super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); const input = this.inputs[inputIndex]; - addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, inputIndex); + addNonWitnessTxCache(this.__CACHE, input, inputIndex); return this; } @@ -285,9 +296,9 @@ export class Psbt extends PsbtBase { if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { - const cache = this.__NON_WITNESS_UTXO_CACHE; + const cache = this.__CACHE; if (!cache.__NON_WITNESS_UTXO_TX_CACHE[idx]) { - addNonWitnessTxCache(this.__NON_WITNESS_UTXO_CACHE, input, idx); + addNonWitnessTxCache(this.__CACHE, input, idx); } const vout = this.__TX.ins[idx].index; const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout] as Output; @@ -327,7 +338,7 @@ export class Psbt extends PsbtBase { inputIndex, input, this.__TX, - this.__NON_WITNESS_UTXO_CACHE, + this.__CACHE, ); if (!script) return false; @@ -361,7 +372,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__TX, - this.__NON_WITNESS_UTXO_CACHE, + this.__CACHE, ); const partialSig = { @@ -382,7 +393,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__TX, - this.__NON_WITNESS_UTXO_CACHE, + this.__CACHE, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -409,9 +420,10 @@ export class Psbt extends PsbtBase { // // -interface NonWitnessUtxoCache { +interface PsbtCache { __NON_WITNESS_UTXO_TX_CACHE: Transaction[]; __NON_WITNESS_UTXO_BUF_CACHE: Buffer[]; + __TX_IN_CACHE: { [index: string]: number }; } interface PsbtOptsOptional { @@ -430,7 +442,7 @@ const DEFAULT_OPTS = { }; function addNonWitnessTxCache( - cache: NonWitnessUtxoCache, + cache: PsbtCache, input: PsbtInput, inputIndex: number, ): void { @@ -460,6 +472,22 @@ function addNonWitnessTxCache( }); } +function checkTxForDupeIns(tx: Transaction, cache: PsbtCache): void { + tx.ins.forEach(input => { + checkTxInputCache(cache, input); + }); +} + +function checkTxInputCache( + cache: PsbtCache, + input: { hash: Buffer; index: number }, +): void { + const key = + reverseBuffer(Buffer.from(input.hash)).toString('hex') + ':' + input.index; + if (cache.__TX_IN_CACHE[key]) throw new Error('Duplicate input detected.'); + cache.__TX_IN_CACHE[key] = 1; +} + function isFinalized(input: PsbtInput): boolean { return !!input.finalScriptSig || !!input.finalScriptWitness; } @@ -469,7 +497,7 @@ function getHashAndSighashType( inputIndex: number, pubkey: Buffer, unsignedTx: Transaction, - cache: NonWitnessUtxoCache, + cache: PsbtCache, ): { hash: Buffer; sighashType: number; @@ -630,7 +658,7 @@ const getHashForSig = ( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, - cache: NonWitnessUtxoCache, + cache: PsbtCache, ): HashForSigData => { const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; @@ -780,7 +808,7 @@ function getScriptFromInput( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, - cache: NonWitnessUtxoCache, + cache: PsbtCache, ): GetScriptReturn { const res: GetScriptReturn = { script: null, diff --git a/types/psbt.d.ts b/types/psbt.d.ts index fae4fff..a708c21 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -7,13 +7,14 @@ import { Transaction } from './transaction'; export declare class Psbt extends PsbtBase { static fromTransaction(this: T, txBuf: Buffer): InstanceType; static fromBuffer(this: T, buffer: Buffer): InstanceType; - private __NON_WITNESS_UTXO_CACHE; + private __CACHE; private __TX; private __TX_BUF_CACHE?; private __FEE_RATE?; private __EXTRACTED_TX?; private opts; constructor(opts?: PsbtOptsOptional); + readonly inputCount: number; setMaximumFeeRate(satoshiPerByte: number): void; setVersion(version: number): this; setLocktime(locktime: number): this; From 5f26654802a5b487fc99a7e82f5fbc89c977845c Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 18:26:52 +0900 Subject: [PATCH 064/111] Add tests --- src/psbt.js | 20 +++++++++----------- test/psbt.js | 28 ++++++++++++++++++++++++++++ ts_src/psbt.ts | 22 ++++++++++------------ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index e797949..529f023 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -10,6 +10,10 @@ const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); const varuint = require('varuint-bitcoin'); +const DEFAULT_OPTS = { + network: networks_1.bitcoin, + maximumFeeRate: 5000, +}; class Psbt extends bip174_1.Psbt { constructor(opts = {}) { super(); @@ -202,11 +206,11 @@ class Psbt extends bip174_1.Psbt { const satoshis = feeRate * vsize; if (feeRate >= this.opts.maximumFeeRate) { throw new Error( - `Warning: You are paying around ${satoshis / 1e8} in fees, which ` + - `is ${feeRate} satoshi per byte for a transaction with a VSize of ` + - `${vsize} bytes (segwit counted as 0.25 byte per byte)\n` + - `Use setMaximumFeeRate method to raise your threshold, or pass ` + - `true to the first arg of extractTransaction.`, + `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, ); } } @@ -254,8 +258,6 @@ class Psbt extends bip174_1.Psbt { const vout = this.__TX.ins[idx].index; const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout]; inputAmount += out.value; - } else { - throw new Error('Missing input value: index #' + idx); } }); this.__EXTRACTED_TX = tx; @@ -341,10 +343,6 @@ class Psbt extends bip174_1.Psbt { } } exports.Psbt = Psbt; -const DEFAULT_OPTS = { - network: networks_1.bitcoin, - maximumFeeRate: 5000, -}; function addNonWitnessTxCache(cache, input, inputIndex) { cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; const tx = transaction_1.Transaction.fromBuffer(input.nonWitnessUtxo); diff --git a/test/psbt.js b/test/psbt.js index 9a3ac92..ecb78ab 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -97,6 +97,16 @@ describe(`Psbt`, () => { arg.forEach(a => adder(i, initBuffers(attr, a))) } else { adder(i, initBuffers(attr, arg)) + if (attr === 'nonWitnessUtxo') { + const first = psbt.inputs[i].nonWitnessUtxo + psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[i] = undefined + const second = psbt.inputs[i].nonWitnessUtxo + psbt.inputs[i].nonWitnessUtxo = Buffer.from([1,2,3]) + psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[i] = undefined + const third = psbt.inputs[i].nonWitnessUtxo + assert.ok(first.equals(second)) + assert.ok(first.equals(third)) + } } } } @@ -140,6 +150,10 @@ describe(`Psbt`, () => { it('Finalizes inputs and gives the expected PSBT', () => { const psbt = Psbt.fromBase64(f.psbt) + assert.throws(() => { + psbt.getFeeRate() + }, new RegExp('PSBT must be finalized to calculate fee rate')) + psbt.finalizeAllInputs() assert.strictEqual(psbt.toBase64(), f.result) @@ -196,6 +210,11 @@ describe(`Psbt`, () => { ECPair.fromWIF(f.shouldThrow.WIF), ) }, {message: f.shouldThrow.errorMessage}) + assert.rejects(async () => { + await psbtThatShouldThrow.signInputAsync( + f.shouldThrow.inputToCheck, + ) + }, new RegExp('Need Signer to sign input')) }) }) }) @@ -218,6 +237,11 @@ describe(`Psbt`, () => { ECPair.fromWIF(f.shouldThrow.WIF), ) }, {message: f.shouldThrow.errorMessage}) + assert.throws(() => { + psbtThatShouldThrow.signInput( + f.shouldThrow.inputToCheck, + ) + }, new RegExp('Need Signer to sign input')) }) }) }) @@ -252,6 +276,9 @@ describe(`Psbt`, () => { console.log(psbt.toBase64()) } }) + assert.throws(() => { + psbt.addInput(f.inputData) + }, new RegExp('Duplicate input detected.')) } }) }) @@ -307,6 +334,7 @@ describe(`Psbt`, () => { index: 0 }); + assert.strictEqual(psbt.inputCount, 1) assert.strictEqual(psbt.__TX.ins[0].sequence, 0xffffffff) psbt.setSequence(0, 0) assert.strictEqual(psbt.__TX.ins[0].sequence, 0) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 24a088e..9072033 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -17,6 +17,11 @@ import * as bscript from './script'; import { Output, Transaction } from './transaction'; const varuint = require('varuint-bitcoin'); +const DEFAULT_OPTS: PsbtOpts = { + network: btcNetwork, + maximumFeeRate: 5000, // satoshi per byte +}; + export class Psbt extends PsbtBase { static fromTransaction( this: T, @@ -250,11 +255,11 @@ export class Psbt extends PsbtBase { const satoshis = feeRate * vsize; if (feeRate >= this.opts.maximumFeeRate) { throw new Error( - `Warning: You are paying around ${satoshis / 1e8} in fees, which ` + - `is ${feeRate} satoshi per byte for a transaction with a VSize of ` + - `${vsize} bytes (segwit counted as 0.25 byte per byte)\n` + - `Use setMaximumFeeRate method to raise your threshold, or pass ` + - `true to the first arg of extractTransaction.`, + `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, ); } } @@ -303,8 +308,6 @@ export class Psbt extends PsbtBase { const vout = this.__TX.ins[idx].index; const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout] as Output; inputAmount += out.value; - } else { - throw new Error('Missing input value: index #' + idx); } }); this.__EXTRACTED_TX = tx; @@ -436,11 +439,6 @@ interface PsbtOpts { maximumFeeRate: number; } -const DEFAULT_OPTS = { - network: btcNetwork, - maximumFeeRate: 5000, // satoshi per byte -}; - function addNonWitnessTxCache( cache: PsbtCache, input: PsbtInput, From 02ba6c78d1e4827dd362cd400c343708bf36c0a1 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 19:40:31 +0900 Subject: [PATCH 065/111] Add integration tests with examples --- test/integration/transactions-psbt.js | 366 ++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 test/integration/transactions-psbt.js diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js new file mode 100644 index 0000000..e48e335 --- /dev/null +++ b/test/integration/transactions-psbt.js @@ -0,0 +1,366 @@ +const { describe, it } = require('mocha') +const assert = require('assert') +const bitcoin = require('../../') +const regtestUtils = require('./_regtest') +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 (and broadcast via 3PBP) a typical Transaction', async () => { + // these are { payment: Payment; keys: ECPair[] } + const alice1 = createPayment('p2pkh') + const alice2 = createPayment('p2pkh') + + // give Alice 2 unspent outputs + const inputData1 = await getInputData(5e4, alice1.payment, false, 'noredeem') + const inputData2 = await getInputData(7e4, alice2.payment, false, 'noredeem') + { + const { + hash, // string of txid or Buffer of tx hash. (txid and hash are reverse order) + index, // the output index of the txo you are spending + nonWitnessUtxo, // the full previous transaction as a Buffer + } = inputData1 + assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1) + } + + // network is only needed if you pass an address to addOutput + // using script (Buffer of scriptPubkey) instead will avoid needed network. + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData1) // alice1 unspent + .addInput(inputData2) // alice2 unspent + .addOutput({ + address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', + value: 8e4 + }) // the actual "spend" + .addOutput({ + address: alice2.payment.address, // OR script, which is a Buffer. + value: 1e4 + }) // Alice's change + // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee + + // Let's show a new feature with PSBT. + // We can have multiple signers sign in parrallel and combine them. + // (this is not necessary, but a nice feature) + + // encode to send out to the signers + const psbtBaseText = psbt.toBase64() + + // each signer imports + const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText) + const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText) + + // Alice signs each input with the respective private keys + signer1.signInput(0, alice1.keys[0]) + signer2.signInput(1, alice2.keys[0]) + + // encode to send back to combiner (signer 1 and 2 are not near each other) + const s1text = signer1.toBase64() + const s2text = signer2.toBase64() + + const final1 = bitcoin.Psbt.fromBase64(s1text) + const final2 = bitcoin.Psbt.fromBase64(s2text) + + // final1.combine(final2) would give the exact same result + psbt.combine(final1, final2) + + // 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() + // it returns an array of the success of each input, also a result attribute + // which is true if all array items are true. + + // build and broadcast our RegTest network + await regtestUtils.broadcast(psbt.extractTransaction().toHex()) + // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839 + }) + + it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => { + const alice1 = createPayment('p2pkh') + const inputData1 = await getInputData(2e5, alice1.payment, false, 'noredeem') + + const data = Buffer.from('bitcoinjs-lib', 'utf8') + const embed = bitcoin.payments.embed({ data: [data] }) + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData1) + .addOutput({ + script: embed.output, + value: 1000 + }) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 1e5 + }) + .signInput(0, alice1.keys[0]) + + psbt.finalizeAllInputs() + + // build and broadcast to the RegTest network + await regtestUtils.broadcast(psbt.extractTransaction().toHex()) + }) + + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => { + const multisig = createPayment('p2sh-p2ms(2 of 4)') + const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh') + { + const { + hash, + index, + nonWitnessUtxo, + redeemScript, // NEW: P2SH needs to give redeemScript when adding an input. + } = inputData1 + assert.deepStrictEqual({ hash, index, nonWitnessUtxo, redeemScript }, inputData1) + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData1) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 1e4 + }) + .signInput(0, multisig.keys[0]) + .signInput(0, multisig.keys[2]) + + psbt.finalizeAllInputs() + + const tx = psbt.extractTransaction() + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()) + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 1e4 + }) + }) + + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => { + const p2sh = createPayment('p2sh-p2wpkh') + const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh') + { + const { + hash, + index, + witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; } + redeemScript, + } = inputData + assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript }, inputData) + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4 + }) + .signInput(0, p2sh.keys[0]) + + psbt.finalizeAllInputs() + + const tx = psbt.extractTransaction() + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()) + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4 + }) + }) + + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { + + // the only thing that changes is you don't give a redeemscript for input data + + const p2wpkh = createPayment('p2wpkh') + const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem') + { + const { + hash, + index, + witnessUtxo, + } = inputData + assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData) + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4 + }) + .signInput(0, p2wpkh.keys[0]) + + psbt.finalizeAllInputs() + + const tx = psbt.extractTransaction() + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()) + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4 + }) + }) + + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => { + const p2wsh = createPayment('p2wsh-p2pk') + const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh') + { + const { + hash, + index, + witnessUtxo, + witnessScript, // NEW: A Buffer of the witnessScript + } = inputData + assert.deepStrictEqual({ hash, index, witnessUtxo, witnessScript }, inputData) + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4 + }) + .signInput(0, p2wsh.keys[0]) + + psbt.finalizeAllInputs() + + const tx = psbt.extractTransaction() + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()) + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4 + }) + }) + + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => { + const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)') + const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh') + { + const { + hash, + index, + witnessUtxo, + redeemScript, + witnessScript, + } = inputData + assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript, witnessScript }, inputData) + } + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4 + }) + .signInput(0, p2sh.keys[0]) + .signInput(0, p2sh.keys[2]) + .signInput(0, p2sh.keys[3]) + + psbt.finalizeAllInputs() + + const tx = psbt.extractTransaction() + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()) + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4 + }) + }) +}) + +function createPayment(_type) { + const splitType = _type.split('-').reverse(); + const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; + const keys = []; + let m; + if (isMultisig) { + const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/) + m = parseInt(match[1]) + let n = parseInt(match[2]) + while (n > 1) { + keys.push(bitcoin.ECPair.makeRandom({ network: regtest })); + n-- + } + } + keys.push(bitcoin.ECPair.makeRandom({ network: regtest })); + + let payment; + splitType.forEach(type => { + if (type.slice(0, 4) === 'p2ms') { + payment = bitcoin.payments.p2ms({ + m, + pubkeys: keys.map(key => key.publicKey).sort(), + network: regtest, + }); + } else if (['p2sh', 'p2wsh'].indexOf(type) > -1) { + payment = bitcoin.payments[type]({ + redeem: payment, + network: regtest, + }); + } else { + payment = bitcoin.payments[type]({ + pubkey: keys[0].publicKey, + network: regtest, + }); + } + }); + + return { + payment, + keys, + }; +} + +function getWitnessUtxo(out) { + delete out.address; + out.script = Buffer.from(out.script, 'hex'); + return out; +} + +async function getInputData(amount, payment, isSegwit, redeemType) { + const unspent = await regtestUtils.faucetComplex(payment.output, amount); + const utx = await regtestUtils.fetch(unspent.txId); + // for non segwit inputs, you must pass the full transaction buffer + const nonWitnessUtxo = Buffer.from(utx.txHex, 'hex'); + // for segwit inputs, you only need the output script and value as an object. + const witnessUtxo = getWitnessUtxo(utx.outs[unspent.vout]); + const mixin = isSegwit ? { witnessUtxo } : { nonWitnessUtxo }; + const mixin2 = {}; + switch (redeemType) { + case 'p2sh': + mixin2.redeemScript = payment.redeem.output; + break; + case 'p2wsh': + mixin2.witnessScript = payment.redeem.output; + break; + case 'p2sh-p2wsh': + mixin2.witnessScript = payment.redeem.redeem.output; + mixin2.redeemScript = payment.redeem.output; + break; + } + return { + hash: unspent.txId, + index: unspent.vout, + ...mixin, + ...mixin2, + }; +} From d0d94c7f06715b74b6447339889b5dbe3bc4d235 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 8 Jul 2019 15:46:06 +0900 Subject: [PATCH 066/111] Add signature verify method --- src/psbt.js | 40 +++++++++++- test/integration/transactions-psbt.js | 92 +++++++++++++++++++++++++-- ts_src/psbt.ts | 50 +++++++++++++-- types/psbt.d.ts | 1 + 4 files changed, 170 insertions(+), 13 deletions(-) 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; } From f66b568e4d86c86368b89cb470b96e650c56f499 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 8 Jul 2019 16:30:59 +0900 Subject: [PATCH 067/111] Add sign all inputs method --- src/psbt.js | 49 ++++++++++++++++++++++++ test/integration/transactions-psbt.js | 9 ++++- ts_src/psbt.ts | 55 +++++++++++++++++++++++++++ types/psbt.d.ts | 2 + 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 3bb1b3f..d89752b 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -338,6 +338,55 @@ class Psbt extends bip174_1.Psbt { } return results.every(res => res === true); } + sign(keyPair) { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + // TODO: Add a pubkey/pubkeyhash cache to each input + // as input information is added, then eventually + // optimize this method. + const results = []; + for (const [i] of this.inputs.entries()) { + try { + this.signInput(i, keyPair); + results.push(true); + } catch (err) { + results.push(false); + } + } + if (results.every(v => v === false)) { + throw new Error('No inputs were signed'); + } + return this; + } + signAsync(keyPair) { + return new Promise((resolve, reject) => { + if (!keyPair || !keyPair.publicKey) + return reject(new Error('Need Signer to sign input')); + // TODO: Add a pubkey/pubkeyhash cache to each input + // as input information is added, then eventually + // optimize this method. + const results = []; + const promises = []; + for (const [i] of this.inputs.entries()) { + promises.push( + this.signInputAsync(i, keyPair).then( + () => { + results.push(true); + }, + () => { + results.push(false); + }, + ), + ); + } + return Promise.all(promises).then(() => { + if (results.every(v => v === false)) { + return reject(new Error('No inputs were signed')); + } + resolve(); + }); + }); + } signInput(inputIndex, keyPair) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index 9fb76f7..37348c1 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -110,8 +110,13 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText) // Alice signs each input with the respective private keys - signer1.signInput(0, alice1.keys[0]) - signer2.signInput(1, alice2.keys[0]) + // signInput and signInputAsync are better + // (They take the input index explicitly as the first arg) + signer1.sign(alice1.keys[0]) + signer2.sign(alice2.keys[0]) + + // If your signer object's sign method returns a promise, use the following + // await signer2.signAsync(alice2.keys[0]) // encode to send back to combiner (signer 1 and 2 are not near each other) const s1text = signer1.toBase64() diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 6ccef08..3461b56 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -405,6 +405,61 @@ export class Psbt extends PsbtBase { return results.every(res => res === true); } + sign(keyPair: Signer): this { + if (!keyPair || !keyPair.publicKey) + throw new Error('Need Signer to sign input'); + + // TODO: Add a pubkey/pubkeyhash cache to each input + // as input information is added, then eventually + // optimize this method. + const results: boolean[] = []; + for (const [i] of this.inputs.entries()) { + try { + this.signInput(i, keyPair); + results.push(true); + } catch (err) { + results.push(false); + } + } + if (results.every(v => v === false)) { + throw new Error('No inputs were signed'); + } + return this; + } + + signAsync(keyPair: SignerAsync): Promise { + return new Promise( + (resolve, reject): any => { + if (!keyPair || !keyPair.publicKey) + return reject(new Error('Need Signer to sign input')); + + // TODO: Add a pubkey/pubkeyhash cache to each input + // as input information is added, then eventually + // optimize this method. + const results: boolean[] = []; + const promises: Array> = []; + for (const [i] of this.inputs.entries()) { + promises.push( + this.signInputAsync(i, keyPair).then( + () => { + results.push(true); + }, + () => { + results.push(false); + }, + ), + ); + } + return Promise.all(promises).then(() => { + if (results.every(v => v === false)) { + return reject(new Error('No inputs were signed')); + } + resolve(); + }); + }, + ); + } + signInput(inputIndex: number, keyPair: Signer): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 1f9ee7b..b2893b8 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -30,6 +30,8 @@ export declare class Psbt extends PsbtBase { }; finalizeInput(inputIndex: number): boolean; validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; + sign(keyPair: Signer): this; + signAsync(keyPair: SignerAsync): Promise; signInput(inputIndex: number, keyPair: Signer): this; signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise; } From e15b51536792659f1afb113379244514af0c0699 Mon Sep 17 00:00:00 2001 From: junderw Date: Mon, 8 Jul 2019 17:40:21 +0900 Subject: [PATCH 068/111] Add tests --- test/psbt.js | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/psbt.js b/test/psbt.js index ecb78ab..cd45c1e 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -154,6 +154,20 @@ describe(`Psbt`, () => { psbt.getFeeRate() }, new RegExp('PSBT must be finalized to calculate fee rate')) + const pubkey = Buffer.from( + '029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f', + 'hex', + ) + assert.strictEqual(psbt.validateSignatures(0), true) + assert.strictEqual(psbt.validateSignatures(0, pubkey), true) + assert.throws(() => { + pubkey[32] = 42 + psbt.validateSignatures(0, pubkey) + }, new RegExp('No signatures for this pubkey')) + assert.throws(() => { + psbt.validateSignatures(42) + }, new RegExp('No signatures to validate')) + psbt.finalizeAllInputs() assert.strictEqual(psbt.toBase64(), f.result) @@ -246,6 +260,54 @@ describe(`Psbt`, () => { }) }) + describe('signAsync', () => { + fixtures.signInput.checks.forEach(f => { + if (f.description === 'checks the input exists') return + it(f.description, async () => { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotReject(async () => { + await psbtThatShouldsign.signAsync( + ECPair.fromWIF(f.shouldSign.WIF), + ) + }) + + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.rejects(async () => { + await psbtThatShouldThrow.signAsync( + ECPair.fromWIF(f.shouldThrow.WIF), + ) + }, new RegExp('No inputs were signed')) + assert.rejects(async () => { + await psbtThatShouldThrow.signAsync() + }, new RegExp('Need Signer to sign input')) + }) + }) + }) + + describe('sign', () => { + fixtures.signInput.checks.forEach(f => { + if (f.description === 'checks the input exists') return + it(f.description, () => { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotThrow(() => { + psbtThatShouldsign.sign( + ECPair.fromWIF(f.shouldSign.WIF), + ) + }) + + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.throws(() => { + psbtThatShouldThrow.sign( + ECPair.fromWIF(f.shouldThrow.WIF), + ) + }, new RegExp('No inputs were signed')) + assert.throws(() => { + psbtThatShouldThrow.sign() + }, new RegExp('Need Signer to sign input')) + }) + }) + }) + describe('fromTransaction', () => { fixtures.fromTransaction.forEach(f => { it('Creates the expected PSBT from a transaction buffer', () => { @@ -363,6 +425,44 @@ describe(`Psbt`, () => { }) }) + describe('create 1-to-1 transaction', () => { + const alice = ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr') + const psbt = new Psbt() + psbt.addInput({ + hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e', + index: 0, + nonWitnessUtxo: Buffer.from( + '0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' + + '452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' + + 'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' + + '9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' + + '631e5e1e66009ce3710ceea5b1ad13ffffffff01905f0100000000001976a9148bb' + + 'c95d2709c71607c60ee3f097c1217482f518d88ac00000000', + 'hex', + ), + sighashType: 1, + }) + psbt.addOutput({ + address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', + value: 80000 + }) + psbt.signInput(0, alice) + assert.throws(() => { + psbt.setVersion(3) + }, new RegExp('Can not modify transaction, signatures exist.')) + psbt.validateSignatures(0) + psbt.finalizeAllInputs() + assert.strictEqual( + psbt.extractTransaction().toHex(), + '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + + 'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' + + 'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' + + '9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' + + 'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' + + '08a22724efa6f6a07b0ec4c79aa88ac00000000', + ) + }) + describe('Method return types', () => { it('fromTransaction returns Psbt type (not base class)', () => { const psbt = Psbt.fromTransaction(Buffer.from([2,0,0,0,0,0,0,0,0,0])); From 09fcb1c6ee85d13a09c5c877020c67fc77b8b280 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 10:57:41 +0900 Subject: [PATCH 069/111] Use function keyword --- src/psbt.js | 60 ++++++++++++++++++++++--------------------- ts_src/psbt.ts | 70 ++++++++++++++++++++++++++------------------------ 2 files changed, 68 insertions(+), 62 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index d89752b..8ddf6ab 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -591,7 +591,7 @@ function checkScriptForPubkey(pubkey, script, action) { ); } } -const getHashForSig = (inputIndex, input, unsignedTx, cache) => { +function getHashForSig(inputIndex, input, unsignedTx, cache) { const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; let hash; @@ -672,45 +672,45 @@ const getHashForSig = (inputIndex, input, unsignedTx, cache) => { sighashType, hash, }; -}; -const scriptCheckerFactory = (payment, paymentScriptName) => ( - inputIndex, - scriptPubKey, - redeemScript, -) => { - 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`, - ); - } -}; +} +function scriptCheckerFactory(payment, paymentScriptName) { + return (inputIndex, scriptPubKey, redeemScript) => { + 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`, + ); + } + }; +} const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); const checkWitnessScript = scriptCheckerFactory( payments.p2wsh, 'Witness script', ); -const isPaymentFactory = payment => script => { - try { - payment({ output: script }); - return true; - } catch (err) { - return false; - } -}; +function isPaymentFactory(payment) { + return script => { + try { + payment({ output: script }); + return true; + } catch (err) { + return false; + } + }; +} const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2MS = isPaymentFactory(payments.p2ms); const isP2PK = isPaymentFactory(payments.p2pk); -const classifyScript = script => { +function classifyScript(script) { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; -}; +} function getScriptFromInput(inputIndex, input, unsignedTx, cache) { const res = { script: null, @@ -748,11 +748,11 @@ function getScriptFromInput(inputIndex, input, unsignedTx, cache) { } return res; } -const hasSigs = (neededSigs, partialSig) => { +function hasSigs(neededSigs, partialSig) { if (!partialSig) return false; if (partialSig.length > neededSigs) throw new Error('Too many signatures'); return partialSig.length === neededSigs; -}; +} function witnessStackToScriptWitness(witness) { let buffer = Buffer.allocUnsafe(0); function writeSlice(slice) { @@ -797,7 +797,9 @@ function scriptWitnessToWitnessStack(buffer) { } return readVector(); } -const range = n => [...Array(n).keys()]; +function range(n) { + return [...Array(n).keys()]; +} function checkTxEmpty(tx) { const isEmpty = tx.ins.every( input => diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 3461b56..122bba8 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -749,12 +749,12 @@ interface HashForSigData { sighashType: number; } -const getHashForSig = ( +function getHashForSig( inputIndex: number, input: PsbtInput, unsignedTx: Transaction, cache: PsbtCache, -): HashForSigData => { +): HashForSigData { const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; let script: Buffer; @@ -839,28 +839,30 @@ const getHashForSig = ( sighashType, hash, }; -}; +} type ScriptCheckerFunction = (idx: number, spk: Buffer, rs: Buffer) => void; -const scriptCheckerFactory = ( +function scriptCheckerFactory( payment: any, paymentScriptName: string, -): ScriptCheckerFunction => ( - inputIndex: number, - scriptPubKey: Buffer, - redeemScript: Buffer, -): void => { - const redeemScriptOutput = payment({ - redeem: { output: redeemScript }, - }).output as Buffer; +): ScriptCheckerFunction { + return ( + inputIndex: number, + scriptPubKey: Buffer, + redeemScript: Buffer, + ): void => { + const redeemScriptOutput = payment({ + redeem: { output: redeemScript }, + }).output as Buffer; - if (!scriptPubKey.equals(redeemScriptOutput)) { - throw new Error( - `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, - ); - } -}; + if (!scriptPubKey.equals(redeemScriptOutput)) { + throw new Error( + `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } + }; +} const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); const checkWitnessScript = scriptCheckerFactory( @@ -870,28 +872,28 @@ const checkWitnessScript = scriptCheckerFactory( type isPaymentFunction = (script: Buffer) => boolean; -const isPaymentFactory = (payment: any): isPaymentFunction => ( - script: Buffer, -): boolean => { - try { - payment({ output: script }); - return true; - } catch (err) { - return false; - } -}; +function isPaymentFactory(payment: any): isPaymentFunction { + return (script: Buffer): boolean => { + try { + payment({ output: script }); + return true; + } catch (err) { + return false; + } + }; +} const isP2WPKH = isPaymentFactory(payments.p2wpkh); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2MS = isPaymentFactory(payments.p2ms); const isP2PK = isPaymentFactory(payments.p2pk); -const classifyScript = (script: Buffer): string => { +function classifyScript(script: Buffer): string { if (isP2WPKH(script)) return 'witnesspubkeyhash'; if (isP2PKH(script)) return 'pubkeyhash'; if (isP2MS(script)) return 'multisig'; if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; -}; +} interface GetScriptReturn { script: Buffer | null; @@ -942,11 +944,11 @@ function getScriptFromInput( return res; } -const hasSigs = (neededSigs: number, partialSig?: any[]): boolean => { +function hasSigs(neededSigs: number, partialSig?: any[]): boolean { if (!partialSig) return false; if (partialSig.length > neededSigs) throw new Error('Too many signatures'); return partialSig.length === neededSigs; -}; +} function witnessStackToScriptWitness(witness: Buffer[]): Buffer { let buffer = Buffer.allocUnsafe(0); @@ -1006,7 +1008,9 @@ function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { return readVector(); } -const range = (n: number): number[] => [...Array(n).keys()]; +function range(n: number): number[] { + return [...Array(n).keys()]; +} function checkTxEmpty(tx: Transaction): void { const isEmpty = tx.ins.every( From 36a966cfcdafd9245d60e31d26c619981d0240fa Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 11:06:39 +0900 Subject: [PATCH 070/111] Check actual sighash flags instead of psbtInput one --- src/psbt.js | 43 +++++++++++++++++++------------------------ ts_src/psbt.ts | 42 ++++++++++++++++++------------------------ 2 files changed, 37 insertions(+), 48 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 8ddf6ab..c3a47ab 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -815,32 +815,27 @@ function checkTxEmpty(tx) { function checkInputsForPartialSig(inputs, action) { inputs.forEach(input => { let throws = false; - if ((input.partialSig || []).length > 0) { - if (input.sighashType !== undefined) { - const whitelist = []; - const isAnyoneCanPay = - input.sighashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; - if (isAnyoneCanPay) whitelist.push('addInput'); - if (!isAnyoneCanPay && action === 'addInput') { - throws = true; - } - const hashType = input.sighashType & 0x1f; - switch (hashType) { - case transaction_1.Transaction.SIGHASH_ALL: - break; - case transaction_1.Transaction.SIGHASH_SINGLE: - case transaction_1.Transaction.SIGHASH_NONE: - whitelist.push('addOutput'); - whitelist.push('setSequence'); - break; - } - if (whitelist.indexOf(action) === -1) { - throws = true; - } - } else { + if ((input.partialSig || []).length === 0) return; + input.partialSig.forEach(pSig => { + const { hashType } = bscript.signature.decode(pSig.signature); + const whitelist = []; + const isAnyoneCanPay = + hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + const hashMod = hashType & 0x1f; + switch (hashMod) { + case transaction_1.Transaction.SIGHASH_ALL: + break; + case transaction_1.Transaction.SIGHASH_SINGLE: + case transaction_1.Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setSequence'); + break; + } + if (whitelist.indexOf(action) === -1) { throws = true; } - } + }); if (throws) { throw new Error('Can not modify transaction, signatures exist.'); } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 122bba8..9e45c83 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1028,32 +1028,26 @@ function checkTxEmpty(tx: Transaction): void { function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { inputs.forEach(input => { let throws = false; - if ((input.partialSig || []).length > 0) { - if (input.sighashType !== undefined) { - const whitelist: string[] = []; - const isAnyoneCanPay = - input.sighashType & Transaction.SIGHASH_ANYONECANPAY; - if (isAnyoneCanPay) whitelist.push('addInput'); - if (!isAnyoneCanPay && action === 'addInput') { - throws = true; - } - const hashType = input.sighashType & 0x1f; - switch (hashType) { - case Transaction.SIGHASH_ALL: - break; - case Transaction.SIGHASH_SINGLE: - case Transaction.SIGHASH_NONE: - whitelist.push('addOutput'); - whitelist.push('setSequence'); - break; - } - if (whitelist.indexOf(action) === -1) { - throws = true; - } - } else { + if ((input.partialSig || []).length === 0) return; + input.partialSig!.forEach(pSig => { + const { hashType } = bscript.signature.decode(pSig.signature); + const whitelist: string[] = []; + const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + const hashMod = hashType & 0x1f; + switch (hashMod) { + case Transaction.SIGHASH_ALL: + break; + case Transaction.SIGHASH_SINGLE: + case Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setSequence'); + break; + } + if (whitelist.indexOf(action) === -1) { throws = true; } - } + }); if (throws) { throw new Error('Can not modify transaction, signatures exist.'); } From 88de1e7b0e140612a733c90cc2303c0e127d39be Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 11:29:20 +0900 Subject: [PATCH 071/111] Refactor: nonWitnessUtxo cache --- src/psbt.js | 44 +++++++++++++++++++++++++------------------- ts_src/psbt.ts | 49 ++++++++++++++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index c3a47ab..fdb457f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -252,12 +252,9 @@ class Psbt extends bip174_1.Psbt { if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { - const cache = this.__CACHE; - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[idx]) { - addNonWitnessTxCache(this.__CACHE, input, idx); - } + const nwTx = nonWitnessUtxoTxFromCache(this.__CACHE, input, idx); const vout = this.__TX.ins[idx].index; - const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout]; + const out = nwTx.outs[vout]; inputAmount += out.value; } }); @@ -436,13 +433,14 @@ function addNonWitnessTxCache(cache, input, inputIndex) { Object.defineProperty(input, 'nonWitnessUtxo', { enumerable: true, get() { - if (self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] !== undefined) { - return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; + if (buf !== undefined) { + return buf; } else { - self.__NON_WITNESS_UTXO_BUF_CACHE[ - selfIndex - ] = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex].toBuffer(); - return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const newBuf = txCache.toBuffer(); + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; + return newBuf; } }, set(data) { @@ -597,10 +595,11 @@ function getHashForSig(inputIndex, input, unsignedTx, cache) { let hash; let script; if (input.nonWitnessUtxo) { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(cache, input, inputIndex); - } - const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); // If a non-witness UTXO is provided, its hash must match the hash specified in the prevout @@ -723,10 +722,11 @@ function getScriptFromInput(inputIndex, input, unsignedTx, cache) { res.isP2SH = true; res.script = input.redeemScript; } else { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(cache, input, inputIndex); - } - const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } @@ -841,6 +841,12 @@ function checkInputsForPartialSig(inputs, action) { } }); } +function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); + } + return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; +} function check32Bit(num) { if ( typeof num !== 'number' || diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 9e45c83..272ac8e 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -305,12 +305,9 @@ export class Psbt extends PsbtBase { if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { - const cache = this.__CACHE; - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[idx]) { - addNonWitnessTxCache(this.__CACHE, input, idx); - } + const nwTx = nonWitnessUtxoTxFromCache(this.__CACHE, input, idx); const vout = this.__TX.ins[idx].index; - const out = cache.__NON_WITNESS_UTXO_TX_CACHE[idx].outs[vout] as Output; + const out = nwTx.outs[vout] as Output; inputAmount += out.value; } }); @@ -548,13 +545,14 @@ function addNonWitnessTxCache( Object.defineProperty(input, 'nonWitnessUtxo', { enumerable: true, get(): Buffer { - if (self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] !== undefined) { - return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; + if (buf !== undefined) { + return buf; } else { - self.__NON_WITNESS_UTXO_BUF_CACHE[ - selfIndex - ] = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex].toBuffer(); - return self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const newBuf = txCache.toBuffer(); + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; + return newBuf; } }, set(data: Buffer): void { @@ -760,10 +758,11 @@ function getHashForSig( let script: Buffer; if (input.nonWitnessUtxo) { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(cache, input, inputIndex); - } - const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); const prevoutHash = unsignedTx.ins[inputIndex].hash; const utxoHash = nonWitnessUtxoTx.getHash(); @@ -918,10 +917,11 @@ function getScriptFromInput( res.isP2SH = true; res.script = input.redeemScript; } else { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(cache, input, inputIndex); - } - const nonWitnessUtxoTx = cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); const prevoutIndex = unsignedTx.ins[inputIndex].index; res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; } @@ -1054,6 +1054,17 @@ function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { }); } +function nonWitnessUtxoTxFromCache( + cache: PsbtCache, + input: PsbtInput, + inputIndex: number, +): Transaction { + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); + } + return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; +} + function check32Bit(num: number): void { if ( typeof num !== 'number' || From e4e51113768a48061fd1ae2982f40ee3fffb05ef Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 11:51:28 +0900 Subject: [PATCH 072/111] Refactor: cache --- src/psbt.js | 149 ++++++++++++++++++++++-------------------- test/psbt.js | 12 ++-- ts_src/psbt.ts | 169 +++++++++++++++++++++++++----------------------- types/psbt.d.ts | 4 -- 4 files changed, 171 insertions(+), 163 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index fdb457f..c549d30 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -22,10 +22,13 @@ class Psbt extends bip174_1.Psbt { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], __TX_IN_CACHE: {}, + __TX: new transaction_1.Transaction(), }; // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); - this.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); + this.__CACHE.__TX = transaction_1.Transaction.fromBuffer( + this.globalMap.unsignedTx, + ); this.setVersion(2); // set cache const self = this; @@ -33,15 +36,15 @@ class Psbt extends bip174_1.Psbt { Object.defineProperty(this.globalMap, 'unsignedTx', { enumerable: true, get() { - if (self.__TX_BUF_CACHE !== undefined) { - return self.__TX_BUF_CACHE; + if (self.__CACHE.__TX_BUF_CACHE !== undefined) { + return self.__CACHE.__TX_BUF_CACHE; } else { - self.__TX_BUF_CACHE = self.__TX.toBuffer(); - return self.__TX_BUF_CACHE; + self.__CACHE.__TX_BUF_CACHE = self.__CACHE.__TX.toBuffer(); + return self.__CACHE.__TX_BUF_CACHE; } }, set(data) { - self.__TX_BUF_CACHE = data; + self.__CACHE.__TX_BUF_CACHE = data; }, }); // Make data hidden when enumerating @@ -52,8 +55,6 @@ class Psbt extends bip174_1.Psbt { }); dpew(this, '__TX', false, true); dpew(this, '__EXTRACTED_TX', false, true); - dpew(this, '__FEE_RATE', false, true); - dpew(this, '__TX_BUF_CACHE', false, true); dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } @@ -61,7 +62,7 @@ class Psbt extends bip174_1.Psbt { const tx = transaction_1.Transaction.fromBuffer(txBuf); checkTxEmpty(tx); const psbt = new this(); - psbt.__TX = tx; + psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); let inputCount = tx.ins.length; let outputCount = tx.outs.length; @@ -90,7 +91,7 @@ class Psbt extends bip174_1.Psbt { }; }; const psbt = super.fromBuffer(buffer, txCountGetter); - psbt.__TX = tx; + psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); return psbt; } @@ -104,63 +105,39 @@ class Psbt extends bip174_1.Psbt { setVersion(version) { check32Bit(version); checkInputsForPartialSig(this.inputs, 'setVersion'); - this.__TX.version = version; - this.__TX_BUF_CACHE = undefined; - this.__EXTRACTED_TX = undefined; + const c = this.__CACHE; + c.__TX.version = version; + c.__TX_BUF_CACHE = undefined; + c.__EXTRACTED_TX = undefined; return this; } setLocktime(locktime) { check32Bit(locktime); checkInputsForPartialSig(this.inputs, 'setLocktime'); - this.__TX.locktime = locktime; - this.__TX_BUF_CACHE = undefined; - this.__EXTRACTED_TX = undefined; + const c = this.__CACHE; + c.__TX.locktime = locktime; + c.__TX_BUF_CACHE = undefined; + c.__EXTRACTED_TX = undefined; return this; } setSequence(inputIndex, sequence) { check32Bit(sequence); checkInputsForPartialSig(this.inputs, 'setSequence'); - if (this.__TX.ins.length <= inputIndex) { + const c = this.__CACHE; + if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); } - this.__TX.ins[inputIndex].sequence = sequence; - this.__TX_BUF_CACHE = undefined; - this.__EXTRACTED_TX = undefined; + c.__TX.ins[inputIndex].sequence = sequence; + c.__TX_BUF_CACHE = undefined; + c.__EXTRACTED_TX = undefined; return this; } addInput(inputData) { checkInputsForPartialSig(this.inputs, 'addInput'); - const self = this; - const inputAdder = (_inputData, txBuf) => { - if ( - !txBuf || - _inputData.hash === undefined || - _inputData.index === undefined || - (!Buffer.isBuffer(_inputData.hash) && - typeof _inputData.hash !== 'string') || - typeof _inputData.index !== 'number' - ) { - throw new Error('Error adding input.'); - } - const prevHash = Buffer.isBuffer(_inputData.hash) - ? _inputData.hash - : bufferutils_1.reverseBuffer(Buffer.from(_inputData.hash, 'hex')); - // Check if input already exists in cache. - const input = { hash: prevHash, index: _inputData.index }; - checkTxInputCache(self.__CACHE, input); - self.__TX.ins.push( - Object.assign({}, input, { - script: Buffer.alloc(0), - sequence: - _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, - witness: [], - }), - ); - return self.__TX.toBuffer(); - }; + const inputAdder = getInputAdder(this.__CACHE); super.addInput(inputData, inputAdder); - this.__FEE_RATE = undefined; - this.__EXTRACTED_TX = undefined; + this.__CACHE.__FEE_RATE = undefined; + this.__CACHE.__EXTRACTED_TX = undefined; return this; } addOutput(outputData) { @@ -182,15 +159,15 @@ class Psbt extends bip174_1.Psbt { ) { throw new Error('Error adding output.'); } - self.__TX.outs.push({ + self.__CACHE.__TX.outs.push({ script: _outputData.script, value: _outputData.value, }); - return self.__TX.toBuffer(); + return self.__CACHE.__TX.toBuffer(); }; super.addOutput(outputData, true, outputAdder); - this.__FEE_RATE = undefined; - this.__EXTRACTED_TX = undefined; + this.__CACHE.__FEE_RATE = undefined; + this.__CACHE.__EXTRACTED_TX = undefined; return this; } addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { @@ -202,8 +179,8 @@ class Psbt extends bip174_1.Psbt { extractTransaction(disableFeeCheck) { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); if (!disableFeeCheck) { - const feeRate = this.__FEE_RATE || this.getFeeRate(); - const vsize = this.__EXTRACTED_TX.virtualSize(); + const feeRate = this.__CACHE.__FEE_RATE || this.getFeeRate(); + const vsize = this.__CACHE.__EXTRACTED_TX.virtualSize(); const satoshis = feeRate * vsize; if (feeRate >= this.opts.maximumFeeRate) { throw new Error( @@ -215,8 +192,8 @@ class Psbt extends bip174_1.Psbt { ); } } - if (this.__EXTRACTED_TX) return this.__EXTRACTED_TX; - const tx = this.__TX.clone(); + if (this.__CACHE.__EXTRACTED_TX) return this.__CACHE.__EXTRACTED_TX; + const tx = this.__CACHE.__TX.clone(); this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (input.finalScriptWitness) { @@ -225,21 +202,21 @@ class Psbt extends bip174_1.Psbt { ); } }); - this.__EXTRACTED_TX = tx; + this.__CACHE.__EXTRACTED_TX = tx; return tx; } getFeeRate() { if (!this.inputs.every(isFinalized)) throw new Error('PSBT must be finalized to calculate fee rate'); - if (this.__FEE_RATE) return this.__FEE_RATE; + if (this.__CACHE.__FEE_RATE) return this.__CACHE.__FEE_RATE; let tx; let inputAmount = 0; let mustFinalize = true; - if (this.__EXTRACTED_TX) { - tx = this.__EXTRACTED_TX; + if (this.__CACHE.__EXTRACTED_TX) { + tx = this.__CACHE.__EXTRACTED_TX; mustFinalize = false; } else { - tx = this.__TX.clone(); + tx = this.__CACHE.__TX.clone(); } this.inputs.forEach((input, idx) => { if (mustFinalize && input.finalScriptSig) @@ -253,17 +230,17 @@ class Psbt extends bip174_1.Psbt { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { const nwTx = nonWitnessUtxoTxFromCache(this.__CACHE, input, idx); - const vout = this.__TX.ins[idx].index; + const vout = this.__CACHE.__TX.ins[idx].index; const out = nwTx.outs[vout]; inputAmount += out.value; } }); - this.__EXTRACTED_TX = tx; + this.__CACHE.__EXTRACTED_TX = tx; const outputAmount = tx.outs.reduce((total, o) => total + o.value, 0); const fee = inputAmount - outputAmount; const bytes = tx.virtualSize(); - this.__FEE_RATE = Math.floor(fee / bytes); - return this.__FEE_RATE; + this.__CACHE.__FEE_RATE = Math.floor(fee / bytes); + return this.__CACHE.__FEE_RATE; } finalizeAllInputs() { const inputResults = range(this.inputs.length).map(idx => @@ -280,7 +257,7 @@ class Psbt extends bip174_1.Psbt { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, - this.__TX, + this.__CACHE.__TX, this.__CACHE, ); if (!script) return false; @@ -322,7 +299,7 @@ class Psbt extends bip174_1.Psbt { ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), - this.__TX, + this.__CACHE.__TX, this.__CACHE, ) : { hash: hashCache, script: scriptCache }; @@ -391,7 +368,7 @@ class Psbt extends bip174_1.Psbt { this.inputs, inputIndex, keyPair.publicKey, - this.__TX, + this.__CACHE.__TX, this.__CACHE, ); const partialSig = { @@ -408,7 +385,7 @@ class Psbt extends bip174_1.Psbt { this.inputs, inputIndex, keyPair.publicKey, - this.__TX, + this.__CACHE.__TX, this.__CACHE, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -847,6 +824,36 @@ function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { } return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; } +function getInputAdder(cache) { + const selfCache = cache; + return (_inputData, txBuf) => { + if ( + !txBuf || + _inputData.hash === undefined || + _inputData.index === undefined || + (!Buffer.isBuffer(_inputData.hash) && + typeof _inputData.hash !== 'string') || + typeof _inputData.index !== 'number' + ) { + throw new Error('Error adding input.'); + } + const prevHash = Buffer.isBuffer(_inputData.hash) + ? _inputData.hash + : bufferutils_1.reverseBuffer(Buffer.from(_inputData.hash, 'hex')); + // Check if input already exists in cache. + const input = { hash: prevHash, index: _inputData.index }; + checkTxInputCache(selfCache, input); + selfCache.__TX.ins.push( + Object.assign({}, input, { + script: Buffer.alloc(0), + sequence: + _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, + witness: [], + }), + ); + return selfCache.__TX.toBuffer(); + }; +} function check32Bit(num) { if ( typeof num !== 'number' || diff --git a/test/psbt.js b/test/psbt.js index cd45c1e..657a67a 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -397,9 +397,9 @@ describe(`Psbt`, () => { }); assert.strictEqual(psbt.inputCount, 1) - assert.strictEqual(psbt.__TX.ins[0].sequence, 0xffffffff) + assert.strictEqual(psbt.__CACHE.__TX.ins[0].sequence, 0xffffffff) psbt.setSequence(0, 0) - assert.strictEqual(psbt.__TX.ins[0].sequence, 0) + assert.strictEqual(psbt.__CACHE.__TX.ins[0].sequence, 0) }) it('throws if input index is too high', () => { @@ -467,24 +467,24 @@ describe(`Psbt`, () => { it('fromTransaction returns Psbt type (not base class)', () => { const psbt = Psbt.fromTransaction(Buffer.from([2,0,0,0,0,0,0,0,0,0])); assert.strictEqual(psbt instanceof Psbt, true); - assert.ok(psbt.__TX); + assert.ok(psbt.__CACHE.__TX); }) it('fromBuffer returns Psbt type (not base class)', () => { const psbt = Psbt.fromBuffer(Buffer.from( '70736274ff01000a01000000000000000000000000', 'hex' //cHNidP8BAAoBAAAAAAAAAAAAAAAA )); assert.strictEqual(psbt instanceof Psbt, true); - assert.ok(psbt.__TX); + assert.ok(psbt.__CACHE.__TX); }) it('fromBase64 returns Psbt type (not base class)', () => { const psbt = Psbt.fromBase64('cHNidP8BAAoBAAAAAAAAAAAAAAAA'); assert.strictEqual(psbt instanceof Psbt, true); - assert.ok(psbt.__TX); + assert.ok(psbt.__CACHE.__TX); }) it('fromHex returns Psbt type (not base class)', () => { const psbt = Psbt.fromHex('70736274ff01000a01000000000000000000000000'); assert.strictEqual(psbt instanceof Psbt, true); - assert.ok(psbt.__TX); + assert.ok(psbt.__CACHE.__TX); }) }) }) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 272ac8e..0174b3b 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -34,7 +34,7 @@ export class Psbt extends PsbtBase { const tx = Transaction.fromBuffer(txBuf); checkTxEmpty(tx); const psbt = new this() as Psbt; - psbt.__TX = tx; + psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); let inputCount = tx.ins.length; let outputCount = tx.outs.length; @@ -71,25 +71,22 @@ export class Psbt extends PsbtBase { }; }; const psbt = super.fromBuffer(buffer, txCountGetter) as Psbt; - psbt.__TX = tx!; + psbt.__CACHE.__TX = tx!; checkTxForDupeIns(tx!, psbt.__CACHE); return psbt as InstanceType; } - private __CACHE = { - __NON_WITNESS_UTXO_TX_CACHE: [] as Transaction[], - __NON_WITNESS_UTXO_BUF_CACHE: [] as Buffer[], - __TX_IN_CACHE: {} as { [index: string]: number }, + private __CACHE: PsbtCache = { + __NON_WITNESS_UTXO_TX_CACHE: [], + __NON_WITNESS_UTXO_BUF_CACHE: [], + __TX_IN_CACHE: {}, + __TX: new Transaction(), }; - private __TX: Transaction; - private __TX_BUF_CACHE?: Buffer; - private __FEE_RATE?: number; - private __EXTRACTED_TX?: Transaction; private opts: PsbtOpts; constructor(opts: PsbtOptsOptional = {}) { super(); // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); - this.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); + this.__CACHE.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); this.setVersion(2); // set cache @@ -98,15 +95,15 @@ export class Psbt extends PsbtBase { Object.defineProperty(this.globalMap, 'unsignedTx', { enumerable: true, get(): Buffer { - if (self.__TX_BUF_CACHE !== undefined) { - return self.__TX_BUF_CACHE; + if (self.__CACHE.__TX_BUF_CACHE !== undefined) { + return self.__CACHE.__TX_BUF_CACHE; } else { - self.__TX_BUF_CACHE = self.__TX.toBuffer(); - return self.__TX_BUF_CACHE; + self.__CACHE.__TX_BUF_CACHE = self.__CACHE.__TX.toBuffer(); + return self.__CACHE.__TX_BUF_CACHE; } }, set(data: Buffer): void { - self.__TX_BUF_CACHE = data; + self.__CACHE.__TX_BUF_CACHE = data; }, }); @@ -123,8 +120,6 @@ export class Psbt extends PsbtBase { }); dpew(this, '__TX', false, true); dpew(this, '__EXTRACTED_TX', false, true); - dpew(this, '__FEE_RATE', false, true); - dpew(this, '__TX_BUF_CACHE', false, true); dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } @@ -141,69 +136,42 @@ export class Psbt extends PsbtBase { setVersion(version: number): this { check32Bit(version); checkInputsForPartialSig(this.inputs, 'setVersion'); - this.__TX.version = version; - this.__TX_BUF_CACHE = undefined; - this.__EXTRACTED_TX = undefined; + const c = this.__CACHE; + c.__TX.version = version; + c.__TX_BUF_CACHE = undefined; + c.__EXTRACTED_TX = undefined; return this; } setLocktime(locktime: number): this { check32Bit(locktime); checkInputsForPartialSig(this.inputs, 'setLocktime'); - this.__TX.locktime = locktime; - this.__TX_BUF_CACHE = undefined; - this.__EXTRACTED_TX = undefined; + const c = this.__CACHE; + c.__TX.locktime = locktime; + c.__TX_BUF_CACHE = undefined; + c.__EXTRACTED_TX = undefined; return this; } setSequence(inputIndex: number, sequence: number): this { check32Bit(sequence); checkInputsForPartialSig(this.inputs, 'setSequence'); - if (this.__TX.ins.length <= inputIndex) { + const c = this.__CACHE; + if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); } - this.__TX.ins[inputIndex].sequence = sequence; - this.__TX_BUF_CACHE = undefined; - this.__EXTRACTED_TX = undefined; + c.__TX.ins[inputIndex].sequence = sequence; + c.__TX_BUF_CACHE = undefined; + c.__EXTRACTED_TX = undefined; return this; } addInput(inputData: TransactionInput): this { checkInputsForPartialSig(this.inputs, 'addInput'); - const self = this; - const inputAdder = ( - _inputData: TransactionInput, - txBuf: Buffer, - ): Buffer => { - if ( - !txBuf || - (_inputData as any).hash === undefined || - (_inputData as any).index === undefined || - (!Buffer.isBuffer((_inputData as any).hash) && - typeof (_inputData as any).hash !== 'string') || - typeof (_inputData as any).index !== 'number' - ) { - throw new Error('Error adding input.'); - } - const prevHash = Buffer.isBuffer(_inputData.hash) - ? _inputData.hash - : reverseBuffer(Buffer.from(_inputData.hash, 'hex')); - - // Check if input already exists in cache. - const input = { hash: prevHash, index: _inputData.index }; - checkTxInputCache(self.__CACHE, input); - - self.__TX.ins.push({ - ...input, - script: Buffer.alloc(0), - sequence: _inputData.sequence || Transaction.DEFAULT_SEQUENCE, - witness: [], - }); - return self.__TX.toBuffer(); - }; + const inputAdder = getInputAdder(this.__CACHE); super.addInput(inputData, inputAdder); - this.__FEE_RATE = undefined; - this.__EXTRACTED_TX = undefined; + this.__CACHE.__FEE_RATE = undefined; + this.__CACHE.__EXTRACTED_TX = undefined; return this; } @@ -229,15 +197,15 @@ export class Psbt extends PsbtBase { ) { throw new Error('Error adding output.'); } - self.__TX.outs.push({ + self.__CACHE.__TX.outs.push({ script: (_outputData as any).script!, value: _outputData.value, }); - return self.__TX.toBuffer(); + return self.__CACHE.__TX.toBuffer(); }; super.addOutput(outputData, true, outputAdder); - this.__FEE_RATE = undefined; - this.__EXTRACTED_TX = undefined; + this.__CACHE.__FEE_RATE = undefined; + this.__CACHE.__EXTRACTED_TX = undefined; return this; } @@ -254,8 +222,8 @@ export class Psbt extends PsbtBase { extractTransaction(disableFeeCheck?: boolean): Transaction { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); if (!disableFeeCheck) { - const feeRate = this.__FEE_RATE || this.getFeeRate(); - const vsize = this.__EXTRACTED_TX!.virtualSize(); + const feeRate = this.__CACHE.__FEE_RATE || this.getFeeRate(); + const vsize = this.__CACHE.__EXTRACTED_TX!.virtualSize(); const satoshis = feeRate * vsize; if (feeRate >= this.opts.maximumFeeRate) { throw new Error( @@ -267,8 +235,8 @@ export class Psbt extends PsbtBase { ); } } - if (this.__EXTRACTED_TX) return this.__EXTRACTED_TX; - const tx = this.__TX.clone(); + if (this.__CACHE.__EXTRACTED_TX) return this.__CACHE.__EXTRACTED_TX; + const tx = this.__CACHE.__TX.clone(); this.inputs.forEach((input, idx) => { if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; if (input.finalScriptWitness) { @@ -277,22 +245,22 @@ export class Psbt extends PsbtBase { ); } }); - this.__EXTRACTED_TX = tx; + this.__CACHE.__EXTRACTED_TX = tx; return tx; } getFeeRate(): number { if (!this.inputs.every(isFinalized)) throw new Error('PSBT must be finalized to calculate fee rate'); - if (this.__FEE_RATE) return this.__FEE_RATE; + if (this.__CACHE.__FEE_RATE) return this.__CACHE.__FEE_RATE; let tx: Transaction; let inputAmount = 0; let mustFinalize = true; - if (this.__EXTRACTED_TX) { - tx = this.__EXTRACTED_TX; + if (this.__CACHE.__EXTRACTED_TX) { + tx = this.__CACHE.__EXTRACTED_TX; mustFinalize = false; } else { - tx = this.__TX.clone(); + tx = this.__CACHE.__TX.clone(); } this.inputs.forEach((input, idx) => { if (mustFinalize && input.finalScriptSig) @@ -306,20 +274,20 @@ export class Psbt extends PsbtBase { inputAmount += input.witnessUtxo.value; } else if (input.nonWitnessUtxo) { const nwTx = nonWitnessUtxoTxFromCache(this.__CACHE, input, idx); - const vout = this.__TX.ins[idx].index; + const vout = this.__CACHE.__TX.ins[idx].index; const out = nwTx.outs[vout] as Output; inputAmount += out.value; } }); - this.__EXTRACTED_TX = tx; + this.__CACHE.__EXTRACTED_TX = tx; const outputAmount = (tx.outs as Output[]).reduce( (total, o) => total + o.value, 0, ); const fee = inputAmount - outputAmount; const bytes = tx.virtualSize(); - this.__FEE_RATE = Math.floor(fee / bytes); - return this.__FEE_RATE; + this.__CACHE.__FEE_RATE = Math.floor(fee / bytes); + return this.__CACHE.__FEE_RATE; } finalizeAllInputs(): { @@ -341,7 +309,7 @@ export class Psbt extends PsbtBase { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, - this.__TX, + this.__CACHE.__TX, this.__CACHE, ); if (!script) return false; @@ -388,7 +356,7 @@ export class Psbt extends PsbtBase { ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), - this.__TX, + this.__CACHE.__TX, this.__CACHE, ) : { hash: hashCache!, script: scriptCache! }; @@ -464,7 +432,7 @@ export class Psbt extends PsbtBase { this.inputs, inputIndex, keyPair.publicKey, - this.__TX, + this.__CACHE.__TX, this.__CACHE, ); @@ -485,7 +453,7 @@ export class Psbt extends PsbtBase { this.inputs, inputIndex, keyPair.publicKey, - this.__TX, + this.__CACHE.__TX, this.__CACHE, ); @@ -517,6 +485,10 @@ interface PsbtCache { __NON_WITNESS_UTXO_TX_CACHE: Transaction[]; __NON_WITNESS_UTXO_BUF_CACHE: Buffer[]; __TX_IN_CACHE: { [index: string]: number }; + __TX: Transaction; + __TX_BUF_CACHE?: Buffer; + __FEE_RATE?: number; + __EXTRACTED_TX?: Transaction; } interface PsbtOptsOptional { @@ -1065,6 +1037,39 @@ function nonWitnessUtxoTxFromCache( return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; } +function getInputAdder( + cache: PsbtCache, +): (_inputData: TransactionInput, txBuf: Buffer) => Buffer { + const selfCache = cache; + return (_inputData: TransactionInput, txBuf: Buffer): Buffer => { + if ( + !txBuf || + (_inputData as any).hash === undefined || + (_inputData as any).index === undefined || + (!Buffer.isBuffer((_inputData as any).hash) && + typeof (_inputData as any).hash !== 'string') || + typeof (_inputData as any).index !== 'number' + ) { + throw new Error('Error adding input.'); + } + const prevHash = Buffer.isBuffer(_inputData.hash) + ? _inputData.hash + : reverseBuffer(Buffer.from(_inputData.hash, 'hex')); + + // Check if input already exists in cache. + const input = { hash: prevHash, index: _inputData.index }; + checkTxInputCache(selfCache, input); + + selfCache.__TX.ins.push({ + ...input, + script: Buffer.alloc(0), + sequence: _inputData.sequence || Transaction.DEFAULT_SEQUENCE, + witness: [], + }); + return selfCache.__TX.toBuffer(); + }; +} + function check32Bit(num: number): void { if ( typeof num !== 'number' || diff --git a/types/psbt.d.ts b/types/psbt.d.ts index b2893b8..775f3b0 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -8,10 +8,6 @@ export declare class Psbt extends PsbtBase { static fromTransaction(this: T, txBuf: Buffer): InstanceType; static fromBuffer(this: T, buffer: Buffer): InstanceType; private __CACHE; - private __TX; - private __TX_BUF_CACHE?; - private __FEE_RATE?; - private __EXTRACTED_TX?; private opts; constructor(opts?: PsbtOptsOptional); readonly inputCount: number; From 497d048ebf3b269ee3782423da10478fe822f194 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 11:57:50 +0900 Subject: [PATCH 073/111] Refactor: externalize outputAdder --- src/psbt.js | 49 ++++++++++++++++++++++++-------------------- ts_src/psbt.ts | 55 +++++++++++++++++++++++++++----------------------- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index c549d30..eff98ae 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -134,10 +134,11 @@ class Psbt extends bip174_1.Psbt { } addInput(inputData) { checkInputsForPartialSig(this.inputs, 'addInput'); - const inputAdder = getInputAdder(this.__CACHE); + const c = this.__CACHE; + const inputAdder = getInputAdder(c); super.addInput(inputData, inputAdder); - this.__CACHE.__FEE_RATE = undefined; - this.__CACHE.__EXTRACTED_TX = undefined; + c.__FEE_RATE = undefined; + c.__EXTRACTED_TX = undefined; return this; } addOutput(outputData) { @@ -148,26 +149,11 @@ class Psbt extends bip174_1.Psbt { const script = address_1.toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } - const self = this; - const outputAdder = (_outputData, txBuf) => { - if ( - !txBuf || - _outputData.script === undefined || - _outputData.value === undefined || - !Buffer.isBuffer(_outputData.script) || - typeof _outputData.value !== 'number' - ) { - throw new Error('Error adding output.'); - } - self.__CACHE.__TX.outs.push({ - script: _outputData.script, - value: _outputData.value, - }); - return self.__CACHE.__TX.toBuffer(); - }; + const c = this.__CACHE; + const outputAdder = getOutputAdder(c); super.addOutput(outputData, true, outputAdder); - this.__CACHE.__FEE_RATE = undefined; - this.__CACHE.__EXTRACTED_TX = undefined; + c.__FEE_RATE = undefined; + c.__EXTRACTED_TX = undefined; return this; } addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { @@ -854,6 +840,25 @@ function getInputAdder(cache) { return selfCache.__TX.toBuffer(); }; } +function getOutputAdder(cache) { + const selfCache = cache; + return (_outputData, txBuf) => { + if ( + !txBuf || + _outputData.script === undefined || + _outputData.value === undefined || + !Buffer.isBuffer(_outputData.script) || + typeof _outputData.value !== 'number' + ) { + throw new Error('Error adding output.'); + } + selfCache.__TX.outs.push({ + script: _outputData.script, + value: _outputData.value, + }); + return selfCache.__TX.toBuffer(); + }; +} function check32Bit(num) { if ( typeof num !== 'number' || diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0174b3b..370b481 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -168,10 +168,11 @@ export class Psbt extends PsbtBase { addInput(inputData: TransactionInput): this { checkInputsForPartialSig(this.inputs, 'addInput'); - const inputAdder = getInputAdder(this.__CACHE); + const c = this.__CACHE; + const inputAdder = getInputAdder(c); super.addInput(inputData, inputAdder); - this.__CACHE.__FEE_RATE = undefined; - this.__CACHE.__EXTRACTED_TX = undefined; + c.__FEE_RATE = undefined; + c.__EXTRACTED_TX = undefined; return this; } @@ -183,29 +184,11 @@ export class Psbt extends PsbtBase { const script = toOutputScript(address, network); outputData = Object.assign(outputData, { script }); } - const self = this; - const outputAdder = ( - _outputData: TransactionOutput, - txBuf: Buffer, - ): Buffer => { - if ( - !txBuf || - (_outputData as any).script === undefined || - (_outputData as any).value === undefined || - !Buffer.isBuffer((_outputData as any).script) || - typeof (_outputData as any).value !== 'number' - ) { - throw new Error('Error adding output.'); - } - self.__CACHE.__TX.outs.push({ - script: (_outputData as any).script!, - value: _outputData.value, - }); - return self.__CACHE.__TX.toBuffer(); - }; + const c = this.__CACHE; + const outputAdder = getOutputAdder(c); super.addOutput(outputData, true, outputAdder); - this.__CACHE.__FEE_RATE = undefined; - this.__CACHE.__EXTRACTED_TX = undefined; + c.__FEE_RATE = undefined; + c.__EXTRACTED_TX = undefined; return this; } @@ -1070,6 +1053,28 @@ function getInputAdder( }; } +function getOutputAdder( + cache: PsbtCache, +): (_outputData: TransactionOutput, txBuf: Buffer) => Buffer { + const selfCache = cache; + return (_outputData: TransactionOutput, txBuf: Buffer): Buffer => { + if ( + !txBuf || + (_outputData as any).script === undefined || + (_outputData as any).value === undefined || + !Buffer.isBuffer((_outputData as any).script) || + typeof (_outputData as any).value !== 'number' + ) { + throw new Error('Error adding output.'); + } + selfCache.__TX.outs.push({ + script: (_outputData as any).script!, + value: _outputData.value, + }); + return selfCache.__TX.toBuffer(); + }; +} + function check32Bit(num: number): void { if ( typeof num !== 'number' || From 9749a216b8cc68fff6e00771cf93af23ce47c52a Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 12:15:20 +0900 Subject: [PATCH 074/111] Refactor: input finalize and get fee shared logic --- src/psbt.js | 104 ++++++++++++++++++++++++--------------------- ts_src/psbt.ts | 112 ++++++++++++++++++++++++++++--------------------- 2 files changed, 120 insertions(+), 96 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index eff98ae..c2f9bd3 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -164,69 +164,42 @@ class Psbt extends bip174_1.Psbt { } extractTransaction(disableFeeCheck) { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + const c = this.__CACHE; if (!disableFeeCheck) { - const feeRate = this.__CACHE.__FEE_RATE || this.getFeeRate(); - const vsize = this.__CACHE.__EXTRACTED_TX.virtualSize(); - const satoshis = feeRate * vsize; - if (feeRate >= this.opts.maximumFeeRate) { - throw new Error( - `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + - `fees, which is ${feeRate} satoshi per byte for a transaction ` + - `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + - `byte). Use setMaximumFeeRate method to raise your threshold, or ` + - `pass true to the first arg of extractTransaction.`, - ); - } + checkFees(this, c, this.opts); } - if (this.__CACHE.__EXTRACTED_TX) return this.__CACHE.__EXTRACTED_TX; - const tx = this.__CACHE.__TX.clone(); - this.inputs.forEach((input, idx) => { - if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; - if (input.finalScriptWitness) { - tx.ins[idx].witness = scriptWitnessToWitnessStack( - input.finalScriptWitness, - ); - } - }); - this.__CACHE.__EXTRACTED_TX = tx; + if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; + const tx = c.__TX.clone(); + inputFinalizeGetAmts(this.inputs, tx, c, true, false); + c.__EXTRACTED_TX = tx; return tx; } getFeeRate() { if (!this.inputs.every(isFinalized)) throw new Error('PSBT must be finalized to calculate fee rate'); - if (this.__CACHE.__FEE_RATE) return this.__CACHE.__FEE_RATE; + const c = this.__CACHE; + if (c.__FEE_RATE) return c.__FEE_RATE; let tx; - let inputAmount = 0; let mustFinalize = true; - if (this.__CACHE.__EXTRACTED_TX) { - tx = this.__CACHE.__EXTRACTED_TX; + if (c.__EXTRACTED_TX) { + tx = c.__EXTRACTED_TX; mustFinalize = false; } else { - tx = this.__CACHE.__TX.clone(); + tx = c.__TX.clone(); } - this.inputs.forEach((input, idx) => { - if (mustFinalize && input.finalScriptSig) - tx.ins[idx].script = input.finalScriptSig; - if (mustFinalize && input.finalScriptWitness) { - tx.ins[idx].witness = scriptWitnessToWitnessStack( - input.finalScriptWitness, - ); - } - if (input.witnessUtxo) { - inputAmount += input.witnessUtxo.value; - } else if (input.nonWitnessUtxo) { - const nwTx = nonWitnessUtxoTxFromCache(this.__CACHE, input, idx); - const vout = this.__CACHE.__TX.ins[idx].index; - const out = nwTx.outs[vout]; - inputAmount += out.value; - } - }); - this.__CACHE.__EXTRACTED_TX = tx; + const inputAmount = inputFinalizeGetAmts( + this.inputs, + tx, + c, + mustFinalize, + true, + ); + c.__EXTRACTED_TX = tx; const outputAmount = tx.outs.reduce((total, o) => total + o.value, 0); const fee = inputAmount - outputAmount; const bytes = tx.virtualSize(); - this.__CACHE.__FEE_RATE = Math.floor(fee / bytes); - return this.__CACHE.__FEE_RATE; + c.__FEE_RATE = Math.floor(fee / bytes); + return c.__FEE_RATE; } finalizeAllInputs() { const inputResults = range(this.inputs.length).map(idx => @@ -859,6 +832,41 @@ function getOutputAdder(cache) { return selfCache.__TX.toBuffer(); }; } +function checkFees(psbt, cache, opts) { + const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); + const vsize = cache.__EXTRACTED_TX.virtualSize(); + const satoshis = feeRate * vsize; + if (feeRate >= opts.maximumFeeRate) { + throw new Error( + `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, + ); + } +} +function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize, getAmounts) { + let inputAmount = 0; + inputs.forEach((input, idx) => { + if (mustFinalize && input.finalScriptSig) + tx.ins[idx].script = input.finalScriptSig; + if (mustFinalize && input.finalScriptWitness) { + tx.ins[idx].witness = scriptWitnessToWitnessStack( + input.finalScriptWitness, + ); + } + if (getAmounts && input.witnessUtxo) { + inputAmount += input.witnessUtxo.value; + } else if (getAmounts && input.nonWitnessUtxo) { + const nwTx = nonWitnessUtxoTxFromCache(cache, input, idx); + const vout = tx.ins[idx].index; + const out = nwTx.outs[vout]; + inputAmount += out.value; + } + }); + return inputAmount; +} function check32Bit(num) { if ( typeof num !== 'number' || diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 370b481..0771436 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -204,73 +204,46 @@ export class Psbt extends PsbtBase { extractTransaction(disableFeeCheck?: boolean): Transaction { if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + const c = this.__CACHE; if (!disableFeeCheck) { - const feeRate = this.__CACHE.__FEE_RATE || this.getFeeRate(); - const vsize = this.__CACHE.__EXTRACTED_TX!.virtualSize(); - const satoshis = feeRate * vsize; - if (feeRate >= this.opts.maximumFeeRate) { - throw new Error( - `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + - `fees, which is ${feeRate} satoshi per byte for a transaction ` + - `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + - `byte). Use setMaximumFeeRate method to raise your threshold, or ` + - `pass true to the first arg of extractTransaction.`, - ); - } + checkFees(this, c, this.opts); } - if (this.__CACHE.__EXTRACTED_TX) return this.__CACHE.__EXTRACTED_TX; - const tx = this.__CACHE.__TX.clone(); - this.inputs.forEach((input, idx) => { - if (input.finalScriptSig) tx.ins[idx].script = input.finalScriptSig; - if (input.finalScriptWitness) { - tx.ins[idx].witness = scriptWitnessToWitnessStack( - input.finalScriptWitness, - ); - } - }); - this.__CACHE.__EXTRACTED_TX = tx; + if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; + const tx = c.__TX.clone(); + inputFinalizeGetAmts(this.inputs, tx, c, true, false); + c.__EXTRACTED_TX = tx; return tx; } getFeeRate(): number { if (!this.inputs.every(isFinalized)) throw new Error('PSBT must be finalized to calculate fee rate'); - if (this.__CACHE.__FEE_RATE) return this.__CACHE.__FEE_RATE; + const c = this.__CACHE; + if (c.__FEE_RATE) return c.__FEE_RATE; let tx: Transaction; - let inputAmount = 0; let mustFinalize = true; - if (this.__CACHE.__EXTRACTED_TX) { - tx = this.__CACHE.__EXTRACTED_TX; + if (c.__EXTRACTED_TX) { + tx = c.__EXTRACTED_TX; mustFinalize = false; } else { - tx = this.__CACHE.__TX.clone(); + tx = c.__TX.clone(); } - this.inputs.forEach((input, idx) => { - if (mustFinalize && input.finalScriptSig) - tx.ins[idx].script = input.finalScriptSig; - if (mustFinalize && input.finalScriptWitness) { - tx.ins[idx].witness = scriptWitnessToWitnessStack( - input.finalScriptWitness, - ); - } - if (input.witnessUtxo) { - inputAmount += input.witnessUtxo.value; - } else if (input.nonWitnessUtxo) { - const nwTx = nonWitnessUtxoTxFromCache(this.__CACHE, input, idx); - const vout = this.__CACHE.__TX.ins[idx].index; - const out = nwTx.outs[vout] as Output; - inputAmount += out.value; - } - }); - this.__CACHE.__EXTRACTED_TX = tx; + const inputAmount = inputFinalizeGetAmts( + this.inputs, + tx, + c, + mustFinalize, + true, + ); + c.__EXTRACTED_TX = tx; const outputAmount = (tx.outs as Output[]).reduce( (total, o) => total + o.value, 0, ); const fee = inputAmount - outputAmount; const bytes = tx.virtualSize(); - this.__CACHE.__FEE_RATE = Math.floor(fee / bytes); - return this.__CACHE.__FEE_RATE; + c.__FEE_RATE = Math.floor(fee / bytes); + return c.__FEE_RATE; } finalizeAllInputs(): { @@ -1075,6 +1048,49 @@ function getOutputAdder( }; } +function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { + const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); + const vsize = cache.__EXTRACTED_TX!.virtualSize(); + const satoshis = feeRate * vsize; + if (feeRate >= opts.maximumFeeRate) { + throw new Error( + `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, + ); + } +} + +function inputFinalizeGetAmts( + inputs: PsbtInput[], + tx: Transaction, + cache: PsbtCache, + mustFinalize: boolean, + getAmounts: boolean, +): number { + let inputAmount = 0; + inputs.forEach((input, idx) => { + if (mustFinalize && input.finalScriptSig) + tx.ins[idx].script = input.finalScriptSig; + if (mustFinalize && input.finalScriptWitness) { + tx.ins[idx].witness = scriptWitnessToWitnessStack( + input.finalScriptWitness, + ); + } + if (getAmounts && input.witnessUtxo) { + inputAmount += input.witnessUtxo.value; + } else if (getAmounts && input.nonWitnessUtxo) { + const nwTx = nonWitnessUtxoTxFromCache(cache, input, idx); + const vout = tx.ins[idx].index; + const out = nwTx.outs[vout] as Output; + inputAmount += out.value; + } + }); + return inputAmount; +} + function check32Bit(num: number): void { if ( typeof num !== 'number' || From 2fd4b9dc54eeffabd35474a732d02a60f4c0a434 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 12:30:51 +0900 Subject: [PATCH 075/111] Refactor: pass only cache to certain functions --- src/psbt.js | 38 ++++++++++++++------------------------ ts_src/psbt.ts | 36 ++++++++++++++---------------------- 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index c2f9bd3..d7cc03a 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -26,25 +26,24 @@ class Psbt extends bip174_1.Psbt { }; // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); - this.__CACHE.__TX = transaction_1.Transaction.fromBuffer( - this.globalMap.unsignedTx, - ); + const c = this.__CACHE; + c.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); this.setVersion(2); // set cache - const self = this; delete this.globalMap.unsignedTx; Object.defineProperty(this.globalMap, 'unsignedTx', { enumerable: true, get() { - if (self.__CACHE.__TX_BUF_CACHE !== undefined) { - return self.__CACHE.__TX_BUF_CACHE; + const buf = c.__TX_BUF_CACHE; + if (buf !== undefined) { + return buf; } else { - self.__CACHE.__TX_BUF_CACHE = self.__CACHE.__TX.toBuffer(); - return self.__CACHE.__TX_BUF_CACHE; + c.__TX_BUF_CACHE = c.__TX.toBuffer(); + return c.__TX_BUF_CACHE; } }, set(data) { - self.__CACHE.__TX_BUF_CACHE = data; + c.__TX_BUF_CACHE = data; }, }); // Make data hidden when enumerating @@ -53,8 +52,6 @@ class Psbt extends bip174_1.Psbt { enumerable, writable, }); - dpew(this, '__TX', false, true); - dpew(this, '__EXTRACTED_TX', false, true); dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } @@ -216,7 +213,6 @@ class Psbt extends bip174_1.Psbt { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, - this.__CACHE.__TX, this.__CACHE, ); if (!script) return false; @@ -258,7 +254,6 @@ class Psbt extends bip174_1.Psbt { ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), - this.__CACHE.__TX, this.__CACHE, ) : { hash: hashCache, script: scriptCache }; @@ -327,7 +322,6 @@ class Psbt extends bip174_1.Psbt { this.inputs, inputIndex, keyPair.publicKey, - this.__CACHE.__TX, this.__CACHE, ); const partialSig = { @@ -344,7 +338,6 @@ class Psbt extends bip174_1.Psbt { this.inputs, inputIndex, keyPair.publicKey, - this.__CACHE.__TX, this.__CACHE, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -400,14 +393,9 @@ function checkTxInputCache(cache, input) { function isFinalized(input) { return !!input.finalScriptSig || !!input.finalScriptWitness; } -function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx, cache) { +function getHashAndSighashType(inputs, inputIndex, pubkey, cache) { const input = utils_1.checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig( - inputIndex, - input, - unsignedTx, - cache, - ); + const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); checkScriptForPubkey(pubkey, script, 'sign'); return { hash, @@ -525,7 +513,8 @@ function checkScriptForPubkey(pubkey, script, action) { ); } } -function getHashForSig(inputIndex, input, unsignedTx, cache) { +function getHashForSig(inputIndex, input, cache) { + const unsignedTx = cache.__TX; const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; let hash; @@ -646,7 +635,8 @@ function classifyScript(script) { if (isP2PK(script)) return 'pubkey'; return 'nonstandard'; } -function getScriptFromInput(inputIndex, input, unsignedTx, cache) { +function getScriptFromInput(inputIndex, input, cache) { + const unsignedTx = cache.__TX; const res = { script: null, isSegwit: false, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0771436..ffe417f 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -52,6 +52,7 @@ export class Psbt extends PsbtBase { } return psbt as InstanceType; } + static fromBuffer( this: T, buffer: Buffer, @@ -75,6 +76,7 @@ export class Psbt extends PsbtBase { checkTxForDupeIns(tx!, psbt.__CACHE); return psbt as InstanceType; } + private __CACHE: PsbtCache = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], @@ -82,28 +84,30 @@ export class Psbt extends PsbtBase { __TX: new Transaction(), }; private opts: PsbtOpts; + constructor(opts: PsbtOptsOptional = {}) { super(); // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); - this.__CACHE.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); + const c = this.__CACHE; + c.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); this.setVersion(2); // set cache - const self = this; delete this.globalMap.unsignedTx; Object.defineProperty(this.globalMap, 'unsignedTx', { enumerable: true, get(): Buffer { - if (self.__CACHE.__TX_BUF_CACHE !== undefined) { - return self.__CACHE.__TX_BUF_CACHE; + const buf = c.__TX_BUF_CACHE; + if (buf !== undefined) { + return buf; } else { - self.__CACHE.__TX_BUF_CACHE = self.__CACHE.__TX.toBuffer(); - return self.__CACHE.__TX_BUF_CACHE; + c.__TX_BUF_CACHE = c.__TX.toBuffer(); + return c.__TX_BUF_CACHE; } }, set(data: Buffer): void { - self.__CACHE.__TX_BUF_CACHE = data; + c.__TX_BUF_CACHE = data; }, }); @@ -118,8 +122,6 @@ export class Psbt extends PsbtBase { enumerable, writable, }); - dpew(this, '__TX', false, true); - dpew(this, '__EXTRACTED_TX', false, true); dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } @@ -265,7 +267,6 @@ export class Psbt extends PsbtBase { const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, - this.__CACHE.__TX, this.__CACHE, ); if (!script) return false; @@ -312,7 +313,6 @@ export class Psbt extends PsbtBase { ? getHashForSig( inputIndex, Object.assign({}, input, { sighashType: sig.hashType }), - this.__CACHE.__TX, this.__CACHE, ) : { hash: hashCache!, script: scriptCache! }; @@ -388,7 +388,6 @@ export class Psbt extends PsbtBase { this.inputs, inputIndex, keyPair.publicKey, - this.__CACHE.__TX, this.__CACHE, ); @@ -409,7 +408,6 @@ export class Psbt extends PsbtBase { this.inputs, inputIndex, keyPair.publicKey, - this.__CACHE.__TX, this.__CACHE, ); @@ -513,19 +511,13 @@ function getHashAndSighashType( inputs: PsbtInput[], inputIndex: number, pubkey: Buffer, - unsignedTx: Transaction, cache: PsbtCache, ): { hash: Buffer; sighashType: number; } { const input = checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig( - inputIndex, - input, - unsignedTx, - cache, - ); + const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); checkScriptForPubkey(pubkey, script, 'sign'); return { hash, @@ -678,9 +670,9 @@ interface HashForSigData { function getHashForSig( inputIndex: number, input: PsbtInput, - unsignedTx: Transaction, cache: PsbtCache, ): HashForSigData { + const unsignedTx = cache.__TX; const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; let script: Buffer; @@ -831,9 +823,9 @@ interface GetScriptReturn { function getScriptFromInput( inputIndex: number, input: PsbtInput, - unsignedTx: Transaction, cache: PsbtCache, ): GetScriptReturn { + const unsignedTx = cache.__TX; const res: GetScriptReturn = { script: null, isSegwit: false, From 479c56bbb4af63f11ac0df072add047f35fb8b81 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 12:58:03 +0900 Subject: [PATCH 076/111] Refactor: Re-order helper functions based on like-kind --- src/psbt.js | 606 +++++++++++++++++++------------------- ts_src/psbt.ts | 773 ++++++++++++++++++++++++------------------------- 2 files changed, 681 insertions(+), 698 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index d7cc03a..8e6f1f2 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -352,31 +352,120 @@ class Psbt extends bip174_1.Psbt { } } exports.Psbt = Psbt; -function addNonWitnessTxCache(cache, input, inputIndex) { - cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; - const tx = transaction_1.Transaction.fromBuffer(input.nonWitnessUtxo); - cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; - const self = cache; - const selfIndex = inputIndex; - delete input.nonWitnessUtxo; - Object.defineProperty(input, 'nonWitnessUtxo', { - enumerable: true, - get() { - const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; - const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; - if (buf !== undefined) { - return buf; - } else { - const newBuf = txCache.toBuffer(); - self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; - return newBuf; +function canFinalize(input, script, scriptType) { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m, input.partialSig); + default: + return false; + } +} +function hasSigs(neededSigs, partialSig) { + if (!partialSig) return false; + if (partialSig.length > neededSigs) throw new Error('Too many signatures'); + return partialSig.length === neededSigs; +} +function isFinalized(input) { + return !!input.finalScriptSig || !!input.finalScriptWitness; +} +function isPaymentFactory(payment) { + return script => { + try { + payment({ output: script }); + return true; + } catch (err) { + return false; + } + }; +} +const isP2MS = isPaymentFactory(payments.p2ms); +const isP2PK = isPaymentFactory(payments.p2pk); +const isP2PKH = isPaymentFactory(payments.p2pkh); +const isP2WPKH = isPaymentFactory(payments.p2wpkh); +function check32Bit(num) { + if ( + typeof num !== 'number' || + num !== Math.floor(num) || + num > 0xffffffff || + num < 0 + ) { + throw new Error('Invalid 32 bit integer'); + } +} +function checkFees(psbt, cache, opts) { + const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); + const vsize = cache.__EXTRACTED_TX.virtualSize(); + const satoshis = feeRate * vsize; + if (feeRate >= opts.maximumFeeRate) { + throw new Error( + `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, + ); + } +} +function checkInputsForPartialSig(inputs, action) { + inputs.forEach(input => { + let throws = false; + if ((input.partialSig || []).length === 0) return; + input.partialSig.forEach(pSig => { + const { hashType } = bscript.signature.decode(pSig.signature); + const whitelist = []; + const isAnyoneCanPay = + hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + const hashMod = hashType & 0x1f; + switch (hashMod) { + case transaction_1.Transaction.SIGHASH_ALL: + break; + case transaction_1.Transaction.SIGHASH_SINGLE: + case transaction_1.Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setSequence'); + break; } - }, - set(data) { - self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; - }, + if (whitelist.indexOf(action) === -1) { + throws = true; + } + }); + if (throws) { + throw new Error('Can not modify transaction, signatures exist.'); + } }); } +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) { + throw new Error( + `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, + ); + } +} +function checkTxEmpty(tx) { + const isEmpty = tx.ins.every( + input => + input.script && + input.script.length === 0 && + input.witness && + input.witness.length === 0, + ); + if (!isEmpty) { + throw new Error('Format Error: Transaction ScriptSigs are not empty'); + } +} function checkTxForDupeIns(tx, cache) { tx.ins.forEach(input => { checkTxInputCache(cache, input); @@ -390,18 +479,23 @@ function checkTxInputCache(cache, input) { if (cache.__TX_IN_CACHE[key]) throw new Error('Duplicate input detected.'); cache.__TX_IN_CACHE[key] = 1; } -function isFinalized(input) { - return !!input.finalScriptSig || !!input.finalScriptWitness; -} -function getHashAndSighashType(inputs, inputIndex, pubkey, cache) { - const input = utils_1.checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); - checkScriptForPubkey(pubkey, script, 'sign'); - return { - hash, - sighashType, +function scriptCheckerFactory(payment, paymentScriptName) { + return (inputIndex, scriptPubKey, redeemScript) => { + 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`, + ); + } }; } +const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); +const checkWitnessScript = scriptCheckerFactory( + payments.p2wsh, + 'Witness script', +); function getFinalScripts( script, scriptType, @@ -437,81 +531,14 @@ function getFinalScripts( finalScriptWitness, }; } -function getSortedSigs(script, partialSig) { - const p2ms = payments.p2ms({ output: script }); - // for each pubkey in order of p2ms script - return p2ms.pubkeys - .map(pk => { - // filter partialSig array by pubkey being equal - return ( - partialSig.filter(ps => { - return ps.pubkey.equals(pk); - })[0] || {} - ).signature; - // Any pubkey without a match will return undefined - // this last filter removes all the undefined items in the array. - }) - .filter(v => !!v); -} -function getPayment(script, scriptType, partialSig) { - let payment; - switch (scriptType) { - case 'multisig': - const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); - break; - case 'pubkey': - payment = payments.p2pk({ - output: script, - signature: partialSig[0].signature, - }); - break; - case 'pubkeyhash': - payment = payments.p2pkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - case 'witnesspubkeyhash': - payment = payments.p2wpkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - } - return payment; -} -function canFinalize(input, script, scriptType) { - switch (scriptType) { - case 'pubkey': - case 'pubkeyhash': - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m, input.partialSig); - default: - return false; - } -} -function checkScriptForPubkey(pubkey, script, 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) { - throw new Error( - `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, - ); - } +function getHashAndSighashType(inputs, inputIndex, pubkey, cache) { + const input = utils_1.checkForInput(inputs, inputIndex); + const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); + checkScriptForPubkey(pubkey, script, 'sign'); + return { + hash, + sighashType, + }; } function getHashForSig(inputIndex, input, cache) { const unsignedTx = cache.__TX; @@ -597,182 +624,6 @@ function getHashForSig(inputIndex, input, cache) { hash, }; } -function scriptCheckerFactory(payment, paymentScriptName) { - return (inputIndex, scriptPubKey, redeemScript) => { - 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`, - ); - } - }; -} -const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); -const checkWitnessScript = scriptCheckerFactory( - payments.p2wsh, - 'Witness script', -); -function isPaymentFactory(payment) { - return script => { - try { - payment({ output: script }); - return true; - } catch (err) { - return false; - } - }; -} -const isP2WPKH = isPaymentFactory(payments.p2wpkh); -const isP2PKH = isPaymentFactory(payments.p2pkh); -const isP2MS = isPaymentFactory(payments.p2ms); -const isP2PK = isPaymentFactory(payments.p2pk); -function classifyScript(script) { - if (isP2WPKH(script)) return 'witnesspubkeyhash'; - if (isP2PKH(script)) return 'pubkeyhash'; - if (isP2MS(script)) return 'multisig'; - if (isP2PK(script)) return 'pubkey'; - return 'nonstandard'; -} -function getScriptFromInput(inputIndex, input, cache) { - const unsignedTx = cache.__TX; - const res = { - script: null, - isSegwit: false, - isP2SH: false, - isP2WSH: false, - }; - if (input.nonWitnessUtxo) { - if (input.redeemScript) { - res.isP2SH = true; - res.script = input.redeemScript; - } else { - const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( - cache, - input, - inputIndex, - ); - const prevoutIndex = unsignedTx.ins[inputIndex].index; - res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; - } - } else if (input.witnessUtxo) { - res.isSegwit = true; - res.isP2SH = !!input.redeemScript; - res.isP2WSH = !!input.witnessScript; - if (input.witnessScript) { - res.script = input.witnessScript; - } else if (input.redeemScript) { - res.script = payments.p2wpkh({ - hash: input.redeemScript.slice(2), - }).output; - } else { - res.script = payments.p2wpkh({ - hash: input.witnessUtxo.script.slice(2), - }).output; - } - } - return res; -} -function hasSigs(neededSigs, partialSig) { - if (!partialSig) return false; - if (partialSig.length > neededSigs) throw new Error('Too many signatures'); - return partialSig.length === neededSigs; -} -function witnessStackToScriptWitness(witness) { - let buffer = Buffer.allocUnsafe(0); - function writeSlice(slice) { - buffer = Buffer.concat([buffer, Buffer.from(slice)]); - } - function writeVarInt(i) { - const currentLen = buffer.length; - const varintLen = varuint.encodingLength(i); - buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); - varuint.encode(i, buffer, currentLen); - } - function writeVarSlice(slice) { - writeVarInt(slice.length); - writeSlice(slice); - } - function writeVector(vector) { - writeVarInt(vector.length); - vector.forEach(writeVarSlice); - } - writeVector(witness); - return buffer; -} -function scriptWitnessToWitnessStack(buffer) { - let offset = 0; - function readSlice(n) { - offset += n; - return buffer.slice(offset - n, offset); - } - function readVarInt() { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - } - function readVarSlice() { - return readSlice(readVarInt()); - } - function readVector() { - const count = readVarInt(); - const vector = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } - return readVector(); -} -function range(n) { - return [...Array(n).keys()]; -} -function checkTxEmpty(tx) { - const isEmpty = tx.ins.every( - input => - input.script && - input.script.length === 0 && - input.witness && - input.witness.length === 0, - ); - if (!isEmpty) { - throw new Error('Format Error: Transaction ScriptSigs are not empty'); - } -} -function checkInputsForPartialSig(inputs, action) { - inputs.forEach(input => { - let throws = false; - if ((input.partialSig || []).length === 0) return; - input.partialSig.forEach(pSig => { - const { hashType } = bscript.signature.decode(pSig.signature); - const whitelist = []; - const isAnyoneCanPay = - hashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY; - if (isAnyoneCanPay) whitelist.push('addInput'); - const hashMod = hashType & 0x1f; - switch (hashMod) { - case transaction_1.Transaction.SIGHASH_ALL: - break; - case transaction_1.Transaction.SIGHASH_SINGLE: - case transaction_1.Transaction.SIGHASH_NONE: - whitelist.push('addOutput'); - whitelist.push('setSequence'); - break; - } - if (whitelist.indexOf(action) === -1) { - throws = true; - } - }); - if (throws) { - throw new Error('Can not modify transaction, signatures exist.'); - } - }); -} -function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(cache, input, inputIndex); - } - return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; -} function getInputAdder(cache) { const selfCache = cache; return (_inputData, txBuf) => { @@ -822,19 +673,162 @@ function getOutputAdder(cache) { return selfCache.__TX.toBuffer(); }; } -function checkFees(psbt, cache, opts) { - const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); - const vsize = cache.__EXTRACTED_TX.virtualSize(); - const satoshis = feeRate * vsize; - if (feeRate >= opts.maximumFeeRate) { - throw new Error( - `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + - `fees, which is ${feeRate} satoshi per byte for a transaction ` + - `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + - `byte). Use setMaximumFeeRate method to raise your threshold, or ` + - `pass true to the first arg of extractTransaction.`, - ); +function getPayment(script, scriptType, partialSig) { + let payment; + switch (scriptType) { + case 'multisig': + const sigs = getSortedSigs(script, partialSig); + payment = payments.p2ms({ + output: script, + signatures: sigs, + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; } + return payment; +} +function getScriptFromInput(inputIndex, input, cache) { + const unsignedTx = cache.__TX; + const res = { + script: null, + isSegwit: false, + isP2SH: false, + isP2WSH: false, + }; + if (input.nonWitnessUtxo) { + if (input.redeemScript) { + res.isP2SH = true; + res.script = input.redeemScript; + } else { + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); + const prevoutIndex = unsignedTx.ins[inputIndex].index; + res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; + } + } else if (input.witnessUtxo) { + res.isSegwit = true; + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript; + if (input.witnessScript) { + res.script = input.witnessScript; + } else if (input.redeemScript) { + res.script = payments.p2wpkh({ + hash: input.redeemScript.slice(2), + }).output; + } else { + res.script = payments.p2wpkh({ + hash: input.witnessUtxo.script.slice(2), + }).output; + } + } + return res; +} +function getSortedSigs(script, partialSig) { + const p2ms = payments.p2ms({ output: script }); + // for each pubkey in order of p2ms script + return p2ms.pubkeys + .map(pk => { + // filter partialSig array by pubkey being equal + return ( + partialSig.filter(ps => { + return ps.pubkey.equals(pk); + })[0] || {} + ).signature; + // Any pubkey without a match will return undefined + // this last filter removes all the undefined items in the array. + }) + .filter(v => !!v); +} +function scriptWitnessToWitnessStack(buffer) { + let offset = 0; + function readSlice(n) { + offset += n; + return buffer.slice(offset - n, offset); + } + function readVarInt() { + const vi = varuint.decode(buffer, offset); + offset += varuint.decode.bytes; + return vi; + } + function readVarSlice() { + return readSlice(readVarInt()); + } + function readVector() { + const count = readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + return readVector(); +} +function witnessStackToScriptWitness(witness) { + let buffer = Buffer.allocUnsafe(0); + function writeSlice(slice) { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + function writeVarInt(i) { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + function writeVarSlice(slice) { + writeVarInt(slice.length); + writeSlice(slice); + } + function writeVector(vector) { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + writeVector(witness); + return buffer; +} +function addNonWitnessTxCache(cache, input, inputIndex) { + cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo; + const tx = transaction_1.Transaction.fromBuffer(input.nonWitnessUtxo); + cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; + const self = cache; + const selfIndex = inputIndex; + delete input.nonWitnessUtxo; + Object.defineProperty(input, 'nonWitnessUtxo', { + enumerable: true, + get() { + const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; + if (buf !== undefined) { + return buf; + } else { + const newBuf = txCache.toBuffer(); + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; + return newBuf; + } + }, + set(data) { + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; + }, + }); } function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize, getAmounts) { let inputAmount = 0; @@ -857,13 +851,19 @@ function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize, getAmounts) { }); return inputAmount; } -function check32Bit(num) { - if ( - typeof num !== 'number' || - num !== Math.floor(num) || - num > 0xffffffff || - num < 0 - ) { - throw new Error('Invalid 32 bit integer'); +function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); } + return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; +} +function classifyScript(script) { + if (isP2WPKH(script)) return 'witnesspubkeyhash'; + if (isP2PKH(script)) return 'pubkeyhash'; + if (isP2MS(script)) return 'multisig'; + if (isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +} +function range(n) { + return [...Array(n).keys()]; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index ffe417f..617cde9 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -425,16 +425,6 @@ export class Psbt extends PsbtBase { } } -// -// -// -// -// Helper functions -// -// -// -// - interface PsbtCache { __NON_WITNESS_UTXO_TX_CACHE: Transaction[]; __NON_WITNESS_UTXO_BUF_CACHE: Buffer[]; @@ -455,38 +445,139 @@ interface PsbtOpts { maximumFeeRate: number; } -function addNonWitnessTxCache( - cache: PsbtCache, +function canFinalize( input: PsbtInput, - inputIndex: number, -): void { - cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!; + script: Buffer, + scriptType: string, +): boolean { + switch (scriptType) { + case 'pubkey': + case 'pubkeyhash': + case 'witnesspubkeyhash': + return hasSigs(1, input.partialSig); + case 'multisig': + const p2ms = payments.p2ms({ output: script }); + return hasSigs(p2ms.m!, input.partialSig); + default: + return false; + } +} - const tx = Transaction.fromBuffer(input.nonWitnessUtxo!); - cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; +function hasSigs(neededSigs: number, partialSig?: any[]): boolean { + if (!partialSig) return false; + if (partialSig.length > neededSigs) throw new Error('Too many signatures'); + return partialSig.length === neededSigs; +} - const self = cache; - const selfIndex = inputIndex; - delete input.nonWitnessUtxo; - Object.defineProperty(input, 'nonWitnessUtxo', { - enumerable: true, - get(): Buffer { - const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; - const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; - if (buf !== undefined) { - return buf; - } else { - const newBuf = txCache.toBuffer(); - self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; - return newBuf; +function isFinalized(input: PsbtInput): boolean { + return !!input.finalScriptSig || !!input.finalScriptWitness; +} + +function isPaymentFactory(payment: any): (script: Buffer) => boolean { + return (script: Buffer): boolean => { + try { + payment({ output: script }); + return true; + } catch (err) { + return false; + } + }; +} +const isP2MS = isPaymentFactory(payments.p2ms); +const isP2PK = isPaymentFactory(payments.p2pk); +const isP2PKH = isPaymentFactory(payments.p2pkh); +const isP2WPKH = isPaymentFactory(payments.p2wpkh); + +function check32Bit(num: number): void { + if ( + typeof num !== 'number' || + num !== Math.floor(num) || + num > 0xffffffff || + num < 0 + ) { + throw new Error('Invalid 32 bit integer'); + } +} + +function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { + const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); + const vsize = cache.__EXTRACTED_TX!.virtualSize(); + const satoshis = feeRate * vsize; + if (feeRate >= opts.maximumFeeRate) { + throw new Error( + `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + + `fees, which is ${feeRate} satoshi per byte for a transaction ` + + `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + + `byte). Use setMaximumFeeRate method to raise your threshold, or ` + + `pass true to the first arg of extractTransaction.`, + ); + } +} + +function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { + inputs.forEach(input => { + let throws = false; + if ((input.partialSig || []).length === 0) return; + input.partialSig!.forEach(pSig => { + const { hashType } = bscript.signature.decode(pSig.signature); + const whitelist: string[] = []; + const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY; + if (isAnyoneCanPay) whitelist.push('addInput'); + const hashMod = hashType & 0x1f; + switch (hashMod) { + case Transaction.SIGHASH_ALL: + break; + case Transaction.SIGHASH_SINGLE: + case Transaction.SIGHASH_NONE: + whitelist.push('addOutput'); + whitelist.push('setSequence'); + break; } - }, - set(data: Buffer): void { - self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; - }, + if (whitelist.indexOf(action) === -1) { + throws = true; + } + }); + if (throws) { + throw new Error('Can not modify transaction, signatures exist.'); + } }); } +function checkScriptForPubkey( + pubkey: Buffer, + 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) { + throw new Error( + `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, + ); + } +} + +function checkTxEmpty(tx: Transaction): void { + const isEmpty = tx.ins.every( + input => + input.script && + input.script.length === 0 && + input.witness && + input.witness.length === 0, + ); + if (!isEmpty) { + throw new Error('Format Error: Transaction ScriptSigs are not empty'); + } +} + function checkTxForDupeIns(tx: Transaction, cache: PsbtCache): void { tx.ins.forEach(input => { checkTxInputCache(cache, input); @@ -503,27 +594,31 @@ function checkTxInputCache( cache.__TX_IN_CACHE[key] = 1; } -function isFinalized(input: PsbtInput): boolean { - return !!input.finalScriptSig || !!input.finalScriptWitness; -} +function scriptCheckerFactory( + payment: any, + paymentScriptName: string, +): (idx: number, spk: Buffer, rs: Buffer) => void { + return ( + inputIndex: number, + scriptPubKey: Buffer, + redeemScript: Buffer, + ): void => { + const redeemScriptOutput = payment({ + redeem: { output: redeemScript }, + }).output as Buffer; -function getHashAndSighashType( - inputs: PsbtInput[], - inputIndex: number, - pubkey: Buffer, - cache: PsbtCache, -): { - hash: Buffer; - sighashType: number; -} { - const input = checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); - checkScriptForPubkey(pubkey, script, 'sign'); - return { - hash, - sighashType, + if (!scriptPubKey.equals(redeemScriptOutput)) { + throw new Error( + `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, + ); + } }; } +const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); +const checkWitnessScript = scriptCheckerFactory( + payments.p2wsh, + 'Witness script', +); function getFinalScripts( script: Buffer, @@ -566,112 +661,33 @@ function getFinalScripts( }; } -function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] { - const p2ms = payments.p2ms({ output: script }); - // for each pubkey in order of p2ms script - return p2ms - .pubkeys!.map(pk => { - // filter partialSig array by pubkey being equal - return ( - partialSig.filter(ps => { - return ps.pubkey.equals(pk); - })[0] || {} - ).signature; - // Any pubkey without a match will return undefined - // this last filter removes all the undefined items in the array. - }) - .filter(v => !!v); -} - -function getPayment( - script: Buffer, - scriptType: string, - partialSig: PartialSig[], -): payments.Payment { - let payment: payments.Payment; - switch (scriptType) { - case 'multisig': - const sigs = getSortedSigs(script, partialSig); - payment = payments.p2ms({ - output: script, - signatures: sigs, - }); - break; - case 'pubkey': - payment = payments.p2pk({ - output: script, - signature: partialSig[0].signature, - }); - break; - case 'pubkeyhash': - payment = payments.p2pkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - case 'witnesspubkeyhash': - payment = payments.p2wpkh({ - output: script, - pubkey: partialSig[0].pubkey, - signature: partialSig[0].signature, - }); - break; - } - return payment!; -} - -function canFinalize( - input: PsbtInput, - script: Buffer, - scriptType: string, -): boolean { - switch (scriptType) { - case 'pubkey': - case 'pubkeyhash': - case 'witnesspubkeyhash': - return hasSigs(1, input.partialSig); - case 'multisig': - const p2ms = payments.p2ms({ output: script }); - return hasSigs(p2ms.m!, input.partialSig); - default: - return false; - } -} - -function checkScriptForPubkey( +function getHashAndSighashType( + inputs: PsbtInput[], + inputIndex: number, pubkey: Buffer, - 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) { - throw new Error( - `Can not ${action} for this input with the key ${pubkey.toString('hex')}`, - ); - } -} - -interface HashForSigData { - script: Buffer; + cache: PsbtCache, +): { hash: Buffer; sighashType: number; +} { + const input = checkForInput(inputs, inputIndex); + const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); + checkScriptForPubkey(pubkey, script, 'sign'); + return { + hash, + sighashType, + }; } function getHashForSig( inputIndex: number, input: PsbtInput, cache: PsbtCache, -): HashForSigData { +): { + script: Buffer; + hash: Buffer; + sighashType: number; +} { const unsignedTx = cache.__TX; const sighashType = input.sighashType || Transaction.SIGHASH_ALL; let hash: Buffer; @@ -760,231 +776,6 @@ function getHashForSig( }; } -type ScriptCheckerFunction = (idx: number, spk: Buffer, rs: Buffer) => void; - -function scriptCheckerFactory( - payment: any, - paymentScriptName: string, -): ScriptCheckerFunction { - return ( - inputIndex: number, - scriptPubKey: Buffer, - redeemScript: Buffer, - ): void => { - const redeemScriptOutput = payment({ - redeem: { output: redeemScript }, - }).output as Buffer; - - if (!scriptPubKey.equals(redeemScriptOutput)) { - throw new Error( - `${paymentScriptName} for input #${inputIndex} doesn't match the scriptPubKey in the prevout`, - ); - } - }; -} - -const checkRedeemScript = scriptCheckerFactory(payments.p2sh, 'Redeem script'); -const checkWitnessScript = scriptCheckerFactory( - payments.p2wsh, - 'Witness script', -); - -type isPaymentFunction = (script: Buffer) => boolean; - -function isPaymentFactory(payment: any): isPaymentFunction { - return (script: Buffer): boolean => { - try { - payment({ output: script }); - return true; - } catch (err) { - return false; - } - }; -} -const isP2WPKH = isPaymentFactory(payments.p2wpkh); -const isP2PKH = isPaymentFactory(payments.p2pkh); -const isP2MS = isPaymentFactory(payments.p2ms); -const isP2PK = isPaymentFactory(payments.p2pk); - -function classifyScript(script: Buffer): string { - if (isP2WPKH(script)) return 'witnesspubkeyhash'; - if (isP2PKH(script)) return 'pubkeyhash'; - if (isP2MS(script)) return 'multisig'; - if (isP2PK(script)) return 'pubkey'; - return 'nonstandard'; -} - -interface GetScriptReturn { - script: Buffer | null; - isSegwit: boolean; - isP2SH: boolean; - isP2WSH: boolean; -} -function getScriptFromInput( - inputIndex: number, - input: PsbtInput, - cache: PsbtCache, -): GetScriptReturn { - const unsignedTx = cache.__TX; - const res: GetScriptReturn = { - script: null, - isSegwit: false, - isP2SH: false, - isP2WSH: false, - }; - if (input.nonWitnessUtxo) { - if (input.redeemScript) { - res.isP2SH = true; - res.script = input.redeemScript; - } else { - const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( - cache, - input, - inputIndex, - ); - const prevoutIndex = unsignedTx.ins[inputIndex].index; - res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; - } - } else if (input.witnessUtxo) { - res.isSegwit = true; - res.isP2SH = !!input.redeemScript; - res.isP2WSH = !!input.witnessScript; - if (input.witnessScript) { - res.script = input.witnessScript; - } else if (input.redeemScript) { - res.script = payments.p2wpkh({ - hash: input.redeemScript.slice(2), - }).output!; - } else { - res.script = payments.p2wpkh({ - hash: input.witnessUtxo.script.slice(2), - }).output!; - } - } - return res; -} - -function hasSigs(neededSigs: number, partialSig?: any[]): boolean { - if (!partialSig) return false; - if (partialSig.length > neededSigs) throw new Error('Too many signatures'); - return partialSig.length === neededSigs; -} - -function witnessStackToScriptWitness(witness: Buffer[]): Buffer { - let buffer = Buffer.allocUnsafe(0); - - function writeSlice(slice: Buffer): void { - buffer = Buffer.concat([buffer, Buffer.from(slice)]); - } - - function writeVarInt(i: number): void { - const currentLen = buffer.length; - const varintLen = varuint.encodingLength(i); - - buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); - varuint.encode(i, buffer, currentLen); - } - - function writeVarSlice(slice: Buffer): void { - writeVarInt(slice.length); - writeSlice(slice); - } - - function writeVector(vector: Buffer[]): void { - writeVarInt(vector.length); - vector.forEach(writeVarSlice); - } - - writeVector(witness); - - return buffer; -} - -function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { - let offset = 0; - - function readSlice(n: number): Buffer { - offset += n; - return buffer.slice(offset - n, offset); - } - - function readVarInt(): number { - const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; - return vi; - } - - function readVarSlice(): Buffer { - return readSlice(readVarInt()); - } - - function readVector(): Buffer[] { - const count = readVarInt(); - const vector: Buffer[] = []; - for (let i = 0; i < count; i++) vector.push(readVarSlice()); - return vector; - } - - return readVector(); -} - -function range(n: number): number[] { - return [...Array(n).keys()]; -} - -function checkTxEmpty(tx: Transaction): void { - const isEmpty = tx.ins.every( - input => - input.script && - input.script.length === 0 && - input.witness && - input.witness.length === 0, - ); - if (!isEmpty) { - throw new Error('Format Error: Transaction ScriptSigs are not empty'); - } -} - -function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { - inputs.forEach(input => { - let throws = false; - if ((input.partialSig || []).length === 0) return; - input.partialSig!.forEach(pSig => { - const { hashType } = bscript.signature.decode(pSig.signature); - const whitelist: string[] = []; - const isAnyoneCanPay = hashType & Transaction.SIGHASH_ANYONECANPAY; - if (isAnyoneCanPay) whitelist.push('addInput'); - const hashMod = hashType & 0x1f; - switch (hashMod) { - case Transaction.SIGHASH_ALL: - break; - case Transaction.SIGHASH_SINGLE: - case Transaction.SIGHASH_NONE: - whitelist.push('addOutput'); - whitelist.push('setSequence'); - break; - } - if (whitelist.indexOf(action) === -1) { - throws = true; - } - }); - if (throws) { - throw new Error('Can not modify transaction, signatures exist.'); - } - }); -} - -function nonWitnessUtxoTxFromCache( - cache: PsbtCache, - input: PsbtInput, - inputIndex: number, -): Transaction { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { - addNonWitnessTxCache(cache, input, inputIndex); - } - return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; -} - function getInputAdder( cache: PsbtCache, ): (_inputData: TransactionInput, txBuf: Buffer) => Buffer { @@ -1040,19 +831,199 @@ function getOutputAdder( }; } -function checkFees(psbt: Psbt, cache: PsbtCache, opts: PsbtOpts): void { - const feeRate = cache.__FEE_RATE || psbt.getFeeRate(); - const vsize = cache.__EXTRACTED_TX!.virtualSize(); - const satoshis = feeRate * vsize; - if (feeRate >= opts.maximumFeeRate) { - throw new Error( - `Warning: You are paying around ${(satoshis / 1e8).toFixed(8)} in ` + - `fees, which is ${feeRate} satoshi per byte for a transaction ` + - `with a VSize of ${vsize} bytes (segwit counted as 0.25 byte per ` + - `byte). Use setMaximumFeeRate method to raise your threshold, or ` + - `pass true to the first arg of extractTransaction.`, - ); +function getPayment( + script: Buffer, + scriptType: string, + partialSig: PartialSig[], +): payments.Payment { + let payment: payments.Payment; + switch (scriptType) { + case 'multisig': + const sigs = getSortedSigs(script, partialSig); + payment = payments.p2ms({ + output: script, + signatures: sigs, + }); + break; + case 'pubkey': + payment = payments.p2pk({ + output: script, + signature: partialSig[0].signature, + }); + break; + case 'pubkeyhash': + payment = payments.p2pkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; + case 'witnesspubkeyhash': + payment = payments.p2wpkh({ + output: script, + pubkey: partialSig[0].pubkey, + signature: partialSig[0].signature, + }); + break; } + return payment!; +} + +interface GetScriptReturn { + script: Buffer | null; + isSegwit: boolean; + isP2SH: boolean; + isP2WSH: boolean; +} +function getScriptFromInput( + inputIndex: number, + input: PsbtInput, + cache: PsbtCache, +): GetScriptReturn { + const unsignedTx = cache.__TX; + const res: GetScriptReturn = { + script: null, + isSegwit: false, + isP2SH: false, + isP2WSH: false, + }; + if (input.nonWitnessUtxo) { + if (input.redeemScript) { + res.isP2SH = true; + res.script = input.redeemScript; + } else { + const nonWitnessUtxoTx = nonWitnessUtxoTxFromCache( + cache, + input, + inputIndex, + ); + const prevoutIndex = unsignedTx.ins[inputIndex].index; + res.script = nonWitnessUtxoTx.outs[prevoutIndex].script; + } + } else if (input.witnessUtxo) { + res.isSegwit = true; + res.isP2SH = !!input.redeemScript; + res.isP2WSH = !!input.witnessScript; + if (input.witnessScript) { + res.script = input.witnessScript; + } else if (input.redeemScript) { + res.script = payments.p2wpkh({ + hash: input.redeemScript.slice(2), + }).output!; + } else { + res.script = payments.p2wpkh({ + hash: input.witnessUtxo.script.slice(2), + }).output!; + } + } + return res; +} + +function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] { + const p2ms = payments.p2ms({ output: script }); + // for each pubkey in order of p2ms script + return p2ms + .pubkeys!.map(pk => { + // filter partialSig array by pubkey being equal + return ( + partialSig.filter(ps => { + return ps.pubkey.equals(pk); + })[0] || {} + ).signature; + // Any pubkey without a match will return undefined + // this last filter removes all the undefined items in the array. + }) + .filter(v => !!v); +} + +function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { + let offset = 0; + + function readSlice(n: number): Buffer { + offset += n; + return buffer.slice(offset - n, offset); + } + + function readVarInt(): number { + const vi = varuint.decode(buffer, offset); + offset += varuint.decode.bytes; + return vi; + } + + function readVarSlice(): Buffer { + return readSlice(readVarInt()); + } + + function readVector(): Buffer[] { + const count = readVarInt(); + const vector: Buffer[] = []; + for (let i = 0; i < count; i++) vector.push(readVarSlice()); + return vector; + } + + return readVector(); +} + +function witnessStackToScriptWitness(witness: Buffer[]): Buffer { + let buffer = Buffer.allocUnsafe(0); + + function writeSlice(slice: Buffer): void { + buffer = Buffer.concat([buffer, Buffer.from(slice)]); + } + + function writeVarInt(i: number): void { + const currentLen = buffer.length; + const varintLen = varuint.encodingLength(i); + + buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]); + varuint.encode(i, buffer, currentLen); + } + + function writeVarSlice(slice: Buffer): void { + writeVarInt(slice.length); + writeSlice(slice); + } + + function writeVector(vector: Buffer[]): void { + writeVarInt(vector.length); + vector.forEach(writeVarSlice); + } + + writeVector(witness); + + return buffer; +} + +function addNonWitnessTxCache( + cache: PsbtCache, + input: PsbtInput, + inputIndex: number, +): void { + cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!; + + const tx = Transaction.fromBuffer(input.nonWitnessUtxo!); + cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx; + + const self = cache; + const selfIndex = inputIndex; + delete input.nonWitnessUtxo; + Object.defineProperty(input, 'nonWitnessUtxo', { + enumerable: true, + get(): Buffer { + const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex]; + const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex]; + if (buf !== undefined) { + return buf; + } else { + const newBuf = txCache.toBuffer(); + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf; + return newBuf; + } + }, + set(data: Buffer): void { + self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data; + }, + }); } function inputFinalizeGetAmts( @@ -1083,13 +1054,25 @@ function inputFinalizeGetAmts( return inputAmount; } -function check32Bit(num: number): void { - if ( - typeof num !== 'number' || - num !== Math.floor(num) || - num > 0xffffffff || - num < 0 - ) { - throw new Error('Invalid 32 bit integer'); +function nonWitnessUtxoTxFromCache( + cache: PsbtCache, + input: PsbtInput, + inputIndex: number, +): Transaction { + if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + addNonWitnessTxCache(cache, input, inputIndex); } + return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; +} + +function classifyScript(script: Buffer): string { + if (isP2WPKH(script)) return 'witnesspubkeyhash'; + if (isP2PKH(script)) return 'pubkeyhash'; + if (isP2MS(script)) return 'multisig'; + if (isP2PK(script)) return 'pubkey'; + return 'nonstandard'; +} + +function range(n: number): number[] { + return [...Array(n).keys()]; } From 0f76aa935ad1effdb2904f36e6bb849128df1dd9 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 13:02:34 +0900 Subject: [PATCH 077/111] Refactor: Use varint from BIP174 --- src/psbt.js | 2 +- ts_src/psbt.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 8e6f1f2..aa73949 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1,6 +1,7 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const bip174_1 = require('bip174'); +const varuint = require('bip174/src/lib/converter/varint'); const utils_1 = require('bip174/src/lib/utils'); const address_1 = require('./address'); const bufferutils_1 = require('./bufferutils'); @@ -10,7 +11,6 @@ const networks_1 = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); -const varuint = require('varuint-bitcoin'); const DEFAULT_OPTS = { network: networks_1.bitcoin, maximumFeeRate: 5000, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 617cde9..a78b37b 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,4 +1,5 @@ import { Psbt as PsbtBase } from 'bip174'; +import * as varuint from 'bip174/src/lib/converter/varint'; import { NonWitnessUtxo, PartialSig, @@ -19,7 +20,6 @@ import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; -const varuint = require('varuint-bitcoin'); const DEFAULT_OPTS: PsbtOpts = { network: btcNetwork, @@ -946,7 +946,7 @@ function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { function readVarInt(): number { const vi = varuint.decode(buffer, offset); - offset += varuint.decode.bytes; + offset += (varuint.decode as any).bytes; return vi; } From ba33f0317f3d102be69ad269eb584a12ff7d5bcc Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 13:55:02 +0900 Subject: [PATCH 078/111] Add check for spending more than you have --- src/psbt.js | 36 ++++++++++++++++-------------------- ts_src/psbt.ts | 45 ++++++++++++++++++++------------------------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index aa73949..4c80a31 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -167,8 +167,7 @@ class Psbt extends bip174_1.Psbt { } if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; const tx = c.__TX.clone(); - inputFinalizeGetAmts(this.inputs, tx, c, true, false); - c.__EXTRACTED_TX = tx; + inputFinalizeGetAmts(this.inputs, tx, c, true); return tx; } getFeeRate() { @@ -184,18 +183,7 @@ class Psbt extends bip174_1.Psbt { } else { tx = c.__TX.clone(); } - const inputAmount = inputFinalizeGetAmts( - this.inputs, - tx, - c, - mustFinalize, - true, - ); - c.__EXTRACTED_TX = tx; - const outputAmount = tx.outs.reduce((total, o) => total + o.value, 0); - const fee = inputAmount - outputAmount; - const bytes = tx.virtualSize(); - c.__FEE_RATE = Math.floor(fee / bytes); + inputFinalizeGetAmts(this.inputs, tx, c, mustFinalize); return c.__FEE_RATE; } finalizeAllInputs() { @@ -830,7 +818,7 @@ function addNonWitnessTxCache(cache, input, inputIndex) { }, }); } -function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize, getAmounts) { +function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize) { let inputAmount = 0; inputs.forEach((input, idx) => { if (mustFinalize && input.finalScriptSig) @@ -840,22 +828,30 @@ function inputFinalizeGetAmts(inputs, tx, cache, mustFinalize, getAmounts) { input.finalScriptWitness, ); } - if (getAmounts && input.witnessUtxo) { + if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; - } else if (getAmounts && input.nonWitnessUtxo) { + } else if (input.nonWitnessUtxo) { const nwTx = nonWitnessUtxoTxFromCache(cache, input, idx); const vout = tx.ins[idx].index; const out = nwTx.outs[vout]; inputAmount += out.value; } }); - return inputAmount; + const outputAmount = tx.outs.reduce((total, o) => total + o.value, 0); + const fee = inputAmount - outputAmount; + if (fee < 0) { + throw new Error('Outputs are spending more than Inputs'); + } + const bytes = tx.virtualSize(); + cache.__EXTRACTED_TX = tx; + cache.__FEE_RATE = Math.floor(fee / bytes); } function nonWitnessUtxoTxFromCache(cache, input, inputIndex) { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + const c = cache.__NON_WITNESS_UTXO_TX_CACHE; + if (!c[inputIndex]) { addNonWitnessTxCache(cache, input, inputIndex); } - return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + return c[inputIndex]; } function classifyScript(script) { if (isP2WPKH(script)) return 'witnesspubkeyhash'; diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index a78b37b..2e4c826 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -212,8 +212,7 @@ export class Psbt extends PsbtBase { } if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; const tx = c.__TX.clone(); - inputFinalizeGetAmts(this.inputs, tx, c, true, false); - c.__EXTRACTED_TX = tx; + inputFinalizeGetAmts(this.inputs, tx, c, true); return tx; } @@ -230,22 +229,8 @@ export class Psbt extends PsbtBase { } else { tx = c.__TX.clone(); } - const inputAmount = inputFinalizeGetAmts( - this.inputs, - tx, - c, - mustFinalize, - true, - ); - c.__EXTRACTED_TX = tx; - const outputAmount = (tx.outs as Output[]).reduce( - (total, o) => total + o.value, - 0, - ); - const fee = inputAmount - outputAmount; - const bytes = tx.virtualSize(); - c.__FEE_RATE = Math.floor(fee / bytes); - return c.__FEE_RATE; + inputFinalizeGetAmts(this.inputs, tx, c, mustFinalize); + return c.__FEE_RATE!; } finalizeAllInputs(): { @@ -1031,8 +1016,7 @@ function inputFinalizeGetAmts( tx: Transaction, cache: PsbtCache, mustFinalize: boolean, - getAmounts: boolean, -): number { +): void { let inputAmount = 0; inputs.forEach((input, idx) => { if (mustFinalize && input.finalScriptSig) @@ -1042,16 +1026,26 @@ function inputFinalizeGetAmts( input.finalScriptWitness, ); } - if (getAmounts && input.witnessUtxo) { + if (input.witnessUtxo) { inputAmount += input.witnessUtxo.value; - } else if (getAmounts && input.nonWitnessUtxo) { + } else if (input.nonWitnessUtxo) { const nwTx = nonWitnessUtxoTxFromCache(cache, input, idx); const vout = tx.ins[idx].index; const out = nwTx.outs[vout] as Output; inputAmount += out.value; } }); - return inputAmount; + const outputAmount = (tx.outs as Output[]).reduce( + (total, o) => total + o.value, + 0, + ); + const fee = inputAmount - outputAmount; + if (fee < 0) { + throw new Error('Outputs are spending more than Inputs'); + } + const bytes = tx.virtualSize(); + cache.__EXTRACTED_TX = tx; + cache.__FEE_RATE = Math.floor(fee / bytes); } function nonWitnessUtxoTxFromCache( @@ -1059,10 +1053,11 @@ function nonWitnessUtxoTxFromCache( input: PsbtInput, inputIndex: number, ): Transaction { - if (!cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]) { + const c = cache.__NON_WITNESS_UTXO_TX_CACHE; + if (!c[inputIndex]) { addNonWitnessTxCache(cache, input, inputIndex); } - return cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex]; + return c[inputIndex]; } function classifyScript(script: Buffer): string { From b8c341dea0be5a8cd3feb313a71b697b31a99b4a Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 15:45:56 +0900 Subject: [PATCH 079/111] Finalize should chain this as well. --- src/psbt.js | 30 ++++++++++++--------- test/integration/transactions-psbt.js | 23 ++++++++-------- ts_src/psbt.ts | 38 ++++++++++++++------------- types/psbt.d.ts | 8 +++--- 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 4c80a31..58a56dd 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -187,14 +187,9 @@ class Psbt extends bip174_1.Psbt { return c.__FEE_RATE; } finalizeAllInputs() { - const inputResults = range(this.inputs.length).map(idx => - this.finalizeInput(idx), - ); - const result = inputResults.every(val => val === true); - return { - result, - inputResults, - }; + utils_1.checkForInput(this.inputs, 0); // making sure we have at least one + range(this.inputs.length).forEach(idx => this.finalizeInput(idx)); + return this; } finalizeInput(inputIndex) { const input = utils_1.checkForInput(this.inputs, inputIndex); @@ -203,9 +198,10 @@ class Psbt extends bip174_1.Psbt { input, this.__CACHE, ); - if (!script) return false; + if (!script) throw new Error(`No script found for input #${inputIndex}`); const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) return false; + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); const { finalScriptSig, finalScriptWitness } = getFinalScripts( script, scriptType, @@ -218,9 +214,17 @@ class Psbt extends bip174_1.Psbt { this.addFinalScriptSigToInput(inputIndex, finalScriptSig); if (finalScriptWitness) this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); - if (!finalScriptSig && !finalScriptWitness) return false; + if (!finalScriptSig && !finalScriptWitness) + throw new Error(`Unknown error finalizing input #${inputIndex}`); this.clearFinalizedInput(inputIndex); - return true; + return this; + } + validateAllSignatures() { + utils_1.checkForInput(this.inputs, 0); // making sure we have at least one + const results = range(this.inputs.length).map(idx => + this.validateSignatures(idx), + ); + return results.reduce((final, res) => res === true && final, true); } validateSignatures(inputIndex, pubkey) { const input = this.inputs[inputIndex]; @@ -261,7 +265,7 @@ class Psbt extends bip174_1.Psbt { // as input information is added, then eventually // optimize this method. const results = []; - for (const [i] of this.inputs.entries()) { + for (const i of range(this.inputs.length)) { try { this.signInput(i, keyPair); results.push(true); diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index 37348c1..a7398a8 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -225,26 +225,25 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { } = inputData assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript }, inputData) } + const keyPair = p2sh.keys[0] + const outputData = { + script: p2sh.payment.output, // sending to myself for fun + value: 2e4 + } - const psbt = new bitcoin.Psbt({ network: regtest }) + const tx = new bitcoin.Psbt() .addInput(inputData) - .addOutput({ - address: regtestUtils.RANDOM_ADDRESS, - value: 2e4 - }) - .signInput(0, p2sh.keys[0]) - - assert.strictEqual(psbt.validateSignatures(0), true) - psbt.finalizeAllInputs() - - const tx = psbt.extractTransaction() + .addOutput(outputData) + .sign(keyPair) + .finalizeAllInputs() + .extractTransaction() // build and broadcast to the Bitcoin RegTest network await regtestUtils.broadcast(tx.toHex()) await regtestUtils.verify({ txId: tx.getId(), - address: regtestUtils.RANDOM_ADDRESS, + address: p2sh.payment.address, vout: 0, value: 2e4 }) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 2e4c826..61ed15e 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -233,31 +233,24 @@ export class Psbt extends PsbtBase { return c.__FEE_RATE!; } - finalizeAllInputs(): { - result: boolean; - inputResults: boolean[]; - } { - const inputResults = range(this.inputs.length).map(idx => - this.finalizeInput(idx), - ); - const result = inputResults.every(val => val === true); - return { - result, - inputResults, - }; + finalizeAllInputs(): this { + checkForInput(this.inputs, 0); // making sure we have at least one + range(this.inputs.length).forEach(idx => this.finalizeInput(idx)); + return this; } - finalizeInput(inputIndex: number): boolean { + finalizeInput(inputIndex: number): this { const input = checkForInput(this.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, this.__CACHE, ); - if (!script) return false; + if (!script) throw new Error(`No script found for input #${inputIndex}`); const scriptType = classifyScript(script); - if (!canFinalize(input, script, scriptType)) return false; + if (!canFinalize(input, script, scriptType)) + throw new Error(`Can not finalize input #${inputIndex}`); const { finalScriptSig, finalScriptWitness } = getFinalScripts( script, @@ -272,10 +265,19 @@ export class Psbt extends PsbtBase { this.addFinalScriptSigToInput(inputIndex, finalScriptSig); if (finalScriptWitness) this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); - if (!finalScriptSig && !finalScriptWitness) return false; + if (!finalScriptSig && !finalScriptWitness) + throw new Error(`Unknown error finalizing input #${inputIndex}`); this.clearFinalizedInput(inputIndex); - return true; + return this; + } + + validateAllSignatures(): boolean { + checkForInput(this.inputs, 0); // making sure we have at least one + const results = range(this.inputs.length).map(idx => + this.validateSignatures(idx), + ); + return results.reduce((final, res) => res === true && final, true); } validateSignatures(inputIndex: number, pubkey?: Buffer): boolean { @@ -319,7 +321,7 @@ export class Psbt extends PsbtBase { // as input information is added, then eventually // optimize this method. const results: boolean[] = []; - for (const [i] of this.inputs.entries()) { + for (const i of range(this.inputs.length)) { try { this.signInput(i, keyPair); results.push(true); diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 775f3b0..40571fa 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -20,11 +20,9 @@ export declare class Psbt extends PsbtBase { addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; extractTransaction(disableFeeCheck?: boolean): Transaction; getFeeRate(): number; - finalizeAllInputs(): { - result: boolean; - inputResults: boolean[]; - }; - finalizeInput(inputIndex: number): boolean; + finalizeAllInputs(): this; + finalizeInput(inputIndex: number): this; + validateAllSignatures(): boolean; validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; sign(keyPair: Signer): this; signAsync(keyPair: SignerAsync): Promise; From 01c7ac39b611e63cfff38d5292b295fe15e65dd3 Mon Sep 17 00:00:00 2001 From: junderw Date: Tue, 9 Jul 2019 18:03:15 +0900 Subject: [PATCH 080/111] Add clone, addInputs, addOutputs --- src/psbt.js | 14 ++ test/integration/transactions-psbt.js | 339 ++++++++++++++------------ ts_src/psbt.ts | 17 ++ types/psbt.d.ts | 3 + 4 files changed, 221 insertions(+), 152 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 58a56dd..33a32a3 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -95,6 +95,12 @@ class Psbt extends bip174_1.Psbt { get inputCount() { return this.inputs.length; } + clone() { + // TODO: more efficient cloning + const res = Psbt.fromBuffer(this.toBuffer()); + res.opts = JSON.parse(JSON.stringify(this.opts)); + return res; + } setMaximumFeeRate(satoshiPerByte) { check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw this.opts.maximumFeeRate = satoshiPerByte; @@ -129,6 +135,10 @@ class Psbt extends bip174_1.Psbt { c.__EXTRACTED_TX = undefined; return this; } + addInputs(inputDatas) { + inputDatas.forEach(inputData => this.addInput(inputData)); + return this; + } addInput(inputData) { checkInputsForPartialSig(this.inputs, 'addInput'); const c = this.__CACHE; @@ -138,6 +148,10 @@ class Psbt extends bip174_1.Psbt { c.__EXTRACTED_TX = undefined; return this; } + addOutputs(outputDatas) { + outputDatas.forEach(outputData => this.addOutput(outputData)); + return this; + } addOutput(outputData) { checkInputsForPartialSig(this.inputs, 'addOutput'); const { address } = outputData; diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index a7398a8..96ae073 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -1,17 +1,19 @@ -const { describe, it } = require('mocha') -const assert = require('assert') -const bitcoin = require('../../') -const regtestUtils = require('./_regtest') -const regtest = regtestUtils.network +const { describe, it } = require('mocha'); +const assert = require('assert'); +const bitcoin = require('../../'); +const regtestUtils = require('./_regtest'); +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. + 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', @@ -21,18 +23,18 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // 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', + '452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' + + 'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' + + '9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' + + '631e5e1e66009ce3710ceea5b1ad13ffffffff01' + + // value in satoshis (Int64LE) = 0x015f90 = 90000 + '905f010000000000' + + // scriptPubkey length + '19' + + // scriptPubkey + '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' + + // locktime + '00000000', 'hex', ), @@ -47,40 +49,50 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // }, // Not featured here: redeemScript. A Buffer of the redeemScript - }) + }); psbt.addOutput({ address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', - value: 80000 - }) - psbt.signInput(0, alice) - psbt.validateSignatures(0) - psbt.finalizeAllInputs() + value: 80000, + }); + psbt.signInput(0, alice); + psbt.validateSignatures(0); + psbt.finalizeAllInputs(); assert.strictEqual( psbt.extractTransaction().toHex(), '02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' + - 'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' + - 'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' + - '9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' + - 'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' + - '08a22724efa6f6a07b0ec4c79aa88ac00000000', - ) - }) + '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') - const alice2 = createPayment('p2pkh') + const alice1 = createPayment('p2pkh'); + const alice2 = createPayment('p2pkh'); // give Alice 2 unspent outputs - const inputData1 = await getInputData(5e4, alice1.payment, false, 'noredeem') - const inputData2 = await getInputData(7e4, alice2.payment, false, 'noredeem') + const inputData1 = await getInputData( + 5e4, + alice1.payment, + false, + 'noredeem', + ); + const inputData2 = await getInputData( + 7e4, + alice2.payment, + false, + 'noredeem', + ); { const { hash, // string of txid or Buffer of tx hash. (txid and hash are reverse order) index, // the output index of the txo you are spending nonWitnessUtxo, // the full previous transaction as a Buffer - } = inputData1 - assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1) + } = inputData1; + assert.deepStrictEqual({ hash, index, nonWitnessUtxo }, inputData1); } // network is only needed if you pass an address to addOutput @@ -90,12 +102,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { .addInput(inputData2) // alice2 unspent .addOutput({ address: 'mwCwTceJvYV27KXBc3NJZys6CjsgsoeHmf', - value: 8e4 + value: 8e4, }) // the actual "spend" .addOutput({ address: alice2.payment.address, // OR script, which is a Buffer. - value: 1e4 - }) // Alice's change + value: 1e4, + }); // Alice's change // (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee // Let's show a new feature with PSBT. @@ -103,231 +115,248 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // (this is not necessary, but a nice feature) // encode to send out to the signers - const psbtBaseText = psbt.toBase64() + const psbtBaseText = psbt.toBase64(); // each signer imports - const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText) - const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText) + const signer1 = bitcoin.Psbt.fromBase64(psbtBaseText); + const signer2 = bitcoin.Psbt.fromBase64(psbtBaseText); // Alice signs each input with the respective private keys // signInput and signInputAsync are better // (They take the input index explicitly as the first arg) - signer1.sign(alice1.keys[0]) - signer2.sign(alice2.keys[0]) + signer1.sign(alice1.keys[0]); + signer2.sign(alice2.keys[0]); // If your signer object's sign method returns a promise, use the following // await signer2.signAsync(alice2.keys[0]) // encode to send back to combiner (signer 1 and 2 are not near each other) - const s1text = signer1.toBase64() - const s2text = signer2.toBase64() + const s1text = signer1.toBase64(); + const s2text = signer2.toBase64(); - const final1 = bitcoin.Psbt.fromBase64(s1text) - const final2 = bitcoin.Psbt.fromBase64(s2text) + const final1 = bitcoin.Psbt.fromBase64(s1text); + const final2 = bitcoin.Psbt.fromBase64(s2text); // final1.combine(final2) would give the exact same result - psbt.combine(final1, final2) + 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) + 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() + psbt.finalizeAllInputs(); // it returns an array of the success of each input, also a result attribute // which is true if all array items are true. // build and broadcast our RegTest network - await regtestUtils.broadcast(psbt.extractTransaction().toHex()) + await regtestUtils.broadcast(psbt.extractTransaction().toHex()); // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839 - }) + }); it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', async () => { - const alice1 = createPayment('p2pkh') - const inputData1 = await getInputData(2e5, alice1.payment, false, 'noredeem') + const alice1 = createPayment('p2pkh'); + const inputData1 = await getInputData( + 2e5, + alice1.payment, + false, + 'noredeem', + ); - const data = Buffer.from('bitcoinjs-lib', 'utf8') - const embed = bitcoin.payments.embed({ data: [data] }) + const data = Buffer.from('bitcoinjs-lib', 'utf8'); + const embed = bitcoin.payments.embed({ data: [data] }); const psbt = new bitcoin.Psbt({ network: regtest }) .addInput(inputData1) .addOutput({ script: embed.output, - value: 1000 + value: 1000, }) .addOutput({ address: regtestUtils.RANDOM_ADDRESS, - value: 1e5 + value: 1e5, }) - .signInput(0, alice1.keys[0]) + .signInput(0, alice1.keys[0]); - assert.strictEqual(psbt.validateSignatures(0), true) - psbt.finalizeAllInputs() + assert.strictEqual(psbt.validateSignatures(0), true); + psbt.finalizeAllInputs(); // build and broadcast to the RegTest network - await regtestUtils.broadcast(psbt.extractTransaction().toHex()) - }) + await regtestUtils.broadcast(psbt.extractTransaction().toHex()); + }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2MS(2 of 4)) (multisig) input', async () => { - const multisig = createPayment('p2sh-p2ms(2 of 4)') - const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh') + const multisig = createPayment('p2sh-p2ms(2 of 4)'); + const inputData1 = await getInputData(2e4, multisig.payment, false, 'p2sh'); { const { hash, index, nonWitnessUtxo, redeemScript, // NEW: P2SH needs to give redeemScript when adding an input. - } = inputData1 - assert.deepStrictEqual({ hash, index, nonWitnessUtxo, redeemScript }, inputData1) + } = inputData1; + assert.deepStrictEqual( + { hash, index, nonWitnessUtxo, redeemScript }, + inputData1, + ); } const psbt = new bitcoin.Psbt({ network: regtest }) .addInput(inputData1) .addOutput({ address: regtestUtils.RANDOM_ADDRESS, - value: 1e4 + value: 1e4, }) .signInput(0, multisig.keys[0]) - .signInput(0, multisig.keys[2]) + .signInput(0, multisig.keys[2]); - assert.strictEqual(psbt.validateSignatures(0), true) - assert.strictEqual(psbt.validateSignatures(0, multisig.keys[0].publicKey), true) + 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() + psbt.validateSignatures(0, multisig.keys[3].publicKey); + }, new RegExp('No signatures for this pubkey')); + psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction() + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()) + await regtestUtils.broadcast(tx.toHex()); await regtestUtils.verify({ txId: tx.getId(), address: regtestUtils.RANDOM_ADDRESS, vout: 0, - value: 1e4 - }) - }) + value: 1e4, + }); + }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WPKH) input', async () => { - const p2sh = createPayment('p2sh-p2wpkh') - const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh') + const p2sh = createPayment('p2sh-p2wpkh'); + const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh'); + const inputData2 = await getInputData(5e4, p2sh.payment, true, 'p2sh'); { const { hash, index, witnessUtxo, // NEW: this is an object of the output being spent { script: Buffer; value: Satoshis; } redeemScript, - } = inputData - assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript }, inputData) + } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, redeemScript }, + inputData, + ); } - const keyPair = p2sh.keys[0] + const keyPair = p2sh.keys[0]; const outputData = { script: p2sh.payment.output, // sending to myself for fun - value: 2e4 - } + value: 2e4, + }; + const outputData2 = { + script: p2sh.payment.output, // sending to myself for fun + value: 7e4, + }; const tx = new bitcoin.Psbt() - .addInput(inputData) - .addOutput(outputData) + .addInputs([inputData, inputData2]) + .addOutputs([outputData, outputData2]) .sign(keyPair) .finalizeAllInputs() - .extractTransaction() + .extractTransaction(); // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()) + await regtestUtils.broadcast(tx.toHex()); await regtestUtils.verify({ txId: tx.getId(), address: p2sh.payment.address, vout: 0, - value: 2e4 - }) - }) + value: 2e4, + }); + }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input', async () => { - // the only thing that changes is you don't give a redeemscript for input data - const p2wpkh = createPayment('p2wpkh') - const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem') + const p2wpkh = createPayment('p2wpkh'); + const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); { - const { - hash, - index, - witnessUtxo, - } = inputData - assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData) + const { hash, index, witnessUtxo } = inputData; + assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData); } const psbt = new bitcoin.Psbt({ network: regtest }) .addInput(inputData) .addOutput({ address: regtestUtils.RANDOM_ADDRESS, - value: 2e4 + value: 2e4, }) - .signInput(0, p2wpkh.keys[0]) + .signInput(0, p2wpkh.keys[0]); - assert.strictEqual(psbt.validateSignatures(0), true) - psbt.finalizeAllInputs() + assert.strictEqual(psbt.validateSignatures(0), true); + psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction() + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()) + await regtestUtils.broadcast(tx.toHex()); await regtestUtils.verify({ txId: tx.getId(), address: regtestUtils.RANDOM_ADDRESS, vout: 0, - value: 2e4 - }) - }) + value: 2e4, + }); + }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WSH(P2PK) input', async () => { - const p2wsh = createPayment('p2wsh-p2pk') - const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh') + const p2wsh = createPayment('p2wsh-p2pk'); + const inputData = await getInputData(5e4, p2wsh.payment, true, 'p2wsh'); { const { hash, index, witnessUtxo, witnessScript, // NEW: A Buffer of the witnessScript - } = inputData - assert.deepStrictEqual({ hash, index, witnessUtxo, witnessScript }, inputData) + } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, witnessScript }, + inputData, + ); } const psbt = new bitcoin.Psbt({ network: regtest }) .addInput(inputData) .addOutput({ address: regtestUtils.RANDOM_ADDRESS, - value: 2e4 + value: 2e4, }) - .signInput(0, p2wsh.keys[0]) + .signInput(0, p2wsh.keys[0]); - assert.strictEqual(psbt.validateSignatures(0), true) - psbt.finalizeAllInputs() + assert.strictEqual(psbt.validateSignatures(0), true); + psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction() + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()) + await regtestUtils.broadcast(tx.toHex()); await regtestUtils.verify({ txId: tx.getId(), address: regtestUtils.RANDOM_ADDRESS, vout: 0, - value: 2e4 - }) - }) + value: 2e4, + }); + }); it('can create (and broadcast via 3PBP) a Transaction, w/ a P2SH(P2WSH(P2MS(3 of 4))) (SegWit multisig) input', async () => { - const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)') - const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh') + const p2sh = createPayment('p2sh-p2wsh-p2ms(3 of 4)'); + const inputData = await getInputData(5e4, p2sh.payment, true, 'p2sh-p2wsh'); { const { hash, @@ -335,54 +364,60 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { witnessUtxo, redeemScript, witnessScript, - } = inputData - assert.deepStrictEqual({ hash, index, witnessUtxo, redeemScript, witnessScript }, inputData) + } = inputData; + assert.deepStrictEqual( + { hash, index, witnessUtxo, redeemScript, witnessScript }, + inputData, + ); } const psbt = new bitcoin.Psbt({ network: regtest }) .addInput(inputData) .addOutput({ address: regtestUtils.RANDOM_ADDRESS, - value: 2e4 + value: 2e4, }) .signInput(0, p2sh.keys[0]) .signInput(0, p2sh.keys[2]) - .signInput(0, p2sh.keys[3]) + .signInput(0, p2sh.keys[3]); - assert.strictEqual(psbt.validateSignatures(0), true) - assert.strictEqual(psbt.validateSignatures(0, p2sh.keys[3].publicKey), true) + 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() + psbt.validateSignatures(0, p2sh.keys[1].publicKey); + }, new RegExp('No signatures for this pubkey')); + psbt.finalizeAllInputs(); - const tx = psbt.extractTransaction() + const tx = psbt.extractTransaction(); // build and broadcast to the Bitcoin RegTest network - await regtestUtils.broadcast(tx.toHex()) + await regtestUtils.broadcast(tx.toHex()); await regtestUtils.verify({ txId: tx.getId(), address: regtestUtils.RANDOM_ADDRESS, vout: 0, - value: 2e4 - }) - }) -}) + value: 2e4, + }); + }); +}); function createPayment(_type, network) { - network = network || regtest + network = network || regtest; const splitType = _type.split('-').reverse(); const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; const keys = []; let m; if (isMultisig) { - const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/) - m = parseInt(match[1]) - let n = parseInt(match[2]) + const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/); + m = parseInt(match[1]); + let n = parseInt(match[2]); while (n > 1) { keys.push(bitcoin.ECPair.makeRandom({ network })); - n-- + n--; } } keys.push(bitcoin.ECPair.makeRandom({ network })); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 61ed15e..bfe340c 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -130,6 +130,13 @@ export class Psbt extends PsbtBase { return this.inputs.length; } + clone(): Psbt { + // TODO: more efficient cloning + const res = Psbt.fromBuffer(this.toBuffer()); + res.opts = JSON.parse(JSON.stringify(this.opts)); + return res; + } + setMaximumFeeRate(satoshiPerByte: number): void { check32Bit(satoshiPerByte); // 42.9 BTC per byte IS excessive... so throw this.opts.maximumFeeRate = satoshiPerByte; @@ -168,6 +175,11 @@ export class Psbt extends PsbtBase { return this; } + addInputs(inputDatas: TransactionInput[]): this { + inputDatas.forEach(inputData => this.addInput(inputData)); + return this; + } + addInput(inputData: TransactionInput): this { checkInputsForPartialSig(this.inputs, 'addInput'); const c = this.__CACHE; @@ -178,6 +190,11 @@ export class Psbt extends PsbtBase { return this; } + addOutputs(outputDatas: TransactionOutput[]): this { + outputDatas.forEach(outputData => this.addOutput(outputData)); + return this; + } + addOutput(outputData: TransactionOutput): this { checkInputsForPartialSig(this.inputs, 'addOutput'); const { address } = outputData as any; diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 40571fa..10617c2 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -11,11 +11,14 @@ export declare class Psbt extends PsbtBase { private opts; constructor(opts?: PsbtOptsOptional); readonly inputCount: number; + clone(): Psbt; setMaximumFeeRate(satoshiPerByte: number): void; setVersion(version: number): this; setLocktime(locktime: number): this; setSequence(inputIndex: number, sequence: number): this; + addInputs(inputDatas: TransactionInput[]): this; addInput(inputData: TransactionInput): this; + addOutputs(outputDatas: TransactionOutput[]): this; addOutput(outputData: TransactionOutput): this; addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; extractTransaction(disableFeeCheck?: boolean): Transaction; From 75f5e8f03c5871c6d80a8f4c4e09ea00da8875d3 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 9 Jul 2019 14:35:40 +0700 Subject: [PATCH 081/111] Use Buffer notation in JSON --- test/fixtures/psbt.json | 36 ++++++++++++++++---------------- test/psbt.js | 46 ++++++++++++++--------------------------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 59a74cd..0525bf6 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -174,37 +174,37 @@ "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=", "inputData": [ { - "nonWitnessUtxo": "0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000", - "redeemScript": "5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae", + "nonWitnessUtxo": "Buffer.from('0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000', 'hex')", + "redeemScript": "Buffer.from('5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae', 'hex')", "bip32Derivation": [ { - "masterFingerprint": "d90c6a4f", - "pubkey": "029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f", + "masterFingerprint": "Buffer.from('d90c6a4f', 'hex')", + "pubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f', 'hex')", "path": "m/0'/0'/0'" }, { - "masterFingerprint": "d90c6a4f", - "pubkey": "02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7", + "masterFingerprint": "Buffer.from('d90c6a4f', 'hex')", + "pubkey": "Buffer.from('02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7', 'hex')", "path": "m/0'/0'/1'" } ] }, { "witnessUtxo": { - "script": "a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887", + "script": "Buffer.from('a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887', 'hex')", "value": 200000000 }, - "redeemScript": "00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903", - "witnessScript": "522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae", + "redeemScript": "Buffer.from('00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903', 'hex')", + "witnessScript": "Buffer.from('522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae', 'hex')", "bip32Derivation": [ { - "masterFingerprint": "d90c6a4f", - "pubkey": "023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73", + "masterFingerprint": "Buffer.from('d90c6a4f', 'hex')", + "pubkey": "Buffer.from('023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73', 'hex')", "path": "m/0'/0'/3'" }, { - "masterFingerprint": "d90c6a4f", - "pubkey": "03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc", + "masterFingerprint": "Buffer.from('d90c6a4f', 'hex')", + "pubkey": "Buffer.from('03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc', 'hex')", "path": "m/0'/0'/2'" } ] @@ -214,8 +214,8 @@ { "bip32Derivation": [ { - "masterFingerprint": "d90c6a4f", - "pubkey": "03a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58771", + "masterFingerprint": "Buffer.from('d90c6a4f', 'hex')", + "pubkey": "Buffer.from('03a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58771', 'hex')", "path": "m/0'/0'/4'" } ] @@ -223,8 +223,8 @@ { "bip32Derivation": [ { - "masterFingerprint": "d90c6a4f", - "pubkey": "027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b50051096", + "masterFingerprint": "Buffer.from('d90c6a4f', 'hex')", + "pubkey": "Buffer.from('027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b50051096', 'hex')", "path": "m/0'/0'/5'" } ] @@ -316,7 +316,7 @@ { "description": "checks for hash and index", "inputData": { - "hash": "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f", + "hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')", "index": 2 }, "equals": "cHNidP8BADMCAAAAAQABAgMEBQYHCAkKCwwNDg8AAQIDBAUGBwgJCgsMDQ4PAgAAAAD/////AAAAAAAAAAA=" diff --git a/test/psbt.js b/test/psbt.js index 657a67a..25bbfa9 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -5,37 +5,23 @@ const ECPair = require('../src/ecpair') const Psbt = require('..').Psbt const NETWORKS = require('../src/networks') -const fixtures = require('./fixtures/psbt') +const initBuffers = object => JSON.parse(JSON.stringify(object), (key, value) => { + const regex = new RegExp(/^Buffer.from\(['"](.*)['"], ['"](.*)['"]\)$/) + const result = regex.exec(value) + if (!result) return value + + const data = result[1] + const encoding = result[2] + + return Buffer.from(data, encoding) +}) + +const fixtures = initBuffers(require('./fixtures/psbt')) const upperCaseFirstLetter = str => str.replace(/^./, s => s.toUpperCase()) const b = hex => Buffer.from(hex, 'hex'); -const initBuffers = (attr, data) => { - if ([ - 'nonWitnessUtxo', - 'redeemScript', - 'witnessScript' - ].includes(attr)) { - data = b(data) - } else if (attr === 'bip32Derivation') { - data.masterFingerprint = b(data.masterFingerprint) - data.pubkey = b(data.pubkey) - } else if (attr === 'witnessUtxo') { - data.script = b(data.script) - } else if (attr === 'hash') { - if ( - typeof data === 'string' && - data.match(/^[0-9a-f]*$/i) && - data.length % 2 === 0 - ) { - data = b(data) - } - } - - return data -}; - describe(`Psbt`, () => { describe('BIP174 Test Vectors', () => { fixtures.bip174.invalid.forEach(f => { @@ -94,9 +80,9 @@ describe(`Psbt`, () => { adder = adder.bind(psbt) const arg = data[attr] if (Array.isArray(arg)) { - arg.forEach(a => adder(i, initBuffers(attr, a))) + arg.forEach(a => adder(i, a)) } else { - adder(i, initBuffers(attr, arg)) + adder(i, arg) if (attr === 'nonWitnessUtxo') { const first = psbt.inputs[i].nonWitnessUtxo psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[i] = undefined @@ -320,7 +306,7 @@ describe(`Psbt`, () => { describe('addInput', () => { fixtures.addInput.checks.forEach(f => { for (const attr of Object.keys(f.inputData)) { - f.inputData[attr] = initBuffers(attr, f.inputData[attr]) + f.inputData[attr] = f.inputData[attr] } it(f.description, () => { const psbt = new Psbt() @@ -349,7 +335,7 @@ describe(`Psbt`, () => { describe('addOutput', () => { fixtures.addOutput.checks.forEach(f => { for (const attr of Object.keys(f.outputData)) { - f.outputData[attr] = initBuffers(attr, f.outputData[attr]) + f.outputData[attr] = f.outputData[attr] } it(f.description, () => { const psbt = new Psbt() From 0d9fa87943e23013aa9d76162afc36ed4a676435 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Tue, 9 Jul 2019 16:21:00 +0700 Subject: [PATCH 082/111] Move nonWitnessUtxo cache tests out into own test --- test/fixtures/psbt.json | 9 ++++++++- test/psbt.js | 33 +++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 0525bf6..9e85b63 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -428,5 +428,12 @@ "transaction": "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000", "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" } - ] + ], + "cache": { + "nonWitnessUtxo": { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuIgYClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8Q2QxqTwAAAIAAAACAAAAAgCIGAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXENkMak8AAACAAAAAgAEAAIAAAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", + "nonWitnessUtxo": "Buffer.from('0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000', 'hex')", + "inputIndex": 0 + } + } } diff --git a/test/psbt.js b/test/psbt.js index 25bbfa9..ce92dc6 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -83,16 +83,6 @@ describe(`Psbt`, () => { arg.forEach(a => adder(i, a)) } else { adder(i, arg) - if (attr === 'nonWitnessUtxo') { - const first = psbt.inputs[i].nonWitnessUtxo - psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[i] = undefined - const second = psbt.inputs[i].nonWitnessUtxo - psbt.inputs[i].nonWitnessUtxo = Buffer.from([1,2,3]) - psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[i] = undefined - const third = psbt.inputs[i].nonWitnessUtxo - assert.ok(first.equals(second)) - assert.ok(first.equals(third)) - } } } } @@ -473,4 +463,27 @@ describe(`Psbt`, () => { assert.ok(psbt.__CACHE.__TX); }) }) + + describe('Cache', () => { + it('non-witness UTXOs are cached', () => { + const f = fixtures.cache.nonWitnessUtxo; + const psbt = Psbt.fromBase64(f.psbt) + const index = f.inputIndex; + + // Cache is empty + assert.strictEqual(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index], undefined) + + // Cache is populated + psbt.addNonWitnessUtxoToInput(index, f.nonWitnessUtxo) + const value = psbt.inputs[index].nonWitnessUtxo + assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(value)) + assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(f.nonWitnessUtxo)) + + // Cache is rebuilt from internal transaction object when cleared + psbt.inputs[index].nonWitnessUtxo = Buffer.from([1,2,3]) + psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index] = undefined + assert.ok(psbt.inputs[index].nonWitnessUtxo.equals(value)) + }) + }) }) + From fa897cf78e48942aebb8cfcc308e3af0be22490d Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 10 Jul 2019 10:19:26 +0900 Subject: [PATCH 083/111] Check signatures for sighash type before finalize --- src/psbt.js | 11 +++++++++++ ts_src/psbt.ts | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index 33a32a3..f352a8b 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -216,6 +216,7 @@ class Psbt extends bip174_1.Psbt { const scriptType = classifyScript(script); if (!canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); + checkPartialSigSighashes(input); const { finalScriptSig, finalScriptWitness } = getFinalScripts( script, scriptType, @@ -446,6 +447,16 @@ function checkInputsForPartialSig(inputs, action) { } }); } +function checkPartialSigSighashes(input) { + if (!input.sighashType || !input.partialSig) return; + const { partialSig, sighashType } = input; + partialSig.forEach(pSig => { + const { hashType } = bscript.signature.decode(pSig.signature); + if (sighashType !== hashType) { + throw new Error('Signature sighash does not match input sighash type'); + } + }); +} function checkScriptForPubkey(pubkey, script, action) { const pubkeyHash = crypto_1.hash160(pubkey); const decompiled = bscript.decompile(script); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index bfe340c..acb35f6 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -269,6 +269,8 @@ export class Psbt extends PsbtBase { if (!canFinalize(input, script, scriptType)) throw new Error(`Can not finalize input #${inputIndex}`); + checkPartialSigSighashes(input); + const { finalScriptSig, finalScriptWitness } = getFinalScripts( script, scriptType, @@ -547,6 +549,17 @@ function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { }); } +function checkPartialSigSighashes(input: PsbtInput): void { + if (!input.sighashType || !input.partialSig) return; + const { partialSig, sighashType } = input; + partialSig.forEach(pSig => { + const { hashType } = bscript.signature.decode(pSig.signature); + if (sighashType !== hashType) { + throw new Error('Signature sighash does not match input sighash type'); + } + }); +} + function checkScriptForPubkey( pubkey: Buffer, script: Buffer, From ccab2652f92329dd6a01ffed4ccbd96e5152acf1 Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 10 Jul 2019 11:15:12 +0900 Subject: [PATCH 084/111] Add sighash checks for signer --- src/psbt.js | 65 ++++++++++++++++++++++++++++++++++++++++++------- ts_src/psbt.ts | 64 ++++++++++++++++++++++++++++++++++++++++++------ types/psbt.d.ts | 8 +++--- 3 files changed, 117 insertions(+), 20 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index f352a8b..c7e8b55 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -273,7 +273,7 @@ class Psbt extends bip174_1.Psbt { } return results.every(res => res === true); } - sign(keyPair) { + sign(keyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); // TODO: Add a pubkey/pubkeyhash cache to each input @@ -282,7 +282,7 @@ class Psbt extends bip174_1.Psbt { const results = []; for (const i of range(this.inputs.length)) { try { - this.signInput(i, keyPair); + this.signInput(i, keyPair, sighashTypes); results.push(true); } catch (err) { results.push(false); @@ -293,7 +293,7 @@ class Psbt extends bip174_1.Psbt { } return this; } - signAsync(keyPair) { + signAsync(keyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) { return new Promise((resolve, reject) => { if (!keyPair || !keyPair.publicKey) return reject(new Error('Need Signer to sign input')); @@ -304,7 +304,7 @@ class Psbt extends bip174_1.Psbt { const promises = []; for (const [i] of this.inputs.entries()) { promises.push( - this.signInputAsync(i, keyPair).then( + this.signInputAsync(i, keyPair, sighashTypes).then( () => { results.push(true); }, @@ -322,7 +322,11 @@ class Psbt extends bip174_1.Psbt { }); }); } - signInput(inputIndex, keyPair) { + signInput( + inputIndex, + keyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( @@ -330,6 +334,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__CACHE, + sighashTypes, ); const partialSig = { pubkey: keyPair.publicKey, @@ -337,7 +342,11 @@ class Psbt extends bip174_1.Psbt { }; return this.addPartialSigToInput(inputIndex, partialSig); } - signInputAsync(inputIndex, keyPair) { + signInputAsync( + inputIndex, + keyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { return new Promise((resolve, reject) => { if (!keyPair || !keyPair.publicKey) return reject(new Error('Need Signer to sign input')); @@ -346,6 +355,7 @@ class Psbt extends bip174_1.Psbt { inputIndex, keyPair.publicKey, this.__CACHE, + sighashTypes, ); Promise.resolve(keyPair.sign(hash)).then(signature => { const partialSig = { @@ -548,19 +558,37 @@ function getFinalScripts( finalScriptWitness, }; } -function getHashAndSighashType(inputs, inputIndex, pubkey, cache) { +function getHashAndSighashType( + inputs, + inputIndex, + pubkey, + cache, + sighashTypes, +) { const input = utils_1.checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); + const { hash, sighashType, script } = getHashForSig( + inputIndex, + input, + cache, + sighashTypes, + ); checkScriptForPubkey(pubkey, script, 'sign'); return { hash, sighashType, }; } -function getHashForSig(inputIndex, input, cache) { +function getHashForSig(inputIndex, input, cache, sighashTypes) { const unsignedTx = cache.__TX; const sighashType = input.sighashType || transaction_1.Transaction.SIGHASH_ALL; + if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { + const str = sighashTypeToString(sighashType); + throw new Error( + `Sighash type is not allowed. Retry the sign method passing the ` + + `sighashTypes array of whitelisted types. Sighash type: ${str}`, + ); + } let hash; let script; if (input.nonWitnessUtxo) { @@ -800,6 +828,25 @@ function scriptWitnessToWitnessStack(buffer) { } return readVector(); } +function sighashTypeToString(sighashType) { + let text = + sighashType & transaction_1.Transaction.SIGHASH_ANYONECANPAY + ? 'SIGHASH_ANYONECANPAY | ' + : ''; + const sigMod = sighashType & 0x1f; + switch (sigMod) { + case transaction_1.Transaction.SIGHASH_ALL: + text += 'SIGHASH_ALL'; + break; + case transaction_1.Transaction.SIGHASH_SINGLE: + text += 'SIGHASH_SINGLE'; + break; + case transaction_1.Transaction.SIGHASH_NONE: + text += 'SIGHASH_NONE'; + break; + } + return text; +} function witnessStackToScriptWitness(witness) { let buffer = Buffer.allocUnsafe(0); function writeSlice(slice) { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index acb35f6..8fb1e97 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -332,7 +332,10 @@ export class Psbt extends PsbtBase { return results.every(res => res === true); } - sign(keyPair: Signer): this { + sign( + keyPair: Signer, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -342,7 +345,7 @@ export class Psbt extends PsbtBase { const results: boolean[] = []; for (const i of range(this.inputs.length)) { try { - this.signInput(i, keyPair); + this.signInput(i, keyPair, sighashTypes); results.push(true); } catch (err) { results.push(false); @@ -354,7 +357,10 @@ export class Psbt extends PsbtBase { return this; } - signAsync(keyPair: SignerAsync): Promise { + signAsync( + keyPair: SignerAsync, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): Promise { return new Promise( (resolve, reject): any => { if (!keyPair || !keyPair.publicKey) @@ -367,7 +373,7 @@ export class Psbt extends PsbtBase { const promises: Array> = []; for (const [i] of this.inputs.entries()) { promises.push( - this.signInputAsync(i, keyPair).then( + this.signInputAsync(i, keyPair, sighashTypes).then( () => { results.push(true); }, @@ -387,7 +393,11 @@ export class Psbt extends PsbtBase { ); } - signInput(inputIndex: number, keyPair: Signer): this { + signInput( + inputIndex: number, + keyPair: Signer, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): this { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( @@ -395,6 +405,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__CACHE, + sighashTypes, ); const partialSig = { @@ -405,7 +416,11 @@ export class Psbt extends PsbtBase { return this.addPartialSigToInput(inputIndex, partialSig); } - signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise { + signInputAsync( + inputIndex: number, + keyPair: SignerAsync, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): Promise { return new Promise( (resolve, reject): void => { if (!keyPair || !keyPair.publicKey) @@ -415,6 +430,7 @@ export class Psbt extends PsbtBase { inputIndex, keyPair.publicKey, this.__CACHE, + sighashTypes, ); Promise.resolve(keyPair.sign(hash)).then(signature => { @@ -683,12 +699,18 @@ function getHashAndSighashType( inputIndex: number, pubkey: Buffer, cache: PsbtCache, + sighashTypes: number[], ): { hash: Buffer; sighashType: number; } { const input = checkForInput(inputs, inputIndex); - const { hash, sighashType, script } = getHashForSig(inputIndex, input, cache); + const { hash, sighashType, script } = getHashForSig( + inputIndex, + input, + cache, + sighashTypes, + ); checkScriptForPubkey(pubkey, script, 'sign'); return { hash, @@ -700,6 +722,7 @@ function getHashForSig( inputIndex: number, input: PsbtInput, cache: PsbtCache, + sighashTypes?: number[], ): { script: Buffer; hash: Buffer; @@ -707,6 +730,13 @@ function getHashForSig( } { const unsignedTx = cache.__TX; const sighashType = input.sighashType || Transaction.SIGHASH_ALL; + if (sighashTypes && sighashTypes.indexOf(sighashType) < 0) { + const str = sighashTypeToString(sighashType); + throw new Error( + `Sighash type is not allowed. Retry the sign method passing the ` + + `sighashTypes array of whitelisted types. Sighash type: ${str}`, + ); + } let hash: Buffer; let script: Buffer; @@ -981,6 +1011,26 @@ function scriptWitnessToWitnessStack(buffer: Buffer): Buffer[] { return readVector(); } +function sighashTypeToString(sighashType: number): string { + let text = + sighashType & Transaction.SIGHASH_ANYONECANPAY + ? 'SIGHASH_ANYONECANPAY | ' + : ''; + const sigMod = sighashType & 0x1f; + switch (sigMod) { + case Transaction.SIGHASH_ALL: + text += 'SIGHASH_ALL'; + break; + case Transaction.SIGHASH_SINGLE: + text += 'SIGHASH_SINGLE'; + break; + case Transaction.SIGHASH_NONE: + text += 'SIGHASH_NONE'; + break; + } + return text; +} + function witnessStackToScriptWitness(witness: Buffer[]): Buffer { let buffer = Buffer.allocUnsafe(0); diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 10617c2..95f6624 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -27,10 +27,10 @@ export declare class Psbt extends PsbtBase { finalizeInput(inputIndex: number): this; validateAllSignatures(): boolean; validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; - sign(keyPair: Signer): this; - signAsync(keyPair: SignerAsync): Promise; - signInput(inputIndex: number, keyPair: Signer): this; - signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise; + sign(keyPair: Signer, sighashTypes?: number[]): this; + signAsync(keyPair: SignerAsync, sighashTypes?: number[]): Promise; + signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this; + signInputAsync(inputIndex: number, keyPair: SignerAsync, sighashTypes?: number[]): Promise; } interface PsbtOptsOptional { network?: Network; From a50ec330334d214c7bc1099912dd0d58715d5032 Mon Sep 17 00:00:00 2001 From: junderw Date: Wed, 10 Jul 2019 16:43:51 +0900 Subject: [PATCH 085/111] Update dep --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 768c79f..fce68c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.13.tgz", - "integrity": "sha512-jWP7Lb27Nmbk6gaZKhJZOyk5LqRWs9z+R2xzgu3W8/iZXIIP2kcR6fh5lNg7GGOiWUaqanWC9rjrDVrBVbXKww==" + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.14.tgz", + "integrity": "sha512-v9cre0W4ZpAJS1v18WUJLE9yKdSZyenGpZBg7CXiZ5n35JPXganH92d4Yk8WXpRfbFZ4SMXTqKLEgpLPX1TmcA==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 1fadfcb..03e6d5f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.13", + "bip174": "0.0.14", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", From da5adcf88f90a00981a5ce0b0a54963173081731 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 17:59:11 +0700 Subject: [PATCH 086/111] Refactor and cleanup validateSignatures tests --- test/fixtures/psbt.json | 7 +++++++ test/psbt.js | 34 ++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 9e85b63..c6eabd9 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -429,6 +429,13 @@ "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" } ], + "validateSignatures": { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", + "index": 0, + "pubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f', 'hex')", + "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')", + "nonExistantIndex": 42 + }, "cache": { "nonWitnessUtxo": { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuIgYClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8Q2QxqTwAAAIAAAACAAAAAgCIGAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXENkMak8AAACAAAAAgAEAAIAAAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", diff --git a/test/psbt.js b/test/psbt.js index ce92dc6..94136af 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -130,20 +130,6 @@ describe(`Psbt`, () => { psbt.getFeeRate() }, new RegExp('PSBT must be finalized to calculate fee rate')) - const pubkey = Buffer.from( - '029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f', - 'hex', - ) - assert.strictEqual(psbt.validateSignatures(0), true) - assert.strictEqual(psbt.validateSignatures(0, pubkey), true) - assert.throws(() => { - pubkey[32] = 42 - psbt.validateSignatures(0, pubkey) - }, new RegExp('No signatures for this pubkey')) - assert.throws(() => { - psbt.validateSignatures(42) - }, new RegExp('No signatures to validate')) - psbt.finalizeAllInputs() assert.strictEqual(psbt.toBase64(), f.result) @@ -401,6 +387,26 @@ describe(`Psbt`, () => { }) }) + describe('validateSignatures', () => { + const f = fixtures.validateSignatures + it('Correctly validates a signature', () => { + const psbt = Psbt.fromBase64(f.psbt) + + assert.strictEqual(psbt.validateSignatures(f.index), true) + assert.throws(() => { + psbt.validateSignatures(f.nonExistantIndex) + }, new RegExp('No signatures to validate')) + }) + + it('Correctly validates a signature against a pubkey', () => { + const psbt = Psbt.fromBase64(f.psbt) + assert.strictEqual(psbt.validateSignatures(f.index, f.pubkey), true) + assert.throws(() => { + psbt.validateSignatures(f.index, f.incorrectPubkey) + }, new RegExp('No signatures for this pubkey')) + }) + }) + describe('create 1-to-1 transaction', () => { const alice = ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr') const psbt = new Psbt() From 47b42e72f4e8bc2f20807ffba4b14c6c1f3d2040 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 18:05:30 +0700 Subject: [PATCH 087/111] Refactor and cleanup getFeeRate tests --- test/fixtures/psbt.json | 3 +++ test/psbt.js | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index c6eabd9..13f4389 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -436,6 +436,9 @@ "incorrectPubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e02a', 'hex')", "nonExistantIndex": 42 }, + "getFeeRate": { + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + }, "cache": { "nonWitnessUtxo": { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEER1IhApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/IQLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU211KuIgYClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8Q2QxqTwAAAIAAAACAAAAAgCIGAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXENkMak8AAACAAAAAgAEAAIAAAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", diff --git a/test/psbt.js b/test/psbt.js index 94136af..cc72e55 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -126,10 +126,6 @@ describe(`Psbt`, () => { it('Finalizes inputs and gives the expected PSBT', () => { const psbt = Psbt.fromBase64(f.psbt) - assert.throws(() => { - psbt.getFeeRate() - }, new RegExp('PSBT must be finalized to calculate fee rate')) - psbt.finalizeAllInputs() assert.strictEqual(psbt.toBase64(), f.result) @@ -407,6 +403,23 @@ describe(`Psbt`, () => { }) }) + describe('getFeeRate', () => { + it('Throws error if called before inputs are finalized', () => { + const f = fixtures.getFeeRate + const psbt = Psbt.fromBase64(f.psbt) + + assert.throws(() => { + psbt.getFeeRate() + }, new RegExp('PSBT must be finalized to calculate fee rate')) + + psbt.finalizeAllInputs() + + assert.doesNotThrow(() => { + psbt.getFeeRate() + }) + }) + }) + describe('create 1-to-1 transaction', () => { const alice = ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr') const psbt = new Psbt() From d05144627588fd98ca7562864e2d891fc7927706 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 18:12:56 +0700 Subject: [PATCH 088/111] Add P2MS test case to finalizer tests --- test/fixtures/psbt.json | 6 ++++++ test/psbt.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 13f4389..2333d65 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -293,8 +293,14 @@ ], "finalizer": [ { + "description": "BIP174 test case", "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" + }, + { + "description": "P2MS input", + "psbt": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAAiAgLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCEgwRQIhAOqqlnUwmc+PqKZVwAAMLPNCKskHhVgk1pXR9d7nmB6SAiA0C24idVdtgl9+sIBR8A3Za5WcswNJ3i8PWTMWR10C4QEiAgPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuUREgwRQIhANnDBUQWjS8hDTPidRZ95rGZK3LWCLIZB1NYx9++eBT8AiASupLR+KNa1Se7eoBkWQcDny+BaBfMQ+p1RTCAXd7/GAEBBItSIQLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCCEC4X74b+vRF3XLgyXyGejmE5VB1f3RY/IReUggyNHgdLYhA+dqu1GC9JCdInbJ1G7BI9cN1Tc/VhDCFiwrtfN8a5REIQO94OZv6yPD4kEZSZBquYA7FPsABZZ6UAteVuEw7fVxzlSuAAA=", + "result": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAABB/0gAQBIMEUCIQDqqpZ1MJnPj6imVcAADCzzQirJB4VYJNaV0fXe55gekgIgNAtuInVXbYJffrCAUfAN2WuVnLMDSd4vD1kzFkddAuEBSDBFAiEA2cMFRBaNLyENM+J1Fn3msZkrctYIshkHU1jH3754FPwCIBK6ktH4o1rVJ7t6gGRZBwOfL4FoF8xD6nVFMIBd3v8YAUyLUiECxZrqO4BvDo4JmjuQB7gv9Trw2q0Ldeke1RClPHZEUQghAuF++G/r0Rd1y4Ml8hno5hOVQdX90WPyEXlIIMjR4HS2IQPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuURCEDveDmb+sjw+JBGUmQarmAOxT7AAWWelALXlbhMO31cc5UrgAA" } ], "extractor": [ diff --git a/test/psbt.js b/test/psbt.js index cc72e55..e1840ce 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -123,7 +123,7 @@ describe(`Psbt`, () => { }) fixtures.bip174.finalizer.forEach(f => { - it('Finalizes inputs and gives the expected PSBT', () => { + it(`Finalizes inputs and gives the expected PSBT: ${f.description}`, () => { const psbt = Psbt.fromBase64(f.psbt) psbt.finalizeAllInputs() From f6ab5b796f8e74353b85dfa8ae0d6cffa90b947d Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 18:54:05 +0700 Subject: [PATCH 089/111] Move all BIP174 test cases into BIP14 describe block --- test/psbt.js | 129 +++++++++++++++++++++++++-------------------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/test/psbt.js b/test/psbt.js index e1840ce..9f7cc1d 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -93,74 +93,74 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.toBase64(), f.result) }) }) - }) - fixtures.bip174.signer.forEach(f => { - it('Signs PSBT to the expected result', () => { - const psbt = Psbt.fromBase64(f.psbt) - - f.keys.forEach(({inputToSign, WIF}) => { - const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet); - psbt.signInput(inputToSign, keyPair); + fixtures.bip174.signer.forEach(f => { + it('Signs PSBT to the expected result', () => { + const psbt = Psbt.fromBase64(f.psbt) + + f.keys.forEach(({inputToSign, WIF}) => { + const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet); + psbt.signInput(inputToSign, keyPair); + }) + + assert.strictEqual(psbt.toBase64(), f.result) }) - - assert.strictEqual(psbt.toBase64(), f.result) }) - }) - - fixtures.bip174.combiner.forEach(f => { - it('Combines two PSBTs to the expected result', () => { - const psbts = f.psbts.map(psbt => Psbt.fromBase64(psbt)) - - psbts[0].combine(psbts[1]) - - // Produces a different Base64 string due to implemetation specific key-value ordering. - // That means this test will fail: - // assert.strictEqual(psbts[0].toBase64(), f.result) - // However, if we compare the actual PSBT properties we can see they are logically identical: - assert.deepStrictEqual(psbts[0], Psbt.fromBase64(f.result)) + + fixtures.bip174.combiner.forEach(f => { + it('Combines two PSBTs to the expected result', () => { + const psbts = f.psbts.map(psbt => Psbt.fromBase64(psbt)) + + psbts[0].combine(psbts[1]) + + // Produces a different Base64 string due to implemetation specific key-value ordering. + // That means this test will fail: + // assert.strictEqual(psbts[0].toBase64(), f.result) + // However, if we compare the actual PSBT properties we can see they are logically identical: + assert.deepStrictEqual(psbts[0], Psbt.fromBase64(f.result)) + }) }) - }) - - fixtures.bip174.finalizer.forEach(f => { - it(`Finalizes inputs and gives the expected PSBT: ${f.description}`, () => { - const psbt = Psbt.fromBase64(f.psbt) - - psbt.finalizeAllInputs() - - assert.strictEqual(psbt.toBase64(), f.result) + + fixtures.bip174.finalizer.forEach(f => { + it(`Finalizes inputs and gives the expected PSBT: ${f.description}`, () => { + const psbt = Psbt.fromBase64(f.psbt) + + psbt.finalizeAllInputs() + + assert.strictEqual(psbt.toBase64(), f.result) + }) }) - }) - - fixtures.bip174.extractor.forEach(f => { - it('Extracts the expected transaction from a PSBT', () => { - const psbt1 = Psbt.fromBase64(f.psbt) - const transaction1 = psbt1.extractTransaction(true).toHex() - - const psbt2 = Psbt.fromBase64(f.psbt) - const transaction2 = psbt2.extractTransaction().toHex() - - assert.strictEqual(transaction1, transaction2) - assert.strictEqual(transaction1, f.transaction) - - const psbt3 = Psbt.fromBase64(f.psbt) - delete psbt3.inputs[0].finalScriptSig - delete psbt3.inputs[0].finalScriptWitness - assert.throws(() => { - psbt3.extractTransaction() - }, new RegExp('Not finalized')) - - const psbt4 = Psbt.fromBase64(f.psbt) - psbt4.setMaximumFeeRate(1) - assert.throws(() => { - psbt4.extractTransaction() - }, new RegExp('Warning: You are paying around [\\d.]+ in fees')) - - const psbt5 = Psbt.fromBase64(f.psbt) - psbt5.extractTransaction(true) - const fr1 = psbt5.getFeeRate() - const fr2 = psbt5.getFeeRate() - assert.strictEqual(fr1, fr2) + + fixtures.bip174.extractor.forEach(f => { + it('Extracts the expected transaction from a PSBT', () => { + const psbt1 = Psbt.fromBase64(f.psbt) + const transaction1 = psbt1.extractTransaction(true).toHex() + + const psbt2 = Psbt.fromBase64(f.psbt) + const transaction2 = psbt2.extractTransaction().toHex() + + assert.strictEqual(transaction1, transaction2) + assert.strictEqual(transaction1, f.transaction) + + const psbt3 = Psbt.fromBase64(f.psbt) + delete psbt3.inputs[0].finalScriptSig + delete psbt3.inputs[0].finalScriptWitness + assert.throws(() => { + psbt3.extractTransaction() + }, new RegExp('Not finalized')) + + const psbt4 = Psbt.fromBase64(f.psbt) + psbt4.setMaximumFeeRate(1) + assert.throws(() => { + psbt4.extractTransaction() + }, new RegExp('Warning: You are paying around [\\d.]+ in fees')) + + const psbt5 = Psbt.fromBase64(f.psbt) + psbt5.extractTransaction(true) + const fr1 = psbt5.getFeeRate() + const fr2 = psbt5.getFeeRate() + assert.strictEqual(fr1, fr2) + }) }) }) @@ -504,5 +504,4 @@ describe(`Psbt`, () => { assert.ok(psbt.inputs[index].nonWitnessUtxo.equals(value)) }) }) -}) - +}) \ No newline at end of file From ec2c14b81f79ee4dfcae004535b5a35d966c3406 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 19:20:10 +0700 Subject: [PATCH 090/111] Extract finalizeAllInputs test out of BIP174 test cases --- test/fixtures/psbt.json | 13 +++++++------ test/psbt.js | 14 +++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 2333d65..8092177 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -293,14 +293,8 @@ ], "finalizer": [ { - "description": "BIP174 test case", "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIgIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1Ptnuylh3EQ2QxqTwAAAIAAAACABAAAgAAiAgJ/Y5l1fS7/VaE2rQLGhLGDi2VW5fG2s0KCqUtrUAUQlhDZDGpPAAAAgAAAAIAFAACAAA==" - }, - { - "description": "P2MS input", - "psbt": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAAiAgLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCEgwRQIhAOqqlnUwmc+PqKZVwAAMLPNCKskHhVgk1pXR9d7nmB6SAiA0C24idVdtgl9+sIBR8A3Za5WcswNJ3i8PWTMWR10C4QEiAgPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuUREgwRQIhANnDBUQWjS8hDTPidRZ95rGZK3LWCLIZB1NYx9++eBT8AiASupLR+KNa1Se7eoBkWQcDny+BaBfMQ+p1RTCAXd7/GAEBBItSIQLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCCEC4X74b+vRF3XLgyXyGejmE5VB1f3RY/IReUggyNHgdLYhA+dqu1GC9JCdInbJ1G7BI9cN1Tc/VhDCFiwrtfN8a5REIQO94OZv6yPD4kEZSZBquYA7FPsABZZ6UAteVuEw7fVxzlSuAAA=", - "result": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAABB/0gAQBIMEUCIQDqqpZ1MJnPj6imVcAADCzzQirJB4VYJNaV0fXe55gekgIgNAtuInVXbYJffrCAUfAN2WuVnLMDSd4vD1kzFkddAuEBSDBFAiEA2cMFRBaNLyENM+J1Fn3msZkrctYIshkHU1jH3754FPwCIBK6ktH4o1rVJ7t6gGRZBwOfL4FoF8xD6nVFMIBd3v8YAUyLUiECxZrqO4BvDo4JmjuQB7gv9Trw2q0Ldeke1RClPHZEUQghAuF++G/r0Rd1y4Ml8hno5hOVQdX90WPyEXlIIMjR4HS2IQPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuURCEDveDmb+sjw+JBGUmQarmAOxT7AAWWelALXlbhMO31cc5UrgAA" } ], "extractor": [ @@ -429,6 +423,13 @@ } ] }, + "finalizeAllInputs": [ + { + "type": "P2MS", + "psbt": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAAiAgLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCEgwRQIhAOqqlnUwmc+PqKZVwAAMLPNCKskHhVgk1pXR9d7nmB6SAiA0C24idVdtgl9+sIBR8A3Za5WcswNJ3i8PWTMWR10C4QEiAgPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuUREgwRQIhANnDBUQWjS8hDTPidRZ95rGZK3LWCLIZB1NYx9++eBT8AiASupLR+KNa1Se7eoBkWQcDny+BaBfMQ+p1RTCAXd7/GAEBBItSIQLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCCEC4X74b+vRF3XLgyXyGejmE5VB1f3RY/IReUggyNHgdLYhA+dqu1GC9JCdInbJ1G7BI9cN1Tc/VhDCFiwrtfN8a5REIQO94OZv6yPD4kEZSZBquYA7FPsABZZ6UAteVuEw7fVxzlSuAAA=", + "result": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAABB/0gAQBIMEUCIQDqqpZ1MJnPj6imVcAADCzzQirJB4VYJNaV0fXe55gekgIgNAtuInVXbYJffrCAUfAN2WuVnLMDSd4vD1kzFkddAuEBSDBFAiEA2cMFRBaNLyENM+J1Fn3msZkrctYIshkHU1jH3754FPwCIBK6ktH4o1rVJ7t6gGRZBwOfL4FoF8xD6nVFMIBd3v8YAUyLUiECxZrqO4BvDo4JmjuQB7gv9Trw2q0Ldeke1RClPHZEUQghAuF++G/r0Rd1y4Ml8hno5hOVQdX90WPyEXlIIMjR4HS2IQPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuURCEDveDmb+sjw+JBGUmQarmAOxT7AAWWelALXlbhMO31cc5UrgAA" + } + ], "fromTransaction": [ { "transaction": "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000", diff --git a/test/psbt.js b/test/psbt.js index 9f7cc1d..6dd01a9 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -122,7 +122,7 @@ describe(`Psbt`, () => { }) fixtures.bip174.finalizer.forEach(f => { - it(`Finalizes inputs and gives the expected PSBT: ${f.description}`, () => { + it("Finalizes inputs and gives the expected PSBT", () => { const psbt = Psbt.fromBase64(f.psbt) psbt.finalizeAllInputs() @@ -266,6 +266,18 @@ describe(`Psbt`, () => { }) }) + describe('finalizeAllInputs', () => { + fixtures.finalizeAllInputs.forEach(f => { + it(`Finalizes inputs of type "${f.type}"`, () => { + const psbt = Psbt.fromBase64(f.psbt) + + psbt.finalizeAllInputs() + + assert.strictEqual(psbt.toBase64(), f.result) + }) + }) + }) + describe('fromTransaction', () => { fixtures.fromTransaction.forEach(f => { it('Creates the expected PSBT from a transaction buffer', () => { From bc56ca0fa1210d991138e78ccb8ff0781581d415 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 19:21:33 +0700 Subject: [PATCH 091/111] Test finalizeAllInputs against P2PK input --- test/fixtures/psbt.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 8092177..ad6ffdf 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -424,6 +424,11 @@ ] }, "finalizeAllInputs": [ + { + "type": "P2PK", + "psbt": "cHNidP8BAFUCAAAAAZfdvR7JvbVsuXunG1P19tTYi8Z0l6WHiz+0jpzTNTt+AAAAAAD/////AYA4AQAAAAAAGXapFMjtQ0xCK7OPd32ZJTY2UcdpFIaJiKwAAAAAAAEAygIAAAABb7TMmNs5UwEEpccLHlaaM0w3HEHhh85NjrUObLDMuZsBAAAAa0gwRQIhANWUevBBcqfRpH67rDuFqI+KHqxViKXxmSM+6/jE5ZHjAiBgO/vQg2EsYhaOD5lQbmZYaymRLukA6SCoXhkN21hiOwEhAoKzWGkj1LJ2iOt5wj6jMOtAMGJH50ZjAYZU81uUu6y+/////wGQXwEAAAAAACMhAzpK4NeYX+xyBOKynjD3BEPDq7EN8brZXje488gxshXwrAAAAAAiAgM6SuDXmF/scgTisp4w9wRDw6uxDfG62V43uPPIMbIV8EgwRQIhAL3H/XEPRAZEbpjBwkuLqUKBSu1Inpb2rganXNFcY2JsAiASrXTM2xODEKp7m7RTzYqBchqlvbl88zO/CGW9SePj2gEAAA==", + "result": "cHNidP8BAFUCAAAAAZfdvR7JvbVsuXunG1P19tTYi8Z0l6WHiz+0jpzTNTt+AAAAAAD/////AYA4AQAAAAAAGXapFMjtQ0xCK7OPd32ZJTY2UcdpFIaJiKwAAAAAAAEAygIAAAABb7TMmNs5UwEEpccLHlaaM0w3HEHhh85NjrUObLDMuZsBAAAAa0gwRQIhANWUevBBcqfRpH67rDuFqI+KHqxViKXxmSM+6/jE5ZHjAiBgO/vQg2EsYhaOD5lQbmZYaymRLukA6SCoXhkN21hiOwEhAoKzWGkj1LJ2iOt5wj6jMOtAMGJH50ZjAYZU81uUu6y+/////wGQXwEAAAAAACMhAzpK4NeYX+xyBOKynjD3BEPDq7EN8brZXje488gxshXwrAAAAAABB0lIMEUCIQC9x/1xD0QGRG6YwcJLi6lCgUrtSJ6W9q4Gp1zRXGNibAIgEq10zNsTgxCqe5u0U82KgXIapb25fPMzvwhlvUnj49oBAAA=" + }, { "type": "P2MS", "psbt": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAAiAgLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCEgwRQIhAOqqlnUwmc+PqKZVwAAMLPNCKskHhVgk1pXR9d7nmB6SAiA0C24idVdtgl9+sIBR8A3Za5WcswNJ3i8PWTMWR10C4QEiAgPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuUREgwRQIhANnDBUQWjS8hDTPidRZ95rGZK3LWCLIZB1NYx9++eBT8AiASupLR+KNa1Se7eoBkWQcDny+BaBfMQ+p1RTCAXd7/GAEBBItSIQLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCCEC4X74b+vRF3XLgyXyGejmE5VB1f3RY/IReUggyNHgdLYhA+dqu1GC9JCdInbJ1G7BI9cN1Tc/VhDCFiwrtfN8a5REIQO94OZv6yPD4kEZSZBquYA7FPsABZZ6UAteVuEw7fVxzlSuAAA=", From f55ee323861bcdd25ccd8c951bf578e7e0ac3965 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 19:21:56 +0700 Subject: [PATCH 092/111] Test finalizeAllInputs against P2PKH input --- test/fixtures/psbt.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index ad6ffdf..c275f9b 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -429,6 +429,11 @@ "psbt": "cHNidP8BAFUCAAAAAZfdvR7JvbVsuXunG1P19tTYi8Z0l6WHiz+0jpzTNTt+AAAAAAD/////AYA4AQAAAAAAGXapFMjtQ0xCK7OPd32ZJTY2UcdpFIaJiKwAAAAAAAEAygIAAAABb7TMmNs5UwEEpccLHlaaM0w3HEHhh85NjrUObLDMuZsBAAAAa0gwRQIhANWUevBBcqfRpH67rDuFqI+KHqxViKXxmSM+6/jE5ZHjAiBgO/vQg2EsYhaOD5lQbmZYaymRLukA6SCoXhkN21hiOwEhAoKzWGkj1LJ2iOt5wj6jMOtAMGJH50ZjAYZU81uUu6y+/////wGQXwEAAAAAACMhAzpK4NeYX+xyBOKynjD3BEPDq7EN8brZXje488gxshXwrAAAAAAiAgM6SuDXmF/scgTisp4w9wRDw6uxDfG62V43uPPIMbIV8EgwRQIhAL3H/XEPRAZEbpjBwkuLqUKBSu1Inpb2rganXNFcY2JsAiASrXTM2xODEKp7m7RTzYqBchqlvbl88zO/CGW9SePj2gEAAA==", "result": "cHNidP8BAFUCAAAAAZfdvR7JvbVsuXunG1P19tTYi8Z0l6WHiz+0jpzTNTt+AAAAAAD/////AYA4AQAAAAAAGXapFMjtQ0xCK7OPd32ZJTY2UcdpFIaJiKwAAAAAAAEAygIAAAABb7TMmNs5UwEEpccLHlaaM0w3HEHhh85NjrUObLDMuZsBAAAAa0gwRQIhANWUevBBcqfRpH67rDuFqI+KHqxViKXxmSM+6/jE5ZHjAiBgO/vQg2EsYhaOD5lQbmZYaymRLukA6SCoXhkN21hiOwEhAoKzWGkj1LJ2iOt5wj6jMOtAMGJH50ZjAYZU81uUu6y+/////wGQXwEAAAAAACMhAzpK4NeYX+xyBOKynjD3BEPDq7EN8brZXje488gxshXwrAAAAAABB0lIMEUCIQC9x/1xD0QGRG6YwcJLi6lCgUrtSJ6W9q4Gp1zRXGNibAIgEq10zNsTgxCqe5u0U82KgXIapb25fPMzvwhlvUnj49oBAAA=" }, + { + "type": "P2PKH", + "psbt": "cHNidP8BAFUCAAAAAaTbj9mE+B5Z8PLsuGUNGOzDqrtwdB08vvSccSCrezh+AAAAAAD/////AYA4AQAAAAAAGXapFDuK+0mR+qtL9tyadI72bKMwH+vuiKwAAAAAAAEAwAIAAAABks7XL87tkSusFA3L2u2CdJeNkqTMobehPNm/wkGQUzoBAAAAa0gwRQIhAMtzqC7axSA7/7nbio9NKQZz2ePuKeaF5T4c8JSXWEdgAiAID39I05hbJMNGSomrF7XVWEsEEHHzX/lkL3vnOOu4cAEhAvu+bfurbDCzxfOLxEhvz9ZyxPLdI1h9wMT8gl2nyLYV/////wGQXwEAAAAAABl2qRTH5239BfS9zrbwUtvpATwjua4LGYisAAAAACICA1oNX0U/6GwAuVI7JHhneD94sm/o+YrfTnRAhdRUMjoISDBFAiEAhk+HbvY6YShBCUmBVCk42sFWH9LTwUv2wbRC/tIuEAwCIED/UkklY3fpdDBN7qSBHFDyEOeHMUzXD0bvtFTAiKP7AQAA", + "result": "cHNidP8BAFUCAAAAAaTbj9mE+B5Z8PLsuGUNGOzDqrtwdB08vvSccSCrezh+AAAAAAD/////AYA4AQAAAAAAGXapFDuK+0mR+qtL9tyadI72bKMwH+vuiKwAAAAAAAEAwAIAAAABks7XL87tkSusFA3L2u2CdJeNkqTMobehPNm/wkGQUzoBAAAAa0gwRQIhAMtzqC7axSA7/7nbio9NKQZz2ePuKeaF5T4c8JSXWEdgAiAID39I05hbJMNGSomrF7XVWEsEEHHzX/lkL3vnOOu4cAEhAvu+bfurbDCzxfOLxEhvz9ZyxPLdI1h9wMT8gl2nyLYV/////wGQXwEAAAAAABl2qRTH5239BfS9zrbwUtvpATwjua4LGYisAAAAAAEHa0gwRQIhAIZPh272OmEoQQlJgVQpONrBVh/S08FL9sG0Qv7SLhAMAiBA/1JJJWN36XQwTe6kgRxQ8hDnhzFM1w9G77RUwIij+wEhA1oNX0U/6GwAuVI7JHhneD94sm/o+YrfTnRAhdRUMjoIAAA=" + }, { "type": "P2MS", "psbt": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAAiAgLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCEgwRQIhAOqqlnUwmc+PqKZVwAAMLPNCKskHhVgk1pXR9d7nmB6SAiA0C24idVdtgl9+sIBR8A3Za5WcswNJ3i8PWTMWR10C4QEiAgPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuUREgwRQIhANnDBUQWjS8hDTPidRZ95rGZK3LWCLIZB1NYx9++eBT8AiASupLR+KNa1Se7eoBkWQcDny+BaBfMQ+p1RTCAXd7/GAEBBItSIQLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCCEC4X74b+vRF3XLgyXyGejmE5VB1f3RY/IReUggyNHgdLYhA+dqu1GC9JCdInbJ1G7BI9cN1Tc/VhDCFiwrtfN8a5REIQO94OZv6yPD4kEZSZBquYA7FPsABZZ6UAteVuEw7fVxzlSuAAA=", From 7377566f98c8497b6f261a34127d6b7bcb57328f Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 19:22:38 +0700 Subject: [PATCH 093/111] Test finalizeAllInputs against P2SH-P2WPKH input --- test/fixtures/psbt.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index c275f9b..de514f9 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -438,6 +438,11 @@ "type": "P2MS", "psbt": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAAiAgLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCEgwRQIhAOqqlnUwmc+PqKZVwAAMLPNCKskHhVgk1pXR9d7nmB6SAiA0C24idVdtgl9+sIBR8A3Za5WcswNJ3i8PWTMWR10C4QEiAgPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuUREgwRQIhANnDBUQWjS8hDTPidRZ95rGZK3LWCLIZB1NYx9++eBT8AiASupLR+KNa1Se7eoBkWQcDny+BaBfMQ+p1RTCAXd7/GAEBBItSIQLFmuo7gG8OjgmaO5AHuC/1OvDarQt16R7VEKU8dkRRCCEC4X74b+vRF3XLgyXyGejmE5VB1f3RY/IReUggyNHgdLYhA+dqu1GC9JCdInbJ1G7BI9cN1Tc/VhDCFiwrtfN8a5REIQO94OZv6yPD4kEZSZBquYA7FPsABZZ6UAteVuEw7fVxzlSuAAA=", "result": "cHNidP8BAFUCAAAAAbmLTo7GeyF7IT5PJznDiTVjhv5pQKcKe8Qa+8rqUpXuAAAAAAD/////AYA4AQAAAAAAGXapFF2G82oAzUpx/kcDUPy58goX16gkiKwAAAAAAAEAvgIAAAABMBwIKVKOFbas6tBD24LPchtrweE2jiXkPgSHdJndEfoAAAAAa0gwRQIhAJNchwx4je/ZqtZXSCD6BKJBQkpkTBGGdLoFZTcPKlDtAiAZefY/giaVw5rcEoJk4TRnFCyMG0dYLV5IVnFWkR38fQEhAi5yX8RMW6plVrMY26hithJ6j1w2mgqh/bOCAEVnbn/Z/////wGQXwEAAAAAABepFIoQ3seh+nGYzkI7MDw5zuSCIxEyhwAAAAABB/0gAQBIMEUCIQDqqpZ1MJnPj6imVcAADCzzQirJB4VYJNaV0fXe55gekgIgNAtuInVXbYJffrCAUfAN2WuVnLMDSd4vD1kzFkddAuEBSDBFAiEA2cMFRBaNLyENM+J1Fn3msZkrctYIshkHU1jH3754FPwCIBK6ktH4o1rVJ7t6gGRZBwOfL4FoF8xD6nVFMIBd3v8YAUyLUiECxZrqO4BvDo4JmjuQB7gv9Trw2q0Ldeke1RClPHZEUQghAuF++G/r0Rd1y4Ml8hno5hOVQdX90WPyEXlIIMjR4HS2IQPnartRgvSQnSJ2ydRuwSPXDdU3P1YQwhYsK7XzfGuURCEDveDmb+sjw+JBGUmQarmAOxT7AAWWelALXlbhMO31cc5UrgAA" + }, + { + "type": "P2SH-P2WPKH", + "psbt": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAAiAgKj88rhJwk3Zxm0p0Rp+xC/6cxmj+I741DHPWPWN7iA+0cwRAIgTRhd9WUpoHYl9tUVmoJ336fJAJInIjdYsoatvRiW8hgCIGOYMlpKRHiHA428Sfa2CdAIIGGQCGhuIgIzj2FN6USnAQEEFgAU4sZupXPxqhcsOB1ghJxBvH4XcesAAA==", + "result": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAABBxcWABTixm6lc/GqFyw4HWCEnEG8fhdx6wAA" } ], "fromTransaction": [ From 876a241e0c6d1f4e69596de945bc453bcd9c3976 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Wed, 10 Jul 2019 19:23:01 +0700 Subject: [PATCH 094/111] Test finalizeAllInputs against P2WPKH input --- test/fixtures/psbt.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index de514f9..72d5662 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -443,6 +443,11 @@ "type": "P2SH-P2WPKH", "psbt": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAAiAgKj88rhJwk3Zxm0p0Rp+xC/6cxmj+I741DHPWPWN7iA+0cwRAIgTRhd9WUpoHYl9tUVmoJ336fJAJInIjdYsoatvRiW8hgCIGOYMlpKRHiHA428Sfa2CdAIIGGQCGhuIgIzj2FN6USnAQEEFgAU4sZupXPxqhcsOB1ghJxBvH4XcesAAA==", "result": "cHNidP8BAFUCAAAAATIK6DMTn8bbrG7ZdiiVU3/YAgzyk3dwa56En58YfXbDAAAAAAD/////AYA4AQAAAAAAGXapFNU4SHWUW9ZNz+BcxCiuU/7UtJoMiKwAAAAAAAEAvgIAAAABly2BCiYZ3slqurlLwE7b8UXINYKfrJ9sQlBovzBAwFsBAAAAa0gwRQIhAJJ+Hyniw+KneWomeQYrP1duH7cfQ3j8GN6/RshZCfuvAiBux7Uu/5QqmSmL+LjoWZY2b9TWdluY6zLTkQWIornmYwEhAvUo9Sy7Pu44z84ZZPrQMQxBPpDJyy9WlLQMGdGIuUy7/////wGQXwEAAAAAABepFPwojcWCH2oE9dUjMmLC3bdK/xeWhwAAAAABBxcWABTixm6lc/GqFyw4HWCEnEG8fhdx6wAA" + }, + { + "type": "P2WPKH", + "psbt": "cHNidP8BAFUCAAAAAQgW434j2ReNazU0B0+Wa45cPWyUkrl6beWc1ZA6s7qYAAAAAAD/////AYA4AQAAAAAAGXapFC6fT3nay4TXcKcXnM8ZZ89Pmi8xiKwAAAAAAAEAvAIAAAABX6KFQJqSuzjln4vq33jes8S/HAJw/W8fB2hmYJvF95AAAAAAakcwRAIgBfylcQEFaoyuzZcVT1AG3nU3FxDE2ZRNSQbH0Pb38pkCIBh+SztZRlITl/WbdA/Z7SVsVqApPeT1HmzpiLcunN4yASECHwUJGd/AygUjfs6zraDrLcjE5arbSBzHCVX/DQD3tuf/////AZBfAQAAAAAAFgAU2gkxRpZzSTR+etKYJ+B243D+sbYAAAAAIgIDV7l7X08dWtzMR2saPXJo782Tot5+PQBMZZOm3GGTUHpIMEUCIQDxPCn2fwfxoYgGVyi5++mXNAmCX2wWRS7LuJB2qQkMVQIgHsxylMEWd7ladApdSpnEtSwWb4/QJGOnsDGZCPxvfucBAAA=", + "result": "cHNidP8BAFUCAAAAAQgW434j2ReNazU0B0+Wa45cPWyUkrl6beWc1ZA6s7qYAAAAAAD/////AYA4AQAAAAAAGXapFC6fT3nay4TXcKcXnM8ZZ89Pmi8xiKwAAAAAAAEAvAIAAAABX6KFQJqSuzjln4vq33jes8S/HAJw/W8fB2hmYJvF95AAAAAAakcwRAIgBfylcQEFaoyuzZcVT1AG3nU3FxDE2ZRNSQbH0Pb38pkCIBh+SztZRlITl/WbdA/Z7SVsVqApPeT1HmzpiLcunN4yASECHwUJGd/AygUjfs6zraDrLcjE5arbSBzHCVX/DQD3tuf/////AZBfAQAAAAAAFgAU2gkxRpZzSTR+etKYJ+B243D+sbYAAAAAAQcAAAA=" } ], "fromTransaction": [ From 9ee115b030b7effaf10b18c4e24e5aa9181efc8f Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Jul 2019 10:11:51 +0900 Subject: [PATCH 095/111] assert the fee calculation is correct --- test/fixtures/psbt.json | 3 ++- test/psbt.js | 34 ++++++++++++++++------------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 72d5662..638eb19 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -464,7 +464,8 @@ "nonExistantIndex": 42 }, "getFeeRate": { - "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", + "fee": 21 }, "cache": { "nonWitnessUtxo": { diff --git a/test/psbt.js b/test/psbt.js index 6dd01a9..0ccd8ef 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -97,22 +97,22 @@ describe(`Psbt`, () => { fixtures.bip174.signer.forEach(f => { it('Signs PSBT to the expected result', () => { const psbt = Psbt.fromBase64(f.psbt) - + f.keys.forEach(({inputToSign, WIF}) => { const keyPair = ECPair.fromWIF(WIF, NETWORKS.testnet); psbt.signInput(inputToSign, keyPair); }) - + assert.strictEqual(psbt.toBase64(), f.result) }) }) - + fixtures.bip174.combiner.forEach(f => { it('Combines two PSBTs to the expected result', () => { const psbts = f.psbts.map(psbt => Psbt.fromBase64(psbt)) - + psbts[0].combine(psbts[1]) - + // Produces a different Base64 string due to implemetation specific key-value ordering. // That means this test will fail: // assert.strictEqual(psbts[0].toBase64(), f.result) @@ -120,41 +120,41 @@ describe(`Psbt`, () => { assert.deepStrictEqual(psbts[0], Psbt.fromBase64(f.result)) }) }) - + fixtures.bip174.finalizer.forEach(f => { it("Finalizes inputs and gives the expected PSBT", () => { const psbt = Psbt.fromBase64(f.psbt) - + psbt.finalizeAllInputs() - + assert.strictEqual(psbt.toBase64(), f.result) }) }) - + fixtures.bip174.extractor.forEach(f => { it('Extracts the expected transaction from a PSBT', () => { const psbt1 = Psbt.fromBase64(f.psbt) const transaction1 = psbt1.extractTransaction(true).toHex() - + const psbt2 = Psbt.fromBase64(f.psbt) const transaction2 = psbt2.extractTransaction().toHex() - + assert.strictEqual(transaction1, transaction2) assert.strictEqual(transaction1, f.transaction) - + const psbt3 = Psbt.fromBase64(f.psbt) delete psbt3.inputs[0].finalScriptSig delete psbt3.inputs[0].finalScriptWitness assert.throws(() => { psbt3.extractTransaction() }, new RegExp('Not finalized')) - + const psbt4 = Psbt.fromBase64(f.psbt) psbt4.setMaximumFeeRate(1) assert.throws(() => { psbt4.extractTransaction() }, new RegExp('Warning: You are paying around [\\d.]+ in fees')) - + const psbt5 = Psbt.fromBase64(f.psbt) psbt5.extractTransaction(true) const fr1 = psbt5.getFeeRate() @@ -426,9 +426,7 @@ describe(`Psbt`, () => { psbt.finalizeAllInputs() - assert.doesNotThrow(() => { - psbt.getFeeRate() - }) + assert.strictEqual(psbt.getFeeRate(), f.fee) }) }) @@ -516,4 +514,4 @@ describe(`Psbt`, () => { assert.ok(psbt.inputs[index].nonWitnessUtxo.equals(value)) }) }) -}) \ No newline at end of file +}) From 266302a3ae29413362d8108ba18e6bfd515e9820 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Jul 2019 10:21:36 +0900 Subject: [PATCH 096/111] Add P2WSH-P2PK finalize vector --- test/fixtures/psbt.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 638eb19..aeed39d 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -448,6 +448,11 @@ "type": "P2WPKH", "psbt": "cHNidP8BAFUCAAAAAQgW434j2ReNazU0B0+Wa45cPWyUkrl6beWc1ZA6s7qYAAAAAAD/////AYA4AQAAAAAAGXapFC6fT3nay4TXcKcXnM8ZZ89Pmi8xiKwAAAAAAAEAvAIAAAABX6KFQJqSuzjln4vq33jes8S/HAJw/W8fB2hmYJvF95AAAAAAakcwRAIgBfylcQEFaoyuzZcVT1AG3nU3FxDE2ZRNSQbH0Pb38pkCIBh+SztZRlITl/WbdA/Z7SVsVqApPeT1HmzpiLcunN4yASECHwUJGd/AygUjfs6zraDrLcjE5arbSBzHCVX/DQD3tuf/////AZBfAQAAAAAAFgAU2gkxRpZzSTR+etKYJ+B243D+sbYAAAAAIgIDV7l7X08dWtzMR2saPXJo782Tot5+PQBMZZOm3GGTUHpIMEUCIQDxPCn2fwfxoYgGVyi5++mXNAmCX2wWRS7LuJB2qQkMVQIgHsxylMEWd7ladApdSpnEtSwWb4/QJGOnsDGZCPxvfucBAAA=", "result": "cHNidP8BAFUCAAAAAQgW434j2ReNazU0B0+Wa45cPWyUkrl6beWc1ZA6s7qYAAAAAAD/////AYA4AQAAAAAAGXapFC6fT3nay4TXcKcXnM8ZZ89Pmi8xiKwAAAAAAAEAvAIAAAABX6KFQJqSuzjln4vq33jes8S/HAJw/W8fB2hmYJvF95AAAAAAakcwRAIgBfylcQEFaoyuzZcVT1AG3nU3FxDE2ZRNSQbH0Pb38pkCIBh+SztZRlITl/WbdA/Z7SVsVqApPeT1HmzpiLcunN4yASECHwUJGd/AygUjfs6zraDrLcjE5arbSBzHCVX/DQD3tuf/////AZBfAQAAAAAAFgAU2gkxRpZzSTR+etKYJ+B243D+sbYAAAAAAQcAAAA=" + }, + { + "type": "P2WSH-P2PK", + "psbt": "cHNidP8BAKYCAAAAAlwKQ3suPWwEJ/zQ9sZsIioOcHKU1KoLMxlMNSXVIkEWAAAAAAD/////YYDJMap+mYgbTrCNAdpWHN+EkKvl+XYao/6co/EQfwMAAAAAAP////8CkF8BAAAAAAAWABRnPBAmVHz2HL+8/1U+QG5L2thjmjhKAAAAAAAAIgAg700yfFRyhWzQnPHIUb/XQqsjlpf4A0uw682pCVWuQ8IAAAAAAAEBKzB1AAAAAAAAIgAgth9oE4cDfC5aV58VgkW5CptHsIxppYzJV8C5kT6aTo8iAgNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06UgwRQIhALO0xRpuqP3aVkm+DPykrtqe6fPNSgNblp9K9MAwmtHJAiAHV5ZvZN8Vi49n/o9ISFyvtHsPXXPKqBxC9m2m2HlpYgEBBSMhA182CcWcQ9L2zg9j8jlt7OlaIATNDgNFMBKB1J14wzTprAABASuAOAEAAAAAACIAILYfaBOHA3wuWlefFYJFuQqbR7CMaaWMyVfAuZE+mk6PIgIDXzYJxZxD0vbOD2PyOW3s6VogBM0OA0UwEoHUnXjDNOlIMEUCIQC6XN6tpo9mYlZej4BXSSh5D1K6aILBfQ4WBWXUrISx6wIgVaxFUsz8y59xJ1V4sZ1qarHX9pZ+MJmLKbl2ZSadisoBAQUjIQNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06awAAAA=", + "result": "cHNidP8BAKYCAAAAAlwKQ3suPWwEJ/zQ9sZsIioOcHKU1KoLMxlMNSXVIkEWAAAAAAD/////YYDJMap+mYgbTrCNAdpWHN+EkKvl+XYao/6co/EQfwMAAAAAAP////8CkF8BAAAAAAAWABRnPBAmVHz2HL+8/1U+QG5L2thjmjhKAAAAAAAAIgAg700yfFRyhWzQnPHIUb/XQqsjlpf4A0uw682pCVWuQ8IAAAAAAAEBKzB1AAAAAAAAIgAgth9oE4cDfC5aV58VgkW5CptHsIxppYzJV8C5kT6aTo8BCG4CSDBFAiEAs7TFGm6o/dpWSb4M/KSu2p7p881KA1uWn0r0wDCa0ckCIAdXlm9k3xWLj2f+j0hIXK+0ew9dc8qoHEL2babYeWliASMhA182CcWcQ9L2zg9j8jlt7OlaIATNDgNFMBKB1J14wzTprAABASuAOAEAAAAAACIAILYfaBOHA3wuWlefFYJFuQqbR7CMaaWMyVfAuZE+mk6PAQhuAkgwRQIhALpc3q2mj2ZiVl6PgFdJKHkPUrpogsF9DhYFZdSshLHrAiBVrEVSzPzLn3EnVXixnWpqsdf2ln4wmYspuXZlJp2KygEjIQNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06awAAAA=" } ], "fromTransaction": [ From 8a5104c33366da88d56b1102be261fe81a9e35d9 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Jul 2019 11:09:05 +0900 Subject: [PATCH 097/111] Add tests --- test/fixtures/psbt.json | 18 ++++++++--- test/psbt.js | 68 ++++++++++++++++++++++++++++++++--------- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index aeed39d..61ab114 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -314,7 +314,7 @@ "exception": "Error adding input." }, { - "description": "checks for hash and index", + "description": "should be equal", "inputData": { "hash": "Buffer.from('000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f', 'hex')", "index": 2 @@ -326,12 +326,19 @@ "addOutput": { "checks": [ { - "description": "checks for hash and index", + "description": "Checks value is number", "outputData": { "address": "1P2NFEBp32V2arRwZNww6tgXEV58FG94mr", "value": "xyz" }, "exception": "Error adding output." + }, + { + "description": "Adds output normally", + "outputData": { + "address": "1P2NFEBp32V2arRwZNww6tgXEV58FG94mr", + "value": 42 + } } ] }, @@ -446,8 +453,8 @@ }, { "type": "P2WPKH", - "psbt": "cHNidP8BAFUCAAAAAQgW434j2ReNazU0B0+Wa45cPWyUkrl6beWc1ZA6s7qYAAAAAAD/////AYA4AQAAAAAAGXapFC6fT3nay4TXcKcXnM8ZZ89Pmi8xiKwAAAAAAAEAvAIAAAABX6KFQJqSuzjln4vq33jes8S/HAJw/W8fB2hmYJvF95AAAAAAakcwRAIgBfylcQEFaoyuzZcVT1AG3nU3FxDE2ZRNSQbH0Pb38pkCIBh+SztZRlITl/WbdA/Z7SVsVqApPeT1HmzpiLcunN4yASECHwUJGd/AygUjfs6zraDrLcjE5arbSBzHCVX/DQD3tuf/////AZBfAQAAAAAAFgAU2gkxRpZzSTR+etKYJ+B243D+sbYAAAAAIgIDV7l7X08dWtzMR2saPXJo782Tot5+PQBMZZOm3GGTUHpIMEUCIQDxPCn2fwfxoYgGVyi5++mXNAmCX2wWRS7LuJB2qQkMVQIgHsxylMEWd7ladApdSpnEtSwWb4/QJGOnsDGZCPxvfucBAAA=", - "result": "cHNidP8BAFUCAAAAAQgW434j2ReNazU0B0+Wa45cPWyUkrl6beWc1ZA6s7qYAAAAAAD/////AYA4AQAAAAAAGXapFC6fT3nay4TXcKcXnM8ZZ89Pmi8xiKwAAAAAAAEAvAIAAAABX6KFQJqSuzjln4vq33jes8S/HAJw/W8fB2hmYJvF95AAAAAAakcwRAIgBfylcQEFaoyuzZcVT1AG3nU3FxDE2ZRNSQbH0Pb38pkCIBh+SztZRlITl/WbdA/Z7SVsVqApPeT1HmzpiLcunN4yASECHwUJGd/AygUjfs6zraDrLcjE5arbSBzHCVX/DQD3tuf/////AZBfAQAAAAAAFgAU2gkxRpZzSTR+etKYJ+B243D+sbYAAAAAAQcAAAA=" + "psbt": "cHNidP8BAFICAAAAAb5UIQFSoXPjqxSmuiSZkM5PjaycnrXQ6VB+u2+MzbSGAAAAAAD/////ASBOAAAAAAAAFgAUXsVBEaHSlhycDORbHHtBKwDo4zIAAAAAAAEBHzB1AAAAAAAAFgAUvIgag7HZu7Rjd/ugmJC/MHlZmAYiAgLN1zezMD4c4uegTbgfJ1GCtN5/hlJYaJt7e8mt1BVzIEcwRAIgXhgL5G81tXP7MAaKJtA0QaFu17bLocOGqxXmDoIfYUACIAOIynpoPS/dTz9Omg2h7v5kiql7ab0SPzWDdxpvpsUcAQAA", + "result": "cHNidP8BAFICAAAAAb5UIQFSoXPjqxSmuiSZkM5PjaycnrXQ6VB+u2+MzbSGAAAAAAD/////ASBOAAAAAAAAFgAUXsVBEaHSlhycDORbHHtBKwDo4zIAAAAAAAEBHzB1AAAAAAAAFgAUvIgag7HZu7Rjd/ugmJC/MHlZmAYBCGsCRzBEAiBeGAvkbzW1c/swBoom0DRBoW7Xtsuhw4arFeYOgh9hQAIgA4jKemg9L91PP06aDaHu/mSKqXtpvRI/NYN3Gm+mxRwBIQLN1zezMD4c4uegTbgfJ1GCtN5/hlJYaJt7e8mt1BVzIAAA" }, { "type": "P2WSH-P2PK", @@ -478,5 +485,8 @@ "nonWitnessUtxo": "Buffer.from('0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000', 'hex')", "inputIndex": 0 } + }, + "clone": { + "psbt": "cHNidP8BAKYCAAAAAlwKQ3suPWwEJ/zQ9sZsIioOcHKU1KoLMxlMNSXVIkEWAAAAAAD/////YYDJMap+mYgbTrCNAdpWHN+EkKvl+XYao/6co/EQfwMAAAAAAP////8CkF8BAAAAAAAWABRnPBAmVHz2HL+8/1U+QG5L2thjmjhKAAAAAAAAIgAg700yfFRyhWzQnPHIUb/XQqsjlpf4A0uw682pCVWuQ8IAAAAAAAEBKzB1AAAAAAAAIgAgth9oE4cDfC5aV58VgkW5CptHsIxppYzJV8C5kT6aTo8iAgNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06UgwRQIhALO0xRpuqP3aVkm+DPykrtqe6fPNSgNblp9K9MAwmtHJAiAHV5ZvZN8Vi49n/o9ISFyvtHsPXXPKqBxC9m2m2HlpYgEBBSMhA182CcWcQ9L2zg9j8jlt7OlaIATNDgNFMBKB1J14wzTprAABASuAOAEAAAAAACIAILYfaBOHA3wuWlefFYJFuQqbR7CMaaWMyVfAuZE+mk6PIgIDXzYJxZxD0vbOD2PyOW3s6VogBM0OA0UwEoHUnXjDNOlIMEUCIQC6XN6tpo9mYlZej4BXSSh5D1K6aILBfQ4WBWXUrISx6wIgVaxFUsz8y59xJ1V4sZ1qarHX9pZ+MJmLKbl2ZSadisoBAQUjIQNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06awAAAA=" } } diff --git a/test/psbt.js b/test/psbt.js index 0ccd8ef..99bd9c1 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -28,7 +28,7 @@ describe(`Psbt`, () => { it(`Invalid: ${f.description}`, () => { assert.throws(() => { Psbt.fromBase64(f.psbt) - }, {message: f.errorMessage}) + }, new RegExp(f.errorMessage)) }) }) @@ -46,7 +46,7 @@ describe(`Psbt`, () => { const psbt = Psbt.fromBase64(f.psbt) assert.throws(() => { psbt.signInput(f.inputToCheck, keyPair) - }, {message: f.errorMessage}) + }, new RegExp(f.errorMessage)) }) }) @@ -181,7 +181,7 @@ describe(`Psbt`, () => { f.shouldThrow.inputToCheck, ECPair.fromWIF(f.shouldThrow.WIF), ) - }, {message: f.shouldThrow.errorMessage}) + }, new RegExp(f.shouldThrow.errorMessage)) assert.rejects(async () => { await psbtThatShouldThrow.signInputAsync( f.shouldThrow.inputToCheck, @@ -208,7 +208,7 @@ describe(`Psbt`, () => { f.shouldThrow.inputToCheck, ECPair.fromWIF(f.shouldThrow.WIF), ) - }, {message: f.shouldThrow.errorMessage}) + }, new RegExp(f.shouldThrow.errorMessage)) assert.throws(() => { psbtThatShouldThrow.signInput( f.shouldThrow.inputToCheck, @@ -276,6 +276,23 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.toBase64(), f.result) }) }) + it('fails if no script found', () => { + const psbt = new Psbt() + psbt.addInput({ + hash: '0000000000000000000000000000000000000000000000000000000000000000', + index: 0 + }) + assert.throws(() => { + psbt.finalizeAllInputs() + }, new RegExp('No script found for input #0')) + psbt.addWitnessUtxoToInput(0, { + script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d', 'hex'), + value: 2e5 + }) + assert.throws(() => { + psbt.finalizeAllInputs() + }, new RegExp('Can not finalize input #0')) + }) }) describe('fromTransaction', () => { @@ -289,9 +306,6 @@ describe(`Psbt`, () => { describe('addInput', () => { fixtures.addInput.checks.forEach(f => { - for (const attr of Object.keys(f.inputData)) { - f.inputData[attr] = f.inputData[attr] - } it(f.description, () => { const psbt = new Psbt() @@ -299,13 +313,14 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.addInput(f.inputData) }, new RegExp(f.exception)) + assert.throws(() => { + psbt.addInputs([f.inputData]) + }, new RegExp(f.exception)) } else { assert.doesNotThrow(() => { - psbt.addInput(f.inputData) + psbt.addInputs([f.inputData]) if (f.equals) { assert.strictEqual(psbt.toBase64(), f.equals) - } else { - console.log(psbt.toBase64()) } }) assert.throws(() => { @@ -318,9 +333,6 @@ describe(`Psbt`, () => { describe('addOutput', () => { fixtures.addOutput.checks.forEach(f => { - for (const attr of Object.keys(f.outputData)) { - f.outputData[attr] = f.outputData[attr] - } it(f.description, () => { const psbt = new Psbt() @@ -328,10 +340,15 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.addOutput(f.outputData) }, new RegExp(f.exception)) + assert.throws(() => { + psbt.addOutputs([f.outputData]) + }, new RegExp(f.exception)) } else { assert.doesNotThrow(() => { psbt.addOutput(f.outputData) - console.log(psbt.toBase64()) + }) + assert.doesNotThrow(() => { + psbt.addOutputs([f.outputData]) }) } }) @@ -381,7 +398,26 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.setSequence(1, 0) - }, {message: 'Input index too high'}) + }, new RegExp('Input index too high')) + }) + }) + + describe('clone', () => { + it('Should clone a psbt exactly with no reference', () => { + const f = fixtures.clone + const psbt = Psbt.fromBase64(f.psbt) + const notAClone = Object.assign(new Psbt(), psbt) // references still active + const clone = psbt.clone() + + assert.strictEqual(psbt.validateAllSignatures(), true) + + assert.strictEqual(clone.toBase64(), psbt.toBase64()) + assert.strictEqual(clone.toBase64(), notAClone.toBase64()) + assert.strictEqual(psbt.toBase64(), notAClone.toBase64()) + psbt.globalMap.unsignedTx[3] = 0xff + assert.notStrictEqual(clone.toBase64(), psbt.toBase64()) + assert.notStrictEqual(clone.toBase64(), notAClone.toBase64()) + assert.strictEqual(psbt.toBase64(), notAClone.toBase64()) }) }) @@ -427,6 +463,8 @@ describe(`Psbt`, () => { psbt.finalizeAllInputs() assert.strictEqual(psbt.getFeeRate(), f.fee) + psbt.__CACHE.__FEE_RATE = undefined + assert.strictEqual(psbt.getFeeRate(), f.fee) }) }) From 2f1609b9189d877810a80d40a4c8e6653f440434 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Jul 2019 11:28:09 +0900 Subject: [PATCH 098/111] Fix: P2WPKH was signing with nonWitnessUtxo --- src/psbt.js | 23 +++++++++++++---------- test/fixtures/psbt.json | 2 +- ts_src/psbt.ts | 25 +++++++++++++++---------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index c7e8b55..752714f 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -404,6 +404,7 @@ const isP2MS = isPaymentFactory(payments.p2ms); const isP2PK = isPaymentFactory(payments.p2pk); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); +const isP2WSHScript = isPaymentFactory(payments.p2wsh); function check32Bit(num) { if ( typeof num !== 'number' || @@ -611,19 +612,16 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript(inputIndex, prevout.script, input.redeemScript); script = input.redeemScript; - hash = unsignedTx.hashForSignature( - inputIndex, - input.redeemScript, - sighashType, - ); } else { script = prevout.script; - hash = unsignedTx.hashForSignature( - inputIndex, - prevout.script, - sighashType, + } + if (isP2WPKH(script) || isP2WSHScript(script)) { + throw new Error( + `Input #${inputIndex} has nonWitnessUtxo but segwit script: ` + + `${script.toString('hex')}`, ); } + hash = unsignedTx.hashForSignature(inputIndex, script, sighashType); } else if (input.witnessUtxo) { let _script; // so we don't shadow the `let script` above if (input.redeemScript) { @@ -647,7 +645,7 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { sighashType, ); script = _script; - } else { + } else if (isP2WSHScript(_script)) { if (!input.witnessScript) throw new Error('Segwit input needs witnessScript if not P2WPKH'); checkWitnessScript(inputIndex, _script, input.witnessScript); @@ -659,6 +657,11 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { ); // 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')}`, + ); } } else { throw new Error('Need a Utxo input item for signing'); diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 61ab114..fbad821 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -121,7 +121,7 @@ "failSignChecks": [ { "description": "A Witness UTXO is provided for a non-witness input", - "errorMessage": "Segwit input needs witnessScript if not P2WPKH", + "errorMessage": "Input #0 has witnessUtxo but non-segwit script", "psbt": "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEBItPf9QUAAAAAGXapFNSO0xELlAFMsRS9Mtb00GbcdCVriKwAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", "inputToCheck": 0 }, diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 8fb1e97..928ca04 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -509,6 +509,7 @@ const isP2MS = isPaymentFactory(payments.p2ms); const isP2PK = isPaymentFactory(payments.p2pk); const isP2PKH = isPaymentFactory(payments.p2pkh); const isP2WPKH = isPaymentFactory(payments.p2wpkh); +const isP2WSHScript = isPaymentFactory(payments.p2wsh); function check32Bit(num: number): void { if ( @@ -764,19 +765,18 @@ function getHashForSig( // If a redeemScript is provided, the scriptPubKey must be for that redeemScript checkRedeemScript(inputIndex, prevout.script, input.redeemScript); script = input.redeemScript; - hash = unsignedTx.hashForSignature( - inputIndex, - input.redeemScript, - sighashType, - ); } else { script = prevout.script; - hash = unsignedTx.hashForSignature( - inputIndex, - prevout.script, - sighashType, + } + + if (isP2WPKH(script) || isP2WSHScript(script)) { + throw new Error( + `Input #${inputIndex} has nonWitnessUtxo but segwit script: ` + + `${script.toString('hex')}`, ); } + + hash = unsignedTx.hashForSignature(inputIndex, script, sighashType); } else if (input.witnessUtxo) { let _script: Buffer; // so we don't shadow the `let script` above if (input.redeemScript) { @@ -800,7 +800,7 @@ function getHashForSig( sighashType, ); script = _script; - } else { + } else if (isP2WSHScript(_script)) { if (!input.witnessScript) throw new Error('Segwit input needs witnessScript if not P2WPKH'); checkWitnessScript(inputIndex, _script, input.witnessScript); @@ -812,6 +812,11 @@ function getHashForSig( ); // 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')}`, + ); } } else { throw new Error('Need a Utxo input item for signing'); From 1feef9569c60f5c7eaf0072a8fd79a81e2cc44b7 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 11 Jul 2019 17:17:49 +0900 Subject: [PATCH 099/111] Composition over inheritance --- package-lock.json | 6 +- package.json | 2 +- src/psbt.js | 199 ++++++++++++++++++++++++--------- test/psbt.js | 12 +- ts_src/psbt.ts | 277 +++++++++++++++++++++++++++++++++++----------- types/psbt.d.ts | 36 +++++- 6 files changed, 402 insertions(+), 130 deletions(-) diff --git a/package-lock.json b/package-lock.json index fce68c4..fabd361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,9 @@ } }, "bip174": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.14.tgz", - "integrity": "sha512-v9cre0W4ZpAJS1v18WUJLE9yKdSZyenGpZBg7CXiZ5n35JPXganH92d4Yk8WXpRfbFZ4SMXTqKLEgpLPX1TmcA==" + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.15.tgz", + "integrity": "sha512-mK/s9p7i+PG7W2s2cAedNVk1NDZQn9wAoq1DlsS2+1zz5TXR3TRTzqRqm9BQtOXwbkxkhfLwlmsOjuiRdAYkdg==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 03e6d5f..a32021a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.14", + "bip174": "0.0.15", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", diff --git a/src/psbt.js b/src/psbt.js index 752714f..578e17c 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -15,9 +15,9 @@ const DEFAULT_OPTS = { network: networks_1.bitcoin, maximumFeeRate: 5000, }; -class Psbt extends bip174_1.Psbt { - constructor(opts = {}) { - super(); +class Psbt { + constructor(opts = {}, data = new bip174_1.Psbt()) { + this.data = data; this.__CACHE = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], @@ -27,11 +27,11 @@ class Psbt extends bip174_1.Psbt { // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); const c = this.__CACHE; - c.__TX = transaction_1.Transaction.fromBuffer(this.globalMap.unsignedTx); - this.setVersion(2); + c.__TX = transaction_1.Transaction.fromBuffer(data.globalMap.unsignedTx); + if (this.data.inputs.length === 0) this.setVersion(2); // set cache - delete this.globalMap.unsignedTx; - Object.defineProperty(this.globalMap, 'unsignedTx', { + delete data.globalMap.unsignedTx; + Object.defineProperty(data.globalMap, 'unsignedTx', { enumerable: true, get() { const buf = c.__TX_BUF_CACHE; @@ -42,8 +42,8 @@ class Psbt extends bip174_1.Psbt { return c.__TX_BUF_CACHE; } }, - set(data) { - c.__TX_BUF_CACHE = data; + set(_data) { + c.__TX_BUF_CACHE = _data; }, }); // Make data hidden when enumerating @@ -55,29 +55,38 @@ class Psbt extends bip174_1.Psbt { dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } - static fromTransaction(txBuf) { + static fromTransaction(txBuf, opts = {}) { const tx = transaction_1.Transaction.fromBuffer(txBuf); checkTxEmpty(tx); - const psbt = new this(); + const psbtBase = new bip174_1.Psbt(); + const psbt = new Psbt(opts, psbtBase); psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); let inputCount = tx.ins.length; let outputCount = tx.outs.length; while (inputCount > 0) { - psbt.inputs.push({ - keyVals: [], + psbtBase.inputs.push({ + unknownKeyVals: [], }); inputCount--; } while (outputCount > 0) { - psbt.outputs.push({ - keyVals: [], + psbtBase.outputs.push({ + unknownKeyVals: [], }); outputCount--; } return psbt; } - static fromBuffer(buffer) { + static fromBase64(data, opts = {}) { + const buffer = Buffer.from(data, 'base64'); + return this.fromBuffer(buffer, opts); + } + static fromHex(data, opts = {}) { + const buffer = Buffer.from(data, 'hex'); + return this.fromBuffer(buffer, opts); + } + static fromBuffer(buffer, opts = {}) { let tx; const txCountGetter = txBuf => { tx = transaction_1.Transaction.fromBuffer(txBuf); @@ -87,17 +96,22 @@ class Psbt extends bip174_1.Psbt { outputCount: tx.outs.length, }; }; - const psbt = super.fromBuffer(buffer, txCountGetter); + const psbtBase = bip174_1.Psbt.fromBuffer(buffer, txCountGetter); + const psbt = new Psbt(opts, psbtBase); psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); return psbt; } get inputCount() { - return this.inputs.length; + return this.data.inputs.length; + } + combine(...those) { + this.data.combine(...those.map(o => o.data)); + return this; } clone() { // TODO: more efficient cloning - const res = Psbt.fromBuffer(this.toBuffer()); + const res = Psbt.fromBuffer(this.data.toBuffer()); res.opts = JSON.parse(JSON.stringify(this.opts)); return res; } @@ -107,7 +121,7 @@ class Psbt extends bip174_1.Psbt { } setVersion(version) { check32Bit(version); - checkInputsForPartialSig(this.inputs, 'setVersion'); + checkInputsForPartialSig(this.data.inputs, 'setVersion'); const c = this.__CACHE; c.__TX.version = version; c.__TX_BUF_CACHE = undefined; @@ -116,7 +130,7 @@ class Psbt extends bip174_1.Psbt { } setLocktime(locktime) { check32Bit(locktime); - checkInputsForPartialSig(this.inputs, 'setLocktime'); + checkInputsForPartialSig(this.data.inputs, 'setLocktime'); const c = this.__CACHE; c.__TX.locktime = locktime; c.__TX_BUF_CACHE = undefined; @@ -125,7 +139,7 @@ class Psbt extends bip174_1.Psbt { } setSequence(inputIndex, sequence) { check32Bit(sequence); - checkInputsForPartialSig(this.inputs, 'setSequence'); + checkInputsForPartialSig(this.data.inputs, 'setSequence'); const c = this.__CACHE; if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); @@ -140,10 +154,15 @@ class Psbt extends bip174_1.Psbt { return this; } addInput(inputData) { - checkInputsForPartialSig(this.inputs, 'addInput'); + checkInputsForPartialSig(this.data.inputs, 'addInput'); const c = this.__CACHE; const inputAdder = getInputAdder(c); - super.addInput(inputData, inputAdder); + this.data.addInput(inputData, inputAdder); + const inputIndex = this.data.inputs.length - 1; + const input = this.data.inputs[inputIndex]; + if (input.nonWitnessUtxo) { + addNonWitnessTxCache(this.__CACHE, input, inputIndex); + } c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; @@ -153,7 +172,7 @@ class Psbt extends bip174_1.Psbt { return this; } addOutput(outputData) { - checkInputsForPartialSig(this.inputs, 'addOutput'); + checkInputsForPartialSig(this.data.inputs, 'addOutput'); const { address } = outputData; if (typeof address === 'string') { const { network } = this.opts; @@ -162,30 +181,24 @@ class Psbt extends bip174_1.Psbt { } const c = this.__CACHE; const outputAdder = getOutputAdder(c); - super.addOutput(outputData, true, outputAdder); + this.data.addOutput(outputData, outputAdder, true); c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; } - addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { - super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); - const input = this.inputs[inputIndex]; - addNonWitnessTxCache(this.__CACHE, input, inputIndex); - return this; - } extractTransaction(disableFeeCheck) { - if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + if (!this.data.inputs.every(isFinalized)) throw new Error('Not finalized'); const c = this.__CACHE; if (!disableFeeCheck) { checkFees(this, c, this.opts); } if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; const tx = c.__TX.clone(); - inputFinalizeGetAmts(this.inputs, tx, c, true); + inputFinalizeGetAmts(this.data.inputs, tx, c, true); return tx; } getFeeRate() { - if (!this.inputs.every(isFinalized)) + if (!this.data.inputs.every(isFinalized)) throw new Error('PSBT must be finalized to calculate fee rate'); const c = this.__CACHE; if (c.__FEE_RATE) return c.__FEE_RATE; @@ -197,16 +210,16 @@ class Psbt extends bip174_1.Psbt { } else { tx = c.__TX.clone(); } - inputFinalizeGetAmts(this.inputs, tx, c, mustFinalize); + inputFinalizeGetAmts(this.data.inputs, tx, c, mustFinalize); return c.__FEE_RATE; } finalizeAllInputs() { - utils_1.checkForInput(this.inputs, 0); // making sure we have at least one - range(this.inputs.length).forEach(idx => this.finalizeInput(idx)); + utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one + range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } finalizeInput(inputIndex) { - const input = utils_1.checkForInput(this.inputs, inputIndex); + const input = utils_1.checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, @@ -226,23 +239,23 @@ class Psbt extends bip174_1.Psbt { isP2WSH, ); if (finalScriptSig) - this.addFinalScriptSigToInput(inputIndex, finalScriptSig); + this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); if (finalScriptWitness) - this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); - this.clearFinalizedInput(inputIndex); + this.data.clearFinalizedInput(inputIndex); return this; } validateAllSignatures() { - utils_1.checkForInput(this.inputs, 0); // making sure we have at least one - const results = range(this.inputs.length).map(idx => + utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one + const results = range(this.data.inputs.length).map(idx => this.validateSignatures(idx), ); return results.reduce((final, res) => res === true && final, true); } validateSignatures(inputIndex, pubkey) { - const input = this.inputs[inputIndex]; + const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; if (!input || !partialSig || partialSig.length < 1) throw new Error('No signatures to validate'); @@ -280,7 +293,7 @@ class Psbt extends bip174_1.Psbt { // as input information is added, then eventually // optimize this method. const results = []; - for (const i of range(this.inputs.length)) { + for (const i of range(this.data.inputs.length)) { try { this.signInput(i, keyPair, sighashTypes); results.push(true); @@ -302,7 +315,7 @@ class Psbt extends bip174_1.Psbt { // optimize this method. const results = []; const promises = []; - for (const [i] of this.inputs.entries()) { + for (const [i] of this.data.inputs.entries()) { promises.push( this.signInputAsync(i, keyPair, sighashTypes).then( () => { @@ -330,7 +343,7 @@ class Psbt extends bip174_1.Psbt { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( - this.inputs, + this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, @@ -340,7 +353,8 @@ class Psbt extends bip174_1.Psbt { pubkey: keyPair.publicKey, signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }; - return this.addPartialSigToInput(inputIndex, partialSig); + this.data.addPartialSigToInput(inputIndex, partialSig); + return this; } signInputAsync( inputIndex, @@ -351,7 +365,7 @@ class Psbt extends bip174_1.Psbt { if (!keyPair || !keyPair.publicKey) return reject(new Error('Need Signer to sign input')); const { hash, sighashType } = getHashAndSighashType( - this.inputs, + this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, @@ -362,11 +376,94 @@ class Psbt extends bip174_1.Psbt { pubkey: keyPair.publicKey, signature: bscript.signature.encode(signature, sighashType), }; - this.addPartialSigToInput(inputIndex, partialSig); + this.data.addPartialSigToInput(inputIndex, partialSig); resolve(); }); }); } + toBuffer() { + return this.data.toBuffer(); + } + toHex() { + return this.data.toHex(); + } + toBase64() { + return this.data.toBase64(); + } + addGlobalXpubToGlobal(globalXpub) { + this.data.addGlobalXpubToGlobal(globalXpub); + return this; + } + addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { + this.data.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); + const input = this.data.inputs[inputIndex]; + addNonWitnessTxCache(this.__CACHE, input, inputIndex); + return this; + } + addWitnessUtxoToInput(inputIndex, witnessUtxo) { + this.data.addWitnessUtxoToInput(inputIndex, witnessUtxo); + return this; + } + addPartialSigToInput(inputIndex, partialSig) { + this.data.addPartialSigToInput(inputIndex, partialSig); + return this; + } + addSighashTypeToInput(inputIndex, sighashType) { + this.data.addSighashTypeToInput(inputIndex, sighashType); + return this; + } + addRedeemScriptToInput(inputIndex, redeemScript) { + this.data.addRedeemScriptToInput(inputIndex, redeemScript); + return this; + } + addWitnessScriptToInput(inputIndex, witnessScript) { + this.data.addWitnessScriptToInput(inputIndex, witnessScript); + return this; + } + addBip32DerivationToInput(inputIndex, bip32Derivation) { + this.data.addBip32DerivationToInput(inputIndex, bip32Derivation); + return this; + } + addFinalScriptSigToInput(inputIndex, finalScriptSig) { + this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); + return this; + } + addFinalScriptWitnessToInput(inputIndex, finalScriptWitness) { + this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + return this; + } + addPorCommitmentToInput(inputIndex, porCommitment) { + this.data.addPorCommitmentToInput(inputIndex, porCommitment); + return this; + } + addRedeemScriptToOutput(outputIndex, redeemScript) { + this.data.addRedeemScriptToOutput(outputIndex, redeemScript); + return this; + } + addWitnessScriptToOutput(outputIndex, witnessScript) { + this.data.addWitnessScriptToOutput(outputIndex, witnessScript); + return this; + } + addBip32DerivationToOutput(outputIndex, bip32Derivation) { + this.data.addBip32DerivationToOutput(outputIndex, bip32Derivation); + return this; + } + addUnknownKeyValToGlobal(keyVal) { + this.data.addUnknownKeyValToGlobal(keyVal); + return this; + } + addUnknownKeyValToInput(inputIndex, keyVal) { + this.data.addUnknownKeyValToInput(inputIndex, keyVal); + return this; + } + addUnknownKeyValToOutput(outputIndex, keyVal) { + this.data.addUnknownKeyValToOutput(outputIndex, keyVal); + return this; + } + clearFinalizedInput(inputIndex) { + this.data.clearFinalizedInput(inputIndex); + return this; + } } exports.Psbt = Psbt; function canFinalize(input, script, scriptType) { diff --git a/test/psbt.js b/test/psbt.js index 99bd9c1..1804e67 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -143,8 +143,8 @@ describe(`Psbt`, () => { assert.strictEqual(transaction1, f.transaction) const psbt3 = Psbt.fromBase64(f.psbt) - delete psbt3.inputs[0].finalScriptSig - delete psbt3.inputs[0].finalScriptWitness + delete psbt3.data.inputs[0].finalScriptSig + delete psbt3.data.inputs[0].finalScriptWitness assert.throws(() => { psbt3.extractTransaction() }, new RegExp('Not finalized')) @@ -414,7 +414,7 @@ describe(`Psbt`, () => { assert.strictEqual(clone.toBase64(), psbt.toBase64()) assert.strictEqual(clone.toBase64(), notAClone.toBase64()) assert.strictEqual(psbt.toBase64(), notAClone.toBase64()) - psbt.globalMap.unsignedTx[3] = 0xff + psbt.data.globalMap.unsignedTx[3] = 0xff assert.notStrictEqual(clone.toBase64(), psbt.toBase64()) assert.notStrictEqual(clone.toBase64(), notAClone.toBase64()) assert.strictEqual(psbt.toBase64(), notAClone.toBase64()) @@ -542,14 +542,14 @@ describe(`Psbt`, () => { // Cache is populated psbt.addNonWitnessUtxoToInput(index, f.nonWitnessUtxo) - const value = psbt.inputs[index].nonWitnessUtxo + const value = psbt.data.inputs[index].nonWitnessUtxo assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(value)) assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(f.nonWitnessUtxo)) // Cache is rebuilt from internal transaction object when cleared - psbt.inputs[index].nonWitnessUtxo = Buffer.from([1,2,3]) + psbt.data.inputs[index].nonWitnessUtxo = Buffer.from([1,2,3]) psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index] = undefined - assert.ok(psbt.inputs[index].nonWitnessUtxo.equals(value)) + assert.ok(psbt.data.inputs[index].nonWitnessUtxo.equals(value)) }) }) }) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 928ca04..5a73031 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,11 +1,21 @@ import { Psbt as PsbtBase } from 'bip174'; import * as varuint from 'bip174/src/lib/converter/varint'; import { + Bip32Derivation, + FinalScriptSig, + FinalScriptWitness, + GlobalXpub, + KeyValue, NonWitnessUtxo, PartialSig, + PorCommitment, PsbtInput, + RedeemScript, + SighashType, TransactionInput, TransactionOutput, + WitnessScript, + WitnessUtxo, } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; import { toOutputScript } from './address'; @@ -26,37 +36,42 @@ const DEFAULT_OPTS: PsbtOpts = { maximumFeeRate: 5000, // satoshi per byte }; -export class Psbt extends PsbtBase { - static fromTransaction( - this: T, - txBuf: Buffer, - ): InstanceType { +export class Psbt { + static fromTransaction(txBuf: Buffer, opts: PsbtOptsOptional = {}): Psbt { const tx = Transaction.fromBuffer(txBuf); checkTxEmpty(tx); - const psbt = new this() as Psbt; + const psbtBase = new PsbtBase(); + const psbt = new Psbt(opts, psbtBase); psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); let inputCount = tx.ins.length; let outputCount = tx.outs.length; while (inputCount > 0) { - psbt.inputs.push({ - keyVals: [], + psbtBase.inputs.push({ + unknownKeyVals: [], }); inputCount--; } while (outputCount > 0) { - psbt.outputs.push({ - keyVals: [], + psbtBase.outputs.push({ + unknownKeyVals: [], }); outputCount--; } - return psbt as InstanceType; + return psbt; } - static fromBuffer( - this: T, - buffer: Buffer, - ): InstanceType { + static fromBase64(data: string, opts: PsbtOptsOptional = {}): Psbt { + const buffer = Buffer.from(data, 'base64'); + return this.fromBuffer(buffer, opts); + } + + static fromHex(data: string, opts: PsbtOptsOptional = {}): Psbt { + const buffer = Buffer.from(data, 'hex'); + return this.fromBuffer(buffer, opts); + } + + static fromBuffer(buffer: Buffer, opts: PsbtOptsOptional = {}): Psbt { let tx: Transaction | undefined; const txCountGetter = ( txBuf: Buffer, @@ -71,10 +86,11 @@ export class Psbt extends PsbtBase { outputCount: tx.outs.length, }; }; - const psbt = super.fromBuffer(buffer, txCountGetter) as Psbt; + const psbtBase = PsbtBase.fromBuffer(buffer, txCountGetter); + const psbt = new Psbt(opts, psbtBase); psbt.__CACHE.__TX = tx!; checkTxForDupeIns(tx!, psbt.__CACHE); - return psbt as InstanceType; + return psbt; } private __CACHE: PsbtCache = { @@ -85,17 +101,19 @@ export class Psbt extends PsbtBase { }; private opts: PsbtOpts; - constructor(opts: PsbtOptsOptional = {}) { - super(); + constructor( + opts: PsbtOptsOptional = {}, + readonly data: PsbtBase = new PsbtBase(), + ) { // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); const c = this.__CACHE; - c.__TX = Transaction.fromBuffer(this.globalMap.unsignedTx!); - this.setVersion(2); + c.__TX = Transaction.fromBuffer(data.globalMap.unsignedTx!); + if (this.data.inputs.length === 0) this.setVersion(2); // set cache - delete this.globalMap.unsignedTx; - Object.defineProperty(this.globalMap, 'unsignedTx', { + delete data.globalMap.unsignedTx; + Object.defineProperty(data.globalMap, 'unsignedTx', { enumerable: true, get(): Buffer { const buf = c.__TX_BUF_CACHE; @@ -106,8 +124,8 @@ export class Psbt extends PsbtBase { return c.__TX_BUF_CACHE; } }, - set(data: Buffer): void { - c.__TX_BUF_CACHE = data; + set(_data: Buffer): void { + c.__TX_BUF_CACHE = _data; }, }); @@ -127,12 +145,17 @@ export class Psbt extends PsbtBase { } get inputCount(): number { - return this.inputs.length; + return this.data.inputs.length; + } + + combine(...those: Psbt[]): this { + this.data.combine(...those.map(o => o.data)); + return this; } clone(): Psbt { // TODO: more efficient cloning - const res = Psbt.fromBuffer(this.toBuffer()); + const res = Psbt.fromBuffer(this.data.toBuffer()); res.opts = JSON.parse(JSON.stringify(this.opts)); return res; } @@ -144,7 +167,7 @@ export class Psbt extends PsbtBase { setVersion(version: number): this { check32Bit(version); - checkInputsForPartialSig(this.inputs, 'setVersion'); + checkInputsForPartialSig(this.data.inputs, 'setVersion'); const c = this.__CACHE; c.__TX.version = version; c.__TX_BUF_CACHE = undefined; @@ -154,7 +177,7 @@ export class Psbt extends PsbtBase { setLocktime(locktime: number): this { check32Bit(locktime); - checkInputsForPartialSig(this.inputs, 'setLocktime'); + checkInputsForPartialSig(this.data.inputs, 'setLocktime'); const c = this.__CACHE; c.__TX.locktime = locktime; c.__TX_BUF_CACHE = undefined; @@ -164,7 +187,7 @@ export class Psbt extends PsbtBase { setSequence(inputIndex: number, sequence: number): this { check32Bit(sequence); - checkInputsForPartialSig(this.inputs, 'setSequence'); + checkInputsForPartialSig(this.data.inputs, 'setSequence'); const c = this.__CACHE; if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); @@ -181,10 +204,16 @@ export class Psbt extends PsbtBase { } addInput(inputData: TransactionInput): this { - checkInputsForPartialSig(this.inputs, 'addInput'); + checkInputsForPartialSig(this.data.inputs, 'addInput'); const c = this.__CACHE; const inputAdder = getInputAdder(c); - super.addInput(inputData, inputAdder); + this.data.addInput(inputData, inputAdder); + + const inputIndex = this.data.inputs.length - 1; + const input = this.data.inputs[inputIndex]; + if (input.nonWitnessUtxo) { + addNonWitnessTxCache(this.__CACHE, input, inputIndex); + } c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; @@ -196,7 +225,7 @@ export class Psbt extends PsbtBase { } addOutput(outputData: TransactionOutput): this { - checkInputsForPartialSig(this.inputs, 'addOutput'); + checkInputsForPartialSig(this.data.inputs, 'addOutput'); const { address } = outputData as any; if (typeof address === 'string') { const { network } = this.opts; @@ -205,36 +234,26 @@ export class Psbt extends PsbtBase { } const c = this.__CACHE; const outputAdder = getOutputAdder(c); - super.addOutput(outputData, true, outputAdder); + this.data.addOutput(outputData, outputAdder, true); c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; } - addNonWitnessUtxoToInput( - inputIndex: number, - nonWitnessUtxo: NonWitnessUtxo, - ): this { - super.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); - const input = this.inputs[inputIndex]; - addNonWitnessTxCache(this.__CACHE, input, inputIndex); - return this; - } - extractTransaction(disableFeeCheck?: boolean): Transaction { - if (!this.inputs.every(isFinalized)) throw new Error('Not finalized'); + if (!this.data.inputs.every(isFinalized)) throw new Error('Not finalized'); const c = this.__CACHE; if (!disableFeeCheck) { checkFees(this, c, this.opts); } if (c.__EXTRACTED_TX) return c.__EXTRACTED_TX; const tx = c.__TX.clone(); - inputFinalizeGetAmts(this.inputs, tx, c, true); + inputFinalizeGetAmts(this.data.inputs, tx, c, true); return tx; } getFeeRate(): number { - if (!this.inputs.every(isFinalized)) + if (!this.data.inputs.every(isFinalized)) throw new Error('PSBT must be finalized to calculate fee rate'); const c = this.__CACHE; if (c.__FEE_RATE) return c.__FEE_RATE; @@ -246,18 +265,18 @@ export class Psbt extends PsbtBase { } else { tx = c.__TX.clone(); } - inputFinalizeGetAmts(this.inputs, tx, c, mustFinalize); + inputFinalizeGetAmts(this.data.inputs, tx, c, mustFinalize); return c.__FEE_RATE!; } finalizeAllInputs(): this { - checkForInput(this.inputs, 0); // making sure we have at least one - range(this.inputs.length).forEach(idx => this.finalizeInput(idx)); + checkForInput(this.data.inputs, 0); // making sure we have at least one + range(this.data.inputs.length).forEach(idx => this.finalizeInput(idx)); return this; } finalizeInput(inputIndex: number): this { - const input = checkForInput(this.inputs, inputIndex); + const input = checkForInput(this.data.inputs, inputIndex); const { script, isP2SH, isP2WSH, isSegwit } = getScriptFromInput( inputIndex, input, @@ -281,26 +300,26 @@ export class Psbt extends PsbtBase { ); if (finalScriptSig) - this.addFinalScriptSigToInput(inputIndex, finalScriptSig); + this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); if (finalScriptWitness) - this.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); - this.clearFinalizedInput(inputIndex); + this.data.clearFinalizedInput(inputIndex); return this; } validateAllSignatures(): boolean { - checkForInput(this.inputs, 0); // making sure we have at least one - const results = range(this.inputs.length).map(idx => + checkForInput(this.data.inputs, 0); // making sure we have at least one + const results = range(this.data.inputs.length).map(idx => this.validateSignatures(idx), ); return results.reduce((final, res) => res === true && final, true); } validateSignatures(inputIndex: number, pubkey?: Buffer): boolean { - const input = this.inputs[inputIndex]; + const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; if (!input || !partialSig || partialSig.length < 1) throw new Error('No signatures to validate'); @@ -343,7 +362,7 @@ export class Psbt extends PsbtBase { // as input information is added, then eventually // optimize this method. const results: boolean[] = []; - for (const i of range(this.inputs.length)) { + for (const i of range(this.data.inputs.length)) { try { this.signInput(i, keyPair, sighashTypes); results.push(true); @@ -371,7 +390,7 @@ export class Psbt extends PsbtBase { // optimize this method. const results: boolean[] = []; const promises: Array> = []; - for (const [i] of this.inputs.entries()) { + for (const [i] of this.data.inputs.entries()) { promises.push( this.signInputAsync(i, keyPair, sighashTypes).then( () => { @@ -401,7 +420,7 @@ export class Psbt extends PsbtBase { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); const { hash, sighashType } = getHashAndSighashType( - this.inputs, + this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, @@ -413,7 +432,8 @@ export class Psbt extends PsbtBase { signature: bscript.signature.encode(keyPair.sign(hash), sighashType), }; - return this.addPartialSigToInput(inputIndex, partialSig); + this.data.addPartialSigToInput(inputIndex, partialSig); + return this; } signInputAsync( @@ -426,7 +446,7 @@ export class Psbt extends PsbtBase { if (!keyPair || !keyPair.publicKey) return reject(new Error('Need Signer to sign input')); const { hash, sighashType } = getHashAndSighashType( - this.inputs, + this.data.inputs, inputIndex, keyPair.publicKey, this.__CACHE, @@ -439,12 +459,143 @@ export class Psbt extends PsbtBase { signature: bscript.signature.encode(signature, sighashType), }; - this.addPartialSigToInput(inputIndex, partialSig); + this.data.addPartialSigToInput(inputIndex, partialSig); resolve(); }); }, ); } + + toBuffer(): Buffer { + return this.data.toBuffer(); + } + + toHex(): string { + return this.data.toHex(); + } + + toBase64(): string { + return this.data.toBase64(); + } + + addGlobalXpubToGlobal(globalXpub: GlobalXpub): this { + this.data.addGlobalXpubToGlobal(globalXpub); + return this; + } + + addNonWitnessUtxoToInput( + inputIndex: number, + nonWitnessUtxo: NonWitnessUtxo, + ): this { + this.data.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); + const input = this.data.inputs[inputIndex]; + addNonWitnessTxCache(this.__CACHE, input, inputIndex); + return this; + } + + addWitnessUtxoToInput(inputIndex: number, witnessUtxo: WitnessUtxo): this { + this.data.addWitnessUtxoToInput(inputIndex, witnessUtxo); + return this; + } + + addPartialSigToInput(inputIndex: number, partialSig: PartialSig): this { + this.data.addPartialSigToInput(inputIndex, partialSig); + return this; + } + + addSighashTypeToInput(inputIndex: number, sighashType: SighashType): this { + this.data.addSighashTypeToInput(inputIndex, sighashType); + return this; + } + + addRedeemScriptToInput(inputIndex: number, redeemScript: RedeemScript): this { + this.data.addRedeemScriptToInput(inputIndex, redeemScript); + return this; + } + + addWitnessScriptToInput( + inputIndex: number, + witnessScript: WitnessScript, + ): this { + this.data.addWitnessScriptToInput(inputIndex, witnessScript); + return this; + } + + addBip32DerivationToInput( + inputIndex: number, + bip32Derivation: Bip32Derivation, + ): this { + this.data.addBip32DerivationToInput(inputIndex, bip32Derivation); + return this; + } + + addFinalScriptSigToInput( + inputIndex: number, + finalScriptSig: FinalScriptSig, + ): this { + this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); + return this; + } + + addFinalScriptWitnessToInput( + inputIndex: number, + finalScriptWitness: FinalScriptWitness, + ): this { + this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + return this; + } + + addPorCommitmentToInput( + inputIndex: number, + porCommitment: PorCommitment, + ): this { + this.data.addPorCommitmentToInput(inputIndex, porCommitment); + return this; + } + + addRedeemScriptToOutput( + outputIndex: number, + redeemScript: RedeemScript, + ): this { + this.data.addRedeemScriptToOutput(outputIndex, redeemScript); + return this; + } + + addWitnessScriptToOutput( + outputIndex: number, + witnessScript: WitnessScript, + ): this { + this.data.addWitnessScriptToOutput(outputIndex, witnessScript); + return this; + } + + addBip32DerivationToOutput( + outputIndex: number, + bip32Derivation: Bip32Derivation, + ): this { + this.data.addBip32DerivationToOutput(outputIndex, bip32Derivation); + return this; + } + + addUnknownKeyValToGlobal(keyVal: KeyValue): this { + this.data.addUnknownKeyValToGlobal(keyVal); + return this; + } + + addUnknownKeyValToInput(inputIndex: number, keyVal: KeyValue): this { + this.data.addUnknownKeyValToInput(inputIndex, keyVal); + return this; + } + + addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this { + this.data.addUnknownKeyValToOutput(outputIndex, keyVal); + return this; + } + + clearFinalizedInput(inputIndex: number): this { + this.data.clearFinalizedInput(inputIndex); + return this; + } } interface PsbtCache { diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 95f6624..533b8fd 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,16 +1,20 @@ /// import { Psbt as PsbtBase } from 'bip174'; -import { NonWitnessUtxo, TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; +import { Bip32Derivation, FinalScriptSig, FinalScriptWitness, GlobalXpub, KeyValue, NonWitnessUtxo, PartialSig, PorCommitment, RedeemScript, SighashType, TransactionInput, TransactionOutput, WitnessScript, WitnessUtxo } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; -export declare class Psbt extends PsbtBase { - static fromTransaction(this: T, txBuf: Buffer): InstanceType; - static fromBuffer(this: T, buffer: Buffer): InstanceType; +export declare class Psbt { + readonly data: PsbtBase; + static fromTransaction(txBuf: Buffer, opts?: PsbtOptsOptional): Psbt; + static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt; + static fromHex(data: string, opts?: PsbtOptsOptional): Psbt; + static fromBuffer(buffer: Buffer, opts?: PsbtOptsOptional): Psbt; private __CACHE; private opts; - constructor(opts?: PsbtOptsOptional); + constructor(opts?: PsbtOptsOptional, data?: PsbtBase); readonly inputCount: number; + combine(...those: Psbt[]): this; clone(): Psbt; setMaximumFeeRate(satoshiPerByte: number): void; setVersion(version: number): this; @@ -20,7 +24,6 @@ export declare class Psbt extends PsbtBase { addInput(inputData: TransactionInput): this; addOutputs(outputDatas: TransactionOutput[]): this; addOutput(outputData: TransactionOutput): this; - addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; extractTransaction(disableFeeCheck?: boolean): Transaction; getFeeRate(): number; finalizeAllInputs(): this; @@ -31,6 +34,27 @@ export declare class Psbt extends PsbtBase { signAsync(keyPair: SignerAsync, sighashTypes?: number[]): Promise; signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this; signInputAsync(inputIndex: number, keyPair: SignerAsync, sighashTypes?: number[]): Promise; + toBuffer(): Buffer; + toHex(): string; + toBase64(): string; + addGlobalXpubToGlobal(globalXpub: GlobalXpub): this; + addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; + addWitnessUtxoToInput(inputIndex: number, witnessUtxo: WitnessUtxo): this; + addPartialSigToInput(inputIndex: number, partialSig: PartialSig): this; + addSighashTypeToInput(inputIndex: number, sighashType: SighashType): this; + addRedeemScriptToInput(inputIndex: number, redeemScript: RedeemScript): this; + addWitnessScriptToInput(inputIndex: number, witnessScript: WitnessScript): this; + addBip32DerivationToInput(inputIndex: number, bip32Derivation: Bip32Derivation): this; + addFinalScriptSigToInput(inputIndex: number, finalScriptSig: FinalScriptSig): this; + addFinalScriptWitnessToInput(inputIndex: number, finalScriptWitness: FinalScriptWitness): this; + addPorCommitmentToInput(inputIndex: number, porCommitment: PorCommitment): this; + addRedeemScriptToOutput(outputIndex: number, redeemScript: RedeemScript): this; + addWitnessScriptToOutput(outputIndex: number, witnessScript: WitnessScript): this; + addBip32DerivationToOutput(outputIndex: number, bip32Derivation: Bip32Derivation): this; + addUnknownKeyValToGlobal(keyVal: KeyValue): this; + addUnknownKeyValToInput(inputIndex: number, keyVal: KeyValue): this; + addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this; + clearFinalizedInput(inputIndex: number): this; } interface PsbtOptsOptional { network?: Network; From f25938d3ca8abf8fcbac2074f81b629661bd1e03 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 11 Jul 2019 16:24:35 +0700 Subject: [PATCH 100/111] Test signing a non-whitelisted sighashtype --- test/fixtures/psbt.json | 9 +++ test/psbt.js | 160 +++++++++++++++++++++++----------------- 2 files changed, 101 insertions(+), 68 deletions(-) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index fbad821..1a37031 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -427,6 +427,15 @@ "inputToCheck": 0, "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } + }, + { + "description": "allows signing non-whitelisted sighashtype when explicitly passed in", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAAYaq+PdOUY2PnV9kZKa82XlqrPByOqwH2TRg2+LQdqs2AAAAAAD/////AAAAAAAAAQEgAOH1BQAAAAAXqRSTNeWHqa9INvSnQ120SZeJc+6JSocBAwSBAAAAAQQWABQvLKRyDqYsPYImhD3eURpDGL10RwAA", + "sighashTypes": [129], + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + } } ] }, diff --git a/test/psbt.js b/test/psbt.js index 1804e67..8ed7b51 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -167,26 +167,32 @@ describe(`Psbt`, () => { describe('signInputAsync', () => { fixtures.signInput.checks.forEach(f => { it(f.description, async () => { - const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) - assert.doesNotReject(async () => { - await psbtThatShouldsign.signInputAsync( - f.shouldSign.inputToCheck, - ECPair.fromWIF(f.shouldSign.WIF), - ) - }) + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotReject(async () => { + await psbtThatShouldsign.signInputAsync( + f.shouldSign.inputToCheck, + ECPair.fromWIF(f.shouldSign.WIF), + f.shouldSign.sighashTypes || undefined, + ) + }) + } - const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) - assert.rejects(async () => { - await psbtThatShouldThrow.signInputAsync( - f.shouldThrow.inputToCheck, - ECPair.fromWIF(f.shouldThrow.WIF), - ) - }, new RegExp(f.shouldThrow.errorMessage)) - assert.rejects(async () => { - await psbtThatShouldThrow.signInputAsync( - f.shouldThrow.inputToCheck, - ) - }, new RegExp('Need Signer to sign input')) + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.rejects(async () => { + await psbtThatShouldThrow.signInputAsync( + f.shouldThrow.inputToCheck, + ECPair.fromWIF(f.shouldThrow.WIF), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp(f.shouldThrow.errorMessage)) + assert.rejects(async () => { + await psbtThatShouldThrow.signInputAsync( + f.shouldThrow.inputToCheck, + ) + }, new RegExp('Need Signer to sign input')) + } }) }) }) @@ -194,26 +200,32 @@ describe(`Psbt`, () => { describe('signInput', () => { fixtures.signInput.checks.forEach(f => { it(f.description, () => { - const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) - assert.doesNotThrow(() => { - psbtThatShouldsign.signInput( - f.shouldSign.inputToCheck, - ECPair.fromWIF(f.shouldSign.WIF), - ) - }) + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotThrow(() => { + psbtThatShouldsign.signInput( + f.shouldSign.inputToCheck, + ECPair.fromWIF(f.shouldSign.WIF), + f.shouldSign.sighashTypes || undefined, + ) + }) + } - const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) - assert.throws(() => { - psbtThatShouldThrow.signInput( - f.shouldThrow.inputToCheck, - ECPair.fromWIF(f.shouldThrow.WIF), - ) - }, new RegExp(f.shouldThrow.errorMessage)) - assert.throws(() => { - psbtThatShouldThrow.signInput( - f.shouldThrow.inputToCheck, - ) - }, new RegExp('Need Signer to sign input')) + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.throws(() => { + psbtThatShouldThrow.signInput( + f.shouldThrow.inputToCheck, + ECPair.fromWIF(f.shouldThrow.WIF), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp(f.shouldThrow.errorMessage)) + assert.throws(() => { + psbtThatShouldThrow.signInput( + f.shouldThrow.inputToCheck, + ) + }, new RegExp('Need Signer to sign input')) + } }) }) }) @@ -222,22 +234,28 @@ describe(`Psbt`, () => { fixtures.signInput.checks.forEach(f => { if (f.description === 'checks the input exists') return it(f.description, async () => { - const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) - assert.doesNotReject(async () => { - await psbtThatShouldsign.signAsync( - ECPair.fromWIF(f.shouldSign.WIF), - ) - }) + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotReject(async () => { + await psbtThatShouldsign.signAsync( + ECPair.fromWIF(f.shouldSign.WIF), + f.shouldSign.sighashTypes || undefined, + ) + }) + } - const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) - assert.rejects(async () => { - await psbtThatShouldThrow.signAsync( - ECPair.fromWIF(f.shouldThrow.WIF), - ) - }, new RegExp('No inputs were signed')) - assert.rejects(async () => { - await psbtThatShouldThrow.signAsync() - }, new RegExp('Need Signer to sign input')) + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.rejects(async () => { + await psbtThatShouldThrow.signAsync( + ECPair.fromWIF(f.shouldThrow.WIF), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp('No inputs were signed')) + assert.rejects(async () => { + await psbtThatShouldThrow.signAsync() + }, new RegExp('Need Signer to sign input')) + } }) }) }) @@ -246,22 +264,28 @@ describe(`Psbt`, () => { fixtures.signInput.checks.forEach(f => { if (f.description === 'checks the input exists') return it(f.description, () => { - const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) - assert.doesNotThrow(() => { - psbtThatShouldsign.sign( - ECPair.fromWIF(f.shouldSign.WIF), - ) - }) + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotThrow(() => { + psbtThatShouldsign.sign( + ECPair.fromWIF(f.shouldSign.WIF), + f.shouldSign.sighashTypes || undefined, + ) + }) + } - const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) - assert.throws(() => { - psbtThatShouldThrow.sign( - ECPair.fromWIF(f.shouldThrow.WIF), - ) - }, new RegExp('No inputs were signed')) - assert.throws(() => { - psbtThatShouldThrow.sign() - }, new RegExp('Need Signer to sign input')) + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.throws(() => { + psbtThatShouldThrow.sign( + ECPair.fromWIF(f.shouldThrow.WIF), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp('No inputs were signed')) + assert.throws(() => { + psbtThatShouldThrow.sign() + }, new RegExp('Need Signer to sign input')) + } }) }) }) From d790288048eb73082fdfddb5ab4bff19e2b17987 Mon Sep 17 00:00:00 2001 From: Luke Childs Date: Thu, 11 Jul 2019 16:25:30 +0700 Subject: [PATCH 101/111] Test the sighashtype is checked when signing --- test/fixtures/psbt.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 1a37031..a29bc8b 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -428,6 +428,15 @@ "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" } }, + { + "description": "checks the sighash type", + "shouldThrow": { + "errorMessage": "Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: SIGHASH_ANYONECANPAY | SIGHASH_ALL", + "psbt": "cHNidP8BADMBAAAAAYaq+PdOUY2PnV9kZKa82XlqrPByOqwH2TRg2+LQdqs2AAAAAAD/////AAAAAAAAAQEgAOH1BQAAAAAXqRSTNeWHqa9INvSnQ120SZeJc+6JSocBAwSBAAAAAQQWABQvLKRyDqYsPYImhD3eURpDGL10RwAA", + "inputToCheck": 0, + "WIF": "KxnAnQh6UJBxLF8Weup77yn8tWhLHhDhnXeyJuzmmcZA5aRdMJni" + } + }, { "description": "allows signing non-whitelisted sighashtype when explicitly passed in", "shouldSign": { From 71ddd656a3bc6a0416d9f4e486143aefc4110d77 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 18 Jul 2019 11:43:24 +0900 Subject: [PATCH 102/111] Modify for new BIP174 interface system --- package-lock.json | 5 +- package.json | 2 +- src/psbt.js | 241 ++++++++++++++-------------------- test/psbt.js | 28 ++-- ts_src/psbt.ts | 324 +++++++++++++++++----------------------------- types/psbt.d.ts | 20 +-- 6 files changed, 231 insertions(+), 389 deletions(-) diff --git a/package-lock.json b/package-lock.json index fabd361..3a7f8be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,9 +200,8 @@ } }, "bip174": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/bip174/-/bip174-0.0.15.tgz", - "integrity": "sha512-mK/s9p7i+PG7W2s2cAedNVk1NDZQn9wAoq1DlsS2+1zz5TXR3TRTzqRqm9BQtOXwbkxkhfLwlmsOjuiRdAYkdg==" + "version": "git+https://github.com/bitcoinjs/bip174.git#5137e367c7a3a4e281ee01574f88977cdd4be896", + "from": "git+https://github.com/bitcoinjs/bip174.git#interface" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index a32021a..0b91e4e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "0.0.15", + "bip174": "git+https://github.com/bitcoinjs/bip174.git#interface", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", diff --git a/src/psbt.js b/src/psbt.js index 578e17c..97e9e9d 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -16,7 +16,7 @@ const DEFAULT_OPTS = { maximumFeeRate: 5000, }; class Psbt { - constructor(opts = {}, data = new bip174_1.Psbt()) { + constructor(opts = {}, data = new bip174_1.Psbt(new PsbtTransaction())) { this.data = data; this.__CACHE = { __NON_WITNESS_UTXO_TX_CACHE: [], @@ -27,11 +27,11 @@ class Psbt { // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); const c = this.__CACHE; - c.__TX = transaction_1.Transaction.fromBuffer(data.globalMap.unsignedTx); + c.__TX = this.data.globalMap.unsignedTx.tx; if (this.data.inputs.length === 0) this.setVersion(2); // set cache - delete data.globalMap.unsignedTx; - Object.defineProperty(data.globalMap, 'unsignedTx', { + this.unsignedTx = Buffer.from([]); + Object.defineProperty(this, 'unsignedTx', { enumerable: true, get() { const buf = c.__TX_BUF_CACHE; @@ -56,24 +56,20 @@ class Psbt { dpew(this, 'opts', false, true); } static fromTransaction(txBuf, opts = {}) { - const tx = transaction_1.Transaction.fromBuffer(txBuf); - checkTxEmpty(tx); - const psbtBase = new bip174_1.Psbt(); + const tx = new PsbtTransaction(txBuf); + checkTxEmpty(tx.tx); + const psbtBase = new bip174_1.Psbt(tx); const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx; - checkTxForDupeIns(tx, psbt.__CACHE); - let inputCount = tx.ins.length; - let outputCount = tx.outs.length; + psbt.__CACHE.__TX = tx.tx; + checkTxForDupeIns(tx.tx, psbt.__CACHE); + let inputCount = tx.tx.ins.length; + let outputCount = tx.tx.outs.length; while (inputCount > 0) { - psbtBase.inputs.push({ - unknownKeyVals: [], - }); + psbtBase.inputs.push({}); inputCount--; } while (outputCount > 0) { - psbtBase.outputs.push({ - unknownKeyVals: [], - }); + psbtBase.outputs.push({}); outputCount--; } return psbt; @@ -87,16 +83,8 @@ class Psbt { return this.fromBuffer(buffer, opts); } static fromBuffer(buffer, opts = {}) { - let tx; - const txCountGetter = txBuf => { - tx = transaction_1.Transaction.fromBuffer(txBuf); - checkTxEmpty(tx); - return { - inputCount: tx.ins.length, - outputCount: tx.outs.length, - }; - }; - const psbtBase = bip174_1.Psbt.fromBuffer(buffer, txCountGetter); + const psbtBase = bip174_1.Psbt.fromBuffer(buffer, transactionFromBuffer); + const tx = psbtBase.globalMap.unsignedTx.tx; const psbt = new Psbt(opts, psbtBase); psbt.__CACHE.__TX = tx; checkTxForDupeIns(tx, psbt.__CACHE); @@ -156,8 +144,9 @@ class Psbt { addInput(inputData) { checkInputsForPartialSig(this.data.inputs, 'addInput'); const c = this.__CACHE; - const inputAdder = getInputAdder(c); - this.data.addInput(inputData, inputAdder); + this.data.addInput(inputData); + const txIn = c.__TX.ins[c.__TX.ins.length - 1]; + checkTxInputCache(c, txIn); const inputIndex = this.data.inputs.length - 1; const input = this.data.inputs[inputIndex]; if (input.nonWitnessUtxo) { @@ -180,8 +169,7 @@ class Psbt { outputData = Object.assign(outputData, { script }); } const c = this.__CACHE; - const outputAdder = getOutputAdder(c); - this.data.addOutput(outputData, outputAdder, true); + this.data.addOutput(outputData); c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; @@ -238,10 +226,9 @@ class Psbt { isP2SH, isP2WSH, ); - if (finalScriptSig) - this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); + if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); if (finalScriptWitness) - this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + this.data.updateInput(inputIndex, { finalScriptWitness }); if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); this.data.clearFinalizedInput(inputIndex); @@ -349,11 +336,13 @@ class Psbt { this.__CACHE, sighashTypes, ); - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }; - this.data.addPartialSigToInput(inputIndex, partialSig); + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }, + ]; + this.data.updateInput(inputIndex, { partialSig }); return this; } signInputAsync( @@ -372,11 +361,13 @@ class Psbt { sighashTypes, ); Promise.resolve(keyPair.sign(hash)).then(signature => { - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(signature, sighashType), - }; - this.data.addPartialSigToInput(inputIndex, partialSig); + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }, + ]; + this.data.updateInput(inputIndex, { partialSig }); resolve(); }); }); @@ -390,62 +381,23 @@ class Psbt { toBase64() { return this.data.toBase64(); } - addGlobalXpubToGlobal(globalXpub) { - this.data.addGlobalXpubToGlobal(globalXpub); + updateGlobal(updateData) { + this.data.updateGlobal(updateData); return this; } - addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo) { - this.data.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); - const input = this.data.inputs[inputIndex]; - addNonWitnessTxCache(this.__CACHE, input, inputIndex); + updateInput(inputIndex, updateData) { + this.data.updateInput(inputIndex, updateData); + if (updateData.nonWitnessUtxo) { + addNonWitnessTxCache( + this.__CACHE, + this.data.inputs[inputIndex], + inputIndex, + ); + } return this; } - addWitnessUtxoToInput(inputIndex, witnessUtxo) { - this.data.addWitnessUtxoToInput(inputIndex, witnessUtxo); - return this; - } - addPartialSigToInput(inputIndex, partialSig) { - this.data.addPartialSigToInput(inputIndex, partialSig); - return this; - } - addSighashTypeToInput(inputIndex, sighashType) { - this.data.addSighashTypeToInput(inputIndex, sighashType); - return this; - } - addRedeemScriptToInput(inputIndex, redeemScript) { - this.data.addRedeemScriptToInput(inputIndex, redeemScript); - return this; - } - addWitnessScriptToInput(inputIndex, witnessScript) { - this.data.addWitnessScriptToInput(inputIndex, witnessScript); - return this; - } - addBip32DerivationToInput(inputIndex, bip32Derivation) { - this.data.addBip32DerivationToInput(inputIndex, bip32Derivation); - return this; - } - addFinalScriptSigToInput(inputIndex, finalScriptSig) { - this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); - return this; - } - addFinalScriptWitnessToInput(inputIndex, finalScriptWitness) { - this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); - return this; - } - addPorCommitmentToInput(inputIndex, porCommitment) { - this.data.addPorCommitmentToInput(inputIndex, porCommitment); - return this; - } - addRedeemScriptToOutput(outputIndex, redeemScript) { - this.data.addRedeemScriptToOutput(outputIndex, redeemScript); - return this; - } - addWitnessScriptToOutput(outputIndex, witnessScript) { - this.data.addWitnessScriptToOutput(outputIndex, witnessScript); - return this; - } - addBip32DerivationToOutput(outputIndex, bip32Derivation) { - this.data.addBip32DerivationToOutput(outputIndex, bip32Derivation); + updateOutput(outputIndex, updateData) { + this.data.updateOutput(outputIndex, updateData); return this; } addUnknownKeyValToGlobal(keyVal) { @@ -466,6 +418,54 @@ class Psbt { } } exports.Psbt = Psbt; +const transactionFromBuffer = buffer => new PsbtTransaction(buffer); +class PsbtTransaction { + constructor(buffer = Buffer.from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0])) { + this.tx = transaction_1.Transaction.fromBuffer(buffer); + if (this.tx.ins.some(input => input.script.length !== 0)) { + throw new Error('Format Error: Transaction ScriptSigs are not empty'); + } + Object.defineProperty(this, 'tx', { + enumerable: false, + writable: true, + }); + } + getInputOutputCounts() { + return { + inputCount: this.tx.ins.length, + outputCount: this.tx.outs.length, + }; + } + addInput(input) { + if ( + input.hash === undefined || + input.index === undefined || + (!Buffer.isBuffer(input.hash) && typeof input.hash !== 'string') || + typeof input.index !== 'number' + ) { + throw new Error('Error adding input.'); + } + const hash = + typeof input.hash === 'string' + ? bufferutils_1.reverseBuffer(Buffer.from(input.hash, 'hex')) + : input.hash; + this.tx.addInput(hash, input.index, input.sequence); + } + addOutput(output) { + if ( + output.script === undefined || + output.value === undefined || + !Buffer.isBuffer(output.script) || + typeof output.value !== 'number' + ) { + throw new Error('Error adding output.'); + } + this.tx.addOutput(output.script, output.value); + } + toBuffer() { + return this.tx.toBuffer(); + } +} function canFinalize(input, script, scriptType) { switch (scriptType) { case 'pubkey': @@ -769,55 +769,6 @@ function getHashForSig(inputIndex, input, cache, sighashTypes) { hash, }; } -function getInputAdder(cache) { - const selfCache = cache; - return (_inputData, txBuf) => { - if ( - !txBuf || - _inputData.hash === undefined || - _inputData.index === undefined || - (!Buffer.isBuffer(_inputData.hash) && - typeof _inputData.hash !== 'string') || - typeof _inputData.index !== 'number' - ) { - throw new Error('Error adding input.'); - } - const prevHash = Buffer.isBuffer(_inputData.hash) - ? _inputData.hash - : bufferutils_1.reverseBuffer(Buffer.from(_inputData.hash, 'hex')); - // Check if input already exists in cache. - const input = { hash: prevHash, index: _inputData.index }; - checkTxInputCache(selfCache, input); - selfCache.__TX.ins.push( - Object.assign({}, input, { - script: Buffer.alloc(0), - sequence: - _inputData.sequence || transaction_1.Transaction.DEFAULT_SEQUENCE, - witness: [], - }), - ); - return selfCache.__TX.toBuffer(); - }; -} -function getOutputAdder(cache) { - const selfCache = cache; - return (_outputData, txBuf) => { - if ( - !txBuf || - _outputData.script === undefined || - _outputData.value === undefined || - !Buffer.isBuffer(_outputData.script) || - typeof _outputData.value !== 'number' - ) { - throw new Error('Error adding output.'); - } - selfCache.__TX.outs.push({ - script: _outputData.script, - value: _outputData.value, - }); - return selfCache.__TX.toBuffer(); - }; -} function getPayment(script, scriptType, partialSig) { let payment; switch (scriptType) { diff --git a/test/psbt.js b/test/psbt.js index 8ed7b51..e52fae2 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -72,20 +72,8 @@ describe(`Psbt`, () => { const fixtureData = f[`${inputOrOutput}Data`] if (fixtureData) { for (const [i, data] of fixtureData.entries()) { - const attrs = Object.keys(data) - for (const attr of attrs) { - const upperAttr = upperCaseFirstLetter(attr) - let adder = psbt[`add${upperAttr}To${upperCaseFirstLetter(inputOrOutput)}`] - if (adder !== undefined) { - adder = adder.bind(psbt) - const arg = data[attr] - if (Array.isArray(arg)) { - arg.forEach(a => adder(i, a)) - } else { - adder(i, arg) - } - } - } + const txt = upperCaseFirstLetter(inputOrOutput) + psbt[`update${txt}`](i, data) } } } @@ -309,9 +297,11 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.finalizeAllInputs() }, new RegExp('No script found for input #0')) - psbt.addWitnessUtxoToInput(0, { - script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d', 'hex'), - value: 2e5 + psbt.updateInput(0, { + witnessUtxo: { + script: Buffer.from('0014d85c2b71d0060b09c9886aeb815e50991dda124d', 'hex'), + value: 2e5 + } }) assert.throws(() => { psbt.finalizeAllInputs() @@ -438,7 +428,7 @@ describe(`Psbt`, () => { assert.strictEqual(clone.toBase64(), psbt.toBase64()) assert.strictEqual(clone.toBase64(), notAClone.toBase64()) assert.strictEqual(psbt.toBase64(), notAClone.toBase64()) - psbt.data.globalMap.unsignedTx[3] = 0xff + psbt.__CACHE.__TX.version |= 0xff0000 assert.notStrictEqual(clone.toBase64(), psbt.toBase64()) assert.notStrictEqual(clone.toBase64(), notAClone.toBase64()) assert.strictEqual(psbt.toBase64(), notAClone.toBase64()) @@ -565,7 +555,7 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index], undefined) // Cache is populated - psbt.addNonWitnessUtxoToInput(index, f.nonWitnessUtxo) + psbt.updateInput(index, { nonWitnessUtxo: f.nonWitnessUtxo }) const value = psbt.data.inputs[index].nonWitnessUtxo assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(value)) assert.ok(psbt.__CACHE.__NON_WITNESS_UTXO_BUF_CACHE[index].equals(f.nonWitnessUtxo)) diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 5a73031..0c20bc2 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1,21 +1,16 @@ import { Psbt as PsbtBase } from 'bip174'; import * as varuint from 'bip174/src/lib/converter/varint'; import { - Bip32Derivation, - FinalScriptSig, - FinalScriptWitness, - GlobalXpub, KeyValue, - NonWitnessUtxo, PartialSig, - PorCommitment, + PsbtGlobalUpdate, PsbtInput, - RedeemScript, - SighashType, + PsbtInputUpdate, + PsbtOutputUpdate, + Transaction as ITransaction, + TransactionFromBuffer, TransactionInput, TransactionOutput, - WitnessScript, - WitnessUtxo, } from 'bip174/src/lib/interfaces'; import { checkForInput } from 'bip174/src/lib/utils'; import { toOutputScript } from './address'; @@ -38,24 +33,20 @@ const DEFAULT_OPTS: PsbtOpts = { export class Psbt { static fromTransaction(txBuf: Buffer, opts: PsbtOptsOptional = {}): Psbt { - const tx = Transaction.fromBuffer(txBuf); - checkTxEmpty(tx); - const psbtBase = new PsbtBase(); + const tx = new PsbtTransaction(txBuf); + checkTxEmpty(tx.tx); + const psbtBase = new PsbtBase(tx); const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx; - checkTxForDupeIns(tx, psbt.__CACHE); - let inputCount = tx.ins.length; - let outputCount = tx.outs.length; + psbt.__CACHE.__TX = tx.tx; + checkTxForDupeIns(tx.tx, psbt.__CACHE); + let inputCount = tx.tx.ins.length; + let outputCount = tx.tx.outs.length; while (inputCount > 0) { - psbtBase.inputs.push({ - unknownKeyVals: [], - }); + psbtBase.inputs.push({}); inputCount--; } while (outputCount > 0) { - psbtBase.outputs.push({ - unknownKeyVals: [], - }); + psbtBase.outputs.push({}); outputCount--; } return psbt; @@ -72,27 +63,16 @@ export class Psbt { } static fromBuffer(buffer: Buffer, opts: PsbtOptsOptional = {}): Psbt { - let tx: Transaction | undefined; - const txCountGetter = ( - txBuf: Buffer, - ): { - inputCount: number; - outputCount: number; - } => { - tx = Transaction.fromBuffer(txBuf); - checkTxEmpty(tx); - return { - inputCount: tx.ins.length, - outputCount: tx.outs.length, - }; - }; - const psbtBase = PsbtBase.fromBuffer(buffer, txCountGetter); + const psbtBase = PsbtBase.fromBuffer(buffer, transactionFromBuffer); + const tx: Transaction = (psbtBase.globalMap.unsignedTx as PsbtTransaction) + .tx; const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx!; - checkTxForDupeIns(tx!, psbt.__CACHE); + psbt.__CACHE.__TX = tx; + checkTxForDupeIns(tx, psbt.__CACHE); return psbt; } + unsignedTx: Buffer; private __CACHE: PsbtCache = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], @@ -103,17 +83,17 @@ export class Psbt { constructor( opts: PsbtOptsOptional = {}, - readonly data: PsbtBase = new PsbtBase(), + readonly data: PsbtBase = new PsbtBase(new PsbtTransaction()), ) { // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); const c = this.__CACHE; - c.__TX = Transaction.fromBuffer(data.globalMap.unsignedTx!); + c.__TX = (this.data.globalMap.unsignedTx as PsbtTransaction).tx; if (this.data.inputs.length === 0) this.setVersion(2); // set cache - delete data.globalMap.unsignedTx; - Object.defineProperty(data.globalMap, 'unsignedTx', { + this.unsignedTx = Buffer.from([]); + Object.defineProperty(this, 'unsignedTx', { enumerable: true, get(): Buffer { const buf = c.__TX_BUF_CACHE; @@ -206,8 +186,9 @@ export class Psbt { addInput(inputData: TransactionInput): this { checkInputsForPartialSig(this.data.inputs, 'addInput'); const c = this.__CACHE; - const inputAdder = getInputAdder(c); - this.data.addInput(inputData, inputAdder); + this.data.addInput(inputData); + const txIn = c.__TX.ins[c.__TX.ins.length - 1]; + checkTxInputCache(c, txIn); const inputIndex = this.data.inputs.length - 1; const input = this.data.inputs[inputIndex]; @@ -233,8 +214,7 @@ export class Psbt { outputData = Object.assign(outputData, { script }); } const c = this.__CACHE; - const outputAdder = getOutputAdder(c); - this.data.addOutput(outputData, outputAdder, true); + this.data.addOutput(outputData); c.__FEE_RATE = undefined; c.__EXTRACTED_TX = undefined; return this; @@ -299,10 +279,9 @@ export class Psbt { isP2WSH, ); - if (finalScriptSig) - this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); + if (finalScriptSig) this.data.updateInput(inputIndex, { finalScriptSig }); if (finalScriptWitness) - this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); + this.data.updateInput(inputIndex, { finalScriptWitness }); if (!finalScriptSig && !finalScriptWitness) throw new Error(`Unknown error finalizing input #${inputIndex}`); @@ -427,12 +406,14 @@ export class Psbt { sighashTypes, ); - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(keyPair.sign(hash), sighashType), - }; + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(keyPair.sign(hash), sighashType), + }, + ]; - this.data.addPartialSigToInput(inputIndex, partialSig); + this.data.updateInput(inputIndex, { partialSig }); return this; } @@ -454,12 +435,14 @@ export class Psbt { ); Promise.resolve(keyPair.sign(hash)).then(signature => { - const partialSig = { - pubkey: keyPair.publicKey, - signature: bscript.signature.encode(signature, sighashType), - }; + const partialSig = [ + { + pubkey: keyPair.publicKey, + signature: bscript.signature.encode(signature, sighashType), + }, + ]; - this.data.addPartialSigToInput(inputIndex, partialSig); + this.data.updateInput(inputIndex, { partialSig }); resolve(); }); }, @@ -478,102 +461,25 @@ export class Psbt { return this.data.toBase64(); } - addGlobalXpubToGlobal(globalXpub: GlobalXpub): this { - this.data.addGlobalXpubToGlobal(globalXpub); + updateGlobal(updateData: PsbtGlobalUpdate): this { + this.data.updateGlobal(updateData); return this; } - addNonWitnessUtxoToInput( - inputIndex: number, - nonWitnessUtxo: NonWitnessUtxo, - ): this { - this.data.addNonWitnessUtxoToInput(inputIndex, nonWitnessUtxo); - const input = this.data.inputs[inputIndex]; - addNonWitnessTxCache(this.__CACHE, input, inputIndex); + updateInput(inputIndex: number, updateData: PsbtInputUpdate): this { + this.data.updateInput(inputIndex, updateData); + if (updateData.nonWitnessUtxo) { + addNonWitnessTxCache( + this.__CACHE, + this.data.inputs[inputIndex], + inputIndex, + ); + } return this; } - addWitnessUtxoToInput(inputIndex: number, witnessUtxo: WitnessUtxo): this { - this.data.addWitnessUtxoToInput(inputIndex, witnessUtxo); - return this; - } - - addPartialSigToInput(inputIndex: number, partialSig: PartialSig): this { - this.data.addPartialSigToInput(inputIndex, partialSig); - return this; - } - - addSighashTypeToInput(inputIndex: number, sighashType: SighashType): this { - this.data.addSighashTypeToInput(inputIndex, sighashType); - return this; - } - - addRedeemScriptToInput(inputIndex: number, redeemScript: RedeemScript): this { - this.data.addRedeemScriptToInput(inputIndex, redeemScript); - return this; - } - - addWitnessScriptToInput( - inputIndex: number, - witnessScript: WitnessScript, - ): this { - this.data.addWitnessScriptToInput(inputIndex, witnessScript); - return this; - } - - addBip32DerivationToInput( - inputIndex: number, - bip32Derivation: Bip32Derivation, - ): this { - this.data.addBip32DerivationToInput(inputIndex, bip32Derivation); - return this; - } - - addFinalScriptSigToInput( - inputIndex: number, - finalScriptSig: FinalScriptSig, - ): this { - this.data.addFinalScriptSigToInput(inputIndex, finalScriptSig); - return this; - } - - addFinalScriptWitnessToInput( - inputIndex: number, - finalScriptWitness: FinalScriptWitness, - ): this { - this.data.addFinalScriptWitnessToInput(inputIndex, finalScriptWitness); - return this; - } - - addPorCommitmentToInput( - inputIndex: number, - porCommitment: PorCommitment, - ): this { - this.data.addPorCommitmentToInput(inputIndex, porCommitment); - return this; - } - - addRedeemScriptToOutput( - outputIndex: number, - redeemScript: RedeemScript, - ): this { - this.data.addRedeemScriptToOutput(outputIndex, redeemScript); - return this; - } - - addWitnessScriptToOutput( - outputIndex: number, - witnessScript: WitnessScript, - ): this { - this.data.addWitnessScriptToOutput(outputIndex, witnessScript); - return this; - } - - addBip32DerivationToOutput( - outputIndex: number, - bip32Derivation: Bip32Derivation, - ): this { - this.data.addBip32DerivationToOutput(outputIndex, bip32Derivation); + updateOutput(outputIndex: number, updateData: PsbtOutputUpdate): this { + this.data.updateOutput(outputIndex, updateData); return this; } @@ -618,6 +524,67 @@ interface PsbtOpts { maximumFeeRate: number; } +const transactionFromBuffer: TransactionFromBuffer = ( + buffer: Buffer, +): ITransaction => new PsbtTransaction(buffer); + +class PsbtTransaction implements ITransaction { + tx: Transaction; + constructor(buffer: Buffer = Buffer.from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0])) { + this.tx = Transaction.fromBuffer(buffer); + if (this.tx.ins.some(input => input.script.length !== 0)) { + throw new Error('Format Error: Transaction ScriptSigs are not empty'); + } + Object.defineProperty(this, 'tx', { + enumerable: false, + writable: true, + }); + } + + getInputOutputCounts(): { + inputCount: number; + outputCount: number; + } { + return { + inputCount: this.tx.ins.length, + outputCount: this.tx.outs.length, + }; + } + + addInput(input: any): void { + if ( + (input as any).hash === undefined || + (input as any).index === undefined || + (!Buffer.isBuffer((input as any).hash) && + typeof (input as any).hash !== 'string') || + typeof (input as any).index !== 'number' + ) { + throw new Error('Error adding input.'); + } + const hash = + typeof input.hash === 'string' + ? reverseBuffer(Buffer.from(input.hash, 'hex')) + : input.hash; + this.tx.addInput(hash, input.index, input.sequence); + } + + addOutput(output: any): void { + if ( + (output as any).script === undefined || + (output as any).value === undefined || + !Buffer.isBuffer((output as any).script) || + typeof (output as any).value !== 'number' + ) { + throw new Error('Error adding output.'); + } + this.tx.addOutput(output.script, output.value); + } + + toBuffer(): Buffer { + return this.tx.toBuffer(); + } +} + function canFinalize( input: PsbtInput, script: Buffer, @@ -979,61 +946,6 @@ function getHashForSig( }; } -function getInputAdder( - cache: PsbtCache, -): (_inputData: TransactionInput, txBuf: Buffer) => Buffer { - const selfCache = cache; - return (_inputData: TransactionInput, txBuf: Buffer): Buffer => { - if ( - !txBuf || - (_inputData as any).hash === undefined || - (_inputData as any).index === undefined || - (!Buffer.isBuffer((_inputData as any).hash) && - typeof (_inputData as any).hash !== 'string') || - typeof (_inputData as any).index !== 'number' - ) { - throw new Error('Error adding input.'); - } - const prevHash = Buffer.isBuffer(_inputData.hash) - ? _inputData.hash - : reverseBuffer(Buffer.from(_inputData.hash, 'hex')); - - // Check if input already exists in cache. - const input = { hash: prevHash, index: _inputData.index }; - checkTxInputCache(selfCache, input); - - selfCache.__TX.ins.push({ - ...input, - script: Buffer.alloc(0), - sequence: _inputData.sequence || Transaction.DEFAULT_SEQUENCE, - witness: [], - }); - return selfCache.__TX.toBuffer(); - }; -} - -function getOutputAdder( - cache: PsbtCache, -): (_outputData: TransactionOutput, txBuf: Buffer) => Buffer { - const selfCache = cache; - return (_outputData: TransactionOutput, txBuf: Buffer): Buffer => { - if ( - !txBuf || - (_outputData as any).script === undefined || - (_outputData as any).value === undefined || - !Buffer.isBuffer((_outputData as any).script) || - typeof (_outputData as any).value !== 'number' - ) { - throw new Error('Error adding output.'); - } - selfCache.__TX.outs.push({ - script: (_outputData as any).script!, - value: _outputData.value, - }); - return selfCache.__TX.toBuffer(); - }; -} - function getPayment( script: Buffer, scriptType: string, diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 533b8fd..e537656 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -1,6 +1,6 @@ /// import { Psbt as PsbtBase } from 'bip174'; -import { Bip32Derivation, FinalScriptSig, FinalScriptWitness, GlobalXpub, KeyValue, NonWitnessUtxo, PartialSig, PorCommitment, RedeemScript, SighashType, TransactionInput, TransactionOutput, WitnessScript, WitnessUtxo } from 'bip174/src/lib/interfaces'; +import { KeyValue, PsbtGlobalUpdate, PsbtInputUpdate, PsbtOutputUpdate, TransactionInput, TransactionOutput } from 'bip174/src/lib/interfaces'; import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; @@ -10,6 +10,7 @@ export declare class Psbt { static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt; static fromHex(data: string, opts?: PsbtOptsOptional): Psbt; static fromBuffer(buffer: Buffer, opts?: PsbtOptsOptional): Psbt; + unsignedTx: Buffer; private __CACHE; private opts; constructor(opts?: PsbtOptsOptional, data?: PsbtBase); @@ -37,20 +38,9 @@ export declare class Psbt { toBuffer(): Buffer; toHex(): string; toBase64(): string; - addGlobalXpubToGlobal(globalXpub: GlobalXpub): this; - addNonWitnessUtxoToInput(inputIndex: number, nonWitnessUtxo: NonWitnessUtxo): this; - addWitnessUtxoToInput(inputIndex: number, witnessUtxo: WitnessUtxo): this; - addPartialSigToInput(inputIndex: number, partialSig: PartialSig): this; - addSighashTypeToInput(inputIndex: number, sighashType: SighashType): this; - addRedeemScriptToInput(inputIndex: number, redeemScript: RedeemScript): this; - addWitnessScriptToInput(inputIndex: number, witnessScript: WitnessScript): this; - addBip32DerivationToInput(inputIndex: number, bip32Derivation: Bip32Derivation): this; - addFinalScriptSigToInput(inputIndex: number, finalScriptSig: FinalScriptSig): this; - addFinalScriptWitnessToInput(inputIndex: number, finalScriptWitness: FinalScriptWitness): this; - addPorCommitmentToInput(inputIndex: number, porCommitment: PorCommitment): this; - addRedeemScriptToOutput(outputIndex: number, redeemScript: RedeemScript): this; - addWitnessScriptToOutput(outputIndex: number, witnessScript: WitnessScript): this; - addBip32DerivationToOutput(outputIndex: number, bip32Derivation: Bip32Derivation): this; + updateGlobal(updateData: PsbtGlobalUpdate): this; + updateInput(inputIndex: number, updateData: PsbtInputUpdate): this; + updateOutput(outputIndex: number, updateData: PsbtOutputUpdate): this; addUnknownKeyValToGlobal(keyVal: KeyValue): this; addUnknownKeyValToInput(inputIndex: number, keyVal: KeyValue): this; addUnknownKeyValToOutput(outputIndex: number, keyVal: KeyValue): this; From 19a33f7da868a65434c9dc486478f732ff771faf Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 18 Jul 2019 14:20:44 +0900 Subject: [PATCH 103/111] Add comments and remove fromTransaction --- src/psbt.js | 76 +++++++++++++++++++++++++++----------- test/fixtures/psbt.json | 6 --- test/psbt.js | 14 ------- ts_src/psbt.ts | 81 ++++++++++++++++++++++++++++------------- types/psbt.d.ts | 37 +++++++++++++++++-- 5 files changed, 144 insertions(+), 70 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 97e9e9d..a3191b4 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -11,10 +11,54 @@ const networks_1 = require('./networks'); const payments = require('./payments'); const bscript = require('./script'); const transaction_1 = require('./transaction'); +/** + * These are the default arguments for a Psbt instance. + */ const DEFAULT_OPTS = { + /** + * A bitcoinjs Network object. This is only used if you pass an `address` + * parameter to addOutput. Otherwise it is not needed and can be left default. + */ network: networks_1.bitcoin, + /** + * When extractTransaction is called, the fee rate is checked. + * THIS IS NOT TO BE RELIED ON. + * It is only here as a last ditch effort to prevent sending a 500 BTC fee etc. + */ maximumFeeRate: 5000, }; +/** + * 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) + * + * Creator: This can be done with `new Psbt()` + * Updater: This can be done with `psbt.addInput(input)`, `psbt.addInputs(inputs)`, + * `psbt.addOutput(output)`, `psbt.addOutputs(outputs)` when you are looking to + * add new inputs and outputs to the PSBT, and `psbt.updateGlobal(itemObject)`, + * `psbt.updateInput(itemObject)`, `psbt.updateOutput(itemObject)` + * addInput requires hash: Buffer | string; and index: number; as attributes + * and can also include any attributes that are used in updateInput method. + * addOutput requires script: Buffer; and value: number; and likewise can include + * data for updateOutput. + * For a list of what attributes should be what types. Check the bip174 library. + * Also, check the integration tests for some examples of usage. + * Signer: There are a few methods. sign and signAsync, which will search all input + * information for your pubkey or pubkeyhash, and only sign inputs where it finds + * your info. Or you can explicitly sign a specific input with signInput and + * signInputAsync. For the async methods you can create a SignerAsync object + * and use something like a hardware wallet to sign with. (You must implement this) + * Combiner: psbts can be combined easily with `psbt.combine(psbt2, psbt3, psbt4 ...)` + * the psbt calling combine will always have precedence when a conflict occurs. + * Combine checks if the internal bitcoin transaction is the same, so be sure that + * all sequences, version, locktime, etc. are the same before combining. + * Input Finalizer: This role is fairly important. Not only does it need to construct + * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. + * Before running `psbt.finalizeAllInputs()` please run `psbt.validateAllSignatures()` + * Running any finalize method will delete any data in the input(s) that are no longer + * needed due to the finalized scripts containing the information. + * Transaction Extractor: This role will perform some checks before returning a + * Transaction object. Such as fee rate not being larger than maximumFeeRate etc. + */ class Psbt { constructor(opts = {}, data = new bip174_1.Psbt(new PsbtTransaction())) { this.data = data; @@ -55,25 +99,6 @@ class Psbt { dpew(this, '__CACHE', false, true); dpew(this, 'opts', false, true); } - static fromTransaction(txBuf, opts = {}) { - const tx = new PsbtTransaction(txBuf); - checkTxEmpty(tx.tx); - const psbtBase = new bip174_1.Psbt(tx); - const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx.tx; - checkTxForDupeIns(tx.tx, psbt.__CACHE); - let inputCount = tx.tx.ins.length; - let outputCount = tx.tx.outs.length; - while (inputCount > 0) { - psbtBase.inputs.push({}); - inputCount--; - } - while (outputCount > 0) { - psbtBase.outputs.push({}); - outputCount--; - } - return psbt; - } static fromBase64(data, opts = {}) { const buffer = Buffer.from(data, 'base64'); return this.fromBuffer(buffer, opts); @@ -418,13 +443,20 @@ class Psbt { } } exports.Psbt = Psbt; +/** + * This function is needed to pass to the bip174 base class's fromBuffer. + * It takes the "transaction buffer" portion of the psbt buffer and returns a + * Transaction (From the bip174 library) interface. + */ const transactionFromBuffer = buffer => new PsbtTransaction(buffer); +/** + * This class implements the Transaction interface from bip174 library. + * It contains a bitcoinjs-lib Transaction object. + */ class PsbtTransaction { constructor(buffer = Buffer.from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0])) { this.tx = transaction_1.Transaction.fromBuffer(buffer); - if (this.tx.ins.some(input => input.script.length !== 0)) { - throw new Error('Format Error: Transaction ScriptSigs are not empty'); - } + checkTxEmpty(this.tx); Object.defineProperty(this, 'tx', { enumerable: false, writable: true, diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index a29bc8b..8d83e9d 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -480,12 +480,6 @@ "result": "cHNidP8BAKYCAAAAAlwKQ3suPWwEJ/zQ9sZsIioOcHKU1KoLMxlMNSXVIkEWAAAAAAD/////YYDJMap+mYgbTrCNAdpWHN+EkKvl+XYao/6co/EQfwMAAAAAAP////8CkF8BAAAAAAAWABRnPBAmVHz2HL+8/1U+QG5L2thjmjhKAAAAAAAAIgAg700yfFRyhWzQnPHIUb/XQqsjlpf4A0uw682pCVWuQ8IAAAAAAAEBKzB1AAAAAAAAIgAgth9oE4cDfC5aV58VgkW5CptHsIxppYzJV8C5kT6aTo8BCG4CSDBFAiEAs7TFGm6o/dpWSb4M/KSu2p7p881KA1uWn0r0wDCa0ckCIAdXlm9k3xWLj2f+j0hIXK+0ew9dc8qoHEL2babYeWliASMhA182CcWcQ9L2zg9j8jlt7OlaIATNDgNFMBKB1J14wzTprAABASuAOAEAAAAAACIAILYfaBOHA3wuWlefFYJFuQqbR7CMaaWMyVfAuZE+mk6PAQhuAkgwRQIhALpc3q2mj2ZiVl6PgFdJKHkPUrpogsF9DhYFZdSshLHrAiBVrEVSzPzLn3EnVXixnWpqsdf2ln4wmYspuXZlJp2KygEjIQNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06awAAAA=" } ], - "fromTransaction": [ - { - "transaction": "020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000", - "result": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAAAAAA=" - } - ], "validateSignatures": { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "index": 0, diff --git a/test/psbt.js b/test/psbt.js index e52fae2..1837760 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -309,15 +309,6 @@ describe(`Psbt`, () => { }) }) - describe('fromTransaction', () => { - fixtures.fromTransaction.forEach(f => { - it('Creates the expected PSBT from a transaction buffer', () => { - const psbt = Psbt.fromTransaction(Buffer.from(f.transaction, 'hex')) - assert.strictEqual(psbt.toBase64(), f.result) - }) - }) - }) - describe('addInput', () => { fixtures.addInput.checks.forEach(f => { it(f.description, () => { @@ -521,11 +512,6 @@ describe(`Psbt`, () => { }) describe('Method return types', () => { - it('fromTransaction returns Psbt type (not base class)', () => { - const psbt = Psbt.fromTransaction(Buffer.from([2,0,0,0,0,0,0,0,0,0])); - assert.strictEqual(psbt instanceof Psbt, true); - assert.ok(psbt.__CACHE.__TX); - }) it('fromBuffer returns Psbt type (not base class)', () => { const psbt = Psbt.fromBuffer(Buffer.from( '70736274ff01000a01000000000000000000000000', 'hex' //cHNidP8BAAoBAAAAAAAAAAAAAAAA diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0c20bc2..97f615f 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -26,32 +26,56 @@ import * as payments from './payments'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; +/** + * These are the default arguments for a Psbt instance. + */ const DEFAULT_OPTS: PsbtOpts = { + /** + * A bitcoinjs Network object. This is only used if you pass an `address` + * parameter to addOutput. Otherwise it is not needed and can be left default. + */ network: btcNetwork, + /** + * When extractTransaction is called, the fee rate is checked. + * THIS IS NOT TO BE RELIED ON. + * It is only here as a last ditch effort to prevent sending a 500 BTC fee etc. + */ maximumFeeRate: 5000, // satoshi per byte }; +/** + * 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) + * + * Creator: This can be done with `new Psbt()` + * Updater: This can be done with `psbt.addInput(input)`, `psbt.addInputs(inputs)`, + * `psbt.addOutput(output)`, `psbt.addOutputs(outputs)` when you are looking to + * add new inputs and outputs to the PSBT, and `psbt.updateGlobal(itemObject)`, + * `psbt.updateInput(itemObject)`, `psbt.updateOutput(itemObject)` + * addInput requires hash: Buffer | string; and index: number; as attributes + * and can also include any attributes that are used in updateInput method. + * addOutput requires script: Buffer; and value: number; and likewise can include + * data for updateOutput. + * For a list of what attributes should be what types. Check the bip174 library. + * Also, check the integration tests for some examples of usage. + * Signer: There are a few methods. sign and signAsync, which will search all input + * information for your pubkey or pubkeyhash, and only sign inputs where it finds + * your info. Or you can explicitly sign a specific input with signInput and + * signInputAsync. For the async methods you can create a SignerAsync object + * and use something like a hardware wallet to sign with. (You must implement this) + * Combiner: psbts can be combined easily with `psbt.combine(psbt2, psbt3, psbt4 ...)` + * the psbt calling combine will always have precedence when a conflict occurs. + * Combine checks if the internal bitcoin transaction is the same, so be sure that + * all sequences, version, locktime, etc. are the same before combining. + * Input Finalizer: This role is fairly important. Not only does it need to construct + * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. + * Before running `psbt.finalizeAllInputs()` please run `psbt.validateAllSignatures()` + * Running any finalize method will delete any data in the input(s) that are no longer + * needed due to the finalized scripts containing the information. + * Transaction Extractor: This role will perform some checks before returning a + * Transaction object. Such as fee rate not being larger than maximumFeeRate etc. + */ export class Psbt { - static fromTransaction(txBuf: Buffer, opts: PsbtOptsOptional = {}): Psbt { - const tx = new PsbtTransaction(txBuf); - checkTxEmpty(tx.tx); - const psbtBase = new PsbtBase(tx); - const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx.tx; - checkTxForDupeIns(tx.tx, psbt.__CACHE); - let inputCount = tx.tx.ins.length; - let outputCount = tx.tx.outs.length; - while (inputCount > 0) { - psbtBase.inputs.push({}); - inputCount--; - } - while (outputCount > 0) { - psbtBase.outputs.push({}); - outputCount--; - } - return psbt; - } - static fromBase64(data: string, opts: PsbtOptsOptional = {}): Psbt { const buffer = Buffer.from(data, 'base64'); return this.fromBuffer(buffer, opts); @@ -356,7 +380,7 @@ export class Psbt { } signAsync( - keyPair: SignerAsync, + keyPair: Signer | SignerAsync, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): Promise { return new Promise( @@ -419,7 +443,7 @@ export class Psbt { signInputAsync( inputIndex: number, - keyPair: SignerAsync, + keyPair: Signer | SignerAsync, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): Promise { return new Promise( @@ -524,17 +548,24 @@ interface PsbtOpts { maximumFeeRate: number; } +/** + * This function is needed to pass to the bip174 base class's fromBuffer. + * It takes the "transaction buffer" portion of the psbt buffer and returns a + * Transaction (From the bip174 library) interface. + */ const transactionFromBuffer: TransactionFromBuffer = ( buffer: Buffer, ): ITransaction => new PsbtTransaction(buffer); +/** + * This class implements the Transaction interface from bip174 library. + * It contains a bitcoinjs-lib Transaction object. + */ class PsbtTransaction implements ITransaction { tx: Transaction; constructor(buffer: Buffer = Buffer.from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0])) { this.tx = Transaction.fromBuffer(buffer); - if (this.tx.ins.some(input => input.script.length !== 0)) { - throw new Error('Format Error: Transaction ScriptSigs are not empty'); - } + checkTxEmpty(this.tx); Object.defineProperty(this, 'tx', { enumerable: false, writable: true, diff --git a/types/psbt.d.ts b/types/psbt.d.ts index e537656..c3a12d0 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -4,9 +4,40 @@ import { KeyValue, PsbtGlobalUpdate, PsbtInputUpdate, PsbtOutputUpdate, Transact import { Signer, SignerAsync } from './ecpair'; import { Network } from './networks'; import { Transaction } from './transaction'; +/** + * 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) + * + * Creator: This can be done with `new Psbt()` + * Updater: This can be done with `psbt.addInput(input)`, `psbt.addInputs(inputs)`, + * `psbt.addOutput(output)`, `psbt.addOutputs(outputs)` when you are looking to + * add new inputs and outputs to the PSBT, and `psbt.updateGlobal(itemObject)`, + * `psbt.updateInput(itemObject)`, `psbt.updateOutput(itemObject)` + * addInput requires hash: Buffer | string; and index: number; as attributes + * and can also include any attributes that are used in updateInput method. + * addOutput requires script: Buffer; and value: number; and likewise can include + * data for updateOutput. + * For a list of what attributes should be what types. Check the bip174 library. + * Also, check the integration tests for some examples of usage. + * Signer: There are a few methods. sign and signAsync, which will search all input + * information for your pubkey or pubkeyhash, and only sign inputs where it finds + * your info. Or you can explicitly sign a specific input with signInput and + * signInputAsync. For the async methods you can create a SignerAsync object + * and use something like a hardware wallet to sign with. (You must implement this) + * Combiner: psbts can be combined easily with `psbt.combine(psbt2, psbt3, psbt4 ...)` + * the psbt calling combine will always have precedence when a conflict occurs. + * Combine checks if the internal bitcoin transaction is the same, so be sure that + * all sequences, version, locktime, etc. are the same before combining. + * Input Finalizer: This role is fairly important. Not only does it need to construct + * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. + * Before running `psbt.finalizeAllInputs()` please run `psbt.validateAllSignatures()` + * Running any finalize method will delete any data in the input(s) that are no longer + * needed due to the finalized scripts containing the information. + * Transaction Extractor: This role will perform some checks before returning a + * Transaction object. Such as fee rate not being larger than maximumFeeRate etc. + */ export declare class Psbt { readonly data: PsbtBase; - static fromTransaction(txBuf: Buffer, opts?: PsbtOptsOptional): Psbt; static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt; static fromHex(data: string, opts?: PsbtOptsOptional): Psbt; static fromBuffer(buffer: Buffer, opts?: PsbtOptsOptional): Psbt; @@ -32,9 +63,9 @@ export declare class Psbt { validateAllSignatures(): boolean; validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; sign(keyPair: Signer, sighashTypes?: number[]): this; - signAsync(keyPair: SignerAsync, sighashTypes?: number[]): Promise; + signAsync(keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this; - signInputAsync(inputIndex: number, keyPair: SignerAsync, sighashTypes?: number[]): Promise; + signInputAsync(inputIndex: number, keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; toBuffer(): Buffer; toHex(): string; toBase64(): string; From def2182eaf862ef1d9e7a7bcc9e2333b390fad16 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 18 Jul 2019 15:57:00 +0900 Subject: [PATCH 104/111] Fix: integration test comments --- test/integration/transactions-psbt.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index 96ae073..7a3a85e 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -48,7 +48,9 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // value: 90000, // }, - // Not featured here: redeemScript. A Buffer of the redeemScript + // Not featured here: + // redeemScript. A Buffer of the redeemScript for P2SH + // witnessScript. A Buffer of the witnessScript for P2WSH }); psbt.addOutput({ address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp', @@ -149,8 +151,6 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // 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(); - // it returns an array of the success of each input, also a result attribute - // which is true if all array items are true. // build and broadcast our RegTest network await regtestUtils.broadcast(psbt.extractTransaction().toHex()); From 1326e0cc4286b3d6f25f50fc67edd3826505d52b Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 10:12:20 +0900 Subject: [PATCH 105/111] Remove the cached buffer getter --- src/psbt.js | 20 -------------------- ts_src/psbt.ts | 23 ----------------------- types/psbt.d.ts | 1 - 3 files changed, 44 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index a3191b4..dab6dbc 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -73,23 +73,6 @@ class Psbt { const c = this.__CACHE; c.__TX = this.data.globalMap.unsignedTx.tx; if (this.data.inputs.length === 0) this.setVersion(2); - // set cache - this.unsignedTx = Buffer.from([]); - Object.defineProperty(this, 'unsignedTx', { - enumerable: true, - get() { - const buf = c.__TX_BUF_CACHE; - if (buf !== undefined) { - return buf; - } else { - c.__TX_BUF_CACHE = c.__TX.toBuffer(); - return c.__TX_BUF_CACHE; - } - }, - set(_data) { - c.__TX_BUF_CACHE = _data; - }, - }); // Make data hidden when enumerating const dpew = (obj, attr, enumerable, writable) => Object.defineProperty(obj, attr, { @@ -137,7 +120,6 @@ class Psbt { checkInputsForPartialSig(this.data.inputs, 'setVersion'); const c = this.__CACHE; c.__TX.version = version; - c.__TX_BUF_CACHE = undefined; c.__EXTRACTED_TX = undefined; return this; } @@ -146,7 +128,6 @@ class Psbt { checkInputsForPartialSig(this.data.inputs, 'setLocktime'); const c = this.__CACHE; c.__TX.locktime = locktime; - c.__TX_BUF_CACHE = undefined; c.__EXTRACTED_TX = undefined; return this; } @@ -158,7 +139,6 @@ class Psbt { throw new Error('Input index too high'); } c.__TX.ins[inputIndex].sequence = sequence; - c.__TX_BUF_CACHE = undefined; c.__EXTRACTED_TX = undefined; return this; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 97f615f..14dca4b 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -96,7 +96,6 @@ export class Psbt { return psbt; } - unsignedTx: Buffer; private __CACHE: PsbtCache = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], @@ -115,24 +114,6 @@ export class Psbt { c.__TX = (this.data.globalMap.unsignedTx as PsbtTransaction).tx; if (this.data.inputs.length === 0) this.setVersion(2); - // set cache - this.unsignedTx = Buffer.from([]); - Object.defineProperty(this, 'unsignedTx', { - enumerable: true, - get(): Buffer { - const buf = c.__TX_BUF_CACHE; - if (buf !== undefined) { - return buf; - } else { - c.__TX_BUF_CACHE = c.__TX.toBuffer(); - return c.__TX_BUF_CACHE; - } - }, - set(_data: Buffer): void { - c.__TX_BUF_CACHE = _data; - }, - }); - // Make data hidden when enumerating const dpew = ( obj: any, @@ -174,7 +155,6 @@ export class Psbt { checkInputsForPartialSig(this.data.inputs, 'setVersion'); const c = this.__CACHE; c.__TX.version = version; - c.__TX_BUF_CACHE = undefined; c.__EXTRACTED_TX = undefined; return this; } @@ -184,7 +164,6 @@ export class Psbt { checkInputsForPartialSig(this.data.inputs, 'setLocktime'); const c = this.__CACHE; c.__TX.locktime = locktime; - c.__TX_BUF_CACHE = undefined; c.__EXTRACTED_TX = undefined; return this; } @@ -197,7 +176,6 @@ export class Psbt { throw new Error('Input index too high'); } c.__TX.ins[inputIndex].sequence = sequence; - c.__TX_BUF_CACHE = undefined; c.__EXTRACTED_TX = undefined; return this; } @@ -533,7 +511,6 @@ interface PsbtCache { __NON_WITNESS_UTXO_BUF_CACHE: Buffer[]; __TX_IN_CACHE: { [index: string]: number }; __TX: Transaction; - __TX_BUF_CACHE?: Buffer; __FEE_RATE?: number; __EXTRACTED_TX?: Transaction; } diff --git a/types/psbt.d.ts b/types/psbt.d.ts index c3a12d0..2b24e65 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -41,7 +41,6 @@ export declare class Psbt { static fromBase64(data: string, opts?: PsbtOptsOptional): Psbt; static fromHex(data: string, opts?: PsbtOptsOptional): Psbt; static fromBuffer(buffer: Buffer, opts?: PsbtOptsOptional): Psbt; - unsignedTx: Buffer; private __CACHE; private opts; constructor(opts?: PsbtOptsOptional, data?: PsbtBase); From 4366b621d7db918b5fa8e59db285ddfefa4f432a Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 11:42:45 +0900 Subject: [PATCH 106/111] Add HD signer methods --- src/psbt.js | 108 ++++++++++++++++++++++++++ test/fixtures/psbt.json | 46 +++++++++++ test/psbt.js | 125 ++++++++++++++++++++++++++++++ ts_src/psbt.ts | 166 ++++++++++++++++++++++++++++++++++++++++ types/psbt.d.ts | 33 ++++++++ 5 files changed, 478 insertions(+) diff --git a/src/psbt.js b/src/psbt.js index dab6dbc..f183bbc 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -278,6 +278,86 @@ class Psbt { } return results.every(res => res === true); } + signHD(hdKeyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + throw new Error('Need HDSigner to sign input'); + } + const results = []; + for (const i of range(this.data.inputs.length)) { + try { + this.signInputHD(i, hdKeyPair, sighashTypes); + results.push(true); + } catch (err) { + results.push(false); + } + } + if (results.every(v => v === false)) { + throw new Error('No inputs were signed'); + } + return this; + } + signHDAsync( + hdKeyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { + return new Promise((resolve, reject) => { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + return reject(new Error('Need HDSigner to sign input')); + } + const results = []; + const promises = []; + for (const i of range(this.data.inputs.length)) { + promises.push( + this.signInputHDAsync(i, hdKeyPair, sighashTypes).then( + () => { + results.push(true); + }, + () => { + results.push(false); + }, + ), + ); + } + return Promise.all(promises).then(() => { + if (results.every(v => v === false)) { + return reject(new Error('No inputs were signed')); + } + resolve(); + }); + }); + } + signInputHD( + inputIndex, + hdKeyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + throw new Error('Need HDSigner to sign input'); + } + const signers = getSignersFromHD(inputIndex, this.data.inputs, hdKeyPair); + signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes)); + return this; + } + signInputHDAsync( + inputIndex, + hdKeyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { + return new Promise((resolve, reject) => { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + return reject(new Error('Need HDSigner to sign input')); + } + const signers = getSignersFromHD(inputIndex, this.data.inputs, hdKeyPair); + const promises = signers.map(signer => + this.signInputAsync(inputIndex, signer, sighashTypes), + ); + return Promise.all(promises) + .then(() => { + resolve(); + }) + .catch(reject); + }); + } sign(keyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); @@ -853,6 +933,34 @@ function getScriptFromInput(inputIndex, input, cache) { } return res; } +function getSignersFromHD(inputIndex, inputs, hdKeyPair) { + const input = utils_1.checkForInput(inputs, inputIndex); + if (!input.bip32Derivation || input.bip32Derivation.length === 0) { + throw new Error('Need bip32Derivation to sign with HD'); + } + const myDerivations = input.bip32Derivation + .map(bipDv => { + if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { + return bipDv; + } else { + return; + } + }) + .filter(v => !!v); + if (myDerivations.length === 0) { + throw new Error( + 'Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint', + ); + } + const signers = myDerivations.map(bipDv => { + const node = hdKeyPair.derivePath(bipDv.path); + if (!bipDv.pubkey.equals(node.publicKey)) { + throw new Error('pubkey did not match bip32Derivation'); + } + return node; + }); + return signers; +} function getSortedSigs(script, partialSig) { const p2ms = payments.p2ms({ output: script }); // for each pubkey in order of p2ms script diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 8d83e9d..528e922 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -448,6 +448,52 @@ } ] }, + "signInputHD": { + "checks": [ + { + "description": "checks the bip32Derivation exists", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYDn85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=", + "inputToCheck": 0, + "xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev" + }, + "shouldThrow": { + "errorMessage": "Need bip32Derivation to sign with HD", + "psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=", + "inputToCheck": 0, + "xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev" + } + }, + { + "description": "checks the bip32Derivation exists", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYDn85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=", + "inputToCheck": 0, + "xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev" + }, + "shouldThrow": { + "errorMessage": "Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint", + "psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYD/85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kY/////ywAAIAAAACAAAAAgAAAAAAAAAAAAAA=", + "inputToCheck": 0, + "xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev" + } + }, + { + "description": "checks the bip32Derivation exists", + "shouldSign": { + "psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYDn85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=", + "inputToCheck": 0, + "xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev" + }, + "shouldThrow": { + "errorMessage": "pubkey did not match bip32Derivation", + "psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYD/85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=", + "inputToCheck": 0, + "xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev" + } + } + ] + }, "finalizeAllInputs": [ { "type": "P2PK", diff --git a/test/psbt.js b/test/psbt.js index 1837760..18a4c99 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -1,6 +1,7 @@ const { describe, it } = require('mocha') const assert = require('assert') +const bip32 = require('bip32') const ECPair = require('../src/ecpair') const Psbt = require('..').Psbt const NETWORKS = require('../src/networks') @@ -278,6 +279,130 @@ describe(`Psbt`, () => { }) }) + describe('signInputHDAsync', () => { + fixtures.signInputHD.checks.forEach(f => { + it(f.description, async () => { + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotReject(async () => { + await psbtThatShouldsign.signInputHDAsync( + f.shouldSign.inputToCheck, + bip32.fromBase58(f.shouldSign.xprv), + f.shouldSign.sighashTypes || undefined, + ) + }) + } + + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.rejects(async () => { + await psbtThatShouldThrow.signInputHDAsync( + f.shouldThrow.inputToCheck, + bip32.fromBase58(f.shouldThrow.xprv), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp(f.shouldThrow.errorMessage)) + assert.rejects(async () => { + await psbtThatShouldThrow.signInputHDAsync( + f.shouldThrow.inputToCheck, + ) + }, new RegExp('Need HDSigner to sign input')) + } + }) + }) + }) + + describe('signInputHD', () => { + fixtures.signInputHD.checks.forEach(f => { + it(f.description, () => { + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotThrow(() => { + psbtThatShouldsign.signInputHD( + f.shouldSign.inputToCheck, + bip32.fromBase58(f.shouldSign.xprv), + f.shouldSign.sighashTypes || undefined, + ) + }) + } + + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.throws(() => { + psbtThatShouldThrow.signInputHD( + f.shouldThrow.inputToCheck, + bip32.fromBase58(f.shouldThrow.xprv), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp(f.shouldThrow.errorMessage)) + assert.throws(() => { + psbtThatShouldThrow.signInputHD( + f.shouldThrow.inputToCheck, + ) + }, new RegExp('Need HDSigner to sign input')) + } + }) + }) + }) + + describe('signHDAsync', () => { + fixtures.signInputHD.checks.forEach(f => { + it(f.description, async () => { + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotReject(async () => { + await psbtThatShouldsign.signHDAsync( + bip32.fromBase58(f.shouldSign.xprv), + f.shouldSign.sighashTypes || undefined, + ) + }) + } + + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.rejects(async () => { + await psbtThatShouldThrow.signHDAsync( + bip32.fromBase58(f.shouldThrow.xprv), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp('No inputs were signed')) + assert.rejects(async () => { + await psbtThatShouldThrow.signHDAsync() + }, new RegExp('Need HDSigner to sign input')) + } + }) + }) + }) + + describe('signHD', () => { + fixtures.signInputHD.checks.forEach(f => { + it(f.description, () => { + if (f.shouldSign) { + const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) + assert.doesNotThrow(() => { + psbtThatShouldsign.signHD( + bip32.fromBase58(f.shouldSign.xprv), + f.shouldSign.sighashTypes || undefined, + ) + }) + } + + if (f.shouldThrow) { + const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) + assert.throws(() => { + psbtThatShouldThrow.signHD( + bip32.fromBase58(f.shouldThrow.xprv), + f.shouldThrow.sighashTypes || undefined, + ) + }, new RegExp('No inputs were signed')) + assert.throws(() => { + psbtThatShouldThrow.signHD() + }, new RegExp('Need HDSigner to sign input')) + } + }) + }) + }) + describe('finalizeAllInputs', () => { fixtures.finalizeAllInputs.forEach(f => { it(`Finalizes inputs of type "${f.type}"`, () => { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 14dca4b..0778226 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -332,6 +332,107 @@ export class Psbt { return results.every(res => res === true); } + signHD( + hdKeyPair: HDSigner, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): this { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + throw new Error('Need HDSigner to sign input'); + } + + const results: boolean[] = []; + for (const i of range(this.data.inputs.length)) { + try { + this.signInputHD(i, hdKeyPair, sighashTypes); + results.push(true); + } catch (err) { + results.push(false); + } + } + if (results.every(v => v === false)) { + throw new Error('No inputs were signed'); + } + return this; + } + + signHDAsync( + hdKeyPair: HDSigner | HDSignerAsync, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): Promise { + return new Promise( + (resolve, reject): any => { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + return reject(new Error('Need HDSigner to sign input')); + } + + const results: boolean[] = []; + const promises: Array> = []; + for (const i of range(this.data.inputs.length)) { + promises.push( + this.signInputHDAsync(i, hdKeyPair, sighashTypes).then( + () => { + results.push(true); + }, + () => { + results.push(false); + }, + ), + ); + } + return Promise.all(promises).then(() => { + if (results.every(v => v === false)) { + return reject(new Error('No inputs were signed')); + } + resolve(); + }); + }, + ); + } + + signInputHD( + inputIndex: number, + hdKeyPair: HDSigner, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): this { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + throw new Error('Need HDSigner to sign input'); + } + const signers = getSignersFromHD( + inputIndex, + this.data.inputs, + hdKeyPair, + ) as Signer[]; + signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes)); + return this; + } + + signInputHDAsync( + inputIndex: number, + hdKeyPair: HDSigner | HDSignerAsync, + sighashTypes: number[] = [Transaction.SIGHASH_ALL], + ): Promise { + return new Promise( + (resolve, reject): any => { + if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { + return reject(new Error('Need HDSigner to sign input')); + } + const signers = getSignersFromHD( + inputIndex, + this.data.inputs, + hdKeyPair, + ); + const promises = signers.map(signer => + this.signInputAsync(inputIndex, signer, sighashTypes), + ); + return Promise.all(promises) + .then(() => { + resolve(); + }) + .catch(reject); + }, + ); + } + sign( keyPair: Signer, sighashTypes: number[] = [Transaction.SIGHASH_ALL], @@ -525,6 +626,38 @@ interface PsbtOpts { maximumFeeRate: number; } +interface HDSignerBase { + /** + * DER format compressed publicKey buffer + */ + publicKey: Buffer; + /** + * The first 4 bytes of the sha256-ripemd160 of the publicKey + */ + fingerprint: Buffer; +} + +interface HDSigner extends HDSignerBase { + /** + * The path string must match /^m(\/\d+'?)+$/ + * ex. m/44'/0'/0'/1/23 levels with ' must be hard derivations + */ + derivePath(path: string): HDSigner; + /** + * Input hash (the "message digest") for the signature algorithm + * Return a 64 byte signature (32 byte r and 32 byte s in that order) + */ + sign(hash: Buffer): Buffer; +} + +/** + * Same as above but with async sign method + */ +interface HDSignerAsync extends HDSignerBase { + derivePath(path: string): HDSignerAsync; + sign(hash: Buffer): Promise; +} + /** * This function is needed to pass to the bip174 base class's fromBuffer. * It takes the "transaction buffer" portion of the psbt buffer and returns a @@ -1042,6 +1175,39 @@ function getScriptFromInput( return res; } +function getSignersFromHD( + inputIndex: number, + inputs: PsbtInput[], + hdKeyPair: HDSigner | HDSignerAsync, +): Array { + const input = checkForInput(inputs, inputIndex); + if (!input.bip32Derivation || input.bip32Derivation.length === 0) { + throw new Error('Need bip32Derivation to sign with HD'); + } + const myDerivations = input.bip32Derivation + .map(bipDv => { + if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { + return bipDv; + } else { + return; + } + }) + .filter(v => !!v); + if (myDerivations.length === 0) { + throw new Error( + 'Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint', + ); + } + const signers: Array = myDerivations.map(bipDv => { + const node = hdKeyPair.derivePath(bipDv!.path); + if (!bipDv!.pubkey.equals(node.publicKey)) { + throw new Error('pubkey did not match bip32Derivation'); + } + return node; + }); + return signers; +} + function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] { const p2ms = payments.p2ms({ output: script }); // for each pubkey in order of p2ms script diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 2b24e65..9557f79 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -61,6 +61,10 @@ export declare class Psbt { finalizeInput(inputIndex: number): this; validateAllSignatures(): boolean; validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; + signHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; + signHDAsync(hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise; + signInputHD(inputIndex: number, hdKeyPair: HDSigner, sighashTypes?: number[]): this; + signInputHDAsync(inputIndex: number, hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise; sign(keyPair: Signer, sighashTypes?: number[]): this; signAsync(keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this; @@ -80,4 +84,33 @@ interface PsbtOptsOptional { network?: Network; maximumFeeRate?: number; } +interface HDSignerBase { + /** + * DER format compressed publicKey buffer + */ + publicKey: Buffer; + /** + * The first 4 bytes of the sha256-ripemd160 of the publicKey + */ + fingerprint: Buffer; +} +interface HDSigner extends HDSignerBase { + /** + * The path string must match /^m(\/\d+'?)+$/ + * ex. m/44'/0'/0'/1/23 levels with ' must be hard derivations + */ + derivePath(path: string): HDSigner; + /** + * Input hash (the "message digest") for the signature algorithm + * Return a 64 byte signature (32 byte r and 32 byte s in that order) + */ + sign(hash: Buffer): Buffer; +} +/** + * Same as above but with async sign method + */ +interface HDSignerAsync extends HDSignerBase { + derivePath(path: string): HDSignerAsync; + sign(hash: Buffer): Promise; +} export {}; From 1c5b0025c8aedc63a201a63046e7cb7ac5712850 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 12:03:32 +0900 Subject: [PATCH 107/111] Update integration test with HD example --- test/integration/transactions-psbt.js | 73 +++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index 7a3a85e..0e4bfe7 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -1,6 +1,8 @@ const { describe, it } = require('mocha'); const assert = require('assert'); const bitcoin = require('../../'); +const bip32 = require('bip32'); +const rng = require('randombytes'); const regtestUtils = require('./_regtest'); const regtest = regtestUtils.network; @@ -403,24 +405,87 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { value: 2e4, }); }); + + it('can create (and broadcast via 3PBP) a Transaction, w/ a P2WPKH input using HD', async () => { + const hdRoot = bip32.fromSeed(rng(64)); + const masterFingerprint = hdRoot.fingerprint; + const path = "m/84'/0'/0'/0/0"; + const childNode = hdRoot.derivePath(path); + const pubkey = childNode.publicKey; + + // This information should be added to your input via updateInput + // You can add multiple bip32Derivation objects for multisig, but + // each must have a unique pubkey. + // + // This is useful because as long as you store the masterFingerprint on + // the PSBT Creator's server, you can have the PSBT Creator do the heavy + // lifting with derivation from your m/84'/0'/0' xpub, (deriving only 0/0 ) + // and your signer just needs to pass in an HDSigner interface (ie. bip32 library) + const updateData = { + bip32Derivation: [ + { + masterFingerprint, + path, + pubkey, + } + ] + } + const p2wpkh = createPayment('p2wpkh', [childNode]); + const inputData = await getInputData(5e4, p2wpkh.payment, true, 'noredeem'); + { + const { hash, index, witnessUtxo } = inputData; + assert.deepStrictEqual({ hash, index, witnessUtxo }, inputData); + } + + // You can add extra attributes for updateData into the addInput(s) object(s) + Object.assign(inputData, updateData) + + const psbt = new bitcoin.Psbt({ network: regtest }) + .addInput(inputData) + // .updateInput(0, updateData) // if you didn't merge the bip32Derivation with inputData + .addOutput({ + address: regtestUtils.RANDOM_ADDRESS, + value: 2e4, + }) + .signInputHD(0, hdRoot); // must sign with root!!! + + assert.strictEqual(psbt.validateSignatures(0), true); + assert.strictEqual(psbt.validateSignatures(0, childNode.publicKey), true); + psbt.finalizeAllInputs(); + + const tx = psbt.extractTransaction(); + + // build and broadcast to the Bitcoin RegTest network + await regtestUtils.broadcast(tx.toHex()); + + await regtestUtils.verify({ + txId: tx.getId(), + address: regtestUtils.RANDOM_ADDRESS, + vout: 0, + value: 2e4, + }); + }); }); -function createPayment(_type, network) { +function createPayment(_type, myKeys, network) { network = network || regtest; const splitType = _type.split('-').reverse(); const isMultisig = splitType[0].slice(0, 4) === 'p2ms'; - const keys = []; + const keys = myKeys || []; let m; if (isMultisig) { const match = splitType[0].match(/^p2ms\((\d+) of (\d+)\)$/); m = parseInt(match[1]); let n = parseInt(match[2]); - while (n > 1) { + if (keys.length > 0 && keys.length !== n) { + throw new Error('Need n keys for multisig') + } + while (!myKeys && n > 1) { keys.push(bitcoin.ECPair.makeRandom({ network })); n--; } } - keys.push(bitcoin.ECPair.makeRandom({ network })); + if (!myKeys) keys.push(bitcoin.ECPair.makeRandom({ network })); let payment; splitType.forEach(type => { From acf59f167c369cdf771ddbd06f8e8fa20220a447 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 12:41:12 +0900 Subject: [PATCH 108/111] Use bip174@1.0.0 --- package-lock.json | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a7f8be..8e641b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -200,8 +200,9 @@ } }, "bip174": { - "version": "git+https://github.com/bitcoinjs/bip174.git#5137e367c7a3a4e281ee01574f88977cdd4be896", - "from": "git+https://github.com/bitcoinjs/bip174.git#interface" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-1.0.0.tgz", + "integrity": "sha512-AaoWrkYtv6A2y8H+qzs6NvRWypzNbADT8PQGpM9rnP+jLzeol+uzhe3Myeuq/dwrHYtmsW8V71HmX2oXhQGagw==" }, "bip32": { "version": "2.0.3", diff --git a/package.json b/package.json index 0b91e4e..806de55 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/node": "10.12.18", "bech32": "^1.1.2", - "bip174": "git+https://github.com/bitcoinjs/bip174.git#interface", + "bip174": "^1.0.0", "bip32": "^2.0.3", "bip66": "^1.1.0", "bitcoin-ops": "^1.4.0", From e19bc58b303fea38a8f7d58d9cea943495f540f1 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 14:53:54 +0900 Subject: [PATCH 109/111] Rename methods --- src/psbt.js | 26 +++++++++++------- test/fixtures/psbt.json | 2 +- test/integration/transactions-psbt.js | 34 ++++++++++++------------ test/psbt.js | 38 +++++++++++++-------------- ts_src/psbt.ts | 20 +++++++------- types/psbt.d.ts | 14 +++++----- 6 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index f183bbc..31ac164 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -42,7 +42,7 @@ const DEFAULT_OPTS = { * data for updateOutput. * For a list of what attributes should be what types. Check the bip174 library. * Also, check the integration tests for some examples of usage. - * Signer: There are a few methods. sign and signAsync, which will search all input + * Signer: There are a few methods. signAllInputs and signAsync, which will search all input * information for your pubkey or pubkeyhash, and only sign inputs where it finds * your info. Or you can explicitly sign a specific input with signInput and * signInputAsync. For the async methods you can create a SignerAsync object @@ -53,7 +53,7 @@ const DEFAULT_OPTS = { * all sequences, version, locktime, etc. are the same before combining. * Input Finalizer: This role is fairly important. Not only does it need to construct * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. - * Before running `psbt.finalizeAllInputs()` please run `psbt.validateAllSignatures()` + * Before running `psbt.finalizeAllInputs()` please run `psbt.validateSignaturesOfAllInputs()` * Running any finalize method will delete any data in the input(s) that are no longer * needed due to the finalized scripts containing the information. * Transaction Extractor: This role will perform some checks before returning a @@ -131,9 +131,9 @@ class Psbt { c.__EXTRACTED_TX = undefined; return this; } - setSequence(inputIndex, sequence) { + setInputSequence(inputIndex, sequence) { check32Bit(sequence); - checkInputsForPartialSig(this.data.inputs, 'setSequence'); + checkInputsForPartialSig(this.data.inputs, 'setInputSequence'); const c = this.__CACHE; if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); @@ -239,14 +239,14 @@ class Psbt { this.data.clearFinalizedInput(inputIndex); return this; } - validateAllSignatures() { + validateSignaturesOfAllInputs() { utils_1.checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => - this.validateSignatures(idx), + this.validateSignaturesOfInput(idx), ); return results.reduce((final, res) => res === true && final, true); } - validateSignatures(inputIndex, pubkey) { + validateSignaturesOfInput(inputIndex, pubkey) { const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; if (!input || !partialSig || partialSig.length < 1) @@ -278,7 +278,10 @@ class Psbt { } return results.every(res => res === true); } - signHD(hdKeyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) { + signAllInputsHD( + hdKeyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } @@ -358,7 +361,10 @@ class Psbt { .catch(reject); }); } - sign(keyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) { + signAllInputs( + keyPair, + sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], + ) { if (!keyPair || !keyPair.publicKey) throw new Error('Need Signer to sign input'); // TODO: Add a pubkey/pubkeyhash cache to each input @@ -635,7 +641,7 @@ function checkInputsForPartialSig(inputs, action) { case transaction_1.Transaction.SIGHASH_SINGLE: case transaction_1.Transaction.SIGHASH_NONE: whitelist.push('addOutput'); - whitelist.push('setSequence'); + whitelist.push('setInputSequence'); break; } if (whitelist.indexOf(action) === -1) { diff --git a/test/fixtures/psbt.json b/test/fixtures/psbt.json index 528e922..22655da 100644 --- a/test/fixtures/psbt.json +++ b/test/fixtures/psbt.json @@ -526,7 +526,7 @@ "result": "cHNidP8BAKYCAAAAAlwKQ3suPWwEJ/zQ9sZsIioOcHKU1KoLMxlMNSXVIkEWAAAAAAD/////YYDJMap+mYgbTrCNAdpWHN+EkKvl+XYao/6co/EQfwMAAAAAAP////8CkF8BAAAAAAAWABRnPBAmVHz2HL+8/1U+QG5L2thjmjhKAAAAAAAAIgAg700yfFRyhWzQnPHIUb/XQqsjlpf4A0uw682pCVWuQ8IAAAAAAAEBKzB1AAAAAAAAIgAgth9oE4cDfC5aV58VgkW5CptHsIxppYzJV8C5kT6aTo8BCG4CSDBFAiEAs7TFGm6o/dpWSb4M/KSu2p7p881KA1uWn0r0wDCa0ckCIAdXlm9k3xWLj2f+j0hIXK+0ew9dc8qoHEL2babYeWliASMhA182CcWcQ9L2zg9j8jlt7OlaIATNDgNFMBKB1J14wzTprAABASuAOAEAAAAAACIAILYfaBOHA3wuWlefFYJFuQqbR7CMaaWMyVfAuZE+mk6PAQhuAkgwRQIhALpc3q2mj2ZiVl6PgFdJKHkPUrpogsF9DhYFZdSshLHrAiBVrEVSzPzLn3EnVXixnWpqsdf2ln4wmYspuXZlJp2KygEjIQNfNgnFnEPS9s4PY/I5bezpWiAEzQ4DRTASgdSdeMM06awAAAA=" } ], - "validateSignatures": { + "validateSignaturesOfInput": { "psbt": "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgf0cwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMASICAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAQEDBAEAAAABBEdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSriIGApWDvzmuCmCXR60Zmt3WNPphCFWdbFzTm0whg/GrluB/ENkMak8AAACAAAAAgAAAAIAiBgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU21xDZDGpPAAAAgAAAAIABAACAAAEBIADC6wsAAAAAF6kUt/X69A49QKWkWbHbNTXyty+pIeiHIgIDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtxHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwEiAgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc0cwRAIgZfRbpZmLWaJ//hp77QFq8fH5DVSzqo90UKpfVqJRA70CIH9yRwOtHtuWaAsoS1bU/8uI9/t1nqu+CKow8puFE4PSAQEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA", "index": 0, "pubkey": "Buffer.from('029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f', 'hex')", diff --git a/test/integration/transactions-psbt.js b/test/integration/transactions-psbt.js index 0e4bfe7..07035ab 100644 --- a/test/integration/transactions-psbt.js +++ b/test/integration/transactions-psbt.js @@ -59,7 +59,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { value: 80000, }); psbt.signInput(0, alice); - psbt.validateSignatures(0); + psbt.validateSignaturesOfInput(0); psbt.finalizeAllInputs(); assert.strictEqual( psbt.extractTransaction().toHex(), @@ -128,8 +128,8 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // Alice signs each input with the respective private keys // signInput and signInputAsync are better // (They take the input index explicitly as the first arg) - signer1.sign(alice1.keys[0]); - signer2.sign(alice2.keys[0]); + signer1.signAllInputs(alice1.keys[0]); + signer2.signAllInputs(alice2.keys[0]); // If your signer object's sign method returns a promise, use the following // await signer2.signAsync(alice2.keys[0]) @@ -147,8 +147,8 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { // 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); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + assert.strictEqual(psbt.validateSignaturesOfInput(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 @@ -183,7 +183,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, alice1.keys[0]); - assert.strictEqual(psbt.validateSignatures(0), true); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); psbt.finalizeAllInputs(); // build and broadcast to the RegTest network @@ -215,13 +215,13 @@ 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.validateSignaturesOfInput(0), true); assert.strictEqual( - psbt.validateSignatures(0, multisig.keys[0].publicKey), + psbt.validateSignaturesOfInput(0, multisig.keys[0].publicKey), true, ); assert.throws(() => { - psbt.validateSignatures(0, multisig.keys[3].publicKey); + psbt.validateSignaturesOfInput(0, multisig.keys[3].publicKey); }, new RegExp('No signatures for this pubkey')); psbt.finalizeAllInputs(); @@ -267,7 +267,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { const tx = new bitcoin.Psbt() .addInputs([inputData, inputData2]) .addOutputs([outputData, outputData2]) - .sign(keyPair) + .signAllInputs(keyPair) .finalizeAllInputs() .extractTransaction(); @@ -300,7 +300,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, p2wpkh.keys[0]); - assert.strictEqual(psbt.validateSignatures(0), true); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); psbt.finalizeAllInputs(); const tx = psbt.extractTransaction(); @@ -340,7 +340,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInput(0, p2wsh.keys[0]); - assert.strictEqual(psbt.validateSignatures(0), true); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); psbt.finalizeAllInputs(); const tx = psbt.extractTransaction(); @@ -383,13 +383,13 @@ 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.validateSignaturesOfInput(0), true); assert.strictEqual( - psbt.validateSignatures(0, p2sh.keys[3].publicKey), + psbt.validateSignaturesOfInput(0, p2sh.keys[3].publicKey), true, ); assert.throws(() => { - psbt.validateSignatures(0, p2sh.keys[1].publicKey); + psbt.validateSignaturesOfInput(0, p2sh.keys[1].publicKey); }, new RegExp('No signatures for this pubkey')); psbt.finalizeAllInputs(); @@ -449,8 +449,8 @@ describe('bitcoinjs-lib (transactions with psbt)', () => { }) .signInputHD(0, hdRoot); // must sign with root!!! - assert.strictEqual(psbt.validateSignatures(0), true); - assert.strictEqual(psbt.validateSignatures(0, childNode.publicKey), true); + assert.strictEqual(psbt.validateSignaturesOfInput(0), true); + assert.strictEqual(psbt.validateSignaturesOfInput(0, childNode.publicKey), true); psbt.finalizeAllInputs(); const tx = psbt.extractTransaction(); diff --git a/test/psbt.js b/test/psbt.js index 18a4c99..2b630d7 100644 --- a/test/psbt.js +++ b/test/psbt.js @@ -249,14 +249,14 @@ describe(`Psbt`, () => { }) }) - describe('sign', () => { + describe('signAllInputs', () => { fixtures.signInput.checks.forEach(f => { if (f.description === 'checks the input exists') return it(f.description, () => { if (f.shouldSign) { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) assert.doesNotThrow(() => { - psbtThatShouldsign.sign( + psbtThatShouldsign.signAllInputs( ECPair.fromWIF(f.shouldSign.WIF), f.shouldSign.sighashTypes || undefined, ) @@ -266,13 +266,13 @@ describe(`Psbt`, () => { if (f.shouldThrow) { const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) assert.throws(() => { - psbtThatShouldThrow.sign( + psbtThatShouldThrow.signAllInputs( ECPair.fromWIF(f.shouldThrow.WIF), f.shouldThrow.sighashTypes || undefined, ) }, new RegExp('No inputs were signed')) assert.throws(() => { - psbtThatShouldThrow.sign() + psbtThatShouldThrow.signAllInputs() }, new RegExp('Need Signer to sign input')) } }) @@ -374,13 +374,13 @@ describe(`Psbt`, () => { }) }) - describe('signHD', () => { + describe('signAllInputsHD', () => { fixtures.signInputHD.checks.forEach(f => { it(f.description, () => { if (f.shouldSign) { const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt) assert.doesNotThrow(() => { - psbtThatShouldsign.signHD( + psbtThatShouldsign.signAllInputsHD( bip32.fromBase58(f.shouldSign.xprv), f.shouldSign.sighashTypes || undefined, ) @@ -390,13 +390,13 @@ describe(`Psbt`, () => { if (f.shouldThrow) { const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt) assert.throws(() => { - psbtThatShouldThrow.signHD( + psbtThatShouldThrow.signAllInputsHD( bip32.fromBase58(f.shouldThrow.xprv), f.shouldThrow.sighashTypes || undefined, ) }, new RegExp('No inputs were signed')) assert.throws(() => { - psbtThatShouldThrow.signHD() + psbtThatShouldThrow.signAllInputsHD() }, new RegExp('Need HDSigner to sign input')) } }) @@ -505,7 +505,7 @@ describe(`Psbt`, () => { }) }) - describe('setSequence', () => { + describe('setInputSequence', () => { it('Sets the sequence number for a given input', () => { const psbt = new Psbt() psbt.addInput({ @@ -515,7 +515,7 @@ describe(`Psbt`, () => { assert.strictEqual(psbt.inputCount, 1) assert.strictEqual(psbt.__CACHE.__TX.ins[0].sequence, 0xffffffff) - psbt.setSequence(0, 0) + psbt.setInputSequence(0, 0) assert.strictEqual(psbt.__CACHE.__TX.ins[0].sequence, 0) }) @@ -527,7 +527,7 @@ describe(`Psbt`, () => { }); assert.throws(() => { - psbt.setSequence(1, 0) + psbt.setInputSequence(1, 0) }, new RegExp('Input index too high')) }) }) @@ -539,7 +539,7 @@ describe(`Psbt`, () => { const notAClone = Object.assign(new Psbt(), psbt) // references still active const clone = psbt.clone() - assert.strictEqual(psbt.validateAllSignatures(), true) + assert.strictEqual(psbt.validateSignaturesOfAllInputs(), true) assert.strictEqual(clone.toBase64(), psbt.toBase64()) assert.strictEqual(clone.toBase64(), notAClone.toBase64()) @@ -561,22 +561,22 @@ describe(`Psbt`, () => { }) }) - describe('validateSignatures', () => { - const f = fixtures.validateSignatures + describe('validateSignaturesOfInput', () => { + const f = fixtures.validateSignaturesOfInput it('Correctly validates a signature', () => { const psbt = Psbt.fromBase64(f.psbt) - assert.strictEqual(psbt.validateSignatures(f.index), true) + assert.strictEqual(psbt.validateSignaturesOfInput(f.index), true) assert.throws(() => { - psbt.validateSignatures(f.nonExistantIndex) + psbt.validateSignaturesOfInput(f.nonExistantIndex) }, new RegExp('No signatures to validate')) }) it('Correctly validates a signature against a pubkey', () => { const psbt = Psbt.fromBase64(f.psbt) - assert.strictEqual(psbt.validateSignatures(f.index, f.pubkey), true) + assert.strictEqual(psbt.validateSignaturesOfInput(f.index, f.pubkey), true) assert.throws(() => { - psbt.validateSignatures(f.index, f.incorrectPubkey) + psbt.validateSignaturesOfInput(f.index, f.incorrectPubkey) }, new RegExp('No signatures for this pubkey')) }) }) @@ -623,7 +623,7 @@ describe(`Psbt`, () => { assert.throws(() => { psbt.setVersion(3) }, new RegExp('Can not modify transaction, signatures exist.')) - psbt.validateSignatures(0) + psbt.validateSignaturesOfInput(0) psbt.finalizeAllInputs() assert.strictEqual( psbt.extractTransaction().toHex(), diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 0778226..863b331 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -58,7 +58,7 @@ const DEFAULT_OPTS: PsbtOpts = { * data for updateOutput. * For a list of what attributes should be what types. Check the bip174 library. * Also, check the integration tests for some examples of usage. - * Signer: There are a few methods. sign and signAsync, which will search all input + * Signer: There are a few methods. signAllInputs and signAsync, which will search all input * information for your pubkey or pubkeyhash, and only sign inputs where it finds * your info. Or you can explicitly sign a specific input with signInput and * signInputAsync. For the async methods you can create a SignerAsync object @@ -69,7 +69,7 @@ const DEFAULT_OPTS: PsbtOpts = { * all sequences, version, locktime, etc. are the same before combining. * Input Finalizer: This role is fairly important. Not only does it need to construct * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. - * Before running `psbt.finalizeAllInputs()` please run `psbt.validateAllSignatures()` + * Before running `psbt.finalizeAllInputs()` please run `psbt.validateSignaturesOfAllInputs()` * Running any finalize method will delete any data in the input(s) that are no longer * needed due to the finalized scripts containing the information. * Transaction Extractor: This role will perform some checks before returning a @@ -168,9 +168,9 @@ export class Psbt { return this; } - setSequence(inputIndex: number, sequence: number): this { + setInputSequence(inputIndex: number, sequence: number): this { check32Bit(sequence); - checkInputsForPartialSig(this.data.inputs, 'setSequence'); + checkInputsForPartialSig(this.data.inputs, 'setInputSequence'); const c = this.__CACHE; if (c.__TX.ins.length <= inputIndex) { throw new Error('Input index too high'); @@ -291,15 +291,15 @@ export class Psbt { return this; } - validateAllSignatures(): boolean { + validateSignaturesOfAllInputs(): boolean { checkForInput(this.data.inputs, 0); // making sure we have at least one const results = range(this.data.inputs.length).map(idx => - this.validateSignatures(idx), + this.validateSignaturesOfInput(idx), ); return results.reduce((final, res) => res === true && final, true); } - validateSignatures(inputIndex: number, pubkey?: Buffer): boolean { + validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean { const input = this.data.inputs[inputIndex]; const partialSig = (input || {}).partialSig; if (!input || !partialSig || partialSig.length < 1) @@ -332,7 +332,7 @@ export class Psbt { return results.every(res => res === true); } - signHD( + signAllInputsHD( hdKeyPair: HDSigner, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { @@ -433,7 +433,7 @@ export class Psbt { ); } - sign( + signAllInputs( keyPair: Signer, sighashTypes: number[] = [Transaction.SIGHASH_ALL], ): this { @@ -812,7 +812,7 @@ function checkInputsForPartialSig(inputs: PsbtInput[], action: string): void { case Transaction.SIGHASH_SINGLE: case Transaction.SIGHASH_NONE: whitelist.push('addOutput'); - whitelist.push('setSequence'); + whitelist.push('setInputSequence'); break; } if (whitelist.indexOf(action) === -1) { diff --git a/types/psbt.d.ts b/types/psbt.d.ts index 9557f79..a133feb 100644 --- a/types/psbt.d.ts +++ b/types/psbt.d.ts @@ -19,7 +19,7 @@ import { Transaction } from './transaction'; * data for updateOutput. * For a list of what attributes should be what types. Check the bip174 library. * Also, check the integration tests for some examples of usage. - * Signer: There are a few methods. sign and signAsync, which will search all input + * Signer: There are a few methods. signAllInputs and signAsync, which will search all input * information for your pubkey or pubkeyhash, and only sign inputs where it finds * your info. Or you can explicitly sign a specific input with signInput and * signInputAsync. For the async methods you can create a SignerAsync object @@ -30,7 +30,7 @@ import { Transaction } from './transaction'; * all sequences, version, locktime, etc. are the same before combining. * Input Finalizer: This role is fairly important. Not only does it need to construct * the input scriptSigs and witnesses, but it SHOULD verify the signatures etc. - * Before running `psbt.finalizeAllInputs()` please run `psbt.validateAllSignatures()` + * Before running `psbt.finalizeAllInputs()` please run `psbt.validateSignaturesOfAllInputs()` * Running any finalize method will delete any data in the input(s) that are no longer * needed due to the finalized scripts containing the information. * Transaction Extractor: This role will perform some checks before returning a @@ -50,7 +50,7 @@ export declare class Psbt { setMaximumFeeRate(satoshiPerByte: number): void; setVersion(version: number): this; setLocktime(locktime: number): this; - setSequence(inputIndex: number, sequence: number): this; + setInputSequence(inputIndex: number, sequence: number): this; addInputs(inputDatas: TransactionInput[]): this; addInput(inputData: TransactionInput): this; addOutputs(outputDatas: TransactionOutput[]): this; @@ -59,13 +59,13 @@ export declare class Psbt { getFeeRate(): number; finalizeAllInputs(): this; finalizeInput(inputIndex: number): this; - validateAllSignatures(): boolean; - validateSignatures(inputIndex: number, pubkey?: Buffer): boolean; - signHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; + validateSignaturesOfAllInputs(): boolean; + validateSignaturesOfInput(inputIndex: number, pubkey?: Buffer): boolean; + signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this; signHDAsync(hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise; signInputHD(inputIndex: number, hdKeyPair: HDSigner, sighashTypes?: number[]): this; signInputHDAsync(inputIndex: number, hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise; - sign(keyPair: Signer, sighashTypes?: number[]): this; + signAllInputs(keyPair: Signer, sighashTypes?: number[]): this; signAsync(keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this; signInputAsync(inputIndex: number, keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise; From d05806fe699b1538aca0fb5bcab241a2f5a3f918 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 15:10:58 +0900 Subject: [PATCH 110/111] Update README, add deprecation warning --- README.md | 8 ++++++++ src/transaction_builder.js | 7 +++++++ test/integration/cltv.js | 1 + test/integration/csv.js | 1 + test/integration/payments.js | 1 + test/integration/transactions.js | 1 + test/transaction_builder.js | 2 ++ ts_src/transaction_builder.ts | 7 +++++++ 8 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 00b5816..6f46004 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,14 @@ The below examples are implemented as integration tests, they should be very eas Otherwise, pull requests are appreciated. Some examples interact (via HTTPS) with a 3rd Party Blockchain Provider (3PBP). +### Warning: Currently the tests use TransactionBuilder, which will be removed in the future (v6.x.x or higher) +We will move towards replacing all instances of TransactionBuilder in the tests with the new Psbt. + +Currently we have a few examples on how to use the newer Psbt class at the following link: +- [Psbt examples](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/transactions-psbt.js) + +The rest of the examples are below (using TransactionBuilder for Transaction creation) + - [Generate a random address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.js) - [Import an address via WIF](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.js) - [Generate a 2-of-3 P2SH multisig address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/addresses.js) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index e63bdd0..a13c481 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -57,6 +57,13 @@ class TransactionBuilder { this.__TX = new transaction_1.Transaction(); this.__TX.version = 2; this.__USE_LOW_R = false; + console.warn( + 'Deprecation Warning: TransactionBuilder will be removed in the future. ' + + '(v6.x.x or later) Please use the Psbt class instead. Examples of usage ' + + 'are available in the transactions-psbt.js integration test file on our ' + + 'Github. A high level explanation is available in the psbt.ts and psbt.js ' + + 'files as well.', + ); } static fromTransaction(transaction, network) { const txb = new TransactionBuilder(network); diff --git a/test/integration/cltv.js b/test/integration/cltv.js index b2fd478..e123652 100644 --- a/test/integration/cltv.js +++ b/test/integration/cltv.js @@ -7,6 +7,7 @@ const bip65 = require('bip65') const alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', regtest) const bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', regtest) +console.warn = () => {} // Silence the Deprecation Warning describe('bitcoinjs-lib (transactions w/ CLTV)', () => { // force update MTP diff --git a/test/integration/csv.js b/test/integration/csv.js index f213c6c..2e133f0 100644 --- a/test/integration/csv.js +++ b/test/integration/csv.js @@ -9,6 +9,7 @@ const alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGk const bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', regtest) const charles = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMSb4Ubnf', regtest) const dave = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsMwS4pqnx', regtest) +console.warn = () => {} // Silence the Deprecation Warning describe('bitcoinjs-lib (transactions w/ CSV)', () => { // force update MTP diff --git a/test/integration/payments.js b/test/integration/payments.js index 256bd00..905eda8 100644 --- a/test/integration/payments.js +++ b/test/integration/payments.js @@ -7,6 +7,7 @@ const keyPairs = [ bitcoin.ECPair.makeRandom({ network: NETWORK }), bitcoin.ECPair.makeRandom({ network: NETWORK }) ] +console.warn = () => {} // Silence the Deprecation Warning async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) { const unspent = await regtestUtils.faucetComplex(prevOutput, 5e4) diff --git a/test/integration/transactions.js b/test/integration/transactions.js index 464460e..a75b6f2 100644 --- a/test/integration/transactions.js +++ b/test/integration/transactions.js @@ -3,6 +3,7 @@ const assert = require('assert') const bitcoin = require('../../') const regtestUtils = require('./_regtest') const regtest = regtestUtils.network +console.warn = () => {} // Silence the Deprecation Warning function rng () { return Buffer.from('YT8dAtK4d16A3P1z+TpwB2jJ4aFH3g9M1EioIBkLEV4=', 'base64') diff --git a/test/transaction_builder.js b/test/transaction_builder.js index a135cca..6374161 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -9,6 +9,8 @@ const Transaction = require('..').Transaction const TransactionBuilder = require('..').TransactionBuilder const NETWORKS = require('../src/networks') +console.warn = () => {} // Silence the Deprecation Warning + const fixtures = require('./fixtures/transaction_builder') function constructSign (f, txb, useOldSignArgs) { diff --git a/ts_src/transaction_builder.ts b/ts_src/transaction_builder.ts index c486285..1edf0f2 100644 --- a/ts_src/transaction_builder.ts +++ b/ts_src/transaction_builder.ts @@ -146,6 +146,13 @@ export class TransactionBuilder { this.__TX = new Transaction(); this.__TX.version = 2; this.__USE_LOW_R = false; + console.warn( + 'Deprecation Warning: TransactionBuilder will be removed in the future. ' + + '(v6.x.x or later) Please use the Psbt class instead. Examples of usage ' + + 'are available in the transactions-psbt.js integration test file on our ' + + 'Github. A high level explanation is available in the psbt.ts and psbt.js ' + + 'files as well.', + ); } setLowR(setting?: boolean): boolean { From 6e447b1f1b4f68fd38f7b23170f5e4ef3ee053ae Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 19 Jul 2019 15:51:38 +0900 Subject: [PATCH 111/111] Refactor: Create cache in constructor --- src/psbt.js | 12 ++++-------- ts_src/psbt.ts | 20 ++++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 31ac164..e975faa 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -62,16 +62,14 @@ const DEFAULT_OPTS = { class Psbt { constructor(opts = {}, data = new bip174_1.Psbt(new PsbtTransaction())) { this.data = data; + // set defaults + this.opts = Object.assign({}, DEFAULT_OPTS, opts); this.__CACHE = { __NON_WITNESS_UTXO_TX_CACHE: [], __NON_WITNESS_UTXO_BUF_CACHE: [], __TX_IN_CACHE: {}, - __TX: new transaction_1.Transaction(), + __TX: this.data.globalMap.unsignedTx.tx, }; - // set defaults - this.opts = Object.assign({}, DEFAULT_OPTS, opts); - const c = this.__CACHE; - c.__TX = this.data.globalMap.unsignedTx.tx; if (this.data.inputs.length === 0) this.setVersion(2); // Make data hidden when enumerating const dpew = (obj, attr, enumerable, writable) => @@ -92,10 +90,8 @@ class Psbt { } static fromBuffer(buffer, opts = {}) { const psbtBase = bip174_1.Psbt.fromBuffer(buffer, transactionFromBuffer); - const tx = psbtBase.globalMap.unsignedTx.tx; const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx; - checkTxForDupeIns(tx, psbt.__CACHE); + checkTxForDupeIns(psbt.__CACHE.__TX, psbt.__CACHE); return psbt; } get inputCount() { diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 863b331..6efbc9d 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -88,20 +88,12 @@ export class Psbt { static fromBuffer(buffer: Buffer, opts: PsbtOptsOptional = {}): Psbt { const psbtBase = PsbtBase.fromBuffer(buffer, transactionFromBuffer); - const tx: Transaction = (psbtBase.globalMap.unsignedTx as PsbtTransaction) - .tx; const psbt = new Psbt(opts, psbtBase); - psbt.__CACHE.__TX = tx; - checkTxForDupeIns(tx, psbt.__CACHE); + checkTxForDupeIns(psbt.__CACHE.__TX, psbt.__CACHE); return psbt; } - private __CACHE: PsbtCache = { - __NON_WITNESS_UTXO_TX_CACHE: [], - __NON_WITNESS_UTXO_BUF_CACHE: [], - __TX_IN_CACHE: {}, - __TX: new Transaction(), - }; + private __CACHE: PsbtCache; private opts: PsbtOpts; constructor( @@ -110,8 +102,12 @@ export class Psbt { ) { // set defaults this.opts = Object.assign({}, DEFAULT_OPTS, opts); - const c = this.__CACHE; - c.__TX = (this.data.globalMap.unsignedTx as PsbtTransaction).tx; + this.__CACHE = { + __NON_WITNESS_UTXO_TX_CACHE: [], + __NON_WITNESS_UTXO_BUF_CACHE: [], + __TX_IN_CACHE: {}, + __TX: (this.data.globalMap.unsignedTx as PsbtTransaction).tx, + }; if (this.data.inputs.length === 0) this.setVersion(2); // Make data hidden when enumerating