Skip to content

Commit bd91ed1

Browse files
committed
wallet: unify outputs grouping process
The 'GroupOutputs()' function performs the same calculations for only-positive and mixed groups, the only difference is that when we look for only-positive groups, we discard negative utxos. So, instead of wasting resources calling GroupOutputs() for positive-only first, then call it again to include the negative ones in the result, we can execute GroupOutputs() only once, including in the response both group types (positive-only and mixed).
1 parent 5596200 commit bd91ed1

File tree

6 files changed

+144
-72
lines changed

6 files changed

+144
-72
lines changed

src/wallet/coinselection.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,28 @@ CAmount OutputGroup::GetSelectionAmount() const
372372
return m_subtract_fee_outputs ? m_value : effective_value;
373373
}
374374

375+
void OutputGroupTypeMap::Push(const OutputGroup& group, OutputType type, bool insert_positive, bool insert_mixed)
376+
{
377+
if (group.m_outputs.empty()) return;
378+
379+
Groups& groups = groups_by_type[type];
380+
if (insert_positive && group.GetSelectionAmount() > 0) {
381+
groups.positive_group.emplace_back(group);
382+
all_groups.positive_group.emplace_back(group);
383+
}
384+
if (insert_mixed) {
385+
groups.mixed_group.emplace_back(group);
386+
all_groups.mixed_group.emplace_back(group);
387+
}
388+
}
389+
390+
std::optional<Groups> OutputGroupTypeMap::Find(OutputType type)
391+
{
392+
auto it_by_type = groups_by_type.find(type);
393+
if (it_by_type == groups_by_type.end()) return std::nullopt;
394+
return it_by_type->second;
395+
}
396+
375397
CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
376398
{
377399
// This function should not be called with empty inputs as that would mean the selection failed

src/wallet/coinselection.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <consensus/amount.h>
99
#include <consensus/consensus.h>
10+
#include <outputtype.h>
1011
#include <policy/feerate.h>
1112
#include <primitives/transaction.h>
1213
#include <random.h>
@@ -249,6 +250,21 @@ struct Groups {
249250
std::vector<OutputGroup> mixed_group;
250251
};
251252

253+
/** Stores several 'Groups' whose were mapped by output type. */
254+
struct OutputGroupTypeMap
255+
{
256+
// Maps output type to output groups.
257+
std::map<OutputType, Groups> groups_by_type;
258+
// All inserted groups, no type distinction.
259+
Groups all_groups;
260+
261+
// Based on the insert flag; appends group to the 'mixed_group' and, if value > 0, to the 'positive_group'.
262+
// This affects both; the groups filtered by type and the overall groups container.
263+
void Push(const OutputGroup& group, OutputType type, bool insert_positive, bool insert_mixed);
264+
// Retrieves 'Groups' filtered by type
265+
std::optional<Groups> Find(OutputType type);
266+
};
267+
252268
/** Compute the waste for this result given the cost of change
253269
* and the opportunity cost of spending these inputs now vs in the future.
254270
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)

src/wallet/spend.cpp

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

407-
std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only)
407+
OutputGroupTypeMap GroupOutputs(const CWallet& wallet,
408+
const CoinsResult& coins,
409+
const CoinSelectionParams& coin_sel_params,
410+
const CoinEligibilityFilter& filter)
408411
{
409-
std::vector<OutputGroup> groups_out;
412+
OutputGroupTypeMap output_groups;
410413

411414
if (!coin_sel_params.m_avoid_partial_spends) {
412-
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup.
413-
for (const COutput& output : outputs) {
414-
// Skip outputs we cannot spend
415-
if (!output.spendable) continue;
416-
417-
size_t ancestors, descendants;
418-
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
419-
420-
// If 'positive_only' is set, filter for positive only before adding the coin
421-
if (!positive_only || output.GetEffectiveValue() > 0) {
422-
// Make an OutputGroup containing just this output
423-
OutputGroup group{coin_sel_params};
415+
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup
416+
for (const auto& [type, outputs] : coins.coins) {
417+
for (const COutput& output : outputs) {
418+
// Skip outputs we cannot spend
419+
if (!output.spendable) continue;
420+
421+
// Get mempool info
422+
size_t ancestors, descendants;
423+
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
424+
425+
// Create a new group per output and add it to the all groups vector
426+
OutputGroup group(coin_sel_params);
424427
group.Insert(std::make_shared<COutput>(output), ancestors, descendants);
425428

426-
if (group.EligibleForSpending(filter)) groups_out.push_back(group);
429+
if (!group.EligibleForSpending(filter)) continue;
430+
output_groups.Push(group, type, /*insert_positive=*/true, /*insert_mixed=*/true);
427431
}
428432
}
429-
return groups_out;
433+
return output_groups;
430434
}
431435

