From 65900f8dc6e7f3a429a796faff06b96bb8e59490 Mon Sep 17 00:00:00 2001 From: Murch Date: Fri, 20 Mar 2026 14:43:30 -0700 Subject: [PATCH 1/5] test: Init coin selection params with feerate The most frequent change to CoinSelectionParams so far seems to be amendments to the feerate, and the feerate propagates to other members of the CoinSelectionParams, so we add it as an input parameter. This also adds the CoinSelectionParams to the BnBFail tests, which previously were always run with the default CoinSelectionParams. --- src/wallet/test/coinselection_tests.cpp | 58 ++++++++++++------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp index 4a0eed8f090..46b0b463c70 100644 --- a/src/wallet/test/coinselection_tests.cpp +++ b/src/wallet/test/coinselection_tests.cpp @@ -18,30 +18,31 @@ static FastRandomContext default_rand; static const int P2WPKH_INPUT_VSIZE = 68; static const int P2WPKH_OUTPUT_VSIZE = 31; -/** 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() +/** Default coin selection parameters allow us to only explicitly set + * parameters when a diverging value is relevant in the context of a test, + * without reiterating the defaults in every test. We use P2WPKH input and + * output weights for the change weights. */ +static CoinSelectionParams init_cs_params(int eff_feerate = 5000) { - CoinSelectionParams dcsp{ + CoinSelectionParams csp{ /*rng_fast=*/default_rand, /*change_output_size=*/P2WPKH_OUTPUT_VSIZE, /*change_spend_size=*/P2WPKH_INPUT_VSIZE, /*min_change_target=*/50'000, - /*effective_feerate=*/CFeeRate(5000), + /*effective_feerate=*/CFeeRate(eff_feerate), /*long_term_feerate=*/CFeeRate(10'000), /*discard_feerate=*/CFeeRate(3000), /*tx_noinputs_size=*/11 + P2WPKH_OUTPUT_VSIZE, //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; + csp.m_change_fee = csp.m_effective_feerate.GetFee(csp.change_output_size); // 155 sats for default feerate of 5000 s/kvB + csp.min_viable_change = /*204 sats=*/csp.m_discard_feerate.GetFee(csp.change_spend_size); + csp.m_cost_of_change = csp.min_viable_change + csp.m_change_fee; // 204 + 155 sats for default feerate of 5000 s/kvB + csp.m_subtract_fee_outputs = false; + return csp; } -static const CoinSelectionParams default_cs_params = init_default_params(); +static const CoinSelectionParams default_cs_params = init_cs_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 = P2WPKH_INPUT_VSIZE) @@ -106,15 +107,15 @@ static void TestBnBSuccess(std::string test_title, std::vector& utx 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); + const auto result = SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/cs_params.m_cost_of_change, max_selection_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, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) +static void TestBnBFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) { - const auto result = SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/default_cs_params.m_cost_of_change, max_selection_weight); + const auto result = SelectCoinsBnB(utxo_pool, selection_target, /*cost_of_change=*/cs_params.m_cost_of_change, max_selection_weight); BOOST_CHECK_MESSAGE(!result, "BnB-Fail: " + test_title); bool max_weight_exceeded = util::ErrorString(result).original.find("The inputs size exceeds the maximum weight") != std::string::npos; BOOST_CHECK(expect_max_weight_exceeded == max_weight_exceeded); @@ -127,11 +128,9 @@ BOOST_AUTO_TEST_CASE(bnb_test) for (int feerate : feerates) { std::vector utxo_pool; - CoinSelectionParams cs_params = init_default_params(); - cs_params.m_effective_feerate = CFeeRate{feerate}; + const CoinSelectionParams cs_params = init_cs_params(feerate); - // Fail for empty UTXO pool - TestBnBFail("Empty UTXO pool", utxo_pool, /*selection_target=*/1 * CENT); + TestBnBFail("Empty UTXO pool", utxo_pool, /*selection_target=*/1 * CENT, cs_params); AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT}, cs_params); @@ -143,19 +142,19 @@ BOOST_AUTO_TEST_CASE(bnb_test) 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); + TestBnBSuccess("Select upper bound", utxo_pool, /*selection_target=*/4 * CENT - 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); + TestBnBFail("Overshoot upper bound", utxo_pool, /*selection_target=*/4 * CENT - cs_params.m_cost_of_change - 1, cs_params); TestBnBSuccess("Select max weight", utxo_pool, /*selection_target=*/4 * CENT, /*expected_input_amounts=*/{1 * CENT, 3 * CENT}, cs_params, /*custom_spending_vsize=*/P2WPKH_INPUT_VSIZE, /*max_selection_weight=*/4 * 2 * P2WPKH_INPUT_VSIZE); - TestBnBFail("Exceed max weight", utxo_pool, /*selection_target=*/4 * CENT, /*max_selection_weight=*/4 * 2 * P2WPKH_INPUT_VSIZE - 1, /*expect_max_weight_exceeded=*/true); + TestBnBFail("Exceed max weight", utxo_pool, /*selection_target=*/4 * CENT, cs_params, /*max_selection_weight=*/4 * 2 * P2WPKH_INPUT_VSIZE - 1, /*expect_max_weight_exceeded=*/true); // 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); + TestBnBFail("Smallest combination too big", utxo_pool, /*selection_target=*/0.5 * CENT, cs_params); + TestBnBFail("No UTXO combination in target window", utxo_pool, /*selection_target=*/7 * CENT, cs_params); + TestBnBFail("Select more than available", utxo_pool, /*selection_target=*/10 * CENT, cs_params); // Test skipping of equivalent input sets std::vector clone_pool; @@ -189,7 +188,7 @@ BOOST_AUTO_TEST_CASE(bnb_test) 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); + doppelgangers.push_back(1 * CENT + cs_params.m_cost_of_change + i); } } AddCoins(doppelganger_pool, doppelgangers, cs_params); @@ -197,8 +196,8 @@ BOOST_AUTO_TEST_CASE(bnb_test) 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); + AddCoins(doppelganger_pool, {1 * CENT + 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, cs_params); } } @@ -209,8 +208,7 @@ BOOST_AUTO_TEST_CASE(bnb_feerate_sensitivity_test) 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}; + const CoinSelectionParams high_feerate_params = init_cs_params(/*eff_feerate=*/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); From 64ab97466f7daf1fdf635548d7207dffb2134cd5 Mon Sep 17 00:00:00 2001 From: Murch Date: Fri, 20 Mar 2026 14:47:53 -0700 Subject: [PATCH 2/5] Test: Add new minimum to tested feerates The default minimum feerate was lowered from 1000 s/kvB to 100 s/kvB. This adjusts the set of feerates used in the tests to accommodate that new feerate and to cover other potential special cases: - zero: 0 s/kvB - minimum non-zero s/kvB: 1 s/kvB - just below the new default minimum feerate: 99 s/kvB - new default minimum feerate: 100 s/kvB - old default minimum feerate: 1000 s/kvB - a few non-round realistic feerates around default minimum feerate, dust feerate, and default LTFRE: 315 s/kvB, 2345 s/kvB, and 10'292 s/kvB - a high feerate that has been exceeded occassionally: 59'764 s/kvB - a huge feerate that is extremely uncommon: 1'500'000 s/kvB --- src/wallet/test/coinselection_tests.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp index 46b0b463c70..347e00f7273 100644 --- a/src/wallet/test/coinselection_tests.cpp +++ b/src/wallet/test/coinselection_tests.cpp @@ -18,6 +18,21 @@ static FastRandomContext default_rand; static const int P2WPKH_INPUT_VSIZE = 68; static const int P2WPKH_OUTPUT_VSIZE = 31; +/** + * This set of feerates is used in the tests to test edge cases around the + * default minimum feerate and other potential special cases: + * - zero: 0 s/kvB + * - minimum non-zero s/kvB: 1 s/kvB + * - just below the new default minimum feerate: 99 s/kvB + * - new default minimum feerate: 100 s/kvB + * - old default minimum feerate: 1000 s/kvB + * - a few non-round realistic feerates around default minimum feerate, + * dust feerate, and default LTFRE: 315 s/kvB, 2345 s/kvB, and + * 10'292 s/kvB + * - a high feerate that has been exceeded occasionally: 59'764 s/kvB + * - a huge feerate that is extremely uncommon: 1'500'000 s/kvB */ +static const std::vector FEERATES = {0, 1, 99, 100, 315, 1'000, 2'345, 10'292, 59'764, 1'500'000}; + /** Default coin selection parameters allow us to only explicitly set * parameters when a diverging value is relevant in the context of a test, * without reiterating the defaults in every test. We use P2WPKH input and @@ -123,9 +138,7 @@ static void TestBnBFail(std::string test_title, std::vector& utxo_p 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) { + for (int feerate : FEERATES) { std::vector utxo_pool; const CoinSelectionParams cs_params = init_cs_params(feerate); From 2840f041c5ede3fdf3307a9ec53314e8f5dcecb9 Mon Sep 17 00:00:00 2001 From: Murch Date: Fri, 20 Mar 2026 14:57:41 -0700 Subject: [PATCH 3/5] test: Rework SRD insufficient balance test This refactor is part of the effort to move the coin selection tests to a framework that can use non-zero, realistic feerates. The insufficient funds failure case is extended with a few additional similar variants of the failure. --- src/wallet/test/coinselection_tests.cpp | 25 +++++++++++++++++++++++++ src/wallet/test/coinselector_tests.cpp | 18 ------------------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp index 347e00f7273..fb3b0b19a8a 100644 --- a/src/wallet/test/coinselection_tests.cpp +++ b/src/wallet/test/coinselection_tests.cpp @@ -236,5 +236,30 @@ BOOST_AUTO_TEST_CASE(bnb_feerate_sensitivity_test) 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); } +static void TestSRDFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) +{ + const auto result = SelectCoinsSRD(utxo_pool, selection_target, cs_params.m_change_fee, cs_params.rng_fast, max_selection_weight); + BOOST_CHECK_MESSAGE(!result, "SRD-Fail: " + test_title); + bool max_weight_exceeded = util::ErrorString(result).original.find("The inputs size exceeds the maximum weight") != std::string::npos; + BOOST_CHECK(expect_max_weight_exceeded == max_weight_exceeded); +} + +BOOST_AUTO_TEST_CASE(srd_test) +{ + for (int feerate : FEERATES) { + std::vector utxo_pool; + + const CoinSelectionParams cs_params = init_cs_params(feerate); + + TestSRDFail("Empty UTXO pool", utxo_pool, /*selection_target=*/1 * CENT, cs_params); + + AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT}, cs_params); + + TestSRDFail("Undershoot minimum change by one sat", utxo_pool, /*selection_target=*/9 * CENT - cs_params.m_change_fee - CHANGE_LOWER + 1, cs_params); + TestSRDFail("Spend more than available", utxo_pool, /*selection_target=*/9 * CENT + 1, cs_params); + TestSRDFail("Spend everything", utxo_pool, /*selection_target=*/9 * CENT, cs_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 fe33e9347e7..740079d2c66 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -1217,24 +1217,6 @@ BOOST_AUTO_TEST_CASE(srd_tests) /*avoid_partial=*/false, }; - { - // ######################################################### - // 1) Insufficient funds, select all provided coins and fail - // ######################################################### - CAmount target = 49.5L * COIN; - int max_selection_weight = 10000; // high enough to not fail for this reason. - const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { - CoinsResult available_coins; - for (int j = 0; j < 10; ++j) { - add_coin(available_coins, wallet, CAmount(1 * COIN)); - add_coin(available_coins, wallet, CAmount(2 * COIN)); - } - return available_coins; - }); - BOOST_CHECK(!res); - BOOST_CHECK(util::ErrorString(res).empty()); // empty means "insufficient funds" - } - { // ########################### // 2) Test max weight exceeded From fe9f53bf0b2e5a500d16fd4b7be0e334c10f16f5 Mon Sep 17 00:00:00 2001 From: Murch Date: Fri, 20 Mar 2026 15:00:41 -0700 Subject: [PATCH 4/5] test: Add SRD success tests Previously, SRD had only failure and edge-case coverage, so we add a few simple coin selection targets that should quickly succeed, including one that will create a minimal change output. --- src/wallet/test/coinselection_tests.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp index fb3b0b19a8a..34b71887edb 100644 --- a/src/wallet/test/coinselection_tests.cpp +++ b/src/wallet/test/coinselection_tests.cpp @@ -236,6 +236,16 @@ BOOST_AUTO_TEST_CASE(bnb_feerate_sensitivity_test) 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); } +static void TestSRDSuccess(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, const int max_selection_weight = MAX_STANDARD_TX_WEIGHT) +{ + CAmount expected_min_amount = selection_target + cs_params.m_change_fee + CHANGE_LOWER; + + const auto result = SelectCoinsSRD(utxo_pool, selection_target, cs_params.m_change_fee, cs_params.rng_fast, max_selection_weight); + BOOST_CHECK_MESSAGE(result, "Falsy result in SRD-Success: " + test_title); + const CAmount selected_effective_value = result->GetSelectedEffectiveValue(); + BOOST_CHECK_MESSAGE(selected_effective_value >= expected_min_amount, strprintf("Selected effective value is lower than expected in SRD-Success: %s. Expected %d, but got %d", test_title, expected_min_amount, selected_effective_value)); +} + static void TestSRDFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) { const auto result = SelectCoinsSRD(utxo_pool, selection_target, cs_params.m_change_fee, cs_params.rng_fast, max_selection_weight); @@ -255,6 +265,14 @@ BOOST_AUTO_TEST_CASE(srd_test) AddCoins(utxo_pool, {1 * CENT, 3 * CENT, 5 * CENT}, cs_params); + TestSRDSuccess("Select 21k sats", utxo_pool, /*selection_target=*/21'000, cs_params); + TestSRDSuccess("Select 1 CENT", utxo_pool, /*selection_target=*/1 * CENT, cs_params); + TestSRDSuccess("Select 3.125 CENT", utxo_pool, /*selection_target=*/3'125'000, cs_params); + TestSRDSuccess("Select 4 CENT", utxo_pool, /*selection_target=*/4 * CENT, cs_params); + TestSRDSuccess("Select 7 CENT", utxo_pool, /*selection_target=*/7 * CENT, cs_params); + + // The minimum change amount for SRD is the feerate dependent `change_fee` plus CHANGE_LOWER + TestSRDSuccess("Create minimum change", utxo_pool, /*selection_target=*/9 * CENT - cs_params.m_change_fee - CHANGE_LOWER, cs_params); TestSRDFail("Undershoot minimum change by one sat", utxo_pool, /*selection_target=*/9 * CENT - cs_params.m_change_fee - CHANGE_LOWER + 1, cs_params); TestSRDFail("Spend more than available", utxo_pool, /*selection_target=*/9 * CENT + 1, cs_params); TestSRDFail("Spend everything", utxo_pool, /*selection_target=*/9 * CENT, cs_params); From 858a0a9c96b622e7d8a6b2b700f26c898971c10e Mon Sep 17 00:00:00 2001 From: Murch Date: Fri, 20 Mar 2026 15:02:39 -0700 Subject: [PATCH 5/5] test: Add SRD maximum weight tests Also: - Add weight check to Success cases. Per the introduction of TRUC transactions, it is more likely that we will attempt to build transactions of limited weight (e.g., TRUC child transactions may not exceed 4000 WU). When SRD exceeds the input weight limit, it evicts the OutputGroup with the lowest effective value before selecting additional UTXOs: we test that SRD will find a solution that depends on the eviction working correctly, and that it fails as expected when no solution is possible. --- src/wallet/test/coinselection_tests.cpp | 9 +++- src/wallet/test/coinselector_tests.cpp | 72 ------------------------- 2 files changed, 8 insertions(+), 73 deletions(-) diff --git a/src/wallet/test/coinselection_tests.cpp b/src/wallet/test/coinselection_tests.cpp index 34b71887edb..6e60af1c0e3 100644 --- a/src/wallet/test/coinselection_tests.cpp +++ b/src/wallet/test/coinselection_tests.cpp @@ -126,6 +126,7 @@ static void TestBnBSuccess(std::string test_title, std::vector& utx 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_CHECK_MESSAGE(result->GetWeight() <= max_selection_weight, strprintf("Selected weight is higher than permitted in BnB-Success: %s. Expected %d, but got %d", test_title, max_selection_weight, result->GetWeight())); } static void TestBnBFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) @@ -172,7 +173,7 @@ BOOST_AUTO_TEST_CASE(bnb_test) // 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); + AddDuplicateCoins(clone_pool, /*count=*/50'000, /*amount=*/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`) @@ -244,6 +245,7 @@ static void TestSRDSuccess(std::string test_title, std::vector& utx BOOST_CHECK_MESSAGE(result, "Falsy result in SRD-Success: " + test_title); const CAmount selected_effective_value = result->GetSelectedEffectiveValue(); BOOST_CHECK_MESSAGE(selected_effective_value >= expected_min_amount, strprintf("Selected effective value is lower than expected in SRD-Success: %s. Expected %d, but got %d", test_title, expected_min_amount, selected_effective_value)); + BOOST_CHECK_MESSAGE(result->GetWeight() <= max_selection_weight, strprintf("Selected weight is higher than permitted in SRD-Success: %s. Expected %d, but got %d", test_title, max_selection_weight, result->GetWeight())); } static void TestSRDFail(std::string test_title, std::vector& utxo_pool, const CAmount& selection_target, const CoinSelectionParams& cs_params = default_cs_params, int max_selection_weight = MAX_STANDARD_TX_WEIGHT, const bool expect_max_weight_exceeded = false) @@ -276,6 +278,11 @@ BOOST_AUTO_TEST_CASE(srd_test) TestSRDFail("Undershoot minimum change by one sat", utxo_pool, /*selection_target=*/9 * CENT - cs_params.m_change_fee - CHANGE_LOWER + 1, cs_params); TestSRDFail("Spend more than available", utxo_pool, /*selection_target=*/9 * CENT + 1, cs_params); TestSRDFail("Spend everything", utxo_pool, /*selection_target=*/9 * CENT, cs_params); + + AddDuplicateCoins(utxo_pool, /*count=*/100, /*amount=*/5 * CENT, cs_params); + AddDuplicateCoins(utxo_pool, /*count=*/3, /*amount=*/7 * CENT, cs_params); + TestSRDSuccess("Select most valuable UTXOs for acceptable weight", utxo_pool, /*selection_target=*/20 * CENT, cs_params, /*max_selection_weight=*/4 * 4 * (P2WPKH_INPUT_VSIZE - 1 )); + TestSRDFail("No acceptable weight possible", utxo_pool, /*selection_target=*/25 * CENT, cs_params, /*max_selection_weight=*/4 * 3 * P2WPKH_INPUT_VSIZE, /*expect_max_weight_exceeded=*/true); } } diff --git a/src/wallet/test/coinselector_tests.cpp b/src/wallet/test/coinselector_tests.cpp index 740079d2c66..ee6a639a43d 100644 --- a/src/wallet/test/coinselector_tests.cpp +++ b/src/wallet/test/coinselector_tests.cpp @@ -1185,78 +1185,6 @@ BOOST_AUTO_TEST_CASE(coin_grinder_tests) } } -static util::Result SelectCoinsSRD(const CAmount& target, - const CoinSelectionParams& cs_params, - const node::NodeContext& m_node, - int max_selection_weight, - std::function coin_setup) -{ - std::unique_ptr wallet = NewWallet(m_node); - CoinEligibilityFilter filter(0, 0, 0); // accept all coins without ancestors - Groups group = GroupOutputs(*wallet, coin_setup(*wallet), cs_params, {{filter}})[filter].all_groups; - return SelectCoinsSRD(group.positive_group, target, cs_params.m_change_fee, cs_params.rng_fast, max_selection_weight); -} - -BOOST_AUTO_TEST_CASE(srd_tests) -{ - // Test SRD: - // 1) Insufficient funds, select all provided coins and fail. - // 2) Exceeded max weight, coin selection always surpasses the max allowed weight. - // 3) Select coins without surpassing the max weight (some coins surpasses the max allowed weight, some others not) - - FastRandomContext rand; - CoinSelectionParams dummy_params{ // Only used to provide the 'avoid_partial' flag. - rand, - /*change_output_size=*/34, - /*change_spend_size=*/68, - /*min_change_target=*/CENT, - /*effective_feerate=*/CFeeRate(0), - /*long_term_feerate=*/CFeeRate(0), - /*discard_feerate=*/CFeeRate(0), - /*tx_noinputs_size=*/10 + 34, // static header size + output size - /*avoid_partial=*/false, - }; - - { - // ########################### - // 2) Test max weight exceeded - // ########################### - CAmount target = 49.5L * COIN; - int max_selection_weight = 3000; - const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { - CoinsResult available_coins; - for (int j = 0; j < 10; ++j) { - /* 10 × 1 BTC + 10 × 2 BTC = 30 BTC. 20 × 272 WU = 5440 WU */ - add_coin(available_coins, wallet, CAmount(1 * COIN), CFeeRate(0), 144, false, 0, true); - add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true); - } - return available_coins; - }); - BOOST_CHECK(!res); - BOOST_CHECK(util::ErrorString(res).original.find("The inputs size exceeds the maximum weight") != std::string::npos); - } - - { - // ################################################################################################################ - // 3) Test that SRD result does not exceed the max weight - // ################################################################################################################ - CAmount target = 25.33L * COIN; - int max_selection_weight = 10000; // WU - const auto& res = SelectCoinsSRD(target, dummy_params, m_node, max_selection_weight, [&](CWallet& wallet) { - CoinsResult available_coins; - for (int j = 0; j < 60; ++j) { // 60 UTXO --> 19,8 BTC total --> 60 × 272 WU = 16320 WU - add_coin(available_coins, wallet, CAmount(0.33 * COIN), CFeeRate(0), 144, false, 0, true); - } - for (int i = 0; i < 10; i++) { // 10 UTXO --> 20 BTC total --> 10 × 272 WU = 2720 WU - add_coin(available_coins, wallet, CAmount(2 * COIN), CFeeRate(0), 144, false, 0, true); - } - return available_coins; - }); - BOOST_CHECK(res); - BOOST_CHECK(res->GetWeight() <= max_selection_weight); - } -} - static util::Result select_coins(const CAmount& target, const CoinSelectionParams& cs_params, const CCoinControl& cc, std::function coin_setup, const node::NodeContext& m_node) { std::unique_ptr wallet = NewWallet(m_node);