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);
};
/**
* 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

View File

@ -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;

View File

@ -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);
});
/**

View File

@ -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();