Skip to content

Commit 6b25481

Browse files
committed
Merge bitcoin/bitcoin#17331: Use effective values throughout coin selection
51a3ac2 Have OutputGroup determine the value to use (Andrew Chow) 6d6d278 Change SelectCoins_test to actually test SelectCoins (Andrew Chow) 9d3bd74 Remove CreateTransaction while loop and some related variables (Andrew Chow) 6f0d518 Remove use_bnb and bnb_used (Andrew Chow) de26eb0 Do both BnB and Knapsack coin selection in SelectCoinsMinConf (Andrew Chow) 01dc8eb Have KnapsackSolver actually use effective values (Andrew Chow) bf26e01 Roll static tx fees into nValueToSelect instead of having it be separate (Andrew Chow) cc3f14b Move output reductions for fee to after coin selection (Andrew Chow) d97d25d Make cost_of_change part of CoinSelectionParams (Andrew Chow) af5867c Move some calculations to common code in SelectCoinsMinConf (Andrew Chow) 1bf4a62 scripted-diff: rename some variables (Andrew Chow) Pull request description: Changes `KnapsackSolver` to use effective values instead of just the nominal txout value. Since fees are taken into account during the selection itself, we finally get rid of the `CreateTransaction` loop as well as a few other things that only were only necessary because of that loop. This should not change coin selection behavior at all (except maybe remove weird edge cases that were caused by the loop). In order to keep behavior the same, `KnapsackSolver` will select outputs with a negative effective value (as it did before). ACKs for top commit: ryanofsky: Code review ACK 51a3ac2. Looks good to go! instagibbs: review ACK bitcoin/bitcoin@51a3ac2 meshcollider: re-light-utACK 51a3ac2 Tree-SHA512: 372c27e00edcd5dbf85177421ba88f20bfdaf1791b6e3dc022c44876ecc379403e2375ed69e71c512c49e6af87641001ff385c4b25ab93684b3a08a53bf3824e
2 parents 7041d25 + 51a3ac2 commit 6b25481

File tree

6 files changed

+309
-380
lines changed

6 files changed

+309
-380
lines changed

src/bench/coin_selection.cpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,14 @@ static void CoinSelection(benchmark::Bench& bench)
4949
}
5050

