Skip to content

Commit 4ea3a8b

Browse files
committed
Merge bitcoin/bitcoin#25806: wallet: group outputs only once, decouple it from Coin Selection
6a302d4 wallet: single output groups filtering and grouping process (furszy) bd91ed1 wallet: unify outputs grouping process (furszy) 5596200 test: coinselector_tests refactor, use CoinsResult instead of plain std::vector (furszy) 34f54a0 wallet: decouple outputs grouping process from each ChooseSelectionResult (furszy) 461f082 refactor: make OutputGroup::m_outputs field a vector of shared_ptr (furszy) d8e749b test: wallet, add coverage for outputs grouping process (furszy) 06ec8f9 wallet: make OutputGroup "positive_only" filter explicit (furszy) Pull request description: The idea originates from bitcoin/bitcoin#24845 (comment). Note: For clarity, it's recommended to start reviewing from the end result to understand the structure of the flow. #### GroupOutputs function rationale: If "Avoid Partial Spends" is enabled, the function gathers outputs with the same script together inside a container. So Coin Selection can treats them as if them were just one possible input and either select them all or not select them. #### How the Inputs Fetch + Selection process roughly works: ``` 1. Fetch user’s manually selected inputs. 2. Fetch wallet available coins (walks through the entire wallet txes map) and insert them into a set of vectors (each vector store outputs from a single type). 3. Coin Selection Process: Call `AttemptSelection` 8 times. Each of them expands the coin eligibility filter (accepting a larger subset of coins in the calculation) until it founds a solutions or completely fails if no solutions gets founds after the 8 rounds. Each `AttemptSelection` call performs the following actions: - For each output type supported by the wallet (P2SH, P2PK, P2WPKH, P2WSH and a combination of all of them): Call ‘ChooseSelectionResult’ providing the respective, filtered by type, coins vector. Which: I. Groups the outputs vector twice (one for positive only and a second one who includes the negative ones as well). - GroupOutputs walks-through the entire inputted coins vector one time at least, + more if we are avoiding partial spends, to generate a vector of OutputGroups. II. Then performs every coin selection algorithm using the recently created vector of OutputGroup: (1) BnB, (2) knapsack and (3) SRD. III. Then returns the best solution out of them. ``` We perform the general operation of gathering outputs, with the same script, into a single container inside: Each coins selection attempt (8 times —> each coin eligibility filter), for each of the outputs vector who were filtered by type (plus another one joining all the outputs as well if needed), twice (one for the positive only outputs effective value and a second one for all of them). So, in the worst case scenario where no solution is found after the 8 Coin Selection attempts, the `GroupOutputs` function is called 80 times (8 * 5 * 2). #### Improvements: This proposal streamlines the process so that the output groups, filtered by coin eligibility and type, are created in a single loop outside of the Coin Selection Process. The new process is as follows: ``` 1. Fetch user’s manually selected inputs. 2. Fetch wallet available coins. 3. Group outputs by each coin eligibility filter and each different output type found. 4. Coin Selection Process: Call AttemptSelection 8 times. Each of them expands the coin eligibility filter (accepting different output groups) until it founds a solutions or completely fails if no solutions gets founds after the 8 rounds. Each ‘AttemptSelection’ call performs the following actions: - For each output type supported by the wallet (P2SH, P2PK, P2WPKH, P2WSH and all of them): A. Call ‘ChooseSelectionResult’ providing the respective, filtered by type, output group. Which: I. Performs every coin selection algorithm using the provided vector of OutputGroup: (1) BnB, (2) knapsack and (3) SRD. II. Then returns the best solution out of them. ``` Extra Note: The next steps after this PR will be to: 1) Merge `AvailableCoins` and `GroupOutputs` processes. 2) Skip entire coin selection rounds if no new coins are added into the subsequent round. 3) Remove global feerates from the OutputGroup class. 4) Remove secondary "grouped" tx creation from `CreateTransactionInternal` by running Coin Selection results over the aps grouped outputs vs non-aps ones. ACKs for top commit: S3RK: ReACK 6a302d4 achow101: ACK 6a302d4 theStack: re-ACK 6a302d4 🥥 Tree-SHA512: dff849063be328e7d9c358ec80239a6db2cd6131963b511b83699b95b337d3106263507eaba0119eaac63e6ac21c6c42d187ae23d79d9220b90c323d44b01d24
2 parents 5e1aab2 + 6a302d4 commit 4ea3a8b

File tree

9 files changed

+476
-149
lines changed

9 files changed

+476
-149
lines changed

src/Makefile.test.include

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ BITCOIN_TESTS += \
179179
wallet/test/ismine_tests.cpp \
180180
wallet/test/rpc_util_tests.cpp \
181181
wallet/test/scriptpubkeyman_tests.cpp \
182-
wallet/test/walletload_tests.cpp
182+
wallet/test/walletload_tests.cpp \
183+
wallet/test/group_outputs_tests.cpp
183184

