Skip to content

Commit 6a302d4

Browse files
committed
wallet: single output groups filtering and grouping process
Optimizes coin selection by performing the "group outputs" procedure only once, outside the "attempt selection" process. Avoiding the repeated execution of the 'GroupOutputs' operation that occurs on each coin eligibility filters (up to 8 of them); then for every coin vector type plus one for all the coins together. This also let us not perform coin selection over coin eligibility filtered groups that don't add new elements. (because, if the previous round failed, and the subsequent one has the same coins, then this new round will fail again).
1 parent bd91ed1 commit 6a302d4

File tree

6 files changed

+55
-39
lines changed

6 files changed

+55
-39
lines changed

src/bench/coin_selection.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ static void CoinSelection(benchmark::Bench& bench)
7575
/*tx_noinputs_size=*/ 0,
7676
/*avoid_partial=*/ false,
7777
};
78+
auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard];
7879
bench.run([&] {
79-
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, available_coins, coin_selection_params, /*allow_mixed_output_types=*/true);
80+
auto result = AttemptSelection(1003 * COIN, group, coin_selection_params, /*allow_mixed_output_types=*/true);
8081
assert(result);
8182
assert(result->GetSelectedValue() == 1003 * COIN);
8283
assert(result->GetInputSet().size() == 2);

src/wallet/coinselection.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ struct CoinEligibilityFilter
193193
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_ancestors) {}
194194
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants) {}
195195
CoinEligibilityFilter(int conf_mine, int conf_theirs, uint64_t max_ancestors, uint64_t max_descendants, bool include_partial) : conf_mine(conf_mine), conf_theirs(conf_theirs), max_ancestors(max_ancestors), max_descendants(max_descendants), m_include_partial_groups(include_partial) {}
196+
197+
bool operator<(const CoinEligibilityFilter& other) const {
198+
return std::tie(conf_mine, conf_theirs, max_ancestors, max_descendants, m_include_partial_groups)
199+
< std::tie(other.conf_mine, other.conf_theirs, other.max_ancestors, other.max_descendants, other.m_include_partial_groups);
200+
}
196201
};
197202

198203
/** A group of UTXOs paid to the same output script. */
@@ -263,8 +268,12 @@ struct OutputGroupTypeMap
263268
void Push(const OutputGroup& group, OutputType type, bool insert_positive, bool insert_mixed);
264269
// Retrieves 'Groups' filtered by type
265270
std::optional<Groups> Find(OutputType type);
271+
// Different output types count
272+
size_t TypesCount() { return groups_by_type.size(); }
266273
};
267274

275+
typedef std::map<CoinEligibilityFilter, OutputGroupTypeMap> FilteredOutputGroups;
276+
268277
/** Compute the waste for this result given the cost of change
269278
* and the opportunity cost of spending these inputs now vs in the future.
270279
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)

src/wallet/spend.cpp

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,12 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
404404
return result;
405405
}
406406

407-
OutputGroupTypeMap GroupOutputs(const CWallet& wallet,
407+
FilteredOutputGroups GroupOutputs(const CWallet& wallet,
408408
const CoinsResult& coins,
409409
const CoinSelectionParams& coin_sel_params,
410-
const CoinEligibilityFilter& filter)
410+
const std::vector<SelectionFilter>& filters)
411411
{
412-
OutputGroupTypeMap output_groups;
412+
FilteredOutputGroups filtered_groups;
413413

414414
if (!coin_sel_params.m_avoid_partial_spends) {
415415
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup
@@ -426,11 +426,15 @@ OutputGroupTypeMap GroupOutputs(const CWallet& wallet,
426426
OutputGroup group(coin_sel_params);
427427
group.Insert(std::make_shared<COutput>(output), ancestors, descendants);
428428

429-
if (!group.EligibleForSpending(filter)) continue;
430-
output_groups.Push(group, type, /*insert_positive=*/true, /*insert_mixed=*/true);
429+
// Each filter maps to a different set of groups
430+
for (const auto& sel_filter : filters) {
431+
const auto& filter = sel_filter.filter;
432+
if (!group.EligibleForSpending(filter)) continue;
433+
filtered_groups[filter].Push(group, type, /*insert_positive=*/true, /*insert_mixed=*/true);
434+
}
431435
}
432436
}
433-
return output_groups;
437+
return filtered_groups;
434438
}
435439

