Skip to content

Commit 83e85c0

Browse files
committed
coinselection: option to exclude change cost from waste score
Useful when the transaction already accounted for the change cost. Which happens when the user, or wallet process, set the coin control custom change address equal to one of the existing output addresses. In other words, when the user selects one of the transaction existing outputs to be increased by the difference between inputs - output. This will be connected to the transaction creation process in the following-up commit.
1 parent 09899ff commit 83e85c0

File tree

3 files changed

+21
-19
lines changed

3 files changed

+21
-19
lines changed

src/wallet/coinselection.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ void OutputGroupTypeMap::Push(const OutputGroup& group, OutputType type, bool in
449449
}
450450
}
451451

452-
CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value)
452+
CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, std::optional<CAmount> change_cost, CAmount target, bool use_effective_value)
453453
{
454454
// This function should not be called with empty inputs as that would mean the selection failed
455455
assert(!inputs.empty());
@@ -465,11 +465,12 @@ CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmo
465465

466466
if (change_cost) {
467467
// Consider the cost of making change and spending it in the future
468-
// If we aren't making change, the caller should've set change_cost to 0
469-
assert(change_cost > 0);
470-
waste += change_cost;
468+
// If we aren't making change, the caller should've set change_cost to std::nullopt
469+
// change_cost will be 0 if the change cost is already covered
470+
assert(change_cost >= 0);
471+
waste += *change_cost;
471472
} else {
472-
// When we are not making change (change_cost == 0), consider the excess we are throwing away to fees
473+
// When we are not making change (change_cost == std::nullopt), consider the excess we are throwing away to fees
473474
assert(selected_effective_value >= target);
474475
waste += selected_effective_value - target;
475476
}
@@ -495,7 +496,7 @@ void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const
495496
if (change > 0) {
496497
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
497498
} else {
498-
m_waste = GetSelectionWaste(m_selected_inputs, 0, m_target, m_use_effective);
499+
m_waste = GetSelectionWaste(m_selected_inputs, /*change_cost=*/std::nullopt, m_target, m_use_effective);
499500
}
500501
}
501502

src/wallet/coinselection.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,14 @@ typedef std::map<CoinEligibilityFilter, OutputGroupTypeMap> FilteredOutputGroups
280280
*
281281
* @param[in] inputs The selected inputs
282282
* @param[in] change_cost The cost of creating change and spending it in the future.
283-
* Only used if there is change, in which case it must be positive.
284-
* Must be 0 if there is no change.
283+
* Only used if there is change, in which case it must be positive, or 0 for an already covered
284+
* change cost (e.g. existent change output).
285+
* Must be std::nullopt if there is no change.
285286
* @param[in] target The amount targeted by the coin selection algorithm.
286287
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
287288
* @return The waste
288289
*/
289-
[[nodiscard]] CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
290+
[[nodiscard]] CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, std::optional<CAmount> change_cost, CAmount target, bool use_effective_value = true);
290291

291292

292293
/** Choose a random change target for each transaction to make it harder to fingerprint the Core

src/wallet/test/coinselector_tests.cpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ BOOST_AUTO_TEST_CASE(waste_test)
840840
// Waste without change is the excess and difference between fee and long term fee
841841
add_coin(1 * COIN, 1, selection, fee, fee - fee_diff);
842842
add_coin(2 * COIN, 2, selection, fee, fee - fee_diff);
843-
const CAmount waste_nochange1 = GetSelectionWaste(selection, 0, target);
843+
const CAmount waste_nochange1 = GetSelectionWaste(selection, /*change_cost=*/std::nullopt, target);
844844
BOOST_CHECK_EQUAL(fee_diff * 2 + excess, waste_nochange1);
845845
selection.clear();
846846

@@ -853,7 +853,7 @@ BOOST_AUTO_TEST_CASE(waste_test)
853853
// Waste without change and fee == long term fee is just the excess
854854
add_coin(1 * COIN, 1, selection, fee, fee);
855855
add_coin(2 * COIN, 2, selection, fee, fee);
856-
BOOST_CHECK_EQUAL(excess, GetSelectionWaste(selection, 0, target));
856+
BOOST_CHECK_EQUAL(excess, GetSelectionWaste(selection, /*change_cost=*/std::nullopt, target));
857857
selection.clear();
858858

