Skip to content

Commit 3e3e052

Browse files
coinselection: Move GetSelectionWaste into SelectionResult
GetSelectionWaste will need to access more context within a selection result, and so should be a private member function rather than a static function. It's only use outside of SelectionResult was for tests which have now been updated to just make a SelectionResult. Co-authored-by: Murch <murch@murch.one>
1 parent c57889d commit 3e3e052

File tree

3 files changed

+143
-116
lines changed

3 files changed

+143
-116
lines changed

src/wallet/coinselection.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,15 @@ 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 SelectionResult::GetSelectionWaste(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
455-
assert(!inputs.empty());
455+
assert(!m_selected_inputs.empty());
456456

457457
// Always consider the cost of spending an input now vs in the future.
458458
CAmount waste = 0;
459459
CAmount selected_effective_value = 0;
460-
for (const auto& coin_ptr : inputs) {
460+
for (const auto& coin_ptr : m_selected_inputs) {
461461
const COutput& coin = *coin_ptr;
462462
waste += coin.GetFee() - coin.long_term_fee;
463463
selected_effective_value += use_effective_value ? coin.GetEffectiveValue() : coin.txout.nValue;
@@ -493,9 +493,9 @@ void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const
493493
const CAmount change = GetChange(min_viable_change, change_fee);
494494

495495
if (change > 0) {
496-
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
496+
m_waste = GetSelectionWaste(change_cost, m_target, m_use_effective);
497497
} else {
498-
m_waste = GetSelectionWaste(m_selected_inputs, 0, m_target, m_use_effective);
498+
m_waste = GetSelectionWaste(0, m_target, m_use_effective);
499499
}
500500
}
501501

src/wallet/coinselection.h

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -275,26 +275,6 @@ struct OutputGroupTypeMap
275275

276276
typedef std::map<CoinEligibilityFilter, OutputGroupTypeMap> FilteredOutputGroups;
277277

278-
/** Compute the waste for this result given the cost of change
279-
* and the opportunity cost of spending these inputs now vs in the future.
280-
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
281-
* If no change, waste = excess + inputs * (effective_feerate - long_term_feerate)
282-
* where excess = selected_effective_value - target
283-
* change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size
284-
*
285-
* Note this function is separate from SelectionResult for the tests.
286-
*
287-
* @param[in] inputs The selected inputs
288-
* @param[in] change_cost The cost of creating change and spending it in the future.
289-
* Only used if there is change, in which case it must be positive.
290-
* Must be 0 if there is no change.
291-
* @param[in] target The amount targeted by the coin selection algorithm.
292-
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
293-
* @return The waste
294-
*/
295-
[[nodiscard]] CAmount GetSelectionWaste(const std::set<std::shared_ptr<COutput>>& inputs, CAmount change_cost, CAmount target, bool use_effective_value = true);
296-
297-
298278
/** Choose a random change target for each transaction to make it harder to fingerprint the Core
299279
* wallet based on the change output values of transactions it creates.
300280
* Change target covers at least change fees and adds a random value on top of it.
@@ -348,6 +328,22 @@ struct SelectionResult
348328
}
349329
}
350330

331+
/** Compute the waste for this result given the cost of change
332+
* and the opportunity cost of spending these inputs now vs in the future.
333+
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
334+
* If no change, waste = excess + inputs * (effective_feerate - long_term_feerate)
335+
* where excess = selected_effective_value - target
336+
* change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size
337+
*
338+
* @param[in] change_cost The cost of creating change and spending it in the future.
339+
* Only used if there is change, in which case it must be positive.
340+
* Must be 0 if there is no change.
341+
* @param[in] target The amount targeted by the coin selection algorithm.
342+
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
343+
* @return The waste
344+
*/
345+
[[nodiscard]] CAmount GetSelectionWaste(CAmount change_cost, CAmount target, bool use_effective_value = true);
346+
351347
public:
352348
explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
353349
: m_target(target), m_algo(algo) {}

src/wallet/test/coinselector_tests.cpp

Lines changed: 122 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,17 @@ static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)
5858
result.AddInput(group);
5959
}
6060