432436
// We want to combine COutputs that have the same scriptPubKey into single OutputGroups
@@ -435,16 +439,12 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
435439
// For each COutput, we check if the scriptPubKey is in the map, and if it is, the COutput is added
436440
// to the last OutputGroup in the vector for the scriptPubKey. When the last OutputGroup has
437441
// OUTPUT_GROUP_MAX_ENTRIES COutputs, a new OutputGroup is added to the end of the vector.
438-
std::map<CScript, std::vector<OutputGroup>> spk_to_groups_map;
439-
for (const auto& output : outputs) {
440-
// Skip outputs we cannot spend
441-
if (!output.spendable) continue;
442-
443-
size_t ancestors, descendants;
444-
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
445-
CScript spk = output.txout.scriptPubKey;
446-
447-
std::vector<OutputGroup>& groups = spk_to_groups_map[spk];
442+
typedef std::map<std::pair<CScript, OutputType>, std::vector<OutputGroup>> ScriptPubKeyToOutgroup;
443+
const auto& group_outputs = [](
444+
const COutput& output, OutputType type, size_t ancestors, size_t descendants,
445+
ScriptPubKeyToOutgroup& groups_map, const CoinSelectionParams& coin_sel_params,
446+
bool positive_only) {
447+
std::vector<OutputGroup>& groups = groups_map[std::make_pair(output.txout.scriptPubKey,type)];
448448

449449
if (groups.size() == 0) {
450450
// No OutputGroups for this scriptPubKey yet, add one
@@ -467,28 +467,49 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
467467
if (!positive_only || output.GetEffectiveValue() > 0) {
468468
group->Insert(std::make_shared<COutput>(output), ancestors, descendants);
469469
}
470+
};
471+
472+
ScriptPubKeyToOutgroup spk_to_groups_map;
473+
ScriptPubKeyToOutgroup spk_to_positive_groups_map;
474+
for (const auto& [type, outs] : coins.coins) {
475+
for (const COutput& output : outs) {
476+
// Skip outputs we cannot spend
477+
if (!output.spendable) continue;
478+
479+
size_t ancestors, descendants;
480+
wallet.chain().getTransactionAncestry(output.outpoint.hash, ancestors, descendants);
481+
482+
group_outputs(output, type, ancestors, descendants, spk_to_groups_map, coin_sel_params, /*positive_only=*/ false);
483+
group_outputs(output, type, ancestors, descendants, spk_to_positive_groups_map,
484+
coin_sel_params, /*positive_only=*/ true);
485+
}
470486
}
471487

472-
// Now we go through the entire map and pull out the OutputGroups
473-
for (const auto& spk_and_groups_pair: spk_to_groups_map) {
474-
const std::vector<OutputGroup>& groups_per_spk= spk_and_groups_pair.second;
488+
// Now we go through the entire maps and pull out the OutputGroups
489+
const auto& push_output_groups = [&](const ScriptPubKeyToOutgroup& groups_map, bool positive_only) {
490+
for (const auto& [script, groups] : groups_map) {
491+
// Go through the vector backwards. This allows for the first item we deal with being the partial group.
492+
for (auto group_it = groups.rbegin(); group_it != groups.rend(); group_it++) {
493+
const OutputGroup& group = *group_it;
475494

476-
// Go through the vector backwards. This allows for the first item we deal with being the partial group.
477-
for (auto group_it = groups_per_spk.rbegin(); group_it != groups_per_spk.rend(); group_it++) {
478-
const OutputGroup& group = *group_it;
495+
if (!group.EligibleForSpending(filter)) continue;
479496

480-
// Don't include partial groups if there are full groups too and we don't want partial groups
481-
if (group_it == groups_per_spk.rbegin() && groups_per_spk.size() > 1 && !filter.m_include_partial_groups) {
482-
continue;
483-
}
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+
}
484501

485-
// Check the OutputGroup's eligibility. Only add the eligible ones.
486-
if (positive_only && group.GetSelectionAmount() <= 0) continue;
487-
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
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);
505+
}
488506
}
489-
}
507+
};
490508

491-
return groups_out;
509+
push_output_groups(spk_to_groups_map, /*positive_only=*/ false);
510+
push_output_groups(spk_to_positive_groups_map, /*positive_only=*/ true);
511+
512+
return output_groups;
492513
}
493514

