Skip to content

Commit 51a3ac2

Browse files
committed
Have OutputGroup determine the value to use
Instead of hijacking the effective_feerate to use the correct value during coin selection, have OutputGroup be aware of whether we are subtracting the fee from the outputs and provide the correct value to use for selection. To do this, OutputGroup now takes CoinSelectionParams and has a new function GetSelectionAmount().
1 parent 6d6d278 commit 51a3ac2

File tree

4 files changed

+78
-75
lines changed

4 files changed

+78
-75
lines changed

src/wallet/coinselection.cpp

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
struct {
1515
bool operator()(const OutputGroup& a, const OutputGroup& b) const
1616
{
17-
return a.effective_value > b.effective_value;
17+
return a.GetSelectionAmount() > b.GetSelectionAmount();
1818
}
1919
} descending;
2020

@@ -73,8 +73,8 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selectio
7373
CAmount curr_available_value = 0;
7474
for (const OutputGroup& utxo : utxo_pool) {
7575
// Assert that this utxo is not negative. It should never be negative, effective value calculation should have removed it
76-
assert(utxo.effective_value > 0);
77-
curr_available_value += utxo.effective_value;
76+
assert(utxo.GetSelectionAmount() > 0);
77+
curr_available_value += utxo.GetSelectionAmount();
7878
}
7979
if (curr_available_value < selection_target) {
8080
return false;
@@ -118,7 +118,7 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selectio
118118
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
119119
while (!curr_selection.empty() && !curr_selection.back()) {
120120
curr_selection.pop_back();
121-
curr_available_value += utxo_pool.at(curr_selection.size()).effective_value;
121+
curr_available_value += utxo_pool.at(curr_selection.size()).GetSelectionAmount();
122122
}
123123

124124
if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched
@@ -128,24 +128,24 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selectio
128128
// Output was included on previous iterations, try excluding now.
129129
curr_selection.back() = false;
130130
OutputGroup& utxo = utxo_pool.at(curr_selection.size() - 1);
131-
curr_value -= utxo.effective_value;
131+
curr_value -= utxo.GetSelectionAmount();
132132
curr_waste -= utxo.fee - utxo.long_term_fee;
133133
} else { // Moving forwards, continuing down this branch
134134
OutputGroup& utxo = utxo_pool.at(curr_selection.size());
135135

136136
// Remove this utxo from the curr_available_value utxo amount
137-
curr_available_value -= utxo.effective_value;
137+
curr_available_value -= utxo.GetSelectionAmount();
138138

139139
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. Since the ratio of fee to
140140
// long term fee is the same, we only need to check if one of those values match in order to know that the waste is the same.
141141
if (!curr_selection.empty() && !curr_selection.back() &&
142-
utxo.effective_value == utxo_pool.at(curr_selection.size() - 1).effective_value &&
142+
utxo.GetSelectionAmount() == utxo_pool.at(curr_selection.size() - 1).GetSelectionAmount() &&
143143
utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) {
144144
curr_selection.push_back(false);
145145
} else {
146146
// Inclusion branch first (Largest First Exploration)
147147
curr_selection.push_back(true);
148-
curr_value += utxo.effective_value;
148+
curr_value += utxo.GetSelectionAmount();
149149
curr_waste += utxo.fee - utxo.long_term_fee;
150150
}
151151
}
@@ -227,14 +227,14 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
227227
Shuffle(groups.begin(), groups.end(), FastRandomContext());
228228

229229
for (const OutputGroup& group : groups) {
230-
if (group.effective_value == nTargetValue) {
230+
if (group.GetSelectionAmount() == nTargetValue) {
231231
util::insert(setCoinsRet, group.m_outputs);
232232
nValueRet += group.m_value;
233233
return true;
234-
} else if (group.effective_value < nTargetValue + MIN_CHANGE) {
234+
} else if (group.GetSelectionAmount() < nTargetValue + MIN_CHANGE) {
235235
applicable_groups.push_back(group);
236-
nTotalLower += group.effective_value;
237-
} else if (!lowest_larger || group.effective_value < lowest_larger->effective_value) {
236+
nTotalLower += group.GetSelectionAmount();
237+
} else if (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount()) {
238238
lowest_larger = group;
239239
}
240240
}
@@ -267,7 +267,7 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
267267
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
268268
// or the next bigger coin is closer), return the bigger coin
269269
if (lowest_larger &&
270-
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->effective_value <= nBest)) {
270+
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->GetSelectionAmount() <= nBest)) {
271271
util::insert(setCoinsRet, lowest_larger->m_outputs);
272272
nValueRet += lowest_larger->m_value;
273273
} else {
@@ -336,3 +336,8 @@ bool OutputGroup::EligibleForSpending(const CoinEligibilityFilter& eligibility_f
336336
&& m_ancestors <= eligibility_filter.max_ancestors
337337
&& m_descendants <= eligibility_filter.max_descendants;
338338
}
339+
340+
CAmount OutputGroup::GetSelectionAmount() const
341+
{
342+
return m_subtract_fee_outputs ? m_value : effective_value;
343+
}

src/wallet/coinselection.h

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,47 @@ class CInputCoin {
5757
}
5858
};
5959

