Skip to content

Commit 4f4ef31

Browse files
committed
Merge #15557: Enhance bumpfee to include inputs when targeting a feerate
184f878 wallet_bumpfee.py: add test for change key preservation (Gregory Sanders) d08becf add functional tests for feerate bumpfee with adding inputs (Gregory Sanders) 0ea47ba generalize bumpfee to add inputs when needed (Gregory Sanders) Pull request description: When targeting a feerate using `bumpfee`, call a new function that directly uses `CWallet::CreateTransaction` and coin control to get the desired result. This allows us to get a superset of previous behavior, with an arbitrary RBF bump of a transaction provided it passes the preconditional checks and spare confirmed utxos are available. Note(s): 0) The coin selection will use knapsack solver for the residual selection. 1) This functionality, just like knapsack coin selection in general, will hoover up negative-value inputs when given the chance. 2) Newly added inputs must be confirmed due to current Core policy. See error: `replacement-adds-unconfirmed` 3) Supporting this with `totalFee` is difficult since the "minimum total fee" option in `CreateTransaction` logic was (rightly)taken out in #10390 . ACKs for commit 184f87: jnewbery: utACK 184f878 Tree-SHA512: fb6542bdfb2c6010e328ec475cf9dcbff4eb2b1a1b27f78010214534908987a5635797196fa05edddffcbcf2987335872dc644a99261886d5cbb34a8f262ad3e
2 parents adc55db + 184f878 commit 4f4ef31

File tree

7 files changed

+220
-48
lines changed

7 files changed

+220
-48
lines changed

src/interfaces/wallet.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,13 @@ class WalletImpl : public Wallet
269269
CAmount& new_fee,
270270
CMutableTransaction& mtx) override
271271
{
272-
return feebumper::CreateTransaction(m_wallet.get(), txid, coin_control, total_fee, errors, old_fee, new_fee, mtx) ==
273-
feebumper::Result::OK;
272+
if (total_fee > 0) {
273+
return feebumper::CreateTotalBumpTransaction(m_wallet.get(), txid, coin_control, total_fee, errors, old_fee, new_fee, mtx) ==
274+
feebumper::Result::OK;
275+
} else {
276+
return feebumper::CreateRateBumpTransaction(m_wallet.get(), txid, coin_control, errors, old_fee, new_fee, mtx) ==
277+
feebumper::Result::OK;
278+
}
274279
}
275280
bool signBumpTransaction(CMutableTransaction& mtx) override { return feebumper::SignTransaction(m_wallet.get(), mtx); }
276281
bool commitBumpTransaction(const uint256& txid,

src/wallet/coincontrol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class CCoinControl
3636
bool m_avoid_partial_spends;
3737
//! Fee estimation mode to control arguments to estimateSmartFee
3838
FeeEstimateMode m_fee_mode;
39+
//! Minimum chain depth value for coin availability
40+
int m_min_depth{0};
3941

4042
CCoinControl()
4143
{

src/wallet/feebumper.cpp

Lines changed: 121 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ bool TransactionCanBeBumped(const CWallet* wallet, const uint256& txid)
7575
return res == feebumper::Result::OK;
7676
}
7777

78-
Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoinControl& coin_control, CAmount total_fee, std::vector<std::string>& errors,
79-
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx)
78+
Result CreateTotalBumpTransaction(const CWallet* wallet, const uint256& txid, const CCoinControl& coin_control, CAmount total_fee, std::vector<std::string>& errors,
79+
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx)
8080
{
81+
new_fee = total_fee;
82+
8183
auto locked_chain = wallet->chain().lock();
8284
LOCK(wallet->cs_wallet);
8385
errors.clear();
@@ -121,7 +123,6 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
121123
// calculate the old fee and fee-rate
122124
old_fee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
123125
CFeeRate nOldFeeRate(old_fee, txSize);
124-
CFeeRate nNewFeeRate;
125126
// The wallet uses a conservative WALLET_INCREMENTAL_RELAY_FEE value to
126127
// future proof against changes to network wide policy for incremental relay
127128
// fee that our node may not be aware of.
@@ -131,34 +132,17 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
131132
walletIncrementalRelayFee = nodeIncrementalRelayFee;
132133
}
133134