61-
static void add_coin(const CAmount& nValue, int nInput, CoinSet& set, CAmount fee = 0, CAmount long_term_fee = 0)
61+
static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result, CAmount fee, CAmount long_term_fee)
6262
{
6363
CMutableTransaction tx;
6464
tx.vout.resize(nInput + 1);
6565
tx.vout[nInput].nValue = nValue;
6666
tx.nLockTime = nextLockTime++; // so all transactions get different hashes
67-
COutput coin(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ 148, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, fee);
68-
coin.long_term_fee = long_term_fee;
69-
set.insert(std::make_shared<COutput>(coin));
67+
std::shared_ptr<COutput> coin = std::make_shared<COutput>(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/ 1, /*input_bytes=*/ 148, /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, /*time=*/ 0, /*from_me=*/ false, fee);
68+
OutputGroup group;
69+
group.Insert(coin, /*ancestors=*/ 0, /*descendants=*/ 0);
70+
coin->long_term_fee = long_term_fee; // group.Insert() will modify long_term_fee, so we need to set it afterwards
71+
result.AddInput(group);
7072
}
7173

7274
static void add_coin(CoinsResult& available_coins, CWallet& wallet, const CAmount& nValue, CFeeRate feerate = CFeeRate(0), int nAge = 6*24, bool fIsFromMe = false, int nInput =0, bool spendable = false, int custom_size = 0)
@@ -827,100 +829,129 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
827829