859859
// Waste will be greater when fee is greater, but long term fee is the same
@@ -876,7 +876,7 @@ BOOST_AUTO_TEST_CASE(waste_test)
876876
// With long term fee greater than fee, waste should be less than when long term fee is less than fee
877877
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
878878
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
879-
const CAmount waste_nochange2 = GetSelectionWaste(selection, 0, target);
879+
const CAmount waste_nochange2 = GetSelectionWaste(selection, /*change_cost=*/std::nullopt, target);
880880
BOOST_CHECK_EQUAL(fee_diff * -2 + excess, waste_nochange2);
881881
BOOST_CHECK_LT(waste_nochange2, waste_nochange1);
882882
selection.clear();
@@ -885,7 +885,7 @@ BOOST_AUTO_TEST_CASE(waste_test)
885885
add_coin(1 * COIN, 1, selection, fee, fee);
886886
add_coin(2 * COIN, 2, selection, fee, fee);
887887
const CAmount exact_target{in_amt - fee * 2};
888-
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /*change_cost=*/0, exact_target));
888+
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /*change_cost=*/std::nullopt, exact_target));
889889
selection.clear();
890890

891891
// No Waste when (fee - long_term_fee) == (-cost_of_change), and no excess
@@ -899,15 +899,15 @@ BOOST_AUTO_TEST_CASE(waste_test)
899899
const CAmount new_target{in_amt - fee * 2 - fee_diff * 2};
900900
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
901901
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
902-
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /*change_cost=*/ 0, new_target));
902+
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /*change_cost=*/std::nullopt, new_target));
903903
selection.clear();
904904

905905
// Negative waste when the long term fee is greater than the current fee and the selected value == target
906906
const CAmount exact_target1{3 * COIN - 2 * fee};
907907
const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff))
908908
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
909909
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
910-
BOOST_CHECK_EQUAL(target_waste1, GetSelectionWaste(selection, /*change_cost=*/ 0, exact_target1));
910+
BOOST_CHECK_EQUAL(target_waste1, GetSelectionWaste(selection, /*change_cost=*/std::nullopt, exact_target1));
911911
selection.clear();
912912

913913
// Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee))
@@ -1071,7 +1071,7 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
10711071
rand,
10721072
/*change_output_size=*/34,
10731073
/*min_change_target=*/CENT,
1074-
/*effective_feerate=*/CFeeRate(0),
1074+
/*effective_feerate=*/CFeeRate(1), // tiny feerate so the waste score isn't always 0.
10751075
/*long_term_feerate=*/CFeeRate(0),
10761076
/*tx_noinputs_size=*/10 + 34, // static header size + output size
10771077
/*avoid_partial=*/false,
@@ -1089,10 +1089,10 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
10891089
target, cs_params, cc, [&](CWallet& wallet) {
10901090
CoinsResult available_coins;
10911091
for (int j = 0; j < 1515; ++j) {
1092-
add_coin(available_coins, wallet, CAmount(0.033 * COIN), CFeeRate(0), 144, false, 0, true);
1092+
add_coin(available_coins, wallet, CAmount(0.033 * COIN), cs_params.m_effective_feerate, 144, false, 0, true);
10931093
}
10941094

1095-
add_coin(available_coins, wallet, CAmount(50 * COIN), CFeeRate(0), 144, false, 0, true);
1095+
add_coin(available_coins, wallet, CAmount(50 * COIN), cs_params.m_effective_feerate, 144, false, 0, true);
10961096
return available_coins;
10971097
},
10981098
m_node);
@@ -1101,7 +1101,7 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
11011101
// Verify that only the 50 BTC UTXO was selected
11021102
const auto& selection_res = result->GetInputSet();
11031103
BOOST_CHECK(selection_res.size() == 1);
1104-
BOOST_CHECK((*selection_res.begin())->GetEffectiveValue() == 50 * COIN);
1104+
BOOST_CHECK((*selection_res.begin())->txout.nValue == 50 * COIN);
11051105
}
11061106

11071107
{

0 commit comments

Comments
 (0)