Skip to content

Commit c840ab0

Browse files
committed
Merge bitcoin/bitcoin#22019: wallet: Introduce SelectionResult for encapsulating a coin selection solution
05300c1 Use SelectionResult in SelectCoins (Andrew Chow) 9d9b101 Use SelectionResult in AttemptSelection (Andrew Chow) bb50850 Use SelectionResult for waste calculation (Andrew Chow) e8f7ae5 Make an OutputGroup for preset inputs (Andrew Chow) 51a9c00 Return SelectionResult from SelectCoinsSRD (Andrew Chow) 0ef6184 Return SelectionResult from KnapsackSolver (Andrew Chow) 60d2ca7 Return SelectionResult from SelectCoinsBnB (Andrew Chow) a339add Make member variables of SelectionResult private (Andrew Chow) cbf0b9f scripted-diff: Use SelectionResult in coin selector tests (Andrew Chow) 9d1d86d Introduce SelectionResult struct (Andrew Chow) 94d851d Fix bnb_search_test to use set equivalence for (Andrew Chow) Pull request description: Instead of returning a set of selected coins and their total value as separate items, encapsulate both of these, and other variables, into a new `SelectionResult` struct. This allows us to have all of the things relevant to a coin selection solution be in a single object. `SelectionResult` enables us to implement the waste calculation in a cleaner way. All of the coin selection functions (`SelectCoinsBnB`, `KnapsackSolver`, `AttemptSelection`, and `SelectCoins`) are changed to use a `SelectionResult` as the output parameter. Based on #22009 ACKs for top commit: laanwj: Code review ACK 05300c1 Tree-SHA512: e4dbb4d78a6cda9c237d230b19e7265591efac5a101a64e6970f0654e2c4f93d13bb5d07b98e8c7b8d37321753dbfc94c28c3a7810cb1c59b5bc29b08a8493ef
2 parents 09e60df + 05300c1 commit c840ab0

File tree

6 files changed

+433
-310
lines changed

6 files changed

+433
-310
lines changed

src/bench/coin_selection.cpp

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,10 @@ static void CoinSelection(benchmark::Bench& bench)
5454
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
5555
/* tx_noinputs_size= */ 0, /* avoid_partial= */ false);
5656
bench.run([&] {
57-
std::set<CInputCoin> setCoinsRet;
58-
CAmount nValueRet;
59-
bool success = AttemptSelection(wallet, 1003 * COIN, filter_standard, coins, setCoinsRet, nValueRet, coin_selection_params);
60-
assert(success);
61-
assert(nValueRet == 1003 * COIN);
62-
assert(setCoinsRet.size() == 2);
57+
auto result = AttemptSelection(wallet, 1003 * COIN, filter_standard, coins, coin_selection_params);
58+
assert(result);
59+
assert(result->GetSelectedValue() == 1003 * COIN);
60+
assert(result->GetInputSet().size() == 2);
6361
});
6462
}
6563

@@ -92,17 +90,14 @@ static void BnBExhaustion(benchmark::Bench& bench)
9290
{
9391
// Setup
9492
std::vector<OutputGroup> utxo_pool;
95-
CoinSet selection;
96-
CAmount value_ret = 0;
9793

9894
bench.run([&] {
9995
// Benchmark
10096
CAmount target = make_hard_case(17, utxo_pool);
101-
SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret); // Should exhaust
97+
SelectCoinsBnB(utxo_pool, target, 0); // Should exhaust
10298

10399
// Cleanup
104100
utxo_pool.clear();
105-
selection.clear();
106101
});
107102
}
108103

src/wallet/coinselection.cpp

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,14 @@ struct {
5656
* bound of the range.
5757
* @param const CAmount& cost_of_change This is the cost of creating and spending a change output.
5858
* This plus selection_target is the upper bound of the range.
59-
* @param std::set<CInputCoin>& out_set -> This is an output parameter for the set of CInputCoins
60-
* that have been selected.
61-
* @param CAmount& value_ret -> This is an output parameter for the total value of the CInputCoins
62-
* that were selected.
59+
* @returns The result of this coin selection algorithm, or std::nullopt
6360
*/
6461

6562
static const size_t TOTAL_TRIES = 100000;
6663

67-
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret)
64+
std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)
6865
{
69-
out_set.clear();
66+
SelectionResult result(selection_target);
7067
CAmount curr_value = 0;
7168

7269
std::vector<bool> curr_selection; // select the utxo at this index
@@ -80,7 +77,7 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selectio
8077
curr_available_value += utxo.GetSelectionAmount();
8178
}
8279
if (curr_available_value < selection_target) {
83-
return false;
80+
return std::nullopt;
8481
}
8582

