Skip to content

Commit

Permalink
Add max_tx_weight to transaction funding options
Browse files Browse the repository at this point in the history
This allows a transaction's weight to be bound under
 a certain weight if possible an desired. This
can be beneficial for future RBF attempts, or
whenever a more restricted spend topology is
desired.
  • Loading branch information
instagibbs committed Jan 19, 2024
1 parent c818607 commit 7bfc80b
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 24 deletions.
4 changes: 4 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "fundrawtransaction", 1, "conf_target"},
{ "fundrawtransaction", 1, "replaceable"},
{ "fundrawtransaction", 1, "solving_data"},
{ "fundrawtransaction", 1, "max_tx_weight"},
{ "fundrawtransaction", 2, "iswitness" },
{ "walletcreatefundedpsbt", 0, "inputs" },
{ "walletcreatefundedpsbt", 1, "outputs" },
Expand All @@ -162,6 +163,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "walletcreatefundedpsbt", 3, "conf_target"},
{ "walletcreatefundedpsbt", 3, "replaceable"},
{ "walletcreatefundedpsbt", 3, "solving_data"},
{ "walletcreatefundedpsbt", 3, "max_tx_weight"},
{ "walletcreatefundedpsbt", 4, "bip32derivs" },
{ "walletprocesspsbt", 1, "sign" },
{ "walletprocesspsbt", 3, "bip32derivs" },
Expand Down Expand Up @@ -206,6 +208,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "send", 4, "conf_target"},
{ "send", 4, "replaceable"},
{ "send", 4, "solving_data"},
{ "send", 4, "max_tx_weight"},
{ "sendall", 0, "recipients" },
{ "sendall", 1, "conf_target" },
{ "sendall", 3, "fee_rate"},
Expand All @@ -223,6 +226,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "sendall", 4, "conf_target"},
{ "sendall", 4, "replaceable"},
{ "sendall", 4, "solving_data"},
{ "sendall", 4, "max_tx_weight"},
{ "simulaterawtransaction", 0, "rawtxs" },
{ "simulaterawtransaction", 1, "options" },
{ "simulaterawtransaction", 1, "include_watchonly"},
Expand Down
3 changes: 3 additions & 0 deletions src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <outputtype.h>
#include <policy/feerate.h>
#include <policy/fees.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <script/keyorigin.h>
#include <script/signingprovider.h>
Expand Down Expand Up @@ -115,6 +116,8 @@ class CCoinControl
std::optional<uint32_t> m_locktime;
//! Version
std::optional<uint32_t> m_version;
//! Caps weight of resulting tx
int m_max_tx_weight{MAX_STANDARD_TX_WEIGHT};

CCoinControl();

Expand Down
43 changes: 26 additions & 17 deletions src/wallet/coinselection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ struct {
static const size_t TOTAL_TRIES = 100000;

util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change,
int max_weight)
int max_input_weight)
{
SelectionResult result(selection_target, SelectionAlgorithm::BNB);
CAmount curr_value = 0;
Expand Down Expand Up @@ -111,7 +111,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
(curr_waste > best_waste && is_feerate_high)) { // Don't select things which we know will be more wasteful if the waste is increasing
backtrack = true;
} else if (curr_selection_weight > max_weight) { // Exceeding weight for standard tx, cannot find more solutions by adding more inputs
} else if (curr_selection_weight > max_input_weight) { // Exceeding weight for standard tx, cannot find more solutions by adding more inputs
max_tx_weight_exceeded = true; // at least one selection attempt exceeded the max weight
backtrack = true;
} else if (curr_value >= selection_target) { // Selected value is within range
Expand Down Expand Up @@ -193,7 +193,7 @@ class MinOutputGroupComparator
};

util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utxo_pool, CAmount target_value, CAmount change_fee, FastRandomContext& rng,
int max_weight)
int max_input_weight)
{
SelectionResult result(target_value, SelectionAlgorithm::SRD);
std::priority_queue<OutputGroup, std::vector<OutputGroup>, MinOutputGroupComparator> heap;
Expand Down Expand Up @@ -223,14 +223,14 @@ util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utx

// If the selection weight exceeds the maximum allowed size, remove the least valuable inputs until we
// are below max weight.
if (weight > max_weight) {
if (weight > max_input_weight) {
max_tx_weight_exceeded = true; // mark it in case we don't find any useful result.
do {
const OutputGroup& to_remove_group = heap.top();
selected_eff_value -= to_remove_group.GetSelectionAmount();
weight -= to_remove_group.m_weight;
heap.pop();
} while (!heap.empty() && weight > max_weight);
} while (!heap.empty() && weight > max_input_weight);
}

// Now check if we are above the target
Expand Down Expand Up @@ -259,9 +259,10 @@ util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utx
*/
static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::vector<OutputGroup>& groups,
const CAmount& nTotalLower, const CAmount& nTargetValue,
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000)
std::vector<char>& vfBest, CAmount& nBest, int iterations = 1000, size_t max_input_weight = MAX_STANDARD_TX_WEIGHT)
{
std::vector<char> vfIncluded;
size_t total_input_weight{0};

// Worst case "best" approximation is just all of the groups.
vfBest.assign(groups.size(), true);
Expand All @@ -286,17 +287,19 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
{
nTotal += groups[i].GetSelectionAmount();
vfIncluded[i] = true;
total_input_weight += groups[i].m_weight;
if (nTotal >= nTargetValue)
{
fReachedTarget = true;
// If the total is between nTargetValue and nBest, it's our new best
// approximation.
if (nTotal < nBest)
if (nTotal < nBest && total_input_weight <= max_input_weight)
{
nBest = nTotal;
vfBest = vfIncluded;
}
nTotal -= groups[i].GetSelectionAmount();
total_input_weight -= groups[i].m_weight;
vfIncluded[i] = false;
}
}
Expand All @@ -306,7 +309,7 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
}

