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.
This commit is contained in:
Murch
2026-03-20 15:02:39 -07:00
parent fe9f53bf0b
commit 858a0a9c96
2 changed files with 8 additions and 73 deletions

View File

@@ -126,6 +126,7 @@ static void TestBnBSuccess(std::string test_title, std::vector<OutputGroup>& 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<OutputGroup>& 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<OutputGroup> 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<OutputGroup>& 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<OutputGroup>& 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);
}
}

View File

@@ -1185,78 +1185,6 @@ BOOST_AUTO_TEST_CASE(coin_grinder_tests)
}
}
static util::Result<SelectionResult> SelectCoinsSRD(const CAmount& target,
const CoinSelectionParams& cs_params,
const node::NodeContext& m_node,
int max_selection_weight,
std::function<CoinsResult(CWallet&)> coin_setup)
{
std::unique_ptr<CWallet> 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<SelectionResult> select_coins(const CAmount& target, const CoinSelectionParams& cs_params, const CCoinControl& cc, std::function<CoinsResult(CWallet&)> coin_setup, const node::NodeContext& m_node)
{
std::unique_ptr<CWallet> wallet = NewWallet(m_node);