Merge bitcoin/bitcoin#28366: Fix waste calculation in SelectionResult
bd34dd85e7Use `exact_target` shorthand in coinselector_tests (Murch)7aa7e30441Fold GetSelectionWaste() into ComputeAndSetWaste() (Murch) Pull request description: PR #26152 moved waste calculation into SelectionResult to be able to correct the waste score on basis of the bump_fee_group_discount for overlapping ancestries. This left two functions with largely overlapping purpose, where one was simply a wrapper of the other. This PR cleans up the overlap, and fixes the double-meaning of `change_cost` where the `GetChange()` function assumed that no change was created when `change_cost` was set to 0. This behavior was exploited in a bunch of tests, but is problematic, because a `change_cost` of 0 is permitted with custom settings for feerate and discard_feerate (i.e. when they’re both 0). ACKs for top commit: achow101: ACKbd34dd85e7furszy: Code ACKbd34dd85e7ismaelsadeeq: Code Review ACKbd34dd85e7Tree-SHA512: 83a2688d45d719dc61a64b5180fe136107faccf401a59df65245c05d701748a03e85ed56fde8c9b7ef39a3ab54374dd3718c559bda5b3f55dafedfd7fed25161
This commit is contained in:
6 files changed
+89
-102
No files matched your search
@@ -874,29 +874,32 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
|
||||
BOOST_AUTO_TEST_CASE(waste_test)
|
||||
{
|
||||
const CAmount fee{100};
|
||||
const CAmount min_viable_change{300};
|
||||
const CAmount change_cost{125};
|
||||
const CAmount change_fee{30};
|
||||
const CAmount fee_diff{40};
|
||||
const CAmount in_amt{3 * COIN};
|
||||
const CAmount target{2 * COIN};
|
||||
const CAmount excess{in_amt - fee * 2 - target};
|
||||
const CAmount excess{80};
|
||||
const CAmount exact_target{in_amt - fee * 2}; // Maximum spendable amount after fees: no change, no excess
|
||||
|
||||
// The following tests that the waste is calculated correctly in various scenarios.
|
||||
// ComputeAndSetWaste will first determine the size of the change output. We don't really
|
||||
// care about the change and just want to use the variant that always includes the change_cost,
|
||||
// so min_viable_change and change_fee are set to 0 to ensure that.
|
||||
// In the following, we test that the waste is calculated correctly in various scenarios.
|
||||
// Usually, RecalculateWaste would compute change_fee and change_cost on basis of the
|
||||
// change output type, current feerate, and discard_feerate, but we use fixed values
|
||||
// across this test to make the test easier to understand.
|
||||
{
|
||||
// Waste with change is the change cost and difference between fee and long term fee
|
||||
SelectionResult selection1{target, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection1, fee, fee - fee_diff);
|
||||
add_coin(1 * COIN, 1, selection1, /*fee=*/fee, /*long_term_fee=*/fee - fee_diff);
|
||||
add_coin(2 * COIN, 2, selection1, fee, fee - fee_diff);
|
||||
selection1.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
|
||||
selection1.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, selection1.GetWaste());
|
||||
|
||||
// Waste will be greater when fee is greater, but long term fee is the same
|
||||
SelectionResult selection2{target, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection2, fee * 2, fee - fee_diff);
|
||||
add_coin(2 * COIN, 2, selection2, fee * 2, fee - fee_diff);
|
||||
selection2.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
|
||||
selection2.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste());
|
||||
|
||||
// Waste with change is the change cost and difference between fee and long term fee
|
||||
@@ -904,25 +907,25 @@ BOOST_AUTO_TEST_CASE(waste_test)
|
||||
SelectionResult selection3{target, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection3, fee, fee + fee_diff);
|
||||
add_coin(2 * COIN, 2, selection3, fee, fee + fee_diff);
|
||||
selection3.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
|
||||
selection3.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, selection3.GetWaste());
|
||||
BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste());
|
||||
}
|
||||
|
||||
{
|
||||
// Waste without change is the excess and difference between fee and long term fee
|
||||
SelectionResult selection_nochange1{target, SelectionAlgorithm::MANUAL};
|
||||
SelectionResult selection_nochange1{exact_target - excess, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection_nochange1, fee, fee - fee_diff);
|
||||
add_coin(2 * COIN, 2, selection_nochange1, fee, fee - fee_diff);
|
||||
selection_nochange1.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
|
||||
selection_nochange1.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(fee_diff * 2 + excess, selection_nochange1.GetWaste());
|
||||
|
||||
// Waste without change is the excess and difference between fee and long term fee
|
||||
// With long term fee greater than fee, waste should be less than when long term fee is less than fee
|
||||
SelectionResult selection_nochange2{target, SelectionAlgorithm::MANUAL};
|
||||
SelectionResult selection_nochange2{exact_target - excess, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection_nochange2, fee, fee + fee_diff);
|
||||
add_coin(2 * COIN, 2, selection_nochange2, fee, fee + fee_diff);
|
||||
selection_nochange2.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
|
||||
selection_nochange2.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(fee_diff * -2 + excess, selection_nochange2.GetWaste());
|
||||
BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste());
|
||||
}
|
||||
@@ -932,57 +935,54 @@ BOOST_AUTO_TEST_CASE(waste_test)
|
||||
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection, fee, fee);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(change_cost, selection.GetWaste());
|
||||
}
|
||||
|
||||
{
|
||||
// Waste without change and fee == long term fee is just the excess
|
||||
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
|
||||
SelectionResult selection{exact_target - excess, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection, fee, fee);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(excess, selection.GetWaste());
|
||||
}
|
||||
|
||||
{
|
||||
// No Waste when fee == long_term_fee, no change, and no excess
|
||||
const CAmount exact_target{in_amt - fee * 2};
|
||||
// Waste is 0 when fee == long_term_fee, no change, and no excess
|
||||
SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection, fee, fee);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost , change_fee);
|
||||
BOOST_CHECK_EQUAL(0, selection.GetWaste());
|
||||
}
|
||||
|
||||
{
|
||||
// No Waste when (fee - long_term_fee) == (-cost_of_change), and no excess
|
||||
// Waste is 0 when (fee - long_term_fee) == (-cost_of_change), and no excess
|
||||
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
|
||||
const CAmount new_change_cost{fee_diff * 2};
|
||||
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, new_change_cost, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, /*change_cost=*/fee_diff * 2, change_fee);
|
||||
BOOST_CHECK_EQUAL(0, selection.GetWaste());
|
||||
}
|
||||
|
||||
{
|
||||
// No Waste when (fee - long_term_fee) == (-excess), no change cost
|
||||
const CAmount new_target{in_amt - fee * 2 - fee_diff * 2};
|
||||
// Waste is 0 when (fee - long_term_fee) == (-excess), no change cost
|
||||
const CAmount new_target{exact_target - /*excess=*/fee_diff * 2};
|
||||
SelectionResult selection{new_target, SelectionAlgorithm::MANUAL};
|
||||
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(0, selection.GetWaste());
|
||||
}
|
||||
|
||||
{
|
||||
// Negative waste when the long term fee is greater than the current fee and the selected value == target
|
||||
const CAmount exact_target{3 * COIN - 2 * fee};
|
||||
SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
|
||||
const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff))
|
||||
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(target_waste1, selection.GetWaste());
|
||||
}
|
||||
|
||||
@@ -990,10 +990,14 @@ BOOST_AUTO_TEST_CASE(waste_test)
|
||||
// Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee))
|
||||
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
|
||||
const CAmount large_fee_diff{90};
|
||||
const CAmount target_waste2{-2 * large_fee_diff + change_cost}; // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost
|
||||
const CAmount target_waste2{-2 * large_fee_diff + change_cost};
|
||||
// = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost
|
||||
// = (2 * 100) - (2 * (100 + 90)) + 125
|
||||
// = 200 - 380 + 125 = -55
|
||||
assert(target_waste2 == -55);
|
||||
add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff);
|
||||
add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff);
|
||||
selection.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
BOOST_CHECK_EQUAL(target_waste2, selection.GetWaste());
|
||||
}
|
||||
}
|
||||
@@ -1018,12 +1022,12 @@ BOOST_AUTO_TEST_CASE(bump_fee_test)
|
||||
inputs[i]->ApplyBumpFee(20*(i+1));
|
||||
}
|
||||
|
||||
selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
CAmount expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60;
|
||||
BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
|
||||
|
||||
selection.SetBumpFeeDiscount(30);
|
||||
selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
expected_waste = fee_diff * -2 + change_cost + /*bump_fees=*/60 - /*group_discount=*/30;
|
||||
BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
|
||||
}
|
||||
@@ -1044,12 +1048,12 @@ BOOST_AUTO_TEST_CASE(bump_fee_test)
|
||||
inputs[i]->ApplyBumpFee(20*(i+1));
|
||||
}
|
||||
|
||||
selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
CAmount expected_waste = fee_diff * -2 + /*bump_fees=*/60 + /*excess = 100 - bump_fees*/40;
|
||||
BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
|
||||
|
||||
selection.SetBumpFeeDiscount(30);
|
||||
selection.ComputeAndSetWaste(min_viable_change, change_cost, change_fee);
|
||||
selection.RecalculateWaste(min_viable_change, change_cost, change_fee);
|
||||
expected_waste = fee_diff * -2 + /*bump_fees=*/60 - /*group_discount=*/30 + /*excess = 100 - bump_fees + group_discount*/70;
|
||||
BOOST_CHECK_EQUAL(expected_waste, selection.GetWaste());
|
||||
}
|
||||
@@ -1429,6 +1433,7 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
|
||||
/*avoid_partial=*/false,
|
||||
};
|
||||
|
||||
int max_weight = MAX_STANDARD_TX_WEIGHT - WITNESS_SCALE_FACTOR * (cs_params.tx_noinputs_size + cs_params.change_output_size);
|
||||
{
|
||||
// Scenario 1:
|
||||
// The actor starts with 1x 50.0 BTC and 1515x 0.033 BTC (~100.0 BTC total) unspent outputs
|
||||
@@ -1450,10 +1455,9 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
|
||||
m_node);
|
||||
|
||||
BOOST_CHECK(result);
|
||||
// Verify that only the 50 BTC UTXO was selected
|
||||
const auto& selection_res = result->GetInputSet();
|
||||
BOOST_CHECK(selection_res.size() == 1);
|
||||
BOOST_CHECK((*selection_res.begin())->GetEffectiveValue() == 50 * COIN);
|
||||
// Verify that the 50 BTC UTXO was selected, and result is below max_weight
|
||||
BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(50 * COIN)));
|
||||
BOOST_CHECK_LE(result->GetWeight(), max_weight);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1479,6 +1483,7 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
|
||||
|
||||
BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.0625 * COIN)));
|
||||
BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(0.025 * COIN)));
|
||||
BOOST_CHECK_LE(result->GetWeight(), max_weight);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -270,7 +270,7 @@ FUZZ_TARGET(coinselection)
|
||||
if (result_srd) {
|
||||
assert(result_srd->GetSelectedValue() >= target);
|
||||
assert(result_srd->GetChange(CHANGE_LOWER, coin_params.m_change_fee) > 0); // Demonstrate that SRD creates change of at least CHANGE_LOWER
|
||||
result_srd->ComputeAndSetWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
|
||||
result_srd->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
|
||||
(void)result_srd->GetShuffledInputVector();
|
||||
(void)result_srd->GetInputSet();
|
||||
}
|
||||
@@ -279,7 +279,7 @@ FUZZ_TARGET(coinselection)
|
||||
auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context, MAX_STANDARD_TX_WEIGHT);
|
||||
if (result_knapsack) {
|
||||
assert(result_knapsack->GetSelectedValue() >= target);
|
||||
result_knapsack->ComputeAndSetWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
|
||||
result_knapsack->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
|
||||
(void)result_knapsack->GetShuffledInputVector();
|
||||
(void)result_knapsack->GetInputSet();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user