mempool_ephemeral_dust.py
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()