coinselector_tests.cpp
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> 8 #include <primitives/transaction.h> 9 #include <random.h> 10 #include <test/util/common.h> 11 #include <test/util/setup_common.h> 12 #include <util/translation.h> 13 #include <wallet/coincontrol.h> 14 #include <wallet/coinselection.h> 15 #include <wallet/spend.h> 16 #include <wallet/test/util.h> 17 #include <wallet/test/wallet_test_fixture.h> 18 #include <wallet/wallet.h> 19 20 #include <algorithm> 21 #include <boost/test/unit_test.hpp> 22 #include <random> 23 24 namespace wallet { 25 BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup) 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 34 static const CoinEligibilityFilter filter_standard(1, 6, 0); 35 static const CoinEligibilityFilter filter_confirmed(1, 1, 0); 36 static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); 37 static int nextLockTime = 0; 38 39 static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result) 40 { 41 CMutableTransaction tx; 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 51 static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result, CAmount fee, CAmount long_term_fee) 52 { 53 CMutableTransaction tx; 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 64 static 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 { 66 CMutableTransaction tx; 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.tx->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 84 std::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 91 std::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 97 /** Check if SelectionResult a is equivalent to SelectionResult b. 98 * Equivalent means same input values, but maybe different inputs (i.e. same value, different prevout) */ 99 static 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 116 /** Check if this selection is equal to another one. Equal means same inputs (i.e same value and prevout) */ 117 static 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 126 inline 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 139 inline 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 158 static 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 168 BOOST_AUTO_TEST_CASE(bnb_search_test) 169 { 170 FastRandomContext rand{}; 171 // Setup 172 std::vector<COutput> utxo_pool; 173 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB); 174 175 //////////////////// 176 // Behavior tests // 177 //////////////////// 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.Clear(); 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 } 212 213 { 214 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 215 216 CoinsResult available_coins; 217 218 coin_selection_params_bnb.m_effective_feerate = CFeeRate(0); 219 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 220 add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 221 add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 222 CCoinControl coin_control; 223 coin_control.m_allow_other_inputs = true; 224 COutput select_coin = available_coins.All().at(0); 225 coin_control.Select(select_coin.outpoint); 226 CoinsResult selected_input; 227 selected_input.Add(OutputType::BECH32, select_coin); 228 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint}); 229 230 LOCK(wallet->cs_wallet); 231 const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb); 232 BOOST_CHECK(result10); 233 } 234 { 235 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 236 LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it 237 238 CoinsResult available_coins; 239 240 // pre selected coin should be selected even if disadvantageous 241 coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000); 242 coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000); 243 244 // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount 245 CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*virtual_bytes=*/68); // bech32 input size (default test output type) 246 add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 247 add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 248 add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 249 250 expected_result.Clear(); 251 add_coin(9 * CENT + input_fee, 2, expected_result); 252 add_coin(1 * CENT + input_fee, 2, expected_result); 253 CCoinControl coin_control; 254 coin_control.m_allow_other_inputs = true; 255 COutput select_coin = available_coins.All().at(1); // pre select 9 coin 256 coin_control.Select(select_coin.outpoint); 257 CoinsResult selected_input; 258 selected_input.Add(OutputType::BECH32, select_coin); 259 available_coins.Erase({(++available_coins.coins[OutputType::BECH32].begin())->outpoint}); 260 const auto result13 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb); 261 BOOST_CHECK(EquivalentResult(expected_result, *result13)); 262 } 263 264 { 265 // Test bnb max weight exceeded 266 // Inputs set [10, 9, 8, 5, 3, 1], Selection Target = 16 and coin 5 exceeding the max weight. 267 268 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 269 270 CoinsResult available_coins; 271 add_coin(available_coins, *wallet, 10 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 272 add_coin(available_coins, *wallet, 9 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 273 add_coin(available_coins, *wallet, 8 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 274 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); 275 add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 276 add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 277 278 CAmount selection_target = 16 * CENT; 279 const auto& no_res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs=*/true), 280 selection_target, /*cost_of_change=*/0, MAX_STANDARD_TX_WEIGHT); 281 BOOST_REQUIRE(!no_res); 282 BOOST_CHECK(util::ErrorString(no_res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); 283 284 // Now add same coin value with a good size and check that it gets selected 285 add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); 286 const auto& res = SelectCoinsBnB(GroupCoins(available_coins.All(), /*subtract_fee_outputs=*/true), selection_target, /*cost_of_change=*/0); 287 288 expected_result.Clear(); 289 add_coin(8 * CENT, 2, expected_result); 290 add_coin(5 * CENT, 2, expected_result); 291 add_coin(3 * CENT, 2, expected_result); 292 BOOST_CHECK(EquivalentResult(expected_result, *res)); 293 } 294 } 295 296 BOOST_AUTO_TEST_CASE(bnb_sffo_restriction) 297 { 298 // Verify the coin selection process does not produce a BnB solution when SFFO is enabled. 299 // This is currently problematic because it could require a change output. And BnB is specialized on changeless solutions. 300 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 301 WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(300, uint256{})); // set a high block so internal UTXOs are selectable 302 303 FastRandomContext rand{}; 304 CoinSelectionParams params{ 305 rand, 306 /*change_output_size=*/ 31, // unused value, p2wpkh output size (wallet default change type) 307 /*change_spend_size=*/ 68, // unused value, p2wpkh input size (high-r signature) 308 /*min_change_target=*/ 0, // dummy, set later 309 /*effective_feerate=*/ CFeeRate(3000), 310 /*long_term_feerate=*/ CFeeRate(1000), 311 /*discard_feerate=*/ CFeeRate(1000), 312 /*tx_noinputs_size=*/ 0, 313 /*avoid_partial=*/ false, 314 }; 315 params.m_subtract_fee_outputs = true; 316 params.m_change_fee = params.m_effective_feerate.GetFee(params.change_output_size); 317 params.m_cost_of_change = params.m_discard_feerate.GetFee(params.change_spend_size) + params.m_change_fee; 318 params.m_min_change_target = params.m_cost_of_change + 1; 319 // Add spendable coin at the BnB selection upper bound 320 CoinsResult available_coins; 321 add_coin(available_coins, *wallet, COIN + params.m_cost_of_change, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true); 322 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); 323 add_coin(available_coins, *wallet, 0.5 * COIN, /*feerate=*/params.m_effective_feerate, /*nAge=*/6, /*fIsFromMe=*/true, /*nInput=*/0, /*spendable=*/true); 324 // Knapsack will only find a changeless solution on an exact match to the satoshi, SRD doesn’t look for changeless 325 // If BnB were run, it would produce a single input solution with the best waste score 326 auto result = WITH_LOCK(wallet->cs_wallet, return SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, COIN, /*coin_control=*/{}, params)); 327 BOOST_CHECK(result.has_value()); 328 BOOST_CHECK_NE(result->GetAlgo(), SelectionAlgorithm::BNB); 329 BOOST_CHECK(result->GetInputSet().size() == 2); 330 // We have only considered BnB, SRD, and Knapsack. Test needs to be reevaluated if new algo is added 331 BOOST_CHECK(result->GetAlgo() == SelectionAlgorithm::SRD || result->GetAlgo() == SelectionAlgorithm::KNAPSACK); 332 } 333 334 BOOST_AUTO_TEST_CASE(knapsack_solver_test) 335 { 336 FastRandomContext rand{}; 337 const auto temp1{[&rand](std::vector<OutputGroup>& g, const CAmount& v, CAmount c) { return KnapsackSolver(g, v, c, rand); }}; 338 const auto KnapsackSolver{temp1}; 339 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 340 341 CoinsResult available_coins; 342 343 // test multiple times to allow for differences in the shuffle order 344 for (int i = 0; i < RUN_TESTS; i++) 345 { 346 available_coins.Clear(); 347 348 // with an empty wallet we can't even pay one cent 349 BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1 * CENT, CENT)); 350 351 add_coin(available_coins, *wallet, 1*CENT, CFeeRate(0), 4); // add a new 1 cent coin 352 353 // with a new 1 cent coin, we still can't find a mature 1 cent 354 BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1 * CENT, CENT)); 355 356 // but we can find a new 1 cent 357 const auto result1 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); 358 BOOST_CHECK(result1); 359 BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT); 360 361 add_coin(available_coins, *wallet, 2*CENT); // add a mature 2 cent coin 362 363 // we can't make 3 cents of mature coins 364 BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 3 * CENT, CENT)); 365 366 // we can make 3 cents of new coins 367 const auto result2 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 3 * CENT, CENT); 368 BOOST_CHECK(result2); 369 BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 3 * CENT); 370 371 add_coin(available_coins, *wallet, 5*CENT); // add a mature 5 cent coin, 372 add_coin(available_coins, *wallet, 10*CENT, CFeeRate(0), 3, true); // a new 10 cent coin sent from one of our own addresses 373 add_coin(available_coins, *wallet, 20*CENT); // and a mature 20 cent coin 374 375 // now we have new: 1+10=11 (of which 10 was self-sent), and mature: 2+5+20=27. total = 38 376 377 // we can't make 38 cents only if we disallow new coins: 378 BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 38 * CENT, CENT)); 379 // we can't even make 37 cents if we don't allow new coins even if they're from us 380 BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard_extra), 38 * CENT, CENT)); 381 // but we can make 37 cents if we accept new coins from ourself 382 const auto result3 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 37 * CENT, CENT); 383 BOOST_CHECK(result3); 384 BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 37 * CENT); 385 // and we can make 38 cents if we accept all new coins 386 const auto result4 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 38 * CENT, CENT); 387 BOOST_CHECK(result4); 388 BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 38 * CENT); 389 390 // try making 34 cents from 1,2,5,10,20 - we can't do it exactly 391 const auto result5 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 34 * CENT, CENT); 392 BOOST_CHECK(result5); 393 BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 35 * CENT); // but 35 cents is closest 394 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) 395 396 // when we try making 7 cents, the smaller coins (1,2,5) are enough. We should see just 2+5 397 const auto result6 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 7 * CENT, CENT); 398 BOOST_CHECK(result6); 399 BOOST_CHECK_EQUAL(result6->GetSelectedValue(), 7 * CENT); 400 BOOST_CHECK_EQUAL(result6->GetInputSet().size(), 2U); 401 402 // when we try making 8 cents, the smaller coins (1,2,5) are exactly enough. 403 const auto result7 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 8 * CENT, CENT); 404 BOOST_CHECK(result7); 405 BOOST_CHECK(result7->GetSelectedValue() == 8 * CENT); 406 BOOST_CHECK_EQUAL(result7->GetInputSet().size(), 3U); 407 408 // when we try making 9 cents, no subset of smaller coins is enough, and we get the next bigger coin (10) 409 const auto result8 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 9 * CENT, CENT); 410 BOOST_CHECK(result8); 411 BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 10 * CENT); 412 BOOST_CHECK_EQUAL(result8->GetInputSet().size(), 1U); 413 414 // now clear out the wallet and start again to test choosing between subsets of smaller coins and the next biggest coin 415 available_coins.Clear(); 416 417 add_coin(available_coins, *wallet, 6*CENT); 418 add_coin(available_coins, *wallet, 7*CENT); 419 add_coin(available_coins, *wallet, 8*CENT); 420 add_coin(available_coins, *wallet, 20*CENT); 421 add_coin(available_coins, *wallet, 30*CENT); // now we have 6+7+8+20+30 = 71 cents total 422 423 // check that we have 71 and not 72 424 const auto result9 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 71 * CENT, CENT); 425 BOOST_CHECK(result9); 426 BOOST_CHECK(!KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 72 * CENT, CENT)); 427 428 // 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 429 const auto result10 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT); 430 BOOST_CHECK(result10); 431 BOOST_CHECK_EQUAL(result10->GetSelectedValue(), 20 * CENT); // we should get 20 in one coin 432 BOOST_CHECK_EQUAL(result10->GetInputSet().size(), 1U); 433 434 add_coin(available_coins, *wallet, 5*CENT); // now we have 5+6+7+8+20+30 = 75 cents total 435 436 // 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 437 const auto result11 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT); 438 BOOST_CHECK(result11); 439 BOOST_CHECK_EQUAL(result11->GetSelectedValue(), 18 * CENT); // we should get 18 in 3 coins 440 BOOST_CHECK_EQUAL(result11->GetInputSet().size(), 3U); 441 442 add_coin(available_coins, *wallet, 18*CENT); // now we have 5+6+7+8+18+20+30 443 444 // 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 445 const auto result12 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 16 * CENT, CENT); 446 BOOST_CHECK(result12); 447 BOOST_CHECK_EQUAL(result12->GetSelectedValue(), 18 * CENT); // we should get 18 in 1 coin 448 BOOST_CHECK_EQUAL(result12->GetInputSet().size(), 1U); // because in the event of a tie, the biggest coin wins 449 450 // now try making 11 cents. we should get 5+6 451 const auto result13 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 11 * CENT, CENT); 452 BOOST_CHECK(result13); 453 BOOST_CHECK_EQUAL(result13->GetSelectedValue(), 11 * CENT); 454 BOOST_CHECK_EQUAL(result13->GetInputSet().size(), 2U); 455 456 // check that the smallest bigger coin is used 457 add_coin(available_coins, *wallet, 1*COIN); 458 add_coin(available_coins, *wallet, 2*COIN); 459 add_coin(available_coins, *wallet, 3*COIN); 460 add_coin(available_coins, *wallet, 4*COIN); // now we have 5+6+7+8+18+20+30+100+200+300+400 = 1094 cents 461 const auto result14 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 95 * CENT, CENT); 462 BOOST_CHECK(result14); 463 BOOST_CHECK_EQUAL(result14->GetSelectedValue(), 1 * COIN); // we should get 1 BTC in 1 coin 464 BOOST_CHECK_EQUAL(result14->GetInputSet().size(), 1U); 465 466 const auto result15 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 195 * CENT, CENT); 467 BOOST_CHECK(result15); 468 BOOST_CHECK_EQUAL(result15->GetSelectedValue(), 2 * COIN); // we should get 2 BTC in 1 coin 469 BOOST_CHECK_EQUAL(result15->GetInputSet().size(), 1U); 470 471 // empty the wallet and start again, now with fractions of a cent, to test small change avoidance 472 473 available_coins.Clear(); 474 add_coin(available_coins, *wallet, CENT * 1 / 10); 475 add_coin(available_coins, *wallet, CENT * 2 / 10); 476 add_coin(available_coins, *wallet, CENT * 3 / 10); 477 add_coin(available_coins, *wallet, CENT * 4 / 10); 478 add_coin(available_coins, *wallet, CENT * 5 / 10); 479 480 // try making 1 * CENT from the 1.5 * CENT 481 // we'll get change smaller than CENT whatever happens, so can expect CENT exactly 482 const auto result16 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT); 483 BOOST_CHECK(result16); 484 BOOST_CHECK_EQUAL(result16->GetSelectedValue(), CENT); 485 486 // but if we add a bigger coin, small change is avoided 487 add_coin(available_coins, *wallet, 1111*CENT); 488 489 // try making 1 from 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 1111 = 1112.5 490 const auto result17 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); 491 BOOST_CHECK(result17); 492 BOOST_CHECK_EQUAL(result17->GetSelectedValue(), 1 * CENT); // we should get the exact amount 493 494 // if we add more small coins: 495 add_coin(available_coins, *wallet, CENT * 6 / 10); 496 add_coin(available_coins, *wallet, CENT * 7 / 10); 497 498 // and try again to make 1.0 * CENT 499 const auto result18 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); 500 BOOST_CHECK(result18); 501 BOOST_CHECK_EQUAL(result18->GetSelectedValue(), 1 * CENT); // we should get the exact amount 502 503 // run the 'mtgox' test (see https://blockexplorer.com/tx/29a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf) 504 // they tried to consolidate 10 50k coins into one 500k coin, and ended up with 50k in change 505 available_coins.Clear(); 506 for (int j = 0; j < 20; j++) 507 add_coin(available_coins, *wallet, 50000 * COIN); 508 509 const auto result19 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 500000 * COIN, CENT); 510 BOOST_CHECK(result19); 511 BOOST_CHECK_EQUAL(result19->GetSelectedValue(), 500000 * COIN); // we should get the exact amount 512 BOOST_CHECK_EQUAL(result19->GetInputSet().size(), 10U); // in ten coins 513 514 // 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), 515 // we need to try finding an exact subset anyway 516 517 // sometimes it will fail, and so we use the next biggest coin: 518 available_coins.Clear(); 519 add_coin(available_coins, *wallet, CENT * 5 / 10); 520 add_coin(available_coins, *wallet, CENT * 6 / 10); 521 add_coin(available_coins, *wallet, CENT * 7 / 10); 522 add_coin(available_coins, *wallet, 1111 * CENT); 523 const auto result20 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 1 * CENT, CENT); 524 BOOST_CHECK(result20); 525 BOOST_CHECK_EQUAL(result20->GetSelectedValue(), 1111 * CENT); // we get the bigger coin 526 BOOST_CHECK_EQUAL(result20->GetInputSet().size(), 1U); 527 528 // but sometimes it's possible, and we use an exact subset (0.4 + 0.6 = 1.0) 529 available_coins.Clear(); 530 add_coin(available_coins, *wallet, CENT * 4 / 10); 531 add_coin(available_coins, *wallet, CENT * 6 / 10); 532 add_coin(available_coins, *wallet, CENT * 8 / 10); 533 add_coin(available_coins, *wallet, 1111 * CENT); 534 const auto result21 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT, CENT); 535 BOOST_CHECK(result21); 536 BOOST_CHECK_EQUAL(result21->GetSelectedValue(), CENT); // we should get the exact amount 537 BOOST_CHECK_EQUAL(result21->GetInputSet().size(), 2U); // in two coins 0.4+0.6 538 539 // test avoiding small change 540 available_coins.Clear(); 541 add_coin(available_coins, *wallet, CENT * 5 / 100); 542 add_coin(available_coins, *wallet, CENT * 1); 543 add_coin(available_coins, *wallet, CENT * 100); 544 545 // trying to make 100.01 from these three coins 546 const auto result22 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 10001 / 100, CENT); 547 BOOST_CHECK(result22); 548 BOOST_CHECK_EQUAL(result22->GetSelectedValue(), CENT * 10105 / 100); // we should get all coins 549 BOOST_CHECK_EQUAL(result22->GetInputSet().size(), 3U); 550 551 // but if we try to make 99.9, we should take the bigger of the two small coins to avoid small change 552 const auto result23 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), CENT * 9990 / 100, CENT); 553 BOOST_CHECK(result23); 554 BOOST_CHECK_EQUAL(result23->GetSelectedValue(), 101 * CENT); 555 BOOST_CHECK_EQUAL(result23->GetInputSet().size(), 2U); 556 } 557 558 // test with many inputs 559 for (CAmount amt=1500; amt < COIN; amt*=10) { 560 available_coins.Clear(); 561 // Create 676 inputs (= (old MAX_STANDARD_TX_SIZE == 100000) / 148 bytes per input) 562 for (uint16_t j = 0; j < 676; j++) 563 add_coin(available_coins, *wallet, amt); 564 565 // We only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. 566 for (int i = 0; i < RUN_TESTS; i++) { 567 const auto result24 = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_confirmed), 2000, CENT); 568 BOOST_CHECK(result24); 569 570 if (amt - 2000 < CENT) { 571 // needs more than one input: 572 uint16_t returnSize = std::ceil((2000.0 + CENT)/amt); 573 CAmount returnValue = amt * returnSize; 574 BOOST_CHECK_EQUAL(result24->GetSelectedValue(), returnValue); 575 BOOST_CHECK_EQUAL(result24->GetInputSet().size(), returnSize); 576 } else { 577 // one input is sufficient: 578 BOOST_CHECK_EQUAL(result24->GetSelectedValue(), amt); 579 BOOST_CHECK_EQUAL(result24->GetInputSet().size(), 1U); 580 } 581 } 582 } 583 584 // test randomness 585 { 586 available_coins.Clear(); 587 for (int i2 = 0; i2 < 100; i2++) 588 add_coin(available_coins, *wallet, COIN); 589 590 // Again, we only create the wallet once to save time, but we still run the coin selection RUN_TESTS times. 591 for (int i = 0; i < RUN_TESTS; i++) { 592 // picking 50 from 100 coins doesn't depend on the shuffle, 593 // but does depend on randomness in the stochastic approximation code 594 const auto result25 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT); 595 BOOST_CHECK(result25); 596 const auto result26 = KnapsackSolver(GroupCoins(available_coins.All()), 50 * COIN, CENT); 597 BOOST_CHECK(result26); 598 BOOST_CHECK(!EqualResult(*result25, *result26)); 599 600 int fails = 0; 601 for (int j = 0; j < RANDOM_REPEATS; j++) 602 { 603 // Test that the KnapsackSolver selects randomly from equivalent coins (same value and same input size). 604 // When choosing 1 from 100 identical coins, 1% of the time, this test will choose the same coin twice 605 // which will cause it to fail. 606 // To avoid that issue, run the test RANDOM_REPEATS times and only complain if all of them fail 607 const auto result27 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT); 608 BOOST_CHECK(result27); 609 const auto result28 = KnapsackSolver(GroupCoins(available_coins.All()), COIN, CENT); 610 BOOST_CHECK(result28); 611 if (EqualResult(*result27, *result28)) 612 fails++; 613 } 614 BOOST_CHECK_NE(fails, RANDOM_REPEATS); 615 } 616 617 // add 75 cents in small change. not enough to make 90 cents, 618 // then try making 90 cents. there are multiple competing "smallest bigger" coins, 619 // one of which should be picked at random 620 add_coin(available_coins, *wallet, 5 * CENT); 621 add_coin(available_coins, *wallet, 10 * CENT); 622 add_coin(available_coins, *wallet, 15 * CENT); 623 add_coin(available_coins, *wallet, 20 * CENT); 624 add_coin(available_coins, *wallet, 25 * CENT); 625 626 for (int i = 0; i < RUN_TESTS; i++) { 627 int fails = 0; 628 for (int j = 0; j < RANDOM_REPEATS; j++) 629 { 630 const auto result29 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT); 631 BOOST_CHECK(result29); 632 const auto result30 = KnapsackSolver(GroupCoins(available_coins.All()), 90 * CENT, CENT); 633 BOOST_CHECK(result30); 634 if (EqualResult(*result29, *result30)) 635 fails++; 636 } 637 BOOST_CHECK_NE(fails, RANDOM_REPEATS); 638 } 639 } 640 } 641 642 BOOST_AUTO_TEST_CASE(ApproximateBestSubset) 643 { 644 FastRandomContext rand{}; 645 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 646 647 CoinsResult available_coins; 648 649 // Test vValue sort order 650 for (int i = 0; i < 1000; i++) 651 add_coin(available_coins, *wallet, 1000 * COIN); 652 add_coin(available_coins, *wallet, 3 * COIN); 653 654 const auto result = KnapsackSolver(KnapsackGroupOutputs(available_coins, *wallet, filter_standard), 1003 * COIN, CENT, rand); 655 BOOST_CHECK(result); 656 BOOST_CHECK_EQUAL(result->GetSelectedValue(), 1003 * COIN); 657 BOOST_CHECK_EQUAL(result->GetInputSet().size(), 2U); 658 } 659 660 // Tests that with the ideal conditions, the coin selector will always be able to find a solution that can pay the target value 661 BOOST_AUTO_TEST_CASE(SelectCoins_test) 662 { 663 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 664 LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it 665 666 // Random generator stuff 667 std::default_random_engine generator; 668 std::exponential_distribution<double> distribution (100); 669 FastRandomContext rand; 670 671 // Run this test 100 times 672 for (int i = 0; i < 100; ++i) 673 { 674 CoinsResult available_coins; 675 CAmount balance{0}; 676 677 // Make a wallet with 1000 exponentially distributed random inputs 678 for (int j = 0; j < 1000; ++j) 679 { 680 CAmount val = distribution(generator)*10000000; 681 add_coin(available_coins, *wallet, val); 682 balance += val; 683 } 684 685 // Generate a random fee rate in the range of 100 - 400 686 CFeeRate rate(rand.randrange(300) + 100); 687 688 // Generate a random target value between 1000 and wallet balance 689 CAmount target = rand.randrange(balance - 1000) + 1000; 690 691 // Perform selection 692 CoinSelectionParams cs_params{ 693 rand, 694 /*change_output_size=*/ 34, 695 /*change_spend_size=*/ 148, 696 /*min_change_target=*/ CENT, 697 /*effective_feerate=*/ CFeeRate(0), 698 /*long_term_feerate=*/ CFeeRate(0), 699 /*discard_feerate=*/ CFeeRate(0), 700 /*tx_noinputs_size=*/ 0, 701 /*avoid_partial=*/ false, 702 }; 703 cs_params.m_cost_of_change = 1; 704 cs_params.min_viable_change = 1; 705 CCoinControl cc; 706 const auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, target, cc, cs_params); 707 BOOST_CHECK(result); 708 BOOST_CHECK_GE(result->GetSelectedValue(), target); 709 } 710 } 711 712 BOOST_AUTO_TEST_CASE(waste_test) 713 { 714 const CAmount fee{100}; 715 const CAmount min_viable_change{300}; 716 const CAmount change_cost{125}; 717 const CAmount change_fee{30}; 718 const CAmount fee_diff{40}; 719 const CAmount in_amt{3 * COIN}; 720 const CAmount target{2 * COIN}; 721 const CAmount excess{80}; 722 const CAmount exact_target{in_amt - fee * 2}; // Maximum spendable amount after fees: no change, no excess 723 724 // In the following, we test that the waste is calculated correctly in various scenarios. 725 // Usually, RecalculateWaste would compute change_fee and change_cost on basis of the 726 // change output type, current feerate, and discard_feerate, but we use fixed values 727 // across this test to make the test easier to understand. 728 { 729 // Waste with change is the change cost and difference between fee and long term fee 730 SelectionResult selection1{target, SelectionAlgorithm::MANUAL}; 731 add_coin(1 * COIN, 1, selection1, /*fee=*/fee, /*long_term_fee=*/fee - fee_diff); 732 add_coin(2 * COIN, 2, selection1, fee, fee - fee_diff); 733 selection1.RecalculateWaste(min_viable_change, change_cost, change_fee); 734 BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, selection1.GetWaste()); 735 736 // Waste will be greater when fee is greater, but long term fee is the same 737 SelectionResult selection2{target, SelectionAlgorithm::MANUAL}; 738 add_coin(1 * COIN, 1, selection2, fee * 2, fee - fee_diff); 739 add_coin(2 * COIN, 2, selection2, fee * 2, fee - fee_diff); 740 selection2.RecalculateWaste(min_viable_change, change_cost, change_fee); 741 BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste()); 742 743 // Waste with change is the change cost and difference between fee and long term fee 744 // With long term fee greater than fee, waste should be less than when long term fee is less than fee 745 SelectionResult selection3{target, SelectionAlgorithm::MANUAL}; 746 add_coin(1 * COIN, 1, selection3, fee, fee + fee_diff); 747 add_coin(2 * COIN, 2, selection3, fee, fee + fee_diff); 748 selection3.RecalculateWaste(min_viable_change, change_cost, change_fee); 749 BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, selection3.GetWaste()); 750 BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste()); 751 } 752 753 { 754 // Waste without change is the excess and difference between fee and long term fee 755 SelectionResult selection_nochange1{exact_target - excess, SelectionAlgorithm::MANUAL}; 756 add_coin(1 * COIN, 1, selection_nochange1, fee, fee - fee_diff); 757 add_coin(2 * COIN, 2, selection_nochange1, fee, fee - fee_diff); 758 selection_nochange1.RecalculateWaste(min_viable_change, change_cost, change_fee); 759 BOOST_CHECK_EQUAL(fee_diff * 2 + excess, selection_nochange1.GetWaste()); 760 761 // Waste without change is the excess and difference between fee and long term fee 762 // With long term fee greater than fee, waste should be less than when long term fee is less than fee 763 SelectionResult selection_nochange2{exact_target - excess, SelectionAlgorithm::MANUAL}; 764 add_coin(1 * COIN, 1, selection_nochange2, fee, fee + fee_diff); 765 add_coin(2 * COIN, 2, selection_nochange2, fee, fee + fee_diff); 766 selection_nochange2.RecalculateWaste(min_viable_change, change_cost, change_fee); 767 BOOST_CHECK_EQUAL(fee_diff * -2 + excess, selection_nochange2.GetWaste()); 768 BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste()); 769 } 770 771 { 772 // Waste with change and fee == long term fee is just cost of change 773 SelectionResult selection{target, SelectionAlgorithm::MANUAL}; 774 add_coin(1 * COIN, 1, selection, fee, fee); 775 add_coin(2 * COIN, 2, selection, fee, fee); 776 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 777 BOOST_CHECK_EQUAL(change_cost, selection.GetWaste()); 778 } 779 780 { 781 // Waste without change and fee == long term fee is just the excess 782 SelectionResult selection{exact_target - excess, SelectionAlgorithm::MANUAL}; 783 add_coin(1 * COIN, 1, selection, fee, fee); 784 add_coin(2 * COIN, 2, selection, fee, fee); 785 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 786 BOOST_CHECK_EQUAL(excess, selection.GetWaste()); 787 } 788 789 { 790 // Waste is 0 when fee == long_term_fee, no change, and no excess 791 SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL}; 792 add_coin(1 * COIN, 1, selection, fee, fee); 793 add_coin(2 * COIN, 2, selection, fee, fee); 794 selection.RecalculateWaste(min_viable_change, change_cost , change_fee); 795 BOOST_CHECK_EQUAL(0, selection.GetWaste()); 796 } 797 798 { 799 // Waste is 0 when (fee - long_term_fee) == (-cost_of_change), and no excess 800 SelectionResult selection{target, SelectionAlgorithm::MANUAL}; 801 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); 802 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); 803 selection.RecalculateWaste(min_viable_change, /*change_cost=*/fee_diff * 2, change_fee); 804 BOOST_CHECK_EQUAL(0, selection.GetWaste()); 805 } 806 807 { 808 // Waste is 0 when (fee - long_term_fee) == (-excess), no change cost 809 const CAmount new_target{exact_target - /*excess=*/fee_diff * 2}; 810 SelectionResult selection{new_target, SelectionAlgorithm::MANUAL}; 811 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); 812 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); 813 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 814 BOOST_CHECK_EQUAL(0, selection.GetWaste()); 815 } 816 817 { 818 // Negative waste when the long term fee is greater than the current fee and the selected value == target 819 SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL}; 820 const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff)) 821 add_coin(1 * COIN, 1, selection, fee, fee + fee_diff); 822 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); 823 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 824 BOOST_CHECK_EQUAL(target_waste1, selection.GetWaste()); 825 } 826 827 { 828 // Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee)) 829 SelectionResult selection{target, SelectionAlgorithm::MANUAL}; 830 const CAmount large_fee_diff{90}; 831 const CAmount target_waste2{-2 * large_fee_diff + change_cost}; 832 // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost 833 // = (2 * 100) - (2 * (100 + 90)) + 125 834 // = 200 - 380 + 125 = -55 835 assert(target_waste2 == -55); 836 add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff); 837 add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff); 838 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 839 BOOST_CHECK_EQUAL(target_waste2, selection.GetWaste()); 840 } 841 } 842 843 844 BOOST_AUTO_TEST_CASE(bump_fee_test) 845 { 846 const CAmount fee{100}; 847 const CAmount min_viable_change{200}; 848 const CAmount change_cost{125}; 849 const CAmount change_fee{35}; 850 const CAmount fee_diff{40}; 851 const CAmount target{2 * COIN}; 852 853 { 854 SelectionResult selection{target, SelectionAlgorithm::MANUAL}; 855 add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff); 856 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); 857 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector(); 858 859 for (size_t i = 0; i < inputs.size(); ++i) { 860 inputs[i]->ApplyBumpFee(20*(i+1)); 861 } 862 863 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 864 CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60; 865 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); 866 867 selection.SetBumpFeeDiscount(30); 868 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 869 expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60 - /*group_discount=*/30; 870 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); 871 } 872 873 { 874 // Test with changeless transaction 875 // 876 // Bump fees and excess both contribute fully to the waste score, 877 // therefore, a bump fee group discount will not change the waste 878 // score as long as we do not create change in both instances. 879 CAmount changeless_target = 3 * COIN - 2 * fee - 100; 880 SelectionResult selection{changeless_target, SelectionAlgorithm::MANUAL}; 881 add_coin(1 * COIN, 1, selection, /*fee=*/fee, /*long_term_fee=*/fee + fee_diff); 882 add_coin(2 * COIN, 2, selection, fee, fee + fee_diff); 883 const std::vector<std::shared_ptr<COutput>> inputs = selection.GetShuffledInputVector(); 884 885 for (size_t i = 0; i < inputs.size(); ++i) { 886 inputs[i]->ApplyBumpFee(20*(i+1)); 887 } 888 889 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 890 CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40; 891 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); 892 893 selection.SetBumpFeeDiscount(30); 894 selection.RecalculateWaste(min_viable_change, change_cost, change_fee); 895 expected_waste = fee_diff * -2 + /*bump_fees=*/60 - /*group_discount=*/30 + /*excess = 100 - bump_fees + group_discount*/70; 896 BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste()); 897 } 898 } 899 900 BOOST_AUTO_TEST_CASE(effective_value_test) 901 { 902 const int input_bytes = 148; 903 const CFeeRate feerate(1000); 904 const CAmount nValue = 10000; 905 const int nInput = 0; 906 907 CMutableTransaction tx; 908 tx.vout.resize(1); 909 tx.vout[nInput].nValue = nValue; 910 911 // standard case, pass feerate in constructor 912 COutput output1(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, feerate); 913 const CAmount expected_ev1 = 9852; // 10000 - 148 914 BOOST_CHECK_EQUAL(output1.GetEffectiveValue(), expected_ev1); 915 916 // input bytes unknown (input_bytes = -1), pass feerate in constructor 917 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); 918 BOOST_CHECK_EQUAL(output2.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1 919 920 // negative effective value, pass feerate in constructor 921 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)); 922 const CAmount expected_ev3 = -4800; // 10000 - 14800 923 BOOST_CHECK_EQUAL(output3.GetEffectiveValue(), expected_ev3); 924 925 // standard case, pass fees in constructor 926 const CAmount fees = 148; 927 COutput output4(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, input_bytes, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fees); 928 BOOST_CHECK_EQUAL(output4.GetEffectiveValue(), expected_ev1); 929 930 // input bytes unknown (input_bytes = -1), pass fees in constructor 931 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); 932 BOOST_CHECK_EQUAL(output5.GetEffectiveValue(), nValue); // The effective value should be equal to the absolute value if input_bytes is -1 933 } 934 935 static util::Result<SelectionResult> CoinGrinder(const CAmount& target, 936 const CoinSelectionParams& cs_params, 937 const node::NodeContext& m_node, 938 int max_selection_weight, 939 std::function<CoinsResult(CWallet&)> coin_setup) 940 { 941 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 942 CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors 943 Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups; 944 return CoinGrinder(group.positive_group, target, cs_params.m_min_change_target, max_selection_weight); 945 } 946 947 BOOST_AUTO_TEST_CASE(coin_grinder_tests) 948 { 949 // Test Coin Grinder: 950 // 1) Insufficient funds, select all provided coins and fail. 951 // 2) Exceeded max weight, coin selection always surpasses the max allowed weight. 952 // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not) 953 // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO 954 // 5) Test finding a solution in a UTXO pool with mixed weights 955 // 6) Test that the lightest solution among many clones is found 956 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead 957 958 FastRandomContext rand; 959 CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag. 960 rand, 961 /*change_output_size=*/34, 962 /*change_spend_size=*/68, 963 /*min_change_target=*/CENT, 964 /*effective_feerate=*/CFeeRate(5000), 965 /*long_term_feerate=*/CFeeRate(2000), 966 /*discard_feerate=*/CFeeRate(1000), 967 /*tx_noinputs_size=*/10 + 34, // static header size + output size 968 /*avoid_partial=*/false, 969 }; 970 971 { 972 // ######################################################### 973 // 1) Insufficient funds, select all provided coins and fail 974 // ######################################################### 975 CAmount target = 49.5L * COIN; 976 int max_selection_weight = 10'000; // high enough to not fail for this reason. 977 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 978 CoinsResult available_coins; 979 for (int j = 0; j < 10; ++j) { 980 add_coin(available_coins, wallet, CAmount(1 * COIN)); 981 add_coin(available_coins, wallet, CAmount(2 * COIN)); 982 } 983 return available_coins; 984 }); 985 BOOST_CHECK(!res); 986 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds" 987 } 988 989 { 990 // ########################### 991 // 2) Test max weight exceeded 992 // ########################### 993 CAmount target = 29.5L * COIN; 994 int max_selection_weight = 3000; 995 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 996 CoinsResult available_coins; 997 for (int j = 0; j < 10; ++j) { 998 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true); 999 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true); 1000 } 1001 return available_coins; 1002 }); 1003 BOOST_CHECK(!res); 1004 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); 1005 } 1006 1007 { 1008 // ############################################################################################################### 1009 // 3) Test that the lowest-weight solution is found when some combinations would exceed the allowed weight 1010 // ################################################################################################################ 1011 CAmount target = 25.33L * COIN; 1012 int max_selection_weight = 10'000; // WU 1013 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1014 CoinsResult available_coins; 1015 for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU 1016 add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(5000), 144, false, 0, true); 1017 } 1018 for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU 1019 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true); 1020 } 1021 return available_coins; 1022 }); 1023 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1024 for (int i = 0; i < 10; ++i) { 1025 add_coin(2 * COIN, i, expected_result); 1026 } 1027 for (int j = 0; j < 17; ++j) { 1028 add_coin(0.33 * COIN, j + 10, expected_result); 1029 } 1030 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1031 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1032 size_t expected_attempts = 37; 1033 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1034 } 1035 1036 { 1037 // ################################################################################################################# 1038 // 4) Test that two less valuable UTXOs with a combined lower weight are preferred over a more valuable heavier UTXO 1039 // ################################################################################################################# 1040 CAmount target = 1.9L * COIN; 1041 int max_selection_weight = 400'000; // WU 1042 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1043 CoinsResult available_coins; 1044 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 148); 1045 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68); 1046 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 68); 1047 return available_coins; 1048 }); 1049 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1050 add_coin(1 * COIN, 1, expected_result); 1051 add_coin(1 * COIN, 2, expected_result); 1052 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1053 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1054 size_t expected_attempts = 3; 1055 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1056 } 1057 1058 { 1059 // ############################################################################################################### 1060 // 5) Test finding a solution in a UTXO pool with mixed weights 1061 // ################################################################################################################ 1062 CAmount target = 30L * COIN; 1063 int max_selection_weight = 400'000; // WU 1064 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1065 CoinsResult available_coins; 1066 for (int j = 0; j < 5; ++j) { 1067 // Add heavy coins {3, 6, 9, 12, 15} 1068 add_coin(available_coins, wallet, CAmount((3 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 350); 1069 // Add medium coins {2, 5, 8, 11, 14} 1070 add_coin(available_coins, wallet, CAmount((2 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 250); 1071 // Add light coins {1, 4, 7, 10, 13} 1072 add_coin(available_coins, wallet, CAmount((1 + 3 * j) * COIN), CFeeRate(5000), 144, false, 0, true, 150); 1073 } 1074 return available_coins; 1075 }); 1076 BOOST_CHECK(res); 1077 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1078 add_coin(14 * COIN, 1, expected_result); 1079 add_coin(13 * COIN, 2, expected_result); 1080 add_coin(4 * COIN, 3, expected_result); 1081 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1082 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1083 size_t expected_attempts = 92; 1084 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1085 } 1086 1087 { 1088 // ################################################################################################################# 1089 // 6) Test that the lightest solution among many clones is found 1090 // ################################################################################################################# 1091 CAmount target = 9.9L * COIN; 1092 int max_selection_weight = 400'000; // WU 1093 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1094 CoinsResult available_coins; 1095 // Expected Result: 4 + 3 + 2 + 1 = 10 BTC at 400 vB 1096 add_coin(available_coins, wallet, CAmount(4 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1097 add_coin(available_coins, wallet, CAmount(3 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1098 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1099 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 100); 1100 // Distracting clones: 1101 for (int j = 0; j < 100; ++j) { 1102 add_coin(available_coins, wallet, CAmount(8 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); 1103 } 1104 for (int j = 0; j < 100; ++j) { 1105 add_coin(available_coins, wallet, CAmount(7 * COIN), CFeeRate(5000), 144, false, 0, true, 800); 1106 } 1107 for (int j = 0; j < 100; ++j) { 1108 add_coin(available_coins, wallet, CAmount(6 * COIN), CFeeRate(5000), 144, false, 0, true, 600); 1109 } 1110 for (int j = 0; j < 100; ++j) { 1111 add_coin(available_coins, wallet, CAmount(5 * COIN), CFeeRate(5000), 144, false, 0, true, 400); 1112 } 1113 return available_coins; 1114 }); 1115 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1116 add_coin(4 * COIN, 0, expected_result); 1117 add_coin(3 * COIN, 0, expected_result); 1118 add_coin(2 * COIN, 0, expected_result); 1119 add_coin(1 * COIN, 0, expected_result); 1120 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1121 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1122 size_t expected_attempts = 38; 1123 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1124 } 1125 1126 { 1127 // ################################################################################################################# 1128 // 7) Test that lots of tiny UTXOs can be skipped if they are too heavy while there are enough funds in lookahead 1129 // ################################################################################################################# 1130 CAmount target = 1.9L * COIN; 1131 int max_selection_weight = 40000; // WU 1132 const auto& res = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1133 CoinsResult available_coins; 1134 add_coin(available_coins, wallet, CAmount(1.8 * COIN), CFeeRate(5000), 144, false, 0, true, 2500); 1135 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); 1136 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(5000), 144, false, 0, true, 1000); 1137 for (int j = 0; j < 100; ++j) { 1138 // make a 100 unique coins only differing by one sat 1139 add_coin(available_coins, wallet, CAmount(0.01 * COIN + j), CFeeRate(5000), 144, false, 0, true, 110); 1140 } 1141 return available_coins; 1142 }); 1143 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1144 add_coin(1 * COIN, 1, expected_result); 1145 add_coin(1 * COIN, 2, expected_result); 1146 BOOST_CHECK(EquivalentResult(expected_result, *res)); 1147 // Demonstrate how following improvements reduce iteration count and catch any regressions in the future. 1148 size_t expected_attempts = 7; 1149 BOOST_CHECK_MESSAGE(res->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, res->GetSelectionsEvaluated())); 1150 } 1151 1152 { 1153 // ################################################################################################################# 1154 // 8) Test input set that has a solution will not find a solution before reaching the attempt limit 1155 // ################################################################################################################# 1156 CAmount target = 8 * COIN; 1157 int max_selection_weight = 3200; // WU 1158 dummy_params.m_min_change_target = 0; 1159 const auto& result_a = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1160 CoinsResult doppelgangers; 1161 for (int i = 0; i < 18; ++i) { 1162 add_coin(doppelgangers, wallet, CAmount(1 * COIN + i), CFeeRate(0), 144, false, 0, true, 96 + i); 1163 } 1164 return doppelgangers; 1165 }); 1166 BOOST_CHECK(result_a); 1167 SelectionResult expected_result(CAmount(0), SelectionAlgorithm::CG); 1168 for (int i = 0; i < 8; ++i) { 1169 add_coin(1 * COIN + i, 0, expected_result); 1170 } 1171 BOOST_CHECK(EquivalentResult(expected_result, *result_a)); 1172 // Demonstrate a solution is found before the attempts limit is reached. 1173 size_t expected_attempts = 87'525; 1174 BOOST_CHECK_MESSAGE(result_a->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result_a->GetSelectionsEvaluated())); 1175 1176 // Adding one more doppelganger causes the attempt limit to be reached before finding a solution. 1177 const auto& result_b = CoinGrinder(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1178 CoinsResult doppelgangers; 1179 for (int i = 0; i < 19; ++i) { 1180 add_coin(doppelgangers, wallet, CAmount(1 * COIN + i), CFeeRate(0), 144, false, 0, true, 96 + i); 1181 } 1182 return doppelgangers; 1183 }); 1184 BOOST_CHECK(!result_b); 1185 } 1186 } 1187 1188 static util::Result<SelectionResult> SelectCoinsSRD(const CAmount& target, 1189 const CoinSelectionParams& cs_params, 1190 const node::NodeContext& m_node, 1191 int max_selection_weight, 1192 std::function<CoinsResult(CWallet&)> coin_setup) 1193 { 1194 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 1195 CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors 1196 Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups; 1197 return SelectCoinsSRD(group.positive_group, target, cs_params.m_change_fee, cs_params.rng_fast, max_selection_weight); 1198 } 1199 1200 BOOST_AUTO_TEST_CASE(srd_tests) 1201 { 1202 // Test SRD: 1203 // 1) Insufficient funds, select all provided coins and fail. 1204 // 2) Exceeded max weight, coin selection always surpasses the max allowed weight. 1205 // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not) 1206 1207 FastRandomContext rand; 1208 CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag. 1209 rand, 1210 /*change_output_size=*/34, 1211 /*change_spend_size=*/68, 1212 /*min_change_target=*/CENT, 1213 /*effective_feerate=*/CFeeRate(0), 1214 /*long_term_feerate=*/CFeeRate(0), 1215 /*discard_feerate=*/CFeeRate(0), 1216 /*tx_noinputs_size=*/10 + 34, // static header size + output size 1217 /*avoid_partial=*/false, 1218 }; 1219 1220 { 1221 // ######################################################### 1222 // 1) Insufficient funds, select all provided coins and fail 1223 // ######################################################### 1224 CAmount target = 49.5L * COIN; 1225 int max_selection_weight = 10000; // high enough to not fail for this reason. 1226 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1227 CoinsResult available_coins; 1228 for (int j = 0; j < 10; ++j) { 1229 add_coin(available_coins, wallet, CAmount(1 * COIN)); 1230 add_coin(available_coins, wallet, CAmount(2 * COIN)); 1231 } 1232 return available_coins; 1233 }); 1234 BOOST_CHECK(!res); 1235 BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds" 1236 } 1237 1238 { 1239 // ########################### 1240 // 2) Test max weight exceeded 1241 // ########################### 1242 CAmount target = 49.5L * COIN; 1243 int max_selection_weight = 3000; 1244 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1245 CoinsResult available_coins; 1246 for (int j = 0; j < 10; ++j) { 1247 /* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */ 1248 add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(0), 144, false, 0, true); 1249 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true); 1250 } 1251 return available_coins; 1252 }); 1253 BOOST_CHECK(!res); 1254 BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); 1255 } 1256 1257 { 1258 // ################################################################################################################ 1259 // 3) Test that SRD result does not exceed the max weight 1260 // ################################################################################################################ 1261 CAmount target = 25.33L * COIN; 1262 int max_selection_weight = 10000; // WU 1263 const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { 1264 CoinsResult available_coins; 1265 for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU 1266 add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(0), 144, false, 0, true); 1267 } 1268 for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU 1269 add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true); 1270 } 1271 return available_coins; 1272 }); 1273 BOOST_CHECK(res); 1274 BOOST_CHECK(res->GetWeight() <= max_selection_weight); 1275 } 1276 } 1277 1278 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) 1279 { 1280 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 1281 auto available_coins = coin_setup(*wallet); 1282 1283 LOCK(wallet->cs_wallet); 1284 auto result = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/ {}, target, cc, cs_params); 1285 if (result) { 1286 const auto signedTxSize = 10 + 34 + 68 * result->GetInputSet().size(); // static header size + output size + inputs size (P2WPKH) 1287 BOOST_CHECK_LE(signedTxSize * WITNESS_SCALE_FACTOR, MAX_STANDARD_TX_WEIGHT); 1288 1289 BOOST_CHECK_GE(result->GetSelectedValue(), target); 1290 } 1291 return result; 1292 } 1293 1294 static bool has_coin(const OutputSet& set, CAmount amount) 1295 { 1296 return std::any_of(set.begin(), set.end(), [&](const auto& coin) { return coin->GetEffectiveValue() == amount; }); 1297 } 1298 1299 BOOST_AUTO_TEST_CASE(check_max_selection_weight) 1300 { 1301 const CAmount target = 49.5L * COIN; 1302 CCoinControl cc; 1303 1304 FastRandomContext rand; 1305 CoinSelectionParams cs_params{ 1306 rand, 1307 /*change_output_size=*/34, 1308 /*change_spend_size=*/68, 1309 /*min_change_target=*/CENT, 1310 /*effective_feerate=*/CFeeRate(0), 1311 /*long_term_feerate=*/CFeeRate(0), 1312 /*discard_feerate=*/CFeeRate(0), 1313 /*tx_noinputs_size=*/10 + 34, // static header size + output size 1314 /*avoid_partial=*/false, 1315 }; 1316 1317 int max_weight = MAX_STANDARD_TX_WEIGHT - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size); 1318 { 1319 // Scenario 1: 1320 // The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs 1321 // Then tries to spend 49.5 BTC 1322 // The 50.0 BTC output should be selected, because the transaction would otherwise be too large 1323 1324 // Perform selection 1325 1326 const auto result = select_coins( 1327 target, cs_params, cc, [&](CWallet& wallet) { 1328 CoinsResult available_coins; 1329 for (int j = 0; j < 1515; ++j) { 1330 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true); 1331 } 1332 1333 add_coin(available_coins, wallet, CAmount(50 * COIN), CFeeRate(0), 144, false, 0, true); 1334 return available_coins; 1335 }, 1336 m_node); 1337 1338 BOOST_CHECK(result); 1339 // Verify that the 50 BTC UTXO was selected, and result is below max_weight 1340 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(50 * COIN))); 1341 BOOST_CHECK_LE(result->GetWeight(), max_weight); 1342 } 1343 1344 { 1345 // Scenario 2: 1346 1347 // The actor starts with 400x 0.0625 BTC and 2000x 0.025 BTC (75.0 BTC total) unspent outputs 1348 // Then tries to spend 49.5 BTC 1349 // A combination of coins should be selected, such that the created transaction is not too large 1350 1351 // Perform selection 1352 const auto result = select_coins( 1353 target, cs_params, cc, [&](CWallet& wallet) { 1354 CoinsResult available_coins; 1355 for (int j = 0; j < 400; ++j) { 1356 add_coin(available_coins, wallet, CAmount(0.0625 * COIN), CFeeRate(0), 144, false, 0, true); 1357 } 1358 for (int j = 0; j < 2000; ++j) { 1359 add_coin(available_coins, wallet, CAmount(0.025 * COIN), CFeeRate(0), 144, false, 0, true); 1360 } 1361 return available_coins; 1362 }, 1363 m_node); 1364 1365 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.0625 * COIN))); 1366 BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.025 * COIN))); 1367 BOOST_CHECK_LE(result->GetWeight(), max_weight); 1368 } 1369 1370 { 1371 // Scenario 3: 1372 1373 // The actor starts with 1515x 0.033 BTC (49.995 BTC total) unspent outputs 1374 // No results should be returned, because the transaction would be too large 1375 1376 // Perform selection 1377 const auto result = select_coins( 1378 target, cs_params, cc, [&](CWallet& wallet) { 1379 CoinsResult available_coins; 1380 for (int j = 0; j < 1515; ++j) { 1381 add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true); 1382 } 1383 return available_coins; 1384 }, 1385 m_node); 1386 1387 // No results 1388 // 1515 inputs * 68 bytes = 103,020 bytes 1389 // 103,020 bytes * 4 = 412,080 weight, which is above the MAX_STANDARD_TX_WEIGHT of 400,000 1390 BOOST_CHECK(!result); 1391 } 1392 } 1393 1394 BOOST_AUTO_TEST_CASE(SelectCoins_effective_value_test) 1395 { 1396 // Test that the effective value is used to check whether preset inputs provide sufficient funds when subtract_fee_outputs is not used. 1397 // This test creates a coin whose value is higher than the target but whose effective value is lower than the target. 1398 // The coin is selected using coin control, with m_allow_other_inputs = false. SelectCoins should fail due to insufficient funds. 1399 1400 std::unique_ptr<CWallet> wallet = NewWallet(m_node); 1401 1402 CoinsResult available_coins; 1403 { 1404 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy"); 1405 add_coin(available_coins, *dummyWallet, 100000); // 0.001 BTC 1406 } 1407 1408 CAmount target{99900}; // 0.000999 BTC 1409 1410 FastRandomContext rand; 1411 CoinSelectionParams cs_params{ 1412 rand, 1413 /*change_output_size=*/34, 1414 /*change_spend_size=*/148, 1415 /*min_change_target=*/1000, 1416 /*effective_feerate=*/CFeeRate(3000), 1417 /*long_term_feerate=*/CFeeRate(1000), 1418 /*discard_feerate=*/CFeeRate(1000), 1419 /*tx_noinputs_size=*/0, 1420 /*avoid_partial=*/false, 1421 }; 1422 CCoinControl cc; 1423 cc.m_allow_other_inputs = false; 1424 COutput output = available_coins.All().at(0); 1425 cc.SetInputWeight(output.outpoint, 148); 1426 cc.Select(output.outpoint).SetTxOut(output.txout); 1427 1428 LOCK(wallet->cs_wallet); 1429 const auto preset_inputs = *Assert(FetchSelectedInputs(*wallet, cc, cs_params)); 1430 available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint}); 1431 1432 const auto result = SelectCoins(*wallet, available_coins, preset_inputs, target, cc, cs_params); 1433 BOOST_CHECK(!result); 1434 } 1435 1436 BOOST_FIXTURE_TEST_CASE(wallet_coinsresult_test, BasicTestingSetup) 1437 { 1438 // Test case to verify CoinsResult object sanity. 1439 CoinsResult available_coins; 1440 { 1441 std::unique_ptr<CWallet> dummyWallet = NewWallet(m_node, /*wallet_name=*/"dummy"); 1442 1443 // Add some coins to 'available_coins' 1444 for (int i=0; i<10; i++) { 1445 add_coin(available_coins, *dummyWallet, 1 * COIN); 1446 } 1447 } 1448 1449 { 1450 // First test case, check that 'CoinsResult::Erase' function works as expected. 1451 // By trying to erase two elements from the 'available_coins' object. 1452 std::unordered_set<COutPoint, SaltedOutpointHasher> outs_to_remove; 1453 const auto& coins = available_coins.All(); 1454 for (int i = 0; i < 2; i++) { 1455 outs_to_remove.emplace(coins[i].outpoint); 1456 } 1457 available_coins.Erase(outs_to_remove); 1458 1459 // Check that the elements were actually removed. 1460 const auto& updated_coins = available_coins.All(); 1461 for (const auto& out: outs_to_remove) { 1462 auto it = std::find_if(updated_coins.begin(), updated_coins.end(), [&out](const COutput &coin) { 1463 return coin.outpoint == out; 1464 }); 1465 BOOST_CHECK(it == updated_coins.end()); 1466 } 1467 // And verify that no extra element were removed 1468 BOOST_CHECK_EQUAL(available_coins.Size(), 8); 1469 } 1470 } 1471 1472 BOOST_AUTO_TEST_SUITE_END() 1473 } // namespace wallet