Skip to content

Commit 77b0707

Browse files
committed
refactor: use CoinsResult struct in SelectCoins
Pass the whole CoinsResult struct to SelectCoins instead of only a vector. This means we now have to remove preselected coins from each OutputType vector and shuffle each vector individually. Pass the whole CoinsResult struct to AttemptSelection. This involves moving the logic in AttemptSelection to a newly named function, ChooseSelectionResult. This will allow us to run ChooseSelectionResult over each OutputType in a later commit. This ensures the backoffs work properly. Update unit and bench tests to use CoinResult.
1 parent 2e67291 commit 77b0707

File tree

4 files changed

+189
-164
lines changed

4 files changed

+189
-164
lines changed

src/bench/coin_selection.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ static void CoinSelection(benchmark::Bench& bench)
5656
addCoin(3 * COIN, wallet, wtxs);
5757

5858
// Create coins
59-
std::vector<COutput> coins;
59+
wallet::CoinsResult available_coins;
6060
for (const auto& wtx : wtxs) {
6161
const auto txout = wtx->tx->vout.at(0);
62-
coins.emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*spendable=*/true, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/ 0);
62+
available_coins.bech32.emplace_back(COutPoint(wtx->GetHash(), 0), txout, /*depth=*/6 * 24, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*spendable=*/true, /*solvable=*/true, /*safe=*/true, wtx->GetTxTime(), /*from_me=*/true, /*fees=*/ 0);
6363
}
6464

6565
const CoinEligibilityFilter filter_standard(1, 6, 0);
@@ -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, coins, coin_selection_params);
79+
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, available_coins, coin_selection_params);
8080
assert(result);
8181
assert(result->GetSelectedValue() == 1003 * COIN);
8282
assert(result->GetInputSet().size() == 2);

src/wallet/spend.cpp

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -450,20 +450,26 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
450450
return groups_out;
451451
}
452452

453-
std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector<COutput> coins,
453+
std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const CoinsResult& available_coins,
454454
const CoinSelectionParams& coin_selection_params)
455+
{
456+
std::optional<SelectionResult> result = ChooseSelectionResult(wallet, nTargetValue, eligibility_filter, available_coins.all(), coin_selection_params);
457+
return result;
458+
};
459+
460+
std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins, const CoinSelectionParams& coin_selection_params)
455461
{
456462
// Vector of results. We will choose the best one based on waste.
457463
std::vector<SelectionResult> results;
458464

459465
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
460-
std::vector<OutputGroup> positive_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, true /* positive_only */);
466+
std::vector<OutputGroup> positive_groups = GroupOutputs(wallet, available_coins, coin_selection_params, eligibility_filter, true /* positive_only */);
461467
if (auto bnb_result{SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change)}) {
462468
results.push_back(*bnb_result);
463469
}
464470

465471
// 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.
466-
std::vector<OutputGroup> all_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, false /* positive_only */);
472+
std::vector<OutputGroup> all_groups = GroupOutputs(wallet, available_coins, coin_selection_params, eligibility_filter, false /* positive_only */);
467473
CAmount target_with_change = nTargetValue;
468474
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
469475
// So we need to include that for KnapsackSolver and SRD as well, as we are expecting to create a change output.
@@ -496,9 +502,8 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
496502
return best_result;
497503
}
498504

499-
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
505+
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control, const CoinSelectionParams& coin_selection_params)
500506
{
501-
std::vector<COutput> vCoins(vAvailableCoins);
502507
CAmount value_to_select = nTargetValue;
503508

504509
OutputGroup preset_inputs(coin_selection_params);
@@ -558,13 +563,13 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
558563
return result;
559564
}
560565

