Skip to content

Commit 701b0cf

Browse files
committed
Merge bitcoin#28366: Fix waste calculation in SelectionResult
bd34dd8 Use `exact_target` shorthand in coinselector_tests (Murch) 7aa7e30 Fold GetSelectionWaste() into ComputeAndSetWaste() (Murch) Pull request description: PR bitcoin#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: ACK bd34dd8 furszy: Code ACK bd34dd8 ismaelsadeeq: Code Review ACK bd34dd8 Tree-SHA512: 83a2688d45d719dc61a64b5180fe136107faccf401a59df65245c05d701748a03e85ed56fde8c9b7ef39a3ab54374dd3718c559bda5b3f55dafedfd7fed25161
2 parents d39f15a + bd34dd8 commit 701b0cf

File tree

6 files changed

+88
-101
lines changed

6 files changed

+88
-101
lines changed

src/bench/coin_selection.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@ static void CoinSelection(benchmark::Bench& bench)
7171
/*change_output_size=*/ 34,
7272
/*change_spend_size=*/ 148,
7373
/*min_change_target=*/ CHANGE_LOWER,
74-
/*effective_feerate=*/ CFeeRate(0),
75-
/*long_term_feerate=*/ CFeeRate(0),
76-
/*discard_feerate=*/ CFeeRate(0),
74+
/*effective_feerate=*/ CFeeRate(20'000),
75+
/*long_term_feerate=*/ CFeeRate(10'000),
76+
/*discard_feerate=*/ CFeeRate(3000),
7777
/*tx_noinputs_size=*/ 0,
7878
/*avoid_partial=*/ false,
7979
};
8080
auto group = wallet::GroupOutputs(wallet, available_coins, coin_selection_params, {{filter_standard}})[filter_standard];
8181
bench.run([&] {
82-
auto result = AttemptSelection(wallet.chain(), 1003 * COIN, group, coin_selection_params, /*allow_mixed_output_types=*/true);
82+
auto result = AttemptSelection(wallet.chain(), 1002.99 * COIN, group, coin_selection_params, /*allow_mixed_output_types=*/true);
8383
assert(result);
8484
assert(result->GetSelectedValue() == 1003 * COIN);
8585
assert(result->GetInputSet().size() == 2);

src/wallet/coinselection.cpp

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
194194
for (const size_t& i : best_selection) {
195195
result.AddInput(utxo_pool.at(i));
196196
}
197-
result.ComputeAndSetWaste(cost_of_change, cost_of_change, CAmount{0});
197+
result.RecalculateWaste(cost_of_change, cost_of_change, CAmount{0});
198198
assert(best_waste == result.GetWaste());
199199

200200
return result;
@@ -792,35 +792,6 @@ void OutputGroupTypeMap::Push(const OutputGroup& group, OutputType type, bool in
792792
}
793793
}
794794

795-
CAmount SelectionResult::GetSelectionWaste(CAmount change_cost, CAmount target, bool use_effective_value)
796-
{
797-
// This function should not be called with empty inputs as that would mean the selection failed
798-
assert(!m_selected_inputs.empty());
799-
800-
// Always consider the cost of spending an input now vs in the future.
801-
CAmount waste = 0;
802-
for (const auto& coin_ptr : m_selected_inputs) {
803-
const COutput& coin = *coin_ptr;
804-
waste += coin.GetFee() - coin.long_term_fee;
805-
}
806-
// Bump fee of whole selection may diverge from sum of individual bump fees
807-
waste -= bump_fee_group_discount;
808-
809-
if (change_cost) {
810-
// Consider the cost of making change and spending it in the future
811-
// If we aren't making change, the caller should've set change_cost to 0
812-
assert(change_cost > 0);
813-
waste += change_cost;
814-
} else {
815-
// When we are not making change (change_cost == 0), consider the excess we are throwing away to fees
816-
CAmount selected_effective_value = use_effective_value ? GetSelectedEffectiveValue() : GetSelectedValue();
817-
assert(selected_effective_value >= target);
818-
waste += selected_effective_value - target;
819-
}
820-
821-
return waste;
822-
}
823-
824795
CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng)
825796
{
826797
if (payment_value <= CHANGE_LOWER / 2) {
@@ -839,16 +810,32 @@ void SelectionResult::SetBumpFeeDiscount(const CAmount discount)
839810
bump_fee_group_discount = discount;
840811
}
841812

842-
843-
void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee)
813+
void SelectionResult::RecalculateWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee)
844814
{
845-
const CAmount change = GetChange(min_viable_change, change_fee);
815+
// This function should not be called with empty inputs as that would mean the selection failed
816+
assert(!m_selected_inputs.empty());
817+
818+
// Always consider the cost of spending an input now vs in the future.
819+
CAmount waste = 0;
820+
for (const auto& coin_ptr : m_selected_inputs) {
821+
const COutput& coin = *coin_ptr;
822+
waste += coin.GetFee() - coin.long_term_fee;
823+
}
824+
// Bump fee of whole selection may diverge from sum of individual bump fees
825+
waste -= bump_fee_group_discount;
846826

847-
if (change > 0) {
848-
m_waste = GetSelectionWaste(change_cost, m_target, m_use_effective);
827+
if (GetChange(min_viable_change, change_fee)) {
828+
// if we have a minimum viable amount after deducting fees, account for
829+
// cost of creating and spending change
830+
waste += change_cost;
849831
} else {
850-
m_waste = GetSelectionWaste(0, m_target, m_use_effective);
832+
// When we are not making change (GetChange(…) == 0), consider the excess we are throwing away to fees
833+
CAmount selected_effective_value = m_use_effective ? GetSelectedEffectiveValue() : GetSelectedValue();
834+
assert(selected_effective_value >= m_target);
835+
waste += selected_effective_value - m_target;
851836
}
837+
838+
m_waste = waste;
852839
}
853840

854841
void SelectionResult::SetAlgoCompleted(bool algo_completed)

src/wallet/coinselection.h

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -350,22 +350,6 @@ struct SelectionResult
350350
}
351351
}
352352

