Bitcoin Core 28.0.0
P2P Digital Currency
Loading...
Searching...
No Matches
coinselector_tests.cpp
Go to the documentation of this file.
1// Copyright (c) 2017-2022 The Bitcoin Core developers
2// Distributed under the MIT software license, see the accompanying
3// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5#include <consensus/amount.h>
6#include <node/context.h>
7#include <policy/policy.h>
9#include <random.h>
11#include <util/translation.h>
12#include <wallet/coincontrol.h>
14#include <wallet/spend.h>
15#include <wallet/test/util.h>
17#include <wallet/wallet.h>
18
19#include <algorithm>
20#include <boost/test/unit_test.hpp>
21#include <random>
22
23namespace wallet {
24BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup)
25
26// how many times to run all the tests to have a chance to catch errors that only show up with particular random shuffles
27#define RUN_TESTS 100
28
29// some tests fail 1% of the time due to bad luck.
30// we repeat those tests this many times and only complain if all iterations of the test fail
31#define RANDOM_REPEATS 5
32
33typedef std::set<std::shared_ptr<COutput>> CoinSet;
34
38static int nextLockTime = 0;
39
40static void add_coin(const CAmount& nValue, int nInput, std::vector<COutput>& set)
41{
43 tx.vout.resize(nInput + 1);
44 tx.vout[nInput].nValue = nValue;
45 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
46 set.emplace_back(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0);
47}
48
49static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
50{
52 tx.vout.resize(nInput + 1);
53 tx.vout[nInput].nValue = nValue;
54 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
55 COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0);
56 OutputGroup group;
57 group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*descendants=*/ 0);
58 result.AddInput(group);
59}
60
61static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result, CAmount fee, CAmount long_term_fee)
62{
64 tx.vout.resize(nInput + 1);
65 tx.vout[nInput].nValue = nValue;
66 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
67 std::shared_ptr<COutput> coin = std::make_shared<COutput>(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ 148, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, fee);
68 OutputGroup group;
69 group.Insert(coin, /*ancestors=*/ 0, /*descendants=*/ 0);
70 coin->long_term_fee = long_term_fee; // group.Insert() will modify long_term_fee, so we need to set it afterwards
71 result.AddInput(group);
72}
73
74static void add_coin(CoinsResult& available_coins, CWallet& wallet, const CAmount& nValue, CFeeRate feerate = CFeeRate(0), int nAge = 6*24, bool fIsFromMe = false, int nInput =0, bool spendable = false, int custom_size = 0)
75{
77 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
78 tx.vout.resize(nInput + 1);
79 tx.vout[nInput].nValue = nValue;
80 if (spendable) {
81 tx.vout[nInput].scriptPubKey = GetScriptForDestination(*Assert(wallet.GetNewDestination(OutputType::BECH32, "")));
82 }
83 uint256 txid = tx.GetHash();
84
85 LOCK(wallet.cs_wallet);
86 auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
87 assert(ret.second);
88 CWalletTx& wtx = (*ret.first).second;
89 const auto& txout = wtx.tx->vout.at(nInput);
90 available_coins.Add(OutputType::BECH32, {COutPoint(wtx.GetHash(), nInput), txout, nAge, custom_size == 0 ? CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr) : custom_size, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, wtx.GetTxTime(), fIsFromMe, feerate});
91}
92
93// Helpers
94std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
95 CAmount change_target, FastRandomContext& rng)
96{
97 auto res{KnapsackSolver(groups, nTargetValue, change_target, rng, MAX_STANDARD_TX_WEIGHT)};
98 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
99}
100
101std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)
102{
103 auto res{SelectCoinsBnB(utxo_pool, selection_target, cost_of_change, MAX_STANDARD_TX_WEIGHT)};
104 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
105}
106
109static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
110{
111 std::vector<CAmount> a_amts;
112 std::vector<CAmount> b_amts;
113 for (const auto& coin : a.GetInputSet()) {
114 a_amts.push_back(coin->txout.nValue);
115 }
116 for (const auto& coin : b.GetInputSet()) {
117 b_amts.push_back(coin->txout.nValue);
118 }
119 std::sort(a_amts.begin(), a_amts.end());
120 std::sort(b_amts.begin(), b_amts.end());
121
122 std::pair<std::vector<CAmount>::iterator, std::vector<CAmount>::iterator> ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin());
123 return ret.first == a_amts.end() && ret.second == b_amts.end();
124}
125
127static bool EqualResult(const SelectionResult& a, const SelectionResult& b)
128{
129 std::pair<CoinSet::iterator, CoinSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(),
130 [](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) {
131 return a->outpoint == b->outpoint;
132 });
133 return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end();
134}
135
136static CAmount make_hard_case(int utxos, std::vector<COutput>& utxo_pool)
137{
138 utxo_pool.clear();
139 CAmount target = 0;
140 for (int i = 0; i < utxos; ++i) {
141 target += CAmount{1} << (utxos+i);
142 add_coin(CAmount{1} << (utxos+i), 2*i, utxo_pool);
143 add_coin((CAmount{1} << (utxos+i)) + (CAmount{1} << (utxos-1-i)), 2*i + 1, utxo_pool);
144 }
145 return target;
146}
147
148inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& available_coins, bool subtract_fee_outputs = false)
149{
150 static std::vector<OutputGroup> static_groups;
151 static_groups.clear();
152 for (auto& coin : available_coins) {
153 static_groups.emplace_back();
154 OutputGroup& group = static_groups.back();
155 group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*descendants=*/ 0);
156 group.m_subtract_fee_outputs = subtract_fee_outputs;
157 }
158 return static_groups;
159}
160
161inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& available_coins, CWallet& wallet, const CoinEligibilityFilter& filter)
162{
163 FastRandomContext rand{};
164 CoinSelectionParams coin_selection_params{
165 rand,
166 /*change_output_size=*/ 0,
167 /*change_spend_size=*/ 0,
168 /*min_change_target=*/ CENT,
169 /*effective_feerate=*/ CFeeRate(0),
170 /*long_term_feerate=*/ CFeeRate(0),
171 /*discard_feerate=*/ CFeeRate(0),
172 /*tx_noinputs_size=*/ 0,
173 /*avoid_partial=*/ false,
174 };
175 static OutputGroupTypeMap static_groups;
176 static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter];
177 return static_groups.all_groups.mixed_group;
178}
179
180static std::unique_ptr<CWallet> NewWallet(const node::NodeContext& m_node, const std::string& wallet_name = "")
181{
182 std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), wallet_name, CreateMockableWalletDatabase());
183 BOOST_CHECK(wallet->LoadWallet() == DBErrors::LOAD_OK);
184 LOCK(wallet->cs_wallet);
185 wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
186 wallet->SetupDescriptorScriptPubKeyMans();
187 return wallet;
188}
189
190// Branch and bound coin selection tests
191BOOST_AUTO_TEST_CASE(bnb_search_test)
192{
193 FastRandomContext rand{};
194 // Setup
195 std::vector<COutput> utxo_pool;
197
199 // Known Outcome tests //
201
202 // Empty utxo pool
203 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT));
204
205 // Add utxos
206 add_coin(1 * CENT, 1, utxo_pool);
207 add_coin(2 * CENT, 2, utxo_pool);
208 add_coin(3 * CENT, 3, utxo_pool);
209 add_coin(4 * CENT, 4, utxo_pool);
210
211 // Select 1 Cent
212 add_coin(1 * CENT, 1, expected_result);
213 const auto result1 = SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT);
214 BOOST_CHECK(result1);
215 BOOST_CHECK(EquivalentResult(expected_result, *result1));
216 BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT);
217 expected_result.Clear();
218
219 // Select 2 Cent
220 add_coin(2 * CENT, 2, expected_result);
221 const auto result2 = SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT);
222 BOOST_CHECK(result2);
223 BOOST_CHECK(EquivalentResult(expected_result, *result2));
224 BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 2 * CENT);
225 expected_result.Clear();
226
227 // Select 5 Cent
228 add_coin(3 * CENT, 3, expected_result);
229 add_coin(2 * CENT, 2, expected_result);
230 const auto result3 = SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT);
231 BOOST_CHECK(result3);
232 BOOST_CHECK(EquivalentResult(expected_result, *result3));
233 BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 5 * CENT);
234 expected_result.Clear();
235
236 // Select 11 Cent, not possible
237 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT));
238 expected_result.Clear();
239
240 // Cost of change is greater than the difference between target value and utxo sum
241 add_coin(1 * CENT, 1, expected_result);
242 const auto result4 = SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT);
243 BOOST_CHECK(result4);
244 BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 1 * CENT);
245 BOOST_CHECK(EquivalentResult(expected_result, *result4));
246 expected_result.Clear();
247
248 // Cost of change is less than the difference between target value and utxo sum
249 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0));
250 expected_result.Clear();
251
252 // Select 10 Cent
253 add_coin(5 * CENT, 5, utxo_pool);
254 add_coin(4 * CENT, 4, expected_result);
255 add_coin(3 * CENT, 3, expected_result);
256 add_coin(2 * CENT, 2, expected_result);
257 add_coin(1 * CENT, 1, expected_result);
258 const auto result5 = SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT);
259 BOOST_CHECK(result5);
260 BOOST_CHECK(EquivalentResult(expected_result, *result5));
261 BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 10 * CENT);
262 expected_result.Clear();
263
264 // Select 0.25 Cent, not possible
265 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT));
266 expected_result.Clear();
267
268 // Iteration exhaustion test
269 CAmount target = make_hard_case(17, utxo_pool);
270 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 1)); // Should exhaust
271 target = make_hard_case(14, utxo_pool);
272 const auto result7 = SelectCoinsBnB(GroupCoins(utxo_pool), target, 1); // Should not exhaust
273 BOOST_CHECK(result7);
274
275 // Test same value early bailout optimization
276 utxo_pool.clear();
277 add_coin(7 * CENT, 7, expected_result);
278 add_coin(7 * CENT, 7, expected_result);
279 add_coin(7 * CENT, 7, expected_result);
280 add_coin(7 * CENT, 7, expected_result);
281 add_coin(2 * CENT, 7, expected_result);
282 add_coin(7 * CENT, 7, utxo_pool);
283 add_coin(7 * CENT, 7, utxo_pool);
284 add_coin(7 * CENT, 7, utxo_pool);
285 add_coin(7 * CENT, 7, utxo_pool);
286 add_coin(2 * CENT, 7, utxo_pool);
287 for (int i = 0; i < 50000; ++i) {
288 add_coin(5 * CENT, 7, utxo_pool);
289 }
290 const auto result8 = SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000);
291 BOOST_CHECK(result8);
292 BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 30 * CENT);
293 BOOST_CHECK(EquivalentResult(expected_result, *result8));
294
296 // Behavior tests //
298 // Select 1 Cent with pool of only greater than 5 Cent
299 utxo_pool.clear();
300 for (int i = 5; i <= 20; ++i) {
301 add_coin(i * CENT, i, utxo_pool);
302 }
303 // Run 100 times, to make sure it is never finding a solution
304 for (int i = 0; i < 100; ++i) {
305 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT));
306 }
307
308 // Make sure that effective value is working in AttemptSelection when BnB is used
309 CoinSelectionParams coin_selection_params_bnb{
310 rand,
311 /*change_output_size=*/ 31,
312 /*change_spend_size=*/ 68,
313 /*min_change_target=*/ 0,
314 /*effective_feerate=*/ CFeeRate(3000),
315 /*long_term_feerate=*/ CFeeRate(1000),
316 /*discard_feerate=*/ CFeeRate(1000),
317 /*tx_noinputs_size=*/ 0,
318 /*avoid_partial=*/ false,
319 };
320 coin_selection_params_bnb.m_change_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_output_size);
321 coin_selection_params_bnb.m_cost_of_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size) + coin_selection_params_bnb.m_change_fee;
322 coin_selection_params_bnb.min_viable_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size);
323
324 {
325 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
326
327 CoinsResult available_coins;
328
329 add_coin(available_coins, *wallet, 1, coin_selection_params_bnb.m_effective_feerate);
330 available_coins.All().at(0).input_bytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail
331 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change));
332
333 // Test fees subtracted from output:
334 available_coins.Clear();
335 add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate);
336 available_coins.All().at(0).input_bytes = 40;
337 const auto result9 = SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change);
338 BOOST_CHECK(result9);
339 BOOST_CHECK_EQUAL(result9->GetSelectedValue(), 1 * CENT);
340 }
341
342 {
343 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
344
345 CoinsResult available_coins;
346
347 coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
348 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
349 add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
350 add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
351 CCoinControl coin_control;
352 coin_control.m_allow_other_inputs = true;
353 COutput select_coin = available_coins.All().at(0);
354 coin_control.Select(select_coin.outpoint);
355 PreSelectedInputs selected_input;
356 selected_input.Insert(select_coin, coin_selection_params_bnb.m_subtract_fee_outputs);
357 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
358
359 LOCK(wallet->cs_wallet);
360 const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb);
361 BOOST_CHECK(result10);
362 }
363 {
364 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
365 LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it
366
367 CoinsResult available_coins;
368
369 // single coin should be selected when effective fee > long term fee
370 coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
371 coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
372
373 // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
374 CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
375 add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
376 add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
377 add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
378
379 expected_result.Clear();
380 add_coin(10 * CENT + input_fee, 2, expected_result);
381 CCoinControl coin_control;
382 const auto result11 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb);
383 BOOST_CHECK(EquivalentResult(expected_result, *result11));
384 available_coins.Clear();
385
386 // more coins should be selected when effective fee < long term fee
387 coin_selection_params_bnb.m_effective_feerate = CFeeRate(3000);
388 coin_selection_params_bnb.m_long_term_feerate = CFeeRate(5000);
389
390 // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
391 input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
392 add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
393 add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
394 add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
395
396 expected_result.Clear();
397 add_coin(9 * CENT + input_fee, 2, expected_result);
398 add_coin(1 * CENT + input_fee, 2, expected_result);
399 const auto result12 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb);
400 BOOST_CHECK(EquivalentResult(expected_result, *result12));
401 available_coins.Clear();
402
403 // pre selected coin should be selected even if disadvantageous
404 coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
405 coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
406
407 // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
408 input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type)
409 add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
410 add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
411 add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
412
413 expected_result.Clear();
414 add_coin(9 * CENT + input_fee, 2, expected_result);
415 add_coin(1 * CENT + input_fee, 2, expected_result);
416 coin_control.m_allow_other_inputs = true;
417 COutput select_coin = available_coins.All().at(1); // pre select 9 coin
418 coin_control.Select(select_coin.outpoint);
419 PreSelectedInputs selected_input;
420 selected_input.Insert(select_coin, coin_selection_params_bnb.m_subtract_fee_outputs);
421 available_coins.Erase({(++available_coins.coins[OutputType::BECH32].begin())->outpoint});
422 const auto result13 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb);
423 BOOST_CHECK(EquivalentResult(expected_result, *result13));
424 }
425
426 {
427 // Test bnb max weight exceeded
428 // Inputs set [10, 9, 8, 5, 3, 1], Selection Target = 16 and coin 5 exceeding the max weight.
429
430 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
431
432 CoinsResult available_coins;
433 add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
434 add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
435 add_coin(available_coins, *wallet, 8 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
436 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true, /*custom_size=*/MAX_STANDARD_TX_WEIGHT);
437 add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
438 add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
439
440 CAmount selection_target = 16 * CENT;
441 const auto& no_res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs*/true),
442 selection_target, /*cost_of_change=*/0, MAX_STANDARD_TX_WEIGHT);
443 BOOST_REQUIRE(!no_res);
444 BOOST_CHECK(util::ErrorString(no_res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
445
446 // Now add same coin value with a good size and check that it gets selected
447 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
448 const auto& res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs*/true), selection_target, /*cost_of_change=*/0);
449
450 expected_result.Clear();
451 add_coin(8 * CENT, 2, expected_result);
452 add_coin(5 * CENT, 2, expected_result);
453 add_coin(3 * CENT, 2, expected_result);
454 BOOST_CHECK(EquivalentResult(expected_result, *res));
455 }
456}
457
458BOOST_AUTO_TEST_CASE(bnb_sffo_restriction)
459{
460 // Verify the coin selection process does not produce a BnB solution when SFFO is enabled.
461 // This is currently problematic because it could require a change output. And BnB is specialized on changeless solutions.
462 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
463 WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(300, uint256{})); // set a high block so internal UTXOs are selectable
464
465 FastRandomContext rand{};
466 CoinSelectionParams params{
467 rand,
468 /*change_output_size=*/ 31, // unused value, p2wpkh output size (wallet default change type)
469 /*change_spend_size=*/ 68, // unused value, p2wpkh input size (high-r signature)
470 /*min_change_target=*/ 0, // dummy, set later
471 /*effective_feerate=*/ CFeeRate(3000),
472 /*long_term_feerate=*/ CFeeRate(1000),
473 /*discard_feerate=*/ CFeeRate(1000),
474 /*tx_noinputs_size=*/ 0,
475 /*avoid_partial=*/ false,
476 };
477 params.m_subtract_fee_outputs = true;
478 params.m_change_fee = params.m_effective_feerate.GetFee(params.change_output_size);
479 params.m_cost_of_change = params.m_discard_feerate.GetFee(params.change_spend_size) + params.m_change_fee;
480 params.m_min_change_target = params.m_cost_of_change + 1;
481 // Add spendable coin at the BnB selection upper bound
482 CoinsResult available_coins;
483 add_coin(available_coins, *wallet, COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
484 add_coin(available_coins, *wallet, 0.5 * COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
485 add_coin(available_coins, *wallet, 0.5 * COIN, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
486 // Knapsack will only find a changeless solution on an exact match to the satoshi, SRD doesn’t look for changeless
487 // If BnB were run, it would produce a single input solution with the best waste score
488 auto result = WITH_LOCK(wallet->cs_wallet, return SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, COIN, /*coin_control=*/{}, params));
489 BOOST_CHECK(result.has_value());
490 BOOST_CHECK_NE(result->GetAlgo(), SelectionAlgorithm::BNB);
491 BOOST_CHECK(result->GetInputSet().size() == 2);
492 // We have only considered BnB, SRD, and Knapsack. Test needs to be reevaluated if new algo is added
493 BOOST_CHECK(result->GetAlgo() == SelectionAlgorithm::SRD || result->GetAlgo() == SelectionAlgorithm::KNAPSACK);
494}
495
496BOOST_AUTO_TEST_CASE(knapsack_solver_test)
497{
498 FastRandomContext rand{};
499 const auto temp1{[&rand](std::vector<OutputGroup>& g, const CAmount& v, CAmount c) { return KnapsackSolver(g, v, c, rand); }};
500 const auto KnapsackSolver{temp1};
501 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
502
503 CoinsResult available_coins;
504
505 // test multiple times to allow for differences in the shuffle order
506 for (int i = 0; i < RUN_TESTS; i++)
507 {
508 available_coins.Clear();
509
510 // with an empty wallet we can't even pay one cent
512
513 add_coin(available_coins, *wallet, 1*CENT, CFeeRate(0), 4); // add a new 1 cent coin
514
515 // with a new 1 cent coin, we still can't find a mature 1 cent
517
518 // but we can find a new 1 cent
519 const auto result1 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
520 BOOST_CHECK(result1);
521 BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT);
522
523 add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin
524
525 // we can't make 3 cents of mature coins
527
528 // we can make 3 cents of new coins
529 const auto result2 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 3 * CENT, CENT);
530 BOOST_CHECK(result2);
531 BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 3 * CENT);
532
533 add_coin(available_coins, *wallet, 5*CENT); // add a mature 5 cent coin,
534 add_coin(available_coins, *wallet, 10*CENT, CFeeRate(0), 3, true); // a new 10 cent coin sent from one of our own addresses
535 add_coin(available_coins, *wallet, 20*CENT); // and a mature 20 cent coin
536
537 // now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27. total = 38
538
539 // we can't make 38 cents only if we disallow new coins:
541 // we can't even make 37 cents if we don't allow new coins even if they're from us
543 // but we can make 37 cents if we accept new coins from ourself
544 const auto result3 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 37 * CENT, CENT);
545 BOOST_CHECK(result3);
546 BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT);
547 // and we can make 38 cents if we accept all new coins
548 const auto result4 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 38 * CENT, CENT);
549 BOOST_CHECK(result4);
550 BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 38 * CENT);
551
552 // try making 34 cents from 1,2,5,10,20 - we can't do it exactly
553 const auto result5 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 34 * CENT, CENT);
554 BOOST_CHECK(result5);
555 BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 35 * CENT); // but 35 cents is closest
556 BOOST_CHECK_EQUAL(result5->GetInputSet().size(), 3U); // the best should be 20+10+5. it's incredibly unlikely the 1 or 2 got included (but possible)
557
558 // when we try making 7 cents, the smaller coins (1,2,5) are enough. We should see just 2+5
559 const auto result6 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 7 * CENT, CENT);
560 BOOST_CHECK(result6);
561 BOOST_CHECK_EQUAL(result6->GetSelectedValue(), 7 * CENT);
562 BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U);
563
564 // when we try making 8 cents, the smaller coins (1,2,5) are exactly enough.
565 const auto result7 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 8 * CENT, CENT);
566 BOOST_CHECK(result7);
567 BOOST_CHECK(result7->GetSelectedValue() == 8 * CENT);
568 BOOST_CHECK_EQUAL(result7->GetInputSet().size(), 3U);
569
570 // when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10)
571 const auto result8 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 9 * CENT, CENT);
572 BOOST_CHECK(result8);
573 BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 10 * CENT);
574 BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U);
575
576 // now clear out the wallet and start again to test choosing between subsets of smaller coins and the next biggest coin
577 available_coins.Clear();
578
579 add_coin(available_coins, *wallet, 6*CENT);
580 add_coin(available_coins, *wallet, 7*CENT);
581 add_coin(available_coins, *wallet, 8*CENT);
582 add_coin(available_coins, *wallet, 20*CENT);
583 add_coin(available_coins, *wallet, 30*CENT); // now we have 6+7+8+20+30 = 71 cents total
584
585 // check that we have 71 and not 72
586 const auto result9 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 71 * CENT, CENT);
587 BOOST_CHECK(result9);
589
590 // now try making 16 cents. the best smaller coins can do is 6+7+8 = 21; not as good at the next biggest coin, 20
591 const auto result10 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
592 BOOST_CHECK(result10);
593 BOOST_CHECK_EQUAL(result10->GetSelectedValue(), 20 * CENT); // we should get 20 in one coin
594 BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U);
595
596 add_coin(available_coins, *wallet, 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total
597
598 // now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, better than the next biggest coin, 20
599 const auto result11 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
600 BOOST_CHECK(result11);
601 BOOST_CHECK_EQUAL(result11->GetSelectedValue(), 18 * CENT); // we should get 18 in 3 coins
602 BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U);
603
604 add_coin(available_coins, *wallet, 18*CENT); // now we have 5+6+7+8+18+20+30
605
606 // and now if we try making 16 cents again, the smaller coins can make 5+6+7 = 18 cents, the same as the next biggest coin, 18
607 const auto result12 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
608 BOOST_CHECK(result12);
609 BOOST_CHECK_EQUAL(result12->GetSelectedValue(), 18 * CENT); // we should get 18 in 1 coin
610 BOOST_CHECK_EQUAL(result12->GetInputSet().size(), 1U); // because in the event of a tie, the biggest coin wins
611
612 // now try making 11 cents. we should get 5+6
613 const auto result13 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 11 * CENT, CENT);
614 BOOST_CHECK(result13);
615 BOOST_CHECK_EQUAL(result13->GetSelectedValue(), 11 * CENT);
616 BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U);
617
618 // check that the smallest bigger coin is used
619 add_coin(available_coins, *wallet, 1*COIN);
620 add_coin(available_coins, *wallet, 2*COIN);
621 add_coin(available_coins, *wallet, 3*COIN);
622 add_coin(available_coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents
623 const auto result14 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 95 * CENT, CENT);
624 BOOST_CHECK(result14);
625 BOOST_CHECK_EQUAL(result14->GetSelectedValue(), 1 * COIN); // we should get 1 BTC in 1 coin
626 BOOST_CHECK_EQUAL(result14->GetInputSet().size(), 1U);
627
628 const auto result15 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 195 * CENT, CENT);
629 BOOST_CHECK(result15);
630 BOOST_CHECK_EQUAL(result15->GetSelectedValue(), 2 * COIN); // we should get 2 BTC in 1 coin
631 BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U);
632
633 // empty the wallet and start again, now with fractions of a cent, to test small change avoidance
634
635 available_coins.Clear();
636 add_coin(available_coins, *wallet, CENT * 1 / 10);
637 add_coin(available_coins, *wallet, CENT * 2 / 10);
638 add_coin(available_coins, *wallet, CENT * 3 / 10);
639 add_coin(available_coins, *wallet, CENT * 4 / 10);
640 add_coin(available_coins, *wallet, CENT * 5 / 10);
641
642 // try making 1 * CENT from the 1.5 * CENT
643 // we'll get change smaller than CENT whatever happens, so can expect CENT exactly
644 const auto result16 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT);
645 BOOST_CHECK(result16);
646 BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT);
647
648 // but if we add a bigger coin, small change is avoided
649 add_coin(available_coins, *wallet, 1111*CENT);
650
651 // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5
652 const auto result17 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
653 BOOST_CHECK(result17);
654 BOOST_CHECK_EQUAL(result17->GetSelectedValue(), 1 * CENT); // we should get the exact amount
655
656 // if we add more small coins:
657 add_coin(available_coins, *wallet, CENT * 6 / 10);
658 add_coin(available_coins, *wallet, CENT * 7 / 10);
659
660 // and try again to make 1.0 * CENT
661 const auto result18 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
662 BOOST_CHECK(result18);
663 BOOST_CHECK_EQUAL(result18->GetSelectedValue(), 1 * CENT); // we should get the exact amount
664
665 // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf)
666 // they tried to consolidate 10 50k coins into one 500k coin, and ended up with 50k in change
667 available_coins.Clear();
668 for (int j = 0; j < 20; j++)
669 add_coin(available_coins, *wallet, 50000 * COIN);
670
671 const auto result19 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 500000 * COIN, CENT);
672 BOOST_CHECK(result19);
673 BOOST_CHECK_EQUAL(result19->GetSelectedValue(), 500000 * COIN); // we should get the exact amount
674 BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins
675
676 // if there's not enough in the smaller coins to make at least 1 * CENT change (0.5+0.6+0.7 < 1.0+1.0),
677 // we need to try finding an exact subset anyway
678
679 // sometimes it will fail, and so we use the next biggest coin:
680 available_coins.Clear();
681 add_coin(available_coins, *wallet, CENT * 5 / 10);
682 add_coin(available_coins, *wallet, CENT * 6 / 10);
683 add_coin(available_coins, *wallet, CENT * 7 / 10);
684 add_coin(available_coins, *wallet, 1111 * CENT);
685 const auto result20 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
686 BOOST_CHECK(result20);
687 BOOST_CHECK_EQUAL(result20->GetSelectedValue(), 1111 * CENT); // we get the bigger coin
688 BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U);
689
690 // but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = 1.0)
691 available_coins.Clear();
692 add_coin(available_coins, *wallet, CENT * 4 / 10);
693 add_coin(available_coins, *wallet, CENT * 6 / 10);
694 add_coin(available_coins, *wallet, CENT * 8 / 10);
695 add_coin(available_coins, *wallet, 1111 * CENT);
696 const auto result21 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT);
697 BOOST_CHECK(result21);
698 BOOST_CHECK_EQUAL(result21->GetSelectedValue(), CENT); // we should get the exact amount
699 BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6
700
701 // test avoiding small change
702 available_coins.Clear();
703 add_coin(available_coins, *wallet, CENT * 5 / 100);
704 add_coin(available_coins, *wallet, CENT * 1);
705 add_coin(available_coins, *wallet, CENT * 100);
706
707 // trying to make 100.01 from these three coins
708 const auto result22 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 10001 / 100, CENT);
709 BOOST_CHECK(result22);
710 BOOST_CHECK_EQUAL(result22->GetSelectedValue(), CENT * 10105 / 100); // we should get all coins
711 BOOST_CHECK_EQUAL(result22->GetInputSet().size(), 3U);
712
713 // but if we try to make 99.9, we should take the bigger of the two small coins to avoid small change
714 const auto result23 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 9990 / 100, CENT);
715 BOOST_CHECK(result23);
716 BOOST_CHECK_EQUAL(result23->GetSelectedValue(), 101 * CENT);
717 BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U);
718 }
719
720 // test with many inputs
721 for (CAmount amt=1500; amt < COIN; amt*=10) {
722 available_coins.Clear();
723 // Create 676 inputs (= (old MAX_STANDARD_TX_SIZE == 100000) / 148 bytes per input)
724 for (uint16_t j = 0; j < 676; j++)
725 add_coin(available_coins, *wallet, amt);
726
727 // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
728 for (int i = 0; i < RUN_TESTS; i++) {
729 const auto result24 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 2000, CENT);
730 BOOST_CHECK(result24);
731
732 if (amt - 2000 < CENT) {
733 // needs more than one input:
734 uint16_t returnSize = std::ceil((2000.0 + CENT)/amt);
735 CAmount returnValue = amt * returnSize;
736 BOOST_CHECK_EQUAL(result24->GetSelectedValue(), returnValue);
737 BOOST_CHECK_EQUAL(result24->GetInputSet().size(), returnSize);
738 } else {
739 // one input is sufficient:
740 BOOST_CHECK_EQUAL(result24->GetSelectedValue(), amt);
741 BOOST_CHECK_EQUAL(result24->GetInputSet().size(), 1U);
742 }
743 }
744 }
745
746 // test randomness
747 {
748 available_coins.Clear();
749 for (int i2 = 0; i2 < 100; i2++)
750 add_coin(available_coins, *wallet, COIN);
751
752 // Again, we only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
753 for (int i = 0; i < RUN_TESTS; i++) {
754 // picking 50 from 100 coins doesn't depend on the shuffle,
755 // but does depend on randomness in the stochastic approximation code
756 const auto result25 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT);
757 BOOST_CHECK(result25);
758 const auto result26 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT);
759 BOOST_CHECK(result26);
760 BOOST_CHECK(!EqualResult(*result25, *result26));
761
762 int fails = 0;
763 for (int j = 0; j < RANDOM_REPEATS; j++)
764 {
765 // Test that the KnapsackSolver selects randomly from equivalent coins (same value and same input size).
766 // When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice
767 // which will cause it to fail.
768 // To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail
769 const auto result27 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT);
770 BOOST_CHECK(result27);
771 const auto result28 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT);
772 BOOST_CHECK(result28);
773 if (EqualResult(*result27, *result28))
774 fails++;
775 }
777 }
778
779 // add 75 cents in small change. not enough to make 90 cents,
780 // then try making 90 cents. there are multiple competing "smallest bigger" coins,
781 // one of which should be picked at random
782 add_coin(available_coins, *wallet, 5 * CENT);
783 add_coin(available_coins, *wallet, 10 * CENT);
784 add_coin(available_coins, *wallet, 15 * CENT);
785 add_coin(available_coins, *wallet, 20 * CENT);
786 add_coin(available_coins, *wallet, 25 * CENT);
787
788 for (int i = 0; i < RUN_TESTS; i++) {
789 int fails = 0;
790 for (int j = 0; j < RANDOM_REPEATS; j++)
791 {
792 const auto result29 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT);
793 BOOST_CHECK(result29);
794 const auto result30 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT);
795 BOOST_CHECK(result30);
796 if (EqualResult(*result29, *result30))
797 fails++;
798 }
800 }
801 }
802}
803
805{
806 FastRandomContext rand{};
807 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
808
809 CoinsResult available_coins;
810
811 // Test vValue sort order
812 for (int i = 0; i < 1000; i++)
813 add_coin(available_coins, *wallet, 1000 * COIN);
814 add_coin(available_coins, *wallet, 3 * COIN);
815
816 const auto result = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1003 * COIN, CENT, rand);
817 BOOST_CHECK(result);
818 BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN);
819 BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U);
820}
821
822// Tests that with the ideal conditions, the coin selector will always be able to find a solution that can pay the target value
823BOOST_AUTO_TEST_CASE(SelectCoins_test)
824{
825 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
826 LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it
827
828 // Random generator stuff
829 std::default_random_engine generator;
830 std::exponential_distribution<double> distribution (100);
832
833 // Run this test 100 times
834 for (int i = 0; i < 100; ++i)
835 {
836 CoinsResult available_coins;
837 CAmount balance{0};
838
839 // Make a wallet with 1000 exponentially distributed random inputs
840 for (int j = 0; j < 1000; ++j)
841 {
842 CAmount val = distribution(generator)*10000000;
843 add_coin(available_coins, *wallet, val);
844 balance += val;
845 }
846
847 // Generate a random fee rate in the range of 100 - 400
848 CFeeRate rate(rand.randrange(300) + 100);
849
850 // Generate a random target value between 1000 and wallet balance
851 CAmount target = rand.randrange(balance - 1000) + 1000;
852
853 // Perform selection
854 CoinSelectionParams cs_params{
855 rand,
856 /*change_output_size=*/ 34,
857 /*change_spend_size=*/ 148,
858 /*min_change_target=*/ CENT,
859 /*effective_feerate=*/ CFeeRate(0),
860 /*long_term_feerate=*/ CFeeRate(0),
861 /*discard_feerate=*/ CFeeRate(0),
862 /*tx_noinputs_size=*/ 0,
863 /*avoid_partial=*/ false,
864 };
865 cs_params.m_cost_of_change = 1;
866 cs_params.min_viable_change = 1;
867 CCoinControl cc;
868 const auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, target, cc, cs_params);
869 BOOST_CHECK(result);
870 BOOST_CHECK_GE(result->GetSelectedValue(), target);
871 }
872}
873
875{
876 const CAmount fee{100};
877 const CAmount min_viable_change{300};
878 const CAmount change_cost{125};
879 const CAmount change_fee{30};
880 const CAmount fee_diff{40};
881 const CAmount in_amt{3 * COIN};
882 const CAmount target{2 * COIN};
883 const CAmount excess{80};
884 const CAmount exact_target{in_amt - fee * 2}; // Maximum spendable amount after fees: no change, no excess
885
886 // In the following, we test that the waste is calculated correctly in various scenarios.
887 // Usually, RecalculateWaste would compute change_fee and change_cost on basis of the
888 // change output type, current feerate, and discard_feerate, but we use fixed values
889 // across this test to make the test easier to understand.
890 {
891 // Waste with change is the change cost and difference between fee and long term fee
893 add_coin(1 * COIN, 1, selection1, /*fee=*/fee, /*long_term_fee=*/fee - fee_diff);
894 add_coin(2 * COIN, 2, selection1, fee, fee - fee_diff);
895 selection1.RecalculateWaste(min_viable_change, change_cost, change_fee);
896 BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, selection1.GetWaste());
897
898 // Waste will be greater when fee is greater, but long term fee is the same
900 add_coin(1 * COIN, 1, selection2, fee * 2, fee - fee_diff);
901 add_coin(2 * COIN, 2, selection2, fee * 2, fee - fee_diff);
902 selection2.RecalculateWaste(min_viable_change, change_cost, change_fee);
903 BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste());
904
905 // Waste with change is the change cost and difference between fee and long term fee
906 // With long term fee greater than fee, waste should be less than when long term fee is less than fee
908 add_coin(1 * COIN, 1, selection3, fee, fee + fee_diff);
909 add_coin(2 * COIN, 2, selection3, fee, fee + fee_diff);
910 selection3.RecalculateWaste(min_viable_change, change_cost, change_fee);
911 BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, selection3.GetWaste());
912 BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste());
913 }
914
915 {
916 // Waste without change is the excess and difference between fee and long term fee
917 SelectionResult selection_nochange1{exact_target - excess, SelectionAlgorithm::MANUAL};
918 add_coin(1 * COIN, 1, selection_nochange1, fee, fee - fee_diff);
919 add_coin(2 * COIN, 2, selection_nochange1, fee, fee - fee_diff);
920 selection_nochange1.RecalculateWaste(min_viable_change, change_cost, change_fee);
921 BOOST_CHECK_EQUAL(fee_diff * 2 + excess, selection_nochange1.GetWaste());
922
923 // Waste without change is the excess and difference between fee and long term fee
924 // With long term fee greater than fee, waste should be less than when long term fee is less than fee
925 SelectionResult selection_nochange2{exact_target - excess, SelectionAlgorithm::MANUAL};
926 add_coin(1 * COIN, 1, selection_nochange2, fee, fee + fee_diff);
927 add_coin(2 * COIN, 2, selection_nochange2, fee, fee + fee_diff);
928 selection_nochange2.RecalculateWaste(min_viable_change, change_cost, change_fee);
929 BOOST_CHECK_EQUAL(fee_diff * -2 + excess, selection_nochange2.GetWaste());
930 BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste());
931 }
932
933 {
934 // Waste with change and fee == long term fee is just cost of change
936 add_coin(1 * COIN, 1, selection, fee, fee);
937 add_coin(2 * COIN, 2, selection, fee, fee);
938 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
939 BOOST_CHECK_EQUAL(change_cost, selection.GetWaste());
940 }
941
942 {
943 // Waste without change and fee == long term fee is just the excess
944 SelectionResult selection{exact_target - excess, SelectionAlgorithm::MANUAL};
945 add_coin(1 * COIN, 1, selection, fee, fee);
946 add_coin(2 * COIN, 2, selection, fee, fee);
947 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
948 BOOST_CHECK_EQUAL(excess, selection.GetWaste());
949 }
950
951 {
952 // Waste is 0 when fee == long_term_fee, no change, and no excess
953 SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
954 add_coin(1 * COIN, 1, selection, fee, fee);
955 add_coin(2 * COIN, 2, selection, fee, fee);
956 selection.RecalculateWaste(min_viable_change, change_cost , change_fee);
957 BOOST_CHECK_EQUAL(0, selection.GetWaste());
958 }
959
960 {
961 // Waste is 0 when (fee - long_term_fee) == (-cost_of_change), and no excess
963 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
964 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
965 selection.RecalculateWaste(min_viable_change, /*change_cost=*/fee_diff * 2, change_fee);
966 BOOST_CHECK_EQUAL(0, selection.GetWaste());
967 }
968
969 {
970 // Waste is 0 when (fee - long_term_fee) == (-excess), no change cost
971 const CAmount new_target{exact_target - /*excess=*/fee_diff * 2};
972 SelectionResult selection{new_target, SelectionAlgorithm::MANUAL};
973 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
974 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
975 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
976 BOOST_CHECK_EQUAL(0, selection.GetWaste());
977 }
978
979 {
980 // Negative waste when the long term fee is greater than the current fee and the selected value == target
981 SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
982 const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff))
983 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
984 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
985 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
986 BOOST_CHECK_EQUAL(target_waste1, selection.GetWaste());
987 }
988
989 {
990 // Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee))
992 const CAmount large_fee_diff{90};
993 const CAmount target_waste2{-2 * large_fee_diff + change_cost};
994 // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost
995 // = (2 * 100) - (2 * (100 + 90)) + 125
996 // = 200 - 380 + 125 = -55
997 assert(target_waste2 == -55);
998 add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff);
999 add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff);
1000 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
1001 BOOST_CHECK_EQUAL(target_waste2, selection.GetWaste());
1002 }
1003}
1004
1005
1007{
1008 const CAmount fee{100};
1009 const CAmount min_viable_change{200};
1010 const CAmount change_cost{125};
1011 const CAmount change_fee{35};
1012 const CAmount fee_diff{40};
1013 const CAmount target{2 * COIN};
1014
1015 {
1017 add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
1018 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
1019 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
1020
1021 for (size_t i = 0; i < inputs.size(); ++i) {
1022 inputs[i]->ApplyBumpFee(20*(i+1));
1023 }
1024
1025 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
1026 CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60;
1027 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
1028
1029 selection.SetBumpFeeDiscount(30);
1030 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
1031 expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60 - /*group_discount=*/30;
1032 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
1033 }
1034
1035 {
1036 // Test with changeless transaction
1037 //
1038 // Bump fees and excess both contribute fully to the waste score,
1039 // therefore, a bump fee group discount will not change the waste
1040 // score as long as we do not create change in both instances.
1041 CAmount changeless_target = 3 * COIN - 2 * fee - 100;
1042 SelectionResult selection{changeless_target, SelectionAlgorithm::MANUAL};
1043 add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
1044 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
1045 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
1046
1047 for (size_t i = 0; i < inputs.size(); ++i) {
1048 inputs[i]->ApplyBumpFee(20*(i+1));
1049 }
1050
1051 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
1052 CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40;
1053 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
1054
1055 selection.SetBumpFeeDiscount(30);
1056 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
1057 expected_waste = fee_diff * -2 + /*bump_fees=*/60 - /*group_discount=*/30 + /*excess = 100 - bump_fees + group_discount*/70;
1058 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
1059 }
1060}
1061
1062BOOST_AUTO_TEST_CASE(effective_value_test)
1063{
1064 const int input_bytes = 148;
1065 const CFeeRate feerate(1000);
1066 const CAmount nValue = 10000;
1067 const int nInput = 0;
1068
1070 tx.vout.resize(1);
1071 tx.vout[nInput].nValue = nValue;
1072
1073 // standard case, pass feerate in constructor
1074 COutput output1(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, feerate);
1075 const CAmount expected_ev1 = 9852; // 10000 - 148
1076 BOOST_CHECK_EQUAL(output1.GetEffectiveValue(), expected_ev1);
1077
1078 // input bytes unknown (input_bytes = -1), pass feerate in constructor
1079 COutput output2(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, feerate);
1080 BOOST_CHECK_EQUAL(output2.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
1081
1082 // negative effective value, pass feerate in constructor
1083 COutput output3(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, CFeeRate(100000));
1084 const CAmount expected_ev3 = -4800; // 10000 - 14800
1085 BOOST_CHECK_EQUAL(output3.GetEffectiveValue(), expected_ev3);
1086
1087 // standard case, pass fees in constructor
1088 const CAmount fees = 148;
1089 COutput output4(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, input_bytes, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, fees);
1090 BOOST_CHECK_EQUAL(output4.GetEffectiveValue(), expected_ev1);
1091
1092 // input bytes unknown (input_bytes = -1), pass fees in constructor
1093 COutput output5(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0);
1094 BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
1095}
1096
1098 const CoinSelectionParams& cs_params,
1100 int max_selection_weight,
1101 std::function<CoinsResult(CWallet&)> coin_setup)
1102{
1103 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1104 CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors
1105 Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups;
1106 return CoinGrinder(group.positive_group, target, cs_params.m_min_change_target, max_selection_weight);
1107}
1108
1109BOOST_AUTO_TEST_CASE(coin_grinder_tests)
1110{
1111 // Test Coin Grinder:
1112 // 1) Insufficient funds, select all provided coins and fail.
1113 // 2) Exceeded max weight, coin selection always surpasses the max allowed weight.
1114 // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not)
1115 // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
1116 // 5) Test finding a solution in a UTXO pool with mixed weights
1117 // 6) Test that the lightest solution among many clones is found
1118 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
1119
1120 FastRandomContext rand;
1121 CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag.
1122 rand,
1123 /*change_output_size=*/34,
1124 /*change_spend_size=*/68,
1125 /*min_change_target=*/CENT,
1126 /*effective_feerate=*/CFeeRate(5000),
1127 /*long_term_feerate=*/CFeeRate(2000),
1128 /*discard_feerate=*/CFeeRate(1000),
1129 /*tx_noinputs_size=*/10 + 34, // static header size + output size
1130 /*avoid_partial=*/false,
1131 };
1132
1133 {
1134 // #########################################################
1135 // 1) Insufficient funds, select all provided coins and fail
1136 // #########################################################
1137 CAmount target = 49.5L * COIN;
1138 int max_selection_weight = 10'000; // high enough to not fail for this reason.
1139 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1140 CoinsResult available_coins;
1141 for (int j = 0; j < 10; ++j) {
1142 add_coin(available_coins, wallet, CAmount(1 * COIN));
1143 add_coin(available_coins, wallet, CAmount(2 * COIN));
1144 }
1145 return available_coins;
1146 });
1147 BOOST_CHECK(!res);
1148 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds"
1149 }
1150
1151 {
1152 // ###########################
1153 // 2) Test max weight exceeded
1154 // ###########################
1155 CAmount target = 29.5L * COIN;
1156 int max_selection_weight = 3000;
1157 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1158 CoinsResult available_coins;
1159 for (int j = 0; j < 10; ++j) {
1160 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true);
1161 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
1162 }
1163 return available_coins;
1164 });
1165 BOOST_CHECK(!res);
1166 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
1167 }
1168
1169 {
1170 // ###############################################################################################################
1171 // 3) Test selection when some coins surpass the max allowed weight while others not. --> must find a good solution
1172 // ################################################################################################################
1173 CAmount target = 25.33L * COIN;
1174 int max_selection_weight = 10'000; // WU
1175 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1176 CoinsResult available_coins;
1177 for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU
1178 add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(5000), 144, false, 0, true);
1179 }
1180 for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU
1181 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
1182 }
1183 return available_coins;
1184 });
1185 BOOST_CHECK(res);
1186 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1187 size_t expected_attempts = 37;
1188 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1189 }
1190
1191 {
1192 // #################################################################################################################
1193 // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
1194 // #################################################################################################################
1195 CAmount target = 1.9L * COIN;
1196 int max_selection_weight = 400'000; // WU
1197 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1198 CoinsResult available_coins;
1199 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 148);
1200 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68);
1201 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68);
1202 return available_coins;
1203 });
1204 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1205 add_coin(1 * COIN, 1, expected_result);
1206 add_coin(1 * COIN, 2, expected_result);
1207 BOOST_CHECK(EquivalentResult(expected_result, *res));
1208 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1209 size_t expected_attempts = 3;
1210 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1211 }
1212
1213 {
1214 // ###############################################################################################################
1215 // 5) Test finding a solution in a UTXO pool with mixed weights
1216 // ################################################################################################################
1217 CAmount target = 30L * COIN;
1218 int max_selection_weight = 400'000; // WU
1219 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1220 CoinsResult available_coins;
1221 for (int j = 0; j < 5; ++j) {
1222 // Add heavy coins {3, 6, 9, 12, 15}
1223 add_coin(available_coins, wallet, CAmount((3 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 350);
1224 // Add medium coins {2, 5, 8, 11, 14}
1225 add_coin(available_coins, wallet, CAmount((2 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 250);
1226 // Add light coins {1, 4, 7, 10, 13}
1227 add_coin(available_coins, wallet, CAmount((1 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 150);
1228 }
1229 return available_coins;
1230 });
1231 BOOST_CHECK(res);
1232 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1233 add_coin(14 * COIN, 1, expected_result);
1234 add_coin(13 * COIN, 2, expected_result);
1235 add_coin(4 * COIN, 3, expected_result);
1236 BOOST_CHECK(EquivalentResult(expected_result, *res));
1237 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1238 size_t expected_attempts = 92;
1239 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1240 }
1241
1242 {
1243 // #################################################################################################################
1244 // 6) Test that the lightest solution among many clones is found
1245 // #################################################################################################################
1246 CAmount target = 9.9L * COIN;
1247 int max_selection_weight = 400'000; // WU
1248 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1249 CoinsResult available_coins;
1250 // Expected Result: 4 + 3 + 2 + 1 = 10 BTC at 400 vB
1251 add_coin(available_coins, wallet, CAmount(4 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1252 add_coin(available_coins, wallet, CAmount(3 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1253 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1254 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1255 // Distracting clones:
1256 for (int j = 0; j < 100; ++j) {
1257 add_coin(available_coins, wallet, CAmount(8 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1258 }
1259 for (int j = 0; j < 100; ++j) {
1260 add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800);
1261 }
1262 for (int j = 0; j < 100; ++j) {
1263 add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600);
1264 }
1265 for (int j = 0; j < 100; ++j) {
1266 add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400);
1267 }
1268 return available_coins;
1269 });
1270 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1271 add_coin(4 * COIN, 0, expected_result);
1272 add_coin(3 * COIN, 0, expected_result);
1273 add_coin(2 * COIN, 0, expected_result);
1274 add_coin(1 * COIN, 0, expected_result);
1275 BOOST_CHECK(EquivalentResult(expected_result, *res));
1276 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1277 size_t expected_attempts = 38;
1278 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1279 }
1280
1281 {
1282 // #################################################################################################################
1283 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
1284 // #################################################################################################################
1285 CAmount target = 1.9L * COIN;
1286 int max_selection_weight = 40000; // WU
1287 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1288 CoinsResult available_coins;
1289 add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500);
1290 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1291 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1292 for (int j = 0; j < 100; ++j) {
1293 // make a 100 unique coins only differing by one sat
1294 add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110);
1295 }
1296 return available_coins;
1297 });
1298 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1299 add_coin(1 * COIN, 1, expected_result);
1300 add_coin(1 * COIN, 2, expected_result);
1301 BOOST_CHECK(EquivalentResult(expected_result, *res));
1302 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1303 size_t expected_attempts = 7;
1304 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1305 }
1306}
1307
1309 const CoinSelectionParams& cs_params,
1311 int max_selection_weight,
1312 std::function<CoinsResult(CWallet&)> coin_setup)
1313{
1314 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1315 CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors
1316 Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups;
1317 return SelectCoinsSRD(group.positive_group, target, cs_params.m_change_fee, cs_params.rng_fast, max_selection_weight);
1318}
1319
1321{
1322 // Test SRD:
1323 // 1) Insufficient funds, select all provided coins and fail.
1324 // 2) Exceeded max weight, coin selection always surpasses the max allowed weight.
1325 // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not)
1326
1327 FastRandomContext rand;
1328 CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag.
1329 rand,
1330 /*change_output_size=*/34,
1331 /*change_spend_size=*/68,
1332 /*min_change_target=*/CENT,
1333 /*effective_feerate=*/CFeeRate(0),
1334 /*long_term_feerate=*/CFeeRate(0),
1335 /*discard_feerate=*/CFeeRate(0),
1336 /*tx_noinputs_size=*/10 + 34, // static header size + output size
1337 /*avoid_partial=*/false,
1338 };
1339
1340 {
1341 // #########################################################
1342 // 1) Insufficient funds, select all provided coins and fail
1343 // #########################################################
1344 CAmount target = 49.5L * COIN;
1345 int max_selection_weight = 10000; // high enough to not fail for this reason.
1346 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1347 CoinsResult available_coins;
1348 for (int j = 0; j < 10; ++j) {
1349 add_coin(available_coins, wallet, CAmount(1 * COIN));
1350 add_coin(available_coins, wallet, CAmount(2 * COIN));
1351 }
1352 return available_coins;
1353 });
1354 BOOST_CHECK(!res);
1355 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds"
1356 }
1357
1358 {
1359 // ###########################
1360 // 2) Test max weight exceeded
1361 // ###########################
1362 CAmount target = 49.5L * COIN;
1363 int max_selection_weight = 3000;
1364 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1365 CoinsResult available_coins;
1366 for (int j = 0; j < 10; ++j) {
1367 /* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */
1368 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(0), 144, false, 0, true);
1369 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true);
1370 }
1371 return available_coins;
1372 });
1373 BOOST_CHECK(!res);
1374 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
1375 }
1376
1377 {
1378 // ################################################################################################################
1379 // 3) Test selection when some coins surpass the max allowed weight while others not. --> must find a good solution
1380 // ################################################################################################################
1381 CAmount target = 25.33L * COIN;
1382 int max_selection_weight = 10000; // WU
1383 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1384 CoinsResult available_coins;
1385 for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU
1386 add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(0), 144, false, 0, true);
1387 }
1388 for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU
1389 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true);
1390 }
1391 return available_coins;
1392 });
1393 BOOST_CHECK(res);
1394 }
1395}
1396
1397static util::Result<SelectionResult> select_coins(const CAmount& target, const CoinSelectionParams& cs_params, const CCoinControl& cc, std::function<CoinsResult(CWallet&)> coin_setup, const node::NodeContext& m_node)
1398{
1399 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1400 auto available_coins = coin_setup(*wallet);
1401
1402 LOCK(wallet->cs_wallet);
1403 auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/ {}, target, cc, cs_params);
1404 if (result) {
1405 const auto signedTxSize = 10 + 34 + 68 * result->GetInputSet().size(); // static header size + output size + inputs size (P2WPKH)
1406 BOOST_CHECK_LE(signedTxSize * WITNESS_SCALE_FACTOR, MAX_STANDARD_TX_WEIGHT);
1407
1408 BOOST_CHECK_GE(result->GetSelectedValue(), target);
1409 }
1410 return result;
1411}
1412
1413static bool has_coin(const CoinSet& set, CAmount amount)
1414{
1415 return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin->GetEffectiveValue() == amount; });
1416}
1417
1418BOOST_AUTO_TEST_CASE(check_max_selection_weight)
1419{
1420 const CAmount target = 49.5L * COIN;
1421 CCoinControl cc;
1422
1423 FastRandomContext rand;
1424 CoinSelectionParams cs_params{
1425 rand,
1426 /*change_output_size=*/34,
1427 /*change_spend_size=*/68,
1428 /*min_change_target=*/CENT,
1429 /*effective_feerate=*/CFeeRate(0),
1430 /*long_term_feerate=*/CFeeRate(0),
1431 /*discard_feerate=*/CFeeRate(0),
1432 /*tx_noinputs_size=*/10 + 34, // static header size + output size
1433 /*avoid_partial=*/false,
1434 };
1435
1436 int max_weight = MAX_STANDARD_TX_WEIGHT - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size);
1437 {
1438 // Scenario 1:
1439 // The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs
1440 // Then tries to spend 49.5 BTC
1441 // The 50.0 BTC output should be selected, because the transaction would otherwise be too large
1442
1443 // Perform selection
1444
1445 const auto result = select_coins(
1446 target, cs_params, cc, [&](CWallet& wallet) {
1447 CoinsResult available_coins;
1448 for (int j = 0; j < 1515; ++j) {
1449 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true);
1450 }
1451
1452 add_coin(available_coins, wallet, CAmount(50 * COIN), CFeeRate(0), 144, false, 0, true);
1453 return available_coins;
1454 },
1455 m_node);
1456
1457 BOOST_CHECK(result);
1458 // Verify that the 50 BTC UTXO was selected, and result is below max_weight
1459 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(50 * COIN)));
1460 BOOST_CHECK_LE(result->GetWeight(), max_weight);
1461 }
1462
1463 {
1464 // Scenario 2:
1465
1466 // The actor starts with 400x 0.0625 BTC and 2000x 0.025 BTC (75.0 BTC total) unspent outputs
1467 // Then tries to spend 49.5 BTC
1468 // A combination of coins should be selected, such that the created transaction is not too large
1469
1470 // Perform selection
1471 const auto result = select_coins(
1472 target, cs_params, cc, [&](CWallet& wallet) {
1473 CoinsResult available_coins;
1474 for (int j = 0; j < 400; ++j) {
1475 add_coin(available_coins, wallet, CAmount(0.0625 * COIN), CFeeRate(0), 144, false, 0, true);
1476 }
1477 for (int j = 0; j < 2000; ++j) {
1478 add_coin(available_coins, wallet, CAmount(0.025 * COIN), CFeeRate(0), 144, false, 0, true);
1479 }
1480 return available_coins;
1481 },
1482 m_node);
1483
1484 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.0625 * COIN)));
1485 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.025 * COIN)));
1486 BOOST_CHECK_LE(result->GetWeight(), max_weight);
1487 }
1488
1489 {
1490 // Scenario 3:
1491
1492 // The actor starts with 1515x 0.033 BTC (49.995 BTC total) unspent outputs
1493 // No results should be returned, because the transaction would be too large
1494
1495 // Perform selection
1496 const auto result = select_coins(
1497 target, cs_params, cc, [&](CWallet& wallet) {
1498 CoinsResult available_coins;
1499 for (int j = 0; j < 1515; ++j) {
1500 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true);
1501 }
1502 return available_coins;
1503 },
1504 m_node);
1505
1506 // No results
1507 // 1515 inputs * 68 bytes = 103,020 bytes
1508 // 103,020 bytes * 4 = 412,080 weight, which is above the MAX_STANDARD_TX_WEIGHT of 400,000
1509 BOOST_CHECK(!result);
1510 }
1511}
1512
1513BOOST_AUTO_TEST_CASE(SelectCoins_effective_value_test)
1514{
1515 // Test that the effective value is used to check whether preset inputs provide sufficient funds when subtract_fee_outputs is not used.
1516 // This test creates a coin whose value is higher than the target but whose effective value is lower than the target.
1517 // The coin is selected using coin control, with m_allow_other_inputs = false. SelectCoins should fail due to insufficient funds.
1518
1519 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1520
1521 CoinsResult available_coins;
1522 {
1523 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy");
1524 add_coin(available_coins, *dummyWallet, 100000); // 0.001 BTC
1525 }
1526
1527 CAmount target{99900}; // 0.000999 BTC
1528
1529 FastRandomContext rand;
1530 CoinSelectionParams cs_params{
1531 rand,
1532 /*change_output_size=*/34,
1533 /*change_spend_size=*/148,
1534 /*min_change_target=*/1000,
1535 /*effective_feerate=*/CFeeRate(3000),
1536 /*long_term_feerate=*/CFeeRate(1000),
1537 /*discard_feerate=*/CFeeRate(1000),
1538 /*tx_noinputs_size=*/0,
1539 /*avoid_partial=*/false,
1540 };
1541 CCoinControl cc;
1542 cc.m_allow_other_inputs = false;
1543 COutput output = available_coins.All().at(0);
1544 cc.SetInputWeight(output.outpoint, 148);
1545 cc.Select(output.outpoint).SetTxOut(output.txout);
1546
1547 LOCK(wallet->cs_wallet);
1548 const auto preset_inputs = *Assert(FetchSelectedInputs(*wallet, cc, cs_params));
1549 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
1550
1551 const auto result = SelectCoins(*wallet, available_coins, preset_inputs, target, cc, cs_params);
1552 BOOST_CHECK(!result);
1553}
1554
1556{
1557 // Test case to verify CoinsResult object sanity.
1558 CoinsResult available_coins;
1559 {
1560 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy");
1561
1562 // Add some coins to 'available_coins'
1563 for (int i=0; i<10; i++) {
1564 add_coin(available_coins, *dummyWallet, 1 * COIN);
1565 }
1566 }
1567
1568 {
1569 // First test case, check that 'CoinsResult::Erase' function works as expected.
1570 // By trying to erase two elements from the 'available_coins' object.
1571 std::unordered_set<COutPoint, SaltedOutpointHasher> outs_to_remove;
1572 const auto& coins = available_coins.All();
1573 for (int i = 0; i < 2; i++) {
1574 outs_to_remove.emplace(coins[i].outpoint);
1575 }
1576 available_coins.Erase(outs_to_remove);
1577
1578 // Check that the elements were actually removed.
1579 const auto& updated_coins = available_coins.All();
1580 for (const auto& out: outs_to_remove) {
1581 auto it = std::find_if(updated_coins.begin(), updated_coins.end(), [&out](const COutput &coin) {
1582 return coin.outpoint == out;
1583 });
1584 BOOST_CHECK(it == updated_coins.end());
1585 }
1586 // And verify that no extra element were removed
1587 BOOST_CHECK_EQUAL(available_coins.Size(), 8);
1588 }
1589}
1590
1592} // namespace wallet
CScript GetScriptForDestination(const CTxDestination &dest)
Generate a Bitcoin scriptPubKey for the given CTxDestination.
int64_t CAmount
Amount in satoshis (Can be negative)
Definition amount.h:12
static constexpr CAmount COIN
The amount of satoshis in one BTC.
Definition amount.h:15
BOOST_CHECK_LT(ZeroL, OneL)
int ret
node::NodeContext m_node
#define Assert(val)
Identity function.
Definition check.h:77
Fee rate in satoshis per kilovirtualbyte: CAmount / kvB.
Definition feerate.h:33
An outpoint - a combination of a transaction hash and an index n into its vout.
Definition transaction.h:29
Fast randomness source.
Definition random.h:377
I randrange(I range) noexcept
Generate a random integer in the range [0..range), with range > 0.
Definition random.h:254
256-bit opaque blob.
Definition uint256.h:178
Coin Control Features.
Definition coincontrol.h:81
PreselectedInput & Select(const COutPoint &outpoint)
Lock-in the given output for spending.
bool m_allow_other_inputs
If true, the selection process can add extra unselected inputs from the wallet while requires all sel...
Definition coincontrol.h:91
void SetInputWeight(const COutPoint &outpoint, int64_t weight)
Set an input's weight.
A CWallet maintains a set of transactions and balances, and provides the ability to create new transa...
Definition wallet.h:300
A transaction with a bunch of additional info that only the owner cares about.
const Txid & GetHash() const LIFETIMEBOUND
CTransactionRef tx
int64_t GetTxTime() const
void SetTxOut(const CTxOut &txout)
Set the previous output for this input.
#define RANDOM_REPEATS
#define RUN_TESTS
static const int WITNESS_SCALE_FACTOR
Definition consensus.h:21
BOOST_AUTO_TEST_SUITE_END()
bilingual_str ErrorString(const Result< T > &result)
Definition result.h:93
static CAmount make_hard_case(int utxos, std::vector< COutput > &utxo_pool)
static const CoinEligibilityFilter filter_standard(1, 6, 0)
FilteredOutputGroups GroupOutputs(const CWallet &wallet, const CoinsResult &coins, const CoinSelectionParams &coin_sel_params, const std::vector< SelectionFilter > &filters, std::vector< OutputGroup > &ret_discarded_groups)
Definition spend.cpp:528
BOOST_FIXTURE_TEST_CASE(wallet_coinsresult_test, BasicTestingSetup)
util::Result< SelectionResult > SelectCoinsBnB(std::vector< OutputGroup > &utxo_pool, const CAmount &selection_target, const CAmount &cost_of_change, int max_selection_weight)
static void add_coin(const CAmount &nValue, int nInput, std::vector< COutput > &set)
static std::unique_ptr< CWallet > NewWallet(const node::NodeContext &m_node, const std::string &wallet_name="")
util::Result< SelectionResult > CoinGrinder(std::vector< OutputGroup > &utxo_pool, const CAmount &selection_target, CAmount change_target, int max_selection_weight)
util::Result< PreSelectedInputs > FetchSelectedInputs(const CWallet &wallet, const CCoinControl &coin_control, const CoinSelectionParams &coin_selection_params)
Fetch and validate coin control selected inputs.
Definition spend.cpp:262
std::vector< OutputGroup > & GroupCoins(const std::vector< COutput > &available_coins, bool subtract_fee_outputs=false)
std::vector< OutputGroup > & KnapsackGroupOutputs(const CoinsResult &available_coins, CWallet &wallet, const CoinEligibilityFilter &filter)
util::Result< SelectionResult > KnapsackSolver(std::vector< OutputGroup > &groups, const CAmount &nTargetValue, CAmount change_target, FastRandomContext &rng, int max_selection_weight)
util::Result< SelectionResult > SelectCoins(const CWallet &wallet, CoinsResult &available_coins, const PreSelectedInputs &pre_set_inputs, const CAmount &nTargetValue, const CCoinControl &coin_control, const CoinSelectionParams &coin_selection_params)
Select all coins from coin_control, and if coin_control 'm_allow_other_inputs=true',...
Definition spend.cpp:770
static const CoinEligibilityFilter filter_standard_extra(6, 6, 0)
static bool has_coin(const CoinSet &set, CAmount amount)
std::set< std::shared_ptr< COutput > > CoinSet
static bool EqualResult(const SelectionResult &a, const SelectionResult &b)
Check if this selection is equal to another one.
BOOST_AUTO_TEST_CASE(bnb_search_test)
static int nextLockTime
static const CoinEligibilityFilter filter_confirmed(1, 1, 0)
int CalculateMaximumSignedInputSize(const CTxOut &txout, const COutPoint outpoint, const SigningProvider *provider, bool can_grind_r, const CCoinControl *coin_control)
Definition spend.cpp:85
static bool EquivalentResult(const SelectionResult &a, const SelectionResult &b)
Check if SelectionResult a is equivalent to SelectionResult b.
static void ApproximateBestSubset(FastRandomContext &insecure_rand, const std::vector< OutputGroup > &groups, const CAmount &nTotalLower, const CAmount &nTargetValue, std::vector< char > &vfBest, CAmount &nBest, int max_selection_weight, int iterations=1000)
Find a subset of the OutputGroups that is at least as large as, but as close as possible to,...
util::Result< SelectionResult > SelectCoinsSRD(const std::vector< OutputGroup > &utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext &rng, int max_selection_weight)
Select coins by Single Random Draw.
static util::Result< SelectionResult > select_coins(const CAmount &target, const CoinSelectionParams &cs_params, const CCoinControl &cc, std::function< CoinsResult(CWallet &)> coin_setup, const node::NodeContext &m_node)
#define BOOST_CHECK_EQUAL(v1, v2)
Definition object.cpp:18
#define BOOST_CHECK(expr)
Definition object.cpp:17
static constexpr int32_t MAX_STANDARD_TX_WEIGHT
The maximum weight for transactions we're willing to relay/mine.
Definition policy.h:27
static CTransactionRef MakeTransactionRef(Tx &&txIn)
static constexpr CAmount CENT
Basic testing setup.
A mutable version of CTransaction.
std::vector< CTxOut > vout
Txid GetHash() const
Compute the hash of this CMutableTransaction.
NodeContext struct containing references to chain state and connection state.
Definition context.h:55
std::unique_ptr< interfaces::Chain > chain
Definition context.h:73
A UTXO under consideration for use in funding a new transaction.
COutPoint outpoint
The outpoint identifying this UTXO.
CTxOut txout
The output itself.
CAmount GetEffectiveValue() const
Parameters for filtering which OutputGroups we may use in coin selection.
Parameters for one iteration of Coin Selection.
FastRandomContext & rng_fast
Randomness to use in the context of coin selection.
CAmount m_min_change_target
Mininmum change to target in Knapsack solver and CoinGrinder: select coins to cover the payment and a...
bool m_subtract_fee_outputs
Indicate that we are subtracting the fee from outputs.
CAmount m_cost_of_change
Cost of creating the change output + cost of spending the change output in the future.
CAmount m_change_fee
Cost of creating the change output.
int change_output_size
Size of a change output in bytes, determined by the output type.
int tx_noinputs_size
Size of the transaction before coin selection, consisting of the header and recipient output(s),...
COutputs available for spending, stored by OutputType.
Definition spend.h:40
void Add(OutputType type, const COutput &out)
Definition spend.cpp:233
std::vector< COutput > All() const
Concatenate and return all COutputs as one vector.
Definition spend.cpp:196
size_t Size() const
The following methods are provided so that CoinsResult can mimic a vector, i.e., methods can work wit...
Definition spend.cpp:187
std::map< OutputType, std::vector< COutput > > coins
Definition spend.h:41
void Erase(const std::unordered_set< COutPoint, SaltedOutpointHasher > &coins_to_remove)
Definition spend.cpp:210
std::vector< OutputGroup > mixed_group
A group of UTXOs paid to the same output script.
Stores several 'Groups' whose were mapped by output type.
void Insert(const COutput &output, bool subtract_fee_outputs)
Definition spend.h:164
const std::set< std::shared_ptr< COutput > > & GetInputSet() const
Get m_selected_inputs.
void AddInput(const OutputGroup &group)
State of transaction not confirmed or conflicting with a known block and not in the mempool.
Definition transaction.h:58
#define LOCK(cs)
Definition sync.h:257
#define WITH_LOCK(cs, code)
Run code while locking a mutex.
Definition sync.h:301
#define strprintf
Format arguments and return the string or write to given std::ostream (see tinyformat::format doc for...
BOOST_CHECK_NE(OneL.ToString(), ArrayToString(ZeroArray, 32))
assert(!tx.IsCoinBase())