5151
const CoinEligibilityFilter filter_standard(1, 6, 0);
52-
const CoinSelectionParams coin_selection_params(/* use_bnb= */ true, /* change_output_size= */ 34,
52+
const CoinSelectionParams coin_selection_params(/* change_output_size= */ 34,
5353
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
5454
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
5555
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
5656
bench.run([&] {
5757
std::set<CInputCoin> setCoinsRet;
5858
CAmount nValueRet;
59-
bool bnb_used;
60-
bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params, bnb_used);
59+
bool success = wallet.SelectCoinsMinConf(1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params);
6160
assert(success);
6261
assert(nValueRet == 1003 * COIN);
6362
assert(setCoinsRet.size() == 2);
@@ -101,12 +100,11 @@ static void BnBExhaustion(benchmark::Bench& bench)
101100
std::vector<OutputGroup> utxo_pool;
102101
CoinSet selection;
103102
CAmount value_ret = 0;
104-
CAmount not_input_fees = 0;
105103

106104
bench.run([&] {
107105
// Benchmark
108106
CAmount target = make_hard_case(17, utxo_pool);
109-
SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret, not_input_fees); // Should exhaust
107+
SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret); // Should exhaust
110108

111109
// Cleanup
112110
utxo_pool.clear();

src/wallet/coinselection.cpp

Lines changed: 27 additions & 25 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

@@ -49,37 +49,34 @@ struct {
4949
* @param const std::vector<CInputCoin>& utxo_pool The set of UTXOs that we are choosing from.
5050
* These UTXOs will be sorted in descending order by effective value and the CInputCoins'
5151
* values are their effective values.
52-
* @param const CAmount& target_value This is the value that we want to select. It is the lower
52+
* @param const CAmount& selection_target This is the value that we want to select. It is the lower
5353
* bound of the range.
5454
* @param const CAmount& cost_of_change This is the cost of creating and spending a change output.
55-
* This plus target_value is the upper bound of the range.
55+
* This plus selection_target is the upper bound of the range.
5656
* @param std::set<CInputCoin>& out_set -> This is an output parameter for the set of CInputCoins
5757
* that have been selected.
5858
* @param CAmount& value_ret -> This is an output parameter for the total value of the CInputCoins
5959
* that were selected.
60-
* @param CAmount not_input_fees -> The fees that need to be paid for the outputs and fixed size
61-
* overhead (version, locktime, marker and flag)
6260
*/
6361

6462
static const size_t TOTAL_TRIES = 100000;
6563

66-
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret, CAmount not_input_fees)
64+
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret)
6765
{
6866
out_set.clear();
6967
CAmount curr_value = 0;
7068

7169
std::vector<bool> curr_selection; // select the utxo at this index
7270
curr_selection.reserve(utxo_pool.size());
73-
CAmount actual_target = not_input_fees + target_value;
7471

7572
// Calculate curr_available_value
7673
CAmount curr_available_value = 0;
7774
for (const OutputGroup& utxo : utxo_pool) {
7875
// Assert that this utxo is not negative. It should never be negative, effective value calculation should have removed it
79-
assert(utxo.effective_value > 0);
80-
curr_available_value += utxo.effective_value;
76+
assert(utxo.GetSelectionAmount() > 0);
77+
curr_available_value += utxo.GetSelectionAmount();
8178
}
82-
if (curr_available_value < actual_target) {
79+
if (curr_available_value < selection_target) {
8380
return false;
8481
}
8582

@@ -94,12 +91,12 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_v
9491
for (size_t i = 0; i < TOTAL_TRIES; ++i) {
9592
// Conditions for starting a backtrack
9693
bool backtrack = false;
97-
if (curr_value + curr_available_value < actual_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
98-
curr_value > actual_target + cost_of_change || // Selected value is out of range, go back and try other branch
94+
if (curr_value + curr_available_value < selection_target || // Cannot possibly reach target with the amount remaining in the curr_available_value.
95+
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
9996
(curr_waste > best_waste && (utxo_pool.at(0).fee - utxo_pool.at(0).long_term_fee) > 0)) { // Don't select things which we know will be more wasteful if the waste is increasing
10097
backtrack = true;
101-
} else if (curr_value >= actual_target) { // Selected value is within range
102-
curr_waste += (curr_value - actual_target); // This is the excess value which is added to the waste for the below comparison
98+
} else if (curr_value >= selection_target) { // Selected value is within range
99+
curr_waste += (curr_value - selection_target); // This is the excess value which is added to the waste for the below comparison
103100
// Adding another UTXO after this check could bring the waste down if the long term fee is higher than the current fee.
104101
// However we are not going to explore that because this optimization for the waste is only done when we have hit our target
105102
// value. Adding any more UTXOs will be just burning the UTXO; it will go entirely to fees. Thus we aren't going to
@@ -112,7 +109,7 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_v
112109
break;
113110
}
114111
}
115-
curr_waste -= (curr_value - actual_target); // Remove the excess value as we will be selecting different coins now
112+
curr_waste -= (curr_value - selection_target); // Remove the excess value as we will be selecting different coins now
116113
backtrack = true;
117114
}
118115

@@ -121,7 +118,7 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_v
121118
// Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
122119
while (!curr_selection.empty() && !curr_selection.back()) {
123120
curr_selection.pop_back();
124-
curr_available_value += utxo_pool.at(curr_selection.size()).effective_value;
121+
curr_available_value += utxo_pool.at(curr_selection.size()).GetSelectionAmount();
125122
}
126123

127124
if (curr_selection.empty()) { // We have walked back to the first utxo and no branch is untraversed. All solutions searched
@@ -131,24 +128,24 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_v
131128
// Output was included on previous iterations, try excluding now.
132129
curr_selection.back() = false;
133130
OutputGroup& utxo = utxo_pool.at(curr_selection.size() - 1);
134-
curr_value -= utxo.effective_value;
131+
curr_value -= utxo.GetSelectionAmount();
135132
curr_waste -= utxo.fee - utxo.long_term_fee;
136133
} else { // Moving forwards, continuing down this branch
137134
OutputGroup& utxo = utxo_pool.at(curr_selection.size());
138135

139136
// Remove this utxo from the curr_available_value utxo amount
140-
curr_available_value -= utxo.effective_value;
137+
curr_available_value -= utxo.GetSelectionAmount();
141138

142139
// Avoid searching a branch if the previous UTXO has the same value and same waste and was excluded. Since the ratio of fee to
143140
// 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.
144141
if (!curr_selection.empty() && !curr_selection.back() &&
145-
utxo.effective_value == utxo_pool.at(curr_selection.size() - 1).effective_value &&
142+
utxo.GetSelectionAmount() == utxo_pool.at(curr_selection.size() - 1).GetSelectionAmount() &&
146143
utxo.fee == utxo_pool.at(curr_selection.size() - 1).fee) {
147144
curr_selection.push_back(false);
148145
} else {
149146
// Inclusion branch first (Largest First Exploration)
150147
curr_selection.push_back(true);
151-
curr_value += utxo.effective_value;
148+
curr_value += utxo.GetSelectionAmount();
152149
curr_waste += utxo.fee - utxo.long_term_fee;
153150
}
154151
}
@@ -230,14 +227,14 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
230227
Shuffle(groups.begin(), groups.end(), FastRandomContext());
231228

232229
for (const OutputGroup& group : groups) {
233-
if (group.m_value == nTargetValue) {
230+
if (group.GetSelectionAmount() == nTargetValue) {
234231
util::insert(setCoinsRet, group.m_outputs);
235232
nValueRet += group.m_value;
236233
return true;
237-
} else if (group.m_value < nTargetValue + MIN_CHANGE) {
234+
} else if (group.GetSelectionAmount() < nTargetValue + MIN_CHANGE) {
238235
applicable_groups.push_back(group);
239-
nTotalLower += group.m_value;
240-
} else if (!lowest_larger || group.m_value < lowest_larger->m_value) {
236+
nTotalLower += group.GetSelectionAmount();
237+
} else if (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount()) {
241238
lowest_larger = group;
242239
}
243240
}
@@ -270,7 +267,7 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
270267
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
271268
// or the next bigger coin is closer), return the bigger coin
272269
if (lowest_larger &&
273-
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->m_value <= nBest)) {
270+
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->GetSelectionAmount() <= nBest)) {
274271
util::insert(setCoinsRet, lowest_larger->m_outputs);
275272
nValueRet += lowest_larger->m_value;
276273
} else {
@@ -339,3 +336,8 @@ bool OutputGroup::EligibleForSpending(const CoinEligibilityFilter& eligibility_f
339336
&& m_ancestors <= eligibility_filter.max_ancestors
340337
&& m_descendants <= eligibility_filter.max_descendants;
341338
}
339+
340+
CAmount OutputGroup::GetSelectionAmount() const
341+
{
342+
return m_subtract_fee_outputs ? m_value : effective_value;
343+
}

src/wallet/coinselection.h

Lines changed: 50 additions & 4 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,18 +150,23 @@ 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

123-
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret, CAmount not_input_fees);
169+
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret);
124170

125171
// Original coin selection algorithm as a fallback
126172
bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& groups, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet);

0 commit comments

Comments
 (0)