Skip to content

Commit 438e048

Browse files
committed
wallet: run coin selection by OutputType
Run coin selection on each OutputType separately, choosing the best solution according to the waste metric. This is to avoid mixing UTXOs that are of different OutputTypes, which can hurt privacy. If no single OutputType can fund the transaction, then coin selection considers the entire wallet, potentially mixing (current behavior). This is done inside AttemptSelection so that all OutputTypes are considered at each back-off in coin selection.
1 parent 77b0707 commit 438e048

File tree

3 files changed

+50
-23
lines changed

3 files changed

+50
-23
lines changed

src/bench/coin_selection.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ static void CoinSelection(benchmark::Bench& bench)
7676
/*avoid_partial=*/ false,
7777
};
7878
bench.run([&] {
79-
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, available_coins, coin_selection_params);
79+
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, available_coins, coin_selection_params, /*allow_mixed_output_types=*/true);
8080
assert(result);
8181
assert(result->GetSelectedValue() == 1003 * COIN);
8282
assert(result->GetInputSet().size() == 2);

src/wallet/spend.cpp

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,34 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
451451
}
452452

453453
std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
454-
const CoinSelectionParams& coin_selection_params)
454+
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types)
455455
{
456-
std::optional<SelectionResult> result = ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.all(), coin_selection_params);
456+
// Run coin selection on each OutputType and compute the Waste Metric
457+
std::vector<SelectionResult> results;
458+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.legacy, coin_selection_params)}) {
459+
results.push_back(*result);
460+
}
461+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.P2SH_segwit, coin_selection_params)}) {
462+
results.push_back(*result);
463+
}
464+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.bech32, coin_selection_params)}) {
465+
results.push_back(*result);
466+
}
467+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.bech32m, coin_selection_params)}) {
468+
results.push_back(*result);
469+
}
470+
471+
// If we can't fund the transaction from any individual OutputType, run coin selection
472+
// over all available coins, else pick the best solution from the results
473+
if (results.size() == 0) {
474+
if (allow_mixed_output_types) {
475+
if (auto result{ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.all(), coin_selection_params)}) {
476+
return result;
477+
}
478+
}
479+
return std::optional<SelectionResult>();
480+
};
481+
std::optional<SelectionResult> result{*std::min_element(results.begin(), results.end())};
457482
return result;
458483
};
459484

@@ -601,34 +626,35 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
601626

602627
// If possible, fund the transaction with confirmed UTXOs only. Prefer at least six
603628
// confirmations on outputs received from other wallets and only spend confirmed change.
604-
if (auto r1{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 6, 0), available_coins, coin_selection_params)}) return r1;
605-
if (auto r2{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 1, 0), available_coins, coin_selection_params)}) return r2;
629+
if (auto r1{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 6, 0), available_coins, coin_selection_params, /*allow_mixed_output_types=*/false)}) return r1;
630+
// Allow mixing only if no solution from any single output type can be found
631+
if (auto r2{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 1, 0), available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) return r2;
606632

607633
// Fall back to using zero confirmation change (but with as few ancestors in the mempool as
608634
// possible) if we cannot fund the transaction otherwise.
609635
if (wallet.m_spend_zero_conf_change) {
610-
if (auto r3{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, 2), available_coins, coin_selection_params)}) return r3;
636+
if (auto r3{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, 2), available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) return r3;
611637
if (auto r4{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, std::min((size_t)4, max_ancestors/3), std::min((size_t)4, max_descendants/3)),
612-
available_coins, coin_selection_params)}) {
638+
available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
613639
return r4;
614640
}
615641
if (auto r5{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2),
616-
available_coins, coin_selection_params)}) {
642+
available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
617643
return r5;
618644
}
619645
// If partial groups are allowed, relax the requirement of spending OutputGroups (groups
620646
// of UTXOs sent to the same address, which are obviously controlled by a single wallet)
621647
// in their entirety.
622648
if (auto r6{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
623-
available_coins, coin_selection_params)}) {
649+
available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
624650
return r6;
625651
}
626652
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
627653
// received from other wallets.
628654
if (coin_control.m_include_unsafe_inputs) {
629655
if (auto r7{AttemptSelection(wallet, value_to_select,
630656
CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
631-
available_coins, coin_selection_params)}) {
657+
available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
632658
return r7;
633659
}
634660
}
@@ -638,7 +664,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& a
638664
if (!fRejectLongChains) {
639665
if (auto r8{AttemptSelection(wallet, value_to_select,
640666
CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint64_t>::max(), true /* include_partial_groups */),
641-
available_coins, coin_selection_params)}) {
667+
available_coins, coin_selection_params, /*allow_mixed_output_types=*/true)}) {
642668
return r8;
643669
}
644670
}

src/wallet/spend.h

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,23 @@ const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint&
9191
std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
9292

9393
std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only);
94-
9594
/**
96-
* Attempt to find a valid input set that meets the provided eligibility filter and target.
97-
* Multiple coin selection algorithms will be run and the input set that produces the least waste
98-
* (according to the waste metric) will be chosen.
95+
* Attempt to find a valid input set that preserves privacy by not mixing OutputTypes.
96+
* `ChooseSelectionResult()` will be called on each OutputType individually and the best
97+
* the solution (according to the waste metric) will be chosen. If a valid input cannot be found from any
98+
* single OutputType, fallback to running `ChooseSelectionResult()` over all available coins.
9999
*
100-
* param@[in] wallet The wallet which provides solving data for the coins
101-
* param@[in] nTargetValue The target value
102-
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
103-
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
104-
* param@[in] coin_selection_params Parameters for the coin selection
105-
* returns If successful, a SelectionResult containing the input set
106-
* If failed, a nullopt
100+
* param@[in] wallet The wallet which provides solving data for the coins
101+
* param@[in] nTargetValue The target value
102+
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
103+
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
104+
* param@[in] coin_selection_params Parameters for the coin selection
105+
* param@[in] allow_mixed_output_types Relax restriction that SelectionResults must be of the same OutputType
106+
* returns If successful, a SelectionResult containing the input set
107+
* If failed, a nullopt
107108
*/
108109
std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
109-
const CoinSelectionParams& coin_selection_params);
110+
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types);
110111

111112
/**
112113
* Attempt to find a valid input set that meets the provided eligibility filter and target.

0 commit comments

Comments
 (0)