test: add coverage for feebumper uncomputable cluster error path #35472

pull 151henry151 wants to merge 1 commits into bitcoin:master from 151henry151:test/feebumper-huge-cluster-coverage changing 1 files +74 −0
  1. 151henry151 commented at 5:23 AM on June 6, 2026: contributor

    Add functional test coverage for the error path in CheckFeeRate() when calculateCombinedBumpFee() returns nullopt because unconfirmed inputs span more than 500 mempool transactions (CTxMemPool::GatherClusters DoS limit).

    The crash on this path was fixed in #34870; this PR adds the missing test requested in #34902.

    The test builds eight independent 64-transaction clusters (512 total, above the 500 limit), keeps a wallet-owned RBF transaction that spends one output from each cluster out of the mempool, and asserts that bumpfee with an explicit fee_rate returns the expected wallet error instead of proceeding with an empty optional.

    Closes #34902.

    Testing

    • ./build/test/functional/wallet_bumpfee.py
    • Temporarily removed the early return in CheckFeeRate() and confirmed the test fails with bad optional_access instead of the expected error message
  2. DrahtBot added the label Tests on Jun 6, 2026
  3. DrahtBot commented at 5:23 AM on June 6, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35472.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK furszy, rapennas

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. 151henry151 commented at 5:24 AM on June 6, 2026: contributor

    @furszy Would you mind taking a look when you have a chance? This adds the functional test coverage for the error path you fixed in #34870.

    CheckFeeRate() only runs when the caller passes an explicit fee_rate; default bumpfee uses fee estimation and does not hit this branch, which is why the test passes {"fee_rate": NORMAL}.

  5. DrahtBot added the label CI failed on Jun 6, 2026
  6. DrahtBot commented at 5:39 AM on June 6, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/27053679324/job/79853785327</sub> <sub>LLM reason (✨ experimental): CI failed because Python linting (py_lint/ruff) reported an error: comment contains an ambiguous × character (RUF003) in test/functional/wallet_bumpfee.py.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  7. 151henry151 force-pushed on Jun 6, 2026
  8. DrahtBot removed the label CI failed on Jun 6, 2026
  9. sedited requested review from furszy on Jul 24, 2026
  10. in test/functional/wallet_bumpfee.py:955 in 22fabac8e1
     950 | +        self.connect_nodes(1, 0)
     951 | +        self.sync_all()
     952 | +        excess = rbf_node.getbalance() - balance_before
     953 | +        if excess > 0:
     954 | +            rbf_node.sendtoaddress(peer_node.getnewaddress(), excess)
     955 | +        self.clear_mempool()
    


    furszy commented at 3:52 PM on July 24, 2026:

    I think you could simplify this:

    def test_bumpfee_uncomputable_cluster(self, rbf_node, dest_address):
        self.log.info('Test that bumpfee fails when unconfirmed UTXOs depend on an enormous cluster')
        # CheckFeeRate() can only fail to compute a combined bump fee when
        # CTxMemPool::GatherClusters() hits its 500 transaction limit. Clusters are capped
        # at 64 transactions, so we need the tx we bump to spend from 8 of them.
        #
        # That tx can't be in the mempool once they're full, or it would merge all 8 into
        # one and get rejected. We can't create it then either, since coin selection runs
        # into the same MiniMiner limit. So we broadcast it while the clusters are still
        # single transactions and evict it afterwards by raising -minrelaytxfee. It stays
        # in the wallet, which is all bumpfee needs.
        NUM_CLUSTERS = 8             # 8 * 64 = 512, above the 500 gather limit
        CLUSTER_SIZE = 64
        SEED_PAYMENT = COIN // 100   # paid to rbf_node by every seed
        SEED_FEE = 20_000            # high enough to survive the -minrelaytxfee bump below
        ORIGINAL_TX_FEE = 2000       # ~3 sat/vB, low enough to get evicted by it
    
        # Keep the peer disconnected so we don't relay txs to it.
        self.disconnect_nodes(0, 1)
    
        # Fund a MiniWallet on rbf_node so the cluster transactions end up in its mempool,
        # which is the one the bump fee calculation looks at. The tag keeps these coins
        # separate from the default MiniWallet other tests use on this node. get_utxo()
        # skips immature coinbases, so mine COINBASE_MATURITY blocks on top.
        miniwallet = MiniWallet(rbf_node, tag_name="feebumper_cluster")
        self.generatetoaddress(rbf_node, COINBASE_MATURITY + NUM_CLUSTERS,
                               miniwallet.get_address(), sync_fun=self.no_op)
        miniwallet.rescan_utxos()
    
        # Each seed pays rbf_node and keeps a MiniWallet output we grow the cluster from.
        rbf_spk = bytes.fromhex(rbf_node.getaddressinfo(rbf_node.getnewaddress())["scriptPubKey"])
        seeds = [miniwallet.send_to(from_node=rbf_node, scriptPubKey=rbf_spk,
                                    amount=SEED_PAYMENT, fee=SEED_FEE)
                 for _ in range(NUM_CLUSTERS)]
    
        # Broadcast the tx we'll bump while every cluster is still a single transaction.
        inputs = [{"txid": seed["txid"], "vout": seed["sent_vout"]} for seed in seeds]
        outputs = {dest_address: Decimal(NUM_CLUSTERS * SEED_PAYMENT - ORIGINAL_TX_FEE) / COIN}
        signed = rbf_node.signrawtransactionwithwallet(rbf_node.createrawtransaction(inputs, outputs))
        original_txid = rbf_node.sendrawtransaction(signed["hex"])
    
        # Bumping -minrelaytxfee to 10 sat/vB drops it on mempool reload and rejects the
        # resubmission the wallet does on startup. The seeds pay much more, so they stay.
        # Note the bump below uses NORMAL (100 sat/vB); if that ever ends up under the
        # -minrelaytxfee here, CheckFeeRate() bails on its mempool minimum check instead.
        self.restart_node(1, ["-minrelaytxfee=0.0001"] + self.extra_args[1])
        rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
        assert_equal(set(rbf_node.getrawmempool()), {seed["txid"] for seed in seeds})
        assert_equal(rbf_node.gettransaction(original_txid)["confirmations"], 0)
    
        # Grow each seed into a full cluster. MiniWallet's default 300 sat/vB keeps these
        # above the raised -minrelaytxfee.
        for seed in seeds:
            miniwallet.send_self_transfer_chain(from_node=rbf_node,
                                                chain_length=CLUSTER_SIZE - 1,
                                                utxo_to_spend=miniwallet.get_utxo(txid=seed["txid"]))
        # Make sure the setup worked, so the check below can't pass for the wrong reason.
        assert_equal(len(rbf_node.getrawmempool()), NUM_CLUSTERS * CLUSTER_SIZE)
    
        assert_raises_rpc_error(-4,
                                "Failed to calculate bump fees, because unconfirmed UTXOs depend on an enormous cluster of unconfirmed transactions.",
                                rbf_node.bumpfee, original_txid, {"fee_rate": NORMAL})
    
        # Restore initial state
        rbf_node.abandontransaction(original_txid)
        self.generate(rbf_node, 1, sync_fun=self.no_op)
        self.restart_node(1, self.extra_args[1])
        rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT)
        self.connect_nodes(1, 0)
        self.sync_all()
    
  11. test: add coverage for feebumper uncomputable cluster error path 47632ab2a3
  12. 151henry151 force-pushed on Jul 26, 2026
  13. 151henry151 commented at 11:47 PM on July 26, 2026: contributor

    Pushed in 47632ab — applied your simplification (squashed into the original commit). Thanks for the review.

  14. 151henry151 requested review from furszy on Jul 27, 2026
  15. furszy commented at 6:48 PM on July 31, 2026: member

    ACK 47632ab2a3876825576b3f07e6f80e799e6ed58f

    Tested it fails without the merged fix commit 6072a2a6a1fdde8444e3515f0d201de3fb3923a1

  16. sedited requested review from ismaelsadeeq on Aug 4, 2026
  17. rapennas commented at 7:32 PM on August 5, 2026: none

    Tested ACK 47632ab2a3

    Tested on Linux with GCC 13.3.0, based on master 8a4bab8e97.

    The functional test passed as proposed:

    ./build/test/functional/wallet_bumpfee.py
    

    I also tested the GatherClusters() boundary by locally changing the cluster sizes:

    • With 501 transactions ([64] * 7 + [53]), bumpfee returned the expected Failed to calculate bump fees... error.
    • With 500 transactions ([64] * 7 + [52]), the cluster gathering step succeeded and transaction creation subsequently returned Unable to create transaction. too many unconfirmed transactions in cluster.

    After updating the expected error for the local 500-transaction experiment, the functional test passed. This confirms that 500 transactions are still computable, while 501 is the first value that triggers the error path covered by this PR.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-11 10:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me