Skip to content

Commit bf26e01

Browse files
committed
Roll static tx fees into nValueToSelect instead of having it be separate
The fees for transaction overhead and recipient outputs are now included in nTargetValue instead of being a separate parameter. For the coin selection algorithms, it doesn't matter that these are separate as in either case, the algorithm needs to select enough to cover these fees. Note that setting nValueToSelect is changed as it now includes not_input_fees. Without the change to how nValueToSelect is increased for KnapsackSolver, this would result in overpaying fees. The change to increase by the difference between nFeeRet and not_input_fees allows this to have the same behavior as previously. Additionally, because we assume that KnapsackSolver will always find a solution that requires change (we assume that BnB always finds a non-change solution), we also include the fee for the change output in KnapsackSolver's target. As part of this, we also use the changeless nFeeRet when iterating for KnapsackSolver. This is because we include the change fee when doing KnapsackSolver, so nFeeRet on further iterations won't include the change fee.
1 parent cc3f14b commit bf26e01

File tree

5 files changed

+35
-34
lines changed

5 files changed

+35
-34
lines changed

src/bench/coin_selection.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,11 @@ static void BnBExhaustion(benchmark::Bench& bench)
101101
std::vector<OutputGroup> utxo_pool;
102102
CoinSet selection;
103103
CAmount value_ret = 0;
104-
CAmount not_input_fees = 0;
105104

106105
bench.run([&] {
107106
// Benchmark
108107
CAmount target = make_hard_case(17, utxo_pool);
109-
SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret, not_input_fees); // Should exhaust
108+
SelectCoinsBnB(utxo_pool, target, 0, selection, value_ret); // Should exhaust
110109

111110
// Cleanup
112111
utxo_pool.clear();

src/wallet/coinselection.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,28 +49,25 @@ struct {
4949
* @param const std::vector<CInputCoin>& utxo_pool The set of UTXOs that we are choosing from.
5050
* These UTXOs will be sorted in descending order by effective value and the CInputCoins'
5151
* values are their effective values.
52-
* @param const CAmount& target_value This is the value that we want to select. It is the lower
52+
* @param const CAmount& selection_target This is the value that we want to select. It is the lower
5353
* bound of the range.
5454
* @param const CAmount& cost_of_change This is the cost of creating and spending a change output.
55-
* This plus target_value is the upper bound of the range.
55+
* This plus selection_target is the upper bound of the range.
5656
* @param std::set<CInputCoin>& out_set -> This is an output parameter for the set of CInputCoins
5757
* that have been selected.
5858
* @param CAmount& value_ret -> This is an output parameter for the total value of the CInputCoins
5959
* that were selected.
60-
* @param CAmount not_input_fees -> The fees that need to be paid for the outputs and fixed size
61-
* overhead (version, locktime, marker and flag)
6260
*/
6361

6462
static const size_t TOTAL_TRIES = 100000;
6563

66-
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret, CAmount not_input_fees)
64+
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret)
6765
{
6866
out_set.clear();
6967
CAmount curr_value = 0;
7068

7169
std::vector<bool> curr_selection; // select the utxo at this index
7270
curr_selection.reserve(utxo_pool.size());
73-
CAmount selection_target = not_input_fees + target_value;
7471

7572
// Calculate curr_available_value
7673
CAmount curr_available_value = 0;

src/wallet/coinselection.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ struct OutputGroup
120120
bool EligibleForSpending(const CoinEligibilityFilter& eligibility_filter) const;
121121
};
122122

123-
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& target_value, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret, CAmount not_input_fees);
123+
bool SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, std::set<CInputCoin>& out_set, CAmount& value_ret);
124124

125125
// Original coin selection algorithm as a fallback
126126
bool KnapsackSolver(const CAmount& nTargetValue, std::vector<OutputGroup>& groups, std::set<CInputCoin>& setCoinsRet, CAmount& nValueRet);

src/wallet/test/coinselector_tests.cpp

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,13 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
148148
CoinSet selection;
149149
CoinSet actual_selection;
150150
CAmount value_ret = 0;
151-
CAmount not_input_fees = 0;
152151

153152
/////////////////////////
154153
// Known Outcome tests //
155154
/////////////////////////
156155

157156
// Empty utxo pool
158-
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
157+
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret));
159158
selection.clear();
160159

