Skip to content

Commit d25e28c

Browse files
committed
Merge #21083: wallet: Avoid requesting fee rates multiple times during coin selection
f9cd2bf Rename CoinSelectionParams::effective_fee to m_effective_feerate (Andrew Chow) bdd0c29 wallet: Move discard feerate fetching to CreateTransaction (Andrew Chow) 448d04b wallet: Move long term feerate setting to CreateTransaction (Andrew Chow) e2f429e wallet: Replace nFeeRateNeeded with effective_fee (Andrew Chow) 1a6a0b0 wallet: Use existing feerate instead of getting a new one (Andrew Chow) Pull request description: During coin selection, there are various places where we need to have a feerate. We need the feerate for the transaction itself, the discard fee rate, and long term feerate. Fetching these each time we need them can lead to a race condition where two feerates that should be the same are actually different. One particular instance where this can happen is during the loop in `CreateTransactionInternal`. After inputs are chosen, the expected transaction fee is calculated using a newly fetched feerate. If `pick_new_inputs == false`, the loop will go again with the assumption that the fee for the transaction remains the same. However because the feerate is fetched again, it is possible that it actually isn't and this causes coin selection to fail. Instead of fetching the feerate each time it is needed, we fetch them all at once at the top of `CreateTransactionInternal`, store them in `CoinSelectionParams`, and use them where needed. While some of these fee rates probably don't need this caching, I've done it for consistency and the guarantee that they remain the same. Fixes #19229 ACKs for top commit: glozow: reACK bitcoin/bitcoin@f9cd2bf fjahr: Code review re-ACK f9cd2bf Xekyo: tACK bitcoin/bitcoin@f9cd2bf meshcollider: Code review + test run ACK f9cd2bf Tree-SHA512: be83ff64ba473c3cdd3469c812e214659b6e2a9584c22ed2b1595618fce0d4b35d0901e61068cd1069fc1a8fb911db01dd7312d05c3b8cbafbe2504ab7a3e863
2 parents 5ef1603 + f9cd2bf commit d25e28c

File tree

4 files changed

+53
-35
lines changed

4 files changed

+53
-35
lines changed

src/bench/coin_selection.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ static void CoinSelection(benchmark::Bench& bench)
4949
}
5050

