From 858a0a9c96b622e7d8a6b2b700f26c898971c10e Mon Sep 17 00:00:00 2001 From: Murch Date: Fri, 20 Mar 2026 15:02:39 -0700 Subject: [PATCH] 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);