diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 3935e363..21f708c7 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -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); diff --git a/lib/http/server.js b/lib/http/server.js index b9c3ecf8..27ef088f 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -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); diff --git a/lib/primitives/coin.js b/lib/primitives/coin.js index d0cfec34..4ea2ec10 100644 --- a/lib/primitives/coin.js +++ b/lib/primitives/coin.js @@ -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; diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index b249a303..768c46b7 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -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); } /* diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index 7543eac1..509d990d 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -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) diff --git a/lib/protocol/policy.js b/lib/protocol/policy.js index 25d492a0..892a60d3 100644 --- a/lib/protocol/policy.js +++ b/lib/protocol/policy.js @@ -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). diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index f7b62282..1fff6168 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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);