436440
// We want to combine COutputs that have the same scriptPubKey into single OutputGroups
@@ -492,41 +496,40 @@ OutputGroupTypeMap GroupOutputs(const CWallet& wallet,
492496
for (auto group_it = groups.rbegin(); group_it != groups.rend(); group_it++) {
493497
const OutputGroup& group = *group_it;
494498

495-
if (!group.EligibleForSpending(filter)) continue;
499+
// Each filter maps to a different set of groups
500+
for (const auto& sel_filter : filters) {
501+
const auto& filter = sel_filter.filter;
502+
if (!group.EligibleForSpending(filter)) continue;
496503

497-
// Don't include partial groups if there are full groups too and we don't want partial groups
498-
if (group_it == groups.rbegin() && groups.size() > 1 && !filter.m_include_partial_groups) {
499-
continue;
500-
}
504+
// Don't include partial groups if there are full groups too and we don't want partial groups
505+
if (group_it == groups.rbegin() && groups.size() > 1 && !filter.m_include_partial_groups) {
506+
continue;
507+
}
501508

502-
OutputType type = script.second;
503-
// Either insert the group into the positive-only groups or the mixed ones.
504-
output_groups.Push(group, type, positive_only, /*insert_mixed=*/!positive_only);
509+
OutputType type = script.second;
510+
// Either insert the group into the positive-only groups or the mixed ones.
511+
filtered_groups[filter].Push(group, type, positive_only, /*insert_mixed=*/!positive_only);
512+
}
505513
}
506514
}
507515
};
508516

509517
push_output_groups(spk_to_groups_map, /*positive_only=*/ false);
510518
push_output_groups(spk_to_positive_groups_map, /*positive_only=*/ true);
511519

512-
return output_groups;
520+
return filtered_groups;
513521
}
514522

515523
// Returns true if the result contains an error and the message is not empty
516524
static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
517525

