Skip to content

Commit 31f8f9e

Browse files
committed
merge bitcoin#22009: Decide which coin selection solution to use based on waste metric
1 parent 24695e1 commit 31f8f9e

File tree

8 files changed

+224
-49
lines changed

8 files changed

+224
-49
lines changed

src/dummywallet.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const
3838
{
3939
argsman.AddHiddenArgs({
4040
"-avoidpartialspends",
41+
"-consolidatefeerate=<amt>",
4142
"-createwalletbackups=<n>",
4243
"-disablewallet",
4344
"-instantsendnotify=<cmd>",

src/wallet/coinselection.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,30 @@ CAmount OutputGroup::GetSelectionAmount() const
402402
{
403403
return m_subtract_fee_outputs ? m_value : effective_value;
404404
}
405+
406+
CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
407+
{
408+
// This function should not be called with empty inputs as that would mean the selection failed
409+
assert(!inputs.empty());
410+
411+
// Always consider the cost of spending an input now vs in the future.
412+
CAmount waste = 0;
413+
CAmount selected_effective_value = 0;
414+
for (const CInputCoin& coin : inputs) {
415+
waste += coin.m_fee - coin.m_long_term_fee;
416+
selected_effective_value += use_effective_value ? coin.effective_value : coin.txout.nValue;
417+
}
418+
419+
if (change_cost) {
420+
// Consider the cost of making change and spending it in the future
421+
// If we aren't making change, the caller should've set change_cost to 0
422+
assert(change_cost > 0);
423+
waste += change_cost;
424+
} else {
425+
// When we are not making change (change_cost == 0), consider the excess we are throwing away to fees
426+
assert(selected_effective_value >= target);
427+
waste += selected_effective_value - target;
428+
}
429+
430+
return waste;
431+
}

src/wallet/coinselection.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ struct OutputGroup
164164
CAmount GetSelectionAmount() const;
165165
};
166166

167+
/** Compute the waste for this result given the cost of change
168+
* and the opportunity cost of spending these inputs now vs in the future.
169+
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
170+
* If no change, waste = excess + inputs * (effective_feerate - long_term_feerate)
171+
* where excess = selected_effective_value - target
172+
* change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size
173+
*
174+
* @param[in] inputs The selected inputs
175+
* @param[in] change_cost The cost of creating change and spending it in the future. Only used if there is change. Must be 0 if there is no change.
176+
* @param[in] target The amount targeted by the coin selection algorithm.
177+
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
178+
* @return The waste
179+
*/
180+
[[nodiscard]] CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
181+
167182
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret);
168183

169184
// Original coin selection algorithm as a fallback

src/wallet/init.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const WalletInitInterface& g_wallet_init_interface = WalletInit();
5757
void WalletInit::AddWalletOptions(ArgsManager& argsman) const
5858
{
5959
argsman.AddArg("-avoidpartialspends", strprintf("Group outputs by address, selecting many (possibly all) or none, instead of selecting on a per-output basis. Privacy is improved as addresses are mostly swept with fewer transactions and outputs are aggregated in clean change addresses. It may result in higher fees due to less optimal coin selection caused by this added limitation and possibly a larger-than-necessary number of inputs being used. Always enabled for wallets with \"avoid_reuse\" enabled, otherwise default: %u.", DEFAULT_AVOIDPARTIALSPENDS), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
60+
argsman.AddArg("-consolidatefeerate=<amt>", strprintf("The maximum feerate (in %s/kvB) at which transaction building may use more inputs than strictly necessary so that the wallet's UTXO pool can be reduced (default: %s).", CURRENCY_UNIT, FormatMoney(DEFAULT_CONSOLIDATE_FEERATE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
6061
argsman.AddArg("-createwalletbackups=<n>", strprintf("Number of automatic wallet backups (default: %u)", nWalletBackups), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
6162
argsman.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
6263
#if HAVE_SYSTEM

src/wallet/spend.cpp

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -364,17 +364,44 @@ bool CWallet::AttemptSelection(const CAmount& nTargetValue, const CoinEligibilit
364364
{
365365
setCoinsRet.clear();
366366
nValueRet = 0;
367+
// Vector of results for use with waste calculation
368+
// In order: calculated waste, selected inputs, selected input value (sum of input values)
369+
// TODO: Use a struct representing the selection result
370+
std::vector<std::tuple<CAmount, std::set<CInputCoin>, CAmount>> results;
367371

368372
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
369373
std::vector<OutputGroup> positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */);
370-
if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) {
371-
return true;
374+
std::set<CInputCoin> bnb_coins;
375+
CAmount bnb_value;
376+
if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, bnb_coins, bnb_value)) {
377+
const auto waste = GetSelectionWaste(bnb_coins, /* cost of change */ CAmount(0), nTargetValue, !coin_selection_params.m_subtract_fee_outputs);
378+
results.emplace_back(std::make_tuple(waste, std::move(bnb_coins), bnb_value));
372379
}
380+
373381
// The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here.
374382
std::vector<OutputGroup> all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */);
375383
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
376384
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
377-
return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee);
385+
std::set<CInputCoin> knapsack_coins;
386+
CAmount knapsack_value;
387+
if (KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, knapsack_coins, knapsack_value, nCoinType == CoinType::ONLY_FULLY_MIXED, m_default_max_tx_fee)) {
388+
const auto waste = GetSelectionWaste(knapsack_coins, coin_selection_params.m_cost_of_change, nTargetValue + coin_selection_params.m_change_fee, !coin_selection_params.m_subtract_fee_outputs);
389+
results.emplace_back(std::make_tuple(waste, std::move(knapsack_coins), knapsack_value));
390+
}
391+
392+
if (results.size() == 0) {
393+
// No solution found
394+
return false;
395+
}
396+
397+
// Choose the result with the least waste
398+
// If the waste is the same, choose the one which spends more inputs.
399+
const auto& best_result = std::min_element(results.begin(), results.end(), [](const auto& a, const auto& b) {
400+
return std::get<0>(a) < std::get<0>(b) || (std::get<0>(a) == std::get<0>(b) && std::get<1>(a).size() > std::get<1>(b).size());
401+
});
402+
setCoinsRet = std::get<1>(*best_result);
403+
nValueRet = std::get<2>(*best_result);
404+
return true;
378405
}
379406

380407
bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet, const CCoinControl& coin_control, CoinSelectionParams& coin_selection_params) const
@@ -608,6 +635,9 @@ bool CWallet::CreateTransactionInternal(
608635
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
609636
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
610637

638+
// Set the long term feerate estimate to the wallet's consolidate feerate
639+
coin_selection_params.m_long_term_feerate = m_consolidate_feerate;
640+
611641
CAmount recipients_sum = 0;
612642
ReserveDestination reservedest(this);
613643
const bool sort_bip69{nChangePosInOut == -1};
@@ -680,11 +710,6 @@ bool CWallet::CreateTransactionInternal(
680710
return false;
681711
}
682712

683-
// Get long term estimate
684-
CCoinControl cc_temp;
685-
cc_temp.m_confirm_target = chain().estimateMaxBlocks();
686-
coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr);
687-
688713
// Calculate the cost of change
689714
// Cost of change is the cost of creating the change output + cost of spending the change output in the future.
690715
// For creating the change output now, we use the effective feerate.

0 commit comments

Comments
 (0)