161160
// Add utxos
@@ -166,15 +165,15 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
166165

167166
// Select 1 Cent
168167
add_coin(1 * CENT, 1, actual_selection);
169-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
168+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 0.5 * CENT, selection, value_ret));
170169
BOOST_CHECK(equal_sets(selection, actual_selection));
171170
BOOST_CHECK_EQUAL(value_ret, 1 * CENT);
172171
actual_selection.clear();
173172
selection.clear();
174173

175174
// Select 2 Cent
176175
add_coin(2 * CENT, 2, actual_selection);
177-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
176+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 2 * CENT, 0.5 * CENT, selection, value_ret));
178177
BOOST_CHECK(equal_sets(selection, actual_selection));
179178
BOOST_CHECK_EQUAL(value_ret, 2 * CENT);
180179
actual_selection.clear();
@@ -183,27 +182,27 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
183182
// Select 5 Cent
184183
add_coin(4 * CENT, 4, actual_selection);
185184
add_coin(1 * CENT, 1, actual_selection);
186-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
185+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 5 * CENT, 0.5 * CENT, selection, value_ret));
187186
BOOST_CHECK(equal_sets(selection, actual_selection));
188187
BOOST_CHECK_EQUAL(value_ret, 5 * CENT);
189188
actual_selection.clear();
190189
selection.clear();
191190

192191
// Select 11 Cent, not possible
193-
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
192+
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 11 * CENT, 0.5 * CENT, selection, value_ret));
194193
actual_selection.clear();
195194
selection.clear();
196195

197196
// Cost of change is greater than the difference between target value and utxo sum
198197
add_coin(1 * CENT, 1, actual_selection);
199-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
198+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0.5 * CENT, selection, value_ret));
200199
BOOST_CHECK_EQUAL(value_ret, 1 * CENT);
201200
BOOST_CHECK(equal_sets(selection, actual_selection));
202201
actual_selection.clear();
203202
selection.clear();
204203

205204
// Cost of change is less than the difference between target value and utxo sum
206-
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0, selection, value_ret, not_input_fees));
205+
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.9 * CENT, 0, selection, value_ret));
207206
actual_selection.clear();
208207
selection.clear();
209208

@@ -212,7 +211,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
212211
add_coin(5 * CENT, 5, actual_selection);
213212
add_coin(4 * CENT, 4, actual_selection);
214213
add_coin(1 * CENT, 1, actual_selection);
215-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
214+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 0.5 * CENT, selection, value_ret));
216215
BOOST_CHECK(equal_sets(selection, actual_selection));
217216
BOOST_CHECK_EQUAL(value_ret, 10 * CENT);
218217
actual_selection.clear();
@@ -223,21 +222,21 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
223222
add_coin(5 * CENT, 5, actual_selection);
224223
add_coin(3 * CENT, 3, actual_selection);
225224
add_coin(2 * CENT, 2, actual_selection);
226-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 5000, selection, value_ret, not_input_fees));
225+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 10 * CENT, 5000, selection, value_ret));
227226
BOOST_CHECK_EQUAL(value_ret, 10 * CENT);
228227
// FIXME: this test is redundant with the above, because 1 Cent is selected, not "too small"
229228
// BOOST_CHECK(equal_sets(selection, actual_selection));
230229

231230
// Select 0.25 Cent, not possible
232-
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT, selection, value_ret, not_input_fees));
231+
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 0.25 * CENT, 0.5 * CENT, selection, value_ret));
233232
actual_selection.clear();
234233
selection.clear();
235234

236235
// Iteration exhaustion test
237236
CAmount target = make_hard_case(17, utxo_pool);
238-
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret, not_input_fees)); // Should exhaust
237+
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret)); // Should exhaust
239238
target = make_hard_case(14, utxo_pool);
240-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret, not_input_fees)); // Should not exhaust
239+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), target, 0, selection, value_ret)); // Should not exhaust
241240

242241
// Test same value early bailout optimization
243242
utxo_pool.clear();
@@ -254,7 +253,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
254253
for (int i = 0; i < 50000; ++i) {
255254
add_coin(5 * CENT, 7, utxo_pool);
256255
}
257-
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000, selection, value_ret, not_input_fees));
256+
BOOST_CHECK(SelectCoinsBnB(GroupCoins(utxo_pool), 30 * CENT, 5000, selection, value_ret));
258257
BOOST_CHECK_EQUAL(value_ret, 30 * CENT);
259258
BOOST_CHECK(equal_sets(selection, actual_selection));
260259

