From 8987c0d8704264ad5de0af31048e1ef97d65702b Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 8 Jan 2017 01:35:48 -0800 Subject: [PATCH] wallet/mtx: fix fee checking and refactor some mtx functions. --- lib/primitives/input.js | 24 ++++++++ lib/primitives/mtx.js | 98 +++++++++++++++++++++++++++++--- lib/wallet/wallet.js | 123 ++++++++++++++++++++-------------------- test/wallet-test.js | 2 +- 4 files changed, 176 insertions(+), 71 deletions(-) diff --git a/lib/primitives/input.js b/lib/primitives/input.js index 79df7172..70396a49 100644 --- a/lib/primitives/input.js +++ b/lib/primitives/input.js @@ -395,6 +395,30 @@ Input.fromRaw = function fromRaw(data, enc) { return new Input().fromRaw(data); }; +/** + * Inject properties from outpoint. + * @private + * @param {Outpoint} outpoint + */ + +Input.prototype.fromOutpoint = function fromOutpoint(outpoint) { + assert(typeof outpoint.hash === 'string'); + assert(typeof outpoint.index === 'number'); + this.prevout.hash = outpoint.hash; + this.prevout.index = outpoint.index; + return this; +}; + +/** + * Instantiate input from outpoint. + * @param {Outpoint} + * @returns {Input} + */ + +Input.fromOutpoint = function fromOutpoint(outpoint) { + return new Input().fromOutpoint(outpoint); +}; + /** * Inject properties from coin. * @private diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 768c46b7..a3bee6b8 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -124,6 +124,62 @@ MTX.prototype.clone = function clone() { return new MTX(this); }; +/** + * Add an outpoint as an input. + * @param {Outpoint} outpoint + */ + +MTX.prototype.addOutpoint = function addOutpoint(outpoint) { + var input = Input.fromOutpoint(outpoint); + this.inputs.push(input); + return this; +}; + +/** + * Add a coin as an input. + * @param {Coin} coin + */ + +MTX.prototype.addCoin = function addCoin(coin) { + var input; + + assert(coin instanceof Coin, 'Cannot add non-coin.'); + + input = Input.fromCoin(coin); + + this.inputs.push(input); + + this.view.addCoin(coin); + + return this; +}; + +/** + * Add a transaction as an input. + * @param {TX} tx + * @param {Number} index + * @param {Number?} height + */ + +MTX.prototype.addTX = function addTX(tx, index, height) { + var input, coin; + + assert(tx instanceof TX, 'Cannot add non-transaction.'); + + if (height == null) + height = -1; + + input = Input.fromTX(tx, index); + + this.inputs.push(input); + + coin = Coin.fromTX(tx, index, height); + + this.view.addCoin(coin); + + return this; +}; + /** * Add an input to the transaction. * @example @@ -1367,6 +1423,27 @@ MTX.prototype.toTX = function toTX() { return new TX(this); }; +/** + * Inject properties from transaction. + * @private + * @param {TX} tx + * @returns {MTX} + */ + +MTX.prototype.fromTX = function fromTX(tx, view) { + return this.fromRaw(tx.toRaw()); +}; + +/** + * Instantiate MTX from TX. + * @param {TX} tx + * @returns {MTX} + */ + +MTX.fromTX = function fromTX(tx) { + return new MTX().fromTX(tx); +}; + /** * Test whether an object is an MTX. * @param {Object} obj @@ -1787,25 +1864,30 @@ function sortRandom(a, b) { function sortInputs(a, b) { var ahash = util.revHex(a.prevout.hash); var bhash = util.revHex(b.prevout.hash); - var res = util.strcmp(ahash, bhash); + var cmp = util.strcmp(ahash, bhash); - if (res !== 0) - return res; + if (cmp !== 0) + return cmp; return a.prevout.index - b.prevout.index; } function sortOutputs(a, b) { - var res = a.value - b.value; + var cmp = a.value - b.value; - if (res !== 0) - return res; + if (cmp !== 0) + return cmp; - return util.cmp(a.script.raw, b.script.raw); + return util.cmp(a.script.toRaw(), b.script.toRaw()); } /* * Expose */ -module.exports = MTX; +exports = MTX; +exports.MTX = MTX; +exports.Selector = CoinSelector; +exports.FundingError = FundingError; + +module.exports = exports; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 1fff6168..9e8b6ba7 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1363,7 +1363,7 @@ Wallet.prototype._importAddress = co(function* importAddress(acct, address) { * transaction size, calculate fee, and add a change output. * @see MTX#selectCoins * @see MTX#fill - * @param {MTX} tx - _Must_ be a mutable transaction. + * @param {MTX} mtx - _Must_ be a mutable transaction. * @param {Object?} options * @param {(String|Number)?} options.account - If no account is * specified, coins from the entire wallet will be filled. @@ -1382,10 +1382,10 @@ Wallet.prototype._importAddress = co(function* importAddress(acct, address) { * fee from existing outputs rather than adding more inputs. */ -Wallet.prototype.fund = co(function* fund(tx, options, force) { +Wallet.prototype.fund = co(function* fund(mtx, options, force) { var unlock = yield this.fundLock.lock(force); try { - return yield this._fund(tx, options); + return yield this._fund(mtx, options); } finally { unlock(); } @@ -1398,7 +1398,7 @@ Wallet.prototype.fund = co(function* fund(tx, options, force) { * @see MTX#fill */ -Wallet.prototype._fund = co(function* fund(tx, options) { +Wallet.prototype._fund = co(function* fund(mtx, options) { var rate, account, coins; if (!options) @@ -1431,7 +1431,7 @@ Wallet.prototype._fund = co(function* fund(tx, options) { // Don't use any locked coins. coins = this.txdb.filterLocked(coins); - yield tx.fund(coins, { + yield mtx.fund(coins, { selection: options.selection, round: options.round, depth: options.depth, @@ -1443,6 +1443,8 @@ Wallet.prototype._fund = co(function* fund(tx, options) { maxFee: options.maxFee, estimate: this.estimateSize.bind(this) }); + + assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.'); }); /** @@ -1550,48 +1552,43 @@ Wallet.prototype.estimateSize = co(function* estimateSize(prev) { Wallet.prototype.createTX = co(function* createTX(options, force) { var outputs = options.outputs; - var i, tx, output, total; + var mtx = new MTX(); + var i, output, total; assert(Array.isArray(outputs), 'Outputs must be an array.'); - - if (outputs.length === 0) - throw new Error('No outputs available.'); - - // Create mutable tx - tx = new MTX(); + assert(outputs.length > 0, 'No outputs available.'); // Add the outputs for (i = 0; i < outputs.length; i++) { output = new Output(outputs[i]); + output.mutable = true; if (output.isDust()) throw new Error('Output is dust.'); - tx.outputs.push(output); + mtx.outputs.push(output); } // Fill the inputs with unspents - yield this.fund(tx, options, force); + yield this.fund(mtx, options, force); // Sort members a la BIP69 - tx.sortMembers(); + mtx.sortMembers(); // Set the locktime to target value. if (options.locktime != null) - tx.setLocktime(options.locktime); + mtx.setLocktime(options.locktime); - if (!tx.isSane()) - throw new Error('CheckTransaction failed.'); + // Consensus sanity checks. + assert(mtx.isSane(), 'TX failed sanity check.'); + assert(mtx.checkInputs(this.db.state.height + 1), 'CheckInputs failed.'); - if (!tx.checkInputs(this.db.state.height + 1)) - throw new Error('CheckInputs failed.'); - - total = yield this.template(tx); + total = yield this.template(mtx); if (total === 0) throw new Error('Templating failed.'); - return tx; + return mtx; }); /** @@ -1630,11 +1627,9 @@ Wallet.prototype._send = co(function* send(options, passphrase) { if (!mtx.isSigned()) throw new Error('TX could not be fully signed.'); - assert(mtx.getFee() <= MTX.MAX_FEE, 'TX exceeds maxfee.'); - tx = mtx.toTX(); - // Sanity checks. + // Policy sanity checks. if (tx.getSigopsCost(mtx.view) > policy.MAX_TX_SIGOPS_COST) throw new Error('TX exceeds policy sigops.'); @@ -1661,11 +1656,16 @@ Wallet.prototype._send = co(function* send(options, passphrase) { Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase) { var wtx = yield this.getTX(hash); - var i, tx, view, oldFee, fee, path, input, output, change; + var i, tx, mtx, view, oldFee, fee, path, input, output, change; + + assert(util.isUInt32(rate), 'Rate must be a number.'); if (!wtx) throw new Error('Transaction not found.'); + if (wtx.height !== -1) + throw new Error('Transaction is confirmed.'); + tx = wtx.tx; if (tx.isCoinbase()) @@ -1676,31 +1676,28 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase) if (!tx.hasCoins(view)) throw new Error('Not all coins available.'); - if (!util.isUInt32(rate)) - throw new Error('Rate must be a number.'); - oldFee = tx.getFee(view); fee = tx.getMinFee(null, rate); - if (fee > MTX.MAX_FEE) - fee = MTX.MAX_FEE; + if (fee > MTX.Selector.MAX_FEE) + fee = MTX.Selector.MAX_FEE; if (oldFee >= fee) throw new Error('Fee is not increasing.'); - tx = MTX.fromRaw(tx.toRaw()); - tx.view = view; + mtx = MTX.fromTX(tx); + mtx.view = view; - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; + for (i = 0; i < mtx.inputs.length; i++) { + input = mtx.inputs[i]; input.script.length = 0; input.script.compile(); input.witness.length = 0; input.witness.compile(); } - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; + for (i = 0; i < mtx.outputs.length; i++) { + output = mtx.outputs[i]; path = yield this.getPath(output.getAddress()); if (!path) @@ -1708,7 +1705,7 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase) if (path.branch === 1) { change = output; - tx.changeIndex = i; + mtx.changeIndex = i; break; } } @@ -1718,7 +1715,7 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase) change.value += oldFee; - if (tx.getFee() !== 0) + if (mtx.getFee() !== 0) throw new Error('Arithmetic error for change.'); change.value -= fee; @@ -1727,18 +1724,20 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase) throw new Error('Fee is too high.'); if (change.isDust()) { - tx.outputs.splice(tx.changeIndex, 1); - tx.changeIndex = -1; + mtx.outputs.splice(mtx.changeIndex, 1); + mtx.changeIndex = -1; } - yield this.sign(tx, passphrase); + yield this.sign(mtx, passphrase); - if (!tx.isSigned()) + if (!mtx.isSigned()) throw new Error('TX could not be fully signed.'); - tx = tx.toTX(); + tx = mtx.toTX(); - this.logger.debug('Increasing fee for wallet tx (%s): %s', this.id, tx.txid()); + this.logger.debug( + 'Increasing fee for wallet tx (%s): %s', + this.id, tx.txid()); yield this.db.addTX(tx); yield this.db.send(tx); @@ -1768,18 +1767,18 @@ Wallet.prototype.resend = co(function* resend() { /** * Derive necessary addresses for signing a transaction. - * @param {TX|Input} tx + * @param {MTX} mtx * @param {Number?} index - Input index. * @returns {Promise} - Returns {@link WalletKey}[]. */ -Wallet.prototype.deriveInputs = co(function* deriveInputs(tx) { +Wallet.prototype.deriveInputs = co(function* deriveInputs(mtx) { var rings = []; var i, paths, path, account, ring; - assert(tx.mutable); + assert(mtx.mutable); - paths = yield this.getInputPaths(tx); + paths = yield this.getInputPaths(mtx); for (i = 0; i < paths.length; i++) { path = paths[i]; @@ -1860,20 +1859,20 @@ Wallet.prototype.getPrivateKey = co(function* getPrivateKey(address, passphrase) /** * Map input addresses to paths. - * @param {TX} tx + * @param {MTX} mtx * @returns {Promise} - Returns {@link Path}[]. */ -Wallet.prototype.getInputPaths = co(function* getInputPaths(tx) { +Wallet.prototype.getInputPaths = co(function* getInputPaths(mtx) { var paths = []; var i, hashes, hash, path; - assert(tx.mutable); + assert(mtx.mutable); - if (!tx.hasCoins()) + if (!mtx.hasCoins()) throw new Error('Not all coins available.'); - hashes = tx.getInputHashes('hex'); + hashes = mtx.getInputHashes('hex'); for (i = 0; i < hashes.length; i++) { hash = hashes[i]; @@ -2058,14 +2057,14 @@ Wallet.prototype.getRedeem = co(function* getRedeem(hash) { * Build input scripts templates for a transaction (does not * sign, only creates signature slots). Only builds scripts * for inputs that are redeemable by this wallet. - * @param {MTX} tx + * @param {MTX} mtx * @returns {Promise} - Returns Number * (total number of scripts built). */ -Wallet.prototype.template = co(function* template(tx) { - var rings = yield this.deriveInputs(tx); - return tx.template(rings); +Wallet.prototype.template = co(function* template(mtx) { + var rings = yield this.deriveInputs(mtx); + return mtx.template(rings); }); /** @@ -2077,7 +2076,7 @@ Wallet.prototype.template = co(function* template(tx) { * of inputs scripts built and signed). */ -Wallet.prototype.sign = co(function* sign(tx, passphrase) { +Wallet.prototype.sign = co(function* sign(mtx, passphrase) { var rings; if (this.watchOnly) @@ -2085,9 +2084,9 @@ Wallet.prototype.sign = co(function* sign(tx, passphrase) { yield this.unlock(passphrase); - rings = yield this.deriveInputs(tx); + rings = yield this.deriveInputs(mtx); - return yield tx.signAsync(rings); + return yield mtx.signAsync(rings); }); /** diff --git a/test/wallet-test.js b/test/wallet-test.js index 5f97669e..d699f58e 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -613,7 +613,7 @@ describe('Wallet', function() { tx.addOutput(to.getAddress(), 5460); cost = tx.getOutputValue(); - total = cost * MTX.MIN_FEE; + total = cost * 10000; coins1 = yield w1.getCoins(); coins2 = yield w2.getCoins();