Skip to content

Commit 8afa602

Browse files
committed
Merge #16727: wallet: Explicit feerate for bumpfee
c812aba test bumpfee fee_rate argument (ezegom) 9f25de3 rpc bumpfee check fee_rate argument (ezegom) 88e5f99 rpc bumpfee: add fee_rate argument (ezegom) 1a4c791 rpc bumpfee: move feerate estimation logic into separate method (ezegom) Pull request description: Taking over for bitcoin/bitcoin#16492 which seems to have gone inactive. Only minor commit cleanups, rebase, and some help text fixes on top of previous PR. Renamed `feeRate` to `fee_rate` to reflect updated guidelines. ACKs for top commit: Sjors: Code review ACK c812aba laanwj: ACK c812aba Tree-SHA512: 5f7f51bd780a573ccef1ccd72b0faf3e5d143f6551060a667560c5163f7d9480e17e73775d1d7bcac0463f3b6b4328f0cff7b27e39483bddc42a530f4583ce30
2 parents 8d39c63 + c812aba commit 8afa602

File tree

3 files changed

+131
-33
lines changed

3 files changed

+131
-33
lines changed

src/wallet/feebumper.cpp

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,86 @@ static feebumper::Result PreconditionChecks(interfaces::Chain::Lock& locked_chai
5757
return feebumper::Result::OK;
5858
}
5959

60+
//! Check if the user provided a valid feeRate
61+
static feebumper::Result CheckFeeRate(const CWallet* wallet, const CWalletTx& wtx, const CFeeRate& newFeerate, const int64_t maxTxSize, std::vector<std::string>& errors) {
62+
// check that fee rate is higher than mempool's minimum fee
63+
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
64+
// This may occur if the user set FeeRate, TotalFee or paytxfee too low, if fallbackfee is too low, or, perhaps,
65+
// in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a
66+
// moment earlier. In this case, we report an error to the user, who may adjust the fee.
67+
CFeeRate minMempoolFeeRate = wallet->chain().mempoolMinFee();
68+
69+
if (newFeerate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) {
70+
errors.push_back(strprintf(
71+
"New fee rate (%s) is lower than the minimum fee rate (%s) to get into the mempool -- ",
72+
FormatMoney(newFeerate.GetFeePerK()),
73+
FormatMoney(minMempoolFeeRate.GetFeePerK())));
74+
return feebumper::Result::WALLET_ERROR;
75+
}
76+
77+
CAmount new_total_fee = newFeerate.GetFee(maxTxSize);
78+
79+
CFeeRate incrementalRelayFee = std::max(wallet->chain().relayIncrementalFee(), CFeeRate(WALLET_INCREMENTAL_RELAY_FEE));
80+
81+
// Given old total fee and transaction size, calculate the old feeRate
82+
CAmount old_fee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
83+
const int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
84+
CFeeRate nOldFeeRate(old_fee, txSize);
85+
// Min total fee is old fee + relay fee
86+
CAmount minTotalFee = nOldFeeRate.GetFee(maxTxSize) + incrementalRelayFee.GetFee(maxTxSize);
87+
88+
if (new_total_fee < minTotalFee) {
89+
errors.push_back(strprintf("Insufficient total fee %s, must be at least %s (oldFee %s + incrementalFee %s)",
90+
FormatMoney(new_total_fee), FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxTxSize)), FormatMoney(incrementalRelayFee.GetFee(maxTxSize))));
91+
return feebumper::Result::INVALID_PARAMETER;
92+
}
93+
94+
CAmount requiredFee = GetRequiredFee(*wallet, maxTxSize);
95+
if (new_total_fee < requiredFee) {
96+
errors.push_back(strprintf("Insufficient total fee (cannot be less than required fee %s)",
97+
FormatMoney(requiredFee)));
98+
return feebumper::Result::INVALID_PARAMETER;
99+
}
100+
101+
// Check that in all cases the new fee doesn't violate maxTxFee
102+
const CAmount max_tx_fee = wallet->m_default_max_tx_fee;
103+
if (new_total_fee > max_tx_fee) {
104+
errors.push_back(strprintf("Specified or calculated fee %s is too high (cannot be higher than -maxtxfee %s)",
105+
FormatMoney(new_total_fee), FormatMoney(max_tx_fee)));
106+
return feebumper::Result::WALLET_ERROR;
107+
}
108+
109+
return feebumper::Result::OK;
110+
}
111+
112+
static CFeeRate EstimateFeeRate(CWallet* wallet, const CWalletTx& wtx, CCoinControl& coin_control, CAmount& old_fee)
113+
{
114+
// Get the fee rate of the original transaction. This is calculated from
115+
// the tx fee/vsize, so it may have been rounded down. Add 1 satoshi to the
116+
// result.
117+
old_fee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
118+
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
119+
CFeeRate feerate(old_fee, txSize);
120+
feerate += CFeeRate(1);
121+
122+
// The node has a configurable incremental relay fee. Increment the fee by
123+
// the minimum of that and the wallet's conservative
124+
// WALLET_INCREMENTAL_RELAY_FEE value to future proof against changes to
125+
// network wide policy for incremental relay fee that our node may not be
126+
// aware of. This ensures we're over the over the required relay fee rate
127+
// (BIP 125 rule 4). The replacement tx will be at least as large as the
128+
// original tx, so the total fee will be greater (BIP 125 rule 3)
129+
CFeeRate node_incremental_relay_fee = wallet->chain().relayIncrementalFee();
130+
CFeeRate wallet_incremental_relay_fee = CFeeRate(WALLET_INCREMENTAL_RELAY_FEE);
131+
feerate += std::max(node_incremental_relay_fee, wallet_incremental_relay_fee);
132+
133+
// Fee rate must also be at least the wallet's GetMinimumFeeRate
134+
CFeeRate min_feerate(GetMinimumFeeRate(*wallet, coin_control, /* feeCalc */ nullptr));
135+
136+
// Set the required fee rate for the replacement transaction in coin control.
137+
return std::max(feerate, min_feerate);
138+
}
139+
60140
namespace feebumper {
61141

62142
bool TransactionCanBeBumped(const CWallet* wallet, const uint256& txid)
@@ -230,31 +310,18 @@ Result CreateRateBumpTransaction(CWallet* wallet, const uint256& txid, const CCo
230310
}
231311
}
232312