8683
// Sort the utxo_pool
@@ -156,25 +153,22 @@ bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selectio
156153

157154
// Check for solution
158155
if (best_selection.empty()) {
159-
return false;
156+
return std::nullopt;
160157
}
161158

162159
// Set output set
163-
value_ret = 0;
164160
for (size_t i = 0; i < best_selection.size(); ++i) {
165161
if (best_selection.at(i)) {
166-
util::insert(out_set, utxo_pool.at(i).m_outputs);
167-
value_ret += utxo_pool.at(i).m_value;
162+
result.AddInput(utxo_pool.at(i));
168163
}
169164
}
170165

171-
return true;
166+
return result;
172167
}
173168

174-
std::optional<std::pair<std::set<CInputCoin>, CAmount>> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value)
169+
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value)
175170
{
176-
std::set<CInputCoin> out_set;
177-
CAmount value_ret = 0;
171+
SelectionResult result(target_value);
178172

179173
std::vector<size_t> indexes;
180174
indexes.resize(utxo_pool.size());
@@ -186,10 +180,9 @@ std::optional<std::pair<std::set<CInputCoin>, CAmount>> SelectCoinsSRD(const std
186180
const OutputGroup& group = utxo_pool.at(i);
187181
Assume(group.GetSelectionAmount() > 0);
188182
selected_eff_value += group.GetSelectionAmount();
189-
value_ret += group.m_value;
190-
util::insert(out_set, group.m_outputs);
183+
result.AddInput(group);
191184
if (selected_eff_value >= target_value) {
192-
return std::make_pair(out_set, value_ret);
185+
return result;
193186
}
194187
}
195188
return std::nullopt;
@@ -241,10 +234,9 @@ static void ApproximateBestSubset(const std::vector<OutputGroup>& groups, const
241234
}
242235
}
243236

244-
bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& groups, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet)
237+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue)
245238
{
246-
setCoinsRet.clear();
247-
nValueRet = 0;
239+
SelectionResult result(nTargetValue);
248240

249241
// List of values less than target
250242
std::optional<OutputGroup> lowest_larger;
@@ -255,9 +247,8 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
255247

256248
for (const OutputGroup& group : groups) {
257249
if (group.GetSelectionAmount() == nTargetValue) {
258-
util::insert(setCoinsRet, group.m_outputs);
259-
nValueRet += group.m_value;
260-
return true;
250+
result.AddInput(group);
251+
return result;
261252
} else if (group.GetSelectionAmount() < nTargetValue + MIN_CHANGE) {
262253
applicable_groups.push_back(group);
263254
nTotalLower += group.GetSelectionAmount();
@@ -268,17 +259,15 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
268259

269260
if (nTotalLower == nTargetValue) {
270261
for (const auto& group : applicable_groups) {
271-
util::insert(setCoinsRet, group.m_outputs);
272-
nValueRet += group.m_value;
262+
result.AddInput(group);
273263
}
274-
return true;
264+
return result;
275265
}
276266

277267
if (nTotalLower < nTargetValue) {
278-
if (!lowest_larger) return false;
279-
util::insert(setCoinsRet, lowest_larger->m_outputs);
280-
nValueRet += lowest_larger->m_value;
281-
return true;
268+
if (!lowest_larger) return std::nullopt;
269+
result.AddInput(*lowest_larger);
270+
return result;
282271
}
283272

284273
// Solve subset sum by stochastic approximation
@@ -295,13 +284,11 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
295284
// or the next bigger coin is closer), return the bigger coin
296285
if (lowest_larger &&
297286
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->GetSelectionAmount() <= nBest)) {
298-
util::insert(setCoinsRet, lowest_larger->m_outputs);
299-
nValueRet += lowest_larger->m_value;
287+
result.AddInput(*lowest_larger);
300288
} else {
301289
for (unsigned int i = 0; i < applicable_groups.size(); i++) {
302290
if (vfBest[i]) {
303-
util::insert(setCoinsRet, applicable_groups[i].m_outputs);
304-
nValueRet += applicable_groups[i].m_value;
291+
result.AddInput(applicable_groups[i]);
305292
}
306293
}
307294

@@ -316,7 +303,7 @@ bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& group
316303
}
317304
}
318305

319-
return true;
306+
return result;
320307
}
321308

