Skip to content

Commit 90bfa9d

Browse files
committed
Merge bitcoin/bitcoin#27308: bumpfee: avoid making bumped transactions with too low fee when replacing outputs
d52fa1b tests: Make sure that bumpfee feerate checks work when replacing outputs (Andrew Chow) be177c1 bumpfee: Check the correct feerate when replacing outputs (Andrew Chow) Pull request description: When replacing the outputs of a transaction during `bumpfee`, it is possible to accidentally create a transaction that will not be accepted into the mempool as it does not meet the incremental relay fee requirements. This occurs because the size estimation used for checking the provided feerate does not account for the replaced outputs; it instead uses the original outputs. When the replaced outputs is significantly different from the original, there can be a large difference in estimated transaction sizes that can make a transaction miss the absolute fee requirements for the incremental relay fee. Unfortunately we do not currently inform the user when the bumped transaction fails to relay, so they could use `bumpfee` and think the transaction has been bumped when it actually has not. This issue is resolved by replacing the outputs before doing the size estimation, and also updating the feerate checker to use the actual fee values when calculating the required minimum fee. Also added a test for this scenario. ACKs for top commit: ishaanam: reACK d52fa1b Xekyo: reACK bitcoin/bitcoin@d52fa1b Tree-SHA512: d18301b587465322dd3fb1bb86496c3675265a56072047576e2baa5cf907dd3b54778f30721f662f0c235709a5568427c18542eb7efbfb6fdd9f481fe676c66b
2 parents 3650e74 + d52fa1b commit 90bfa9d

File tree

2 files changed

+37
-11
lines changed

2 files changed

+37
-11
lines changed

src/wallet/feebumper.cpp

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWallet
6363
}
6464

6565
//! Check if the user provided a valid feeRate
66-
static feebumper::Result CheckFeeRate(const CWallet& wallet, const CWalletTx& wtx, const CFeeRate& newFeerate, const int64_t maxTxSize, CAmount old_fee, std::vector<bilingual_str>& errors)
66+
static feebumper::Result CheckFeeRate(const CWallet& wallet, const CFeeRate& newFeerate, const int64_t maxTxSize, CAmount old_fee, std::vector<bilingual_str>& errors)
6767
{
6868
// check that fee rate is higher than mempool's minimum fee
6969
// (no point in bumping fee if we know that the new tx won't be accepted to the mempool)
@@ -84,15 +84,12 @@ static feebumper::Result CheckFeeRate(const CWallet& wallet, const CWalletTx& wt
8484

8585
CFeeRate incrementalRelayFee = std::max(wallet.chain().relayIncrementalFee(), CFeeRate(WALLET_INCREMENTAL_RELAY_FEE));
8686

87-
// Given old total fee and transaction size, calculate the old feeRate
88-
const int64_t txSize = GetVirtualTransactionSize(*(wtx.tx));
89-
CFeeRate nOldFeeRate(old_fee, txSize);
9087
// Min total fee is old fee + relay fee
91-
CAmount minTotalFee = nOldFeeRate.GetFee(maxTxSize) + incrementalRelayFee.GetFee(maxTxSize);
88+
CAmount minTotalFee = old_fee + incrementalRelayFee.GetFee(maxTxSize);
9289

9390
if (new_total_fee < minTotalFee) {
9491
errors.push_back(strprintf(Untranslated("Insufficient total fee %s, must be at least %s (oldFee %s + incrementalFee %s)"),
95-
FormatMoney(new_total_fee), FormatMoney(minTotalFee), FormatMoney(nOldFeeRate.GetFee(maxTxSize)), FormatMoney(incrementalRelayFee.GetFee(maxTxSize))));
92+
FormatMoney(new_total_fee), FormatMoney(minTotalFee), FormatMoney(old_fee), FormatMoney(incrementalRelayFee.GetFee(maxTxSize))));
9693
return feebumper::Result::INVALID_PARAMETER;
9794
}
9895