util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
CAmount change_target, FastRandomContext& rng, int max_weight)
CAmount change_target, FastRandomContext& rng, int max_input_weight)
{
SelectionResult result(nTargetValue, SelectionAlgorithm::KNAPSACK);

Expand All @@ -320,13 +323,13 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
Shuffle(groups.begin(), groups.end(), rng);

for (const OutputGroup& group : groups) {
if (group.GetSelectionAmount() == nTargetValue) {
if (group.GetSelectionAmount() == nTargetValue && group.m_weight <= max_input_weight) {
result.AddInput(group);
return result;
} else if (group.GetSelectionAmount() < nTargetValue + change_target) {
applicable_groups.push_back(group);
nTotalLower += group.GetSelectionAmount();
} else if (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount()) {
} else if (group.m_weight <= max_input_weight && (!lowest_larger || group.GetSelectionAmount() < lowest_larger->GetSelectionAmount())) {
lowest_larger = group;
}
}
Expand All @@ -335,23 +338,29 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
for (const auto& group : applicable_groups) {
result.AddInput(group);
}
return result;
if (result.GetWeight() <= max_input_weight) return result;

// Try something else
result.Clear();
}

if (nTotalLower < nTargetValue) {
if (!lowest_larger) return util::Error();
if (!lowest_larger) return util::Error{Untranslated(strprintf("Only %d coins to cover target value of %d", nTotalLower, nTargetValue))};
result.AddInput(*lowest_larger);
return result;
if (result.GetWeight() <= max_input_weight) return result;

// Try something else
result.Clear();
}

// Solve subset sum by stochastic approximation
std::sort(applicable_groups.begin(), applicable_groups.end(), descending);
std::vector<char> vfBest;
CAmount nBest;

ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest);
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue, vfBest, nBest, max_input_weight);
if (nBest != nTargetValue && nTotalLower >= nTargetValue + change_target) {
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + change_target, vfBest, nBest);
ApproximateBestSubset(rng, applicable_groups, nTotalLower, nTargetValue + change_target, vfBest, nBest, max_input_weight);
}

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

// If the result exceeds the maximum allowed size, return closest UTXO above the target
if (result.GetWeight() > max_weight) {
if (result.GetWeight() > max_input_weight) {
// No coin above target, nothing to do.
if (!lowest_larger) return ErrorMaxWeightExceeded();

Expand All @@ -386,7 +395,7 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
LogPrint(BCLog::SELECTCOINS, "%stotal %s\n", log_message, FormatMoney(nBest));
}
}

Assume(result.GetWeight() <= max_input_weight);
return result;
}

Expand Down
11 changes: 9 additions & 2 deletions src/wallet/coinselection.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <consensus/consensus.h>
#include <outputtype.h>
#include <policy/feerate.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <random.h>
#include <util/check.h>
Expand Down Expand Up @@ -174,10 +175,15 @@ struct CoinSelectionParams {
* 1) Received from other wallets, 2) replacing other txs, 3) that have been replaced.
*/
bool m_include_unsafe_inputs = false;
/**
* The maximally sized transaction we are willing to make.
*/
int32_t m_max_tx_weight{MAX_STANDARD_TX_WEIGHT};

CoinSelectionParams(FastRandomContext& rng_fast, size_t change_output_size, size_t change_spend_size,
CAmount min_change_target, CFeeRate effective_feerate,
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial)
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial,
int32_t max_tx_weight=MAX_STANDARD_TX_WEIGHT)
: rng_fast{rng_fast},
change_output_size(change_output_size),
change_spend_size(change_spend_size),
Expand All @@ -186,7 +192,8 @@ struct CoinSelectionParams {
m_long_term_feerate(long_term_feerate),
m_discard_feerate(discard_feerate),
tx_noinputs_size(tx_noinputs_size),
m_avoid_partial_spends(avoid_partial)
m_avoid_partial_spends(avoid_partial),
m_max_tx_weight(max_tx_weight)
{
}
CoinSelectionParams(FastRandomContext& rng_fast)
Expand Down
31 changes: 31 additions & 0 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
{"minconf", UniValueType(UniValue::VNUM)},
{"maxconf", UniValueType(UniValue::VNUM)},
{"input_weights", UniValueType(UniValue::VARR)},
{"max_tx_weight", UniValueType(UniValue::VNUM)},
},
true, true);

