compare both utxo selection algorithms
This commit is contained in:
parent
d3faad0639
commit
504b52d695
|
@ -93,5 +93,25 @@ Utils.formatAmount = function(satoshis, unit, opts) {
|
||||||
return addSeparators(amount, opts.thousandsSeparator || ',', opts.decimalSeparator || '.', u.minDecimals);
|
return addSeparators(amount, opts.thousandsSeparator || ',', opts.decimalSeparator || '.', u.minDecimals);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Utils.formatAmountInBtc = function(amount) {
|
||||||
|
return Utils.formatAmount(amount, 'btc') + 'btc';
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.formatUtxos = function(utxos) {
|
||||||
|
if (_.isEmpty(utxos)) return 'none';
|
||||||
|
return _.map([].concat(utxos), function(i) {
|
||||||
|
var amount = Utils.formatAmountInBtc(i.satoshis);
|
||||||
|
var confirmations = i.confirmations ? i.confirmations + 'c' : 'u';
|
||||||
|
return amount + '/' + confirmations;
|
||||||
|
}).join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.formatRatio = function(ratio) {
|
||||||
|
return (ratio * 100.).toFixed(4) + '%';
|
||||||
|
};
|
||||||
|
|
||||||
|
Utils.formatSize = function(size) {
|
||||||
|
return (size / 1000.).toFixed(4) + 'kB';
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = Utils;
|
module.exports = Utils;
|
||||||
|
|
|
@ -1376,27 +1376,6 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatAmount(amount) {
|
|
||||||
return Utils.formatAmount(amount, 'btc') + 'btc';
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatInputs(inputs) {
|
|
||||||
if (_.isEmpty(inputs)) return 'none';
|
|
||||||
return _.map([].concat(inputs), function(i) {
|
|
||||||
var amount = formatAmount(i.satoshis);
|
|
||||||
var confirmations = i.confirmations ? i.confirmations + 'c' : 'u';
|
|
||||||
return amount + '/' + confirmations;
|
|
||||||
}).join(', ');
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatRatio(ratio) {
|
|
||||||
return (ratio * 100.).toFixed(4) + '%';
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatSize(size) {
|
|
||||||
return (size / 1000.).toFixed(4) + 'kB';
|
|
||||||
};
|
|
||||||
|
|
||||||
function select(utxos, cb) {
|
function select(utxos, cb) {
|
||||||
var txpAmount = txp.getTotalAmount();
|
var txpAmount = txp.getTotalAmount();
|
||||||
var baseTxpSize = txp.getEstimatedSize();
|
var baseTxpSize = txp.getEstimatedSize();
|
||||||
|
@ -1407,16 +1386,16 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
var totalValueInUtxos = _.sum(utxos, 'satoshis');
|
var totalValueInUtxos = _.sum(utxos, 'satoshis');
|
||||||
var netValueInUtxos = totalValueInUtxos - baseTxpFee - (utxos.length * feePerInput);
|
var netValueInUtxos = totalValueInUtxos - baseTxpFee - (utxos.length * feePerInput);
|
||||||
if (totalValueInUtxos < txpAmount) {
|
if (totalValueInUtxos < txpAmount) {
|
||||||
log.debug('Total value in all utxos (' + formatAmount(totalValueInUtxos) + ') is insufficient to cover for txp amount (' + formatAmount(txpAmount) + ')');
|
log.debug('Total value in all utxos (' + Utils.formatAmountInBtc(totalValueInUtxos) + ') is insufficient to cover for txp amount (' + Utils.formatAmountInBtc(txpAmount) + ')');
|
||||||
return cb(Errors.INSUFFICIENT_FUNDS);
|
return cb(Errors.INSUFFICIENT_FUNDS);
|
||||||
}
|
}
|
||||||
if (netValueInUtxos < txpAmount) {
|
if (netValueInUtxos < txpAmount) {
|
||||||
log.debug('Value after fees in all utxos (' + formatAmount(netValueInUtxos) + ') is insufficient to cover for txp amount (' + formatAmount(txpAmount) + ')');
|
log.debug('Value after fees in all utxos (' + Utils.formatAmountInBtc(netValueInUtxos) + ') is insufficient to cover for txp amount (' + Utils.formatAmountInBtc(txpAmount) + ')');
|
||||||
return cb(Errors.INSUFFICIENT_FUNDS_FOR_FEE);
|
return cb(Errors.INSUFFICIENT_FUNDS_FOR_FEE);
|
||||||
}
|
}
|
||||||
|
|
||||||
var bigInputThreshold = txpAmount * Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + (baseTxpFee + feePerInput);
|
var bigInputThreshold = txpAmount * Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + (baseTxpFee + feePerInput);
|
||||||
log.debug('Big input threshold ' + formatAmount(bigInputThreshold));
|
log.debug('Big input threshold ' + Utils.formatAmountInBtc(bigInputThreshold));
|
||||||
|
|
||||||
var partitions = _.partition(utxos, function(utxo) {
|
var partitions = _.partition(utxos, function(utxo) {
|
||||||
return utxo.satoshis > bigInputThreshold;
|
return utxo.satoshis > bigInputThreshold;
|
||||||
|
@ -1427,41 +1406,41 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
return -utxo.satoshis;
|
return -utxo.satoshis;
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug('Considering ' + bigInputs.length + ' big inputs (' + formatInputs(bigInputs) + ')');
|
log.debug('Considering ' + bigInputs.length + ' big inputs (' + Utils.formatUtxos(bigInputs) + ')');
|
||||||
log.debug('Considering ' + smallInputs.length + ' small inputs (' + formatInputs(smallInputs) + ')');
|
log.debug('Considering ' + smallInputs.length + ' small inputs (' + Utils.formatUtxos(smallInputs) + ')');
|
||||||
|
|
||||||
var total = 0;
|
var total = 0;
|
||||||
var selected = [];
|
var selected = [];
|
||||||
var error;
|
var error;
|
||||||
|
|
||||||
_.each(smallInputs, function(input, i) {
|
_.each(smallInputs, function(input, i) {
|
||||||
log.debug('Input #' + i + ': ' + formatInputs(input));
|
log.debug('Input #' + i + ': ' + Utils.formatUtxos(input));
|
||||||
|
|
||||||
if (input.satoshis < feePerInput) {
|
if (input.satoshis < feePerInput) {
|
||||||
log.debug('The input does not cover the extra fees (' + formatAmount(feePerInput) + ')');
|
log.debug('The input does not cover the extra fees (' + Utils.formatAmountInBtc(feePerInput) + ')');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputAmount = input.satoshis - feePerInput;
|
var inputAmount = input.satoshis - feePerInput;
|
||||||
log.debug('The input contributes ' + formatAmount(inputAmount));
|
log.debug('The input contributes ' + Utils.formatAmountInBtc(inputAmount));
|
||||||
|
|
||||||
selected.push(input);
|
selected.push(input);
|
||||||
|
|
||||||
var txpSize = baseTxpSize + selected.length * sizePerInput;
|
var txpSize = baseTxpSize + selected.length * sizePerInput;
|
||||||
var txpFee = baseTxpFee + selected.length * feePerInput;
|
var txpFee = baseTxpFee + selected.length * feePerInput;
|
||||||
|
|
||||||
log.debug('Tx size: ' + formatSize(txpSize) + ', Tx fee: ' + formatAmount(txpFee));
|
log.debug('Tx size: ' + Utils.formatSize(txpSize) + ', Tx fee: ' + Utils.formatAmountInBtc(txpFee));
|
||||||
|
|
||||||
var amountVsFeeRatio = txpFee / txpAmount;
|
var amountVsFeeRatio = txpFee / txpAmount;
|
||||||
var singleInputFeeVsFeeRatio = txpFee / (baseTxpFee + feePerInput);
|
var singleInputFeeVsFeeRatio = txpFee / (baseTxpFee + feePerInput);
|
||||||
var amountVsUtxoRatio = inputAmount / txpAmount;
|
var amountVsUtxoRatio = inputAmount / txpAmount;
|
||||||
|
|
||||||
log.debug('Tx amount/Fee: ' + formatRatio(amountVsFeeRatio) + ' (max: ' + formatRatio(Defaults.UTXO_SELECTION_MAX_TX_AMOUNT_VS_FEE_FACTOR) + ')');
|
log.debug('Tx amount/Fee: ' + Utils.formatRatio(amountVsFeeRatio) + ' (max: ' + Utils.formatRatio(Defaults.UTXO_SELECTION_MAX_TX_AMOUNT_VS_FEE_FACTOR) + ')');
|
||||||
log.debug('Single-input fee/Multi-input fee: ' + formatRatio(singleInputFeeVsFeeRatio) + ' (max: ' + formatRatio(Defaults.UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR) + ')' + ' loses wrt single-input tx: ' + formatAmount((selected.length - 1) * feePerInput));
|
log.debug('Single-input fee/Multi-input fee: ' + Utils.formatRatio(singleInputFeeVsFeeRatio) + ' (max: ' + Utils.formatRatio(Defaults.UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR) + ')' + ' loses wrt single-input tx: ' + Utils.formatAmountInBtc((selected.length - 1) * feePerInput));
|
||||||
log.debug('Tx amount/input amount:' + formatRatio(amountVsUtxoRatio) + ' (min: ' + formatRatio(Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR) + ')');
|
log.debug('Tx amount/input amount:' + Utils.formatRatio(amountVsUtxoRatio) + ' (min: ' + Utils.formatRatio(Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR) + ')');
|
||||||
|
|
||||||
if (txpSize / 1000. > Defaults.MAX_TX_SIZE_IN_KB) {
|
if (txpSize / 1000. > Defaults.MAX_TX_SIZE_IN_KB) {
|
||||||
log.debug('Breaking because tx size (' + formatSize(txpSize) + ') is too big (max: ' + formatSize(Defaults.MAX_TX_SIZE_IN_KB * 1000.) + ')');
|
log.debug('Breaking because tx size (' + Utils.formatSize(txpSize) + ') is too big (max: ' + Utils.formatSize(Defaults.MAX_TX_SIZE_IN_KB * 1000.) + ')');
|
||||||
error = Errors.TX_MAX_SIZE_EXCEEDED;
|
error = Errors.TX_MAX_SIZE_EXCEEDED;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1480,17 +1459,17 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
total += inputAmount;
|
total += inputAmount;
|
||||||
log.debug('Cumuled total so far: ' + formatAmount(total));
|
log.debug('Cumuled total so far: ' + Utils.formatAmountInBtc(total));
|
||||||
|
|
||||||
if (total >= txpAmount) return false;
|
if (total >= txpAmount) return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (total < txpAmount) {
|
if (total < txpAmount) {
|
||||||
log.debug('Could not reach Txp total (' + formatAmount(txpAmount) + '), still missing: ' + formatAmount(txpAmount - total));
|
log.debug('Could not reach Txp total (' + Utils.formatAmountInBtc(txpAmount) + '), still missing: ' + Utils.formatAmountInBtc(txpAmount - total));
|
||||||
|
|
||||||
selected = [];
|
selected = [];
|
||||||
if (!_.isEmpty(bigInputs)) {
|
if (!_.isEmpty(bigInputs)) {
|
||||||
log.debug('Using big input: ', formatInputs(_.first(bigInputs)));
|
log.debug('Using big input: ', Utils.formatUtxos(_.first(bigInputs)));
|
||||||
var input = _.first(bigInputs);
|
var input = _.first(bigInputs);
|
||||||
total = input.satoshis;
|
total = input.satoshis;
|
||||||
selected = [input];
|
selected = [input];
|
||||||
|
@ -1505,7 +1484,7 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
return cb(null, selected);
|
return cb(null, selected);
|
||||||
};
|
};
|
||||||
|
|
||||||
log.debug('Selecting inputs for a ' + formatAmount(txp.getTotalAmount()) + ' txp');
|
log.debug('Selecting inputs for a ' + Utils.formatAmountInBtc(txp.getTotalAmount()) + ' txp');
|
||||||
|
|
||||||
self._getUtxosForCurrentWallet(null, function(err, utxos) {
|
self._getUtxosForCurrentWallet(null, function(err, utxos) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
@ -1533,7 +1512,7 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
utxos = _.filter(utxos, 'confirmations');
|
utxos = _.filter(utxos, 'confirmations');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug('Considering ' + utxos.length + ' utxos (' + formatInputs(utxos) + ')');
|
log.debug('Considering ' + utxos.length + ' utxos (' + Utils.formatUtxos(utxos) + ')');
|
||||||
|
|
||||||
var groups = [6, 1];
|
var groups = [6, 1];
|
||||||
if (!txp.excludeUnconfirmedUtxos) groups.push(0);
|
if (!txp.excludeUnconfirmedUtxos) groups.push(0);
|
||||||
|
@ -1559,7 +1538,7 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug('Candidate utxos: ' + formatInputs(candidateUtxos));
|
log.debug('Candidate utxos: ' + Utils.formatUtxos(candidateUtxos));
|
||||||
|
|
||||||
lastGroupLength = candidateUtxos.length;
|
lastGroupLength = candidateUtxos.length;
|
||||||
|
|
||||||
|
@ -1573,7 +1552,7 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
selectionError = null;
|
selectionError = null;
|
||||||
inputs = selected;
|
inputs = selected;
|
||||||
|
|
||||||
log.debug('Selected inputs from this group: ' + formatInputs(inputs));
|
log.debug('Selected inputs from this group: ' + Utils.formatUtxos(inputs));
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
@ -1585,7 +1564,7 @@ WalletService.prototype._selectTxInputs2 = function(txp, utxosToExclude, cb) {
|
||||||
var err = self._checkTxAndEstimateFee(txp);
|
var err = self._checkTxAndEstimateFee(txp);
|
||||||
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
log.debug('Successfully built transaction. Total fees: ', formatAmount(txp.fee));
|
log.debug('Successfully built transaction. Total fees: ', Utils.formatAmountInBtc(txp.fee));
|
||||||
} else {
|
} else {
|
||||||
log.warn('Error building transaction', err);
|
log.warn('Error building transaction', err);
|
||||||
}
|
}
|
||||||
|
@ -1775,7 +1754,7 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
|
||||||
txp.version = '1.0.1';
|
txp.version = '1.0.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
self._selectTxInputs2(txp, opts.utxosToExclude, function(err) {
|
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
$.checkState(txp.inputs);
|
$.checkState(txp.inputs);
|
||||||
|
@ -1817,6 +1796,22 @@ WalletService.prototype.createTxLegacy = function(opts, cb) {
|
||||||
WalletService.prototype.createTx = function(opts, cb) {
|
WalletService.prototype.createTx = function(opts, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
function logStatistics(prefix, txp) {
|
||||||
|
var totalAmount = txp.getTotalAmount();
|
||||||
|
var inputs = _.sortBy(_.map(txp.inputs, function(input) {
|
||||||
|
return _.pick(input, 'satoshis', 'confirmations');
|
||||||
|
}), 'satoshis');
|
||||||
|
var totalLocked = _.sum(inputs, 'satoshis');
|
||||||
|
var overhead = totalLocked - totalAmount;
|
||||||
|
|
||||||
|
log.info(prefix, 'TXP ID: ' + txp.id);
|
||||||
|
log.info(prefix, 'Total amount: ' + Utils.formatAmountInBtc(totalAmount));
|
||||||
|
log.info(prefix, 'Fee: ' + Utils.formatAmountInBtc(txp.fee) + ' (per KB: ' + Utils.formatAmountInBtc(txp.feePerKb) + ')');
|
||||||
|
log.info(prefix, 'Exclude unconfirmed: ' + txp.excludeUnconfirmedUtxos);
|
||||||
|
log.info(prefix, 'Total locked: ' + Utils.formatAmountInBtc(totalLocked) + ', Overhead: ' + Utils.formatAmountInBtc(overhead) + ' (' + Utils.formatRatio(overhead / totalAmount) + ')');
|
||||||
|
log.info(prefix, 'Inputs: ', Utils.formatUtxos(inputs));
|
||||||
|
};
|
||||||
|
|
||||||
if (!Utils.checkRequired(opts, ['outputs', 'feePerKb']))
|
if (!Utils.checkRequired(opts, ['outputs', 'feePerKb']))
|
||||||
return cb(new ClientError('Required argument missing'));
|
return cb(new ClientError('Required argument missing'));
|
||||||
|
|
||||||
|
@ -1859,8 +1854,20 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
var txp = Model.TxProposal.create(txOpts);
|
var txp = Model.TxProposal.create(txOpts);
|
||||||
|
|
||||||
self._selectTxInputs2(txp, opts.utxosToExclude, function(err) {
|
self._selectTxInputs2(txp, opts.utxosToExclude, function(err) {
|
||||||
|
if (err) {
|
||||||
|
log.error('Could not select inputs using new algorithm', err);
|
||||||
|
} else {
|
||||||
|
logStatistics('NEW_UTXO_SEL', txp);
|
||||||
|
}
|
||||||
|
|
||||||
|
txp.setInputs([]);
|
||||||
|
txp.fee = null;
|
||||||
|
|
||||||
|
self._selectTxInputs(txp, opts.utxosToExclude, function(err) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
logStatistics('OLD_UTXO_SEL', txp);
|
||||||
|
|
||||||
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) {
|
self.storage.storeAddressAndWallet(wallet, txp.changeAddress, function(err) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
@ -1873,6 +1880,7 @@ WalletService.prototype.createTx = function(opts, cb) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
|
WalletService.prototype._verifyRequestPubKey = function(requestPubKey, signature, xPubKey) {
|
||||||
|
|
|
@ -3169,7 +3169,7 @@ describe('Wallet service', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UTXO Selection', function() {
|
describe.skip('UTXO Selection', function() {
|
||||||
var server, wallet;
|
var server, wallet;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
helpers.createAndJoinWallet(2, 3, function(s, w) {
|
||||||
|
@ -3189,8 +3189,6 @@ describe('Wallet service', function() {
|
||||||
feePerKb: 10e2,
|
feePerKb: 10e2,
|
||||||
};
|
};
|
||||||
server.createTx(txOpts, function(err, txp) {
|
server.createTx(txOpts, function(err, txp) {
|
||||||
console.log('*** [server.js ln3193] err:', err); // TODO
|
|
||||||
|
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(txp);
|
should.exist(txp);
|
||||||
txp.inputs.length.should.equal(1);
|
txp.inputs.length.should.equal(1);
|
||||||
|
|
Loading…
Reference in New Issue