Skip to content

Commit efbf4e7

Browse files
committed
Merge bitcoin/bitcoin#29523: Wallet: Add max_tx_weight to transaction funding options (take 2)
734076c [wallet, rpc]: add `max_tx_weight` to tx funding options (ismaelsadeeq) b6fc504 [wallet]: update the data type of `change_output_size`, `change_spend_size` and `tx_noinputs_size` to `int` (ismaelsadeeq) baab0d2 [doc]: update reason for deducting change output weight (ismaelsadeeq) 7f61d31 [refactor]: update coin selection algorithms input parameter `max_weight` name (ismaelsadeeq) Pull request description: This PR taken over from #29264 The PR added an option `max_tx_weight` to transaction funding RPC's that ensures the resulting transaction weight does not exceed the specified `max_tx_weight` limit. If `max_tx_weight` is not given `MAX_STANDARD_TX_WEIGHT` is used as the max threshold. This PR addressed outstanding review comments in #29264 For more context and rationale behind this PR see https://delvingbitcoin.org/t/lightning-transactions-with-v3-and-ephemeral-anchors/418/11?u=instagibbs ACKs for top commit: achow101: ACK 734076c furszy: utACK 734076c rkrux: reACK [734076c](bitcoin/bitcoin@734076c) Tree-SHA512: 013501aa443d239ee2ac01bccfc5296490c27b4edebe5cfca6b96c842375e895e5cfeb5424e82e359be581460f8be92095855763a62779a18ccd5bdfdd7ddce7
2 parents 3679fa1 + 734076c commit efbf4e7

File tree

10 files changed

+178
-76
lines changed

10 files changed

+178
-76
lines changed

src/rpc/client.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
146146
{ "fundrawtransaction", 1, "conf_target"},
147147
{ "fundrawtransaction", 1, "replaceable"},
148148
{ "fundrawtransaction", 1, "solving_data"},
149+
{ "fundrawtransaction", 1, "max_tx_weight"},
149150
{ "fundrawtransaction", 2, "iswitness" },
150151
{ "walletcreatefundedpsbt", 0, "inputs" },
151152
{ "walletcreatefundedpsbt", 1, "outputs" },
@@ -164,6 +165,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
164165
{ "walletcreatefundedpsbt", 3, "conf_target"},
165166
{ "walletcreatefundedpsbt", 3, "replaceable"},
166167
{ "walletcreatefundedpsbt", 3, "solving_data"},
168+
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
167169
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
168170
{ "walletprocesspsbt", 1, "sign" },
169171
{ "walletprocesspsbt", 3, "bip32derivs" },
@@ -208,6 +210,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
208210
{ "send", 4, "conf_target"},
209211
{ "send", 4, "replaceable"},
210212
{ "send", 4, "solving_data"},
213+
{ "send", 4, "max_tx_weight"},
211214
{ "sendall", 0, "recipients" },
212215
{ "sendall", 1, "conf_target" },
213216
{ "sendall", 3, "fee_rate"},

src/wallet/coincontrol.h

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ class CCoinControl
115115
std::optional<uint32_t> m_locktime;
116116
//! Version
117117
std::optional<uint32_t> m_version;
118+
//! Caps weight of resulting tx
119+
std::optional<int> m_max_tx_weight{std::nullopt};
118120

119121
CCoinControl();
120122

src/wallet/coinselection.cpp