494515
// Returns true if the result contains an error and the message is not empty
@@ -497,13 +518,15 @@ static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util
497518
util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
498519
const CoinSelectionParams& coin_selection_params, bool allow_mixed_output_types)
499520
{
521+
// Calculate all the output groups filtered by type at once
522+
OutputGroupTypeMap groups = GroupOutputs(wallet, available_coins, coin_selection_params, {eligibility_filter});
523+
500524
// Run coin selection on each OutputType and compute the Waste Metric
501525
std::vector<SelectionResult> results;
502526
for (const auto& [type, coins] : available_coins.coins) {
503-
Groups groups;
504-
groups.positive_group = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, true /* positive_only */);
505-
groups.mixed_group = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, false /* positive_only */);
506-
auto result{ChooseSelectionResult(nTargetValue, groups, coin_selection_params)};
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)};
507530
// If any specific error message appears here, then something particularly wrong happened.
508531
if (HasErrorMsg(result)) return result; // So let's return the specific error.
509532
// Append the favorable result.
@@ -517,11 +540,7 @@ util::Result<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmo
517540
// over all available coins, which would allow mixing.
518541
// If TypesCount() <= 1, there is nothing to mix.
519542
if (allow_mixed_output_types && available_coins.TypesCount() > 1) {
520-
const auto& all = available_coins.All();
521-
Groups groups;
522-
groups.positive_group = GroupOutputs(wallet, all, coin_selection_params, eligibility_filter, true /* positive_only */);
523-
groups.mixed_group = GroupOutputs(wallet, all, coin_selection_params, eligibility_filter, false /* positive_only */);
524-
return ChooseSelectionResult(nTargetValue, groups, coin_selection_params);
543+
return ChooseSelectionResult(nTargetValue, groups.all_groups, coin_selection_params);
525544
}
526545
// 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
527546
// find a solution using all available coins

src/wallet/spend.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,14 @@ 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-
std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only);
109+
/**
110+
* Group coins by the provided filters.
111+
*/
112+
OutputGroupTypeMap GroupOutputs(const CWallet& wallet,
113+
const CoinsResult& coins,
114+
const CoinSelectionParams& coin_sel_params,
115+
const CoinEligibilityFilter& filter);
116+
110117
/**
111118
* Attempt to find a valid input set that preserves privacy by not mixing OutputTypes.
112119
* `ChooseSelectionResult()` will be called on each OutputType individually and the best

src/wallet/test/coinselector_tests.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& availab
153153
/*tx_noinputs_size=*/ 0,
154154
/*avoid_partial=*/ false,
155155
};
156-
static std::vector<OutputGroup> static_groups;
157-
static_groups = GroupOutputs(wallet, available_coins.All(), coin_selection_params, filter, /*positive_only=*/false);
158-
return static_groups;
156+
static OutputGroupTypeMap static_groups;
157+
static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {filter});
158+
return static_groups.all_groups.mixed_group;
159159
}
160160

161161
// Branch and bound coin selection tests

src/wallet/test/group_outputs_tests.cpp

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -80,28 +80,28 @@ class GroupVerifier
8080
CoinsResult coins_pool;
8181
FastRandomContext rand;
8282

83-
void GroupVerify(const CoinEligibilityFilter& filter,
83+
void GroupVerify(const OutputType type,
84+
const CoinEligibilityFilter& filter,
8485
bool avoid_partial_spends,
8586
bool positive_only,
8687
int expected_size)
8788
{
88-
std::vector<OutputGroup> groups = GroupOutputs(*wallet,
89-
coins_pool.All(),
90-
makeSelectionParams(rand, avoid_partial_spends),
91-
filter,
92-
positive_only);
93-
BOOST_CHECK_EQUAL(groups.size(), expected_size);
89+
OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), filter);
90+
std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
91+
groups.groups_by_type[type].mixed_group;
92+
BOOST_CHECK_EQUAL(groups_out.size(), expected_size);
9493
}
9594

