wallet/mtx: fix fee checking and refactor some mtx functions.

This commit is contained in:
Christopher Jeffrey 2017-01-08 01:35:48 -08:00
parent fa8ec48bc5
commit 8987c0d870
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 176 additions and 71 deletions

View File

@ -395,6 +395,30 @@ Input.fromRaw = function fromRaw(data, enc) {
return new Input().fromRaw(data); 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. * Inject properties from coin.
* @private * @private

View File

@ -124,6 +124,62 @@ MTX.prototype.clone = function clone() {
return new MTX(this); 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. * Add an input to the transaction.
* @example * @example
@ -1367,6 +1423,27 @@ MTX.prototype.toTX = function toTX() {
return new TX(this); 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. * Test whether an object is an MTX.
* @param {Object} obj * @param {Object} obj
@ -1787,25 +1864,30 @@ function sortRandom(a, b) {
function sortInputs(a, b) { function sortInputs(a, b) {
var ahash = util.revHex(a.prevout.hash); var ahash = util.revHex(a.prevout.hash);
var bhash = util.revHex(b.prevout.hash); var bhash = util.revHex(b.prevout.hash);
var res = util.strcmp(ahash, bhash); var cmp = util.strcmp(ahash, bhash);
if (res !== 0) if (cmp !== 0)
return res; return cmp;
return a.prevout.index - b.prevout.index; return a.prevout.index - b.prevout.index;
} }
function sortOutputs(a, b) { function sortOutputs(a, b) {
var res = a.value - b.value; var cmp = a.value - b.value;
if (res !== 0) if (cmp !== 0)
return res; return cmp;
return util.cmp(a.script.raw, b.script.raw); return util.cmp(a.script.toRaw(), b.script.toRaw());
} }
/* /*
* Expose * Expose
*/ */
module.exports = MTX; exports = MTX;
exports.MTX = MTX;
exports.Selector = CoinSelector;
exports.FundingError = FundingError;
module.exports = exports;

View File

@ -1363,7 +1363,7 @@ Wallet.prototype._importAddress = co(function* importAddress(acct, address) {
* transaction size, calculate fee, and add a change output. * transaction size, calculate fee, and add a change output.
* @see MTX#selectCoins * @see MTX#selectCoins
* @see MTX#fill * @see MTX#fill
* @param {MTX} tx - _Must_ be a mutable transaction. * @param {MTX} mtx - _Must_ be a mutable transaction.
* @param {Object?} options * @param {Object?} options
* @param {(String|Number)?} options.account - If no account is * @param {(String|Number)?} options.account - If no account is
* specified, coins from the entire wallet will be filled. * 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. * 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); var unlock = yield this.fundLock.lock(force);
try { try {
return yield this._fund(tx, options); return yield this._fund(mtx, options);
} finally { } finally {
unlock(); unlock();
} }
@ -1398,7 +1398,7 @@ Wallet.prototype.fund = co(function* fund(tx, options, force) {
* @see MTX#fill * @see MTX#fill
*/ */
Wallet.prototype._fund = co(function* fund(tx, options) { Wallet.prototype._fund = co(function* fund(mtx, options) {
var rate, account, coins; var rate, account, coins;
if (!options) if (!options)
@ -1431,7 +1431,7 @@ Wallet.prototype._fund = co(function* fund(tx, options) {
// Don't use any locked coins. // Don't use any locked coins.
coins = this.txdb.filterLocked(coins); coins = this.txdb.filterLocked(coins);
yield tx.fund(coins, { yield mtx.fund(coins, {
selection: options.selection, selection: options.selection,
round: options.round, round: options.round,
depth: options.depth, depth: options.depth,
@ -1443,6 +1443,8 @@ Wallet.prototype._fund = co(function* fund(tx, options) {
maxFee: options.maxFee, maxFee: options.maxFee,
estimate: this.estimateSize.bind(this) 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) { Wallet.prototype.createTX = co(function* createTX(options, force) {
var outputs = options.outputs; 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.'); assert(Array.isArray(outputs), 'Outputs must be an array.');
assert(outputs.length > 0, 'No outputs available.');
if (outputs.length === 0)
throw new Error('No outputs available.');
// Create mutable tx
tx = new MTX();
// Add the outputs // Add the outputs
for (i = 0; i < outputs.length; i++) { for (i = 0; i < outputs.length; i++) {
output = new Output(outputs[i]); output = new Output(outputs[i]);
output.mutable = true;
if (output.isDust()) if (output.isDust())
throw new Error('Output is dust.'); throw new Error('Output is dust.');
tx.outputs.push(output); mtx.outputs.push(output);
} }
// Fill the inputs with unspents // Fill the inputs with unspents
yield this.fund(tx, options, force); yield this.fund(mtx, options, force);
// Sort members a la BIP69 // Sort members a la BIP69
tx.sortMembers(); mtx.sortMembers();
// Set the locktime to target value. // Set the locktime to target value.
if (options.locktime != null) if (options.locktime != null)
tx.setLocktime(options.locktime); mtx.setLocktime(options.locktime);
if (!tx.isSane()) // Consensus sanity checks.
throw new Error('CheckTransaction failed.'); 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)) total = yield this.template(mtx);
throw new Error('CheckInputs failed.');
total = yield this.template(tx);
if (total === 0) if (total === 0)
throw new Error('Templating failed.'); throw new Error('Templating failed.');
return tx; return mtx;
}); });
/** /**
@ -1630,11 +1627,9 @@ Wallet.prototype._send = co(function* send(options, passphrase) {
if (!mtx.isSigned()) if (!mtx.isSigned())
throw new Error('TX could not be fully signed.'); throw new Error('TX could not be fully signed.');
assert(mtx.getFee() <= MTX.MAX_FEE, 'TX exceeds maxfee.');
tx = mtx.toTX(); tx = mtx.toTX();
// Sanity checks. // Policy sanity checks.
if (tx.getSigopsCost(mtx.view) > policy.MAX_TX_SIGOPS_COST) if (tx.getSigopsCost(mtx.view) > policy.MAX_TX_SIGOPS_COST)
throw new Error('TX exceeds policy sigops.'); 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) { Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase) {
var wtx = yield this.getTX(hash); 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) if (!wtx)
throw new Error('Transaction not found.'); throw new Error('Transaction not found.');
if (wtx.height !== -1)
throw new Error('Transaction is confirmed.');
tx = wtx.tx; tx = wtx.tx;
if (tx.isCoinbase()) if (tx.isCoinbase())
@ -1676,31 +1676,28 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase)
if (!tx.hasCoins(view)) if (!tx.hasCoins(view))
throw new Error('Not all coins available.'); throw new Error('Not all coins available.');
if (!util.isUInt32(rate))
throw new Error('Rate must be a number.');
oldFee = tx.getFee(view); oldFee = tx.getFee(view);
fee = tx.getMinFee(null, rate); fee = tx.getMinFee(null, rate);
if (fee > MTX.MAX_FEE) if (fee > MTX.Selector.MAX_FEE)
fee = MTX.MAX_FEE; fee = MTX.Selector.MAX_FEE;
if (oldFee >= fee) if (oldFee >= fee)
throw new Error('Fee is not increasing.'); throw new Error('Fee is not increasing.');
tx = MTX.fromRaw(tx.toRaw()); mtx = MTX.fromTX(tx);
tx.view = view; mtx.view = view;
for (i = 0; i < tx.inputs.length; i++) { for (i = 0; i < mtx.inputs.length; i++) {
input = tx.inputs[i]; input = mtx.inputs[i];
input.script.length = 0; input.script.length = 0;
input.script.compile(); input.script.compile();
input.witness.length = 0; input.witness.length = 0;
input.witness.compile(); input.witness.compile();
} }
for (i = 0; i < tx.outputs.length; i++) { for (i = 0; i < mtx.outputs.length; i++) {
output = tx.outputs[i]; output = mtx.outputs[i];
path = yield this.getPath(output.getAddress()); path = yield this.getPath(output.getAddress());
if (!path) if (!path)
@ -1708,7 +1705,7 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase)
if (path.branch === 1) { if (path.branch === 1) {
change = output; change = output;
tx.changeIndex = i; mtx.changeIndex = i;
break; break;
} }
} }
@ -1718,7 +1715,7 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase)
change.value += oldFee; change.value += oldFee;
if (tx.getFee() !== 0) if (mtx.getFee() !== 0)
throw new Error('Arithmetic error for change.'); throw new Error('Arithmetic error for change.');
change.value -= fee; change.value -= fee;
@ -1727,18 +1724,20 @@ Wallet.prototype.increaseFee = co(function* increaseFee(hash, rate, passphrase)
throw new Error('Fee is too high.'); throw new Error('Fee is too high.');
if (change.isDust()) { if (change.isDust()) {
tx.outputs.splice(tx.changeIndex, 1); mtx.outputs.splice(mtx.changeIndex, 1);
tx.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.'); 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.addTX(tx);
yield this.db.send(tx); yield this.db.send(tx);
@ -1768,18 +1767,18 @@ Wallet.prototype.resend = co(function* resend() {
/** /**
* Derive necessary addresses for signing a transaction. * Derive necessary addresses for signing a transaction.
* @param {TX|Input} tx * @param {MTX} mtx
* @param {Number?} index - Input index. * @param {Number?} index - Input index.
* @returns {Promise} - Returns {@link WalletKey}[]. * @returns {Promise} - Returns {@link WalletKey}[].
*/ */
Wallet.prototype.deriveInputs = co(function* deriveInputs(tx) { Wallet.prototype.deriveInputs = co(function* deriveInputs(mtx) {
var rings = []; var rings = [];
var i, paths, path, account, ring; 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++) { for (i = 0; i < paths.length; i++) {
path = paths[i]; path = paths[i];
@ -1860,20 +1859,20 @@ Wallet.prototype.getPrivateKey = co(function* getPrivateKey(address, passphrase)
/** /**
* Map input addresses to paths. * Map input addresses to paths.
* @param {TX} tx * @param {MTX} mtx
* @returns {Promise} - Returns {@link Path}[]. * @returns {Promise} - Returns {@link Path}[].
*/ */
Wallet.prototype.getInputPaths = co(function* getInputPaths(tx) { Wallet.prototype.getInputPaths = co(function* getInputPaths(mtx) {
var paths = []; var paths = [];
var i, hashes, hash, path; var i, hashes, hash, path;
assert(tx.mutable); assert(mtx.mutable);
if (!tx.hasCoins()) if (!mtx.hasCoins())
throw new Error('Not all coins available.'); throw new Error('Not all coins available.');
hashes = tx.getInputHashes('hex'); hashes = mtx.getInputHashes('hex');
for (i = 0; i < hashes.length; i++) { for (i = 0; i < hashes.length; i++) {
hash = hashes[i]; hash = hashes[i];
@ -2058,14 +2057,14 @@ Wallet.prototype.getRedeem = co(function* getRedeem(hash) {
* Build input scripts templates for a transaction (does not * Build input scripts templates for a transaction (does not
* sign, only creates signature slots). Only builds scripts * sign, only creates signature slots). Only builds scripts
* for inputs that are redeemable by this wallet. * for inputs that are redeemable by this wallet.
* @param {MTX} tx * @param {MTX} mtx
* @returns {Promise} - Returns Number * @returns {Promise} - Returns Number
* (total number of scripts built). * (total number of scripts built).
*/ */
Wallet.prototype.template = co(function* template(tx) { Wallet.prototype.template = co(function* template(mtx) {
var rings = yield this.deriveInputs(tx); var rings = yield this.deriveInputs(mtx);
return tx.template(rings); return mtx.template(rings);
}); });
/** /**
@ -2077,7 +2076,7 @@ Wallet.prototype.template = co(function* template(tx) {
* of inputs scripts built and signed). * of inputs scripts built and signed).
*/ */
Wallet.prototype.sign = co(function* sign(tx, passphrase) { Wallet.prototype.sign = co(function* sign(mtx, passphrase) {
var rings; var rings;
if (this.watchOnly) if (this.watchOnly)
@ -2085,9 +2084,9 @@ Wallet.prototype.sign = co(function* sign(tx, passphrase) {
yield this.unlock(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);
}); });
/** /**

View File

@ -613,7 +613,7 @@ describe('Wallet', function() {
tx.addOutput(to.getAddress(), 5460); tx.addOutput(to.getAddress(), 5460);
cost = tx.getOutputValue(); cost = tx.getOutputValue();
total = cost * MTX.MIN_FEE; total = cost * 10000;
coins1 = yield w1.getCoins(); coins1 = yield w1.getCoins();
coins2 = yield w2.getCoins(); coins2 = yield w2.getCoins();