Skip to content

Commit 2d11258

Browse files
committed
coin selection: BnB, don't return selection if exceeds max allowed tx weight
1 parent d3a1c09 commit 2d11258

File tree

6 files changed

+23
-7
lines changed

6 files changed

+23
-7
lines changed

src/bench/coin_selection.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <bench/bench.h>
66
#include <interfaces/chain.h>
77
#include <node/context.h>
8+
#include <policy/policy.h>
89
#include <wallet/coinselection.h>
910
#include <wallet/spend.h>
1011
#include <wallet/wallet.h>
@@ -115,7 +116,7 @@ static void BnBExhaustion(benchmark::Bench& bench)
115116
bench.run([&] {
116117
// Benchmark
117118
CAmount target = make_hard_case(17, utxo_pool);
118-
SelectCoinsBnB(utxo_pool, target, 0); // Should exhaust
119+
SelectCoinsBnB(utxo_pool, target, 0, MAX_STANDARD_TX_WEIGHT); // Should exhaust
119120

120121
// Cleanup
121122
utxo_pool.clear();

src/wallet/coinselection.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ struct {
7171

7272
static const size_t TOTAL_TRIES = 100000;
7373

74-
util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)
74+
util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change,
75+
int max_weight)
7576
{
7677
SelectionResult result(selection_target, SelectionAlgorithm::BNB);
7778
CAmount curr_value = 0;
7879
std::vector<size_t> curr_selection; // selected utxo indexes
80+
int curr_selection_weight = 0; // sum of selected utxo weight
7981

8082
// Calculate curr_available_value
8183
CAmount curr_available_value = 0;
@@ -97,6 +99,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
9799
CAmount best_waste = MAX_MONEY;
98100

99101
bool is_feerate_high = utxo_pool.at(0).fee > utxo_pool.at(0).long_term_fee;
102+
bool max_tx_weight_exceeded = false;
100103

101104
// Depth First search loop for choosing the UTXOs
102105
for (size_t curr_try = 0, utxo_pool_index = 0; curr_try < TOTAL_TRIES; ++curr_try, ++utxo_pool_index) {
@@ -106,6 +109,9 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
106109
curr_value > selection_target + cost_of_change || // Selected value is out of range, go back and try other branch
107110
(curr_waste > best_waste && is_feerate_high)) { // Don't select things which we know will be more wasteful if the waste is increasing
108111
backtrack = true;
112+
} else if (curr_selection_weight > max_weight) { // Exceeding weight for standard tx, cannot find more solutions by adding more inputs
113+
max_tx_weight_exceeded = true; // at least one selection attempt exceeded the max weight
114+
backtrack = true;
109115
} else if (curr_value >= selection_target) { // Selected value is within range
110116
curr_waste += (curr_value - selection_target); // This is the excess value which is added to the waste for the below comparison
111117
// Adding another UTXO after this check could bring the waste down if the long term fee is higher than the current fee.
@@ -135,6 +141,7 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
135141
OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
136142
curr_value -= utxo.GetSelectionAmount();
137143
curr_waste -= utxo.fee - utxo.long_term_fee;
144+
curr_selection_weight -= utxo.m_weight;
138145
curr_selection.pop_back();
139146
} else { // Moving forwards, continuing down this branch
140147
OutputGroup& utxo = utxo_pool.at(utxo_pool_index);
@@ -154,13 +161,14 @@ util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool
154161
curr_selection.push_back(utxo_pool_index);
155162
curr_value += utxo.GetSelectionAmount();
156163
curr_waste += utxo.fee - utxo.long_term_fee;
164+
curr_selection_weight += utxo.m_weight;
157165
}
158166
}
159167
}
160168

161169
// Check for solution
162170
if (best_selection.empty()) {
163-
return util::Error();
171+
return max_tx_weight_exceeded ? ErrorMaxWeightExceeded() : util::Error();
164172
}
165173

166174
// Set output set

src/wallet/coinselection.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ struct SelectionResult
409409
int GetWeight() const { return m_weight; }
410410
};
411411

412-
util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change);
412+
util::Result<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change,
413+
int max_weight);
413414

414415
/** Select coins by Single Random Draw. OutputGroups are selected randomly from the eligible
415416
* outputs until the target is satisfied

src/wallet/spend.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ util::Result<SelectionResult> ChooseSelectionResult(const CAmount& nTargetValue,
566566
// Maximum allowed weight
567567
int max_inputs_weight = MAX_STANDARD_TX_WEIGHT - (coin_selection_params.tx_noinputs_size * WITNESS_SCALE_FACTOR);
568568

569-
if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change)}) {
569+
if (auto bnb_result{SelectCoinsBnB(groups.positive_group, nTargetValue, coin_selection_params.m_cost_of_change, max_inputs_weight)}) {
570570
results.push_back(*bnb_result);
571571
} else append_error(bnb_result);
572572

src/wallet/test/coinselector_tests.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,20 @@ 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
90+
// Helpers
9191
std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,
9292
CAmount change_target, FastRandomContext& rng)
9393
{
9494
auto res{KnapsackSolver(groups, nTargetValue, change_target, rng, MAX_STANDARD_TX_WEIGHT)};
9595
return res ? std::optional<SelectionResult>(*res) : std::nullopt;
9696
}
9797

98+
std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)
99+
{
100+
auto res{SelectCoinsBnB(utxo_pool, selection_target, cost_of_change, MAX_STANDARD_TX_WEIGHT)};
101+
return res ? std::optional<SelectionResult>(*res) : std::nullopt;
102+
}
103+
98104
/** Check if SelectionResult a is equivalent to SelectionResult b.
99105
* Equivalent means same input values, but maybe different inputs (i.e. same value, different prevout) */
100106
static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)

src/wallet/test/fuzz/coinselection.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ FUZZ_TARGET(coinselection)
8888
GroupCoins(fuzzed_data_provider, utxo_pool, coin_params, /*positive_only=*/false, group_all);
8989

9090
// Run coinselection algorithms
91-
const auto result_bnb = SelectCoinsBnB(group_pos, target, cost_of_change);
91+
const auto result_bnb = SelectCoinsBnB(group_pos, target, cost_of_change, MAX_STANDARD_TX_WEIGHT);
9292

9393
auto result_srd = SelectCoinsSRD(group_pos, target, fast_random_context, MAX_STANDARD_TX_WEIGHT);
9494
if (result_srd) result_srd->ComputeAndSetWaste(cost_of_change, cost_of_change, 0);

0 commit comments

Comments
 (0)