Skip to content

Commit 734076c

Browse files
[wallet, rpc]: add max_tx_weight to tx funding options
This allows a transaction's weight to be bound under a certain weight if possible and desired. This can be beneficial for future RBF attempts, or whenever a more restricted spend topology is desired. Co-authored-by: Greg Sanders <[email protected]>
1 parent b6fc504 commit 734076c

File tree

9 files changed

+119
-17
lines changed

9 files changed

+119
-17
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

+23-8
Original file line numberDiff line numberDiff line change
@@ -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
}
@@ -652,6 +655,7 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
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
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,
@@ -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

+6-2
Original file line numberDiff line numberDiff line change
@@ -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

178180
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, int 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)

src/wallet/rpc/spend.cpp

+14
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
542542
{"minconf", UniValueType(UniValue::VNUM)},
543543
{"maxconf", UniValueType(UniValue::VNUM)},
544544
{"input_weights", UniValueType(UniValue::VARR)},
545+
{"max_tx_weight", UniValueType(UniValue::VNUM)},
545546
},
546547
true, true);
547548

@@ -701,6 +702,10 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
701702
}
702703
}
703704

705+
if (options.exists("max_tx_weight")) {
706+
coinControl.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
707+
}
708+
704709
if (recipients.empty())
705710
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");
706711

@@ -786,6 +791,8 @@ RPCHelpMan fundrawtransaction()
786791
},
787792
},
788793
},
794+
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
795+
"Transaction building will fail if this can not be satisfied."},
789796
},
790797
FundTxDoc()),
791798
RPCArgOptions{
@@ -1240,6 +1247,8 @@ RPCHelpMan send()
12401247
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
12411248
},
12421249
},
1250+
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
1251+
"Transaction building will fail if this can not be satisfied."},
12431252
},
12441253
FundTxDoc()),
12451254
RPCArgOptions{.oneline_description="options"}},
@@ -1287,6 +1296,9 @@ RPCHelpMan send()
12871296
// Automatically select coins, unless at least one is manually selected. Can
12881297
// be overridden by options.add_inputs.
12891298
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
1299+
if (options.exists("max_tx_weight")) {
1300+
coin_control.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
1301+
}
12901302
SetOptionsInputWeights(options["inputs"], options);
12911303
// Clear tx.vout since it is not meant to be used now that we are passing outputs directly.
12921304
// This sets us up for a future PR to completely remove tx from the function signature in favor of passing inputs directly
@@ -1697,6 +1709,8 @@ RPCHelpMan walletcreatefundedpsbt()
16971709
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
16981710
},
16991711
},
1712+
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
1713+
"Transaction building will fail if this can not be satisfied."},
17001714
},
17011715
FundTxDoc()),
17021716
RPCArgOptions{.oneline_description="options"}},

src/wallet/spend.cpp

+17-4
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,12 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
696696
};
697697

698698
// Maximum allowed weight for selected coins.
699-
int max_selection_weight = MAX_STANDARD_TX_WEIGHT - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
699+
int max_transaction_weight = coin_selection_params.m_max_tx_weight.value_or(MAX_STANDARD_TX_WEIGHT);
700+
int tx_weight_no_input = coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR;
701+
int max_selection_weight = max_transaction_weight - tx_weight_no_input;
702+
if (max_selection_weight <= 0) {
703+
return util::Error{_("Maximum transaction weight is less than transaction weight without inputs")};
704+
}
700705

701706
// SFFO frequently causes issues in the context of changeless input sets: skip BnB when SFFO is active
702707
if (!coin_selection_params.m_subtract_fee_outputs) {
@@ -706,7 +711,11 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
706711
}
707712

708713
// Deduct change weight because remaining Coin Selection algorithms can create change output
709-
max_selection_weight -= (coin_selection_params.change_output_size * WITNESS_SCALE_FACTOR);
714+
int change_outputs_weight = coin_selection_params.change_output_size * WITNESS_SCALE_FACTOR;
715+
max_selection_weight -= change_outputs_weight;
716+
if (max_selection_weight < 0 && results.empty()) {
717+
return util::Error{_("Maximum transaction weight is too low, can not accommodate change output")};
718+
}
710719

711720
// The knapsack solver has some legacy behavior where it will spend dust outputs. We retain this behavior, so don't filter for positive only here.
712721
if (auto knapsack_result{KnapsackSolver(groups.mixed_group, nTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast, max_selection_weight)}) {
@@ -801,7 +810,7 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
801810
coin_selection_params.m_change_fee);
802811