+36-21
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ struct {
8484
* bound of the range.
8585
* @param const CAmount& cost_of_change This is the cost of creating and spending a change output.
8686
* This plus selection_target is the upper bound of the range.
87-
* @param int max_weight The maximum weight available for the input set.
87+
* @param int max_selection_weight The maximum allowed weight for a selection result to be valid.
8888
* @returns The result of this coin selection algorithm, or std::nullopt
8989
*/
9090

9191
static const size_t TOTAL_TRIES = 100000;
9292

9393
util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change,
94-
int max_weight)
94+
int max_selection_weight)
9595
{
9696
SelectionResult result(selection_target, SelectionAlgorithm::BNB);
9797
CAmount curr_value = 0;
@@ -128,7 +128,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
128128
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
129129
(curr_waste > best_waste && is_feerate_high)) { // Don't select things which we know will be more wasteful if the waste is increasing
130130
backtrack = true;
131-
} else if (curr_selection_weight > max_weight) { // Exceeding weight for standard tx, cannot find more solutions by adding more inputs
131+
} else if (curr_selection_weight > max_selection_weight) { // Selected UTXOs weight exceeds the maximum weight allowed, cannot find more solutions by adding more inputs
132132
max_tx_weight_exceeded = true; // at least one selection attempt exceeded the max weight
133133
backtrack = true;
134134
} else if (curr_value >= selection_target) { // Selected value is within range
@@ -319,10 +319,10 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
319319
* group with multiple as a heavier UTXO with the combined amount here.)
320320
* @param const CAmount& selection_target This is the minimum amount that we need for the transaction without considering change.
321321
* @param const CAmount& change_target The minimum budget for creating a change output, by which we increase the selection_target.
322-
* @param int max_weight The maximum permitted weight for the input set.
322+
* @param int max_selection_weight The maximum allowed weight for a selection result to be valid.
323323
* @returns The result of this coin selection algorithm, or std::nullopt
324324
*/
325-
util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, CAmount change_target, int max_weight)
325+
util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, CAmount change_target, int max_selection_weight)
326326
{
327327
std::sort(utxo_pool.begin(), utxo_pool.end(), descending_effval_weight);
328328
// The sum of UTXO amounts after this UTXO index, e.g. lookahead[5] = Σ(UTXO[6+].amount)
@@ -359,7 +359,7 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
359359

360360
// The weight of the currently selected input set, and the weight of the best selection
361361
int curr_weight = 0;
362-
int best_selection_weight = max_weight; // Tie is fine, because we prefer lower selection amount
362+
int best_selection_weight = max_selection_weight; // Tie is fine, because we prefer lower selection amount
363363

364364
// Whether the input sets generated during this search have exceeded the maximum transaction weight at any point
365365
bool max_tx_weight_exceeded = false;
@@ -436,8 +436,8 @@ util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, c
436436
// Insufficient funds with lookahead: CUT
437437
should_cut = true;
438438
} else if (curr_weight > best_selection_weight) {
439-
// best_selection_weight is initialized to max_weight
440-
if (curr_weight > max_weight) max_tx_weight_exceeded = true;
439+
// best_selection_weight is initialized to max_selection_weight
440+
if (curr_weight > max_selection_weight) max_tx_weight_exceeded = true;
441441
// Worse weight than best solution. More UTXOs only increase weight:
442442
// CUT if last selected group had minimal weight, else SHIFT
443443
if (utxo_pool[curr_tail].m_weight <= min_tail_weight[curr_tail]) {
@@ -535,7 +535,7 @@ class MinOutputGroupComparator
535535
};
536536

537537
util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng,
538-
int max_weight)
538+
int max_selection_weight)
539539
{
540540
SelectionResult result(target_value, SelectionAlgorithm::SRD);
541541
std::priority_queue<OutputGroup, std::vector<OutputGroup>, MinOutputGroupComparator> heap;
@@ -565,14 +565,14 @@ util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utx
565565

566566
// If the selection weight exceeds the maximum allowed size, remove the least valuable inputs until we
567567
// are below max weight.
568-
if (weight > max_weight) {
568+
if (weight > max_selection_weight) {
569569
max_tx_weight_exceeded = true; // mark it in case we don't find any useful result.
570570
do {
571571
const OutputGroup& to_remove_group = heap.top();
572572
selected_eff_value -= to_remove_group.GetSelectionAmount();
573573
weight -= to_remove_group.m_weight;
574574
heap.pop();
575-
} while (!heap.empty() && weight > max_weight);
575+
} while (!heap.empty() && weight > max_selection_weight);
576576
}
577577

578578
// Now check if we are above the target
@@ -597,11 +597,12 @@ util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utx
597597
* nTargetValue, with indices corresponding to groups. If the ith
598598
* entry is true, that means the ith group in groups was selected.
599599
* param@[out] nBest Total amount of subset chosen that is closest to nTargetValue.
600+
* paramp[in] max_selection_weight The maximum allowed weight for a selection result to be valid.
600601
* param@[in] iterations Maximum number of tries.
601602
*/
602603
static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::vector<OutputGroup>& groups,
603604
const CAmount& nTotalLower, const CAmount& nTargetValue,
604-
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000)
605+
std::vector<char>& vfBest, CAmount& nBest, int max_selection_weight, int iterations = 1000)
605606
{
606607
std::vector<char> vfIncluded;
607608

@@ -613,6 +614,7 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
613614
{
614615
vfIncluded.assign(groups.size(), false);
615616
CAmount nTotal = 0;
617+
int selected_coins_weight{0};
616618
bool fReachedTarget = false;
617619
for (int nPass = 0; nPass < 2 && !fReachedTarget; nPass++)
618620
{
@@ -627,9 +629,9 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
627629
if (nPass == 0 ? insecure_rand.randbool() : !vfIncluded[i])
628630
{
629631
nTotal += groups[i].GetSelectionAmount();
632+
selected_coins_weight += groups[i].m_weight;
630633
vfIncluded[i] = true;
631-
if (nTotal >= nTargetValue)
632-
{
634+
if (nTotal >= nTargetValue && selected_coins_weight <= max_selection_weight) {
633635
fReachedTarget = true;
634636
// If the total is between nTargetValue and nBest, it's our new best
635637
// approximation.
@@ -639,6 +641,7 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
639641
vfBest = vfIncluded;
640642
}
641643
nTotal -= groups[i].GetSelectionAmount();
644+
selected_coins_weight -= groups[i].m_weight;
642645
vfIncluded[i] = false;
643646
}
644647
}
@@ -648,10 +651,11 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
648651
}
649652

650653
util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
651-
CAmount change_target, FastRandomContext& rng, int max_weight)
654+
CAmount change_target, FastRandomContext& rng, int max_selection_weight)
652655
{
653656
SelectionResult result(nTargetValue, SelectionAlgorithm::KNAPSACK);
654657

658+
bool max_weight_exceeded{false};
655659
// List of values less than target
656660
std::optional<OutputGroup> lowest_larger;
657661
// Groups with selection amount smaller than the target and any change we might produce.
@@ -662,6 +666,10 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
662666
std::shuffle(groups.begin(), groups.end(), rng);
663667

664668
for (const OutputGroup& group : groups) {
669+
if (group.m_weight > max_selection_weight) {
670+
max_weight_exceeded = true;
671+
continue;
672+
}
665673
if (group.GetSelectionAmount() == nTargetValue) {
666674
result.AddInput(group);
667675
return result;
@@ -677,11 +685,18 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
677685
for (const auto& group : applicable_groups) {
678686
result.AddInput(group);
679687
}
680-
return result;
688+
if (result.GetWeight() <= max_selection_weight) return result;
689+
else max_weight_exceeded = true;
690+
691+
// Try something else
692+
result.Clear();
681693
}
682694

683695
if (nTotalLower < nTargetValue) {
684-
if (!lowest_larger) return util::Error();
696+
if (!lowest_larger) {
697+
if (max_weight_exceeded) return ErrorMaxWeightExceeded();
698+
return util::Error();
699+
}
685700
result.AddInput(*lowest_larger);
686701
return result;
687702
}
@@ -691,9 +706,9 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
691706
std::vector<char> vfBest;
692707
CAmount nBest;
693708

694-
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest);
709+
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest, max_selection_weight);
695710
if (nBest != nTargetValue && nTotalLower >= nTargetValue + change_target) {
696-
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + change_target, vfBest, nBest);
711+
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + change_target, vfBest, nBest, max_selection_weight);
697712
}
698713