60+
/** Parameters for one iteration of Coin Selection. */
61+
struct CoinSelectionParams
62+
{
63+
/** Size of a change output in bytes, determined by the output type. */
64+
size_t change_output_size = 0;
65+
/** Size of the input to spend a change output in virtual bytes. */
66+
size_t change_spend_size = 0;
67+
/** Cost of creating the change output. */
68+
CAmount m_change_fee{0};
69+
/** Cost of creating the change output + cost of spending the change output in the future. */
70+
CAmount m_cost_of_change{0};
71+
/** The targeted feerate of the transaction being built. */
72+
CFeeRate m_effective_feerate;
73+
/** The feerate estimate used to estimate an upper bound on what should be sufficient to spend
74+
* the change output sometime in the future. */
75+
CFeeRate m_long_term_feerate;
76+
/** If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees. */
77+
CFeeRate m_discard_feerate;
78+
/** Size of the transaction before coin selection, consisting of the header and recipient
79+
* output(s), excluding the inputs and change output(s). */
80+
size_t tx_noinputs_size = 0;
81+
/** Indicate that we are subtracting the fee from outputs */
82+
bool m_subtract_fee_outputs = false;
83+
/** When true, always spend all (up to OUTPUT_GROUP_MAX_ENTRIES) or none of the outputs
84+
* associated with the same address. This helps reduce privacy leaks resulting from address
85+
* reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */
86+
bool m_avoid_partial_spends = false;
87+
88+
CoinSelectionParams(size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
89+
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) :
90+
change_output_size(change_output_size),
91+
change_spend_size(change_spend_size),
92+
m_effective_feerate(effective_feerate),
93+
m_long_term_feerate(long_term_feerate),
94+
m_discard_feerate(discard_feerate),
95+
tx_noinputs_size(tx_noinputs_size),
96+
m_avoid_partial_spends(avoid_partial)
97+
{}
98+
CoinSelectionParams() {}
99+
};
100+
60101
/** Parameters for filtering which OutputGroups we may use in coin selection.
61102
* We start by being very selective and requiring multiple confirmations and
62103
* then get more permissive if we cannot fund the transaction. */
@@ -109,15 +150,20 @@ struct OutputGroup
109150
* a lower feerate). Calculated using long term fee estimate. This is used to decide whether
110151
* it could be economical to create a change output. */
111152
CFeeRate m_long_term_feerate{0};
153+
/** Indicate that we are subtracting the fee from outputs.
154+
* When true, the value that is used for coin selection is the UTXO's real value rather than effective value */
155+
bool m_subtract_fee_outputs{false};
112156

113157
OutputGroup() {}
114-
OutputGroup(const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate) :
115-
m_effective_feerate(effective_feerate),
116-
m_long_term_feerate(long_term_feerate)
158+
OutputGroup(const CoinSelectionParams& params) :
159+
m_effective_feerate(params.m_effective_feerate),
160+
m_long_term_feerate(params.m_long_term_feerate),
161+
m_subtract_fee_outputs(params.m_subtract_fee_outputs)
117162
{}
118163

119164
void Insert(const CInputCoin& output, int depth, bool from_me, size_t ancestors, size_t descendants, bool positive_only);
120165
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
166+
CAmount GetSelectionAmount() const;
121167
};
122168

123169
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret);

src/wallet/wallet.cpp

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,20 +2399,13 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil
23992399
setCoinsRet.clear();
24002400
nValueRet = 0;
24012401

2402-
// Get the feerate for effective value.
2403-
// When subtracting the fee from the outputs, we want the effective feerate to be 0
2404-
CFeeRate effective_feerate{0};
2405-
if (!coin_selection_params.m_subtract_fee_outputs) {
2406-
effective_feerate = coin_selection_params.m_effective_feerate;
2407-
}
2408-
2409-
std::vector<OutputGroup> positive_groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, coin_selection_params.m_long_term_feerate, eligibility_filter, true /* positive_only */);
24102402
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
2403+
std::vector<OutputGroup> positive_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, true /* positive_only */);
24112404
if (SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet)) {
24122405
return true;
24132406
}
24142407
// 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.
2415-
std::vector<OutputGroup> all_groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, coin_selection_params.m_long_term_feerate, eligibility_filter, false /* positive_only */);
2408+
std::vector<OutputGroup> all_groups = GroupOutputs(coins, coin_selection_params, eligibility_filter, false /* positive_only */);
24162409
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
24172410
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
24182411
return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, all_groups, setCoinsRet, nValueRet);
@@ -4223,12 +4216,12 @@ bool CWalletTx::IsImmatureCoinBase() const
42234216
return GetBlocksToMaturity() > 0;
42244217
}
42254218

