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