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..f211e507528 --- /dev/null +++ b/src/wallet/test/coinselection_tests.cpp @@ -0,0 +1,122 @@ +// 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)); + } +} + +/** 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) +{ + 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); + 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())); +} + +BOOST_AUTO_TEST_CASE(bnb_test) +{ + std::vector utxo_pool; + AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT}); + + // Simple success cases + TestBnBSuccess("Select smallest UTXO", utxo_pool, /*selection_target=*/1 * CENT, /*expected_input_amounts=*/{1 * CENT}); + TestBnBSuccess("Select middle UTXO", utxo_pool, /*selection_target=*/3 * CENT, /*expected_input_amounts=*/{3 * CENT}); + TestBnBSuccess("Select biggest UTXO", utxo_pool, /*selection_target=*/5 * CENT, /*expected_input_amounts=*/{5 * CENT}); + TestBnBSuccess("Select two UTXOs", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}); + TestBnBSuccess("Select all UTXOs", utxo_pool, /*selection_target=*/9 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT, 5 * CENT}); + + // 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}); +} + +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..ad872de192a 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -208,59 +208,14 @@ BOOST_AUTO_TEST_CASE(bnb_search_test) 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();