699714
// If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
@@ -709,7 +724,7 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
709724
}
710725

711726
// If the result exceeds the maximum allowed size, return closest UTXO above the target
712-
if (result.GetWeight() > max_weight) {
727+
if (result.GetWeight() > max_selection_weight) {
713728
// No coin above target, nothing to do.
714729
if (!lowest_larger) return ErrorMaxWeightExceeded();
715730

@@ -728,7 +743,7 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
728743
LogPrint(BCLog::SELECTCOINS, "%stotal %s\n", log_message, FormatMoney(nBest));
729744
}
730745
}
731-
746+
Assume(result.GetWeight() <= max_selection_weight);
732747
return result;
733748
}
734749

src/wallet/coinselection.h

+15-11
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,9 @@ struct CoinSelectionParams {
139139
/** Randomness to use in the context of coin selection. */
140140
FastRandomContext& rng_fast;
141141
/** Size of a change output in bytes, determined by the output type. */
142-
size_t change_output_size = 0;
142+
int change_output_size = 0;
143143
/** Size of the input to spend a change output in virtual bytes. */
144-
size_t change_spend_size = 0;
144+
int change_spend_size = 0;
145145
/** Mininmum change to target in Knapsack solver and CoinGrinder:
146146
* select coins to cover the payment and at least this value of change. */
147147
CAmount m_min_change_target{0};
@@ -162,7 +162,7 @@ struct CoinSelectionParams {
162162
CFeeRate m_discard_feerate;
163163
/** Size of the transaction before coin selection, consisting of the header and recipient
164164
* output(s), excluding the inputs and change output(s). */
165-
size_t tx_noinputs_size = 0;
165+
int tx_noinputs_size = 0;
166166
/** Indicate that we are subtracting the fee from outputs */
167167
bool m_subtract_fee_outputs = false;
168168
/** When true, always spend all (up to OUTPUT_GROUP_MAX_ENTRIES) or none of the outputs
@@ -174,10 +174,13 @@ struct CoinSelectionParams {
174174
* 1) Received from other wallets, 2) replacing other txs, 3) that have been replaced.
175175
*/
176176
bool m_include_unsafe_inputs = false;
177+
/** The maximum weight for this transaction. */
178+
std::optional<int> m_max_tx_weight{std::nullopt};
177179

178-
CoinSelectionParams(FastRandomContext& rng_fast, size_t change_output_size, size_t change_spend_size,
180+
CoinSelectionParams(FastRandomContext& rng_fast, int change_output_size, int change_spend_size,
179181
CAmount min_change_target, CFeeRate effective_feerate,
180-
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial)
182+
CFeeRate long_term_feerate, CFeeRate discard_feerate, int tx_noinputs_size, bool avoid_partial,
183+
std::optional<int> max_tx_weight = std::nullopt)
181184
: rng_fast{rng_fast},
182185
change_output_size(change_output_size),
183186
change_spend_size(change_spend_size),
@@ -186,7 +189,8 @@ struct CoinSelectionParams {
186189
m_long_term_feerate(long_term_feerate),
187190
m_discard_feerate(discard_feerate),
188191
tx_noinputs_size(tx_noinputs_size),
189-
m_avoid_partial_spends(avoid_partial)
192+
m_avoid_partial_spends(avoid_partial),
193+
m_max_tx_weight(max_tx_weight)
190194
{
191195
}
192196
CoinSelectionParams(FastRandomContext& rng_fast)
@@ -440,25 +444,25 @@ struct SelectionResult
440444
};
441445

442446
util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change,
443-
int max_weight);
447+
int max_selection_weight);
444448

445-
util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, CAmount change_target, int max_weight);
449+
util::Result<SelectionResult> CoinGrinder(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, CAmount change_target, int max_selection_weight);
446450

447451
/** Select coins by Single Random Draw. OutputGroups are selected randomly from the eligible
448452
* outputs until the target is satisfied
449453
*
450454
* @param[in] utxo_pool The positive effective value OutputGroups eligible for selection
451455
* @param[in] target_value The target value to select for
452456
* @param[in] rng The randomness source to shuffle coins
453-
* @param[in] max_weight The maximum allowed weight for a selection result to be valid
457+
* @param[in] max_selection_weight The maximum allowed weight for a selection result to be valid
454458
* @returns If successful, a valid SelectionResult, otherwise, util::Error
455459
*/
456460
util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng,
457-
int max_weight);
461+
int max_selection_weight);
458462

459463
// Original coin selection algorithm as a fallback
460464
util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
461-
CAmount change_target, FastRandomContext& rng, int max_weight);
465+
CAmount change_target, FastRandomContext& rng, int max_selection_weight);
462466
} // namespace wallet
463467

464468
#endif // BITCOIN_WALLET_COINSELECTION_H

0 commit comments

Comments
 (0)