184185
FUZZ_SUITE_LD_COMMON +=\
185186
$(SQLITE_LIBS) \

src/bench/coin_selection.cpp

Lines changed: 3 additions & 2 deletions
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);
@@ -91,7 +92,7 @@ static void add_coin(const CAmount& nValue, int nInput, std::vector<OutputGroup>
9192
tx.vout[nInput].nValue = nValue;
9293
COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 0, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ true, /*fees=*/ 0);
9394
set.emplace_back();
94-
set.back().Insert(output, /*ancestors=*/ 0, /*descendants=*/ 0, /*positive_only=*/ false);
95+
set.back().Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*descendants=*/ 0);
9596
}
9697
// Copied from src/wallet/test/coinselector_tests.cpp
9798
static CAmount make_hard_case(int utxos, std::vector<OutputGroup>& utxo_pool)

src/wallet/coinselection.cpp

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,9 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
333333
334334
******************************************************************************/
335335

336-
void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only) {
337-
// Filter for positive only here before adding the coin
338-
if (positive_only && output.GetEffectiveValue() <= 0) return;
339-
336+
void OutputGroup::Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants) {
340337
m_outputs.push_back(output);
341-
COutput& coin = m_outputs.back();
338+
auto& coin = *m_outputs.back();
342339

343340
fee += coin.GetFee();
344341

@@ -358,8 +355,8 @@ void OutputGroup::Insert(const COutput& output, size_t ancestors, size_t descend
358355
// coin itself; thus, this value is counted as the max, not the sum
359356
m_descendants = std::max(m_descendants, descendants);
360357

361-
if (output.input_bytes > 0) {
362-
m_weight += output.input_bytes * WITNESS_SCALE_FACTOR;
358+
if (output->input_bytes > 0) {
359+
m_weight += output->input_bytes * WITNESS_SCALE_FACTOR;
363360
}
364361
}
365362

@@ -375,15 +372,38 @@ CAmount OutputGroup::GetSelectionAmount() const
375372
return m_subtract_fee_outputs ? m_value : effective_value;
376373
}
377374

378-
CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
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+
397+
CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
379398
{
380399
// This function should not be called with empty inputs as that would mean the selection failed
381400
assert(!inputs.empty());
382401

383402
// Always consider the cost of spending an input now vs in the future.
384403
CAmount waste = 0;
385404
CAmount selected_effective_value = 0;
386-
for (const COutput& coin : inputs) {
405+
for (const auto& coin_ptr : inputs) {
406+
const COutput& coin = *coin_ptr;
387407
waste += coin.GetFee() - coin.long_term_fee;
388408
selected_effective_value += use_effective_value ? coin.GetEffectiveValue() : coin.txout.nValue;
389409
}
@@ -431,12 +451,12 @@ CAmount SelectionResult::GetWaste() const
431451

432452
CAmount SelectionResult::GetSelectedValue() const
433453
{
434-
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.txout.nValue; });
454+
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->txout.nValue; });
435455
}
436456

437457
CAmount SelectionResult::GetSelectedEffectiveValue() const
438458
{
439-
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.GetEffectiveValue(); });
459+
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin->GetEffectiveValue(); });
440460
}
441461

442462
void SelectionResult::Clear()
@@ -455,14 +475,14 @@ void SelectionResult::AddInput(const OutputGroup& group)
455475
m_weight += group.m_weight;
456476
}
457477

458-
void SelectionResult::AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs)
478+
void SelectionResult::AddInputs(const std::set<std::shared_ptr<COutput>>& inputs, bool subtract_fee_outputs)
459479
{
460480
// As it can fail, combine inputs first
461481
InsertInputs(inputs);
462482
m_use_effective = !subtract_fee_outputs;
463483

464484
m_weight += std::accumulate(inputs.cbegin(), inputs.cend(), 0, [](int sum, const auto& coin) {
465-
return sum + std::max(coin.input_bytes, 0) * WITNESS_SCALE_FACTOR;
485+
return sum + std::max(coin->input_bytes, 0) * WITNESS_SCALE_FACTOR;
466486
});
467487
}
468488

@@ -480,14 +500,14 @@ void SelectionResult::Merge(const SelectionResult& other)
480500
m_weight += other.m_weight;
481501
}
482502

483-
const std::set<COutput>& SelectionResult::GetInputSet() const
503+
const std::set<std::shared_ptr<COutput>>& SelectionResult::GetInputSet() const
484504
{
485505
return m_selected_inputs;
486506
}
487507

