Skip to content

Commit 6107ec2

Browse files
committed
coin selection: knapsack, select closest UTXO above target if result exceeds max tx size
The simplest scenario where this is useful is on the 'check_max_weight' unit test already: We create 1515 UTXOs with 0.033 BTC each, and 1 UTXO with 50 BTC. Then perform Coin Selection. As the selection of the 1515 small UTXOs exceeds the max allowed tx size, the expectation here is to receive a selection result that only contain the big UTXO (which is not happening for the reasons stated below). As knapsack returns a result that exceeds the max allowed transaction size, we fallback to SRD, which selects coins randomly up until the target is met. So we end up with a selection result with lot more coins than what is needed.
1 parent 1284223 commit 6107ec2

File tree

5 files changed

+45
-7
lines changed

5 files changed

+45
-7
lines changed

src/wallet/coinselection.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
#include <optional>
1616

1717
namespace wallet {
18+
// Common selection error across the algorithms
19+
static util::Result<SelectionResult> ErrorMaxWeightExceeded()
20+
{
21+
return util::Error{_("The inputs size exceeds the maximum weight. "
22+
"Please try sending a smaller amount or manually consolidating your wallet's UTXOs")};
23+
}
24+
1825
// Descending order comparator
1926
struct {
2027
bool operator()(const OutputGroup& a, const OutputGroup& b) const
@@ -253,7 +260,7 @@ static void ApproximateBestSubset(FastRandomContext& insecure_rand, const std::v
253260
}
254261

255262
util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
256-
CAmount change_target, FastRandomContext& rng)
263+
CAmount change_target, FastRandomContext& rng, int max_weight)
257264
{
258265
SelectionResult result(nTargetValue, SelectionAlgorithm::KNAPSACK);
259266

@@ -313,6 +320,16 @@ util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, c
313320
}
314321
}
315322

323+
// If the result exceeds the maximum allowed size, return closest UTXO above the target
324+
if (result.GetWeight() > max_weight) {
325+
// No coin above target, nothing to do.
326+
if (!lowest_larger) return ErrorMaxWeightExceeded();
327+
328+
// Return closest UTXO above target
329+
result.Clear();
330+
result.AddInput(*lowest_larger);
331+
}
332+
316333
if (LogAcceptCategory(BCLog::SELECTCOINS, BCLog::Level::Debug)) {
317334
std::string log_message{"Coin selection best subset: "};
318335
for (unsigned int i = 0; i < applicable_groups.size(); i++) {

src/wallet/coinselection.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ util::Result<SelectionResult> SelectCoinsSRD(const std::vector<OutputGroup>& utx
422422

423423
// Original coin selection algorithm as a fallback
424424
util::Result<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
425-
CAmount change_target, FastRandomContext& rng);
425+
CAmount change_target, FastRandomContext& rng, int max_weight);
426426
} // namespace wallet
427427

428428
#endif // BITCOIN_WALLET_COINSELECTION_H

src/wallet/spend.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,12 +563,18 @@ util::Result<SelectionResult> ChooseSelectionResult(const CAmount& nTargetValue,
563563
}
564564
};
565565

566+
// Maximum allowed weight
567+
int max_inputs_weight = MAX_STANDARD_TX_WEIGHT - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
568+
566569
if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change)}) {
567570
results.push_back(*bnb_result);
568571
} else append_error(bnb_result);
569572

573+
// As Knapsack and SRD can create change, also deduce change weight.
574+
max_inputs_weight -= (coin_selection_params.change_output_size * WITNESS_SCALE_FACTOR);
575+
570576
// 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.
571-
if (auto knapsack_result{KnapsackSolver(groups.mixed_group, nTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast)}) {
577+
if (auto knapsack_result{KnapsackSolver(groups.mixed_group, nTargetValue, coin_selection_params.m_min_change_target, coin_selection_params.rng_fast, max_inputs_weight)}) {
572578
knapsack_result->ComputeAndSetWaste(coin_selection_params.min_viable_change, coin_selection_params.m_cost_of_change, coin_selection_params.m_change_fee);
573579
results.push_back(*knapsack_result);
574580
} else append_error(knapsack_result);

src/wallet/test/coinselector_tests.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ static void add_coin(CoinsResult& available_coins, CWallet& wallet, const CAmoun
8787
available_coins.Add(OutputType::BECH32, {COutPoint(wtx.GetHash(), nInput), txout, nAge, CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), /*spendable=*/ true, /*solvable=*/ true, /*safe=*/ true, wtx.GetTxTime(), fIsFromMe, feerate});
8888
}
8989

90+
// Helper
91+
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
92+
CAmount change_target, FastRandomContext& rng)
93+
{
94+
auto res{KnapsackSolver(groups, nTargetValue, change_target, rng, MAX_STANDARD_TX_WEIGHT)};
95+
return res ? std::optional<SelectionResult>(*res) : std::nullopt;
96+
}
97+
9098
/** Check if SelectionResult a is equivalent to SelectionResult b.
9199
* Equivalent means same input values, but maybe different inputs (i.e. same value, different prevout) */
92100
static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)
@@ -987,7 +995,10 @@ BOOST_AUTO_TEST_CASE(check_max_weight)
987995
chain);
988996

989997
BOOST_CHECK(result);
990-
BOOST_CHECK(has_coin(result->GetInputSet(), CAmount(50 * COIN)));
998+
// Verify that only the 50 BTC UTXO was selected
999+
const auto& selection_res = result->GetInputSet();
1000+
BOOST_CHECK(selection_res.size() == 1);
1001+
BOOST_CHECK((*selection_res.begin())->GetEffectiveValue() == 50 * COIN);
9911002
}
9921003

9931004
{

src/wallet/test/fuzz/coinselection.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
#include <policy/feerate.h>
6+
#include <policy/policy.h>
67
#include <primitives/transaction.h>
78
#include <test/fuzz/FuzzedDataProvider.h>
89
#include <test/fuzz/fuzz.h>
@@ -44,6 +45,9 @@ static void GroupCoins(FuzzedDataProvider& fuzzed_data_provider, const std::vect
4445
if (valid_outputgroup) output_groups.push_back(output_group);
4546
}
4647

48+
// Returns true if the result contains an error and the message is not empty
49+
static bool HasErrorMsg(const util::Result<SelectionResult>& res) { return !util::ErrorString(res).empty(); }
50+
4751
FUZZ_TARGET(coinselection)
4852
{
4953
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
@@ -90,12 +94,12 @@ FUZZ_TARGET(coinselection)
9094
if (result_srd) result_srd->ComputeAndSetWaste(cost_of_change, cost_of_change, 0);
9195

9296
CAmount change_target{GenerateChangeTarget(target, coin_params.m_change_fee, fast_random_context)};
93-
auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context);
97+
auto result_knapsack = KnapsackSolver(group_all, target, change_target, fast_random_context, MAX_STANDARD_TX_WEIGHT);
9498
if (result_knapsack) result_knapsack->ComputeAndSetWaste(cost_of_change, cost_of_change, 0);
9599

96100
// If the total balance is sufficient for the target and we are not using
97-
// effective values, Knapsack should always find a solution.
98-
if (total_balance >= target && subtract_fee_outputs) {
101+
// effective values, Knapsack should always find a solution (unless the selection exceeded the max tx weight).
102+
if (total_balance >= target && subtract_fee_outputs && !HasErrorMsg(result_knapsack)) {
99103
assert(result_knapsack);
100104
}
101105
}

0 commit comments

Comments
 (0)