Bitcoin Core 32.99.0
P2P Digital Currency
Loading...
Searching...
No Matches
coinselector_tests.cpp
Go to the documentation of this file.
1// Copyright (c) 2017-present 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>
10#include <test/util/common.h>
12#include <util/translation.h>
13#include <wallet/coincontrol.h>
15#include <wallet/spend.h>
16#include <wallet/test/util.h>
18#include <wallet/wallet.h>
19
20#include <algorithm>
21#include <boost/test/unit_test.hpp>
22#include <random>
23
24namespace wallet {
26
27// how many times to run all the tests to have a chance to catch errors that only show up with particular random shuffles
28#define RUN_TESTS 100
29
30// some tests fail 1% of the time due to bad luck.
31// we repeat those tests this many times and only complain if all iterations of the test fail
32#define RANDOM_REPEATS 5
33
37static int nextLockTime = 0;
38
39static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
40{
42 tx.vout.resize(nInput + 1);
43 tx.vout[nInput].nValue = nValue;
44 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
45 COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/ 0);
46 OutputGroup group;
47 group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*cluster_count=*/ 0);
48 result.AddInput(group);
49}
50
51static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result, CAmount fee, CAmount long_term_fee)
52{
54 tx.vout.resize(nInput + 1);
55 tx.vout[nInput].nValue = nValue;
56 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
57 std::shared_ptr<COutput> coin = std::make_shared<COutput>(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/148, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fee);
58 OutputGroup group;
59 group.Insert(coin, /*ancestors=*/ 0, /*cluster_count=*/ 0);
60 coin->long_term_fee = long_term_fee; // group.Insert() will modify long_term_fee, so we need to set it afterwards
61 result.AddInput(group);
62}
63
64static 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)
65{
67 tx.nLockTime = nextLockTime++; // so all transactions get different hashes
68 tx.vout.resize(nInput + 1);
69 tx.vout[nInput].nValue = nValue;
70 if (spendable) {
71 tx.vout[nInput].scriptPubKey = GetScriptForDestination(*Assert(wallet.GetNewDestination(OutputType::BECH32, "")));
72 }
73 Txid txid = tx.GetHash();
74
75 LOCK(wallet.cs_wallet);
76 auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
77 assert(ret.second);
78 CWalletTx& wtx = (*ret.first).second;
79 const auto& txout = wtx.GetTx()->vout.at(nInput);
80 available_coins.Add(OutputType::BECH32, {COutPoint(wtx.GetHash(), nInput), txout, nAge, custom_size == 0 ? CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr) : custom_size, /*solvable=*/true, /*safe=*/true, wtx.GetTxTime(), fIsFromMe, feerate});
81}
82
83// Helpers
84std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
86{
88 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
89}
90
91std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)
92{
94 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
95}
96
100{
101 std::vector<CAmount> a_amts;
102 std::vector<CAmount> b_amts;
103 for (const auto& coin : a.GetInputSet()) {
104 a_amts.push_back(coin->txout.nValue);
105 }
106 for (const auto& coin : b.GetInputSet()) {
107 b_amts.push_back(coin->txout.nValue);
108 }
109 std::sort(a_amts.begin(), a_amts.end());
110 std::sort(b_amts.begin(), b_amts.end());
111
112 std::pair<std::vector<CAmount>::iterator, std::vector<CAmount>::iterator> ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin());
113 return ret.first == a_amts.end() && ret.second == b_amts.end();
114}
115
117static bool EqualResult(const SelectionResult& a, const SelectionResult& b)
118{
119 std::pair<OutputSet::iterator, OutputSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(),
120 [](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) {
121 return a->outpoint == b->outpoint;
122 });
123 return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end();
124}
125
126inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& available_coins, bool subtract_fee_outputs = false)
127{
128 static std::vector<OutputGroup> static_groups;
129 static_groups.clear();
130 for (auto& coin : available_coins) {
131 static_groups.emplace_back();
132 OutputGroup& group = static_groups.back();
133 group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*cluster_count=*/ 0);
134 group.m_subtract_fee_outputs = subtract_fee_outputs;
135 }
136 return static_groups;
137}
138
139inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& available_coins, CWallet& wallet, const CoinEligibilityFilter& filter)
140{
141 FastRandomContext rand{};
143 rand,
144 /*change_output_size=*/ 0,
145 /*change_spend_size=*/ 0,
146 /*min_change_target=*/ CENT,
147 /*effective_feerate=*/ CFeeRate(0),
148 /*long_term_feerate=*/ CFeeRate(0),
149 /*discard_feerate=*/ CFeeRate(0),
150 /*tx_noinputs_size=*/ 0,
151 /*avoid_partial=*/ false,
152 };
156}
157
158static std::unique_ptr<CWallet> NewWallet(const node::NodeContext& m_node, const std::string& wallet_name = "")
159{
160 std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), wallet_name, CreateMockableWalletDatabase());
161 LOCK(wallet->cs_wallet);
162 wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
163 wallet->SetupDescriptorScriptPubKeyMans();
164 return wallet;
165}
166
167// Branch and bound coin selection tests
169{
170 FastRandomContext rand{};
171 // Setup
173 size_t expected_attempts;
174
176 // Behavior tests //
178
179 // Make sure that effective value is working in AttemptSelection when BnB is used
181 rand,
182 /*change_output_size=*/ 31,
183 /*change_spend_size=*/ 68,
184 /*min_change_target=*/ 0,
185 /*effective_feerate=*/ CFeeRate(3000),
186 /*long_term_feerate=*/ CFeeRate(1000),
187 /*discard_feerate=*/ CFeeRate(1000),
188 /*tx_noinputs_size=*/ 0,
189 /*avoid_partial=*/ false,
190 };
191 coin_selection_params_bnb.m_change_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_output_size);
192 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;
193 coin_selection_params_bnb.min_viable_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size);
194
195 {
196 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
197
199
200 add_coin(available_coins, *wallet, 1, coin_selection_params_bnb.m_effective_feerate);
201 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
203
204 // Test fees subtracted from output:
205 available_coins = {};
206 add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate);
207 available_coins.All().at(0).input_bytes = 40;
208 const auto result9 = SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change);
210 BOOST_CHECK_EQUAL(result9->GetSelectedValue(), 1 * CENT);
212 BOOST_CHECK_MESSAGE(result9->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result9->GetSelectionsEvaluated()));
213 }
214
215 {
216 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
217
219
220 coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
221 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
222 add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
223 add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
226 COutput select_coin = available_coins.All().at(0);
227 coin_control.Select(select_coin.outpoint);
230 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
231
232 LOCK(wallet->cs_wallet);
236 BOOST_CHECK_MESSAGE(result10->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result10->GetSelectionsEvaluated()));
237 }
238 {
239 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
240 LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it
241
243
244 // pre selected coin should be selected even if disadvantageous
245 coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);
246 coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);
247
248 // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount
249 CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*virtual_bytes=*/68); // bech32 input size (default test output type)
250 add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
251 add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
252 add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
253
254 expected_result.Clear();
255 add_coin(9 * CENT + input_fee, 2, expected_result);
256 add_coin(1 * CENT + input_fee, 2, expected_result);
259 COutput select_coin = available_coins.All().at(1); // pre select 9 coin
260 coin_control.Select(select_coin.outpoint);
263 available_coins.Erase({(++available_coins.coins[OutputType::BECH32].begin())->outpoint});
265 BOOST_CHECK(EquivalentResult(expected_result, *result13));
267 BOOST_CHECK_MESSAGE(result13->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result13->GetSelectionsEvaluated()));
268 }
269
270 {
271 // Test bnb max weight exceeded
272 // Inputs set [10, 9, 8, 5, 3, 1], Selection Target = 16 and coin 5 exceeding the max weight.
273
274 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
275
277 add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
278 add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
279 add_coin(available_coins, *wallet, 8 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
280 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);
281 add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
282 add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
283
285 const auto& no_res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs=*/true),
286 selection_target, /*cost_of_change=*/0, MAX_STANDARD_TX_WEIGHT);
288 BOOST_CHECK(util::ErrorString(no_res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
289
290 // Now add same coin value with a good size and check that it gets selected
291 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
292 const auto& res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs=*/true), selection_target, /*cost_of_change=*/0);
293
294 expected_result.Clear();
295 add_coin(8 * CENT, 2, expected_result);
296 add_coin(5 * CENT, 2, expected_result);
297 add_coin(3 * CENT, 2, expected_result);
298 BOOST_CHECK(EquivalentResult(expected_result, *res));
300 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
301 }
302}
303
305{
306 // Verify the coin selection process does not produce a BnB solution when SFFO is enabled.
307 // This is currently problematic because it could require a change output. And BnB is specialized on changeless solutions.
308 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
309 WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(300, uint256{})); // set a high block so internal UTXOs are selectable
310
311 FastRandomContext rand{};
312 CoinSelectionParams params{
313 rand,
314 /*change_output_size=*/ 31, // unused value, p2wpkh output size (wallet default change type)
315 /*change_spend_size=*/ 68, // unused value, p2wpkh input size (high-r signature)
316 /*min_change_target=*/ 0, // dummy, set later
317 /*effective_feerate=*/ CFeeRate(3000),
318 /*long_term_feerate=*/ CFeeRate(1000),
319 /*discard_feerate=*/ CFeeRate(1000),
320 /*tx_noinputs_size=*/ 0,
321 /*avoid_partial=*/ false,
322 };
323 params.m_subtract_fee_outputs = true;
324 params.m_change_fee = params.m_effective_feerate.GetFee(params.change_output_size);
325 params.m_cost_of_change = params.m_discard_feerate.GetFee(params.change_spend_size) + params.m_change_fee;
326 params.m_min_change_target = params.m_cost_of_change + 1;
327 // Add spendable coin at the BnB selection upper bound
329 add_coin(available_coins, *wallet, COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
330 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);
331 add_coin(available_coins, *wallet, 0.5 * COIN, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true);
332 // Knapsack will only find a changeless solution on an exact match to the satoshi, SRD doesn’t look for changeless
333 // If BnB were run, it would produce a single input solution with the best waste score
334 auto result = WITH_LOCK(wallet->cs_wallet, return SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, COIN, /*coin_control=*/{}, params));
335 BOOST_CHECK(result.has_value());
336 BOOST_CHECK_NE(result->GetAlgo(), SelectionAlgorithm::BNB);
337 BOOST_CHECK(result->GetInputSet().size() == 2);
338 // We have only considered BnB, SRD, and Knapsack. Test needs to be reevaluated if new algo is added
339 BOOST_CHECK(result->GetAlgo() == SelectionAlgorithm::SRD || result->GetAlgo() == SelectionAlgorithm::KNAPSACK);
340}
341
343{
344 FastRandomContext rand{};
345 const auto temp1{[&rand](std::vector<OutputGroup>& g, const CAmount& v, CAmount c) { return KnapsackSolver(g, v, c, rand); }};
346 const auto KnapsackSolver{temp1};
347 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
348
350
351 // test multiple times to allow for differences in the shuffle order
352 for (int i = 0; i < RUN_TESTS; i++)
353 {
354 available_coins = {};
355
356 // with an empty wallet we can't even pay one cent
358
359 add_coin(available_coins, *wallet, 1*CENT, CFeeRate(0), 4); // add a new 1 cent coin
360
361 // with a new 1 cent coin, we still can't find a mature 1 cent
363
364 // but we can find a new 1 cent
367 BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT);
368
369 add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin
370
371 // we can't make 3 cents of mature coins
373
374 // we can make 3 cents of new coins
377 BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 3 * CENT);
378
379 add_coin(available_coins, *wallet, 5*CENT); // add a mature 5 cent coin,
380 add_coin(available_coins, *wallet, 10*CENT, CFeeRate(0), 3, true); // a new 10 cent coin sent from one of our own addresses
381 add_coin(available_coins, *wallet, 20*CENT); // and a mature 20 cent coin
382
383 // now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27. total = 38
384
385 // we can't make 38 cents only if we disallow new coins:
387 // we can't even make 37 cents if we don't allow new coins even if they're from us
389 // but we can make 37 cents if we accept new coins from ourself
392 BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT);
393 // and we can make 38 cents if we accept all new coins
396 BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 38 * CENT);
397
398 // try making 34 cents from 1,2,5,10,20 - we can't do it exactly
401 BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 35 * CENT); // but 35 cents is closest
402 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)
403
404 // when we try making 7 cents, the smaller coins (1,2,5) are enough. We should see just 2+5
407 BOOST_CHECK_EQUAL(result6->GetSelectedValue(), 7 * CENT);
408 BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U);
409
410 // when we try making 8 cents, the smaller coins (1,2,5) are exactly enough.
413 BOOST_CHECK(result7->GetSelectedValue() == 8 * CENT);
414 BOOST_CHECK_EQUAL(result7->GetInputSet().size(), 3U);
415
416 // when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10)
419 BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 10 * CENT);
420 BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U);
421
422 // now clear out the wallet and start again to test choosing between subsets of smaller coins and the next biggest coin
423 available_coins = {};
424
429 add_coin(available_coins, *wallet, 30*CENT); // now we have 6+7+8+20+30 = 71 cents total
430
431 // check that we have 71 and not 72
435
436 // 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
439 BOOST_CHECK_EQUAL(result10->GetSelectedValue(), 20 * CENT); // we should get 20 in one coin
440 BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U);
441
442 add_coin(available_coins, *wallet, 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total
443
444 // 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
447 BOOST_CHECK_EQUAL(result11->GetSelectedValue(), 18 * CENT); // we should get 18 in 3 coins
448 BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U);
449
450 add_coin(available_coins, *wallet, 18*CENT); // now we have 5+6+7+8+18+20+30
451
452 // 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
455 BOOST_CHECK_EQUAL(result12->GetSelectedValue(), 18 * CENT); // we should get 18 in 1 coin
456 BOOST_CHECK_EQUAL(result12->GetInputSet().size(), 1U); // because in the event of a tie, the biggest coin wins
457
458 // now try making 11 cents. we should get 5+6
461 BOOST_CHECK_EQUAL(result13->GetSelectedValue(), 11 * CENT);
462 BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U);
463
464 // check that the smallest bigger coin is used
468 add_coin(available_coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents
471 BOOST_CHECK_EQUAL(result14->GetSelectedValue(), 1 * COIN); // we should get 1 BTC in 1 coin
472 BOOST_CHECK_EQUAL(result14->GetInputSet().size(), 1U);
473
476 BOOST_CHECK_EQUAL(result15->GetSelectedValue(), 2 * COIN); // we should get 2 BTC in 1 coin
477 BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U);
478
479 // empty the wallet and start again, now with fractions of a cent, to test small change avoidance
480
481 available_coins = {};
482 add_coin(available_coins, *wallet, CENT * 1 / 10);
483 add_coin(available_coins, *wallet, CENT * 2 / 10);
484 add_coin(available_coins, *wallet, CENT * 3 / 10);
485 add_coin(available_coins, *wallet, CENT * 4 / 10);
486 add_coin(available_coins, *wallet, CENT * 5 / 10);
487
488 // try making 1 * CENT from the 1.5 * CENT
489 // we'll get change smaller than CENT whatever happens, so can expect CENT exactly
492 BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT);
493
494 // but if we add a bigger coin, small change is avoided
496
497 // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5
500 BOOST_CHECK_EQUAL(result17->GetSelectedValue(), 1 * CENT); // we should get the exact amount
501
502 // if we add more small coins:
503 add_coin(available_coins, *wallet, CENT * 6 / 10);
504 add_coin(available_coins, *wallet, CENT * 7 / 10);
505
506 // and try again to make 1.0 * CENT
509 BOOST_CHECK_EQUAL(result18->GetSelectedValue(), 1 * CENT); // we should get the exact amount
510
511 // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf)
512 // they tried to consolidate 10 50k coins into one 500k coin, and ended up with 50k in change
513 available_coins = {};
514 for (int j = 0; j < 20; j++)
516
519 BOOST_CHECK_EQUAL(result19->GetSelectedValue(), 500000 * COIN); // we should get the exact amount
520 BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins
521
522 // 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),
523 // we need to try finding an exact subset anyway
524
525 // sometimes it will fail, and so we use the next biggest coin:
526 available_coins = {};
527 add_coin(available_coins, *wallet, CENT * 5 / 10);
528 add_coin(available_coins, *wallet, CENT * 6 / 10);
529 add_coin(available_coins, *wallet, CENT * 7 / 10);
533 BOOST_CHECK_EQUAL(result20->GetSelectedValue(), 1111 * CENT); // we get the bigger coin
534 BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U);
535
536 // but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = 1.0)
537 available_coins = {};
538 add_coin(available_coins, *wallet, CENT * 4 / 10);
539 add_coin(available_coins, *wallet, CENT * 6 / 10);
540 add_coin(available_coins, *wallet, CENT * 8 / 10);
544 BOOST_CHECK_EQUAL(result21->GetSelectedValue(), CENT); // we should get the exact amount
545 BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6
546
547 // test avoiding small change
548 available_coins = {};
549 add_coin(available_coins, *wallet, CENT * 5 / 100);
552
553 // trying to make 100.01 from these three coins
556 BOOST_CHECK_EQUAL(result22->GetSelectedValue(), CENT * 10105 / 100); // we should get all coins
557 BOOST_CHECK_EQUAL(result22->GetInputSet().size(), 3U);
558
559 // but if we try to make 99.9, we should take the bigger of the two small coins to avoid small change
562 BOOST_CHECK_EQUAL(result23->GetSelectedValue(), 101 * CENT);
563 BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U);
564 }
565
566 // test with many inputs
567 for (CAmount amt=1500; amt < COIN; amt*=10) {
568 available_coins = {};
569 // Create 676 inputs (= (old MAX_STANDARD_TX_SIZE == 100000) / 148 bytes per input)
570 for (uint16_t j = 0; j < 676; j++)
572
573 // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
574 for (int i = 0; i < RUN_TESTS; i++) {
577
578 if (amt - 2000 < CENT) {
579 // needs more than one input:
580 uint16_t returnSize = std::ceil((2000.0 + CENT)/amt);
581 CAmount returnValue = amt * returnSize;
582 BOOST_CHECK_EQUAL(result24->GetSelectedValue(), returnValue);
583 BOOST_CHECK_EQUAL(result24->GetInputSet().size(), returnSize);
584 } else {
585 // one input is sufficient:
586 BOOST_CHECK_EQUAL(result24->GetSelectedValue(), amt);
587 BOOST_CHECK_EQUAL(result24->GetInputSet().size(), 1U);
588 }
589 }
590 }
591
592 // test randomness
593 {
594 available_coins = {};
595 for (int i2 = 0; i2 < 100; i2++)
597
598 // Again, we only create the wallet once to save time, but we still run the coin selection RUN_TESTS times.
599 for (int i = 0; i < RUN_TESTS; i++) {
600 // picking 50 from 100 coins doesn't depend on the shuffle,
601 // but does depend on randomness in the stochastic approximation code
602 const auto result25 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT);
604 const auto result26 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT);
607
608 int fails = 0;
609 for (int j = 0; j < RANDOM_REPEATS; j++)
610 {
611 // Test that the KnapsackSolver selects randomly from equivalent coins (same value and same input size).
612 // When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice
613 // which will cause it to fail.
614 // To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail
620 fails++;
621 }
623 }
624
625 // add 75 cents in small change. not enough to make 90 cents,
626 // then try making 90 cents. there are multiple competing "smallest bigger" coins,
627 // one of which should be picked at random
633
634 for (int i = 0; i < RUN_TESTS; i++) {
635 int fails = 0;
636 for (int j = 0; j < RANDOM_REPEATS; j++)
637 {
638 const auto result29 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT);
640 const auto result30 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT);
643 fails++;
644 }
646 }
647 }
648}
649
651{
652 FastRandomContext rand{};
653 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
654
656
657 // Test vValue sort order
658 for (int i = 0; i < 1000; i++)
661
662 const auto result = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1003 * COIN, CENT, rand);
663 BOOST_CHECK(result);
664 BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN);
665 BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U);
666}
667
668// Tests that with the ideal conditions, the coin selector will always be able to find a solution that can pay the target value
670{
671 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
672 LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it
673
674 // Random generator stuff
675 std::default_random_engine generator;
676 std::exponential_distribution<double> distribution (100);
678
679 // Run this test 100 times
680 for (int i = 0; i < 100; ++i)
681 {
683 CAmount balance{0};
684
685 // Make a wallet with 1000 exponentially distributed random inputs
686 for (int j = 0; j < 1000; ++j)
687 {
688 CAmount val = distribution(generator)*10000000;
690 balance += val;
691 }
692
693 // Generate a random fee rate in the range of 100 - 400
694 CFeeRate rate(rand.randrange(300) + 100);
695
696 // Generate a random target value between 1000 and wallet balance
697 CAmount target = rand.randrange(balance - 1000) + 1000;
698
699 // Perform selection
701 rand,
702 /*change_output_size=*/ 34,
703 /*change_spend_size=*/ 148,
704 /*min_change_target=*/ CENT,
705 /*effective_feerate=*/ CFeeRate(0),
706 /*long_term_feerate=*/ CFeeRate(0),
707 /*discard_feerate=*/ CFeeRate(0),
708 /*tx_noinputs_size=*/ 0,
709 /*avoid_partial=*/ false,
710 };
711 cs_params.m_cost_of_change = 1;
712 cs_params.min_viable_change = 1;
714 const auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, target, cc, cs_params);
715 BOOST_CHECK(result);
716 BOOST_CHECK_GE(result->GetSelectedValue(), target);
717 }
718}
719
721{
722 const CAmount fee{100};
723 const CAmount min_viable_change{300};
724 const CAmount change_cost{125};
725 const CAmount change_fee{30};
726 const CAmount fee_diff{40};
727 const CAmount in_amt{3 * COIN};
728 const CAmount target{2 * COIN};
729 const CAmount excess{80};
730 const CAmount exact_target{in_amt - fee * 2}; // Maximum spendable amount after fees: no change, no excess
731
732 // In the following, we test that the waste is calculated correctly in various scenarios.
733 // Usually, RecalculateWaste would compute change_fee and change_cost on basis of the
734 // change output type, current feerate, and discard_feerate, but we use fixed values
735 // across this test to make the test easier to understand.
736 {
737 // Waste with change is the change cost and difference between fee and long term fee
739 add_coin(1 * COIN, 1, selection1, /*fee=*/fee, /*long_term_fee=*/fee - fee_diff);
740 add_coin(2 * COIN, 2, selection1, fee, fee - fee_diff);
741 selection1.RecalculateWaste(min_viable_change, change_cost, change_fee);
743
744 // Waste will be greater when fee is greater, but long term fee is the same
746 add_coin(1 * COIN, 1, selection2, fee * 2, fee - fee_diff);
747 add_coin(2 * COIN, 2, selection2, fee * 2, fee - fee_diff);
748 selection2.RecalculateWaste(min_viable_change, change_cost, change_fee);
749 BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste());
750
751 // Waste with change is the change cost and difference between fee and long term fee
752 // With long term fee greater than fee, waste should be less than when long term fee is less than fee
754 add_coin(1 * COIN, 1, selection3, fee, fee + fee_diff);
755 add_coin(2 * COIN, 2, selection3, fee, fee + fee_diff);
756 selection3.RecalculateWaste(min_viable_change, change_cost, change_fee);
758 BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste());
759 }
760
761 {
762 // Waste without change is the excess and difference between fee and long term fee
766 selection_nochange1.RecalculateWaste(min_viable_change, change_cost, change_fee);
768
769 // Waste without change is the excess and difference between fee and long term fee
770 // With long term fee greater than fee, waste should be less than when long term fee is less than fee
774 selection_nochange2.RecalculateWaste(min_viable_change, change_cost, change_fee);
777 }
778
779 {
780 // Waste with change and fee == long term fee is just cost of change
782 add_coin(1 * COIN, 1, selection, fee, fee);
783 add_coin(2 * COIN, 2, selection, fee, fee);
784 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
786 }
787
788 {
789 // Waste without change and fee == long term fee is just the excess
791 add_coin(1 * COIN, 1, selection, fee, fee);
792 add_coin(2 * COIN, 2, selection, fee, fee);
793 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
795 }
796
797 {
798 // Waste is 0 when fee == long_term_fee, no change, and no excess
800 add_coin(1 * COIN, 1, selection, fee, fee);
801 add_coin(2 * COIN, 2, selection, fee, fee);
802 selection.RecalculateWaste(min_viable_change, change_cost , change_fee);
803 BOOST_CHECK_EQUAL(0, selection.GetWaste());
804 }
805
806 {
807 // Waste is 0 when (fee - long_term_fee) == (-cost_of_change), and no excess
809 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
810 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
811 selection.RecalculateWaste(min_viable_change, /*change_cost=*/fee_diff * 2, change_fee);
812 BOOST_CHECK_EQUAL(0, selection.GetWaste());
813 }
814
815 {
816 // Waste is 0 when (fee - long_term_fee) == (-excess), no change cost
817 const CAmount new_target{exact_target - /*excess=*/fee_diff * 2};
819 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
820 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
821 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
822 BOOST_CHECK_EQUAL(0, selection.GetWaste());
823 }
824
825 {
826 // Negative waste when the long term fee is greater than the current fee and the selected value == target
828 const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff))
829 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
830 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
831 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
833 }
834
835 {
836 // Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee))
838 const CAmount large_fee_diff{90};
840 // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost
841 // = (2 * 100) - (2 * (100 + 90)) + 125
842 // = 200 - 380 + 125 = -55
843 assert(target_waste2 == -55);
846 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
848 }
849}
850
851
853{
854 const CAmount fee{100};
855 const CAmount min_viable_change{200};
856 const CAmount change_cost{125};
857 const CAmount change_fee{35};
858 const CAmount fee_diff{40};
859 const CAmount target{2 * COIN};
860
861 {
863 add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
864 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
865 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
866
867 for (size_t i = 0; i < inputs.size(); ++i) {
868 inputs[i]->ApplyBumpFee(20*(i+1));
869 }
870
871 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
872 CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60;
874
875 selection.SetBumpFeeDiscount(30);
876 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
877 expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60 - /*group_discount=*/30;
879 }
880
881 {
882 // Test with changeless transaction
883 //
884 // Bump fees and excess both contribute fully to the waste score,
885 // therefore, a bump fee group discount will not change the waste
886 // score as long as we do not create change in both instances.
887 CAmount changeless_target = 3 * COIN - 2 * fee - 100;
889 add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff);
890 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
891 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector();
892
893 for (size_t i = 0; i < inputs.size(); ++i) {
894 inputs[i]->ApplyBumpFee(20*(i+1));
895 }
896
897 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
898 CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40;
900
901 selection.SetBumpFeeDiscount(30);
902 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
903 expected_waste = fee_diff * -2 + /*bump_fees=*/60 - /*group_discount=*/30 + /*excess = 100 - bump_fees + group_discount*/70;
905 }
906}
907
909{
910 const int input_bytes = 148;
911 const CFeeRate feerate(1000);
912 const CAmount nValue = 10000;
913 const int nInput = 0;
914
916 tx.vout.resize(1);
917 tx.vout[nInput].nValue = nValue;
918
919 // standard case, pass feerate in constructor
920 COutput output1(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, feerate);
921 const CAmount expected_ev1 = 9852; // 10000 - 148
922 BOOST_CHECK_EQUAL(output1.GetEffectiveValue(), expected_ev1);
923
924 // input bytes unknown (input_bytes = -1), pass feerate in constructor
925 COutput output2(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/ false, feerate);
926 BOOST_CHECK_EQUAL(output2.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
927
928 // negative effective value, pass feerate in constructor
929 COutput output3(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, CFeeRate(100000));
930 const CAmount expected_ev3 = -4800; // 10000 - 14800
931 BOOST_CHECK_EQUAL(output3.GetEffectiveValue(), expected_ev3);
932
933 // standard case, pass fees in constructor
934 const CAmount fees = 148;
935 COutput output4(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fees);
936 BOOST_CHECK_EQUAL(output4.GetEffectiveValue(), expected_ev1);
937
938 // input bytes unknown (input_bytes = -1), pass fees in constructor
939 COutput output5(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/0);
940 BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1
941}
942
947 std::function<CoinsResult(CWallet&)> coin_setup)
948{
949 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
950 CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors
951 Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups;
952 return CoinGrinder(group.positive_group, target, cs_params.m_min_change_target, max_selection_weight);
953}
954
956{
957 // Test Coin Grinder:
958 // 1) Insufficient funds, select all provided coins and fail.
959 // 2) Exceeded max weight, coin selection always surpasses the max allowed weight.
960 // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not)
961 // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
962 // 5) Test finding a solution in a UTXO pool with mixed weights
963 // 6) Test that the lightest solution among many clones is found
964 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
965
967 CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag.
968 rand,
969 /*change_output_size=*/34,
970 /*change_spend_size=*/68,
971 /*min_change_target=*/CENT,
972 /*effective_feerate=*/CFeeRate(5000),
973 /*long_term_feerate=*/CFeeRate(2000),
974 /*discard_feerate=*/CFeeRate(1000),
975 /*tx_noinputs_size=*/10 + 34, // static header size + output size
976 /*avoid_partial=*/false,
977 };
978
979 {
980 // #########################################################
981 // 1) Insufficient funds, select all provided coins and fail
982 // #########################################################
983 CAmount target = 49.5L * COIN;
984 int max_selection_weight = 10'000; // high enough to not fail for this reason.
987 for (int j = 0; j < 10; ++j) {
990 }
991 return available_coins;
992 });
994 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds"
995 }
996
997 {
998 // ###########################
999 // 2) Test max weight exceeded
1000 // ###########################
1001 CAmount target = 29.5L * COIN;
1002 int max_selection_weight = 3000;
1005 for (int j = 0; j < 10; ++j) {
1006 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true);
1007 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
1008 }
1009 return available_coins;
1010 });
1011 BOOST_CHECK(!res);
1012 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos);
1013 }
1014
1015 {
1016 // ###############################################################################################################
1017 // 3) Test that the lowest-weight solution is found when some combinations would exceed the allowed weight
1018 // ################################################################################################################
1019 CAmount target = 25.33L * COIN;
1020 int max_selection_weight = 10'000; // WU
1023 for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU
1024 add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(5000), 144, false, 0, true);
1025 }
1026 for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU
1027 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true);
1028 }
1029 return available_coins;
1030 });
1031 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1032 for (int i = 0; i < 10; ++i) {
1033 add_coin(2 * COIN, i, expected_result);
1034 }
1035 for (int j = 0; j < 17; ++j) {
1036 add_coin(0.33 * COIN, j + 10, expected_result);
1037 }
1038 BOOST_CHECK(EquivalentResult(expected_result, *res));
1039 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1040 size_t expected_attempts = 37;
1041 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1042 }
1043
1044 {
1045 // #################################################################################################################
1046 // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO
1047 // #################################################################################################################
1048 CAmount target = 1.9L * COIN;
1049 int max_selection_weight = 400'000; // WU
1052 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 148);
1053 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68);
1054 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68);
1055 return available_coins;
1056 });
1057 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1058 add_coin(1 * COIN, 1, expected_result);
1059 add_coin(1 * COIN, 2, expected_result);
1060 BOOST_CHECK(EquivalentResult(expected_result, *res));
1061 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1062 size_t expected_attempts = 3;
1063 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1064 }
1065
1066 {
1067 // ###############################################################################################################
1068 // 5) Test finding a solution in a UTXO pool with mixed weights
1069 // ################################################################################################################
1070 CAmount target = 30L * COIN;
1071 int max_selection_weight = 400'000; // WU
1074 for (int j = 0; j < 5; ++j) {
1075 // Add heavy coins {3, 6, 9, 12, 15}
1076 add_coin(available_coins, wallet, CAmount((3 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 350);
1077 // Add medium coins {2, 5, 8, 11, 14}
1078 add_coin(available_coins, wallet, CAmount((2 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 250);
1079 // Add light coins {1, 4, 7, 10, 13}
1080 add_coin(available_coins, wallet, CAmount((1 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 150);
1081 }
1082 return available_coins;
1083 });
1085 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1086 add_coin(14 * COIN, 1, expected_result);
1087 add_coin(13 * COIN, 2, expected_result);
1088 add_coin(4 * COIN, 3, expected_result);
1089 BOOST_CHECK(EquivalentResult(expected_result, *res));
1090 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1091 size_t expected_attempts = 92;
1092 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1093 }
1094
1095 {
1096 // #################################################################################################################
1097 // 6) Test that the lightest solution among many clones is found
1098 // #################################################################################################################
1099 CAmount target = 9.9L * COIN;
1100 int max_selection_weight = 400'000; // WU
1103 // Expected Result: 4 + 3 + 2 + 1 = 10 BTC at 400 vB
1104 add_coin(available_coins, wallet, CAmount(4 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1105 add_coin(available_coins, wallet, CAmount(3 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1106 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1107 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 100);
1108 // Distracting clones:
1109 for (int j = 0; j < 100; ++j) {
1110 add_coin(available_coins, wallet, CAmount(8 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1111 }
1112 for (int j = 0; j < 100; ++j) {
1113 add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800);
1114 }
1115 for (int j = 0; j < 100; ++j) {
1116 add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600);
1117 }
1118 for (int j = 0; j < 100; ++j) {
1119 add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400);
1120 }
1121 return available_coins;
1122 });
1123 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1124 add_coin(4 * COIN, 0, expected_result);
1125 add_coin(3 * COIN, 0, expected_result);
1126 add_coin(2 * COIN, 0, expected_result);
1127 add_coin(1 * COIN, 0, expected_result);
1128 BOOST_CHECK(EquivalentResult(expected_result, *res));
1129 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1130 size_t expected_attempts = 38;
1131 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1132 }
1133
1134 {
1135 // #################################################################################################################
1136 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead
1137 // #################################################################################################################
1138 CAmount target = 1.9L * COIN;
1139 int max_selection_weight = 40000; // WU
1142 add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500);
1143 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1144 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000);
1145 for (int j = 0; j < 100; ++j) {
1146 // make a 100 unique coins only differing by one sat
1147 add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110);
1148 }
1149 return available_coins;
1150 });
1151 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1152 add_coin(1 * COIN, 1, expected_result);
1153 add_coin(1 * COIN, 2, expected_result);
1154 BOOST_CHECK(EquivalentResult(expected_result, *res));
1155 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future.
1156 size_t expected_attempts = 7;
1157 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
1158 }
1159
1160 {
1161 // #################################################################################################################
1162 // 8) Test input set that has a solution will not find a solution before reaching the attempt limit
1163 // #################################################################################################################
1164 CAmount target = 8 * COIN;
1165 int max_selection_weight = 3200; // WU
1166 dummy_params.m_min_change_target = 0;
1169 for (int i = 0; i < 18; ++i) {
1170 add_coin(doppelgangers, wallet, CAmount(1 * COIN + i), CFeeRate(0), 144, false, 0, true, 96 + i);
1171 }
1172 return doppelgangers;
1173 });
1175 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG);
1176 for (int i = 0; i < 8; ++i) {
1177 add_coin(1 * COIN + i, 0, expected_result);
1178 }
1179 BOOST_CHECK(EquivalentResult(expected_result, *result_a));
1180 // Demonstrate a solution is found before the attempts limit is reached.
1181 size_t expected_attempts = 87'525;
1182 BOOST_CHECK_MESSAGE(result_a->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result_a->GetSelectionsEvaluated()));
1183
1184 // Adding one more doppelganger causes the attempt limit to be reached before finding a solution.
1187 for (int i = 0; i < 19; ++i) {
1188 add_coin(doppelgangers, wallet, CAmount(1 * COIN + i), CFeeRate(0), 144, false, 0, true, 96 + i);
1189 }
1190 return doppelgangers;
1191 });
1193 }
1194}
1195
1197{
1198 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1200
1201 LOCK(wallet->cs_wallet);
1202 auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/ {}, target, cc, cs_params);
1203 if (result) {
1204 const auto signedTxSize = 10 + 34 + 68 * result->GetInputSet().size(); // static header size + output size + inputs size (P2WPKH)
1206
1207 BOOST_CHECK_GE(result->GetSelectedValue(), target);
1208 }
1209 return result;
1210}
1211
1212static bool has_coin(const OutputSet& set, CAmount amount)
1213{
1214 return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin->GetEffectiveValue() == amount; });
1215}
1216
1218{
1219 const CAmount target = 49.5L * COIN;
1221
1222 FastRandomContext rand;
1224 rand,
1225 /*change_output_size=*/34,
1226 /*change_spend_size=*/68,
1227 /*min_change_target=*/CENT,
1228 /*effective_feerate=*/CFeeRate(0),
1229 /*long_term_feerate=*/CFeeRate(0),
1230 /*discard_feerate=*/CFeeRate(0),
1231 /*tx_noinputs_size=*/10 + 34, // static header size + output size
1232 /*avoid_partial=*/false,
1233 };
1234
1235 int max_weight = MAX_STANDARD_TX_WEIGHT - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size);
1236 {
1237 // Scenario 1:
1238 // The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs
1239 // Then tries to spend 49.5 BTC
1240 // The 50.0 BTC output should be selected, because the transaction would otherwise be too large
1241
1242 // Perform selection
1243
1244 const auto result = select_coins(
1245 target, cs_params, cc, [&](CWallet& wallet) {
1247 for (int j = 0; j < 1515; ++j) {
1248 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true);
1249 }
1250
1251 add_coin(available_coins, wallet, CAmount(50 * COIN), CFeeRate(0), 144, false, 0, true);
1252 return available_coins;
1253 },
1254 m_node);
1255
1256 BOOST_CHECK(result);
1257 // Verify that the 50 BTC UTXO was selected, and result is below max_weight
1258 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(50 * COIN)));
1259 BOOST_CHECK_LE(result->GetWeight(), max_weight);
1260 }
1261
1262 {
1263 // Scenario 2:
1264
1265 // The actor starts with 400x 0.0625 BTC and 2000x 0.025 BTC (75.0 BTC total) unspent outputs
1266 // Then tries to spend 49.5 BTC
1267 // A combination of coins should be selected, such that the created transaction is not too large
1268
1269 // Perform selection
1270 const auto result = select_coins(
1271 target, cs_params, cc, [&](CWallet& wallet) {
1273 for (int j = 0; j < 400; ++j) {
1274 add_coin(available_coins, wallet, CAmount(0.0625 * COIN), CFeeRate(0), 144, false, 0, true);
1275 }
1276 for (int j = 0; j < 2000; ++j) {
1277 add_coin(available_coins, wallet, CAmount(0.025 * COIN), CFeeRate(0), 144, false, 0, true);
1278 }
1279 return available_coins;
1280 },
1281 m_node);
1282
1283 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.0625 * COIN)));
1284 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.025 * COIN)));
1285 BOOST_CHECK_LE(result->GetWeight(), max_weight);
1286 }
1287
1288 {
1289 // Scenario 3:
1290
1291 // The actor starts with 1515x 0.033 BTC (49.995 BTC total) unspent outputs
1292 // No results should be returned, because the transaction would be too large
1293
1294 // Perform selection
1295 const auto result = select_coins(
1296 target, cs_params, cc, [&](CWallet& wallet) {
1298 for (int j = 0; j < 1515; ++j) {
1299 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true);
1300 }
1301 return available_coins;
1302 },
1303 m_node);
1304
1305 // No results
1306 // 1515 inputs * 68 bytes = 103,020 bytes
1307 // 103,020 bytes * 4 = 412,080 weight, which is above the MAX_STANDARD_TX_WEIGHT of 400,000
1308 BOOST_CHECK(!result);
1309 }
1310}
1311
1313{
1314 // Test that the effective value is used to check whether preset inputs provide sufficient funds when subtract_fee_outputs is not used.
1315 // This test creates a coin whose value is higher than the target but whose effective value is lower than the target.
1316 // The coin is selected using coin control, with m_allow_other_inputs = false. SelectCoins should fail due to insufficient funds.
1317
1318 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1319
1321 {
1322 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy");
1323 add_coin(available_coins, *dummyWallet, 100000); // 0.001 BTC
1324 }
1325
1326 CAmount target{99900}; // 0.000999 BTC
1327
1328 FastRandomContext rand;
1330 rand,
1331 /*change_output_size=*/34,
1332 /*change_spend_size=*/148,
1333 /*min_change_target=*/1000,
1334 /*effective_feerate=*/CFeeRate(3000),
1335 /*long_term_feerate=*/CFeeRate(1000),
1336 /*discard_feerate=*/CFeeRate(1000),
1337 /*tx_noinputs_size=*/0,
1338 /*avoid_partial=*/false,
1339 };
1341 cc.m_allow_other_inputs = false;
1342 COutput output = available_coins.All().at(0);
1343 cc.SetInputWeight(output.outpoint, 148);
1344 cc.Select(output.outpoint).SetTxOut(output.txout);
1345
1346 LOCK(wallet->cs_wallet);
1348 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
1349
1351 BOOST_CHECK(!result);
1352}
1353
1355{
1356 // Test case to verify CoinsResult object sanity.
1358 {
1359 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy");
1360
1361 // Add some coins to 'available_coins'
1362 for (int i=0; i<10; i++) {
1364 }
1365 }
1366
1367 {
1368 // First test case, check that 'CoinsResult::Erase' function works as expected.
1369 // By trying to erase two elements from the 'available_coins' object.
1370 std::unordered_set<COutPoint, SaltedOutpointHasher> outs_to_remove;
1371 const auto& coins = available_coins.All();
1372 for (int i = 0; i < 2; i++) {
1373 outs_to_remove.emplace(coins[i].outpoint);
1374 }
1376
1377 // Check that the elements were actually removed.
1378 const auto& updated_coins = available_coins.All();
1379 for (const auto& out: outs_to_remove) {
1380 auto it = std::find_if(updated_coins.begin(), updated_coins.end(), [&out](const COutput &coin) {
1381 return coin.outpoint == out;
1382 });
1383 BOOST_CHECK(it == updated_coins.end());
1384 }
1385 // And verify that no extra element were removed
1387 }
1388}
1389
1391} // 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
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:119
Fee rate in satoshis per virtualbyte: CAmount / vB the feerate is represented internally as FeeFrac.
Definition feerate.h:32
CAmount GetFee(int32_t virtual_bytes) const
Return the fee in satoshis for the given vsize in vbytes.
Definition feerate.cpp:27
An outpoint - a combination of a transaction hash and an index n into its vout.
Definition transaction.h:30
Fast randomness source.
Definition random.h:386
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:196
Coin Control Features.
Definition coincontrol.h:83
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:93
A CWallet maintains a set of transactions and balances, and provides the ability to create new transa...
Definition wallet.h:318
A transaction with a bunch of additional info that only the owner cares about.
const Txid & GetHash() const LIFETIMEBOUND
int64_t GetTxTime() const
CTransactionRef GetTx() const
#define RANDOM_REPEATS
#define RUN_TESTS
constexpr int WITNESS_SCALE_FACTOR
Definition consensus.h:21
BOOST_FIXTURE_TEST_SUITE(cuckoocache_tests, BasicTestingSetup)
Test Suite for CuckooCache.
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE)
uint64_t fee
bilingual_str ErrorString(const Result< T > &result)
Definition result.h:93
static const CoinEligibilityFilter filter_standard(1, 6, 0)
std::unique_ptr< WalletDatabase > CreateMockableWalletDatabase()
Definition util.cpp:126
FilteredOutputGroups GroupOutputs(const CWallet &wallet, const CoinsResult &coins, const CoinSelectionParams &coin_sel_params, const std::vector< SelectionFilter > &filters, std::vector< OutputGroup > &ret_discarded_groups)
Group coins by the provided filters, groups that pass no filter are appended to ret_discarded_groups.
Definition spend.cpp:568
util::Result< CoinsResult > FetchSelectedInputs(const CWallet &wallet, const CCoinControl &coin_control, const CoinSelectionParams &coin_selection_params)
Fetch and validate coin control selected inputs.
Definition spend.cpp:265
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, uint32_t nInput, std::vector< OutputGroup > &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)
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)
std::set< std::shared_ptr< COutput >, OutputPtrComparator > OutputSet
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 CoinsResult &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:812
static bool has_coin(const OutputSet &set, CAmount amount)
static const CoinEligibilityFilter filter_standard_extra(6, 6, 0)
static bool EqualResult(const SelectionResult &a, const SelectionResult &b)
Check if this selection is equal to another one.
BOOST_AUTO_TEST_CASE(bnb_test)
@ WALLET_FLAG_DESCRIPTORS
Indicate that this wallet supports DescriptorScriptPubKeyMan.
Definition walletutil.h:53
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:93
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,...
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:17
#define BOOST_CHECK(expr)
Definition object.cpp:16
constexpr int32_t MAX_STANDARD_TX_WEIGHT
The maximum weight for transactions we're willing to relay/mine.
Definition policy.h:38
static CTransactionRef MakeTransactionRef(Tx &&txIn)
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:57
std::unique_ptr< interfaces::Chain > chain
Definition context.h:78
A UTXO under consideration for use in funding a new transaction.
COutPoint outpoint
The outpoint identifying this UTXO.
CTxOut txout
The output itself.
Parameters for filtering which OutputGroups we may use in coin selection.
Parameters for one iteration of Coin Selection.
COutputs available for spending, stored by OutputType.
Definition spend.h:45
void Add(OutputType type, const COutput &out)
Definition spend.cpp:236
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 AddInput(const OutputGroup &group)
const OutputSet & GetInputSet() const
Get m_selected_inputs.
State of transaction not confirmed or conflicting with a known block and not in the mempool.
Definition transaction.h:61
#define LOCK(cs)
Definition sync.h:268
#define WITH_LOCK(cs, code)
Run code while locking a mutex.
Definition sync.h:299
FastRandomContext rng
#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))
constexpr auto Ticks(Dur2 d)
Helper to count the seconds of a duration/time_point.
Definition time.h:82
assert(!tx.IsCoinBase())