561-
// remove preset inputs from vCoins so that Coin Selection doesn't pick them.
562-
for (std::vector<COutput>::iterator it = vCoins.begin(); it != vCoins.end() && coin_control.HasSelected();)
563-
{
564-
if (preset_coins.count(it->outpoint))
565-
it = vCoins.erase(it);
566-
else
567-
++it;
566+
// remove preset inputs from coins so that Coin Selection doesn't pick them.
567+
if (coin_control.HasSelected()) {
568+
available_coins.legacy.erase(remove_if(available_coins.legacy.begin(), available_coins.legacy.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.legacy.end());
569+
available_coins.P2SH_segwit.erase(remove_if(available_coins.P2SH_segwit.begin(), available_coins.P2SH_segwit.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.P2SH_segwit.end());
570+
available_coins.bech32.erase(remove_if(available_coins.bech32.begin(), available_coins.bech32.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.bech32.end());
571+
available_coins.bech32m.erase(remove_if(available_coins.bech32m.begin(), available_coins.bech32m.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.bech32m.end());
572+
available_coins.other.erase(remove_if(available_coins.other.begin(), available_coins.other.end(), [&](const COutput& c) { return preset_coins.count(c.outpoint); }), available_coins.other.end());
568573
}
569574

570575
unsigned int limit_ancestor_count = 0;
@@ -576,11 +581,15 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
576581

577582
// form groups from remaining coins; note that preset coins will not
578583
// automatically have their associated (same address) coins included
579-
if (coin_control.m_avoid_partial_spends && vCoins.size() > OUTPUT_GROUP_MAX_ENTRIES) {
584+
if (coin_control.m_avoid_partial_spends && available_coins.size() > OUTPUT_GROUP_MAX_ENTRIES) {
580585
// Cases where we have 101+ outputs all pointing to the same destination may result in
581586
// privacy leaks as they will potentially be deterministically sorted. We solve that by
582587
// explicitly shuffling the outputs before processing
583-
Shuffle(vCoins.begin(), vCoins.end(), coin_selection_params.rng_fast);
588+
Shuffle(available_coins.legacy.begin(), available_coins.legacy.end(), coin_selection_params.rng_fast);
589+
Shuffle(available_coins.P2SH_segwit.begin(), available_coins.P2SH_segwit.end(), coin_selection_params.rng_fast);
590+
Shuffle(available_coins.bech32.begin(), available_coins.bech32.end(), coin_selection_params.rng_fast);
591+
Shuffle(available_coins.bech32m.begin(), available_coins.bech32m.end(), coin_selection_params.rng_fast);
592+
Shuffle(available_coins.other.begin(), available_coins.other.end(), coin_selection_params.rng_fast);
584593
}
585594

586595
// Coin Selection attempts to select inputs from a pool of eligible UTXOs to fund the
@@ -592,34 +601,34 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
592601

593602
// If possible, fund the transaction with confirmed UTXOs only. Prefer at least six
594603
// confirmations on outputs received from other wallets and only spend confirmed change.
595-
if (auto r1{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 6, 0), vCoins, coin_selection_params)}) return r1;
596-
if (auto r2{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(1, 1, 0), vCoins, coin_selection_params)}) return r2;
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;
597606

598607
// Fall back to using zero confirmation change (but with as few ancestors in the mempool as
599608
// possible) if we cannot fund the transaction otherwise.
600609
if (wallet.m_spend_zero_conf_change) {
601-
if (auto r3{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, 2), vCoins, coin_selection_params)}) return r3;
610+
if (auto r3{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, 2), available_coins, coin_selection_params)}) return r3;
602611
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)),
603-
vCoins, coin_selection_params)}) {
612+
available_coins, coin_selection_params)}) {
604613
return r4;
605614
}
606615
if (auto r5{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, max_ancestors/2, max_descendants/2),
607-
vCoins, coin_selection_params)}) {
616+
available_coins, coin_selection_params)}) {
608617
return r5;
609618
}
610619
// If partial groups are allowed, relax the requirement of spending OutputGroups (groups
611620
// of UTXOs sent to the same address, which are obviously controlled by a single wallet)
612621
// in their entirety.
613622
if (auto r6{AttemptSelection(wallet, value_to_select, CoinEligibilityFilter(0, 1, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
614-
vCoins, coin_selection_params)}) {
623+
available_coins, coin_selection_params)}) {
615624
return r6;
616625
}
617626
// Try with unsafe inputs if they are allowed. This may spend unconfirmed outputs
618627
// received from other wallets.
619628
if (coin_control.m_include_unsafe_inputs) {
620629
if (auto r7{AttemptSelection(wallet, value_to_select,
621630
CoinEligibilityFilter(0 /* conf_mine */, 0 /* conf_theirs */, max_ancestors-1, max_descendants-1, true /* include_partial_groups */),
622-
vCoins, coin_selection_params)}) {
631+
available_coins, coin_selection_params)}) {
623632
return r7;
624633
}
625634
}
@@ -629,7 +638,7 @@ std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vec
629638
if (!fRejectLongChains) {
630639
if (auto r8{AttemptSelection(wallet, value_to_select,
631640
CoinEligibilityFilter(0, 1, std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint64_t>::max(), true /* include_partial_groups */),
632-
vCoins, coin_selection_params)}) {
641+
available_coins, coin_selection_params)}) {
633642
return r8;
634643
}
635644
}
@@ -849,7 +858,7 @@ static BResult<CreatedTransactionResult> CreateTransactionInternal(
849858
CAmount selection_target = recipients_sum + not_input_fees;
850859

851860
// Get available coins
852-
auto res_available_coins = AvailableCoins(wallet,
861+
auto available_coins = AvailableCoins(wallet,
853862
&coin_control,
854863
coin_selection_params.m_effective_feerate,
855864
1, /*nMinimumAmount*/
@@ -858,7 +867,7 @@ static BResult<CreatedTransactionResult> CreateTransactionInternal(
858867
0); /*nMaximumCount*/
859868

860869
// Choose coins to use
861-
std::optional<SelectionResult> result = SelectCoins(wallet, res_available_coins.all(), /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
870+
std::optional<SelectionResult> result = SelectCoins(wallet, available_coins, /*nTargetValue=*/selection_target, coin_control, coin_selection_params);
862871
if (!result) {
863872
return _("Insufficient funds");
864873
}

src/wallet/spend.h

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ TxSize CalculateMaximumSignedTxSize(const CTransaction& tx, const CWallet* walle
3535
* method for concatenating and returning all COutputs as one vector.
3636
*
3737
* clear(), size() methods are implemented so that one can interact with
38-
* the CoinsResult struct as if it were a vector
38+
* the CoinsResult struct as if it was a vector
3939
*/
4040
struct CoinsResult {
4141
/** Vectors for each OutputType */
@@ -100,26 +100,42 @@ std::vector<OutputGroup> GroupOutputs(const CWallet& wallet, const std::vector<C
100100
* param@[in] wallet The wallet which provides solving data for the coins
101101
* param@[in] nTargetValue The target value
102102
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
103-
* param@[in] coins The vector of coins available for selection prior to filtering
103+
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
104104
* param@[in] coin_selection_params Parameters for the coin selection
105105
* returns If successful, a SelectionResult containing the input set
106106
* If failed, a nullopt
107107
*/
108-
std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, std::vector<COutput> coins,
108+
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+
111+
/**
112+
* Attempt to find a valid input set that meets the provided eligibility filter and target.
113+
* Multiple coin selection algorithms will be run and the input set that produces the least waste
114+
* (according to the waste metric) will be chosen.
115+
*
116+
* param@[in] wallet The wallet which provides solving data for the coins
117+
* param@[in] nTargetValue The target value
118+
* param@[in] eligilibity_filter A filter containing rules for which coins are allowed to be included in this selection
119+
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
120+
* param@[in] coin_selection_params Parameters for the coin selection
121+
* returns If successful, a SelectionResult containing the input set
122+
* If failed, a nullopt
123+
*/
124+
std::optional<SelectionResult> ChooseSelectionResult(const CWallet& wallet, const CAmount& nTargetValue, const CoinEligibilityFilter& eligibility_filter, const std::vector<COutput>& available_coins,
109125
const CoinSelectionParams& coin_selection_params);
110126

111127
/**
112128
* Select a set of coins such that nTargetValue is met and at least
113129
* all coins from coin_control are selected; never select unconfirmed coins if they are not ours
114130
* param@[in] wallet The wallet which provides data necessary to spend the selected coins
115-
* param@[in] vAvailableCoins The vector of coins available to be spent
131+
* param@[in] available_coins The struct of coins, organized by OutputType, available for selection prior to filtering
116132
* param@[in] nTargetValue The target value
117133
* param@[in] coin_selection_params Parameters for this coin selection such as feerates, whether to avoid partial spends,
118134
* and whether to subtract the fee from the outputs.
119135
* returns If successful, a SelectionResult containing the selected coins
120136
* If failed, a nullopt.
121137
*/
122-
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, const std::vector<COutput>& vAvailableCoins, const CAmount& nTargetValue, const CCoinControl& coin_control,
138+
std::optional<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& available_coins, const CAmount& nTargetValue, const CCoinControl& coin_control,
123139
const CoinSelectionParams& coin_selection_params) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
124140

125141
struct CreatedTransactionResult

0 commit comments

Comments
 (0)