134-
if (total_fee > 0) {
135-
CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + nodeIncrementalRelayFee.GetFee(maxNewTxSize);
136-
if (total_fee < minTotalFee) {
137-
errors.push_back(strprintf("Insufficient totalFee, must be at least %s (oldFee %s + incrementalFee %s)",
138-
FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxNewTxSize)), FormatMoney(nodeIncrementalRelayFee.GetFee(maxNewTxSize))));
139-
return Result::INVALID_PARAMETER;
140-
}
141-
CAmount requiredFee = GetRequiredFee(*wallet, maxNewTxSize);
142-
if (total_fee < requiredFee) {
143-
errors.push_back(strprintf("Insufficient totalFee (cannot be less than required fee %s)",
144-
FormatMoney(requiredFee)));
145-
return Result::INVALID_PARAMETER;
146-
}
147-
new_fee = total_fee;
148-
nNewFeeRate = CFeeRate(total_fee, maxNewTxSize);
149-
} else {
150-
new_fee = GetMinimumFee(*wallet, maxNewTxSize, coin_control, nullptr /* FeeCalculation */);
151-
nNewFeeRate = CFeeRate(new_fee, maxNewTxSize);
152-
153-
// New fee rate must be at least old rate + minimum incremental relay rate
154-
// walletIncrementalRelayFee.GetFeePerK() should be exact, because it's initialized
155-
// in that unit (fee per kb).
156-
// However, nOldFeeRate is a calculated value from the tx fee/size, so
157-
// add 1 satoshi to the result, because it may have been rounded down.
158-
if (nNewFeeRate.GetFeePerK() < nOldFeeRate.GetFeePerK() + 1 + walletIncrementalRelayFee.GetFeePerK()) {
159-
nNewFeeRate = CFeeRate(nOldFeeRate.GetFeePerK() + 1 + walletIncrementalRelayFee.GetFeePerK());
160-
new_fee = nNewFeeRate.GetFee(maxNewTxSize);
161-
}
135+
CAmount minTotalFee = nOldFeeRate.GetFee(maxNewTxSize) + nodeIncrementalRelayFee.GetFee(maxNewTxSize);
136+
if (total_fee < minTotalFee) {
137+
errors.push_back(strprintf("Insufficient totalFee, must be at least %s (oldFee %s + incrementalFee %s)",
138+
FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxNewTxSize)), FormatMoney(nodeIncrementalRelayFee.GetFee(maxNewTxSize))));
139+
return Result::INVALID_PARAMETER;
140+
}
141+
CAmount requiredFee = GetRequiredFee(*wallet, maxNewTxSize);
142+
if (total_fee < requiredFee) {
143+
errors.push_back(strprintf("Insufficient totalFee (cannot be less than required fee %s)",
144+
FormatMoney(requiredFee)));
145+
return Result::INVALID_PARAMETER;
162146
}
163147

164148
// Check that in all cases the new fee doesn't violate maxTxFee
@@ -175,14 +159,14 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
175159
// in a rare situation where the mempool minimum fee increased significantly since the fee estimation just a
176160
// moment earlier. In this case, we report an error to the user, who may use total_fee to make an adjustment.
177161
CFeeRate minMempoolFeeRate = wallet->chain().mempoolMinFee();
162+
CFeeRate nNewFeeRate = CFeeRate(total_fee, maxNewTxSize);
178163
if (nNewFeeRate.GetFeePerK() < minMempoolFeeRate.GetFeePerK()) {
179164
errors.push_back(strprintf(
180165
"New fee rate (%s) is lower than the minimum fee rate (%s) to get into the mempool -- "
181-
"the totalFee value should be at least %s or the settxfee value should be at least %s to add transaction",
166+
"the totalFee value should be at least %s to add transaction",
182167
FormatMoney(nNewFeeRate.GetFeePerK()),
183168
FormatMoney(minMempoolFeeRate.GetFeePerK()),
184-
FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize)),
185-
FormatMoney(minMempoolFeeRate.GetFeePerK())));
169+
FormatMoney(minMempoolFeeRate.GetFee(maxNewTxSize))));
186170
return Result::WALLET_ERROR;
187171
}
188172

@@ -212,6 +196,109 @@ Result CreateTransaction(const CWallet* wallet, const uint256& txid, const CCoin
212196
}
213197
}
214198