322309
/******************************************************************************
@@ -395,3 +382,51 @@ CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cos
395382

396383
return waste;
397384
}
385+
386+
void SelectionResult::ComputeAndSetWaste(CAmount change_cost)
387+
{
388+
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
389+
}
390+
391+
CAmount SelectionResult::GetWaste() const
392+
{
393+
Assume(m_waste != std::nullopt);
394+
return *m_waste;
395+
}
396+
397+
CAmount SelectionResult::GetSelectedValue() const
398+
{
399+
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.txout.nValue; });
400+
}
401+
402+
void SelectionResult::Clear()
403+
{
404+
m_selected_inputs.clear();
405+
m_waste.reset();
406+
}
407+
408+
void SelectionResult::AddInput(const OutputGroup& group)
409+
{
410+
util::insert(m_selected_inputs, group.m_outputs);
411+
m_use_effective = !group.m_subtract_fee_outputs;
412+
}
413+
414+
const std::set<CInputCoin>& SelectionResult::GetInputSet() const
415+
{
416+
return m_selected_inputs;
417+
}
418+
419+
std::vector<CInputCoin> SelectionResult::GetShuffledInputVector() const
420+
{
421+
std::vector<CInputCoin> coins(m_selected_inputs.begin(), m_selected_inputs.end());
422+
Shuffle(coins.begin(), coins.end(), FastRandomContext());
423+
return coins;
424+
}
425+
426+
bool SelectionResult::operator<(SelectionResult other) const
427+
{
428+
Assume(m_waste != std::nullopt);
429+
Assume(other.m_waste != std::nullopt);
430+
// As this operator is only used in std::min_element, we want the result that has more inputs when waste are equal.
431+
return *m_waste < *other.m_waste || (*m_waste == *other.m_waste && m_selected_inputs.size() > other.m_selected_inputs.size());
432+
}

src/wallet/coinselection.h

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ struct OutputGroup
187187
* where excess = selected_effective_value - target
188188
* change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size
189189
*
190+
* Note this function is separate from SelectionResult for the tests.
191+
*
190192
* @param[in] inputs The selected inputs
191193
* @param[in] change_cost The cost of creating change and spending it in the future.
192194
* Only used if there is change, in which case it must be positive.
@@ -197,18 +199,55 @@ struct OutputGroup
197199
*/
198200
[[nodiscard]] CAmount GetSelectionWaste(const std::set<CInputCoin>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
199201

200-
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret);
202+
struct SelectionResult
203+
{
204+
private:
205+
/** Set of inputs selected by the algorithm to use in the transaction */
206+
std::set<CInputCoin> m_selected_inputs;
207+
/** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */
208+
const CAmount m_target;
209+
/** Whether the input values for calculations should be the effective value (true) or normal value (false) */
210+
bool m_use_effective{false};
211+
/** The computed waste */
212+
std::optional<CAmount> m_waste;
213+
214+
public:
215+
explicit SelectionResult(const CAmount target)
216+
: m_target(target) {}
217+
218+
SelectionResult() = delete;
219+
220+
/** Get the sum of the input values */
221+
[[nodiscard]] CAmount GetSelectedValue() const;
222+
223+
void Clear();
224+
225+
void AddInput(const OutputGroup& group);
226+
227+
/** Calculates and stores the waste for this selection via GetSelectionWaste */
228+
void ComputeAndSetWaste(CAmount change_cost);
229+
[[nodiscard]] CAmount GetWaste() const;
230+
231+
/** Get m_selected_inputs */
232+
const std::set<CInputCoin>& GetInputSet() const;
233+
/** Get the vector of CInputCoins that will be used to fill in a CTransaction's vin */
234+
std::vector<CInputCoin> GetShuffledInputVector() const;
235+
236+
bool operator<(SelectionResult other) const;
237+
};
238+
239+
std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change);
201240

202241
/** Select coins by Single Random Draw. OutputGroups are selected randomly from the eligible
203242
* outputs until the target is satisfied
204243
*
205244
* @param[in] utxo_pool The positive effective value OutputGroups eligible for selection
206245
* @param[in] target_value The target value to select for
207-
* @returns If successful, a pair of set of outputs and total selected value, otherwise, std::nullopt
246+
* @returns If successful, a SelectionResult, otherwise, std::nullopt
208247
*/
209-
std::optional<std::pair<std::set<CInputCoin>, CAmount>> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value);
248+
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value);
210249

211250
// Original coin selection algorithm as a fallback
212-
bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& groups, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet);
251+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue);
213252

214253
#endif // BITCOIN_WALLET_COINSELECTION_H

0 commit comments

Comments
 (0)