96-
void GroupAndVerify(const CoinEligibilityFilter& filter,
95+
void GroupAndVerify(const OutputType type,
96+
const CoinEligibilityFilter& filter,
9797
int expected_with_partial_spends_size,
9898
int expected_without_partial_spends_size,
9999
bool positive_only)
100100
{
101101
// First avoid partial spends
102-
GroupVerify(filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size);
102+
GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size);
103103
// Second don't avoid partial spends
104-
GroupVerify(filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
104+
GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
105105
}
106106
};
107107

@@ -125,7 +125,8 @@ BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
125125
addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);
126126
}
127127

128-
group_verifier.GroupAndVerify(BASIC_FILTER,
128+
group_verifier.GroupAndVerify(OutputType::BECH32,
129+
BASIC_FILTER,
129130
/*expected_with_partial_spends_size=*/ GROUP_SIZE,
130131
/*expected_without_partial_spends_size=*/ 1,
131132
/*positive_only=*/ true);
@@ -140,7 +141,8 @@ BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
140141
addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true);
141142
}
142143

143-
group_verifier.GroupAndVerify(BASIC_FILTER,
144+
group_verifier.GroupAndVerify(OutputType::BECH32,
145+
BASIC_FILTER,
144146
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
145147
/*expected_without_partial_spends_size=*/ 2,
146148
/*positive_only=*/ true);
@@ -154,13 +156,15 @@ BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
154156
BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);
155157

156158
// First expect no changes with "positive_only" enabled
157-
group_verifier.GroupAndVerify(BASIC_FILTER,
159+
group_verifier.GroupAndVerify(OutputType::BECH32,
160+
BASIC_FILTER,
158161
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
159162
/*expected_without_partial_spends_size=*/ 2,
160163
/*positive_only=*/ true);
161164

162165
// Then expect changes with "positive_only" disabled
163-
group_verifier.GroupAndVerify(BASIC_FILTER,
166+
group_verifier.GroupAndVerify(OutputType::BECH32,
167+
BASIC_FILTER,
164168
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
165169
/*expected_without_partial_spends_size=*/ 3,
166170
/*positive_only=*/ false);
@@ -176,7 +180,8 @@ BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
176180
/*is_from_me=*/false, CFeeRate(0), /*depth=*/5);
177181

178182
// Expect no changes from this round and the previous one (point 4)
179-
group_verifier.GroupAndVerify(BASIC_FILTER,
183+
group_verifier.GroupAndVerify(OutputType::BECH32,
184+
BASIC_FILTER,
180185
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
181186
/*expected_without_partial_spends_size=*/ 3,
182187
/*positive_only=*/ false);
@@ -192,7 +197,8 @@ BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
192197
/*is_from_me=*/true, CFeeRate(0), /*depth=*/0);
193198

194199
// Expect no changes from this round and the previous one (point 5)
195-
group_verifier.GroupAndVerify(BASIC_FILTER,
200+
group_verifier.GroupAndVerify(OutputType::BECH32,
201+
BASIC_FILTER,
196202
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
197203
/*expected_without_partial_spends_size=*/ 3,
198204
/*positive_only=*/ false);
@@ -209,14 +215,16 @@ BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
209215

210216
// Exclude partial groups only adds one more group to the previous test case (point 6)
211217
int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;
212-
group_verifier.GroupAndVerify(BASIC_FILTER,
218+
group_verifier.GroupAndVerify(OutputType::BECH32,
219+
BASIC_FILTER,
213220
/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
214221
/*expected_without_partial_spends_size=*/ 4,
215222
/*positive_only=*/ false);
216223

217224
// Include partial groups should add one more group inside the "avoid partial spends" count
218225
const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
219-
group_verifier.GroupAndVerify(avoid_partial_groups_filter,
226+
group_verifier.GroupAndVerify(OutputType::BECH32,
227+
avoid_partial_groups_filter,
220228
/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
221229
/*expected_without_partial_spends_size=*/ 5,
222230
/*positive_only=*/ false);

0 commit comments

Comments
 (0)