@@ -268,7 +267,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
268267
}
269268
// Run 100 times, to make sure it is never finding a solution
270269
for (int i = 0; i < 100; ++i) {
271-
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT, selection, value_ret, not_input_fees));
270+
BOOST_CHECK(!SelectCoinsBnB(GroupCoins(utxo_pool), 1 * CENT, 2 * CENT, selection, value_ret));
272271
}
273272

274273
// Make sure that effective value is working in SelectCoinsMinConf when BnB is used

src/wallet/wallet.cpp

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,9 +2399,6 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil
23992399
setCoinsRet.clear();
24002400
nValueRet = 0;
24012401

2402-
// Calculate the fees for things that aren't inputs, excluding the change output
2403-
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
2404-
24052402
// Get the feerate for effective value.
24062403
// When subtracting the fee from the outputs, we want the effective feerate to be 0
24072404
CFeeRate effective_feerate{0};
@@ -2412,14 +2409,17 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil
24122409
if (coin_selection_params.use_bnb) {
24132410
std::vector<OutputGroup> positive_groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, coin_selection_params.m_long_term_feerate, eligibility_filter, true /* positive_only */);
24142411
bnb_used = true;
2415-
return SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet, not_input_fees);
2412+
// Note that unlike KnapsackSolver, we do not include the fee for creating a change output as BnB will not create a change output.
2413+
return SelectCoinsBnB(positive_groups, nTargetValue, coin_selection_params.m_cost_of_change, setCoinsRet, nValueRet);
24162414
} else {
24172415
// 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.
24182416
// The knapsack solver currently does not use effective values, so we give GroupOutputs feerates of 0 so it sets the effective values to be the same as the real value.
24192417
std::vector<OutputGroup> groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, CFeeRate(0), CFeeRate(0), eligibility_filter, false /* positive_only */);
24202418

24212419
bnb_used = false;
2422-
return KnapsackSolver(nTargetValue, groups, setCoinsRet, nValueRet);
2420+
// While nTargetValue includes the transaction fees for non-input things, it does not include the fee for creating a change output.
2421+
// So we need to include that for KnapsackSolver as well, as we are expecting to create a change output.
2422+
return KnapsackSolver(nTargetValue + coin_selection_params.m_change_fee, groups, setCoinsRet, nValueRet);
24232423
}
24242424
}
24252425

@@ -2931,10 +2931,6 @@ bool CWallet::CreateTransactionInternal(
29312931
txNew.vin.clear();
29322932
txNew.vout.clear();
29332933

2934-
CAmount nValueToSelect = nValue;
2935-
if (nSubtractFeeFromAmount == 0)
2936-
nValueToSelect += nFeeRet;
2937-
29382934
// vouts to the payees
29392935
if (!coin_selection_params.m_subtract_fee_outputs) {
29402936
coin_selection_params.tx_noinputs_size = 11; // Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 output count, 1 witness overhead (dummy, flag, stack size)
@@ -2956,11 +2952,21 @@ bool CWallet::CreateTransactionInternal(
29562952
txNew.vout.push_back(txout);
29572953
}
29582954

2955+
// Include the fees for things that aren't inputs, excluding the change output
2956+
const CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
2957+
CAmount nValueToSelect = nValue + not_input_fees;
2958+
2959+
// For KnapsackSolver, when we are not subtracting the fee from the recipients, we also want to include the fees for the
2960+
// inputs that we found in the previous iteration.
2961+
if (!coin_selection_params.use_bnb && nSubtractFeeFromAmount == 0) {
2962+
nValueToSelect += std::max(CAmount(0), nFeeRet - not_input_fees);
2963+
}
2964+
29592965
// Choose coins to use
29602966
bool bnb_used = false;
29612967
nValueIn = 0;
29622968
setCoins.clear();
2963-
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used))
2969+
if (!SelectCoins(vAvailableCoins, /* nTargetValue */ nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used))
29642970
{
29652971
// If BnB was used, it was the first pass. No longer the first pass and continue loop with knapsack.
29662972
if (bnb_used) {

0 commit comments

Comments
 (0)