@@ -234,7 +231,8 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
234231
// is one). If outputs vector is non-empty, replace original
235232
// outputs with its contents, otherwise use original outputs.
236233
std::vector<CRecipient> recipients;
237-
for (const auto& output : outputs.empty() ? wtx.tx->vout : outputs) {
234+
const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs;
235+
for (const auto& output : txouts) {
238236
if (!OutputIsChange(wallet, output)) {
239237
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
240238
recipients.push_back(recipient);
@@ -249,13 +247,14 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
249247
// The user provided a feeRate argument.
250248
// We calculate this here to avoid compiler warning on the cs_wallet lock
251249
// We need to make a temporary transaction with no input witnesses as the dummy signer expects them to be empty for external inputs
252-
CMutableTransaction mtx{*wtx.tx};
253-
for (auto& txin : mtx.vin) {
250+
CMutableTransaction temp_mtx{*wtx.tx};
251+
for (auto& txin : temp_mtx.vin) {
254252
txin.scriptSig.clear();
255253
txin.scriptWitness.SetNull();
256254
}
257-
const int64_t maxTxSize{CalculateMaximumSignedTxSize(CTransaction(mtx), &wallet, &new_coin_control).vsize};
258-
Result res = CheckFeeRate(wallet, wtx, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
255+
temp_mtx.vout = txouts;
256+
const int64_t maxTxSize{CalculateMaximumSignedTxSize(CTransaction(temp_mtx), &wallet, &new_coin_control).vsize};
257+
Result res = CheckFeeRate(wallet, *new_coin_control.m_feerate, maxTxSize, old_fee, errors);
259258
if (res != Result::OK) {
260259
return res;
261260
}

test/functional/wallet_bumpfee.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
assert_equal,
2727
assert_greater_than,
2828
assert_raises_rpc_error,
29+
get_fee,
2930
)
3031
from test_framework.wallet import MiniWallet
3132

@@ -100,6 +101,7 @@ def run_test(self):
100101
test_change_script_match(self, rbf_node, dest_address)
101102
test_settxfee(self, rbf_node, dest_address)
102103
test_maxtxfee_fails(self, rbf_node, dest_address)
104+
test_feerate_checks_replaced_outputs(self, rbf_node)
103105
# These tests wipe out a number of utxos that are expected in other tests
104106
test_small_output_with_feerate_succeeds(self, rbf_node, dest_address)
105107
test_no_more_inputs_fails(self, rbf_node, dest_address)
@@ -668,5 +670,30 @@ def test_no_more_inputs_fails(self, rbf_node, dest_address):
668670
self.clear_mempool()
669671

670672

673+
def test_feerate_checks_replaced_outputs(self, rbf_node):
674+
self.log.info("Test that feerate checks use replaced outputs")
675+
outputs = []
676+
for i in range(50):
677+
outputs.append({rbf_node.getnewaddress(address_type="bech32"): 1})
678+
tx_res = rbf_node.send(outputs=outputs, fee_rate=5)
679+
tx_details = rbf_node.gettransaction(txid=tx_res["txid"], verbose=True)
680+
681+
# Calculate the minimum feerate required for the bump to work.
682+
# Since the bumped tx will replace all of the outputs with a single output, we can estimate that its size will 31 * (len(outputs) - 1) bytes smaller
683+
tx_size = tx_details["decoded"]["vsize"]
684+
est_bumped_size = tx_size - (len(tx_details["decoded"]["vout"]) - 1) * 31
685+
inc_fee_rate = max(rbf_node.getmempoolinfo()["incrementalrelayfee"], Decimal(0.00005000)) # Wallet has a fixed incremental relay fee of 5 sat/vb
686+
# RPC gives us fee as negative
687+
min_fee = (-tx_details["fee"] + get_fee(est_bumped_size, inc_fee_rate)) * Decimal(1e8)
688+
min_fee_rate = (min_fee / est_bumped_size).quantize(Decimal("1.000"))
689+
690+
# Attempt to bumpfee and replace all outputs with a single one using a feerate slightly less than the minimum
691+
new_outputs = [{rbf_node.getnewaddress(address_type="bech32"): 49}]
692+
assert_raises_rpc_error(-8, "Insufficient total fee", rbf_node.bumpfee, tx_res["txid"], {"fee_rate": min_fee_rate - 1, "outputs": new_outputs})
693+
694+
# Bumpfee and replace all outputs with a single one using the minimum feerate
695+
rbf_node.bumpfee(tx_res["txid"], {"fee_rate": min_fee_rate, "outputs": new_outputs})
696+
697+
671698
if __name__ == "__main__":
672699
BumpFeeTest().main()

0 commit comments

Comments
 (0)