233-
// Get the fee rate of the original transaction. This is calculated from
234-
// the tx fee/vsize, so it may have been rounded down. Add 1 satoshi to the
235-
// result.
236-
old_fee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
237-
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
238-
// Feerate of thing we are bumping
239-
CFeeRate feerate(old_fee, txSize);
240-
feerate += CFeeRate(1);
241-
242-
// The node has a configurable incremental relay fee. Increment the fee by
243-
// the minimum of that and the wallet's conservative
244-
// WALLET_INCREMENTAL_RELAY_FEE value to future proof against changes to
245-
// network wide policy for incremental relay fee that our node may not be
246-
// aware of. This ensures we're over the over the required relay fee rate
247-
// (BIP 125 rule 4). The replacement tx will be at least as large as the
248-
// original tx, so the total fee will be greater (BIP 125 rule 3)
249-
CFeeRate node_incremental_relay_fee = wallet->chain().relayIncrementalFee();
250-
CFeeRate wallet_incremental_relay_fee = CFeeRate(WALLET_INCREMENTAL_RELAY_FEE);
251-
feerate += std::max(node_incremental_relay_fee, wallet_incremental_relay_fee);
252-
253-
// Fee rate must also be at least the wallet's GetMinimumFeeRate
254-
CFeeRate min_feerate(GetMinimumFeeRate(*wallet, new_coin_control, /* feeCalc */ nullptr));
255-
256-
// Set the required fee rate for the replacement transaction in coin control.
257-
new_coin_control.m_feerate = std::max(feerate, min_feerate);
313+
if (coin_control.m_feerate) {
314+
// The user provided a feeRate argument.
315+
// We calculate this here to avoid compiler warning on the cs_wallet lock
316+
const int64_t maxTxSize = CalculateMaximumSignedTxSize(*wtx.tx, wallet);
317+
Result res = CheckFeeRate(wallet, wtx, *(new_coin_control.m_feerate), maxTxSize, errors);
318+
if (res != Result::OK) {
319+
return res;
320+
}
321+
} else {
322+
// The user did not provide a feeRate argument
323+
new_coin_control.m_feerate = EstimateFeeRate(wallet, wtx, new_coin_control, old_fee);
324+
}
258325

