From 14eeb309df25c59ebf01f05078036425c33b9a65 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 5 Jul 2019 12:28:04 +0900 Subject: [PATCH] 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 {};