803812
// Verify we haven't exceeded the maximum allowed weight
804-
int max_inputs_weight = MAX_STANDARD_TX_WEIGHT - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
813+
int max_inputs_weight = coin_selection_params.m_max_tx_weight.value_or(MAX_STANDARD_TX_WEIGHT) - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
805814
if (op_selection_result->GetWeight() > max_inputs_weight) {
806815
return util::Error{_("The combination of the pre-selected inputs and the wallet automatic inputs selection exceeds the transaction maximum weight. "
807816
"Please try sending a smaller amount or manually consolidating your wallet's UTXOs")};
@@ -1002,7 +1011,11 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
10021011
CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy
10031012
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
10041013
coin_selection_params.m_include_unsafe_inputs = coin_control.m_include_unsafe_inputs;
1005-
1014+
coin_selection_params.m_max_tx_weight = coin_control.m_max_tx_weight.value_or(MAX_STANDARD_TX_WEIGHT);
1015+
int minimum_tx_weight = MIN_STANDARD_TX_NONWITNESS_SIZE * WITNESS_SCALE_FACTOR;
1016+
if (coin_selection_params.m_max_tx_weight.value() < minimum_tx_weight || coin_selection_params.m_max_tx_weight.value() > MAX_STANDARD_TX_WEIGHT) {
1017+
return util::Error{strprintf(_("Maximum transaction weight must be between %d and %d"), minimum_tx_weight, MAX_STANDARD_TX_WEIGHT)};
1018+
}
10061019
// Set the long term feerate estimate to the wallet's consolidate feerate
10071020
coin_selection_params.m_long_term_feerate = wallet.m_consolidate_feerate;
10081021

src/wallet/test/fuzz/coinselection.cpp

+8-3
Original file line numberDiff line numberDiff line change
@@ -256,29 +256,34 @@ FUZZ_TARGET(coinselection)
256256
(void)group.EligibleForSpending(filter);
257257
}
258258

259+
int max_selection_weight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, std::numeric_limits<int>::max());
260+
259261
// Run coinselection algorithms
260262
auto result_bnb = coin_params.m_subtract_fee_outputs ? util::Error{Untranslated("BnB disabled when SFFO is enabled")} :
261-
SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, MAX_STANDARD_TX_WEIGHT);
263+
SelectCoinsBnB(group_pos, target, coin_params.m_cost_of_change, max_selection_weight);
262264
if (result_bnb) {
263265
assert(result_bnb->GetChange(coin_params.min_viable_change, coin_params.m_change_fee) == 0);
264266
assert(result_bnb->GetSelectedValue() >= target);
267+
assert(result_bnb->GetWeight() <= max_selection_weight);
265268
(void)result_bnb->GetShuffledInputVector();
266269
(void)result_bnb->GetInputSet();
267270
}
268271

269-
auto result_srd = SelectCoinsSRD(group_pos, target, coin_params.m_change_fee, fast_random_context, MAX_STANDARD_TX_WEIGHT);
272+
auto result_srd = SelectCoinsSRD(group_pos, target, coin_params.m_change_fee, fast_random_context, max_selection_weight);
270273
if (result_srd) {
271274
assert(result_srd->GetSelectedValue() >= target);
272275
assert(result_srd->GetChange(CHANGE_LOWER, coin_params.m_change_fee) > 0); // Demonstrate that SRD creates change of at least CHANGE_LOWER
276+
assert(result_srd->GetWeight() <= max_selection_weight);
273277
result_srd->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
274278
(void)result_srd->GetShuffledInputVector();
275279
(void)result_srd->GetInputSet();
276280
}
277281

278282
CAmount change_target{GenerateChangeTarget(target, coin_params.m_change_fee, fast_random_context)};
279-
auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context, MAX_STANDARD_TX_WEIGHT);
283+
auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context, max_selection_weight);
280284
if (result_knapsack) {
281285
assert(result_knapsack->GetSelectedValue() >= target);
286+
assert(result_knapsack->GetWeight() <= max_selection_weight);
282287
result_knapsack->RecalculateWaste(coin_params.min_viable_change, coin_params.m_cost_of_change, coin_params.m_change_fee);
283288
(void)result_knapsack->GetShuffledInputVector();
284289
(void)result_knapsack->GetInputSet();

0 commit comments

Comments
 (0)