259326
// Fill in required inputs we are double-spending(all of them)
260327
// N.B.: bip125 doesn't require all the inputs in the replaced transaction to be

src/wallet/rpcwallet.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3309,7 +3309,7 @@ static UniValue bumpfee(const JSONRPCRequest& request)
33093309
"The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n"
33103310
"By default, the new fee will be calculated automatically using estimatesmartfee.\n"
33113311
"The user can specify a confirmation target for estimatesmartfee.\n"
3312-
"Alternatively, the user can specify totalFee (DEPRECATED), or use RPC settxfee to set a higher fee rate.\n"
3312+
"Alternatively, the user can specify totalFee (DEPRECATED), or fee_rate (" + CURRENCY_UNIT + " per kB) for the new transaction .\n"
33133313
"At a minimum, the new fee rate must be high enough to pay an additional new relay fee (incrementalfee\n"
33143314
"returned by getnetworkinfo) to enter the node's mempool.\n",
33153315
{
@@ -3321,6 +3321,9 @@ static UniValue bumpfee(const JSONRPCRequest& request)
33213321
" In rare cases, the actual fee paid might be slightly higher than the specified\n"
33223322
" totalFee if the tx change output has to be removed because it is too close to\n"
33233323
" the dust threshold."},
3324+
{"fee_rate", RPCArg::Type::NUM, /* default */ "fallback to 'confTarget'", "FeeRate (NOT total fee) to pay, in " + CURRENCY_UNIT + " per kB\n"
3325+
" Specify a fee rate instead of relying on the built-in fee estimator.\n"
3326+
" Must be at least 0.0001 BTC per kB higher than the current transaction fee rate.\n"},
33243327
{"replaceable", RPCArg::Type::BOOL, /* default */ "true", "Whether the new transaction should still be\n"
33253328
" marked bip-125 replaceable. If true, the sequence numbers in the transaction will\n"
33263329
" be left unchanged from the original. If false, any input sequence numbers in the\n"
@@ -3362,13 +3365,15 @@ static UniValue bumpfee(const JSONRPCRequest& request)
33623365
{
33633366
{"confTarget", UniValueType(UniValue::VNUM)},
33643367
{"totalFee", UniValueType(UniValue::VNUM)},
3368+
{"fee_rate", UniValueType(UniValue::VNUM)},
33653369
{"replaceable", UniValueType(UniValue::VBOOL)},
33663370
{"estimate_mode", UniValueType(UniValue::VSTR)},
33673371
},
33683372
true, true);
3369-
3370-
if (options.exists("confTarget") && options.exists("totalFee")) {
3371-
throw JSONRPCError(RPC_INVALID_PARAMETER, "confTarget and totalFee options should not both be set. Please provide either a confirmation target for fee estimation or an explicit total fee for the transaction.");
3373+
if (options.exists("confTarget") && (options.exists("totalFee") || options.exists("fee_rate"))) {
3374+
throw JSONRPCError(RPC_INVALID_PARAMETER, "confTarget can't be set with totalFee or fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.");
3375+
} else if (options.exists("fee_rate") && options.exists("totalFee")) {
3376+
throw JSONRPCError(RPC_INVALID_PARAMETER, "fee_rate can't be set along with totalFee.");
33723377
} else if (options.exists("confTarget")) { // TODO: alias this to conf_target
33733378
coin_control.m_confirm_target = ParseConfirmTarget(options["confTarget"], pwallet->chain().estimateMaxBlocks());
33743379
} else if (options.exists("totalFee")) {
@@ -3379,6 +3384,12 @@ static UniValue bumpfee(const JSONRPCRequest& request)
33793384
if (totalFee <= 0) {
33803385
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid totalFee %s (must be greater than 0)", FormatMoney(totalFee)));
33813386
}
3387+
} else if (options.exists("fee_rate")) {
3388+
CFeeRate fee_rate(AmountFromValue(options["fee_rate"]));
3389+
if (fee_rate <= 0) {
3390+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid fee_rate %s (must be greater than 0)", fee_rate.ToString()));
3391+
}
3392+
coin_control.m_feerate = fee_rate;
33823393
}
33833394

33843395
if (options.exists("replaceable")) {

test/functional/wallet_bumpfee.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ def run_test(self):
6767

6868
self.log.info("Running tests")
6969
dest_address = peer_node.getnewaddress()
70-
test_simple_bumpfee_succeeds(self, rbf_node, peer_node, dest_address)
70+
test_simple_bumpfee_succeeds(self, "default", rbf_node, peer_node, dest_address)
71+
test_simple_bumpfee_succeeds(self, "fee_rate", rbf_node, peer_node, dest_address)
72+
test_feerate_args(self, rbf_node, peer_node, dest_address)
7173
test_segwit_bumpfee_succeeds(rbf_node, dest_address)
7274
test_nonrbf_bumpfee_fails(peer_node, dest_address)
7375
test_notmine_bumpfee_fails(rbf_node, peer_node, dest_address)
@@ -88,12 +90,15 @@ def run_test(self):
8890
self.log.info("Success")
8991

9092

91-
def test_simple_bumpfee_succeeds(self, rbf_node, peer_node, dest_address):
93+
def test_simple_bumpfee_succeeds(self, mode, rbf_node, peer_node, dest_address):
9294
rbfid = spend_one_input(rbf_node, dest_address)
9395
rbftx = rbf_node.gettransaction(rbfid)
9496
self.sync_mempools((rbf_node, peer_node))
9597
assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool()
96-
bumped_tx = rbf_node.bumpfee(rbfid)
98+
if mode == "fee_rate":
99+
bumped_tx = rbf_node.bumpfee(rbfid, {"fee_rate":0.0015})
100+
else:
101+
bumped_tx = rbf_node.bumpfee(rbfid)
97102
assert_equal(bumped_tx["errors"], [])
98103
assert bumped_tx["fee"] - abs(rbftx["fee"]) > 0
99104
# check that bumped_tx propagates, original tx was evicted and has a wallet conflict
@@ -109,6 +114,22 @@ def test_simple_bumpfee_succeeds(self, rbf_node, peer_node, dest_address):
109114
assert_equal(oldwtx["replaced_by_txid"], bumped_tx["txid"])
110115
assert_equal(bumpedwtx["replaces_txid"], rbfid)
111116

117+
def test_feerate_args(self, rbf_node, peer_node, dest_address):
118+
rbfid = spend_one_input(rbf_node, dest_address)
119+
self.sync_mempools((rbf_node, peer_node))
120+
assert rbfid in rbf_node.getrawmempool() and rbfid in peer_node.getrawmempool()
121+
122+
assert_raises_rpc_error(-8, "confTarget can't be set with totalFee or fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"fee_rate":0.00001, "confTarget":1})
123+
assert_raises_rpc_error(-8, "confTarget can't be set with totalFee or fee_rate. Please provide either a confirmation target in blocks for automatic fee estimation, or an explicit fee rate.", rbf_node.bumpfee, rbfid, {"totalFee":0.00001, "confTarget":1})
124+
assert_raises_rpc_error(-8, "fee_rate can't be set along with totalFee.", rbf_node.bumpfee, rbfid, {"fee_rate":0.00001, "totalFee":0.001})
125+
126+
# Bumping to just above minrelay should fail to increase total fee enough, at least
127+
assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, rbfid, {"fee_rate":0.00001000})
128+
129+
assert_raises_rpc_error(-3, "Amount out of range", rbf_node.bumpfee, rbfid, {"fee_rate":-1})
130+
131+
assert_raises_rpc_error(-4, "is too high (cannot be higher than", rbf_node.bumpfee, rbfid, {"fee_rate":1})
132+
112133

113134
def test_segwit_bumpfee_succeeds(rbf_node, dest_address):
114135
# Create a transaction with segwit output, then create an RBF transaction
@@ -176,7 +197,6 @@ def test_bumpfee_with_descendant_fails(rbf_node, rbf_node_address, dest_address)
176197
rbf_node.sendrawtransaction(tx["hex"])
177198
assert_raises_rpc_error(-8, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id)
178199

179-
180200
def test_small_output_fails(rbf_node, dest_address):
181201
# cannot bump fee with a too-small output
182202
rbfid = spend_one_input(rbf_node, dest_address)

0 commit comments

Comments
 (0)