199+
return Result::OK;
200+
}
201+
202+
203+
Result CreateRateBumpTransaction(CWallet* wallet, const uint256& txid, const CCoinControl& coin_control, std::vector<std::string>& errors,
204+
CAmount& old_fee, CAmount& new_fee, CMutableTransaction& mtx)
205+
{
206+
// We are going to modify coin control later, copy to re-use
207+
CCoinControl new_coin_control(coin_control);
208+
209+
auto locked_chain = wallet->chain().lock();
210+
LOCK(wallet->cs_wallet);
211+
errors.clear();
212+
auto it = wallet->mapWallet.find(txid);
213+
if (it == wallet->mapWallet.end()) {
214+
errors.push_back("Invalid or non-wallet transaction id");
215+
return Result::INVALID_ADDRESS_OR_KEY;
216+
}
217+
const CWalletTx& wtx = it->second;
218+
219+
Result result = PreconditionChecks(*locked_chain, wallet, wtx, errors);
220+
if (result != Result::OK) {
221+
return result;
222+
}
223+
224+
// Fill in recipients(and preserve a single change key if there is one)
225+
std::vector<CRecipient> recipients;
226+
for (const auto& output : wtx.tx->vout) {
227+
if (!wallet->IsChange(output)) {
228+
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
229+
recipients.push_back(recipient);
230+
} else {
231+
CTxDestination change_dest;
232+
ExtractDestination(output.scriptPubKey, change_dest);
233+
new_coin_control.destChange = change_dest;
234+
}
235+
}
236+
237+
// Get the fee rate of the original transaction. This is calculated from
238+
// the tx fee/vsize, so it may have been rounded down. Add 1 satoshi to the
239+
// result.
240+
old_fee = wtx.GetDebit(ISMINE_SPENDABLE) - wtx.tx->GetValueOut();
241+
int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
242+
// Feerate of thing we are bumping
243+
CFeeRate feerate(old_fee, txSize);
244+
feerate += CFeeRate(1);
245+
246+
// The node has a configurable incremental relay fee. Increment the fee by
247+
// the minimum of that and the wallet's conservative
248+
// WALLET_INCREMENTAL_RELAY_FEE value to future proof against changes to
249+
// network wide policy for incremental relay fee that our node may not be
250+
// aware of. This ensures we're over the over the required relay fee rate
251+
// (BIP 125 rule 4). The replacement tx will be at least as large as the
252+
// original tx, so the total fee will be greater (BIP 125 rule 3)
253+
CFeeRate node_incremental_relay_fee = wallet->chain().relayIncrementalFee();
254+
CFeeRate wallet_incremental_relay_fee = CFeeRate(WALLET_INCREMENTAL_RELAY_FEE);
255+
feerate += std::max(node_incremental_relay_fee, wallet_incremental_relay_fee);
256+
257+
// Fee rate must also be at least the wallet's GetMinimumFeeRate
258+
CFeeRate min_feerate(GetMinimumFeeRate(*wallet, new_coin_control, /* feeCalc */ nullptr));
259+
260+
// Set the required fee rate for the replacement transaction in coin control.
261+
new_coin_control.m_feerate = std::max(feerate, min_feerate);
262+
263+
// Fill in required inputs we are double-spending(all of them)
264+
// N.B.: bip125 doesn't require all the inputs in the replaced transaction to be
265+
// used in the replacement transaction, but it's very important for wallets to make
266+
// sure that happens. If not, it would be possible to bump a transaction A twice to
267+
// A2 and A3 where A2 and A3 don't conflict (or alternatively bump A to A2 and A2
268+
// to A3 where A and A3 don't conflict). If both later get confirmed then the sender
269+
// has accidentally double paid.
270+
for (const auto& inputs : wtx.tx->vin) {
271+
new_coin_control.Select(COutPoint(inputs.prevout));
272+
}
273+
new_coin_control.fAllowOtherInputs = true;
274+
275+
// We cannot source new unconfirmed inputs(bip125 rule 2)
276+
new_coin_control.m_min_depth = 1;
277+
278+
CTransactionRef tx_new = MakeTransactionRef();
279+
CReserveKey reservekey(wallet);
280+
CAmount fee_ret;
281+
int change_pos_in_out = -1; // No requested location for change
282+
std::string fail_reason;
283+
if (!wallet->CreateTransaction(*locked_chain, recipients, tx_new, reservekey, fee_ret, change_pos_in_out, fail_reason, new_coin_control, false)) {
284+
errors.push_back("Unable to create transaction: " + fail_reason);
285+
return Result::WALLET_ERROR;
286+
}
287+
288+
// If change key hasn't been ReturnKey'ed by this point, we take it out of keypool
289+
reservekey.KeepKey();
290+
291+
// Write back new fee if successful
292+
new_fee = fee_ret;
293+
294+
// Write back transaction
295+
mtx = CMutableTransaction(*tx_new);
296+
// Mark new tx not replaceable, if requested.
297+
if (!coin_control.m_signal_bip125_rbf.get_value_or(wallet->m_signal_rbf)) {
298+
for (auto& input : mtx.vin) {
299+
if (input.nSequence < 0xfffffffe) input.nSequence = 0xfffffffe;
300+
}
301+
}
215302

216303
return Result::OK;
217304
}

