Bitcoin Core 32.1.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,
85 CAmount change_target, FastRandomContext& rng)
86{
87 auto res{KnapsackSolver(groups, nTargetValue, change_target, rng, MAX_STANDARD_TX_WEIGHT)};
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{
93 auto res{SelectCoinsBnB(utxo_pool, selection_target, cost_of_change, MAX_STANDARD_TX_WEIGHT)};
94 return res ? std::optional<SelectionResult>(*res) : std::nullopt;
95}
96
99static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
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{};
142 CoinSelectionParams coin_selection_params{
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 };
153 static OutputGroupTypeMap static_groups;
154 static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter];
155 return static_groups.all_groups.mixed_group;
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
168BOOST_AUTO_TEST_CASE(bnb_search_test)
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
180 CoinSelectionParams coin_selection_params_bnb{
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
198 CoinsResult available_coins;
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
202 BOOST_CHECK(!SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change));
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);
209 BOOST_CHECK(result9);
210 BOOST_CHECK_EQUAL(result9->GetSelectedValue(), 1 * CENT);
211 expected_attempts = 1;
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
218 CoinsResult available_coins;
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);
224 CCoinControl coin_control;
225 coin_control.m_allow_other_inputs = true;
226 COutput select_coin = available_coins.All().at(0);
227 coin_control.Select(select_coin.outpoint);
228 CoinsResult selected_input;
229 selected_input.Add(OutputType::BECH32, select_coin);
230 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
231
232 LOCK(wallet->cs_wallet);
233 const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb);
234 BOOST_CHECK(result10);
235 expected_attempts = 3;
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
242 CoinsResult available_coins;
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);
257 CCoinControl coin_control;
258 coin_control.m_allow_other_inputs = true;
259 COutput select_coin = available_coins.All().at(1); // pre select 9 coin
260 coin_control.Select(select_coin.outpoint);
261 CoinsResult selected_input;
262 selected_input.Add(OutputType::BECH32, select_coin);
263 available_coins.Erase({(++available_coins.coins[OutputType::BECH32].begin())->outpoint});
264 const auto result13 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb);
265 BOOST_CHECK(EquivalentResult(expected_result, *result13));
266 expected_attempts = 2;
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
276 CoinsResult available_coins;
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
284 CAmount selection_target = 16 * CENT;
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);
287 BOOST_REQUIRE(!no_res);
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));
299 expected_attempts = 22;
300 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated()));
301 }
302}
303
304BOOST_AUTO_TEST_CASE(bnb_sffo_restriction)
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;
326 params.m_min_change_target = params.m_cost_of_change + 1;
327 // Add spendable coin at the BnB selection upper bound
328 CoinsResult available_coins;
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
342BOOST_AUTO_TEST_CASE(knapsack_solver_test)
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
349 CoinsResult available_coins;
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
365 const auto result1 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
366 BOOST_CHECK(result1);
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
375 const auto result2 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 3 * CENT, CENT);
376 BOOST_CHECK(result2);
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
390 const auto result3 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 37 * CENT, CENT);
391 BOOST_CHECK(result3);
392 BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT);
393 // and we can make 38 cents if we accept all new coins
394 const auto result4 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 38 * CENT, CENT);
395 BOOST_CHECK(result4);
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
399 const auto result5 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 34 * CENT, CENT);
400 BOOST_CHECK(result5);
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
405 const auto result6 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 7 * CENT, CENT);
406 BOOST_CHECK(result6);
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.
411 const auto result7 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 8 * CENT, CENT);
412 BOOST_CHECK(result7);
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)
417 const auto result8 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 9 * CENT, CENT);
418 BOOST_CHECK(result8);
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
425 add_coin(available_coins, *wallet, 6*CENT);
426 add_coin(available_coins, *wallet, 7*CENT);
427 add_coin(available_coins, *wallet, 8*CENT);
428 add_coin(available_coins, *wallet, 20*CENT);
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
432 const auto result9 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 71 * CENT, CENT);
433 BOOST_CHECK(result9);
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
437 const auto result10 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
438 BOOST_CHECK(result10);
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
445 const auto result11 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
446 BOOST_CHECK(result11);
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
453 const auto result12 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT);
454 BOOST_CHECK(result12);
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
459 const auto result13 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 11 * CENT, CENT);
460 BOOST_CHECK(result13);
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
465 add_coin(available_coins, *wallet, 1*COIN);
466 add_coin(available_coins, *wallet, 2*COIN);
467 add_coin(available_coins, *wallet, 3*COIN);
468 add_coin(available_coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents
469 const auto result14 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 95 * CENT, CENT);
470 BOOST_CHECK(result14);
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
474 const auto result15 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 195 * CENT, CENT);
475 BOOST_CHECK(result15);
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
490 const auto result16 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT);
491 BOOST_CHECK(result16);
492 BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT);
493
494 // but if we add a bigger coin, small change is avoided
495 add_coin(available_coins, *wallet, 1111*CENT);
496
497 // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5
498 const auto result17 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
499 BOOST_CHECK(result17);
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
507 const auto result18 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
508 BOOST_CHECK(result18);
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++)
515 add_coin(available_coins, *wallet, 50000 * COIN);
516
517 const auto result19 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 500000 * COIN, CENT);
518 BOOST_CHECK(result19);
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);
530 add_coin(available_coins, *wallet, 1111 * CENT);
531 const auto result20 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT);
532 BOOST_CHECK(result20);
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);
541 add_coin(available_coins, *wallet, 1111 * CENT);
542 const auto result21 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT);
543 BOOST_CHECK(result21);
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);
550 add_coin(available_coins, *wallet, CENT * 1);
551 add_coin(available_coins, *wallet, CENT * 100);
552
553 // trying to make 100.01 from these three coins
554 const auto result22 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 10001 / 100, CENT);
555 BOOST_CHECK(result22);
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
560 const auto result23 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 9990 / 100, CENT);
561 BOOST_CHECK(result23);
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++)
571 add_coin(available_coins, *wallet, amt);
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++) {
575 const auto result24 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 2000, CENT);
576 BOOST_CHECK(result24);
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++)
596 add_coin(available_coins, *wallet, COIN);
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);
603 BOOST_CHECK(result25);
604 const auto result26 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT);
605 BOOST_CHECK(result26);
606 BOOST_CHECK(!EqualResult(*result25, *result26));
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
615 const auto result27 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT);
616 BOOST_CHECK(result27);
617 const auto result28 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT);
618 BOOST_CHECK(result28);
619 if (EqualResult(*result27, *result28))
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
628 add_coin(available_coins, *wallet, 5 * CENT);
629 add_coin(available_coins, *wallet, 10 * CENT);
630 add_coin(available_coins, *wallet, 15 * CENT);
631 add_coin(available_coins, *wallet, 20 * CENT);
632 add_coin(available_coins, *wallet, 25 * CENT);
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);
639 BOOST_CHECK(result29);
640 const auto result30 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT);
641 BOOST_CHECK(result30);
642 if (EqualResult(*result29, *result30))
643 fails++;
644 }
646 }
647 }
648}
649
650BOOST_AUTO_TEST_CASE(ApproximateBestSubset)
651{
652 FastRandomContext rand{};
653 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
654
655 CoinsResult available_coins;
656
657 // Test vValue sort order
658 for (int i = 0; i < 1000; i++)
659 add_coin(available_coins, *wallet, 1000 * COIN);
660 add_coin(available_coins, *wallet, 3 * COIN);
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
669BOOST_AUTO_TEST_CASE(SelectCoins_test)
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 {
682 CoinsResult available_coins;
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;
689 add_coin(available_coins, *wallet, val);
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
700 CoinSelectionParams cs_params{
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;
713 CCoinControl cc;
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);
742 BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, selection1.GetWaste());
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);
757 BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, selection3.GetWaste());
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
763 SelectionResult selection_nochange1{exact_target - excess, SelectionAlgorithm::MANUAL};
764 add_coin(1 * COIN, 1, selection_nochange1, fee, fee - fee_diff);
765 add_coin(2 * COIN, 2, selection_nochange1, fee, fee - fee_diff);
766 selection_nochange1.RecalculateWaste(min_viable_change, change_cost, change_fee);
767 BOOST_CHECK_EQUAL(fee_diff * 2 + excess, selection_nochange1.GetWaste());
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
771 SelectionResult selection_nochange2{exact_target - excess, SelectionAlgorithm::MANUAL};
772 add_coin(1 * COIN, 1, selection_nochange2, fee, fee + fee_diff);
773 add_coin(2 * COIN, 2, selection_nochange2, fee, fee + fee_diff);
774 selection_nochange2.RecalculateWaste(min_viable_change, change_cost, change_fee);
775 BOOST_CHECK_EQUAL(fee_diff * -2 + excess, selection_nochange2.GetWaste());
776 BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste());
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);
785 BOOST_CHECK_EQUAL(change_cost, selection.GetWaste());
786 }
787
788 {
789 // Waste without change and fee == long term fee is just the excess
790 SelectionResult selection{exact_target - excess, SelectionAlgorithm::MANUAL};
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);
794 BOOST_CHECK_EQUAL(excess, selection.GetWaste());
795 }
796
797 {
798 // Waste is 0 when fee == long_term_fee, no change, and no excess
799 SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
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};
818 SelectionResult selection{new_target, SelectionAlgorithm::MANUAL};
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
827 SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
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);
832 BOOST_CHECK_EQUAL(target_waste1, selection.GetWaste());
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};
839 const CAmount target_waste2{-2 * large_fee_diff + change_cost};
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);
844 add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff);
845 add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff);
846 selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
847 BOOST_CHECK_EQUAL(target_waste2, selection.GetWaste());
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;
873 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
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;
878 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
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;
888 SelectionResult selection{changeless_target, SelectionAlgorithm::MANUAL};
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;
899 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
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;
904 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
905 }
906}
907
908BOOST_AUTO_TEST_CASE(effective_value_test)
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
944 const CoinSelectionParams& cs_params,
945 const node::NodeContext& m_node,
946 int max_selection_weight,
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
955BOOST_AUTO_TEST_CASE(coin_grinder_tests)
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.
985 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
986 CoinsResult available_coins;
987 for (int j = 0; j < 10; ++j) {
988 add_coin(available_coins, wallet, CAmount(1 * COIN));
989 add_coin(available_coins, wallet, CAmount(2 * COIN));
990 }
991 return available_coins;
992 });
993 BOOST_CHECK(!res);
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;
1003 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1004 CoinsResult available_coins;
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
1021 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1022 CoinsResult available_coins;
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
1050 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1051 CoinsResult available_coins;
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
1072 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1073 CoinsResult available_coins;
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 });
1084 BOOST_CHECK(res);
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
1101 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1102 CoinsResult available_coins;
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
1140 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1141 CoinsResult available_coins;
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;
1167 const auto& result_a = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1168 CoinsResult doppelgangers;
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 });
1174 BOOST_CHECK(result_a);
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.
1185 const auto& result_b = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) {
1186 CoinsResult doppelgangers;
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 });
1192 BOOST_CHECK(!result_b);
1193 }
1194}
1195
1196static 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)
1197{
1198 std::unique_ptr<CWallet> wallet = NewWallet(m_node);
1199 auto available_coins = coin_setup(*wallet);
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)
1205 BOOST_CHECK_LE(signedTxSize * WITNESS_SCALE_FACTOR, MAX_STANDARD_TX_WEIGHT);
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
1217BOOST_AUTO_TEST_CASE(check_max_selection_weight)
1218{
1219 const CAmount target = 49.5L * COIN;
1220 CCoinControl cc;
1221
1222 FastRandomContext rand;
1223 CoinSelectionParams cs_params{
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) {
1246 CoinsResult available_coins;
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) {
1272 CoinsResult available_coins;
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) {
1297 CoinsResult available_coins;
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
1312BOOST_AUTO_TEST_CASE(SelectCoins_effective_value_test)
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
1320 CoinsResult available_coins;
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;
1329 CoinSelectionParams cs_params{
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 };
1340 CCoinControl cc;
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);
1347 const auto preset_inputs = *Assert(FetchSelectedInputs(*wallet, cc, cs_params));
1348 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});
1349
1350 const auto result = SelectCoins(*wallet, available_coins, preset_inputs, target, cc, cs_params);
1351 BOOST_CHECK(!result);
1352}
1353
1355{
1356 // Test case to verify CoinsResult object sanity.
1357 CoinsResult available_coins;
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++) {
1363 add_coin(available_coins, *dummyWallet, 1 * COIN);
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 }
1375 available_coins.Erase(outs_to_remove);
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
1386 BOOST_CHECK_EQUAL(available_coins.Size(), 8);
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
#define Assert(val)
Identity function.
Definition check.h:116
Fee rate in satoshis per virtualbyte: CAmount / vB the feerate is represented internally as FeeFrac.
Definition feerate.h:32
CFeeRate()=default
Fee rate of 0 satoshis per 0 vB.
CAmount GetFee(int32_t virtual_bytes) const
Return the fee in satoshis for the given vsize in vbytes.
Definition feerate.cpp:21
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
PreselectedInput & Select(const COutPoint &outpoint)
Lock-in the given output for spending.
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:310
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
void SetTxOut(const CTxOut &txout)
Set the previous output for this input.
#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()
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:120
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: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 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)
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:811
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)
std::set< std::shared_ptr< COutput >, OutputPtrComparator > OutputSet
@ 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:92
static void add_coin(const CAmount &nValue, int nInput, SelectionResult &result)
static bool EquivalentResult(const SelectionResult &a, const SelectionResult &b)
Check if SelectionResult a is equivalent to SelectionResult b.
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
constexpr CAmount CENT
Basic testing setup.
A mutable version of CTransaction.
std::vector< CTxOut > vout
Txid GetHash() const
Compute the hash of this CMutableTransaction.
std::unique_ptr< interfaces::Chain > chain
Definition context.h:80
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.
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.
CFeeRate m_effective_feerate
The targeted feerate of the transaction being built.
CAmount min_viable_change
Minimum amount for creating a change output.
int change_spend_size
Size of the input to spend a change output in virtual bytes.
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.
CFeeRate m_long_term_feerate
The feerate estimate used to estimate an upper bound on what should be sufficient to spend the change...
CFeeRate m_discard_feerate
If the cost to spend a change output at the discard feerate exceeds its value, drop it to fees.
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:45
void Add(OutputType type, const COutput &out)
Definition spend.cpp:236
std::vector< COutput > All() const
Concatenate and return all COutputs as one vector.
Definition spend.cpp:203
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:194
std::map< OutputType, std::vector< COutput > > coins
Definition spend.h:46
void Erase(const std::unordered_set< COutPoint, SaltedOutpointHasher > &coins_to_remove)
Definition spend.cpp:213
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 SetBumpFeeDiscount(CAmount discount)
How much individual inputs overestimated the bump fees for shared ancestries.
void RecalculateWaste(CAmount min_viable_change, CAmount change_cost, CAmount change_fee)
Calculates and stores the waste for this result given the cost of change and the opportunity cost of ...
void AddInput(const OutputGroup &group)
const OutputSet & GetInputSet() const
Get m_selected_inputs.
std::vector< std::shared_ptr< COutput > > GetShuffledInputVector() const
Get the vector of COutputs that will be used to fill in a CTransaction's vin.
Testing setup and teardown for wallet.
#define LOCK(cs)
Definition sync.h:268
#define WITH_LOCK(cs, code)
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...
transaction_identifier< false > Txid
Txid commits to all transaction fields except the witness.
BOOST_CHECK_NE(OneL.ToString(), ArrayToString(ZeroArray, 32))
assert(!tx.IsCoinBase())