Skip to content

Commit 2bd9aa5

Browse files
committed
Merge bitcoin#25647: wallet: return change from SelectionResult
4fef534 wallet: use GetChange() when computing waste (S3RK) 87e0ef9 wallet: use GetChange() in tx building (S3RK) 15e97a6 wallet: add SelectionResult::GetChange (S3RK) 72cad28 wallet: calculate and store min_viable_change (S3RK) e3210a7 wallet: account for preselected inputs in target (S3RK) f8e7963 wallet: add SelectionResult::Merge (S3RK) 06f558e wallet: accurate SelectionResult::m_target (S3RK) c8cf08e wallet: ensure m_min_change_target always covers change fee (S3RK) Pull request description: Benefits: 1. more accurate waste calculation for knapsack. Waste calculation is now consistent with tx building code. Before we always assumed change for knapsack even when the solution is changeless4. 2. simpler tx building code. Only create change output when it's needed 3. makes it easier to correctly account for fees for CPFP inputs (should be done in a follow up) In the first three commits we fix the code to accurately track selection target in `SelectionResult::m_target` Then we introduce new variable `min_change` that represents the minimum viable change amount Then we introduce `SelectionResult::GetChange()` which incapsulates dropping change for fee logic and uses correct values of `SelectionResult::m_target` Then we use `SelectionResult::GetChange()` in both tx building and waste calculation code This PR is a refactoring and shouldn't change the behaviour. There is only one known small change (arguably a bug fix). Before we dropped change output if it's smaller than `cost_of_change` after paying change fees. This is incorrect as `cost_of_change` already includes `change_fee`. ACKs for top commit: achow101: ACK 4fef534 Xekyo: crACK 4fef534 furszy: Code review ACK 4fef534 w0xlt: ACK bitcoin@4fef534 Tree-SHA512: 31a7455d4129bc39a444da0f16ad478d690d4d9627b2b8fdb5605facc6488171926bf02f5d7d9a545b2b59efafcf5bb3d404005e4da15c7b44b3f7d441afb941
2 parents 92bb700 + 4fef534 commit 2bd9aa5

File tree

5 files changed

+153
-90
lines changed

5 files changed

+153
-90
lines changed

src/wallet/coinselection.cpp

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_poo
156156
for (const size_t& i : best_selection) {
157157
result.AddInput(utxo_pool.at(i));
158158
}
159-
result.ComputeAndSetWaste(CAmount{0});
159+
result.ComputeAndSetWaste(cost_of_change, cost_of_change, CAmount{0});
160160
assert(best_waste == result.GetWaste());
161161

162162
return result;
@@ -166,6 +166,12 @@ std::optional<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& ut
166166
{
167167
SelectionResult result(target_value, SelectionAlgorithm::SRD);
168168

169+
// Include change for SRD as we want to avoid making really small change if the selection just
170+
// barely meets the target. Just use the lower bound change target instead of the randomly
171+
// generated one, since SRD will result in a random change amount anyway; avoid making the
172+
// target needlessly large.
173+
target_value += CHANGE_LOWER;
174+
169175
std::vector<size_t> indexes;
170176
indexes.resize(utxo_pool.size());
171177
std::iota(indexes.begin(), indexes.end(), 0);
@@ -389,20 +395,26 @@ CAmount GetSelectionWaste(const std::set<COutput>& inputs, CAmount change_cost,
389395
return waste;
390396
}
391397

392-
CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng)
398+
CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng)
393399
{
394400
if (payment_value <= CHANGE_LOWER / 2) {
395-
return CHANGE_LOWER;
401+
return change_fee + CHANGE_LOWER;
396402
} else {
397403
// random value between 50ksat and min (payment_value * 2, 1milsat)
398404
const auto upper_bound = std::min(payment_value * 2, CHANGE_UPPER);
399-
return rng.randrange(upper_bound - CHANGE_LOWER) + CHANGE_LOWER;
405+
return change_fee + rng.randrange(upper_bound - CHANGE_LOWER) + CHANGE_LOWER;
400406
}
401407
}
402408

403-
void SelectionResult::ComputeAndSetWaste(CAmount change_cost)
409+
void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee)
404410
{
405-
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
411+
const CAmount change = GetChange(min_viable_change, change_fee);
412+
413+
if (change > 0) {
414+
m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective);
415+
} else {
416+
m_waste = GetSelectionWaste(m_selected_inputs, 0, m_target, m_use_effective);
417+
}
406418
}
407419

408420
CAmount SelectionResult::GetWaste() const
@@ -415,6 +427,11 @@ CAmount SelectionResult::GetSelectedValue() const
415427
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.txout.nValue; });
416428
}
417429

430+
CAmount SelectionResult::GetSelectedEffectiveValue() const
431+
{
432+
return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.GetEffectiveValue(); });
433+
}
434+
418435
void SelectionResult::Clear()
419436
{
420437
m_selected_inputs.clear();
@@ -427,6 +444,16 @@ void SelectionResult::AddInput(const OutputGroup& group)
427444
m_use_effective = !group.m_subtract_fee_outputs;
428445
}
429446

