mempool_ephemeral_dust.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
6 from decimal import Decimal
7
8 from test_framework.messages import (
9 COIN,
10 CTxOut,
11 )
12 from test_framework.test_framework import BitcoinTestFramework
13 from test_framework.mempool_util import assert_mempool_contents
14 from test_framework.util import (
15 assert_equal,
16 assert_greater_than,
17 assert_raises_rpc_error,
18 assert_not_equal,
19 )
20 from test_framework.wallet import (
21 MiniWallet,
22 )
23 from test_framework.blocktools import (
24 create_empty_fork
25 )
26
27 class EphemeralDustTest(BitcoinTestFramework):
28 def set_test_params(self):
29 # Mempools should match via 1P1C p2p relay
30 self.num_nodes = 2
31
32 # Don't test trickling logic
33 self.noban_tx_relay = True
34
35 def add_output_to_create_multi_result(self, result, output_value=0):
36 """ Add output without changing absolute tx fee
37 """
38 assert len(result["tx"].vout) > 0
39 assert result["tx"].vout[0].nValue >= output_value
40 result["tx"].vout.append(CTxOut(output_value, result["tx"].vout[0].scriptPubKey))
41 # Take value from first output
42 result["tx"].vout[0].nValue -= output_value
43 result["new_utxos"][0]["value"] = Decimal(result["tx"].vout[0].nValue) / COIN
44 new_txid = result["tx"].txid_hex
45 result["txid"] = new_txid
46 result["wtxid"] = result["tx"].wtxid_hex
47 result["hex"] = result["tx"].serialize().hex()
48 for new_utxo in result["new_utxos"]:
49 new_utxo["txid"] = new_txid
50 new_utxo["wtxid"] = result["tx"].wtxid_hex
51
52 result["new_utxos"].append({"txid": new_txid, "vout": len(result["tx"].vout) - 1, "value": Decimal(output_value) / COIN, "height": 0, "coinbase": False, "confirmations": 0})
53
54 def create_ephemeral_dust_package(self, *, tx_version, dust_tx_fee=0, dust_value=0, num_dust_outputs=1, extra_sponsors=None):
55 """Creates a 1P1C package containing ephemeral dust. By default, the parent transaction
56 is zero-fee and creates a single zero-value dust output, and all of its outputs are
57 spent by the child."""
58 dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=dust_tx_fee, version=tx_version)
59 for _ in range(num_dust_outputs):
60 self.add_output_to_create_multi_result(dusty_tx, dust_value)
61
62 extra_sponsors = extra_sponsors or []
63 sweep_tx = self.wallet.create_self_transfer_multi(
64 utxos_to_spend=dusty_tx["new_utxos"] + extra_sponsors,
65 version=tx_version,
66 )
67
68 return dusty_tx, sweep_tx
69
70 def trigger_reorg(self, fork_blocks):
71 """Trigger reorg of the fork blocks."""
72 for block in fork_blocks:
73 self.nodes[0].submitblock(block.serialize().hex())
74 assert_equal(self.nodes[0].getbestblockhash(), fork_blocks[-1].hash_hex)
75
76 def run_test(self):
77
78 node = self.nodes[0]
79 self.wallet = MiniWallet(node)
80
81 self.test_normal_dust()
82 self.test_sponsor_cycle()
83 self.test_node_restart()
84 self.test_fee_having_parent()
85 self.test_multidust()
86 self.test_nonzero_dust()
87 self.test_non_truc()
88 self.test_unspent_ephemeral()
89 self.test_reorgs()
90 self.test_no_minrelay_fee()
91
92 def test_normal_dust(self):
93 self.log.info("Create 0-value dusty output, show that it works inside truc when spent in package")
94
95 assert_equal(self.nodes[0].getrawmempool(), [])
96 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3)
97
98 # Test doesn't work because lack of package feerates
99 test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"], sweep_tx["hex"]])
100 assert not test_res[0]["allowed"]
101 assert_equal(test_res[0]["reject-reason"], "min relay fee not met")
102
103 # And doesn't work on its own
104 assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, dusty_tx["hex"])
105
106 # If we add modified fees, it is still not allowed due to dust check
107 self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=COIN)
108 test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])
109 assert not test_res[0]["allowed"]
110 assert_equal(test_res[0]["reject-reason"], "dust")
111 # Reset priority
112 self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-COIN)
113 assert_equal(self.nodes[0].getprioritisedtransactions(), {})
114
115 # Package evaluation succeeds
116 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
117 assert_equal(res["package_msg"], "success")
118 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
119
120 # Entry is denied when non-0-fee, either base or unmodified.
121 # If in-mempool, we're not allowed to prioritise due to detected dust output
122 assert_raises_rpc_error(-8, "Priority is not supported for transactions with dust outputs.", self.nodes[0].prioritisetransaction, dusty_tx["txid"], 0, 1)
123 assert_equal(self.nodes[0].getprioritisedtransactions(), {})
124
125 self.generate(self.nodes[0], 1)
126 assert_equal(self.nodes[0].getrawmempool(), [])
127
128 def test_node_restart(self):
129 self.log.info("Test that an ephemeral package is rejected on restart due to individual evaluation")
130
131 assert_equal(self.nodes[0].getrawmempool(), [])
132 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3)
133
134 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
135 assert_equal(res["package_msg"], "success")
136 assert_equal(len(self.nodes[0].getrawmempool()), 2)
137 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
138
139 # Node restart; doesn't allow ephemeral transaction back in due to individual submission
140 # resulting in 0-fee. Supporting re-submission of CPFP packages on restart is desired but not
141 # yet implemented.
142 self.restart_node(0)
143 self.restart_node(1)
144 self.connect_nodes(0, 1)
145 assert_mempool_contents(self, self.nodes[0], expected=[])
146
147 def test_fee_having_parent(self):
148 self.log.info("Test that a transaction with ephemeral dust may not have non-0 base fee")
149
150 assert_equal(self.nodes[0].getrawmempool(), [])
151
152 sats_fee = 1
153 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, dust_tx_fee=sats_fee)
154 assert_equal(int(COIN * dusty_tx["fee"]), sats_fee) # has fees
155 assert_greater_than(dusty_tx["tx"].vout[0].nValue, 330) # main output is not dust
156 assert_equal(dusty_tx["tx"].vout[1].nValue, 0) # added one is dust
157
158 # When base fee is non-0, we report dust like usual
159 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
160 assert_equal(res["package_msg"], "transaction failed")
161 assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")
162
163 # Priority is ignored: rejected even if modified fee is 0
164 self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-sats_fee)
165 self.nodes[1].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-sats_fee)
166 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
167 assert_equal(res["package_msg"], "transaction failed")
168 assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")
169
170 # Will not be accepted if base fee is 0 with modified fee of non-0
171 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3)
172
173 self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=1000)
174 self.nodes[1].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=1000)
175
176 # It's rejected submitted alone
177 test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])
178 assert not test_res[0]["allowed"]
179 assert_equal(test_res[0]["reject-reason"], "dust")
180
181 # Or as a package
182 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
183 assert_equal(res["package_msg"], "transaction failed")
184 assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")
185
186 assert_mempool_contents(self, self.nodes[0], expected=[])
187
188 def test_multidust(self):
189 self.log.info("Test that a transaction with multiple ephemeral dusts is not allowed")
190
191 assert_mempool_contents(self, self.nodes[0], expected=[])
192 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, num_dust_outputs=2)
193
194 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
195 assert_equal(res["package_msg"], "transaction failed")
196 assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust")
197 assert_equal(self.nodes[0].getrawmempool(), [])
198
199 def test_nonzero_dust(self):
200 self.log.info("Test that a single output of any satoshi amount is allowed, not checking spending")
201
202 # We aren't checking spending, allow it in with no fee
203 self.restart_node(0, extra_args=["-minrelaytxfee=0"])
204 self.restart_node(1, extra_args=["-minrelaytxfee=0"])
205 self.connect_nodes(0, 1)
206
207 # 330 is dust threshold for taproot outputs
208 for value in [1, 329, 330]:
209 assert_equal(self.nodes[0].getrawmempool(), [])
210 dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=value)
211 test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])
212 assert test_res[0]["allowed"]
213
214 self.restart_node(0, extra_args=[])
215 self.restart_node(1, extra_args=[])
216 self.connect_nodes(0, 1)
217 assert_mempool_contents(self, self.nodes[0], expected=[])
218
219 def test_non_truc(self):
220 self.log.info("Test that v2 dust-having transaction is also accepted if spent")
221
222 assert_equal(self.nodes[0].getrawmempool(), [])
223 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=2)
224
225 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
226 assert_equal(res["package_msg"], "success")
227 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
228 self.generate(self.nodes[0], 1)
229
230 def test_unspent_ephemeral(self):
231 self.log.info("Test that spending from a tx with ephemeral outputs is only allowed if dust is spent as well")
232
233 assert_equal(self.nodes[0].getrawmempool(), [])
234 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, dust_value=329)
235
236 # Valid sweep we will RBF incorrectly by not spending dust as well
237 self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
238 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
239
240 # Doesn't spend in-mempool dust output from parent
241 unspent_sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=2000, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
242 assert_greater_than(unspent_sweep_tx["fee"], sweep_tx["fee"])
243 res = self.nodes[0].submitpackage([dusty_tx["hex"], unspent_sweep_tx["hex"]])
244 assert_equal(res["tx-results"][unspent_sweep_tx["wtxid"]]["error"], f"missing-ephemeral-spends, tx {unspent_sweep_tx['txid']} (wtxid={unspent_sweep_tx['wtxid']}) did not spend parent's ephemeral dust")
245 assert_raises_rpc_error(-26, f"missing-ephemeral-spends, tx {unspent_sweep_tx['txid']} (wtxid={unspent_sweep_tx['wtxid']}) did not spend parent's ephemeral dust", self.nodes[0].sendrawtransaction, unspent_sweep_tx["hex"])
246 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
247
248 # Spend works with dust spent
249 sweep_tx_2 = self.wallet.create_self_transfer_multi(fee_per_output=2000, utxos_to_spend=dusty_tx["new_utxos"], version=3)
250 assert_not_equal(sweep_tx["hex"], sweep_tx_2["hex"])
251 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx_2["hex"]])
252 assert_equal(res["package_msg"], "success")
253
254 # Re-set and test again with nothing from package in mempool this time
255 self.generate(self.nodes[0], 1)
256 assert_equal(self.nodes[0].getrawmempool(), [])
257
258 dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=329)
259
260 # Spend non-dust only
261 unspent_sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
262
263 res = self.nodes[0].submitpackage([dusty_tx["hex"], unspent_sweep_tx["hex"]])
264 assert_equal(res["package_msg"], "unspent-dust")
265
266 assert_equal(self.nodes[0].getrawmempool(), [])
267
268 # Now spend dust only which should work
269 second_coin = self.wallet.get_utxo() # another fee-bringing coin
270 sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][1], second_coin], version=3)
271
272 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
273 assert_equal(res["package_msg"], "success")
274 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
275
276 self.generate(self.nodes[0], 1)
277 assert_mempool_contents(self, self.nodes[0], expected=[])
278
279 def test_sponsor_cycle(self):
280 self.log.info("Test that dust txn is not evicted when it becomes childless, but won't be mined")
281
282 assert_equal(self.nodes[0].getrawmempool(), [])
283 sponsor_coin = self.wallet.get_utxo()
284 # Bring "fee" input that can be double-spend separately
285 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, extra_sponsors=[sponsor_coin])
286
287 res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
288 assert_equal(res["package_msg"], "success")
289 assert_equal(len(self.nodes[0].getrawmempool()), 2)
290 # sync to make sure unsponsor_tx hits second node's mempool after initial package
291 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
292
293 # Now we RBF away the child using the sponsor input only
294 unsponsor_tx = self.wallet.create_self_transfer_multi(
295 utxos_to_spend=[sponsor_coin],
296 num_outputs=1,
297 fee_per_output=2000,
298 version=3
299 )
300 self.nodes[0].sendrawtransaction(unsponsor_tx["hex"])
301
302 # Parent is now childless and fee-free, so will not be mined
303 entry_info = self.nodes[0].getmempoolentry(dusty_tx["txid"])
304 assert_equal(entry_info["descendantcount"], 1)
305 assert_equal(entry_info["fees"]["descendant"], Decimal(0))
306
307 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], unsponsor_tx["tx"]])
308
309 # Dust tx is not mined
310 self.generate(self.nodes[0], 1)
311 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]])
312
313 # Create sweep that doesn't spend conflicting sponsor coin
314 sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)
315
316 # Can resweep
317 self.nodes[0].sendrawtransaction(sweep_tx["hex"])
318 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
319
320 self.generate(self.nodes[0], 1)
321 assert_mempool_contents(self, self.nodes[0], expected=[])
322
323 def test_reorgs(self):
324 self.log.info("Test that reorgs avoid ephemeral dust spentness checks")
325
326 assert_equal(self.nodes[0].getrawmempool(), [])
327
328 # Many shallow re-orgs confuse block gossiping making test less reliable otherwise
329 self.disconnect_nodes(0, 1)
330
331 # Get dusty tx mined, then check that it makes it back into mempool on reorg
332 # due to bypass_limits allowing 0-fee individually, and creation of single dust
333
334 # Prep for fork with empty blocks
335 fork_blocks = create_empty_fork(self.nodes[0])
336
337 dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3)
338 assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, dusty_tx["hex"])
339
340 self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_tx["hex"]], sync_fun=self.no_op)
341 self.trigger_reorg(fork_blocks)
342 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]], sync=False)
343
344 # Create a sweep that has dust of its own and leaves dusty_tx's dust unspent
345 sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)
346 self.add_output_to_create_multi_result(sweep_tx)
347 assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, sweep_tx["hex"])
348
349 # Prep for fork with empty blocks
350 fork_blocks = create_empty_fork(self.nodes[0])
351
352 # Mine the sweep then re-org, the sweep will make it back in due to lack of eph dust spend checks on reorg
353 self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_tx["hex"], sweep_tx["hex"]], sync_fun=self.no_op)
354 self.trigger_reorg(fork_blocks)
355 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]], sync=False)
356
357 # Test that dusty tx being reorged back into mempool doesn't invalidate descendants
358 # whether they spend dust or not
359
360 # Mine the parent transaction only while preparing a fork
361 fork_blocks = create_empty_fork(self.nodes[0])
362 self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_tx["hex"]], sync_fun=self.no_op)
363 utxo = self.wallet.get_utxo()
364 # No in-mempool deps, use version=2 and chain off of it
365 second_sweep_tx = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], utxos_to_spend=[dusty_tx["new_utxos"][1], utxo], version=2)
366 child_chain = self.wallet.send_self_transfer_chain(from_node=self.nodes[0], chain_length=10, utxo_to_spend=second_sweep_tx["new_utxos"][0])
367
368 # Everything but parent in pool
369 expected_pool = [sweep_tx["tx"], second_sweep_tx["tx"]] + [child["tx"] for child in child_chain]
370 assert_mempool_contents(self, self.nodes[0], expected=expected_pool, sync=False)
371
372 # Add ultimate parent back into mempool
373 expected_pool = [dusty_tx["tx"]] + expected_pool
374 self.trigger_reorg(fork_blocks)
375 assert_mempool_contents(self, self.nodes[0], expected=expected_pool, sync=False)
376
377 hex_to_mine = [tx.serialize().hex() for tx in expected_pool]
378 self.generateblock(self.nodes[0], self.wallet.get_address(), hex_to_mine, sync_fun=self.no_op)
379 assert_equal(self.nodes[0].getrawmempool(), [])
380
381 self.log.info("Test that ephemeral dust tx with fees or multi dust don't enter mempool via reorg")
382 multi_dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, num_dust_outputs=2)
383
384 # Prep for fork with empty blocks
385 fork_blocks = create_empty_fork(self.nodes[0])
386
387 self.generateblock(self.nodes[0], self.wallet.get_address(), [multi_dusty_tx["hex"]], sync_fun=self.no_op)
388 self.trigger_reorg(fork_blocks)
389 assert_equal(self.nodes[0].getrawmempool(), [])
390
391 # With fee and one dust
392 dusty_fee_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_tx_fee=1)
393
394 # Prep for fork with empty blocks
395 fork_blocks = create_empty_fork(self.nodes[0])
396
397 self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_fee_tx["hex"]], sync_fun=self.no_op)
398 self.trigger_reorg(fork_blocks)
399 assert_equal(self.nodes[0].getrawmempool(), [])
400
401 # Re-connect and make sure we have same state still
402 self.connect_nodes(0, 1)
403 self.sync_all()
404
405 # N.B. this extra_args can be removed post cluster mempool
406 def test_no_minrelay_fee(self):
407 self.log.info("Test that ephemeral dust works in non-TRUC contexts when there's no minrelay requirement")
408
409 # Note: since minrelay is 0, it is not testing 1P1C relay
410 self.restart_node(0, extra_args=["-minrelaytxfee=0"])
411 self.restart_node(1, extra_args=["-minrelaytxfee=0"])
412 self.connect_nodes(0, 1)
413
414 assert_equal(self.nodes[0].getrawmempool(), [])
415 dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=2)
416
417 self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])
418
419 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])
420
421 # generate coins for next tests
422 self.generate(self.nodes[0], 1)
423 self.wallet.rescan_utxos()
424 assert_equal(self.nodes[0].getrawmempool(), [])
425
426 self.log.info("Test batched ephemeral dust sweep")
427 dusty_txs = []
428 for _ in range(24):
429 dusty_txs.append(self.wallet.create_self_transfer_multi(fee_per_output=0, version=2))
430 self.add_output_to_create_multi_result(dusty_txs[-1])
431
432 all_parent_utxos = [utxo for tx in dusty_txs for utxo in tx["new_utxos"]]
433
434 # Missing one dust spend from a single parent, child rejected
435 insufficient_sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=25000, utxos_to_spend=all_parent_utxos[:-1], version=2)
436
437 res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])
438 assert_equal(res['package_msg'], "transaction failed")
439 assert_equal(res['tx-results'][insufficient_sweep_tx['wtxid']]['error'], f"missing-ephemeral-spends, tx {insufficient_sweep_tx['txid']} (wtxid={insufficient_sweep_tx['wtxid']}) did not spend parent's ephemeral dust")
440 # Everything got in except for insufficient spend
441 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs])
442
443 # Next put some parents in mempool, but not others, and test unspent dust again with all parents spent
444 B_coin = self.wallet.get_utxo() # coin to cycle out CPFP
445 sweep_all_but_one_tx = self.wallet.create_self_transfer_multi(fee_per_output=20000, utxos_to_spend=all_parent_utxos[:-2] + [B_coin], version=2)
446 res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs[:-1]] + [sweep_all_but_one_tx["hex"]])
447 assert_equal(res['package_msg'], "success")
448 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])
449
450 res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])
451 assert_equal(res['package_msg'], "transaction failed")
452 assert_equal(res['tx-results'][insufficient_sweep_tx["wtxid"]]["error"], f"missing-ephemeral-spends, tx {insufficient_sweep_tx['txid']} (wtxid={insufficient_sweep_tx['wtxid']}) did not spend parent's ephemeral dust")
453 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])
454
455 # Cycle out the partial sweep to avoid triggering package RBF behavior which limits package to no in-mempool ancestors
456 cancel_sweep = self.wallet.create_self_transfer_multi(fee_per_output=21000, utxos_to_spend=[B_coin], version=2)
457 self.nodes[0].sendrawtransaction(cancel_sweep["hex"])
458 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [cancel_sweep["tx"]])
459
460 # Sweeps all dust, where all dusty txs are already in-mempool
461 sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=25000, utxos_to_spend=all_parent_utxos, version=2)
462
463 # N.B. Since we have multiple parents these are not propagating via 1P1C relay.
464 # minrelay being zero allows them to propagate on their own.
465 res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [sweep_tx["hex"]])
466 assert_equal(res['package_msg'], "success")
467 assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_tx["tx"], cancel_sweep["tx"]])
468
469 self.generate(self.nodes[0], 1)
470 self.wallet.rescan_utxos()
471 assert_equal(self.nodes[0].getrawmempool(), [])
472
473 # Other topology tests (e.g., grandparents and parents both with dust) require relaxation of submitpackage topology
474
475 self.restart_node(0, extra_args=[])
476 self.restart_node(1, extra_args=[])
477 self.connect_nodes(0, 1)
478
479 assert_equal(self.nodes[0].getrawmempool(), [])
480
481 if __name__ == "__main__":
482 EphemeralDustTest(__file__).main()
483