5151
const CoinEligibilityFilter filter_standard(1, 6, 0);
52-
const CoinSelectionParams coin_selection_params(true, 34, 148, CFeeRate(0), 0, false);
52+
const CoinSelectionParams coin_selection_params(/* use_bnb= */ true, /* change_output_size= */ 34,
53+
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
54+
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
55+
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
5356
bench.run([&] {
5457
std::set<CInputCoin> setCoinsRet;
5558
CAmount nValueRet;

src/wallet/test/coinselector_tests.cpp

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ static CAmount balance = 0;
3535
CoinEligibilityFilter filter_standard(1, 6, 0);
3636
CoinEligibilityFilter filter_confirmed(1, 1, 0);
3737
CoinEligibilityFilter filter_standard_extra(6, 6, 0);
38-
CoinSelectionParams coin_selection_params(false, 0, 0, CFeeRate(0), 0, false);
38+
CoinSelectionParams coin_selection_params(/* use_bnb= */ false, /* change_output_size= */ 0,
39+
/* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(0),
40+
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
41+
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
3942

4043
static void add_coin(const CAmount& nValue, int nInput, std::vector<CInputCoin>& set)
4144
{
@@ -269,7 +272,10 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
269272
}
270273

271274
// Make sure that effective value is working in SelectCoinsMinConf when BnB is used
272-
CoinSelectionParams coin_selection_params_bnb(true, 0, 0, CFeeRate(3000), 0, false);
275+
CoinSelectionParams coin_selection_params_bnb(/* use_bnb= */ true, /* change_output_size= */ 0,
276+
/* change_spend_size= */ 0, /* effective_feerate= */ CFeeRate(3000),
277+
/* long_term_feerate= */ CFeeRate(1000), /* discard_feerate= */ CFeeRate(1000),
278+
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
273279
CoinSet setCoinsRet;
274280
CAmount nValueRet;
275281
bool bnb_used;
@@ -301,7 +307,7 @@ BOOST_AUTO_TEST_CASE(bnb_search_test)
301307
CCoinControl coin_control;
302308
coin_control.fAllowOtherInputs = true;
303309
coin_control.Select(COutPoint(vCoins.at(0).tx->GetHash(), vCoins.at(0).i));
304-
coin_selection_params_bnb.effective_fee = CFeeRate(0);
310+
coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);
305311
BOOST_CHECK(wallet->SelectCoins(vCoins, 10 * CENT, setCoinsRet, nValueRet, coin_control, coin_selection_params_bnb, bnb_used));
306312
BOOST_CHECK(bnb_used);
307313
BOOST_CHECK(coin_selection_params_bnb.use_bnb);
@@ -639,8 +645,14 @@ BOOST_AUTO_TEST_CASE(SelectCoins_test)
639645
CAmount target = rand.randrange(balance - 1000) + 1000;
640646

641647
// Perform selection
642-
CoinSelectionParams coin_selection_params_knapsack(false, 34, 148, CFeeRate(0), 0, false);
643-
CoinSelectionParams coin_selection_params_bnb(true, 34, 148, CFeeRate(0), 0, false);
648+
CoinSelectionParams coin_selection_params_knapsack(/* use_bnb= */ false, /* change_output_size= */ 34,
649+
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
650+
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
651+
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
652+
CoinSelectionParams coin_selection_params_bnb(/* use_bnb= */ true, /* change_output_size= */ 34,
653+
/* change_spend_size= */ 148, /* effective_feerate= */ CFeeRate(0),
654+
/* long_term_feerate= */ CFeeRate(0), /* discard_feerate= */ CFeeRate(0),
655+
/* tx_no_inputs_size= */ 0, /* avoid_partial= */ false);
644656
CoinSet out_set;
645657
CAmount out_value = 0;
646658
bool bnb_used = false;

src/wallet/wallet.cpp

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,26 +2399,20 @@ bool CWallet::SelectCoinsMinConf(const CAmount& nTargetValue, const CoinEligibil
23992399
nValueRet = 0;
24002400

24012401
if (coin_selection_params.use_bnb) {
2402-
// Get long term estimate
2403-
FeeCalculation feeCalc;
2404-
CCoinControl temp;
2405-
temp.m_confirm_target = 1008;
2406-
CFeeRate long_term_feerate = GetMinimumFeeRate(*this, temp, &feeCalc);
2407-
24082402
// Get the feerate for effective value.
24092403
// When subtracting the fee from the outputs, we want the effective feerate to be 0
24102404
CFeeRate effective_feerate{0};
24112405
if (!coin_selection_params.m_subtract_fee_outputs) {
2412-
effective_feerate = coin_selection_params.effective_fee;
2406+
effective_feerate = coin_selection_params.m_effective_feerate;
24132407
}
24142408

2415-
std::vector<OutputGroup> groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, long_term_feerate, eligibility_filter, true /* positive_only */);
2409+
std::vector<OutputGroup> groups = GroupOutputs(coins, !coin_selection_params.m_avoid_partial_spends, effective_feerate, coin_selection_params.m_long_term_feerate, eligibility_filter, true /* positive_only */);
24162410

24172411
// Calculate cost of change
2418-
CAmount cost_of_change = GetDiscardRate(*this).GetFee(coin_selection_params.change_spend_size) + coin_selection_params.effective_fee.GetFee(coin_selection_params.change_output_size);
2412+
CAmount cost_of_change = coin_selection_params.m_discard_feerate.GetFee(coin_selection_params.change_spend_size) + coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.change_output_size);
24192413

24202414
// Calculate the fees for things that aren't inputs
2421-
CAmount not_input_fees = coin_selection_params.effective_fee.GetFee(coin_selection_params.tx_noinputs_size);
2415+
CAmount not_input_fees = coin_selection_params.m_effective_feerate.GetFee(coin_selection_params.tx_noinputs_size);
24222416
bnb_used = true;
24232417
return SelectCoinsBnB(groups, nTargetValue, cost_of_change, setCoinsRet, nValueRet, not_input_fees);
24242418
} else {
@@ -2472,7 +2466,7 @@ bool CWallet::SelectCoins(const std::vector<COutput>& vAvailableCoins, const CAm
24722466
if (coin.m_input_bytes <= 0) {
24732467
return false; // Not solvable, can't estimate size for fee
24742468
}
2475-
coin.effective_value = coin.txout.nValue - coin_selection_params.effective_fee.GetFee(coin.m_input_bytes);
2469+
coin.effective_value = coin.txout.nValue - coin_selection_params.m_effective_feerate.GetFee(coin.m_input_bytes);
24762470
if (coin_selection_params.use_bnb) {
24772471
value_to_select -= coin.effective_value;
24782472
} else {
@@ -2840,17 +2834,28 @@ bool CWallet::CreateTransactionInternal(
28402834
CTxOut change_prototype_txout(0, scriptChange);
28412835
coin_selection_params.change_output_size = GetSerializeSize(change_prototype_txout);
28422836

2843-
CFeeRate discard_rate = GetDiscardRate(*this);
2837+
// Set discard feerate
2838+
coin_selection_params.m_discard_feerate = GetDiscardRate(*this);
28442839

28452840
// Get the fee rate to use effective values in coin selection
2846-
CFeeRate nFeeRateNeeded = GetMinimumFeeRate(*this, coin_control, &feeCalc);
2841+
coin_selection_params.m_effective_feerate = GetMinimumFeeRate(*this, coin_control, &feeCalc);
28472842
// Do not, ever, assume that it's fine to change the fee rate if the user has explicitly
28482843
// provided one
2849-
if (coin_control.m_feerate && nFeeRateNeeded > *coin_control.m_feerate) {
2850-
error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), nFeeRateNeeded.ToString(FeeEstimateMode::SAT_VB));
2844+
if (coin_control.m_feerate && coin_selection_params.m_effective_feerate > *coin_control.m_feerate) {
2845+
error = strprintf(_("Fee rate (%s) is lower than the minimum fee rate setting (%s)"), coin_control.m_feerate->ToString(FeeEstimateMode::SAT_VB), coin_selection_params.m_effective_feerate.ToString(FeeEstimateMode::SAT_VB));
2846+
return false;
2847+
}
2848+
if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) {
2849+
// eventually allow a fallback fee
2850+
error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
28512851
return false;
28522852
}
28532853

2854+
// Get long term estimate
2855+
CCoinControl cc_temp;
2856+
cc_temp.m_confirm_target = chain().estimateMaxBlocks();
2857+
coin_selection_params.m_long_term_feerate = GetMinimumFeeRate(*this, cc_temp, nullptr);
2858+
28542859
nFeeRet = 0;
28552860
bool pick_new_inputs = true;
28562861
CAmount nValueIn = 0;
@@ -2924,7 +2929,6 @@ bool CWallet::CreateTransactionInternal(
29242929
} else {
29252930
coin_selection_params.change_spend_size = (size_t)change_spend_size;
29262931
}
2927-
coin_selection_params.effective_fee = nFeeRateNeeded;
29282932
if (!SelectCoins(vAvailableCoins, nValueToSelect, setCoins, nValueIn, coin_control, coin_selection_params, bnb_used))
29292933
{
29302934
// If BnB was used, it was the first pass. No longer the first pass and continue loop with knapsack.
@@ -2950,7 +2954,7 @@ bool CWallet::CreateTransactionInternal(
29502954
// Never create dust outputs; if we would, just
29512955
// add the dust to the fee.
29522956
// The nChange when BnB is used is always going to go to fees.
2953-
if (IsDust(newTxOut, discard_rate) || bnb_used)
2957+
if (IsDust(newTxOut, coin_selection_params.m_discard_feerate) || bnb_used)
29542958
{
29552959
nChangePosInOut = -1;
29562960
nFeeRet += nChange;
@@ -2988,13 +2992,7 @@ bool CWallet::CreateTransactionInternal(
29882992
return false;
29892993
}
29902994

2991-
nFeeNeeded = GetMinimumFee(*this, nBytes, coin_control, &feeCalc);
2992-
if (feeCalc.reason == FeeReason::FALLBACK && !m_allow_fallback_fee) {
2993-
// eventually allow a fallback fee
2994-
error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
2995-
return false;
2996-
}
2997-
2995+
nFeeNeeded = coin_selection_params.m_effective_feerate.GetFee(nBytes);
29982996
if (nFeeRet >= nFeeNeeded) {
29992997
// Reduce fee to only the needed amount if possible. This
30002998
// prevents potential overpayment in fees if the coins
@@ -3008,8 +3006,8 @@ bool CWallet::CreateTransactionInternal(
30083006
// change output. Only try this once.
30093007
if (nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 && pick_new_inputs) {
30103008
unsigned int tx_size_with_change = nBytes + coin_selection_params.change_output_size + 2; // Add 2 as a buffer in case increasing # of outputs changes compact size
3011-
CAmount fee_needed_with_change = GetMinimumFee(*this, tx_size_with_change, coin_control, nullptr);
3012-
CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, discard_rate);
3009+
CAmount fee_needed_with_change = coin_selection_params.m_effective_feerate.GetFee(tx_size_with_change);
3010+
CAmount minimum_value_for_change = GetDustThreshold(change_prototype_txout, coin_selection_params.m_discard_feerate);
30133011
if (nFeeRet >= fee_needed_with_change + minimum_value_for_change) {
30143012
pick_new_inputs = false;
30153013
nFeeRet = fee_needed_with_change;

src/wallet/wallet.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -608,17 +608,22 @@ struct CoinSelectionParams
608608
bool use_bnb = true;
609609
size_t change_output_size = 0;
610610
size_t change_spend_size = 0;
611-
CFeeRate effective_fee = CFeeRate(0);
611+
CFeeRate m_effective_feerate;
612+
CFeeRate m_long_term_feerate;
613+
CFeeRate m_discard_feerate;
612614
size_t tx_noinputs_size = 0;
613615
//! Indicate that we are subtracting the fee from outputs
614616
bool m_subtract_fee_outputs = false;
615617
bool m_avoid_partial_spends = false;
616618

617-
CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_fee, size_t tx_noinputs_size, bool avoid_partial) :
619+
CoinSelectionParams(bool use_bnb, size_t change_output_size, size_t change_spend_size, CFeeRate effective_feerate,
620+
CFeeRate long_term_feerate, CFeeRate discard_feerate, size_t tx_noinputs_size, bool avoid_partial) :
618621
use_bnb(use_bnb),
619622
change_output_size(change_output_size),
620623
change_spend_size(change_spend_size),
621-
effective_fee(effective_fee),
624+
m_effective_feerate(effective_feerate),
625+
m_long_term_feerate(long_term_feerate),
626+
m_discard_feerate(discard_feerate),
622627
tx_noinputs_size(tx_noinputs_size),
623628
m_avoid_partial_spends(avoid_partial)
624629
{}

0 commit comments

Comments
 (0)