diff --git a/src/wallet/test/CMakeLists.txt b/src/wallet/test/CMakeLists.txt index f14a78ca1d2..8564eface55 100644 --- a/src/wallet/test/CMakeLists.txt +++ b/src/wallet/test/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(test_bitcoin wallet_test_fixture.cpp db_tests.cpp coinselector_tests.cpp + coinselection_tests.cpp feebumper_tests.cpp group_outputs_tests.cpp init_tests.cpp diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp new file mode 100644 index 00000000000..4e4b4b67aa1 --- /dev/null +++ b/src/wallet/test/coinselection_tests.cpp @@ -0,0 +1,219 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include + +#include + +namespace wallet { +BOOST_FIXTURE_TEST_SUITE(coinselection_tests, TestingSetup) + +static int next_lock_time = 0; +static FastRandomContext default_rand; + +/** Default coin selection parameters (dcsp) allow us to only explicitly set + * parameters when a diverging value is relevant in the context of a test. + * We use P2WPKH input and output weights for the change weights. */ +static CoinSelectionParams init_default_params() +{ + CoinSelectionParams dcsp{ + /*rng_fast*/default_rand, + /*change_output_size=*/31, + /*change_spend_size=*/68, + /*min_change_target=*/50'000, + /*effective_feerate=*/CFeeRate(5000), + /*long_term_feerate=*/CFeeRate(10'000), + /*discard_feerate=*/CFeeRate(3000), + /*tx_noinputs_size=*/11 + 31, //static header size + output size + /*avoid_partial=*/false, + }; + dcsp.m_change_fee = /*155 sats=*/dcsp.m_effective_feerate.GetFee(dcsp.change_output_size); + dcsp.min_viable_change = /*204 sats=*/dcsp.m_discard_feerate.GetFee(dcsp.change_spend_size); + dcsp.m_cost_of_change = /*204 + 155 sats=*/dcsp.min_viable_change + dcsp.m_change_fee; + dcsp.m_subtract_fee_outputs = false; + return dcsp; +} + +static const CoinSelectionParams default_cs_params = init_default_params(); + +/** Make one OutputGroup with a single UTXO that either has a given effective value (default) or a given amount (`is_eff_value = false`). */ +static OutputGroup MakeCoin(const CAmount& amount, bool is_eff_value = true, CoinSelectionParams cs_params = default_cs_params, int custom_spending_vsize = 68) +{ + // Always assume that we only have one input + CMutableTransaction tx; + tx.vout.resize(1); + CAmount fees = cs_params.m_effective_feerate.GetFee(custom_spending_vsize); + tx.vout[0].nValue = amount + int(is_eff_value) * fees; + tx.nLockTime = next_lock_time++; // so all transactions get different hashes + OutputGroup group(cs_params); + group.Insert(std::make_shared(COutPoint(tx.GetHash(), 0), tx.vout.at(0), /*depth=*/1, /*input_bytes=*/custom_spending_vsize, /*spendable=*/true, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/fees), /*ancestors=*/0, /*descendants=*/0); + return group; +} + +/** Make multiple OutputGroups with the given values as their effective value */ +static void AddCoins(std::vector& utxo_pool, std::vector coins, CoinSelectionParams cs_params = default_cs_params) +{ + for (CAmount c : coins) { + utxo_pool.push_back(MakeCoin(c, true, cs_params)); + } +} + +/** Make multiple coins that share the same effective value */ +static void AddDuplicateCoins(std::vector& utxo_pool, int count, int amount, CoinSelectionParams cs_params = default_cs_params) { + for (int i = 0 ; i < count; ++i) { + utxo_pool.push_back(MakeCoin(amount, true, cs_params)); + } +} + +/** Check if SelectionResult a is equivalent to SelectionResult b. + * Two results are equivalent if they are composed of the same input values, even if they have different inputs (i.e., same value, different prevout) */ +static bool HaveEquivalentValues(const SelectionResult& a, const SelectionResult& b) +{ + std::vector a_amts; + std::vector b_amts; + for (const auto& coin : a.GetInputSet()) { + a_amts.push_back(coin->txout.nValue); + } + for (const auto& coin : b.GetInputSet()) { + b_amts.push_back(coin->txout.nValue); + } + std::sort(a_amts.begin(), a_amts.end()); + std::sort(b_amts.begin(), b_amts.end()); + + auto ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin()); + return ret.first == a_amts.end() && ret.second == b_amts.end(); +} + +static std::string InputAmountsToString(const SelectionResult& selection) +{ + return "[" + util::Join(selection.GetInputSet(), " ", [](const auto& input){ return util::ToString(input->txout.nValue);}) + "]"; +} + +static void TestBnBSuccess(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const std::vector& expected_input_amounts, const CoinSelectionParams& cs_params = default_cs_params, int custom_spending_vsize = 68) +{ + SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB); + CAmount expected_amount = 0; + for (CAmount input_amount : expected_input_amounts) { + OutputGroup group = MakeCoin(input_amount, true, cs_params, custom_spending_vsize); + expected_amount += group.m_value; + expected_result.AddInput(group); + } + + const auto result = SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/default_cs_params.m_cost_of_change, /*max_selection_weight=*/MAX_STANDARD_TX_WEIGHT); + BOOST_CHECK_MESSAGE(result, "Falsy result in BnB-Success: " + test_title); + BOOST_CHECK_MESSAGE(HaveEquivalentValues(expected_result, *result), strprintf("Result mismatch in BnB-Success: %s. Expected %s, but got %s", test_title, InputAmountsToString(expected_result), InputAmountsToString(*result))); + BOOST_CHECK_MESSAGE(result->GetSelectedValue() == expected_amount, strprintf("Selected amount mismatch in BnB-Success: %s. Expected %d, but got %d", test_title, expected_amount, result->GetSelectedValue())); +} + +static void TestBnBFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target) +{ + BOOST_CHECK_MESSAGE(!SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/default_cs_params.m_cost_of_change, /*max_selection_weight=*/MAX_STANDARD_TX_WEIGHT), "BnB-Fail: " + test_title); +} + +BOOST_AUTO_TEST_CASE(bnb_test) +{ + std::vector feerates = {0, 1, 5'000, 10'000, 25'000, 59'764, 500'000, 999'000, 1'500'000}; + + for (int feerate : feerates) { + std::vector utxo_pool; + + CoinSelectionParams cs_params = init_default_params(); + cs_params.m_effective_feerate = CFeeRate{feerate}; + + // Fail for empty UTXO pool + TestBnBFail("Empty UTXO pool", utxo_pool, /*selection_target=*/1 * CENT); + + AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT}, cs_params); + + // Simple success cases + TestBnBSuccess("Select smallest UTXO", utxo_pool, /*selection_target=*/1 * CENT, /*expected_input_amounts=*/{1 * CENT}, cs_params); + TestBnBSuccess("Select middle UTXO", utxo_pool, /*selection_target=*/3 * CENT, /*expected_input_amounts=*/{3 * CENT}, cs_params); + TestBnBSuccess("Select biggest UTXO", utxo_pool, /*selection_target=*/5 * CENT, /*expected_input_amounts=*/{5 * CENT}, cs_params); + TestBnBSuccess("Select two UTXOs", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, cs_params); + TestBnBSuccess("Select all UTXOs", utxo_pool, /*selection_target=*/9 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT, 5 * CENT}, cs_params); + + // BnB finds changeless solution while overshooting by up to cost_of_change + TestBnBSuccess("Select upper bound", utxo_pool, /*selection_target=*/4 * CENT - default_cs_params.m_cost_of_change, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, cs_params); + + // BnB fails to find changeless solution when overshooting by cost_of_change + 1 sat + TestBnBFail("Overshoot upper bound", utxo_pool, /*selection_target=*/4 * CENT - default_cs_params.m_cost_of_change - 1); + + // Simple cases without BnB solution + TestBnBFail("Smallest combination too big", utxo_pool, /*selection_target=*/0.5 * CENT); + TestBnBFail("No UTXO combination in target window", utxo_pool, /*selection_target=*/7 * CENT); + TestBnBFail("Select more than available", utxo_pool, /*selection_target=*/10 * CENT); + + // Test skipping of equivalent input sets + std::vector clone_pool; + AddCoins(clone_pool, {2 * CENT, 7 * CENT, 7 * CENT}, cs_params); + AddDuplicateCoins(clone_pool, 50'000, 5 * CENT, cs_params); + TestBnBSuccess("Skip equivalent input sets", clone_pool, /*selection_target=*/16 * CENT, /*expected_input_amounts=*/{2 * CENT, 7 * CENT, 7 * CENT}, cs_params); + + /* Test BnB attempt limit (`TOTAL_TRIES`) + * + * Generally, on a diverse UTXO pool BnB will quickly pass over UTXOs bigger than the target and then start + * combining small counts of UTXOs that in sum remain under the selection_target+cost_of_change. When there are + * multiple UTXOs that have matching amount and cost, combinations with equivalent input sets are skipped. The + * UTXO pool for this test is specifically crafted to create as much branching as possible. The selection target + * is 8 CENT while all UTXOs are slightly bigger than 1 CENT. The smallest eight are 100,000…100,007 sats, while + * the larger nine are 100,368…100,375 (i.e., 100,008…100,016 sats plus cost_of_change (359 sats)). + * + * Because BnB will only select input sets that fall between selection_target and selection_target + + * cost_of_change, and the search traverses the UTXO pool from large to small amounts, the search will visit + * every single combination of eight inputs. All except the last combination will overshoot by more than + * cost_of_change on the eighth input, because the larger nine inputs each exceed 1 CENT by more than + * cost_of_change. Only the last combination consisting of the eight smallest UTXOs falls into the target + * window. + */ + std::vector doppelganger_pool; + std::vector doppelgangers; + std::vector expected_inputs; + for (int i = 0; i < 17; ++i) { + if (i < 8) { + // The eight smallest UTXOs can be combined to create expected_result + doppelgangers.push_back(1 * CENT + i); + expected_inputs.push_back(doppelgangers[i]); + } else { + // Any eight UTXOs including at least one UTXO with the added cost_of_change will exceed target window + doppelgangers.push_back(1 * CENT + default_cs_params.m_cost_of_change + i); + } + } + AddCoins(doppelganger_pool, doppelgangers, cs_params); + // Among up to 17 unique UTXOs of similar effective value we will find a solution composed of the eight smallest UTXOs + TestBnBSuccess("Combine smallest 8 of 17 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT, /*expected_input_amounts=*/expected_inputs, cs_params); + + // Starting with 18 unique UTXOs of similar effective value we will not find the solution due to exceeding the attempt limit + AddCoins(doppelganger_pool, {1 * CENT + default_cs_params.m_cost_of_change + 17}, cs_params); + TestBnBFail("Exhaust looking for smallest 8 of 18 unique UTXOs", doppelganger_pool, /*selection_target=*/8 * CENT); + } +} + +BOOST_AUTO_TEST_CASE(bnb_feerate_sensitivity_test) +{ + // Create sets of UTXOs with the same effective amounts at different feerates (but different absolute amounts) + std::vector low_feerate_pool; // 5 sat/vB (default, and lower than long_term_feerate of 10 sat/vB) + AddCoins(low_feerate_pool, {2 * CENT, 3 * CENT, 5 * CENT, 10 * CENT}); + TestBnBSuccess("Select many inputs at low feerates", low_feerate_pool, /*selection_target=*/10 * CENT, /*expected_input_amounts=*/{2 * CENT, 3 * CENT, 5 * CENT}); + + CoinSelectionParams high_feerate_params = init_default_params(); + high_feerate_params.m_effective_feerate = CFeeRate{25'000}; + std::vector high_feerate_pool; // 25 sat/vB (greater than long_term_feerate of 10 sat/vB) + AddCoins(high_feerate_pool, {2 * CENT, 3 * CENT, 5 * CENT, 10 * CENT}, high_feerate_params); + TestBnBSuccess("Select one input at high feerates", high_feerate_pool, /*selection_target=*/10 * CENT, /*expected_input_amounts=*/{10 * CENT}, high_feerate_params); + + // Add heavy inputs {6, 7} to existing {2, 3, 5, 10} + low_feerate_pool.push_back(MakeCoin(6 * CENT, true, default_cs_params, /*custom_spending_vsize=*/500)); + low_feerate_pool.push_back(MakeCoin(7 * CENT, true, default_cs_params, /*custom_spending_vsize=*/500)); + TestBnBSuccess("Prefer two heavy inputs over two light inputs at low feerates", low_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{6 * CENT, 7 * CENT}, default_cs_params, /*custom_spending_vsize=*/500); + + high_feerate_pool.push_back(MakeCoin(6 * CENT, true, high_feerate_params, /*custom_spending_vsize=*/500)); + high_feerate_pool.push_back(MakeCoin(7 * CENT, true, high_feerate_params, /*custom_spending_vsize=*/500)); + TestBnBSuccess("Prefer two light inputs over two heavy inputs at high feerates", high_feerate_pool, /*selection_target=*/13 * CENT, /*expected_input_amounts=*/{3 * CENT, 10 * CENT}, high_feerate_params); +} + +BOOST_AUTO_TEST_SUITE_END() +} // namespace wallet diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index fbe48a9cdfb..fd35d147b93 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -37,15 +37,6 @@ static const CoinEligibilityFilter filter_confirmed(1, 1, 0); static const CoinEligibilityFilter filter_standard_extra(6, 6, 0); static int nextLockTime = 0; -static void add_coin(const CAmount& nValue, int nInput, std::vector& set) -{ - CMutableTransaction tx; - tx.vout.resize(nInput + 1); - tx.vout[nInput].nValue = nValue; - tx.nLockTime = nextLockTime++; // so all transactions get different hashes - set.emplace_back(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ -1, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, /*fees=*/ 0); -} - static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result) { CMutableTransaction tx; @@ -133,18 +124,6 @@ static bool EqualResult(const SelectionResult& a, const SelectionResult& b) return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end(); } -static CAmount make_hard_case(int utxos, std::vector& utxo_pool) -{ - utxo_pool.clear(); - CAmount target = 0; - for (int i = 0; i < utxos; ++i) { - target += CAmount{1} << (utxos+i); - add_coin(CAmount{1} << (utxos+i), 2*i, utxo_pool); - add_coin((CAmount{1} << (utxos+i)) + (CAmount{1} << (utxos-1-i)), 2*i + 1, utxo_pool); - } - return target; -} - inline std::vector& GroupCoins(const std::vector& available_coins, bool subtract_fee_outputs = false) { static std::vector static_groups; @@ -195,115 +174,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) std::vector utxo_pool; SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB); - ///////////////////////// - // Known Outcome tests // - ///////////////////////// - - // Empty utxo pool - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT)); - - // Add utxos - add_coin(1 * CENT, 1, utxo_pool); - add_coin(2 * CENT, 2, utxo_pool); - add_coin(3 * CENT, 3, utxo_pool); - add_coin(4 * CENT, 4, utxo_pool); - - // Select 1 Cent - add_coin(1 * CENT, 1, expected_result); - const auto result1 = SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT); - BOOST_CHECK(result1); - BOOST_CHECK(EquivalentResult(expected_result, *result1)); - BOOST_CHECK_EQUAL(result1->GetSelectedValue(), 1 * CENT); - expected_result.Clear(); - - // Select 2 Cent - add_coin(2 * CENT, 2, expected_result); - const auto result2 = SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT); - BOOST_CHECK(result2); - BOOST_CHECK(EquivalentResult(expected_result, *result2)); - BOOST_CHECK_EQUAL(result2->GetSelectedValue(), 2 * CENT); - expected_result.Clear(); - - // Select 5 Cent - add_coin(3 * CENT, 3, expected_result); - add_coin(2 * CENT, 2, expected_result); - const auto result3 = SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT); - BOOST_CHECK(result3); - BOOST_CHECK(EquivalentResult(expected_result, *result3)); - BOOST_CHECK_EQUAL(result3->GetSelectedValue(), 5 * CENT); - expected_result.Clear(); - - // Select 11 Cent, not possible - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT)); - expected_result.Clear(); - - // Cost of change is greater than the difference between target value and utxo sum - add_coin(1 * CENT, 1, expected_result); - const auto result4 = SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT); - BOOST_CHECK(result4); - BOOST_CHECK_EQUAL(result4->GetSelectedValue(), 1 * CENT); - BOOST_CHECK(EquivalentResult(expected_result, *result4)); - expected_result.Clear(); - - // Cost of change is less than the difference between target value and utxo sum - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0)); - expected_result.Clear(); - - // Select 10 Cent - add_coin(5 * CENT, 5, utxo_pool); - add_coin(4 * CENT, 4, expected_result); - add_coin(3 * CENT, 3, expected_result); - add_coin(2 * CENT, 2, expected_result); - add_coin(1 * CENT, 1, expected_result); - const auto result5 = SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT); - BOOST_CHECK(result5); - BOOST_CHECK(EquivalentResult(expected_result, *result5)); - BOOST_CHECK_EQUAL(result5->GetSelectedValue(), 10 * CENT); - expected_result.Clear(); - - // Select 0.25 Cent, not possible - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT)); - expected_result.Clear(); - - // Iteration exhaustion test - CAmount target = make_hard_case(17, utxo_pool); - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 1)); // Should exhaust - target = make_hard_case(14, utxo_pool); - const auto result7 = SelectCoinsBnB(GroupCoins(utxo_pool), target, 1); // Should not exhaust - BOOST_CHECK(result7); - - // Test same value early bailout optimization - utxo_pool.clear(); - add_coin(7 * CENT, 7, expected_result); - add_coin(7 * CENT, 7, expected_result); - add_coin(7 * CENT, 7, expected_result); - add_coin(7 * CENT, 7, expected_result); - add_coin(2 * CENT, 7, expected_result); - add_coin(7 * CENT, 7, utxo_pool); - add_coin(7 * CENT, 7, utxo_pool); - add_coin(7 * CENT, 7, utxo_pool); - add_coin(7 * CENT, 7, utxo_pool); - add_coin(2 * CENT, 7, utxo_pool); - for (int i = 0; i < 50000; ++i) { - add_coin(5 * CENT, 7, utxo_pool); - } - const auto result8 = SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000); - BOOST_CHECK(result8); - BOOST_CHECK_EQUAL(result8->GetSelectedValue(), 30 * CENT); - BOOST_CHECK(EquivalentResult(expected_result, *result8)); - //////////////////// // Behavior tests // //////////////////// - // Select 1 Cent with pool of only greater than 5 Cent - utxo_pool.clear(); - for (int i = 5; i <= 20; ++i) { - add_coin(i * CENT, i, utxo_pool); - } - // Run 100 times, to make sure it is never finding a solution - for (int i = 0; i < 100; ++i) { - BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT)); - } // Make sure that effective value is working in AttemptSelection when BnB is used CoinSelectionParams coin_selection_params_bnb{ @@ -366,7 +239,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) CoinsResult available_coins; - // single coin should be selected when effective fee > long term fee + // pre selected coin should be selected even if disadvantageous coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000); coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000); @@ -377,42 +250,9 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); expected_result.Clear(); - add_coin(10 * CENT + input_fee, 2, expected_result); + add_coin(9 * CENT + input_fee, 2, expected_result); + add_coin(1 * CENT + input_fee, 2, expected_result); CCoinControl coin_control; - const auto result11 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb); - BOOST_CHECK(EquivalentResult(expected_result, *result11)); - available_coins.Clear(); - - // more coins should be selected when effective fee < long term fee - coin_selection_params_bnb.m_effective_feerate = CFeeRate(3000); - coin_selection_params_bnb.m_long_term_feerate = CFeeRate(5000); - - // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount - input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type) - add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); - add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); - add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); - - expected_result.Clear(); - add_coin(9 * CENT + input_fee, 2, expected_result); - add_coin(1 * CENT + input_fee, 2, expected_result); - const auto result12 = SelectCoins(*wallet, available_coins, /*pre_set_inputs=*/{}, 10 * CENT, coin_control, coin_selection_params_bnb); - BOOST_CHECK(EquivalentResult(expected_result, *result12)); - available_coins.Clear(); - - // pre selected coin should be selected even if disadvantageous - coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000); - coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000); - - // Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount - input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*num_bytes=*/68); // bech32 input size (default test output type) - add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); - add_coin(available_coins, *wallet, 9 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); - add_coin(available_coins, *wallet, 1 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true); - - expected_result.Clear(); - add_coin(9 * CENT + input_fee, 2, expected_result); - add_coin(1 * CENT + input_fee, 2, expected_result); coin_control.m_allow_other_inputs = true; COutput select_coin = available_coins.All().at(1); // pre select 9 coin coin_control.Select(select_coin.outpoint);