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