Skip to content

Commit 1e52e6b

Browse files
committed
refactor coin selection for parameterizable change target
no behavior changes, since the target is always MIN_CHANGE
1 parent c9b5790 commit 1e52e6b

File tree

5 files changed

+110
-80
lines changed

5 files changed

+110
-80
lines changed

src/bench/coin_selection.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ using wallet::CWalletTx;
1919
using wallet::CoinEligibilityFilter;
2020
using wallet::CoinSelectionParams;
2121
using wallet::CreateDummyWalletDatabase;
22+
using wallet::MIN_CHANGE;
2223
using wallet::OutputGroup;
2324
using wallet::SelectCoinsBnB;
2425
using wallet::TxStateInactive;
@@ -66,6 +67,7 @@ static void CoinSelection(benchmark::Bench& bench)
6667
rand,
6768
/* change_output_size= */ 34,
6869
/* change_spend_size= */ 148,
70+
/*min_change_target=*/ MIN_CHANGE,
6971
/* effective_feerate= */ CFeeRate(0),
7072
/* long_term_feerate= */ CFeeRate(0),
7173
/* discard_feerate= */ CFeeRate(0),

src/wallet/coinselection.cpp

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,24 @@ std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& ut
187187
return std::nullopt;
188188
}
189189

190-
static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::vector<OutputGroup>& groups, const CAmount& nTotalLower, const CAmount& nTargetValue,
190+
/** Find a subset of the OutputGroups that is at least as large as, but as close as possible to, the
191+
* target amount; solve subset sum.
192+
* param@[in] groups OutputGroups to choose from, sorted by value in descending order.
193+
* param@[in] nTotalLower Total (effective) value of the UTXOs in groups.
194+
* param@[in] nTargetValue Subset sum target, not including change.
195+
* param@[out] vfBest Boolean vector representing the subset chosen that is closest to
196+
* nTargetValue, with indices corresponding to groups. If the ith
197+
* entry is true, that means the ith group in groups was selected.
198+
* param@[out] nBest Total amount of subset chosen that is closest to nTargetValue.
199+
* param@[in] iterations Maximum number of tries.
200+
*/
201+
static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::vector<OutputGroup>& groups,
202+
const CAmount& nTotalLower, const CAmount& nTargetValue,
191203
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000)
192204
{
193205
std::vector<char> vfIncluded;
194206

207+
// Worst case "best" approximation is just all of the groups.
195208
vfBest.assign(groups.size(), true);
196209
nBest = nTotalLower;
197210

@@ -217,6 +230,8 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
217230
if (nTotal >= nTargetValue)
218231
{
219232
fReachedTarget = true;
233+
// If the total is between nTargetValue and nBest, it's our new best
234+
// approximation.
220235
if (nTotal < nBest)
221236
{
222237
nBest = nTotal;
@@ -231,12 +246,15 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
231246
}
232247
}
233248

234-
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, FastRandomContext& rng)
249+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
250+
CAmount change_target, FastRandomContext& rng)
235251
{
236252
SelectionResult result(nTargetValue);
237253

238254
// List of values less than target
239255
std::optional<OutputGroup> lowest_larger;
256+
// Groups with selection amount smaller than the target and any change we might produce.
257+
// Don't include groups larger than this, because they will only cause us to overshoot.
240258
std::vector<OutputGroup> applicable_groups;
241259
CAmount nTotalLower = 0;
242260

@@ -246,7 +264,7 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
246264
if (group.GetSelectionAmount() == nTargetValue) {
247265
result.AddInput(group);
248266
return result;
249-
} else if (group.GetSelectionAmount() < nTargetValue + MIN_CHANGE) {
267+
} else if (group.GetSelectionAmount() < nTargetValue + change_target) {
250268
applicable_groups.push_back(group);
251269
nTotalLower += group.GetSelectionAmount();
252270
} else if (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount()) {
@@ -273,14 +291,14 @@ std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups,
273291
CAmount nBest;
274292

275293
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest);
276-
if (nBest != nTargetValue && nTotalLower >= nTargetValue + MIN_CHANGE) {
277-
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + MIN_CHANGE, vfBest, nBest);
294+
if (nBest != nTargetValue && nTotalLower >= nTargetValue + change_target) {
295+
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + change_target, vfBest, nBest);
278296
}
279297

280298
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
281299
// or the next bigger coin is closer), return the bigger coin
282300
if (lowest_larger &&
283-
((nBest != nTargetValue && nBest < nTargetValue + MIN_CHANGE) || lowest_larger->GetSelectionAmount() <= nBest)) {
301+
((nBest != nTargetValue && nBest < nTargetValue + change_target) || lowest_larger->GetSelectionAmount() <= nBest)) {
284302
result.AddInput(*lowest_larger);
285303
} else {
286304
for (unsigned int i = 0; i < applicable_groups.size(); i++) {

src/wallet/coinselection.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ struct CoinSelectionParams {
9494
size_t change_output_size = 0;
9595
/** Size of the input to spend a change output in virtual bytes. */
9696
size_t change_spend_size = 0;
97+
/** Mininmum change to target in Knapsack solver: select coins to cover the payment and
98+
* at least this value of change. */
99+
CAmount m_min_change_target{MIN_CHANGE};
97100
/** Cost of creating the change output. */
98101
CAmount m_change_fee{0};
99102
/** Cost of creating the change output + cost of spending the change output in the future. */
@@ -115,11 +118,13 @@ struct CoinSelectionParams {
115118
* reuse. Dust outputs are not eligible to be added to output groups and thus not considered. */
116119
bool m_avoid_partial_spends = false;
117120

118-
CoinSelectionParams(FastRandomContext& rng_fast, size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
121+
CoinSelectionParams(FastRandomContext& rng_fast, size_t change_output_size, size_t change_spend_size,
122+
CAmount min_change_target, CFeeRate effective_feerate,
119123
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial)
120124
: rng_fast{rng_fast},
121125
change_output_size(change_output_size),
122126
change_spend_size(change_spend_size),
127+
m_min_change_target(min_change_target),
123128
m_effective_feerate(effective_feerate),
124129
m_long_term_feerate(long_term_feerate),
125130
m_discard_feerate(discard_feerate),
@@ -267,7 +272,8 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
267272
std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, FastRandomContext& rng);
268273

269274
// Original coin selection algorithm as a fallback
270-
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue, FastRandomContext& rng);
275+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
276+
CAmount change_target, FastRandomContext& rng);
271277
} // namespace wallet
272278

273279
#endif // BITCOIN_WALLET_COINSELECTION_H

src/wallet/spend.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ std::optional<SelectionResult> AttemptSelection(const CWallet& wallet, const CAm
389389
std::vector<OutputGroup> all_groups = GroupOutputs(wallet, coins, coin_selection_params, eligibility_filter, false /* positive_only */);
390390
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
391391
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
392-
if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue + coin_selection_params.m_change_fee, coin_selection_params.rng_fast)}) {
392+
if (auto knapsack_result{KnapsackSolver(all_groups, nTargetValue + coin_selection_params.m_change_fee,
393+
coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
393394
knapsack_result->ComputeAndSetWaste(coin_selection_params.m_cost_of_change);
394395
results.push_back(*knapsack_result);
395396
}

0 commit comments

Comments
 (0)