353-
/** Compute the waste for this result given the cost of change
354-
* and the opportunity cost of spending these inputs now vs in the future.
355-
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate)
356-
* If no change, waste = excess + inputs * (effective_feerate - long_term_feerate)
357-
* where excess = selected_effective_value - target
358-
* change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size
359-
*
360-
* @param[in] change_cost The cost of creating change and spending it in the future.
361-
* Only used if there is change, in which case it must be positive.
362-
* Must be 0 if there is no change.
363-
* @param[in] target The amount targeted by the coin selection algorithm.
364-
* @param[in] use_effective_value Whether to use the input's effective value (when true) or the real value (when false).
365-
* @return The waste
366-
*/
367-
[[nodiscard]] CAmount GetSelectionWaste(CAmount change_cost, CAmount target, bool use_effective_value = true);
368-
369353
public:
370354
explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
371355
: m_target(target), m_algo(algo) {}
@@ -387,8 +371,19 @@ struct SelectionResult
387371
/** How much individual inputs overestimated the bump fees for shared ancestries */
388372
void SetBumpFeeDiscount(const CAmount discount);
389373

390-
/** Calculates and stores the waste for this selection via GetSelectionWaste */
391-
void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee);
374+
/** Calculates and stores the waste for this result given the cost of change
375+
* and the opportunity cost of spending these inputs now vs in the future.
376+
* If change exists, waste = change_cost + inputs * (effective_feerate - long_term_feerate) - bump_fee_group_discount
377+
* If no change, waste = excess + inputs * (effective_feerate - long_term_feerate) - bump_fee_group_discount
378+
* where excess = selected_effective_value - target
379+
* change_cost = effective_feerate * change_output_size + long_term_feerate * change_spend_size
380+
*
381+
* @param[in] min_viable_change The minimum amount necessary to make a change output economic
382+
* @param[in] change_cost The cost of creating a change output and spending it in the future. Only
383+
* used if there is change, in which case it must be non-negative.
384+
* @param[in] change_fee The fee for creating a change output
385+
*/
386+
void RecalculateWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee);
392387
[[nodiscard]] CAmount GetWaste() const;
393388

394389
/** Tracks that algorithm was able to exhaustively search the entire combination space before hitting limit of tries */

src/wallet/spend.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
711711

712712
if (coin_selection_params.m_effective_feerate > CFeeRate{3 * coin_selection_params.m_long_term_feerate}) { // Minimize input set for feerates of at least 3×LTFRE (default: 30 ṩ/vB+)
713713
if (auto cg_result{CoinGrinder(groups.positive_group, nTargetValue, coin_selection_params.m_min_change_target, max_inputs_weight)}) {
714-
cg_result->ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
714+
cg_result->RecalculateWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
715715
results.push_back(*cg_result);
716716
} else {
717717
append_error(std::move(cg_result));
@@ -746,7 +746,7 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
746746
if (bump_fee_overestimate) {
747747
result.SetBumpFeeDiscount(bump_fee_overestimate);
748748
}
749-
result.ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
749+
result.RecalculateWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
750750
}
751751

752752
// Choose the result with the least waste
@@ -771,7 +771,7 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
771771
if (selection_target <= 0) {
772772
SelectionResult result(nTargetValue, SelectionAlgorithm::MANUAL);
773773
result.AddInputs(pre_set_inputs.coins, coin_selection_params.m_subtract_fee_outputs);
774-
result.ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
774+
result.RecalculateWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
775775
return result;
776776
}
777777

@@ -792,7 +792,7 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
792792
SelectionResult preselected(pre_set_inputs.total_amount, SelectionAlgorithm::MANUAL);
793793
preselected.AddInputs(pre_set_inputs.coins, coin_selection_params.m_subtract_fee_outputs);
794794
op_selection_result->Merge(preselected);
795-
op_selection_result->ComputeAndSetWaste(coin_selection_params.min_viable_change,
795+
op_selection_result->RecalculateWaste(coin_selection_params.min_viable_change,
796796
coin_selection_params.m_cost_of_change,
797797
coin_selection_params.m_change_fee);
798798
}

0 commit comments

Comments
 (0)