src/wallet/feebumper.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ enum class Result
2828
//! Return whether transaction can be bumped.
2929
bool TransactionCanBeBumped(const CWallet* wallet, const uint256& txid);
3030

31-
//! Create bumpfee transaction.
32-
Result CreateTransaction(const CWallet* wallet,
31+
//! Create bumpfee transaction based on total amount.
32+
Result CreateTotalBumpTransaction(const CWallet* wallet,
3333
const uint256& txid,
3434
const CCoinControl& coin_control,
3535
CAmount total_fee,
@@ -38,6 +38,15 @@ Result CreateTransaction(const CWallet* wallet,
3838
CAmount& new_fee,
3939
CMutableTransaction& mtx);
4040

41+
//! Create bumpfee transaction based on feerate estimates.
42+
Result CreateRateBumpTransaction(CWallet* wallet,
43+
const uint256& txid,
44+
const CCoinControl& coin_control,
45+
std::vector<std::string>& errors,
46+
CAmount& old_fee,
47+
CAmount& new_fee,
48+
CMutableTransaction& mtx);
49+
4150
//! Sign the new transaction,
4251
//! @return false if the tx couldn't be found or if it was
4352
//! impossible to create the signature(s)

src/wallet/rpcwallet.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3167,9 +3167,9 @@ static UniValue bumpfee(const JSONRPCRequest& request)
31673167
RPCHelpMan{"bumpfee",
31683168
"\nBumps the fee of an opt-in-RBF transaction T, replacing it with a new transaction B.\n"
31693169
"An opt-in RBF transaction with the given txid must be in the wallet.\n"
3170-
"The command will pay the additional fee by decreasing (or perhaps removing) its change output.\n"
3171-
"If the change output is not big enough to cover the increased fee, the command will currently fail\n"
3172-
"instead of adding new inputs to compensate. (A future implementation could improve this.)\n"
3170+
"The command will pay the additional fee by reducing change outputs or adding inputs when necessary. It may add a new change output if one does not already exist.\n"
3171+
"If `totalFee` is given, adding inputs is not supported, so there must be a single change output that is big enough or it will fail.\n"
3172+
"All inputs in the original transaction will be included in the replacement transaction.\n"
31733173
"The command will fail if the wallet or mempool contains a transaction that spends one of T's outputs.\n"
31743174
"By default, the new fee will be calculated automatically using estimatesmartfee.\n"
31753175
"The user can specify a confirmation target for estimatesmartfee.\n"
@@ -3266,7 +3266,14 @@ static UniValue bumpfee(const JSONRPCRequest& request)
32663266
CAmount old_fee;
32673267
CAmount new_fee;
32683268
CMutableTransaction mtx;
3269-
feebumper::Result res = feebumper::CreateTransaction(pwallet, hash, coin_control, totalFee, errors, old_fee, new_fee, mtx);
3269+
feebumper::Result res;
3270+
if (totalFee > 0) {
3271+
// Targeting total fee bump. Requires a change output of sufficient size.
3272+
res = feebumper::CreateTotalBumpTransaction(pwallet, hash, coin_control, totalFee, errors, old_fee, new_fee, mtx);
3273+
} else {
3274+
// Targeting feerate bump.
3275+
res = feebumper::CreateRateBumpTransaction(pwallet, hash, coin_control, errors, old_fee, new_fee, mtx);
3276+
}
32703277
if (res != feebumper::Result::OK) {
32713278
switch(res) {
32723279
case feebumper::Result::INVALID_ADDRESS_OR_KEY:

src/wallet/wallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2736,7 +2736,7 @@ bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std
27362736
LOCK(cs_wallet);
27372737
{
27382738
std::vector<COutput> vAvailableCoins;
2739-
AvailableCoins(*locked_chain, vAvailableCoins, true, &coin_control);
2739+
AvailableCoins(*locked_chain, vAvailableCoins, true, &coin_control, 1, MAX_MONEY, MAX_MONEY, 0, coin_control.m_min_depth);
27402740
CoinSelectionParams coin_selection_params; // Parameters for coin selection, init with dummy
27412741

27422742
// Create change script that will be used if we need change

0 commit comments

Comments
 (0)