4226-
std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const
4219+
std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const
42274220
{
42284221
std::vector<OutputGroup> groups_out;
42294222

4230-
if (separate_coins) {
4231-
// Single coin means no grouping. Each COutput gets its own OutputGroup.
4223+
if (!coin_sel_params.m_avoid_partial_spends) {
4224+
// Allowing partial spends means no grouping. Each COutput gets its own OutputGroup.
42324225
for (const COutput& output : outputs) {
42334226
// Skip outputs we cannot spend
42344227
if (!output.fSpendable) continue;
@@ -4238,11 +4231,11 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu
42384231
CInputCoin input_coin = output.GetInputCoin();
42394232

42404233
// Make an OutputGroup containing just this output
4241-
OutputGroup group{effective_feerate, long_term_feerate};
4234+
OutputGroup group{coin_sel_params};
42424235
group.Insert(input_coin, output.nDepth, output.tx->IsFromMe(ISMINE_ALL), ancestors, descendants, positive_only);
42434236

42444237
// Check the OutputGroup's eligibility. Only add the eligible ones.
4245-
if (positive_only && group.effective_value <= 0) continue;
4238+
if (positive_only && group.GetSelectionAmount() <= 0) continue;
42464239
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
42474240
}
42484241
return groups_out;
@@ -4268,7 +4261,7 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu
42684261

42694262
if (groups.size() == 0) {
42704263
// No OutputGroups for this scriptPubKey yet, add one
4271-
groups.emplace_back(effective_feerate, long_term_feerate);
4264+
groups.emplace_back(coin_sel_params);
42724265
}
42734266

42744267
// Get the last OutputGroup in the vector so that we can add the CInputCoin to it
@@ -4279,7 +4272,7 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu
42794272
// to avoid surprising users with very high fees.
42804273
if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) {
42814274
// The last output group is full, add a new group to the vector and use that group for the insertion
4282-
groups.emplace_back(effective_feerate, long_term_feerate);
4275+
groups.emplace_back(coin_sel_params);
42834276
group = &groups.back();
42844277
}
42854278

@@ -4301,7 +4294,7 @@ std::vector<OutputGroup> CWallet::GroupOutputs(const std::vector<COutput>& outpu
43014294
}
43024295

43034296
// Check the OutputGroup's eligibility. Only add the eligible ones.
4304-
if (positive_only && group.effective_value <= 0) continue;
4297+
if (positive_only && group.GetSelectionAmount() <= 0) continue;
43054298
if (group.m_outputs.size() > 0 && group.EligibleForSpending(filter)) groups_out.push_back(group);
43064299
}
43074300
}

src/wallet/wallet.h

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -612,47 +612,6 @@ class COutput
612612
}
613613
};
614614

615-
/** Parameters for one iteration of Coin Selection. */
616-
struct CoinSelectionParams
617-
{
618-
/** Size of a change output in bytes, determined by the output type. */
619-
size_t change_output_size = 0;
620-
/** Size of the input to spend a change output in virtual bytes. */
621-
size_t change_spend_size = 0;
622-
/** Cost of creating the change output. */
623-
CAmount m_change_fee{0};
624-
/** Cost of creating the change output + cost of spending the change output in the future. */
625-
CAmount m_cost_of_change{0};
626-
/** The targeted feerate of the transaction being built. */
627-
CFeeRate m_effective_feerate;
628-
/** The feerate estimate used to estimate an upper bound on what should be sufficient to spend
629-
* the change output sometime in the future. */
630-
CFeeRate m_long_term_feerate;
631-
/** If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees. */
632-
CFeeRate m_discard_feerate;
633-
/** Size of the transaction before coin selection, consisting of the header and recipient
634-
* output(s), excluding the inputs and change output(s). */
635-
size_t tx_noinputs_size = 0;
636-
/** Indicate that we are subtracting the fee from outputs */
637-
bool m_subtract_fee_outputs = false;
638-
/** When true, always spend all (up to OUTPUT_GROUP_MAX_ENTRIES) or none of the outputs
639-
* associated with the same address. This helps reduce privacy leaks resulting from address
640-
* reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */
641-
bool m_avoid_partial_spends = false;
642-
643-
CoinSelectionParams(size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
644-
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) :
645-
change_output_size(change_output_size),
646-
change_spend_size(change_spend_size),
647-
m_effective_feerate(effective_feerate),
648-
m_long_term_feerate(long_term_feerate),
649-
m_discard_feerate(discard_feerate),
650-
tx_noinputs_size(tx_noinputs_size),
651-
m_avoid_partial_spends(avoid_partial)
652-
{}
653-
CoinSelectionParams() {}
654-
};
655-
656615
class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime
657616
/**
658617
* A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions.
@@ -883,7 +842,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
883842
bool IsSpentKey(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
884843
void SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
885844

886-
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool separate_coins, const CFeeRate& effective_feerate, const CFeeRate& long_term_feerate, const CoinEligibilityFilter& filter, bool positive_only) const;
845+
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, const CoinSelectionParams& coin_sel_params, const CoinEligibilityFilter& filter, bool positive_only) const;
887846

888847
/** Display address on an external signer. Returns false if external signer support is not compiled */
889848
bool DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

0 commit comments

Comments
 (0)