518-
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
526+
util::Result<SelectionResult> AttemptSelection(const CAmount& nTargetValue, OutputGroupTypeMap& groups,
519527
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types)
520528
{
521-
// Calculate all the output groups filtered by type at once
522-
OutputGroupTypeMap groups = GroupOutputs(wallet, available_coins, coin_selection_params, {eligibility_filter});
523-
524529
// Run coin selection on each OutputType and compute the Waste Metric
525530
std::vector<SelectionResult> results;
526-
for (const auto& [type, coins] : available_coins.coins) {
527-
auto group_for_type = groups.Find(type);
528-
if (!group_for_type) continue;
529-
auto result{ChooseSelectionResult(nTargetValue, *group_for_type, coin_selection_params)};
531+
for (auto& [type, group] : groups.groups_by_type) {
532+
auto result{ChooseSelectionResult(nTargetValue, group, coin_selection_params)};
530533
// If any specific error message appears here, then something particularly wrong happened.
531534
if (HasErrorMsg(result)) return result; // So let's return the specific error.
532535
// Append the favorable result.
@@ -539,7 +542,7 @@ util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmo
539542
// If we can't fund the transaction from any individual OutputType, run coin selection one last time
540543
// over all available coins, which would allow mixing.
541544
// If TypesCount() <= 1, there is nothing to mix.
542-
if (allow_mixed_output_types && available_coins.TypesCount() > 1) {
545+
if (allow_mixed_output_types && groups.TypesCount() > 1) {
543546
return ChooseSelectionResult(nTargetValue, groups.all_groups, coin_selection_params);
544547
}
545548
// Either mixing is not allowed and we couldn't find a solution from any single OutputType, or mixing was allowed and we still couldn't
@@ -634,11 +637,6 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
634637
return op_selection_result;
635638
}
636639

637-
struct SelectionFilter {
638-
CoinEligibilityFilter filter;
639-
bool allow_mixed_output_types{true};
640-
};
641-
642640
util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, CoinsResult& available_coins, const CAmount& value_to_select, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
643641
{
644642
unsigned int limit_ancestor_count = 0;
@@ -693,12 +691,17 @@ util::Result<SelectionResult> AutomaticCoinSelection(const CWallet& wallet, Coin
693691
}
694692
}
695693

694+
// Group outputs and map them by coin eligibility filter
695+
FilteredOutputGroups filtered_groups = GroupOutputs(wallet, available_coins, coin_selection_params, ordered_filters);
696+
696697
// Walk-through the filters until the solution gets found.
697698
// If no solution is found, return the first detailed error (if any).
698699
// future: add "error level" so the worst one can be picked instead.
699700
std::vector<util::Result<SelectionResult>> res_detailed_errors;
700701
for (const auto& select_filter : ordered_filters) {
701-
if (auto res{AttemptSelection(wallet, value_to_select, select_filter.filter, available_coins,
702+
auto it = filtered_groups.find(select_filter.filter);
703+
if (it == filtered_groups.end()) continue;
704+
if (auto res{AttemptSelection(value_to_select, it->second,
702705
coin_selection_params, select_filter.allow_mixed_output_types)}) {
703706
return res; // result found
704707
} else {

src/wallet/spend.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,32 +106,35 @@ const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint&
106106
*/
107107
std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
108108

109+
struct SelectionFilter {
110+
CoinEligibilityFilter filter;
111+
bool allow_mixed_output_types{true};
112+
};
113+
109114
/**
110115
* Group coins by the provided filters.
111116
*/
112-
OutputGroupTypeMap GroupOutputs(const CWallet& wallet,
117+
FilteredOutputGroups GroupOutputs(const CWallet& wallet,
113118
const CoinsResult& coins,
114119
const CoinSelectionParams& coin_sel_params,
115-
const CoinEligibilityFilter& filter);
120+
const std::vector<SelectionFilter>& filters);
116121

117122
/**
118123
* Attempt to find a valid input set that preserves privacy by not mixing OutputTypes.
119124
* `ChooseSelectionResult()` will be called on each OutputType individually and the best
120125
* the solution (according to the waste metric) will be chosen. If a valid input cannot be found from any
121126
* single OutputType, fallback to running `ChooseSelectionResult()` over all available coins.
122127
*
123-
* param@[in] wallet The wallet which provides solving data for the coins
124128
* param@[in] nTargetValue The target value
125-
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
126-
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
129+
* param@[in] groups The grouped outputs mapped by coin eligibility filters
127130
* param@[in] coin_selection_params Parameters for the coin selection
128131
* param@[in] allow_mixed_output_types Relax restriction that SelectionResults must be of the same OutputType
129132
* returns If successful, a SelectionResult containing the input set
130133
* If failed, returns (1) an empty error message if the target was not reached (general "Insufficient funds")
131134
* or (2) an specific error message if there was something particularly wrong (e.g. a selection
132135
* result that surpassed the tx max weight size).
133136
*/
134-
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
137+
util::Result<SelectionResult> AttemptSelection(const CAmount& nTargetValue, OutputGroupTypeMap& groups,
135138
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types);
136139

137140
/**

src/wallet/test/coinselector_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& availab
154154
/*avoid_partial=*/ false,
155155
};
156156
static OutputGroupTypeMap static_groups;
157-
static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {filter});
157+
static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter];
158158
return static_groups.all_groups.mixed_group;
159159
}
160160

src/wallet/test/group_outputs_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class GroupVerifier
8686
bool positive_only,
8787
int expected_size)
8888
{
89-
OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), filter);
89+
OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];
9090
std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
9191
groups.groups_by_type[type].mixed_group;
9292
BOOST_CHECK_EQUAL(groups_out.size(), expected_size);

0 commit comments

Comments
 (0)