mempool_truc.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 from decimal import Decimal
6
7 from test_framework.test_framework import BitcoinTestFramework
8 from test_framework.util import (
9 assert_not_equal,
10 assert_equal,
11 assert_greater_than,
12 assert_greater_than_or_equal,
13 assert_raises_rpc_error,
14 get_fee,
15 )
16 from test_framework.wallet import (
17 COIN,
18 DEFAULT_FEE,
19 MiniWallet,
20 )
21 from test_framework.blocktools import (
22 create_empty_fork,
23 )
24
25 MAX_REPLACEMENT_CANDIDATES = 100
26 TRUC_MAX_VSIZE = 10000
27 TRUC_CHILD_MAX_VSIZE = 1000
28
29 def cleanup(extra_args=None):
30 def decorator(func):
31 def wrapper(self):
32 if extra_args is not None:
33 self.restart_node(0, extra_args=extra_args)
34 func(self)
35
36 # Clear mempool again after test
37 self.generate(self.nodes[0], 1)
38 if extra_args is not None:
39 self.restart_node(0)
40 return wrapper
41 return decorator
42
43 class MempoolTRUC(BitcoinTestFramework):
44 def set_test_params(self):
45 self.num_nodes = 1
46 self.extra_args = [[]]
47 self.setup_clean_chain = True
48
49 def check_mempool(self, txids):
50 """Assert exact contents of the node's mempool (by txid)."""
51 mempool_contents = self.nodes[0].getrawmempool()
52 assert_equal(len(txids), len(mempool_contents))
53 assert all([txid in txids for txid in mempool_contents])
54
55 def trigger_reorg(self, fork_blocks):
56 """Trigger reorg of the fork blocks."""
57 for block in fork_blocks:
58 self.nodes[0].submitblock(block.serialize().hex())
59 assert_equal(self.nodes[0].getbestblockhash(), fork_blocks[-1].hash_hex)
60
61 @cleanup()
62 def test_truc_max_vsize(self):
63 node = self.nodes[0]
64 self.log.info("Test TRUC-specific maximum transaction vsize")
65 tx_v3_heavy = self.wallet.create_self_transfer(target_vsize=TRUC_MAX_VSIZE + 1, version=3)
66 assert_greater_than_or_equal(tx_v3_heavy["tx"].get_vsize(), TRUC_MAX_VSIZE)
67 expected_error_heavy = f"TRUC-violation, version=3 tx {tx_v3_heavy['txid']} (wtxid={tx_v3_heavy['wtxid']}) is too big"
68 assert_raises_rpc_error(-26, expected_error_heavy, node.sendrawtransaction, tx_v3_heavy["hex"])
69 self.check_mempool([])
70
71 # Ensure we are hitting the TRUC-specific limit and not something else
72 tx_v2_heavy = self.wallet.send_self_transfer(from_node=node, target_vsize=TRUC_MAX_VSIZE + 1, version=2)
73 self.check_mempool([tx_v2_heavy["txid"]])
74
75 @cleanup()
76 def test_truc_acceptance(self):
77 node = self.nodes[0]
78 self.log.info("Test a child of a TRUC transaction cannot be more than 1000vB")
79 tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3)
80 self.check_mempool([tx_v3_parent_normal["txid"]])
81 tx_v3_child_heavy = self.wallet.create_self_transfer(
82 utxo_to_spend=tx_v3_parent_normal["new_utxo"],
83 target_vsize=TRUC_CHILD_MAX_VSIZE + 1,
84 version=3
85 )
86 assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), TRUC_CHILD_MAX_VSIZE)
87 expected_error_child_heavy = f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big"
88 assert_raises_rpc_error(-26, expected_error_child_heavy, node.sendrawtransaction, tx_v3_child_heavy["hex"])
89 self.check_mempool([tx_v3_parent_normal["txid"]])
90 # tx has no descendants
91 assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1)
92
93 self.log.info("Test that, during replacements, only the new transaction counts for TRUC descendant limit")
94 tx_v3_child_almost_heavy = self.wallet.send_self_transfer(
95 from_node=node,
96 fee_rate=DEFAULT_FEE,
97 utxo_to_spend=tx_v3_parent_normal["new_utxo"],
98 target_vsize=TRUC_CHILD_MAX_VSIZE - 3,
99 version=3
100 )
101 assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_almost_heavy["tx"].get_vsize())
102 self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy["txid"]])
103 assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)
104 tx_v3_child_almost_heavy_rbf = self.wallet.send_self_transfer(
105 from_node=node,
106 fee_rate=DEFAULT_FEE * 2,
107 utxo_to_spend=tx_v3_parent_normal["new_utxo"],
108 target_vsize=875,
109 version=3
110 )
111 assert_greater_than_or_equal(tx_v3_child_almost_heavy["tx"].get_vsize() + tx_v3_child_almost_heavy_rbf["tx"].get_vsize(),
112 TRUC_CHILD_MAX_VSIZE)
113 self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]])
114 assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2)
115
116 @cleanup(extra_args=None)
117 def test_truc_replacement(self):
118 node = self.nodes[0]
119 self.log.info("Test TRUC transactions may be replaced by TRUC transactions")
120 utxo_v3_bip125 = self.wallet.get_utxo()
121 tx_v3_bip125 = self.wallet.send_self_transfer(
122 from_node=node,
123 fee_rate=DEFAULT_FEE,
124 utxo_to_spend=utxo_v3_bip125,
125 version=3
126 )
127 self.check_mempool([tx_v3_bip125["txid"]])
128
129 tx_v3_bip125_rbf = self.wallet.send_self_transfer(
130 from_node=node,
131 fee_rate=DEFAULT_FEE * 2,
132 utxo_to_spend=utxo_v3_bip125,
133 version=3
134 )
135 self.check_mempool([tx_v3_bip125_rbf["txid"]])
136
137 self.log.info("Test TRUC transactions may be replaced by non-TRUC (BIP125) transactions")
138 tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer(
139 from_node=node,
140 fee_rate=DEFAULT_FEE * 3,
141 utxo_to_spend=utxo_v3_bip125,
142 version=2
143 )
144 self.check_mempool([tx_v3_bip125_rbf_v2["txid"]])
145
146 self.log.info("Test that replacements cannot cause violation of inherited TRUC")
147 utxo_v3_parent = self.wallet.get_utxo()
148 tx_v3_parent = self.wallet.send_self_transfer(
149 from_node=node,
150 fee_rate=DEFAULT_FEE,
151 utxo_to_spend=utxo_v3_parent,
152 version=3
153 )
154 tx_v3_child = self.wallet.send_self_transfer(
155 from_node=node,
156 fee_rate=DEFAULT_FEE,
157 utxo_to_spend=tx_v3_parent["new_utxo"],
158 version=3
159 )
160 self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])
161
162 tx_v3_child_rbf_v2 = self.wallet.create_self_transfer(
163 fee_rate=DEFAULT_FEE * 2,
164 utxo_to_spend=tx_v3_parent["new_utxo"],
165 version=2
166 )
167 expected_error_v2_v3 = f"TRUC-violation, non-version=3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})"
168 assert_raises_rpc_error(-26, expected_error_v2_v3, node.sendrawtransaction, tx_v3_child_rbf_v2["hex"])
169 self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]])
170
171
172 @cleanup()
173 def test_truc_reorg(self):
174 node = self.nodes[0]
175
176 # Prep for fork
177 fork_blocks = create_empty_fork(node)
178 self.log.info("Test that, during a reorg, TRUC rules are not enforced")
179 self.check_mempool([])
180
181 # Testing 2<-3 versions allowed
182 tx_v2_block = self.wallet.create_self_transfer(version=2)
183
184 # Testing 3<-2 versions allowed
185 tx_v3_block = self.wallet.create_self_transfer(version=3)
186
187 # Testing overly-large child size
188 tx_v3_block2 = self.wallet.create_self_transfer(version=3)
189
190 # Also create a linear chain of 3 TRUC transactions that will be directly mined, followed by one v2 in-mempool after block is made
191 tx_chain_1 = self.wallet.create_self_transfer(version=3)
192 tx_chain_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_1["new_utxo"], version=3)
193 tx_chain_3 = self.wallet.create_self_transfer(utxo_to_spend=tx_chain_2["new_utxo"], version=3)
194
195 tx_to_mine = [tx_v3_block["hex"], tx_v2_block["hex"], tx_v3_block2["hex"], tx_chain_1["hex"], tx_chain_2["hex"], tx_chain_3["hex"]]
196 self.generateblock(node, output="raw(42)", transactions=tx_to_mine)
197
198 self.check_mempool([])
199
200 tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2)
201 tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3)
202 tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_vsize=1250, version=3)
203 assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE)
204 tx_chain_4 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_chain_3["new_utxo"], version=2)
205 self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_4["txid"]])
206
207 # Reorg should have all block transactions re-accepted, ignoring TRUC enforcement
208 self.trigger_reorg(fork_blocks)
209 self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"], tx_chain_1["txid"], tx_chain_2["txid"], tx_chain_3["txid"], tx_chain_4["txid"]])
210
211 @cleanup(extra_args=["-limitclustercount=1"])
212 def test_nondefault_package_limits(self):
213 """
214 Max standard tx size + TRUC rules imply the cluster rules (at their default
215 values), but those checks must not be skipped. Ensure both sets of checks are done by
216 changing the cluster limit configurations.
217 """
218 node = self.nodes[0]
219 self.log.info("Test that a decreased cluster count limit also applies to TRUC child")
220 parent_target_vsize = 9990
221 child_target_vsize = 500
222 tx_v3_parent_large1 = self.wallet.send_self_transfer(
223 from_node=node,
224 target_vsize=parent_target_vsize,
225 version=3
226 )
227 tx_v3_child_large1 = self.wallet.create_self_transfer(
228 utxo_to_spend=tx_v3_parent_large1["new_utxo"],
229 target_vsize=child_target_vsize,
230 version=3
231 )
232
233 # Parent and child are within v3 limits, but cluster count limit is exceeded.
234 assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large1["tx"].get_vsize())
235 assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_large1["tx"].get_vsize())
236
237 assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_v3_child_large1["hex"])
238 self.check_mempool([tx_v3_parent_large1["txid"]])
239 assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1)
240 self.generate(node, 1)
241
242 self.log.info("Test that a decreased limitclustersize also applies to TRUC child")
243 self.restart_node(0, extra_args=["-limitclustersize=10", "-acceptnonstdtxn=1"])
244 tx_v3_parent_large2 = self.wallet.send_self_transfer(from_node=node, target_vsize=parent_target_vsize, version=3)
245 tx_v3_child_large2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large2["new_utxo"], target_vsize=child_target_vsize, version=3)
246 # Parent and child are within TRUC limits
247 assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large2["tx"].get_vsize())
248 assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_large2["tx"].get_vsize())
249 assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_v3_child_large2["hex"])
250 self.log.info("Test that a decreased limitclustercount also applies to TRUC transactions")
251 self.restart_node(0, extra_args=["-limitclustercount=1", "-acceptnonstdtxn=1"])
252 assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_v3_child_large2["hex"])
253 self.check_mempool([tx_v3_parent_large2["txid"]])
254
255 @cleanup()
256 def test_truc_ancestors_package(self):
257 self.log.info("Test that TRUC ancestor limits are checked within the package")
258 node = self.nodes[0]
259 tx_v3_parent_normal = self.wallet.create_self_transfer(
260 fee_rate=0,
261 target_vsize=1001,
262 version=3
263 )
264 tx_v3_parent_2_normal = self.wallet.create_self_transfer(
265 fee_rate=0,
266 target_vsize=1001,
267 version=3
268 )
269 tx_v3_child_multiparent = self.wallet.create_self_transfer_multi(
270 utxos_to_spend=[tx_v3_parent_normal["new_utxo"], tx_v3_parent_2_normal["new_utxo"]],
271 fee_per_output=10000,
272 version=3
273 )
274 tx_v3_child_heavy = self.wallet.create_self_transfer_multi(
275 utxos_to_spend=[tx_v3_parent_normal["new_utxo"]],
276 target_vsize=TRUC_CHILD_MAX_VSIZE + 1,
277 fee_per_output=10000,
278 version=3
279 )
280
281 self.check_mempool([])
282 result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_parent_2_normal["hex"], tx_v3_child_multiparent["hex"]])
283 assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors")
284 self.check_mempool([])
285
286 self.check_mempool([])
287 result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_child_heavy["hex"]])
288 # tx_v3_child_heavy is heavy based on vsize, not sigops.
289 assert_equal(result['package_msg'], f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes")
290 self.check_mempool([])
291
292 tx_v3_parent = self.wallet.create_self_transfer(version=3)
293 tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxo"], version=3)
294 tx_v3_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_child["new_utxo"], version=3)
295 result = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child["hex"], tx_v3_grandchild["hex"]])
296 for txresult in result:
297 assert_equal(txresult["package-error"], f"TRUC-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors")
298
299 @cleanup(extra_args=None)
300 def test_truc_ancestors_package_and_mempool(self):
301 """
302 A TRUC transaction in a package cannot have 2 TRUC parents.
303 Test that if we have a transaction graph A -> B -> C, where A, B, C are
304 all TRUC transactions, that we cannot use submitpackage to get the
305 transactions all into the mempool.
306
307 Verify, in particular, that if A is already in the mempool, then
308 submitpackage(B, C) will fail.
309 """
310 node = self.nodes[0]
311 self.log.info("Test that TRUC ancestor limits include transactions within the package and all in-mempool ancestors")
312 # This is our transaction "A":
313 tx_in_mempool = self.wallet.send_self_transfer(from_node=node, version=3)
314
315 # Verify that A is in the mempool
316 self.check_mempool([tx_in_mempool["txid"]])
317
318 # tx_0fee_parent is our transaction "B"; just create it.
319 tx_0fee_parent = self.wallet.create_self_transfer(utxo_to_spend=tx_in_mempool["new_utxo"], fee=0, fee_rate=0, version=3)
320
321 # tx_child_violator is our transaction "C"; create it:
322 tx_child_violator = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx_0fee_parent["new_utxo"]], version=3)
323
324 # submitpackage(B, C) should fail
325 result = node.submitpackage([tx_0fee_parent["hex"], tx_child_violator["hex"]])
326 assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors")
327 self.check_mempool([tx_in_mempool["txid"]])
328
329 @cleanup(extra_args=None)
330 def test_sibling_eviction_package(self):
331 """
332 When a transaction has a mempool sibling, it may be eligible for sibling eviction.
333 However, this option is only available in single transaction acceptance. It doesn't work in
334 a multi-testmempoolaccept (where RBF is disabled) or when doing package CPFP.
335 """
336 self.log.info("Test TRUC sibling eviction in submitpackage and multi-testmempoolaccept")
337 node = self.nodes[0]
338 # Add a parent + child to mempool
339 tx_mempool_parent = self.wallet.send_self_transfer_multi(
340 from_node=node,
341 utxos_to_spend=[self.wallet.get_utxo()],
342 num_outputs=2,
343 version=3
344 )
345 tx_mempool_sibling = self.wallet.send_self_transfer(
346 from_node=node,
347 utxo_to_spend=tx_mempool_parent["new_utxos"][0],
348 version=3
349 )
350 self.check_mempool([tx_mempool_parent["txid"], tx_mempool_sibling["txid"]])
351
352 tx_sibling_1 = self.wallet.create_self_transfer(
353 utxo_to_spend=tx_mempool_parent["new_utxos"][1],
354 version=3,
355 fee_rate=DEFAULT_FEE*100,
356 )
357 tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_sibling_1["new_utxo"], version=3)
358
359 tx_sibling_2 = self.wallet.create_self_transfer(
360 utxo_to_spend=tx_mempool_parent["new_utxos"][0],
361 version=3,
362 fee_rate=DEFAULT_FEE*200,
363 )
364
365 tx_sibling_3 = self.wallet.create_self_transfer(
366 utxo_to_spend=tx_mempool_parent["new_utxos"][1],
367 version=3,
368 fee_rate=0,
369 )
370 tx_bumps_parent_with_sibling = self.wallet.create_self_transfer(
371 utxo_to_spend=tx_sibling_3["new_utxo"],
372 version=3,
373 fee_rate=DEFAULT_FEE*300,
374 )
375
376 # Fails with another non-related transaction via testmempoolaccept
377 tx_unrelated = self.wallet.create_self_transfer(version=3)
378 result_test_unrelated = node.testmempoolaccept([tx_sibling_1["hex"], tx_unrelated["hex"]])
379 assert_equal(result_test_unrelated[0]["reject-reason"], "TRUC-violation")
380
381 # Fails in a package via testmempoolaccept
382 result_test_1p1c = node.testmempoolaccept([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]])
383 assert_equal(result_test_1p1c[0]["reject-reason"], "TRUC-violation")
384
385 # Allowed when tx is submitted in a package and evaluated individually.
386 # Note that the child failed since it would be the 3rd generation.
387 result_package_indiv = node.submitpackage([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]])
388 self.check_mempool([tx_mempool_parent["txid"], tx_sibling_1["txid"]])
389 expected_error_gen3 = f"TRUC-violation, tx {tx_has_mempool_uncle['txid']} (wtxid={tx_has_mempool_uncle['wtxid']}) would have too many ancestors"
390
391 assert_equal(result_package_indiv["tx-results"][tx_has_mempool_uncle['wtxid']]['error'], expected_error_gen3)
392
393 # Allowed when tx is submitted in a package with in-mempool parent (which is deduplicated).
394 node.submitpackage([tx_mempool_parent["hex"], tx_sibling_2["hex"]])
395 self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]])
396
397 # Child cannot pay for sibling eviction for parent, as it violates TRUC topology limits
398 result_package_cpfp = node.submitpackage([tx_sibling_3["hex"], tx_bumps_parent_with_sibling["hex"]])
399 self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]])
400 expected_error_cpfp = f"TRUC-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit"
401
402 assert_equal(result_package_cpfp["tx-results"][tx_sibling_3['wtxid']]['error'], expected_error_cpfp)
403
404
405 @cleanup()
406 def test_truc_package_inheritance(self):
407 self.log.info("Test that TRUC inheritance is checked within package")
408 node = self.nodes[0]
409 tx_v3_parent = self.wallet.create_self_transfer(
410 fee_rate=0,
411 target_vsize=1001,
412 version=3
413 )
414 tx_v2_child = self.wallet.create_self_transfer_multi(
415 utxos_to_spend=[tx_v3_parent["new_utxo"]],
416 fee_per_output=10000,
417 version=2
418 )
419 self.check_mempool([])
420 result = node.submitpackage([tx_v3_parent["hex"], tx_v2_child["hex"]])
421 assert_equal(result['package_msg'], f"TRUC-violation, non-version=3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})")
422 self.check_mempool([])
423
424 @cleanup(extra_args=None)
425 def test_truc_in_testmempoolaccept(self):
426 node = self.nodes[0]
427
428 self.log.info("Test that TRUC inheritance is accurately assessed in testmempoolaccept")
429 tx_v2 = self.wallet.create_self_transfer(version=2)
430 tx_v2_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=2)
431 tx_v3_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=3)
432 tx_v3 = self.wallet.create_self_transfer(version=3)
433 tx_v2_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=2)
434 tx_v3_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=3)
435
436 # testmempoolaccept paths don't require child-with-parents topology. Ensure that topology
437 # assumptions aren't made in inheritance checks.
438 test_accept_v2_and_v3 = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"]])
439 assert all([result["allowed"] for result in test_accept_v2_and_v3])
440
441 test_accept_v3_from_v2 = node.testmempoolaccept([tx_v2["hex"], tx_v3_from_v2["hex"]])
442 expected_error_v3_from_v2 = f"TRUC-violation, version=3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-version=3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})"
443 for result in test_accept_v3_from_v2:
444 assert_equal(result["package-error"], expected_error_v3_from_v2)
445
446 test_accept_v2_from_v3 = node.testmempoolaccept([tx_v3["hex"], tx_v2_from_v3["hex"]])
447 expected_error_v2_from_v3 = f"TRUC-violation, non-version=3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from version=3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})"
448 for result in test_accept_v2_from_v3:
449 assert_equal(result["package-error"], expected_error_v2_from_v3)
450
451 test_accept_pairs = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"], tx_v2_from_v2["hex"], tx_v3_from_v3["hex"]])
452 assert all([result["allowed"] for result in test_accept_pairs])
453
454 self.log.info("Test that descendant violations are caught in testmempoolaccept")
455 tx_v3_independent = self.wallet.create_self_transfer(version=3)
456 tx_v3_parent = self.wallet.create_self_transfer_multi(num_outputs=2, version=3)
457 tx_v3_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][0], version=3)
458 tx_v3_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][1], version=3)
459 test_accept_2children = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
460 expected_error_2children = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
461 for result in test_accept_2children:
462 assert_equal(result["package-error"], expected_error_2children)
463
464 # Extra TRUC transaction does not get incorrectly marked as extra descendant
465 test_accept_1child_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_independent["hex"]])
466 assert all([result["allowed"] for result in test_accept_1child_with_exra])
467
468 # Extra TRUC transaction does not make us ignore the extra descendant
469 test_accept_2children_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"], tx_v3_independent["hex"]])
470 expected_error_extra = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit"
471 for result in test_accept_2children_with_exra:
472 assert_equal(result["package-error"], expected_error_extra)
473 # Same result if the parent is already in mempool
474 node.sendrawtransaction(tx_v3_parent["hex"])
475 test_accept_2children_with_in_mempool_parent = node.testmempoolaccept([tx_v3_child_1["hex"], tx_v3_child_2["hex"]])
476 for result in test_accept_2children_with_in_mempool_parent:
477 assert_equal(result["package-error"], expected_error_extra)
478
479 @cleanup(extra_args=None)
480 def test_reorg_2child_rbf(self):
481 node = self.nodes[0]
482 self.log.info("Test that children of a TRUC transaction can be replaced individually, even if there are multiple due to reorg")
483
484 # Prep for fork
485 fork_blocks = create_empty_fork(node)
486 ancestor_tx = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3)
487 self.check_mempool([ancestor_tx["txid"]])
488
489 self.generate(node, 1)[0]
490 self.check_mempool([])
491
492 child_1 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][0])
493 child_2 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][1])
494 self.check_mempool([child_1["txid"], child_2["txid"]])
495
496 # Create a reorg, causing ancestor_tx to exceed the 1-child limit
497 self.trigger_reorg(fork_blocks)
498 self.check_mempool([ancestor_tx["txid"], child_1["txid"], child_2["txid"]])
499 assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3)
500
501 # Create a replacement of child_1. It does not conflict with child_2.
502 child_1_conflict = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][0], fee_rate=Decimal("0.01"))
503
504 # Ensure child_1 and child_1_conflict are different transactions
505 assert_not_equal(child_1_conflict["txid"], child_1["txid"])
506 self.check_mempool([ancestor_tx["txid"], child_1_conflict["txid"], child_2["txid"]])
507 assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3)
508
509 @cleanup(extra_args=None)
510 def test_truc_sibling_eviction(self):
511 self.log.info("Test sibling eviction for TRUC")
512 node = self.nodes[0]
513 tx_v3_parent = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3)
514 # This is the sibling to replace
515 tx_v3_child_1 = self.wallet.send_self_transfer(
516 from_node=node, utxo_to_spend=tx_v3_parent["new_utxos"][0], fee_rate=DEFAULT_FEE * 2, version=3
517 )
518 assert tx_v3_child_1["txid"] in node.getrawmempool()
519
520 self.log.info("Test tx must be higher feerate than sibling to evict it")
521 tx_v3_child_2_rule6 = self.wallet.create_self_transfer(
522 utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE, version=3
523 )
524 rule6_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule6['txid']}"
525 assert_raises_rpc_error(-26, rule6_str, node.sendrawtransaction, tx_v3_child_2_rule6["hex"])
526 self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']])
527
528 self.log.info("Test tx must meet absolute fee rules to evict sibling")
529 tx_v3_child_2_rule4 = self.wallet.create_self_transfer(
530 utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=2 * DEFAULT_FEE + Decimal("0.00000001"), version=3
531 )
532 rule4_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule4['txid']}, not enough additional fees to relay"
533 assert_raises_rpc_error(-26, rule4_str, node.sendrawtransaction, tx_v3_child_2_rule4["hex"])
534 self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']])
535
536 self.log.info("Test tx cannot cause more than 100 evictions including RBF and sibling eviction")
537 # First add 4 groups of 25 transactions.
538 utxos_for_conflict = []
539 txids_v2_100 = []
540 for _ in range(4):
541 confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)
542 utxos_for_conflict.append(confirmed_utxo)
543 # 25 is within descendant limits
544 chain_length = int(MAX_REPLACEMENT_CANDIDATES / 4)
545 chain = self.wallet.create_self_transfer_chain(chain_length=chain_length, utxo_to_spend=confirmed_utxo)
546 for item in chain:
547 txids_v2_100.append(item["txid"])
548 node.sendrawtransaction(item["hex"])
549 self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]])
550
551 # Replacing 100 transactions is fine
552 tx_v3_replacement_only = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_for_conflict, fee_per_output=4000000)
553 # Override maxfeerate - it costs a lot to replace these 100 transactions.
554 assert node.testmempoolaccept([tx_v3_replacement_only["hex"]], maxfeerate=0)[0]["allowed"]
555 self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]])
556
557 self.log.info("Test sibling eviction is successful if it meets all RBF rules")
558 tx_v3_child_2 = self.wallet.create_self_transfer(
559 utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE*10, version=3
560 )
561 node.sendrawtransaction(tx_v3_child_2["hex"])
562 self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_2["txid"]])
563
564 self.log.info("Test that it's possible to do a sibling eviction and RBF at the same time")
565 utxo_unrelated_conflict = self.wallet.get_utxo(confirmed_only=True)
566 tx_unrelated_replacee = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_unrelated_conflict)
567 assert tx_unrelated_replacee["txid"] in node.getrawmempool()
568
569 fee_to_beat = max(int(tx_v3_child_2["fee"] * COIN), int(tx_unrelated_replacee["fee"]*COIN))
570
571 tx_v3_child_3 = self.wallet.create_self_transfer_multi(
572 utxos_to_spend=[tx_v3_parent["new_utxos"][0], utxo_unrelated_conflict], fee_per_output=fee_to_beat*4, version=3
573 )
574 node.sendrawtransaction(tx_v3_child_3["hex"])
575 self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_3["txid"]])
576
577 @cleanup(extra_args=None)
578 def test_reorg_sibling_eviction_1p2c(self):
579 node = self.nodes[0]
580 self.log.info("Test that sibling eviction is not allowed when multiple siblings exist")
581
582 # Prep for fork
583 fork_blocks = create_empty_fork(node)
584 tx_with_multi_children = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=3, version=3, confirmed_only=True)
585 self.check_mempool([tx_with_multi_children["txid"]])
586
587 self.generate(node, 1)
588 self.check_mempool([])
589
590 tx_with_sibling1 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][0])
591 tx_with_sibling2 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][1])
592 self.check_mempool([tx_with_sibling1["txid"], tx_with_sibling2["txid"]])
593
594 # Create a reorg, bringing tx_with_multi_children back into the mempool with a descendant count of 3.
595 self.trigger_reorg(fork_blocks)
596 self.check_mempool([tx_with_multi_children["txid"], tx_with_sibling1["txid"], tx_with_sibling2["txid"]])
597 assert_equal(node.getmempoolentry(tx_with_multi_children["txid"])["descendantcount"], 3)
598
599 # Sibling eviction is not allowed because there are two siblings
600 tx_with_sibling3 = self.wallet.create_self_transfer(
601 version=3,
602 utxo_to_spend=tx_with_multi_children["new_utxos"][2],
603 fee_rate=DEFAULT_FEE*50
604 )
605 expected_error_2siblings = f"TRUC-violation, tx {tx_with_multi_children['txid']} (wtxid={tx_with_multi_children['wtxid']}) would exceed descendant count limit"
606 assert_raises_rpc_error(-26, expected_error_2siblings, node.sendrawtransaction, tx_with_sibling3["hex"])
607
608 # However, an RBF (with conflicting inputs) is possible even if the resulting cluster size exceeds 2
609 tx_with_sibling3_rbf = self.wallet.send_self_transfer(
610 from_node=node,
611 version=3,
612 utxo_to_spend=tx_with_multi_children["new_utxos"][0],
613 fee_rate=DEFAULT_FEE*50
614 )
615 self.check_mempool([tx_with_multi_children["txid"], tx_with_sibling3_rbf["txid"], tx_with_sibling2["txid"]])
616
617 @cleanup(extra_args=None)
618 def test_minrelay_in_package_combos(self):
619 node = self.nodes[0]
620 self.log.info("Test that all transaction versions can be under minrelaytxfee for various settings...")
621
622 for minrelay_setting in (0, 5, 10, 100, 500, 1000, 5000, 333333, 2500000):
623 self.log.info(f"-> Test -minrelaytxfee={minrelay_setting}sat/kvB...")
624 setting_decimal = minrelay_setting / Decimal(COIN)
625 self.restart_node(0, extra_args=[f"-minrelaytxfee={setting_decimal:.8f}", "-persistmempool=0"])
626 minrelayfeerate = node.getmempoolinfo()["minrelaytxfee"]
627 high_feerate = minrelayfeerate * 50
628
629 tx_v3_0fee_parent = self.wallet.create_self_transfer(fee=0, fee_rate=0, confirmed_only=True, version=3)
630 tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_0fee_parent["new_utxo"], fee_rate=high_feerate, version=3)
631 total_v3_fee = tx_v3_child["fee"] + tx_v3_0fee_parent["fee"]
632 total_v3_size = tx_v3_child["tx"].get_vsize() + tx_v3_0fee_parent["tx"].get_vsize()
633 assert_greater_than_or_equal(total_v3_fee, get_fee(total_v3_size, minrelayfeerate))
634 if minrelayfeerate > 0:
635 assert_greater_than(get_fee(tx_v3_0fee_parent["tx"].get_vsize(), minrelayfeerate), 0)
636 # Always need to pay at least 1 satoshi for entry, even if minimum feerate is very low
637 assert_greater_than(total_v3_fee, 0)
638 # Also create a version where the child is at minrelaytxfee
639 tx_v3_child_minrelay = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_0fee_parent["new_utxo"], fee_rate=minrelayfeerate, version=3)
640 result_truc_minrelay = node.submitpackage([tx_v3_0fee_parent["hex"], tx_v3_child_minrelay["hex"]])
641 assert_equal(result_truc_minrelay["package_msg"], "transaction failed")
642
643 tx_v2_0fee_parent = self.wallet.create_self_transfer(fee=0, fee_rate=0, confirmed_only=True, version=2)
644 tx_v2_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v2_0fee_parent["new_utxo"], fee_rate=high_feerate, version=2)
645 total_v2_fee = tx_v2_child["fee"] + tx_v2_0fee_parent["fee"]
646 total_v2_size = tx_v2_child["tx"].get_vsize() + tx_v2_0fee_parent["tx"].get_vsize()
647 assert_greater_than_or_equal(total_v2_fee, get_fee(total_v2_size, minrelayfeerate))
648 if minrelayfeerate > 0:
649 assert_greater_than(get_fee(tx_v2_0fee_parent["tx"].get_vsize(), minrelayfeerate), 0)
650 # Always need to pay at least 1 satoshi for entry, even if minimum feerate is very low
651 assert_greater_than(total_v2_fee, 0)
652
653 result_truc = node.submitpackage([tx_v3_0fee_parent["hex"], tx_v3_child["hex"]], maxfeerate=0)
654 assert_equal(result_truc["package_msg"], "success")
655
656 result_non_truc = node.submitpackage([tx_v2_0fee_parent["hex"], tx_v2_child["hex"]], maxfeerate=0)
657 assert_equal(result_non_truc["package_msg"], "success")
658 self.check_mempool([tx_v2_0fee_parent["txid"], tx_v2_child["txid"], tx_v3_0fee_parent["txid"], tx_v3_child["txid"]])
659
660
661 def run_test(self):
662 self.log.info("Generate blocks to create UTXOs")
663 node = self.nodes[0]
664 self.wallet = MiniWallet(node)
665 self.generate(self.wallet, 200)
666 self.test_truc_max_vsize()
667 self.test_truc_acceptance()
668 self.test_truc_replacement()
669 self.test_truc_reorg()
670 self.test_nondefault_package_limits()
671 self.test_truc_ancestors_package()
672 self.test_truc_ancestors_package_and_mempool()
673 self.test_sibling_eviction_package()
674 self.test_truc_package_inheritance()
675 self.test_truc_in_testmempoolaccept()
676 self.test_reorg_2child_rbf()
677 self.test_truc_sibling_eviction()
678 self.test_reorg_sibling_eviction_1p2c()
679 self.test_minrelay_in_package_combos()
680
681
682 if __name__ == "__main__":
683 MempoolTRUC(__file__).main()
684