447+
void SelectionResult::Merge(const SelectionResult& other)
448+
{
449+
m_target += other.m_target;
450+
m_use_effective |= other.m_use_effective;
451+
if (m_algo == SelectionAlgorithm::MANUAL) {
452+
m_algo = other.m_algo;
453+
}
454+
util::insert(m_selected_inputs, other.m_selected_inputs);
455+
}
456+
430457
const std::set<COutput>& SelectionResult::GetInputSet() const
431458
{
432459
return m_selected_inputs;
@@ -464,4 +491,24 @@ std::string GetAlgorithmName(const SelectionAlgorithm algo)
464491
}
465492
assert(false);
466493
}
494+
495+
CAmount SelectionResult::GetChange(const CAmount min_viable_change, const CAmount change_fee) const
496+
{
497+
// change = SUM(inputs) - SUM(outputs) - fees
498+
// 1) With SFFO we don't pay any fees
499+
// 2) Otherwise we pay all the fees:
500+
// - input fees are covered by GetSelectedEffectiveValue()
501+
// - non_input_fee is included in m_target
502+
// - change_fee
503+
const CAmount change = m_use_effective
504+
? GetSelectedEffectiveValue() - m_target - change_fee
505+
: GetSelectedValue() - m_target;
506+
507+
if (change < min_viable_change) {
508+
return 0;
509+
}
510+
511+
return change;
512+
}
513+
467514
} // namespace wallet

src/wallet/coinselection.h

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ struct CoinSelectionParams {
123123
/** Mininmum change to target in Knapsack solver: select coins to cover the payment and
124124
* at least this value of change. */
125125
CAmount m_min_change_target{0};
126+
/** Minimum amount for creating a change output.
127+
* If change budget is smaller than min_change then we forgo creation of change output.
128+
*/
129+
CAmount min_viable_change{0};
126130
/** Cost of creating the change output. */
127131
CAmount m_change_fee{0};
128132
/** Cost of creating the change output + cost of spending the change output in the future. */
@@ -252,6 +256,7 @@ struct OutputGroup
252256

253257
/** Choose a random change target for each transaction to make it harder to fingerprint the Core
254258
* wallet based on the change output values of transactions it creates.
259+
* Change target covers at least change fees and adds a random value on top of it.
255260
* The random value is between 50ksat and min(2 * payment_value, 1milsat)
256261
* When payment_value <= 25ksat, the value is just 50ksat.
257262
*
@@ -261,8 +266,9 @@ struct OutputGroup
261266
* coins selected are just sufficient to cover the payment amount ("unnecessary input" heuristic).
262267
*
263268
* @param[in] payment_value Average payment value of the transaction output(s).
269+
* @param[in] change_fee Fee for creating a change output.
264270
*/
265-
[[nodiscard]] CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng);
271+
[[nodiscard]] CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng);
266272

267273
enum class SelectionAlgorithm : uint8_t
268274
{
@@ -279,17 +285,16 @@ struct SelectionResult
279285
private:
280286
/** Set of inputs selected by the algorithm to use in the transaction */
281287
std::set<COutput> m_selected_inputs;
288+
/** The target the algorithm selected for. Equal to the recipient amount plus non-input fees */
289+
CAmount m_target;
290+
/** The algorithm used to produce this result */
291+
SelectionAlgorithm m_algo;
282292
/** Whether the input values for calculations should be the effective value (true) or normal value (false) */
283293
bool m_use_effective{false};
284294
/** The computed waste */
285295
std::optional<CAmount> m_waste;
286296

287297
public:
288-
/** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */
289-
const CAmount m_target;
290-
/** The algorithm used to produce this result */
291-
const SelectionAlgorithm m_algo;
292-
293298
explicit SelectionResult(const CAmount target, SelectionAlgorithm algo)
294299
: m_target(target), m_algo(algo) {}
295300

@@ -298,20 +303,47 @@ struct SelectionResult
298303
/** Get the sum of the input values */
299304
[[nodiscard]] CAmount GetSelectedValue() const;
300305

306+
[[nodiscard]] CAmount GetSelectedEffectiveValue() const;
307+
301308
void Clear();
302309

303310
void AddInput(const OutputGroup& group);
304311

305312
/** Calculates and stores the waste for this selection via GetSelectionWaste */
306-
void ComputeAndSetWaste(CAmount change_cost);
313+
void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee);
307314
[[nodiscard]] CAmount GetWaste() const;
308315

316+
void Merge(const SelectionResult& other);
317+
309318
/** Get m_selected_inputs */
310319
const std::set<COutput>& GetInputSet() const;
311320
/** Get the vector of COutputs that will be used to fill in a CTransaction's vin */
312321
std::vector<COutput> GetShuffledInputVector() const;
313322

314323
bool operator<(SelectionResult other) const;
324+
325+
/** Get the amount for the change output after paying needed fees.
326+
*
327+
* The change amount is not 100% precise due to discrepancies in fee calculation.
328+
* The final change amount (if any) should be corrected after calculating the final tx fees.
329+
* When there is a discrepancy, most of the time the final change would be slightly bigger than estimated.
330+
*
331+
* Following are the possible factors of discrepancy:
332+
* + non-input fees always include segwit flags
333+
* + input fee estimation always include segwit stack size
334+
* + input fees are rounded individually and not collectively, which leads to small rounding errors
335+
* - input counter size is always assumed to be 1vbyte
336+
*
337+
* @param[in] min_viable_change Minimum amount for change output, if change would be less then we forgo change
338+
* @param[in] change_fee Fees to include change output in the tx
339+
* @returns Amount for change output, 0 when there is no change.
340+
*
341+
*/
342+
CAmount GetChange(const CAmount min_viable_change, const CAmount change_fee) const;
343+
344+
CAmount GetTarget() const { return m_target; }
345+
346+
SelectionAlgorithm GetAlgo() const { return m_algo; }
315347
};
316348

317349
std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change);

0 commit comments

Comments
 (0)