mtx: coin selector refactor.

This commit is contained in:
Christopher Jeffrey 2017-01-07 10:45:07 -08:00
parent fb0b2b53d7
commit 0eef277984
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
7 changed files with 137 additions and 141 deletions

View File

@ -3602,7 +3602,7 @@ RPC.prototype._toListTX = co(function* _toListTX(wtx) {
amount: Amount.btc(receive ? received : -sent, true),
label: member.path ? member.path.name : undefined,
vout: index,
confirmations: details.confirmations,
confirmations: details.getDepth(),
blockhash: details.block ? util.revHex(details.block) : null,
blockindex: details.index,
blocktime: details.ts,
@ -3853,8 +3853,13 @@ RPC.prototype.sendmany = co(function* sendmany(args) {
if (args.length > 3)
comment = toString(args[3]);
if (args.length > 4)
subtractFee = toArray(args[4]);
if (args.length > 4) {
subtractFee = args[4];
if (typeof subtractFee !== 'boolean') {
if (!util.isNumber(subtractFee))
throw new RPCError('Invalid parameter.');
}
}
keys = Object.keys(sendTo);
@ -3879,7 +3884,7 @@ RPC.prototype.sendmany = co(function* sendmany(args) {
outputs: outputs,
subtractFee: subtractFee,
account: account,
confirmations: minconf
depth: minconf
};
tx = yield wallet.send(options);

View File

@ -296,11 +296,17 @@ HTTPServer.prototype._init = function _init() {
}
if (params.confirmations != null) {
options.confirmations = Number(params.confirmations);
enforce(util.isNumber(options.confirmations),
options.depth = Number(params.confirmations);
enforce(util.isNumber(options.depth),
'Confirmations must be a number.');
}
if (params.depth != null) {
options.depth = Number(params.depth);
enforce(util.isNumber(options.depth),
'Depth must be a number.');
}
if (params.fee)
options.fee = Amount.value(params.fee);

View File

@ -104,6 +104,9 @@ Coin.prototype.getDepth = function getDepth(height) {
if (this.height === -1)
return 0;
if (height === -1)
return 0;
if (height < this.height)
return 0;

View File

@ -36,9 +36,7 @@ var opcodes = Script.opcodes;
* @param {Number?} options.changeIndex
* @param {Input[]?} options.inputs
* @param {Output[]?} options.outputs
* @property {Number} version - Transaction version. Note that BCoin reads
* versions as unsigned even though they are signed at the protocol level.
* This value will never be negative.
* @property {Number} version - Transaction version.
* @property {Number} flag - Flag field for segregated witness.
* Always non-zero (1 if not present).
* @property {Input[]} inputs
@ -63,24 +61,6 @@ function MTX(options) {
util.inherits(MTX, TX);
/**
* Minimum fee to start with
* during coin selection.
* @const {Amount}
* @default
*/
MTX.MIN_FEE = 10000;
/**
* Maximum fee to allow
* after coin selection.
* @const {Amount}
* @default
*/
MTX.MAX_FEE = consensus.COIN / 10;
/**
* Inject properties from options object.
* @private
@ -147,20 +127,21 @@ MTX.prototype.clone = function clone() {
/**
* Add an input to the transaction.
* @example
* tx.addInput({ prevout: { hash: ... }, sequence: ... });
* tx.addInput(prev, prevIndex);
* tx.addInput(coin);
* tx.addInput(bcoin.coin.fromTX(prev, prevIndex));
* @param {Object|TX|Coin} options - Options object, transaction, or coin.
* @param {Number?} index - Input of output if `options` is a TX.
* tx.addInput({ prevout: { hash: ... }, script: ... });
* tx.addInput(tx, index);
* tx.addInput(new Outpoint(hash, index));
* tx.addInput(Coin.fromTX(prev, prevIndex, -1));
* @param {TX|Coin|Outpoint|Input|Object} coin
* @param {Number?} index - Input of output if `coin` is a TX.
* @param {Number?} height - Coin height if `coin` is a TX.
*/
MTX.prototype.addInput = function addInput(coin, index) {
MTX.prototype.addInput = function addInput(coin, index, height) {
var input = new Input();
if (coin instanceof TX) {
input.fromTX(coin, index);
coin = Coin.fromTX(coin, index, -1);
coin = Coin.fromTX(coin, index, height || -1);
}
if (coin instanceof Coin) {
@ -187,10 +168,9 @@ MTX.prototype.addInput = function addInput(coin, index) {
* @example
* tx.addOutput({ address: ..., value: 100000 });
* tx.addOutput({ address: ..., value: Amount.value('0.1') });
* tx.addOutput(receivingWallet, Amount.value('0.1'));
* @param {Wallet|KeyRing|Object} obj - Wallet, Address,
* or options (see {@link Script.createOutputScript} for options).
* @param {Amount?} value - Only needs to be present for non-options.
* tx.addOutput(address, Amount.value('0.1'));
* @param {KeyRing|Base58Address|Address|Script|Output|Object} options
* @param {Amount?} value - Only needs to be present for non-output options.
*/
MTX.prototype.addOutput = function addOutput(options, value) {
@ -805,15 +785,11 @@ MTX.prototype.isInputSigned = function isInputSigned(index, coin) {
assert(input, 'Input does not exist.');
assert(coin, 'No coin passed.');
// Get the prevout's script
prev = coin.script;
// Script length, needed for multisig
vector = input.script;
redeem = false;
// We need to grab the redeem script when
// signing p2sh transactions.
// Grab redeem script if possible.
if (prev.isScripthash()) {
prev = input.script.getRedeem();
if (!prev)
@ -1072,6 +1048,7 @@ MTX.prototype.estimateSize = co(function* estimateSize(estimate) {
continue;
}
// Call out to the custom estimator.
if (estimate) {
size = yield estimate(prev);
if (size !== -1) {
@ -1083,18 +1060,18 @@ MTX.prototype.estimateSize = co(function* estimateSize(estimate) {
// P2SH
if (prev.isScripthash()) {
// varint size
total += 2;
total += 1;
// 2-of-3 multisig input
total += 257;
total += 149;
continue;
}
// P2WSH
if (prev.isWitnessScripthash()) {
// varint-len
// varint-items-len
size += 1;
// 2-of-3 multisig input
size += 257;
size += 149;
// vsize
size = (size + scale - 1) / scale | 0;
total += size;
@ -1140,19 +1117,7 @@ MTX.prototype.selectCoins = function selectCoins(coins, options) {
*/
MTX.prototype.subtractFee = function subtractFee(fee, index) {
var i, min, output, hash, addrs;
if (Buffer.isBuffer(index) || typeof index === 'string')
index = [index];
if (Array.isArray(index)) {
addrs = [];
for (i = 0; i < index.length; i++) {
hash = Address.getHash(index[i]);
if (hash)
addrs.push(hash);
}
}
var i, min, output;
if (typeof index === 'number') {
output = this.outputs[index];
@ -1174,24 +1139,13 @@ MTX.prototype.subtractFee = function subtractFee(fee, index) {
output = this.outputs[i];
min = fee + output.getDustThreshold();
if (addrs) {
hash = output.getHash();
if (!hash)
continue;
if (util.indexOf(addrs, hash) === -1)
continue;
}
if (output.value >= min) {
output.value -= fee;
break;
return;
}
}
if (i === this.outputs.length)
throw new Error('Could not subtract fee.');
throw new Error('Could not subtract fee.');
};
/**
@ -1202,7 +1156,7 @@ MTX.prototype.subtractFee = function subtractFee(fee, index) {
*/
MTX.prototype.fund = co(function* fund(coins, options) {
var i, select, change;
var i, select, coin, change;
assert(options, 'Options are required.');
assert(options.changeAddress, 'Change address is required.');
@ -1223,6 +1177,7 @@ MTX.prototype.fund = co(function* fund(coins, options) {
change = new Output();
change.value = select.change;
change.script.fromAddress(select.changeAddress);
change.mutable = true;
if (change.isDust(policy.MIN_RELAY)) {
// Do nothing. Change is added to fee.
@ -1261,15 +1216,19 @@ MTX.prototype.sortMembers = function sortMembers() {
/**
* Avoid fee sniping.
* @param {Number?} [height=network.height] - Current chain height.
* @param {Number} - Current chain height.
* @see bitcoin/src/wallet/wallet.cpp
*/
MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) {
assert(typeof height === 'number', 'Must pass in height.');
if ((Math.random() * 10 | 0) === 0)
height = Math.max(0, height - (Math.random() * 100 | 0));
if (util.random(0, 10) === 0) {
height -= util.random(0, 100);
if (height < 0)
height = 0;
}
this.setLocktime(height);
};
@ -1282,10 +1241,13 @@ MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) {
MTX.prototype.setLocktime = function setLocktime(locktime) {
var i, input;
assert(util.isUInt32(locktime), 'Locktime must be a uint32.');
assert(this.inputs.length > 0, 'Cannot set sequence with no inputs.');
for (i = 0; i < this.inputs.length; i++) {
input = this.inputs[i];
if (input.sequence === 0xffffffff)
input.sequence = 0xffffffff - 1;
input.sequence = 0xfffffffe;
}
this.locktime = locktime;
@ -1302,6 +1264,7 @@ MTX.prototype.setSequence = function setSequence(index, locktime, seconds) {
var input = this.inputs[index];
assert(input, 'Input does not exist.');
assert(util.isUInt32(locktime), 'Locktime must be a uint32.');
this.version = 2;
@ -1316,16 +1279,17 @@ MTX.prototype.setSequence = function setSequence(index, locktime, seconds) {
};
/**
* Mark inputs and outputs as mutable.
* Mark outputs as mutable.
* @private
* @param {Boolean} flag
*/
MTX.prototype._mutable = function _mutable(value) {
MTX.prototype._mutable = function _mutable(flag) {
var i, output;
for (i = 0; i < this.outputs.length; i++) {
output = this.outputs[i];
output.mutable = value;
output.mutable = flag;
}
return this;
@ -1433,7 +1397,7 @@ function CoinSelector(tx, options) {
this.index = 0;
this.chosen = [];
this.change = 0;
this.fee = 0;
this.fee = CoinSelector.MIN_FEE;
this.selection = 'age';
this.shouldSubtract = false;
@ -1441,7 +1405,7 @@ function CoinSelector(tx, options) {
this.height = -1;
this.depth = -1;
this.hardFee = -1;
this.rate = MTX.MIN_FEE;
this.rate = CoinSelector.FEE_RATE;
this.maxFee = -1;
this.round = false;
this.changeAddress = null;
@ -1453,6 +1417,33 @@ function CoinSelector(tx, options) {
this.fromOptions(options);
}
/**
* Default fee rate
* for coin selection.
* @const {Amount}
* @default
*/
CoinSelector.FEE_RATE = 10000;
/**
* Minimum fee to start with
* during coin selection.
* @const {Amount}
* @default
*/
CoinSelector.MIN_FEE = 10000;
/**
* Maximum fee to allow
* after coin selection.
* @const {Amount}
* @default
*/
CoinSelector.MAX_FEE = consensus.COIN / 10;
/**
* Initialize selector options.
* @param {Object} options
@ -1542,7 +1533,7 @@ CoinSelector.prototype.init = function init(coins) {
this.index = 0;
this.chosen = [];
this.change = 0;
this.fee = 0;
this.fee = CoinSelector.MIN_FEE;
this.tx.inputs.length = 0;
switch (this.selection) {
@ -1587,7 +1578,7 @@ CoinSelector.prototype.isFull = function isFull() {
*/
CoinSelector.prototype.isSpendable = function isSpendable(coin) {
var conf;
var depth;
if (this.height === -1)
return true;
@ -1598,22 +1589,17 @@ CoinSelector.prototype.isSpendable = function isSpendable(coin) {
if (this.height + 1 < coin.height + consensus.COINBASE_MATURITY)
return false;
return true;
}
if (this.depth > 0) {
if (coin.height === -1)
return this.depth <= 0;
if (this.depth === -1)
return true;
conf = this.height - coin.height;
depth = coin.getDepth(this.height);
if (conf < 0)
return false;
conf += 1;
if (conf < this.depth)
return false;
}
if (depth < this.depth)
return false;
return true;
};
@ -1627,20 +1613,22 @@ CoinSelector.prototype.isSpendable = function isSpendable(coin) {
CoinSelector.prototype.getFee = function getFee(size) {
var fee;
if (this.round)
if (this.round) {
// This is mostly here for testing.
// i.e. A fee rounded to the nearest
// kb is easier to predict ahead of time.
fee = policy.getRoundFee(size, this.rate);
else
} else {
fee = policy.getMinFee(size, this.rate);
}
if (fee > MTX.MAX_FEE)
fee = MTX.MAX_FEE;
return fee;
return Math.min(fee, CoinSelector.MAX_FEE);
};
/**
* Fund the transaction with more
* coins if the `total` was updated.
* coins if the `output value + fee`
* total was updated.
*/
CoinSelector.prototype.fund = function fund() {
@ -1652,16 +1640,12 @@ CoinSelector.prototype.fund = function fund() {
if (!this.isSpendable(coin))
continue;
// Add new inputs until TX will have enough
// funds to cover both minimum post cost
// and fee.
this.tx.addInput(coin);
this.chosen.push(coin);
if (this.selection === 'all')
continue;
// Stop once we're full.
if (this.isFull())
break;
}
@ -1676,10 +1660,16 @@ CoinSelector.prototype.fund = function fund() {
CoinSelector.prototype.select = co(function* select(coins) {
this.init(coins);
if (this.hardFee !== -1)
this.selectHard(this.hardFee);
else
if (this.hardFee !== -1) {
this.selectHard();
} else {
// This is potentially asynchronous:
// it may invoke the size estimator
// required for redeem scripts (we
// may be calling out to a wallet
// or something similar).
yield this.selectEstimate();
}
if (!this.isFull()) {
// Still failing to get enough funds.
@ -1700,28 +1690,28 @@ CoinSelector.prototype.select = co(function* select(coins) {
*/
CoinSelector.prototype.selectEstimate = co(function* selectEstimate() {
var output = new Output();
var size;
var change, size;
// Initial fee.
this.fee = MTX.MIN_FEE;
// Transfer `total` funds maximum.
// Set minimum fee and do
// an initial round of funding.
this.fee = CoinSelector.MIN_FEE;
this.fund();
// Add dummy output (for `change`) to
// calculate maximum TX size.
// Add dummy output for change.
change = new Output();
change.mutable = true;
if (this.changeAddress) {
output.script.fromAddress(this.changeAddress);
change.script.fromAddress(this.changeAddress);
} else {
// In case we don't have a change address,
// we use a fake p2pkh output to gauge size.
output.script.fromPubkeyhash(encoding.ZERO_HASH160);
change.script.fromPubkeyhash(encoding.ZERO_HASH160);
}
this.tx.outputs.push(output);
this.tx.outputs.push(change);
// Keep recalculating fee and funding
// Keep recalculating the fee and funding
// until we reach some sort of equilibrium.
do {
size = yield this.tx.estimateSize(this.estimate);
@ -1739,17 +1729,10 @@ CoinSelector.prototype.selectEstimate = co(function* selectEstimate() {
/**
* Initiate selection based on a hard fee.
* @param {Amount} fee
*/
CoinSelector.prototype.selectHard = function selectHard(fee) {
// Initial fee.
this.fee = fee;
if (this.fee > MTX.MAX_FEE)
this.fee = MTX.MAX_FEE;
// Transfer `total` funds maximum.
CoinSelector.prototype.selectHard = function selectHard() {
this.fee = Math.min(this.hardFee, CoinSelector.MAX_FEE);
this.fund();
};
@ -1818,7 +1801,7 @@ function sortOutputs(a, b) {
if (res !== 0)
return res;
return util.cmp(a.script.toRaw(), b.script.toRaw());
return util.cmp(a.script.raw, b.script.raw);
}
/*

View File

@ -1496,7 +1496,6 @@ TX.prototype.isStandard = function isStandard(ret) {
*/
TX.prototype.hasStandardInputs = function hasStandardInputs(view) {
var maxSigops = policy.MAX_SCRIPTHASH_SIGOPS;
var i, input, coin, redeem;
if (this.isCoinbase())
@ -1518,7 +1517,7 @@ TX.prototype.hasStandardInputs = function hasStandardInputs(view) {
if (!redeem)
return false;
if (redeem.getSigops(true) > maxSigops)
if (redeem.getSigops(true) > policy.MAX_P2SH_SIGOPS)
return false;
continue;
@ -1650,7 +1649,7 @@ TX.prototype.getWitnessStandard = function getWitnessStandard(view) {
ret = BAD_NONSTD_P2WSH;
}
redeem = new Script(redeem);
redeem = Script.fromRaw(redeem);
if (redeem.isPubkey()) {
if (input.witness.length - 1 !== 1)

View File

@ -89,7 +89,7 @@ exports.FREE_THRESHOLD = consensus.COIN * 144 / 250;
* @default
*/
exports.MAX_SCRIPTHASH_SIGOPS = 15;
exports.MAX_P2SH_SIGOPS = 15;
/**
* Max serialized nulldata size (policy).

View File

@ -1434,14 +1434,14 @@ Wallet.prototype._fund = co(function* fund(tx, options) {
yield tx.fund(coins, {
selection: options.selection,
round: options.round,
depth: options.confirmations,
depth: options.depth,
hardFee: options.hardFee,
subtractFee: options.subtractFee,
changeAddress: account.change.getAddress(),
height: this.db.state.height,
rate: rate,
maxFee: options.maxFee,
estimate: this.estimate.bind(this)
estimate: this.estimateSize.bind(this)
});
});
@ -1472,7 +1472,7 @@ Wallet.prototype.getAccountByAddress = co(function* getAccountByAddress(address)
* @returns {Number}
*/
Wallet.prototype.estimate = co(function* estimate(prev) {
Wallet.prototype.estimateSize = co(function* estimateSize(prev) {
var scale = consensus.WITNESS_SCALE_FACTOR;
var address = prev.getAddress();
var account = yield this.getAccountByAddress(address);