mempool_cluster.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2024-present The Bitcoin Core developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test cluster mempool accessors and limits"""
   6  
   7  from decimal import Decimal
   8  
   9  from test_framework.mempool_util import (
  10      DEFAULT_CLUSTER_LIMIT,
  11      DEFAULT_CLUSTER_SIZE_LIMIT_KVB,
  12  )
  13  from test_framework.messages import (
  14      COIN,
  15  )
  16  from test_framework.test_framework import BitcoinTestFramework
  17  from test_framework.wallet import (
  18      MiniWallet,
  19  )
  20  from test_framework.util import (
  21      assert_equal,
  22      assert_greater_than,
  23      assert_greater_than_or_equal,
  24      assert_raises_rpc_error,
  25  )
  26  
  27  def weight_to_vsize(weight):
  28      # Divide by 4, round up
  29      return (weight + 3) // 4
  30  
  31  def cleanup(func):
  32      def wrapper(self, *args, **kwargs):
  33          func(self, *args, **kwargs)
  34  
  35          # Mine blocks to clear the mempool and replenish the wallet's confirmed UTXOs.
  36          while (len(self.nodes[0].getrawmempool()) > 0):
  37              self.generate(self.nodes[0], 1)
  38          self.wallet.rescan_utxos(include_mempool=True)
  39      return wrapper
  40  
  41  class MempoolClusterTest(BitcoinTestFramework):
  42      def set_test_params(self):
  43          self.num_nodes = 1
  44  
  45      def add_chain_cluster(self, node, cluster_count, target_vsize=None):
  46          """Create a cluster of transactions, with the count specified.
  47          The topology is a chain: the i'th transaction depends on the (i-1)'th transaction.
  48          Optionally provide a target_vsize for each transaction.
  49          """
  50          parent_tx = self.wallet.send_self_transfer(from_node=node, confirmed_only=True, target_vsize=target_vsize)
  51          utxo_to_spend = parent_tx["new_utxo"]
  52          all_txids = [parent_tx["txid"]]
  53          all_results = [parent_tx]
  54  
  55          while len(all_results) < cluster_count:
  56              next_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_to_spend, target_vsize=target_vsize)
  57              assert next_tx["txid"] in node.getrawmempool()
  58  
  59              # Confirm that each transaction is in the same cluster as the first.
  60              assert_equal(node.getmempoolcluster(next_tx['txid']), node.getmempoolcluster(parent_tx['txid']))
  61  
  62              # Confirm that the ancestors are what we expect
  63              mempool_ancestors = node.getmempoolancestors(next_tx['txid'])
  64              assert_equal(sorted(mempool_ancestors), sorted(all_txids))
  65  
  66              # Confirm that each successive transaction is added as a descendant.
  67              assert all([ next_tx["txid"] in node.getmempooldescendants(x) for x in all_txids ])
  68  
  69              # Update for next iteration
  70              all_results.append(next_tx)
  71              all_txids.append(next_tx["txid"])
  72              utxo_to_spend = next_tx["new_utxo"]
  73  
  74          assert_equal(node.getmempoolcluster(parent_tx['txid'])['txcount'], cluster_count)
  75          return all_results
  76  
  77      def check_feerate_diagram(self, node):
  78          """Sanity check the feerate diagram."""
  79          feeratediagram = node.getmempoolfeeratediagram()
  80          last_val = {"weight": 0, "fee": 0}
  81          for x in feeratediagram:
  82              # The weight is always positive, except for the first iteration
  83              assert (x['weight'] > 0
  84                     or x['fee'] == 0)
  85              # Monotonically decreasing fee per weight
  86              assert_greater_than_or_equal(last_val['fee'] * x['weight'], x['fee'] * last_val['weight'])
  87              last_val = x
  88  
  89      def test_limit_enforcement(self, cluster_submitted, target_vsize_per_tx=None):
  90          """
  91          the cluster may change as a result of these transactions, so cluster_submitted is mutated accordingly
  92          """
  93          # Cluster has already been submitted and has at least 3 transactions, otherwise this test won't work.
  94          assert_greater_than_or_equal(len(cluster_submitted), 3)
  95          node = self.nodes[0]
  96          last_result = cluster_submitted[-1]
  97  
  98          # Test that adding one more transaction to the cluster will fail.
  99          bad_tx = self.wallet.create_self_transfer(utxo_to_spend=last_result["new_utxo"], target_vsize=target_vsize_per_tx)
 100          assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, bad_tx["hex"])
 101  
 102          # It should also limit cluster sizes during replacement
 103          utxo_to_double_spend = self.wallet.get_utxo(confirmed_only=True)
 104          fee = Decimal("0.000001")
 105          tx_to_replace = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_double_spend, fee=fee)
 106          node.sendrawtransaction(tx_to_replace["hex"])
 107  
 108          # Multiply fee by 5, which should easily cover the cost to replace (but
 109          # is still too large a cluster). Otherwise, use the target vsize at
 110          # 10sat/vB
 111          fee_to_use = target_vsize_per_tx * 10 if target_vsize_per_tx is not None else int(fee * COIN * 5)
 112          bad_tx_also_replacement = self.wallet.create_self_transfer_multi(
 113              utxos_to_spend=[last_result["new_utxo"], utxo_to_double_spend],
 114              target_vsize=target_vsize_per_tx,
 115              fee_per_output=fee_to_use,
 116          )
 117          assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, bad_tx_also_replacement["hex"])
 118  
 119          # Replace the last transaction. We are extending the cluster by one, but also removing one: 64 + 1 - 1 = 64
 120          # In the case of vsize, it should similarly cancel out.
 121          second_to_last_utxo = cluster_submitted[-2]["new_utxo"]
 122          fee_to_beat = cluster_submitted[-1]["fee"]
 123          vsize_to_use = cluster_submitted[-1]["tx"].get_vsize() if target_vsize_per_tx is not None else None
 124          good_tx_replacement = self.wallet.create_self_transfer(utxo_to_spend=second_to_last_utxo, fee=fee_to_beat * 5, target_vsize=vsize_to_use)
 125          node.sendrawtransaction(good_tx_replacement["hex"], maxfeerate=0)
 126  
 127          cluster_submitted[-1] = good_tx_replacement
 128  
 129      def test_limit_enforcement_package(self, cluster_submitted):
 130          node = self.nodes[0]
 131          # Create a package from the second to last transaction.
 132          # This shouldn't work because the effect is {max_cluster_count} + 2 - 1 = {max_cluster_count} + 1
 133          last_utxo = cluster_submitted[-2]["new_utxo"]
 134          fee_to_beat = cluster_submitted[-1]["fee"]
 135          # We do not use package RBF here because it has additional restrictions on mempool ancestors.
 136          parent_tx_bad = self.wallet.create_self_transfer(utxo_to_spend=last_utxo, fee=fee_to_beat * 5)
 137          child_tx_bad = self.wallet.create_self_transfer(utxo_to_spend=parent_tx_bad["new_utxo"])
 138          # The parent should be submitted, but the child rejected.
 139          result_parent_only = node.submitpackage([parent_tx_bad["hex"], child_tx_bad["hex"]])
 140  
 141          assert parent_tx_bad["txid"] in node.getrawmempool()
 142          assert child_tx_bad["txid"] not in node.getrawmempool()
 143          assert_equal(result_parent_only["package_msg"], "transaction failed")
 144          assert_equal(result_parent_only["tx-results"][child_tx_bad["wtxid"]]["error"], "too-large-cluster")
 145          assert_equal(result_parent_only["replaced-transactions"], [cluster_submitted[-1]["txid"]])
 146  
 147          # Now, create a package from the third to last transaction.
 148          # This should work because the effect is {max_cluster_count} + 2 - 2 = {max_cluster_count}
 149          third_to_last_utxo = cluster_submitted[-3]["new_utxo"]
 150          # Tweak locktime to not recreate same tx as its meant to replace, fee needs to be even higher
 151          parent_tx_good = self.wallet.create_self_transfer(utxo_to_spend=third_to_last_utxo, locktime=1, fee=fee_to_beat * 10)
 152          child_tx_good = self.wallet.create_self_transfer(utxo_to_spend=parent_tx_good["new_utxo"])
 153          assert parent_tx_good["txid"] != cluster_submitted[-2]["txid"]
 154          assert child_tx_good["txid"] != parent_tx_bad["txid"]
 155          result_both_good = node.submitpackage([parent_tx_good["hex"], child_tx_good["hex"]], maxfeerate=0)
 156          assert_equal(result_both_good["package_msg"], "success")
 157          assert parent_tx_good["txid"] in node.getrawmempool()
 158          assert child_tx_good["txid"] in node.getrawmempool()
 159          assert_equal(set(result_both_good["replaced-transactions"]), set([parent_tx_bad["txid"], cluster_submitted[-2]["txid"]]))
 160  
 161      @cleanup
 162      def test_cluster_count_limit(self, max_cluster_count):
 163          node = self.nodes[0]
 164          cluster_submitted = self.add_chain_cluster(node, max_cluster_count)
 165          self.check_feerate_diagram(node)
 166          for result in cluster_submitted:
 167              assert_equal(node.getmempoolcluster(result["txid"])['txcount'], max_cluster_count)
 168  
 169          self.log.info("Test that cluster count limit is enforced")
 170          self.test_limit_enforcement(cluster_submitted)
 171          self.log.info("Test that the resulting cluster count is correctly calculated in a package")
 172          self.test_limit_enforcement_package(cluster_submitted)
 173  
 174      @cleanup
 175      def test_cluster_size_limit(self, max_cluster_size_vbytes):
 176          node = self.nodes[0]
 177          # This number should be smaller than the cluster count limit.
 178          num_txns = 10
 179          # Leave some buffer so it is possible to add a reasonably-sized transaction.
 180          target_vsize_per_tx = int((max_cluster_size_vbytes - 500) / num_txns)
 181          cluster_submitted = self.add_chain_cluster(node, num_txns, target_vsize_per_tx)
 182  
 183          vsize_remaining = max_cluster_size_vbytes - weight_to_vsize(node.getmempoolcluster(cluster_submitted[0]["txid"])['clusterweight'])
 184          self.log.info("Test that cluster size limit is enforced")
 185          self.test_limit_enforcement(cluster_submitted, target_vsize_per_tx=vsize_remaining + 4)
 186  
 187          # Try another cluster and add a small transaction: it should succeed
 188          last_result = cluster_submitted[-1]
 189          small_tx = self.wallet.create_self_transfer(utxo_to_spend=last_result["new_utxo"], target_vsize=vsize_remaining)
 190          node.sendrawtransaction(small_tx["hex"])
 191  
 192      @cleanup
 193      def test_cluster_merging(self, max_cluster_count):
 194          node = self.nodes[0]
 195  
 196          self.log.info(f"Test merging 2 clusters with transaction counts totaling {max_cluster_count}")
 197          for num_txns_cluster1 in [1, 5, 10]:
 198              # Create a chain of transactions
 199              cluster1 = self.add_chain_cluster(node, num_txns_cluster1)
 200              for result in cluster1:
 201                  node.sendrawtransaction(result["hex"])
 202              utxo_from_cluster1 = cluster1[-1]["new_utxo"]
 203  
 204              # Make the next cluster, which contains the remaining transactions
 205              assert_greater_than(max_cluster_count, num_txns_cluster1)
 206              num_txns_cluster2 = max_cluster_count - num_txns_cluster1
 207              cluster2 = self.add_chain_cluster(node, num_txns_cluster2)
 208              for result in cluster2:
 209                  node.sendrawtransaction(result["hex"])
 210              utxo_from_cluster2 = cluster2[-1]["new_utxo"]
 211  
 212              # Now create a transaction that spends from both clusters, which would merge them.
 213              tx_merger = self.wallet.create_self_transfer_multi(utxos_to_spend=[utxo_from_cluster1, utxo_from_cluster2])
 214              assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger["hex"])
 215  
 216              # Spending from the clusters independently should work
 217              tx_spending_cluster1 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_from_cluster1)
 218              tx_spending_cluster2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_from_cluster2)
 219              assert tx_spending_cluster1["txid"] in node.getrawmempool()
 220              assert tx_spending_cluster2["txid"] in node.getrawmempool()
 221  
 222          self.log.info(f"Test merging {max_cluster_count} clusters with 1 transaction spending from all of them")
 223          utxos_to_merge = []
 224          for _ in range(max_cluster_count):
 225              # Use a confirmed utxo to ensure distinct clusters
 226              confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)
 227              singleton = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=confirmed_utxo)
 228              assert singleton["txid"] in node.getrawmempool()
 229              utxos_to_merge.append(singleton["new_utxo"])
 230  
 231          assert_equal(len(utxos_to_merge), max_cluster_count)
 232          tx_merger = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge)
 233          assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger["hex"])
 234  
 235          # Spending from 1 fewer cluster should work
 236          tx_merger_all_but_one = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge[:-1])
 237          node.sendrawtransaction(tx_merger_all_but_one["hex"])
 238          assert tx_merger_all_but_one["txid"] in node.getrawmempool()
 239  
 240      @cleanup
 241      def test_cluster_merging_size(self, max_cluster_size_vbytes):
 242          node = self.nodes[0]
 243  
 244          self.log.info(f"Test merging clusters with sizes totaling {max_cluster_size_vbytes} vB")
 245          num_txns = 10
 246          # Leave some buffer so it is possible to add a reasonably-sized transaction.
 247          utxos_to_merge = []
 248          vsize_remaining = max_cluster_size_vbytes
 249          for _ in range(num_txns):
 250              confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)
 251              singleton = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=confirmed_utxo)
 252              assert singleton["txid"] in node.getrawmempool()
 253              utxos_to_merge.append(singleton["new_utxo"])
 254              vsize_remaining -= singleton["tx"].get_vsize()
 255  
 256          assert_greater_than_or_equal(vsize_remaining, 500)
 257  
 258          # Create a transaction spending from all clusters that exceeds the cluster size limit.
 259          tx_merger_too_big = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge, target_vsize=vsize_remaining + 4, fee_per_output=10000)
 260          assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger_too_big["hex"])
 261  
 262          # A transaction that is slightly smaller should work.
 263          tx_merger_small = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge[:-1], target_vsize=vsize_remaining - 4, fee_per_output=10000)
 264          node.sendrawtransaction(tx_merger_small["hex"])
 265          assert tx_merger_small["txid"] in node.getrawmempool()
 266  
 267      @cleanup
 268      def test_cluster_limit_rbf(self, max_cluster_count):
 269          node = self.nodes[0]
 270  
 271          # Use min feerate for the to-be-replaced transactions. There are many, so replacement cost can be expensive.
 272          min_feerate = node.getmempoolinfo()["mempoolminfee"]
 273  
 274          self.log.info("Test that cluster size calculation takes RBF into account")
 275          utxos_created_by_parents = []
 276          fees_rbf_sats = 0
 277          for _ in range(max_cluster_count - 1):
 278              parent_tx = self.wallet.send_self_transfer(from_node=node, confirmed_only=True)
 279              utxo_to_replace = parent_tx["new_utxo"]
 280              child_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_to_replace, fee_rate=min_feerate)
 281  
 282              fees_rbf_sats += int(child_tx["fee"] * COIN)
 283              utxos_created_by_parents.append(utxo_to_replace)
 284  
 285          # This transaction would create a cluster of size max_cluster_count
 286          # Importantly, the node should account for the fact that half of the transactions will be replaced.
 287          tx_merger_replacer = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_created_by_parents, fee_per_output=fees_rbf_sats * 2)
 288          node.sendrawtransaction(tx_merger_replacer["hex"])
 289          assert tx_merger_replacer["txid"] in node.getrawmempool()
 290          assert_equal(node.getmempoolcluster(tx_merger_replacer["txid"])['txcount'], max_cluster_count)
 291  
 292          self.log.info("Test that cluster size calculation takes package RBF into account")
 293          utxos_to_replace = []
 294          fee_rbf_decimal = 0
 295          for _ in range(max_cluster_count):
 296              confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)
 297              tx_to_replace = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=confirmed_utxo, fee_rate=min_feerate)
 298              fee_rbf_decimal += tx_to_replace["fee"]
 299              utxos_to_replace.append(confirmed_utxo)
 300  
 301          tx_replacer = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_replace)
 302          assert tx_replacer["txid"] not in node.getrawmempool()
 303          tx_replacer_sponsor = self.wallet.create_self_transfer(utxo_to_spend=tx_replacer["new_utxos"][0], fee=fee_rbf_decimal * 2)
 304  
 305          node.submitpackage([tx_replacer["hex"], tx_replacer_sponsor["hex"]], maxfeerate=0)
 306          assert tx_replacer["txid"] in node.getrawmempool()
 307          assert tx_replacer_sponsor["txid"] in node.getrawmempool()
 308          assert_equal(node.getmempoolcluster(tx_replacer["txid"])['txcount'], 2)
 309  
 310      @cleanup
 311      def test_getmempoolcluster(self):
 312          node = self.nodes[0]
 313  
 314          self.log.info("Testing getmempoolcluster")
 315  
 316          assert_equal(node.getrawmempool(), [])
 317  
 318          # Key should exist and be trivially optimal
 319          assert node.getmempoolinfo()["optimal"]
 320  
 321          # Not in-mempool
 322          not_mempool_tx = self.wallet.create_self_transfer()
 323          assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolcluster, not_mempool_tx["txid"])
 324  
 325          # Test that chunks are being recomputed properly
 326  
 327          # One chunk with one tx
 328          first_chunk_tx = self.wallet.send_self_transfer(from_node=node)
 329          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 330          assert_equal(first_chunk_info, {'clusterweight': first_chunk_tx["tx"].get_weight(), 'txcount': 1, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunk_tx["tx"].get_weight(), 'txs': [first_chunk_tx["txid"]]}]})
 331  
 332          # Another unconnected tx, nothing should change
 333          self.wallet.send_self_transfer(from_node=node)
 334          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 335          assert_equal(first_chunk_info, {'clusterweight': first_chunk_tx["tx"].get_weight(), 'txcount': 1, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunk_tx["tx"].get_weight(), 'txs': [first_chunk_tx["txid"]]}]})
 336  
 337          # Second connected tx, makes one chunk still with high enough fee
 338          second_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=first_chunk_tx["new_utxo"], fee_rate=Decimal("0.01"))
 339          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 340          # output is same across same cluster transactions
 341          assert_equal(first_chunk_info, node.getmempoolcluster(second_chunk_tx["txid"]))
 342          chunkweight = first_chunk_tx["tx"].get_weight() + second_chunk_tx["tx"].get_weight()
 343          chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"]
 344          assert_equal(first_chunk_info, {'clusterweight': chunkweight, 'txcount': 2, 'chunks': [{'chunkfee': chunkfee, 'chunkweight': chunkweight, 'txs': [first_chunk_tx["txid"], second_chunk_tx["txid"]]}]})
 345  
 346          # Third connected tx, makes one chunk still with high enough fee
 347          third_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=second_chunk_tx["new_utxo"], fee_rate=Decimal("0.1"))
 348          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 349          # output is same across same cluster transactions
 350          assert_equal(first_chunk_info, node.getmempoolcluster(third_chunk_tx["txid"]))
 351          chunkweight = first_chunk_tx["tx"].get_weight() + second_chunk_tx["tx"].get_weight() + third_chunk_tx["tx"].get_weight()
 352          chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"] + third_chunk_tx["fee"]
 353          assert_equal(first_chunk_info, {'clusterweight': chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': chunkfee, 'chunkweight': chunkweight, 'txs': [first_chunk_tx["txid"], second_chunk_tx["txid"], third_chunk_tx["txid"]]}]})
 354  
 355          # Now test single cluster with each tx being its own chunk
 356  
 357          # One chunk with one tx
 358          first_chunk_tx = self.wallet.send_self_transfer(from_node=node)
 359          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 360          assert_equal(first_chunk_info, {'clusterweight': first_chunk_tx["tx"].get_weight(), 'txcount': 1, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunk_tx["tx"].get_weight(), 'txs': [first_chunk_tx["txid"]]}]})
 361  
 362          # Second connected tx, lower fee
 363          second_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=first_chunk_tx["new_utxo"], fee_rate=Decimal("0.000002"))
 364          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 365          # output is same across same cluster transactions
 366          assert_equal(first_chunk_info, node.getmempoolcluster(second_chunk_tx["txid"]))
 367          first_chunkweight = first_chunk_tx["tx"].get_weight()
 368          second_chunkweight = second_chunk_tx["tx"].get_weight()
 369          assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight, 'txcount': 2, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"], 'chunkweight': second_chunkweight, 'txs': [second_chunk_tx["txid"]]}]})
 370  
 371          # Third connected tx, even lower fee
 372          third_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=second_chunk_tx["new_utxo"], fee_rate=Decimal("0.000001"))
 373          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 374          # output is same across same cluster transactions
 375          assert_equal(first_chunk_info, node.getmempoolcluster(third_chunk_tx["txid"]))
 376          first_chunkweight = first_chunk_tx["tx"].get_weight()
 377          second_chunkweight = second_chunk_tx["tx"].get_weight()
 378          third_chunkweight = third_chunk_tx["tx"].get_weight()
 379          chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"] + third_chunk_tx["fee"]
 380          assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight + third_chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"], 'chunkweight': second_chunkweight, 'txs': [second_chunk_tx["txid"]]}, {'chunkfee': third_chunk_tx["fee"], 'chunkweight': third_chunkweight, 'txs': [third_chunk_tx["txid"]]}]})
 381  
 382          # We expect known optimality directly after txn submission
 383          assert node.getmempoolinfo()["optimal"]
 384  
 385          # If we prioritise the last transaction it can join the second transaction's chunk.
 386          node.prioritisetransaction(third_chunk_tx["txid"], 0, int(third_chunk_tx["fee"]*COIN) + 1)
 387          first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])
 388          assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight + third_chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"] + 2*third_chunk_tx["fee"] + Decimal("0.00000001"), 'chunkweight': second_chunkweight + third_chunkweight, 'txs': [second_chunk_tx["txid"], third_chunk_tx["txid"]]}]})
 389  
 390      def run_test(self):
 391          node = self.nodes[0]
 392          self.wallet = MiniWallet(node)
 393          self.generate(self.wallet, 400)
 394  
 395          self.test_getmempoolcluster()
 396  
 397          self.test_cluster_limit_rbf(DEFAULT_CLUSTER_LIMIT)
 398  
 399          for cluster_size_limit_kvb in [10, 20, 33, 100, DEFAULT_CLUSTER_SIZE_LIMIT_KVB]:
 400              self.log.info(f"-> Resetting node with -limitclustersize={cluster_size_limit_kvb}")
 401              self.restart_node(0, extra_args=[f"-limitclustersize={cluster_size_limit_kvb}"])
 402  
 403              cluster_size_limit = cluster_size_limit_kvb * 1000
 404              self.test_cluster_size_limit(cluster_size_limit)
 405              self.test_cluster_merging_size(cluster_size_limit)
 406  
 407          for cluster_count_limit in [4, 10, 16, 32, DEFAULT_CLUSTER_LIMIT]:
 408              self.log.info(f"-> Resetting node with -limitclustercount={cluster_count_limit}")
 409              self.restart_node(0, extra_args=[f"-limitclustercount={cluster_count_limit}"])
 410  
 411              self.test_cluster_count_limit(cluster_count_limit)
 412              if cluster_count_limit > 10:
 413                  self.test_cluster_merging(cluster_count_limit)
 414  
 415  
 416  if __name__ == '__main__':
 417      MempoolClusterTest(__file__).main()
 418