Skip to content

Commit 70676e4

Browse files
committed
Merge bitcoin/bitcoin#22009: wallet: Decide which coin selection solution to use based on waste metric
86beee0 Use waste metric for deciding which selection to use (Andrew Chow) b3df0ca tests: Test GetSelectionWaste (Andrew Chow) 4f5ad43 Add waste metric calculation function (Andrew Chow) 935b3dd scripted-diff: tests: Use KnapsackSolver directly (Andrew Chow) 6a023a6 tests: Add KnapsackGroupOutputs helper function (Andrew Chow) d5069fc tests: Use SelectCoinsBnB directly instead of AttemptSelection (Andrew Chow) 54de7b4 Allow the long term feerate to be configured, default of 10 sat/vb (Andrew Chow) Pull request description: Branch and Bound introduced a metric that we call waste. This metric is used as part of bounding the search tree, but it can be generalized to all coin selection solutions, including those with change. As such, this PR introduces the waste metric at a higher level so that we can run both of our coin selection algorithms (BnB and KnapsackSolver) and choose the one which has the least waste. In the event that both find a solution with the same change, we choose the one that spends more inputs. Also this PR sets the long term feerate to 10 sat/vb rather than using the 1008 block estimate. This allows the long term feerate to be the feerate that we switch between consolidating and optimizing for fees. This also removes a bug where the long term feerate would incorrectly be set to the fallback fee. While this doesn't matter prior to this PR, it does have an effect following this. The long term feerate can be configured by the user through a new `-consolidatefeerate` option. ACKs for top commit: Xekyo: reACK 86beee0 via git range-diff fe47558...86beee0 meshcollider: re-utACK 86beee0 Tree-SHA512: 54b154b346538eca68ae2a3b83a033b495c1605c14f842bfc43ded2256b110983ce674c647fe753cf0305b1b178403d8d60d6d4203c7a712bec784be52e90d42
2 parents a820e79 + 86beee0 commit 70676e4

File tree

9 files changed

+210
-44
lines changed

9 files changed

+210
-44
lines changed

src/dummywallet.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void DummyWalletInit::AddWalletOptions(ArgsManager& argsman) const
2828
"-addresstype",
2929
"-avoidpartialspends",
3030
"-changetype",
31+
"-consolidatefeerate=<amt>",
3132
"-disablewallet",
3233
"-discardfee=<amt>",
3334
"-fallbackfee=<amt>",

src/wallet/coinselection.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,30 @@ CAmount OutputGroup::GetSelectionAmount() const
341341
{
342342
return m_subtract_fee_outputs ? m_value : effective_value;
343343
}
344+
345+
CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
346+
{
347+
// This function should not be called with empty inputs as that would mean the selection failed
348+
assert(!inputs.empty());
349+
350+
// Always consider the cost of spending an input now vs in the future.
351+
CAmount waste = 0;
352+
CAmount selected_effective_value = 0;
353+
for (const CInputCoin& coin : inputs) {
354+
waste += coin.m_fee - coin.m_long_term_fee;
355+
selected_effective_value += use_effective_value ? coin.effective_value : coin.txout.nValue;
356+
}
357+
358+
if (change_cost) {
359+
// Consider the cost of making change and spending it in the future
360+
// If we aren't making change, the caller should've set change_cost to 0
361+
assert(change_cost > 0);
362+
waste += change_cost;
363+
} else {
364+
// When we are not making change (change_cost == 0), consider the excess we are throwing away to fees
365+
assert(selected_effective_value >= target);
366+
waste += selected_effective_value - target;
367+
}
368+
369+
return waste;
370+
}

src/wallet/coinselection.h

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

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

171186
// 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
@@ -45,6 +45,7 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
4545
argsman.AddArg("-addresstype", strprintf("What type of addresses to use (\"legacy\", \"p2sh-segwit\", or \"bech32\", default: \"%s\")", FormatOutputType(DEFAULT_ADDRESS_TYPE)), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
4646
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);
4747
argsman.AddArg("-changetype", "What type of change to use (\"legacy\", \"p2sh-segwit\", or \"bech32\"). Default is same as -addresstype, except when -addresstype=p2sh-segwit a native segwit output is used when sending to a native segwit address)", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
48+
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);
4849
argsman.AddArg("-disablewallet", "Do not load the wallet and disable wallet RPC calls", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
4950
argsman.AddArg("-discardfee=<amt>", strprintf("The fee rate (in %s/kvB) that indicates your tolerance for discarding change by adding it to the fee (default: %s). "
5051
"Note: An output is discarded if it is dust at this rate, but we will always discard up to the dust relay fee and a discard fee above that is limited by the fee estimate for the longest target",

src/wallet/spend.cpp

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,17 +357,44 @@ bool CWallet::AttemptSelection(const CAmount& nTargetValue, const CoinEligibilit
357357
{
358358
setCoinsRet.clear();
359359
nValueRet = 0;
360+
// Vector of results for use with waste calculation
361+
// In order: calculated waste, selected inputs, selected input value (sum of input values)
362+
// TODO: Use a struct representing the selection result
363+
std::vector<std::tuple<CAmount, std::set<CInputCoin>, CAmount>> results;
360364

361365
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
362366
std::vector<OutputGroup> positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */);
363-
if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) {
364-
return true;
367+
std::set<CInputCoin> bnb_coins;
368+
CAmount bnb_value;
369+
if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, bnb_coins, bnb_value)) {
370+
const auto waste = GetSelectionWaste(bnb_coins, /* cost of change */ CAmount(0), nTargetValue, !coin_selection_params.m_subtract_fee_outputs);
371+
results.emplace_back(std::make_tuple(waste, std::move(bnb_coins), bnb_value));
365372
}
373+
366374
// 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.
367375
std::vector<OutputGroup> all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */);
368376
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
369377
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
370-
return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet);
378+
std::set<CInputCoin> knapsack_coins;
379+
CAmount knapsack_value;
380+
if (KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, knapsack_coins, knapsack_value)) {
381+
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);
382+
results.emplace_back(std::make_tuple(waste, std::move(knapsack_coins), knapsack_value));
383+
}
384+
385+
if (results.size() == 0) {
386+
// No solution found
387+
return false;
388+
}
389+
390+
// Choose the result with the least waste
391+
// If the waste is the same, choose the one which spends more inputs.
392+
const auto& best_result = std::min_element(results.begin(), results.end(), [](const auto& a, const auto& b) {
393+
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());
394+
});
395+
setCoinsRet = std::get<1>(*best_result);
396+
nValueRet = std::get<2>(*best_result);
397+
return true;
371398
}
372399

373400
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
@@ -586,6 +613,9 @@ bool CWallet::CreateTransactionInternal(
586613
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
587614
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
588615

616+
// Set the long term feerate estimate to the wallet's consolidate feerate
617+
coin_selection_params.m_long_term_feerate = m_consolidate_feerate;
618+
589619
CAmount recipients_sum = 0;
590620
const OutputType change_type = TransactionChangeType(coin_control.m_change_type ? *coin_control.m_change_type : m_default_change_type, vecSend);
591621
ReserveDestination reservedest(this, change_type);
@@ -659,11 +689,6 @@ bool CWallet::CreateTransactionInternal(
659689
return false;
660690
}
661691

662-
// Get long term estimate
663-
CCoinControl cc_temp;
664-
cc_temp.m_confirm_target = chain().estimateMaxBlocks();
665-
coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr);
666-
667692
// Calculate the cost of change
668693
// Cost of change is the cost of creating the change output + cost of spending the change output in the future.
669694
// For creating the change output now, we use the effective feerate.

0 commit comments

Comments
 (0)