828830
BOOST_AUTO_TEST_CASE(waste_test)
829831
{
830-
CoinSet selection;
831832
const CAmount fee{100};
832833
const CAmount change_cost{125};
833834
const CAmount fee_diff{40};
834835
const CAmount in_amt{3 * COIN};
835836
const CAmount target{2 * COIN};
836837
const CAmount excess{in_amt - fee * 2 - target};
837838

838-
// Waste with change is the change cost and difference between fee and long term fee
839-
add_coin(1 * COIN, 1, selection, fee, fee - fee_diff);
840-
add_coin(2 * COIN, 2, selection, fee, fee - fee_diff);
841-
const CAmount waste1 = GetSelectionWaste(selection, change_cost, target);
842-
BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, waste1);
843-
selection.clear();
844-
845-
// Waste without change is the excess and difference between fee and long term fee
846-
add_coin(1 * COIN, 1, selection, fee, fee - fee_diff);
847-
add_coin(2 * COIN, 2, selection, fee, fee - fee_diff);
848-
const CAmount waste_nochange1 = GetSelectionWaste(selection, 0, target);
849-
BOOST_CHECK_EQUAL(fee_diff * 2 + excess, waste_nochange1);
850-
selection.clear();
851-
852-
// Waste with change and fee == long term fee is just cost of change
853-
add_coin(1 * COIN, 1, selection, fee, fee);
854-
add_coin(2 * COIN, 2, selection, fee, fee);
855-
BOOST_CHECK_EQUAL(change_cost, GetSelectionWaste(selection, change_cost, target));
856-
selection.clear();
857-
858-
// Waste without change and fee == long term fee is just the excess
859-
add_coin(1 * COIN, 1, selection, fee, fee);
860-
add_coin(2 * COIN, 2, selection, fee, fee);
861-
BOOST_CHECK_EQUAL(excess, GetSelectionWaste(selection, 0, target));
862-
selection.clear();
863-
864-
// Waste will be greater when fee is greater, but long term fee is the same
865-
add_coin(1 * COIN, 1, selection, fee * 2, fee - fee_diff);
866-
add_coin(2 * COIN, 2, selection, fee * 2, fee - fee_diff);
867-
const CAmount waste2 = GetSelectionWaste(selection, change_cost, target);
868-
BOOST_CHECK_GT(waste2, waste1);
869-
selection.clear();
870-
871-
// Waste with change is the change cost and difference between fee and long term fee
872-
// With long term fee greater than fee, waste should be less than when long term fee is less than fee
873-
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
874-
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
875-
const CAmount waste3 = GetSelectionWaste(selection, change_cost, target);
876-
BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, waste3);
877-
BOOST_CHECK_LT(waste3, waste1);
878-
selection.clear();
879-
880-
// Waste without change is the excess and difference between fee and long term fee
881-
// With long term fee greater than fee, waste should be less than when long term fee is less than fee
882-
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
883-
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
884-
const CAmount waste_nochange2 = GetSelectionWaste(selection, 0, target);
885-
BOOST_CHECK_EQUAL(fee_diff * -2 + excess, waste_nochange2);
886-
BOOST_CHECK_LT(waste_nochange2, waste_nochange1);
887-
selection.clear();
888-
889-
// No Waste when fee == long_term_fee, no change, and no excess
890-
add_coin(1 * COIN, 1, selection, fee, fee);
891-
add_coin(2 * COIN, 2, selection, fee, fee);
892-
const CAmount exact_target{in_amt - fee * 2};
893-
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /*change_cost=*/0, exact_target));
894-
selection.clear();
895-
896-
// No Waste when (fee - long_term_fee) == (-cost_of_change), and no excess
897-
const CAmount new_change_cost{fee_diff * 2};
898-
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
899-
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
900-
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, new_change_cost, target));
901-
selection.clear();
902-
903-
// No Waste when (fee - long_term_fee) == (-excess), no change cost
904-
const CAmount new_target{in_amt - fee * 2 - fee_diff * 2};
905-
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
906-
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
907-
BOOST_CHECK_EQUAL(0, GetSelectionWaste(selection, /*change_cost=*/ 0, new_target));
908-
selection.clear();
909-
910-
// Negative waste when the long term fee is greater than the current fee and the selected value == target
911-
const CAmount exact_target1{3 * COIN - 2 * fee};
912-
const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff))
913-
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
914-
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
915-
BOOST_CHECK_EQUAL(target_waste1, GetSelectionWaste(selection, /*change_cost=*/ 0, exact_target1));
916-
selection.clear();
917-
918-
// Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee))
919-
const CAmount large_fee_diff{90};
920-
const CAmount target_waste2{-2 * large_fee_diff + change_cost}; // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost
921-
add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff);
922-
add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff);
923-
BOOST_CHECK_EQUAL(target_waste2, GetSelectionWaste(selection, change_cost, target));
839+
// The following tests that the waste is calculated correctly in various scenarios.
840+
// ComputeAndSetWaste will first determine the size of the change output. We don't really
841+
// care about the change and just want to use the variant that always includes the change_cost,
842+
// so min_viable_change and change_fee are set to 0 to ensure that.
843+
{
844+
// Waste with change is the change cost and difference between fee and long term fee
845+
SelectionResult selection1{target, SelectionAlgorithm::MANUAL};
846+
add_coin(1 * COIN, 1, selection1, fee, fee - fee_diff);
847+
add_coin(2 * COIN, 2, selection1, fee, fee - fee_diff);
848+
selection1.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
849+
BOOST_CHECK_EQUAL(fee_diff * 2 + change_cost, selection1.GetWaste());
850+
851+
// Waste will be greater when fee is greater, but long term fee is the same
852+
SelectionResult selection2{target, SelectionAlgorithm::MANUAL};
853+
add_coin(1 * COIN, 1, selection2, fee * 2, fee - fee_diff);
854+
add_coin(2 * COIN, 2, selection2, fee * 2, fee - fee_diff);
855+
selection2.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
856+
BOOST_CHECK_GT(selection2.GetWaste(), selection1.GetWaste());
857+
858+
// Waste with change is the change cost and difference between fee and long term fee
859+
// With long term fee greater than fee, waste should be less than when long term fee is less than fee
860+
SelectionResult selection3{target, SelectionAlgorithm::MANUAL};
861+
add_coin(1 * COIN, 1, selection3, fee, fee + fee_diff);
862+
add_coin(2 * COIN, 2, selection3, fee, fee + fee_diff);
863+
selection3.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
864+
BOOST_CHECK_EQUAL(fee_diff * -2 + change_cost, selection3.GetWaste());
865+
BOOST_CHECK_LT(selection3.GetWaste(), selection1.GetWaste());
866+
}
867+
868+
{
869+
// Waste without change is the excess and difference between fee and long term fee
870+
SelectionResult selection_nochange1{target, SelectionAlgorithm::MANUAL};
871+
add_coin(1 * COIN, 1, selection_nochange1, fee, fee - fee_diff);
872+
add_coin(2 * COIN, 2, selection_nochange1, fee, fee - fee_diff);
873+
selection_nochange1.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
874+
BOOST_CHECK_EQUAL(fee_diff * 2 + excess, selection_nochange1.GetWaste());
875+
876+
// Waste without change is the excess and difference between fee and long term fee
877+
// With long term fee greater than fee, waste should be less than when long term fee is less than fee
878+
SelectionResult selection_nochange2{target, SelectionAlgorithm::MANUAL};
879+
add_coin(1 * COIN, 1, selection_nochange2, fee, fee + fee_diff);
880+
add_coin(2 * COIN, 2, selection_nochange2, fee, fee + fee_diff);
881+
selection_nochange2.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
882+
BOOST_CHECK_EQUAL(fee_diff * -2 + excess, selection_nochange2.GetWaste());
883+
BOOST_CHECK_LT(selection_nochange2.GetWaste(), selection_nochange1.GetWaste());
884+
}
885+
886+
{
887+
// Waste with change and fee == long term fee is just cost of change
888+
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
889+
add_coin(1 * COIN, 1, selection, fee, fee);
890+
add_coin(2 * COIN, 2, selection, fee, fee);
891+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
892+
BOOST_CHECK_EQUAL(change_cost, selection.GetWaste());
893+
}
894+
895+
{
896+
// Waste without change and fee == long term fee is just the excess
897+
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
898+
add_coin(1 * COIN, 1, selection, fee, fee);
899+
add_coin(2 * COIN, 2, selection, fee, fee);
900+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
901+
BOOST_CHECK_EQUAL(excess, selection.GetWaste());
902+
}
903+
904+
{
905+
// No Waste when fee == long_term_fee, no change, and no excess
906+
const CAmount exact_target{in_amt - fee * 2};
907+
SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
908+
add_coin(1 * COIN, 1, selection, fee, fee);
909+
add_coin(2 * COIN, 2, selection, fee, fee);
910+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
911+
BOOST_CHECK_EQUAL(0, selection.GetWaste());
912+
}
913+
914+
{
915+
// No Waste when (fee - long_term_fee) == (-cost_of_change), and no excess
916+
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
917+
const CAmount new_change_cost{fee_diff * 2};
918+
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
919+
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
920+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, new_change_cost, /*change_fee=*/0);
921+
BOOST_CHECK_EQUAL(0, selection.GetWaste());
922+
}
923+
924+
{
925+
// No Waste when (fee - long_term_fee) == (-excess), no change cost
926+
const CAmount new_target{in_amt - fee * 2 - fee_diff * 2};
927+
SelectionResult selection{new_target, SelectionAlgorithm::MANUAL};
928+
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
929+
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
930+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
931+
BOOST_CHECK_EQUAL(0, selection.GetWaste());
932+
}
933+
934+
{
935+
// Negative waste when the long term fee is greater than the current fee and the selected value == target
936+
const CAmount exact_target{3 * COIN - 2 * fee};
937+
SelectionResult selection{exact_target, SelectionAlgorithm::MANUAL};
938+
const CAmount target_waste1{-2 * fee_diff}; // = (2 * fee) - (2 * (fee + fee_diff))
939+
add_coin(1 * COIN, 1, selection, fee, fee + fee_diff);
940+
add_coin(2 * COIN, 2, selection, fee, fee + fee_diff);
941+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, /*change_cost=*/0, /*change_fee=*/0);
942+
BOOST_CHECK_EQUAL(target_waste1, selection.GetWaste());
943+
}
944+
945+
{
946+
// Negative waste when the long term fee is greater than the current fee and change_cost < - (inputs * (fee - long_term_fee))
947+
SelectionResult selection{target, SelectionAlgorithm::MANUAL};
948+
const CAmount large_fee_diff{90};
949+
const CAmount target_waste2{-2 * large_fee_diff + change_cost}; // = (2 * fee) - (2 * (fee + large_fee_diff)) + change_cost
950+
add_coin(1 * COIN, 1, selection, fee, fee + large_fee_diff);
951+
add_coin(2 * COIN, 2, selection, fee, fee + large_fee_diff);
952+
selection.ComputeAndSetWaste(/*min_viable_change=*/0, change_cost, /*change_fee=*/0);
953+
BOOST_CHECK_EQUAL(target_waste2, selection.GetWaste());
954+
}
924955
}
925956

926957
BOOST_AUTO_TEST_CASE(effective_value_test)

0 commit comments

Comments
 (0)