Expand Down Expand Up @@ -703,6 +704,14 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
}
}

if (options.exists("max_tx_weight")) {
coinControl.m_max_tx_weight = options["max_tx_weight"].getInt<int>();

if (coinControl.m_max_tx_weight < 0 || coinControl.m_max_tx_weight > MAX_STANDARD_TX_WEIGHT) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, max tx weight must be between 0 and %d", MAX_STANDARD_TX_WEIGHT));
}
}

if (tx.vout.size() == 0)
throw JSONRPCError(RPC_INVALID_PARAMETER, "TX must have at least one output");

Expand Down Expand Up @@ -799,6 +808,8 @@ RPCHelpMan fundrawtransaction()
},
},
},
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
"Transaction selection will fail if this can not be satisfied."},
},
FundTxDoc()),
RPCArgOptions{
Expand Down Expand Up @@ -1239,6 +1250,8 @@ RPCHelpMan send()
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
},
},
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
"Transaction selection will fail if this can not be satisfied."},
},
FundTxDoc()),
RPCArgOptions{.oneline_description="options"}},
Expand Down Expand Up @@ -1280,6 +1293,9 @@ RPCHelpMan send()
// Automatically select coins, unless at least one is manually selected. Can
// be overridden by options.add_inputs.
coin_control.m_allow_other_inputs = rawTx.vin.size() == 0;
if (options.exists("max_tx_weight")) {
coin_control.m_max_tx_weight = options["max_tx_weight"].getInt<int>();
}
SetOptionsInputWeights(options["inputs"], options);
auto txr = FundTransaction(*pwallet, rawTx, options, coin_control, /*override_min_fee=*/false);

Expand Down Expand Up @@ -1337,6 +1353,8 @@ RPCHelpMan sendall()
{"send_max", RPCArg::Type::BOOL, RPCArg::Default{false}, "When true, only use UTXOs that can pay for their own fees to maximize the output amount. When 'false' (default), no UTXO is left behind. send_max is incompatible with providing specific inputs."},
{"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Require inputs with at least this many confirmations."},
{"maxconf", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Require inputs with at most this many confirmations."},
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
"Transaction selection will fail if this can not be satisfied."},
},
FundTxDoc()
),
Expand Down Expand Up @@ -1472,6 +1490,17 @@ RPCHelpMan sendall()
const CAmount fee_from_size{fee_rate.GetFee(tx_size.vsize)};
const CAmount effective_value{total_input_value - fee_from_size};

// Cap tx weight based on estimated size if requested
if (options.exists("max_tx_weight")) {
const auto max_tx_weight = options["max_tx_weight"].getInt<int>();
if (max_tx_weight < 0 || max_tx_weight > MAX_STANDARD_TX_WEIGHT) {
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Invalid parameter, max tx weight must be between 0 %d", MAX_STANDARD_TX_WEIGHT));
}
if (max_tx_weight < tx_size.weight) {
throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Requested max tx weight exceeded: %d vs %s", options["max_tx_weight"].getInt<int>(), tx_size.weight));
}
}

if (fee_from_size > pwallet->m_default_max_tx_fee) {
throw JSONRPCError(RPC_WALLET_ERROR, TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED).original);
}
Expand Down Expand Up @@ -1679,6 +1708,8 @@ RPCHelpMan walletcreatefundedpsbt()
{"vout_index", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The zero-based output index, before a change output is added."},
},
},
{"max_tx_weight", RPCArg::Type::NUM, RPCArg::Default{MAX_STANDARD_TX_WEIGHT}, "The maximum acceptable transaction weight.\n"
"Transaction selection will fail if this can not be satisfied."},
},
FundTxDoc()),
RPCArgOptions{.oneline_description="options"}},
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ util::Result<SelectionResult> ChooseSelectionResult(interfaces::Chain& chain, co
};

// Maximum allowed weight
int max_inputs_weight = MAX_STANDARD_TX_WEIGHT - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
int max_inputs_weight = coin_selection_params.m_max_tx_weight - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);

// SFFO frequently causes issues in the context of changeless input sets: skip BnB when SFFO is active
if (!coin_selection_params.m_subtract_fee_outputs) {
Expand Down Expand Up @@ -984,6 +984,7 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal(
CoinSelectionParams coin_selection_params{rng_fast}; // Parameters for coin selection, init with dummy
coin_selection_params.m_avoid_partial_spends = coin_control.m_avoid_partial_spends;
coin_selection_params.m_include_unsafe_inputs = coin_control.m_include_unsafe_inputs;
coin_selection_params.m_max_tx_weight = coin_control.m_max_tx_weight;

// Set the long term feerate estimate to the wallet's consolidate feerate
coin_selection_params.m_long_term_feerate = wallet.m_consolidate_feerate;
Expand Down
Loading

0 comments on commit 7bfc80b

Please sign in to comment.