488-
std::vector<COutput> SelectionResult::GetShuffledInputVector() const
508+
std::vector<std::shared_ptr<COutput>> SelectionResult::GetShuffledInputVector() const
489509
{
490-
std::vector<COutput> coins(m_selected_inputs.begin(), m_selected_inputs.end());
510+
std::vector<std::shared_ptr<COutput>> coins(m_selected_inputs.begin(), m_selected_inputs.end());
491511
Shuffle(coins.begin(), coins.end(), FastRandomContext());
492512
return coins;
493513
}

src/wallet/coinselection.h

Lines changed: 39 additions & 7 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>
@@ -192,13 +193,18 @@ struct CoinEligibilityFilter
192193
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) {}
193194
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) {}
194195
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+
}
195201
};
196202

197203
/** A group of UTXOs paid to the same output script. */
198204
struct OutputGroup
199205
{
200206
/** The list of UTXOs contained in this output group. */
201-
std::vector<COutput> m_outputs;
207+
std::vector<std::shared_ptr<COutput>> m_outputs;
202208
/** Whether the UTXOs were sent by the wallet to itself. This is relevant because we may want at
203209
* least a certain number of confirmations on UTXOs received from outside wallets while trusting
204210
* our own UTXOs more. */
@@ -237,11 +243,37 @@ struct OutputGroup
237243
m_subtract_fee_outputs(params.m_subtract_fee_outputs)
238244
{}
239245

240-
void Insert(const COutput& output, size_t ancestors, size_t descendants, bool positive_only);
246+
void Insert(const std::shared_ptr<COutput>& output, size_t ancestors, size_t descendants);
241247
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
242248
CAmount GetSelectionAmount() const;
243249
};
244250

251+
struct Groups {
252+
// Stores 'OutputGroup' containing only positive UTXOs (value > 0).
253+
std::vector<OutputGroup> positive_group;
254+
// Stores 'OutputGroup' which may contain both positive and negative UTXOs.
255+
std::vector<OutputGroup> mixed_group;
256+
};
257+
258+
/** Stores several 'Groups' whose were mapped by output type. */
259+
struct OutputGroupTypeMap
260+
{
261+
// Maps output type to output groups.
262+
std::map<OutputType, Groups> groups_by_type;
263+
// All inserted groups, no type distinction.
264+
Groups all_groups;
265+
266+
// Based on the insert flag; appends group to the 'mixed_group' and, if value > 0, to the 'positive_group'.
267+
// This affects both; the groups filtered by type and the overall groups container.
268+
void Push(const OutputGroup& group, OutputType type, bool insert_positive, bool insert_mixed);
269+
// Retrieves 'Groups' filtered by type
270+
std::optional<Groups> Find(OutputType type);
271+
// Different output types count
272+
size_t TypesCount() { return groups_by_type.size(); }
273+
};
274+
275+
typedef std::map<CoinEligibilityFilter, OutputGroupTypeMap> FilteredOutputGroups;
276+
245277
/** Compute the waste for this result given the cost of change
246278
* and the opportunity cost of spending these inputs now vs in the future.
247279
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
@@ -259,7 +291,7 @@ struct OutputGroup
259291
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
260292
* @return The waste
261293
*/
262-
[[nodiscard]] CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
294+
[[nodiscard]] CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
263295

264296

265297
/** Choose a random change target for each transaction to make it harder to fingerprint the Core
@@ -292,7 +324,7 @@ struct SelectionResult
292324
{
293325
private:
294326
/** Set of inputs selected by the algorithm to use in the transaction */
295-
std::set<COutput> m_selected_inputs;
327+
std::set<std::shared_ptr<COutput>> m_selected_inputs;
296328
/** The target the algorithm selected for. Equal to the recipient amount plus non-input fees */
297329
CAmount m_target;
298330
/** The algorithm used to produce this result */
@@ -329,7 +361,7 @@ struct SelectionResult
329361
void Clear();
330362

331363
void AddInput(const OutputGroup& group);
332-
void AddInputs(const std::set<COutput>& inputs, bool subtract_fee_outputs);
364+
void AddInputs(const std::set<std::shared_ptr<COutput>>& inputs, bool subtract_fee_outputs);
333365

334366
/** Calculates and stores the waste for this selection via GetSelectionWaste */
335367
void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee);
@@ -344,9 +376,9 @@ struct SelectionResult
344376
void Merge(const SelectionResult& other);
345377

346378
/** Get m_selected_inputs */
347-
const std::set<COutput>& GetInputSet() const;
379+
const std::set<std::shared_ptr<COutput>>& GetInputSet() const;
348380
/** Get the vector of COutputs that will be used to fill in a CTransaction's vin */
349-
std::vector<COutput> GetShuffledInputVector() const;
381+
std::vector<std::shared_ptr<COutput>> GetShuffledInputVector() const;
350382

351383
bool operator<(SelectionResult other) const;
352384

0 commit comments

Comments
 (0)