Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
251res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx_2["hex"]])252assert_equal(res["package_msg"], "success")254# Re-set and test again with nothing from package in mempool this time255self.generate(self.nodes[0], 1)256assert_equal(self.nodes[0].getrawmempool(), [])258dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=329)260# Spend non-dust only261unspent_sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)263res = self.nodes[0].submitpackage([dusty_tx["hex"], unspent_sweep_tx["hex"]])264assert_equal(res["package_msg"], "unspent-dust")266assert_equal(self.nodes[0].getrawmempool(), [])268# Now spend dust only which should work269second_coin = self.wallet.get_utxo() # another fee-bringing coin270sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[dusty_tx["new_utxos"][1], second_coin], version=3)272res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])273assert_equal(res["package_msg"], "success")274assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])276self.generate(self.nodes[0], 1)277assert_mempool_contents(self, self.nodes[0], expected=[])279def test_sponsor_cycle(self):280self.log.info("Test that dust txn is not evicted when it becomes childless, but won't be mined")282assert_equal(self.nodes[0].getrawmempool(), [])283sponsor_coin = self.wallet.get_utxo()284# Bring "fee" input that can be double-spend separately285dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, extra_sponsors=[sponsor_coin])287res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])288assert_equal(res["package_msg"], "success")289assert_equal(len(self.nodes[0].getrawmempool()), 2)290# sync to make sure unsponsor_tx hits second node's mempool after initial package291assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])293# Now we RBF away the child using the sponsor input only294unsponsor_tx = self.wallet.create_self_transfer_multi(295utxos_to_spend=[sponsor_coin],296num_outputs=1,297fee_per_output=2000,298version=3299)300self.nodes[0].sendrawtransaction(unsponsor_tx["hex"])302# Parent is now childless and fee-free, so will not be mined303entry_info = self.nodes[0].getmempoolentry(dusty_tx["txid"])304assert_equal(entry_info["descendantcount"], 1)305assert_equal(entry_info["fees"]["descendant"], Decimal(0))307assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], unsponsor_tx["tx"]])309# Dust tx is not mined310self.generate(self.nodes[0], 1)311assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]])313# Create sweep that doesn't spend conflicting sponsor coin314sweep_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=dusty_tx["new_utxos"], version=3)316# Can resweep317self.nodes[0].sendrawtransaction(sweep_tx["hex"])318assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])320self.generate(self.nodes[0], 1)321assert_mempool_contents(self, self.nodes[0], expected=[])323def test_reorgs(self):324self.log.info("Test that reorgs avoid ephemeral dust spentness checks")326assert_equal(self.nodes[0].getrawmempool(), [])328# Many shallow re-orgs confuse block gossiping making test less reliable otherwise329self.disconnect_nodes(0, 1)331# Get dusty tx mined, then check that it makes it back into mempool on reorg332# due to bypass_limits allowing 0-fee individually, and creation of single dust334# Prep for fork with empty blocks335fork_blocks = create_empty_fork(self.nodes[0])337dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3)338assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, dusty_tx["hex"])340self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_tx["hex"]], sync_fun=self.no_op)341self.trigger_reorg(fork_blocks)342assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"]], sync=False)344# Create a sweep that has dust of its own and leaves dusty_tx's dust unspent345sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=0, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)346self.add_output_to_create_multi_result(sweep_tx)347assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, sweep_tx["hex"])349# Prep for fork with empty blocks350fork_blocks = create_empty_fork(self.nodes[0])352# Mine the sweep then re-org, the sweep will make it back in due to lack of eph dust spend checks on reorg353self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_tx["hex"], sweep_tx["hex"]], sync_fun=self.no_op)354self.trigger_reorg(fork_blocks)355assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]], sync=False)357# Test that dusty tx being reorged back into mempool doesn't invalidate descendants358# whether they spend dust or not360# Mine the parent transaction only while preparing a fork361fork_blocks = create_empty_fork(self.nodes[0])362self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_tx["hex"]], sync_fun=self.no_op)363utxo = self.wallet.get_utxo()364# No in-mempool deps, use version=2 and chain off of it365second_sweep_tx = self.wallet.send_self_transfer_multi(from_node=self.nodes[0], utxos_to_spend=[dusty_tx["new_utxos"][1], utxo], version=2)366child_chain = self.wallet.send_self_transfer_chain(from_node=self.nodes[0], chain_length=10, utxo_to_spend=second_sweep_tx["new_utxos"][0])368# Everything but parent in pool369expected_pool = [sweep_tx["tx"], second_sweep_tx["tx"]] + [child["tx"] for child in child_chain]370assert_mempool_contents(self, self.nodes[0], expected=expected_pool, sync=False)372# Add ultimate parent back into mempool373expected_pool = [dusty_tx["tx"]] + expected_pool374self.trigger_reorg(fork_blocks)375assert_mempool_contents(self, self.nodes[0], expected=expected_pool, sync=False)377hex_to_mine = [tx.serialize().hex() for tx in expected_pool]378self.generateblock(self.nodes[0], self.wallet.get_address(), hex_to_mine, sync_fun=self.no_op)379assert_equal(self.nodes[0].getrawmempool(), [])381self.log.info("Test that ephemeral dust tx with fees or multi dust don't enter mempool via reorg")382multi_dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, num_dust_outputs=2)384# Prep for fork with empty blocks385fork_blocks = create_empty_fork(self.nodes[0])387self.generateblock(self.nodes[0], self.wallet.get_address(), [multi_dusty_tx["hex"]], sync_fun=self.no_op)388self.trigger_reorg(fork_blocks)389assert_equal(self.nodes[0].getrawmempool(), [])391# With fee and one dust392dusty_fee_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_tx_fee=1)394# Prep for fork with empty blocks395fork_blocks = create_empty_fork(self.nodes[0])397self.generateblock(self.nodes[0], self.wallet.get_address(), [dusty_fee_tx["hex"]], sync_fun=self.no_op)398self.trigger_reorg(fork_blocks)399assert_equal(self.nodes[0].getrawmempool(), [])401# Re-connect and make sure we have same state still402self.connect_nodes(0, 1)403self.sync_all()405# N.B. this extra_args can be removed post cluster mempool406def test_no_minrelay_fee(self):407self.log.info("Test that ephemeral dust works in non-TRUC contexts when there's no minrelay requirement")409# Note: since minrelay is 0, it is not testing 1P1C relay410self.restart_node(0, extra_args=["-minrelaytxfee=0"])411self.restart_node(1, extra_args=["-minrelaytxfee=0"])412self.connect_nodes(0, 1)414assert_equal(self.nodes[0].getrawmempool(), [])415dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=2)417self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])419assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])421# generate coins for next tests422self.generate(self.nodes[0], 1)423self.wallet.rescan_utxos()424assert_equal(self.nodes[0].getrawmempool(), [])426self.log.info("Test batched ephemeral dust sweep")427dusty_txs = []428for _ in range(24):429dusty_txs.append(self.wallet.create_self_transfer_multi(fee_per_output=0, version=2))430self.add_output_to_create_multi_result(dusty_txs[-1])432all_parent_utxos = [utxo for tx in dusty_txs for utxo in tx["new_utxos"]]434# Missing one dust spend from a single parent, child rejected435insufficient_sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=25000, utxos_to_spend=all_parent_utxos[:-1], version=2)437res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])438assert_equal(res['package_msg'], "transaction failed")439assert_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 spend441assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs])443# Next put some parents in mempool, but not others, and test unspent dust again with all parents spent444B_coin = self.wallet.get_utxo() # coin to cycle out CPFP445sweep_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)446res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs[:-1]] + [sweep_all_but_one_tx["hex"]])447assert_equal(res['package_msg'], "success")448assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])450res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])451assert_equal(res['package_msg'], "transaction failed")452assert_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")453assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])455# Cycle out the partial sweep to avoid triggering package RBF behavior which limits package to no in-mempool ancestors456cancel_sweep = self.wallet.create_self_transfer_multi(fee_per_output=21000, utxos_to_spend=[B_coin], version=2)457self.nodes[0].sendrawtransaction(cancel_sweep["hex"])458assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [cancel_sweep["tx"]])460# Sweeps all dust, where all dusty txs are already in-mempool461sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=25000, utxos_to_spend=all_parent_utxos, version=2)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.465res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [sweep_tx["hex"]])466assert_equal(res['package_msg'], "success")467assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_tx["tx"], cancel_sweep["tx"]])469self.generate(self.nodes[0], 1)470self.wallet.rescan_utxos()471assert_equal(self.nodes[0].getrawmempool(), [])473# Other topology tests (e.g., grandparents and parents both with dust) require relaxation of submitpackage topology475self.restart_node(0, extra_args=[])476self.restart_node(1, extra_args=[])477self.connect_nodes(0, 1)479assert_equal(self.nodes[0].getrawmempool(), [])481if __name__ == "__main__":482EphemeralDustTest(__file__).main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.