1 #!/usr/bin/env python3
2 # Copyright (c) 2024-present 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 """
6 Test opportunistic 1p1c package submission logic.
7 """
8 9 from decimal import Decimal
10 import time
11 from test_framework.mempool_util import (
12 DEFAULT_MIN_RELAY_TX_FEE,
13 fill_mempool,
14 )
15 from test_framework.messages import (
16 CInv,
17 COIN,
18 CTxInWitness,
19 MAX_BIP125_RBF_SEQUENCE,
20 MSG_WTX,
21 msg_inv,
22 msg_tx,
23 tx_from_hex,
24 )
25 from test_framework.p2p import (
26 P2PInterface,
27 )
28 from test_framework.test_framework import LimenkaTestFramework
29 from test_framework.util import (
30 assert_equal,
31 assert_greater_than,
32 )
33 from test_framework.wallet import (
34 MiniWallet,
35 MiniWalletMode,
36 )
37 38 # 1sat/vB feerate denominated in BTC/KvB
39 FEERATE_1SAT_VB = Decimal("0.00001000")
40 # Number of seconds to wait to ensure no getdata is received
41 GETDATA_WAIT = 60
42 43 def cleanup(func):
44 def wrapper(self, *args, **kwargs):
45 try:
46 func(self, *args, **kwargs)
47 finally:
48 self.nodes[0].disconnect_p2ps()
49 # Do not clear the node's mempool, as each test requires mempool min feerate > min
50 # relay feerate. However, do check that this is the case.
51 assert self.nodes[0].getmempoolinfo()["mempoolminfee"] > self.nodes[0].getnetworkinfo()["relayfee"]
52 # Ensure we do not try to spend the same UTXOs in subsequent tests, as they will look like RBF attempts.
53 self.wallet.rescan_utxos(include_mempool=True)
54 55 # Resets if mocktime was used
56 self.nodes[0].setmocktime(0)
57 return wrapper
58 59 class PackageRelayTest(LimenkaTestFramework):
60 def set_test_params(self):
61 self.setup_clean_chain = True
62 self.num_nodes = 1
63 self.extra_args = [[
64 "-datacarriersize=100000",
65 "-maxmempool=5",
66 ]]
67 self.supports_cli = False
68 69 def create_tx_below_mempoolminfee(self, wallet):
70 """Create a 1-input 0.1sat/vB transaction using a confirmed UTXO. Decrement and use
71 self.sequence so that subsequent calls to this function result in unique transactions."""
72 73 self.sequence -= 1
74 assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN)
75 76 return wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, sequence=self.sequence, confirmed_only=True)
77 78 @cleanup
79 def test_basic_child_then_parent(self):
80 node = self.nodes[0]
81 self.log.info("Check that opportunistic 1p1c logic works when child is received before parent")
82 83 low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
84 high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
85 86 peer_sender = node.add_p2p_connection(P2PInterface())
87 88 # 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input.
89 high_child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
90 peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
91 peer_sender.wait_for_getdata([high_child_wtxid_int])
92 peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
93 94 # 2. Node requests the missing parent by txid.
95 parent_txid_int = int(low_fee_parent["txid"], 16)
96 peer_sender.wait_for_getdata([parent_txid_int])
97 98 # 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted.
99 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
100 101 # 4. Both transactions should now be in mempool.
102 node_mempool = node.getrawmempool()
103 assert low_fee_parent["txid"] in node_mempool
104 assert high_fee_child["txid"] in node_mempool
105 106 node.disconnect_p2ps()
107 108 @cleanup
109 def test_basic_parent_then_child(self, wallet):
110 node = self.nodes[0]
111 low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
112 high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)
113 114 peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
115 peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")
116 117 # 1. Parent is relayed first. It is too low feerate.
118 parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
119 peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
120 peer_sender.wait_for_getdata([parent_wtxid_int])
121 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
122 assert low_fee_parent["txid"] not in node.getrawmempool()
123 124 # Send again from peer_ignored, check that it is ignored
125 peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
126 assert "getdata" not in peer_ignored.last_message
127 128 # 2. Child is relayed next. It is missing an input.
129 high_child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
130 peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
131 peer_sender.wait_for_getdata([high_child_wtxid_int])
132 peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
133 134 # 3. Node requests the missing parent by txid.
135 # It should do so even if it has previously rejected that parent for being too low feerate.
136 parent_txid_int = int(low_fee_parent["txid"], 16)
137 peer_sender.wait_for_getdata([parent_txid_int])
138 139 # 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted.
140 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
141 142 # 5. Both transactions should now be in mempool.
143 node_mempool = node.getrawmempool()
144 assert low_fee_parent["txid"] in node_mempool
145 assert high_fee_child["txid"] in node_mempool
146 147 @cleanup
148 def test_low_and_high_child(self, wallet):
149 node = self.nodes[0]
150 low_fee_parent = self.create_tx_below_mempoolminfee(wallet)
151 # This feerate is above mempoolminfee, but not enough to also bump the low feerate parent.
152 feerate_just_above = node.getmempoolinfo()["mempoolminfee"]
153 med_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=feerate_just_above)
154 high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
155 156 peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
157 peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")
158 159 self.log.info("Check that tx caches low fee parent + low fee child package rejections")
160 161 # 1. Send parent, rejected for being low feerate.
162 parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
163 peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
164 peer_sender.wait_for_getdata([parent_wtxid_int])
165 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
166 assert low_fee_parent["txid"] not in node.getrawmempool()
167 168 # Send again from peer_ignored, check that it is ignored
169 peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
170 assert "getdata" not in peer_ignored.last_message
171 172 # 2. Send an (orphan) child that has a higher feerate, but not enough to bump the parent.
173 med_child_wtxid_int = int(med_fee_child["tx"].getwtxid(), 16)
174 peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))
175 peer_sender.wait_for_getdata([med_child_wtxid_int])
176 peer_sender.send_and_ping(msg_tx(med_fee_child["tx"]))
177 178 # 3. Node requests the orphan's missing parent.
179 parent_txid_int = int(low_fee_parent["txid"], 16)
180 peer_sender.wait_for_getdata([parent_txid_int])
181 182 # 4. The low parent + low child are submitted as a package. They are not accepted due to low package feerate.
183 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
184 185 assert low_fee_parent["txid"] not in node.getrawmempool()
186 assert med_fee_child["txid"] not in node.getrawmempool()
187 188 # If peer_ignored announces the low feerate child, it should be ignored
189 peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))
190 assert "getdata" not in peer_ignored.last_message
191 # If either peer sends the parent again, package evaluation should not be attempted
192 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
193 peer_ignored.send_and_ping(msg_tx(low_fee_parent["tx"]))
194 195 assert low_fee_parent["txid"] not in node.getrawmempool()
196 assert med_fee_child["txid"] not in node.getrawmempool()
197 198 # 5. Send the high feerate (orphan) child
199 high_child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
200 peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))
201 peer_sender.wait_for_getdata([high_child_wtxid_int])
202 peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
203 204 # 6. Node requests the orphan's parent, even though it has already been rejected, both by
205 # itself and with a child. This is necessary, otherwise high_fee_child can be censored.
206 parent_txid_int = int(low_fee_parent["txid"], 16)
207 peer_sender.wait_for_getdata([parent_txid_int])
208 209 # 7. The low feerate parent + high feerate child are submitted as a package.
210 peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
211 212 # 8. Both transactions should now be in mempool
213 node_mempool = node.getrawmempool()
214 assert low_fee_parent["txid"] in node_mempool
215 assert high_fee_child["txid"] in node_mempool
216 assert med_fee_child["txid"] not in node_mempool
217 218 @cleanup
219 def test_orphan_consensus_failure(self):
220 self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer")
221 node = self.nodes[0]
222 low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
223 coin = low_fee_parent["new_utxo"]
224 address = node.get_deterministic_priv_key().address
225 # Create raw transaction spending the parent, but with no signature (a consensus error).
226 hex_orphan_no_sig = node.createrawtransaction([{"txid": coin["txid"], "vout": coin["vout"]}], {address : coin["value"] - Decimal("0.0001")})
227 tx_orphan_bad_wit = tx_from_hex(hex_orphan_no_sig)
228 tx_orphan_bad_wit.wit.vtxinwit.append(CTxInWitness())
229 tx_orphan_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
230 231 bad_orphan_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0)
232 parent_sender = node.add_p2p_connection(P2PInterface())
233 234 # 1. Child is received first. It is missing an input.
235 child_wtxid_int = int(tx_orphan_bad_wit.getwtxid(), 16)
236 bad_orphan_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
237 bad_orphan_sender.wait_for_getdata([child_wtxid_int])
238 bad_orphan_sender.send_and_ping(msg_tx(tx_orphan_bad_wit))
239 240 # 2. Node requests the missing parent by txid.
241 parent_txid_int = int(low_fee_parent["txid"], 16)
242 bad_orphan_sender.wait_for_getdata([parent_txid_int])
243 244 # 3. A different peer relays the parent. Package is not evaluated because the transactions
245 # were not sent from the same peer.
246 parent_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
247 248 # 4. Transactions should not be in mempool.
249 node_mempool = node.getrawmempool()
250 assert low_fee_parent["txid"] not in node_mempool
251 assert tx_orphan_bad_wit.rehash() not in node_mempool
252 253 # 5. Have the other peer send the tx too, so that tx_orphan_bad_wit package is attempted.
254 bad_orphan_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
255 256 # The bad orphan sender should not be disconnected.
257 bad_orphan_sender.sync_with_ping()
258 259 # The peer that didn't provide the orphan should not be disconnected.
260 parent_sender.sync_with_ping()
261 262 @cleanup
263 def test_parent_consensus_failure(self):
264 self.log.info("Check opportunistic 1p1c logic with consensus-invalid parent causes disconnect of the correct peer")
265 node = self.nodes[0]
266 low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)
267 high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)
268 269 # Create invalid version of parent with a bad signature.
270 tx_parent_bad_wit = tx_from_hex(low_fee_parent["hex"])
271 tx_parent_bad_wit.wit.vtxinwit.append(CTxInWitness())
272 tx_parent_bad_wit.wit.vtxinwit[0].scriptWitness.stack = [b'garbage']
273 274 package_sender = node.add_p2p_connection(P2PInterface())
275 fake_parent_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=0)
276 277 # 1. Child is received first. It is missing an input.
278 child_wtxid_int = int(high_fee_child["tx"].getwtxid(), 16)
279 package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=child_wtxid_int)]))
280 package_sender.wait_for_getdata([child_wtxid_int])
281 package_sender.send_and_ping(msg_tx(high_fee_child["tx"]))
282 283 # 2. Node requests the missing parent by txid.
284 parent_txid_int = int(tx_parent_bad_wit.rehash(), 16)
285 package_sender.wait_for_getdata([parent_txid_int])
286 287 # 3. A different node relays the parent. The parent is first evaluated by itself and
288 # rejected for being too low feerate. It is not evaluated as a package because the child was
289 # sent from a different peer, so we don't find out that the child is consensus-invalid.
290 fake_parent_sender.send_and_ping(msg_tx(tx_parent_bad_wit))
291 292 # 4. Transactions should not be in mempool.
293 node_mempool = node.getrawmempool()
294 assert tx_parent_bad_wit.rehash() not in node_mempool
295 assert high_fee_child["txid"] not in node_mempool
296 297 self.log.info("Check that fake parent does not cause orphan to be deleted and real package can still be submitted")
298 # 5. Child-sending should not have been punished and the orphan should remain in orphanage.
299 # It can send the "real" parent transaction, and the package is accepted.
300 parent_wtxid_int = int(low_fee_parent["tx"].getwtxid(), 16)
301 package_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))
302 package_sender.wait_for_getdata([parent_wtxid_int])
303 package_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))
304 305 node_mempool = node.getrawmempool()
306 assert low_fee_parent["txid"] in node_mempool
307 assert high_fee_child["txid"] in node_mempool
308 309 @cleanup
310 def test_multiple_parents(self):
311 self.log.info("Check that node does not request more than 1 previously-rejected low feerate parent")
312 313 node = self.nodes[0]
314 node.setmocktime(int(time.time()))
315 316 # 2-parent-1-child package where both parents are below mempool min feerate
317 parent_low_1 = self.create_tx_below_mempoolminfee(self.wallet_nonsegwit)
318 parent_low_2 = self.create_tx_below_mempoolminfee(self.wallet_nonsegwit)
319 child_bumping = self.wallet_nonsegwit.create_self_transfer_multi(
320 utxos_to_spend=[parent_low_1["new_utxo"], parent_low_2["new_utxo"]],
321 fee_per_output=999*parent_low_1["tx"].get_vsize(),
322 )
323 324 peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
325 326 # 1. Send both parents. Each should be rejected for being too low feerate.
327 # Send unsolicited so that we can later check that no "getdata" was ever received.
328 peer_sender.send_and_ping(msg_tx(parent_low_1["tx"]))
329 peer_sender.send_and_ping(msg_tx(parent_low_2["tx"]))
330 331 # parent_low_1 and parent_low_2 are rejected for being low feerate.
332 assert parent_low_1["txid"] not in node.getrawmempool()
333 assert parent_low_2["txid"] not in node.getrawmempool()
334 335 # 2. Send child.
336 peer_sender.send_and_ping(msg_tx(child_bumping["tx"]))
337 338 # 3. Node should not request any parents, as it should recognize that it will not accept
339 # multi-parent-1-child packages.
340 node.bumpmocktime(GETDATA_WAIT)
341 peer_sender.sync_with_ping()
342 assert "getdata" not in peer_sender.last_message
343 344 @cleanup
345 def test_other_parent_in_mempool(self):
346 self.log.info("Check opportunistic 1p1c fails if child already has another parent in mempool")
347 node = self.nodes[0]
348 349 # This parent needs CPFP
350 parent_low = self.create_tx_below_mempoolminfee(self.wallet)
351 # This parent does not need CPFP and can be submitted alone ahead of time
352 parent_high = self.wallet.create_self_transfer(fee_rate=FEERATE_1SAT_VB*10, confirmed_only=True)
353 child = self.wallet.create_self_transfer_multi(
354 utxos_to_spend=[parent_high["new_utxo"], parent_low["new_utxo"]],
355 fee_per_output=999*parent_low["tx"].get_vsize(),
356 )
357 358 peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")
359 360 # 1. Send first parent which will be accepted.
361 peer_sender.send_and_ping(msg_tx(parent_high["tx"]))
362 assert parent_high["txid"] in node.getrawmempool()
363 364 # 2. Send child.
365 peer_sender.send_and_ping(msg_tx(child["tx"]))
366 367 # 3. Node requests parent_low. However, 1p1c fails because package-not-child-with-unconfirmed-parents
368 parent_low_txid_int = int(parent_low["txid"], 16)
369 peer_sender.wait_for_getdata([parent_low_txid_int])
370 peer_sender.send_and_ping(msg_tx(parent_low["tx"]))
371 372 node_mempool = node.getrawmempool()
373 assert parent_high["txid"] in node_mempool
374 assert parent_low["txid"] not in node_mempool
375 assert child["txid"] not in node_mempool
376 377 # Same error if submitted through submitpackage without parent_high
378 package_hex_missing_parent = [parent_low["hex"], child["hex"]]
379 result_missing_parent = node.submitpackage(package_hex_missing_parent)
380 assert_equal(result_missing_parent["package_msg"], "package-not-child-with-unconfirmed-parents")
381 382 def run_test(self):
383 node = self.nodes[0]
384 # To avoid creating transactions with the same txid (can happen if we set the same feerate
385 # and reuse the same input as a previous transaction that wasn't successfully submitted),
386 # we give each subtest a different nSequence for its transactions.
387 self.sequence = MAX_BIP125_RBF_SEQUENCE
388 389 self.wallet = MiniWallet(node)
390 self.wallet_nonsegwit = MiniWallet(node, mode=MiniWalletMode.RAW_P2PK)
391 self.generate(self.wallet_nonsegwit, 10)
392 self.generate(self.wallet, 20)
393 394 fill_mempool(self, node)
395 396 self.log.info("Check opportunistic 1p1c logic when parent (txid != wtxid) is received before child")
397 self.test_basic_parent_then_child(self.wallet)
398 399 self.log.info("Check opportunistic 1p1c logic when parent (txid == wtxid) is received before child")
400 self.test_basic_parent_then_child(self.wallet_nonsegwit)
401 402 self.log.info("Check opportunistic 1p1c logic when child is received before parent")
403 self.test_basic_child_then_parent()
404 405 self.log.info("Check opportunistic 1p1c logic when 2 candidate children exist (parent txid != wtxid)")
406 self.test_low_and_high_child(self.wallet)
407 408 self.log.info("Check opportunistic 1p1c logic when 2 candidate children exist (parent txid == wtxid)")
409 self.test_low_and_high_child(self.wallet_nonsegwit)
410 411 self.test_orphan_consensus_failure()
412 self.test_parent_consensus_failure()
413 self.test_multiple_parents()
414 self.test_other_parent_in_mempool()
415 416 417 if __name__ == '__main__':
418 PackageRelayTest(__file__).main()
419