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.
1#!/usr/bin/env python32# Copyright (c) 2024-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.6from decimal import Decimal8from test_framework.messages import (9COIN,10CTxOut,11)12from test_framework.test_framework import BitcoinTestFramework13from test_framework.mempool_util import assert_mempool_contents14from test_framework.util import (15assert_equal,16assert_greater_than,17assert_raises_rpc_error,18assert_not_equal,19)20from test_framework.wallet import (21MiniWallet,22)23from test_framework.blocktools import (24create_empty_fork25)27class EphemeralDustTest(BitcoinTestFramework):28def set_test_params(self):29# Mempools should match via 1P1C p2p relay30self.num_nodes = 232# Don't test trickling logic33self.noban_tx_relay = True35def add_output_to_create_multi_result(self, result, output_value=0):36""" Add output without changing absolute tx fee37"""38assert len(result["tx"].vout) > 039assert result["tx"].vout[0].nValue >= output_value40result["tx"].vout.append(CTxOut(output_value, result["tx"].vout[0].scriptPubKey))41# Take value from first output42result["tx"].vout[0].nValue -= output_value43result["new_utxos"][0]["value"] = Decimal(result["tx"].vout[0].nValue) / COIN44new_txid = result["tx"].txid_hex45result["txid"] = new_txid46result["wtxid"] = result["tx"].wtxid_hex47result["hex"] = result["tx"].serialize().hex()48for new_utxo in result["new_utxos"]:49new_utxo["txid"] = new_txid50new_utxo["wtxid"] = result["tx"].wtxid_hex52result["new_utxos"].append({"txid": new_txid, "vout": len(result["tx"].vout) - 1, "value": Decimal(output_value) / COIN, "height": 0, "coinbase": False, "confirmations": 0})54def 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 transaction56is zero-fee and creates a single zero-value dust output, and all of its outputs are57spent by the child."""58dusty_tx = self.wallet.create_self_transfer_multi(fee_per_output=dust_tx_fee, version=tx_version)59for _ in range(num_dust_outputs):60self.add_output_to_create_multi_result(dusty_tx, dust_value)62extra_sponsors = extra_sponsors or []63sweep_tx = self.wallet.create_self_transfer_multi(64utxos_to_spend=dusty_tx["new_utxos"] + extra_sponsors,65version=tx_version,66)68return dusty_tx, sweep_tx70def trigger_reorg(self, fork_blocks):71"""Trigger reorg of the fork blocks."""72for block in fork_blocks:73self.nodes[0].submitblock(block.serialize().hex())74assert_equal(self.nodes[0].getbestblockhash(), fork_blocks[-1].hash_hex)76def run_test(self):78node = self.nodes[0]79self.wallet = MiniWallet(node)81self.test_normal_dust()82self.test_sponsor_cycle()83self.test_node_restart()84self.test_fee_having_parent()85self.test_multidust()86self.test_nonzero_dust()87self.test_non_truc()88self.test_unspent_ephemeral()89self.test_reorgs()90self.test_no_minrelay_fee()92def test_normal_dust(self):93self.log.info("Create 0-value dusty output, show that it works inside truc when spent in package")95assert_equal(self.nodes[0].getrawmempool(), [])96dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3)98# Test doesn't work because lack of package feerates99test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"], sweep_tx["hex"]])100assert not test_res[0]["allowed"]101assert_equal(test_res[0]["reject-reason"], "min relay fee not met")103# And doesn't work on its own104assert_raises_rpc_error(-26, "min relay fee not met", self.nodes[0].sendrawtransaction, dusty_tx["hex"])106# If we add modified fees, it is still not allowed due to dust check107self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=COIN)108test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])109assert not test_res[0]["allowed"]110assert_equal(test_res[0]["reject-reason"], "dust")111# Reset priority112self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-COIN)113assert_equal(self.nodes[0].getprioritisedtransactions(), {})115# Package evaluation succeeds116res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])117assert_equal(res["package_msg"], "success")118assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])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 output122assert_raises_rpc_error(-8, "Priority is not supported for transactions with dust outputs.", self.nodes[0].prioritisetransaction, dusty_tx["txid"], 0, 1)123assert_equal(self.nodes[0].getprioritisedtransactions(), {})125self.generate(self.nodes[0], 1)126assert_equal(self.nodes[0].getrawmempool(), [])128def test_node_restart(self):129self.log.info("Test that an ephemeral package is rejected on restart due to individual evaluation")131assert_equal(self.nodes[0].getrawmempool(), [])132dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3)134res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])135assert_equal(res["package_msg"], "success")136assert_equal(len(self.nodes[0].getrawmempool()), 2)137assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])139# Node restart; doesn't allow ephemeral transaction back in due to individual submission140# resulting in 0-fee. Supporting re-submission of CPFP packages on restart is desired but not141# yet implemented.142self.restart_node(0)143self.restart_node(1)144self.connect_nodes(0, 1)145assert_mempool_contents(self, self.nodes[0], expected=[])147def test_fee_having_parent(self):148self.log.info("Test that a transaction with ephemeral dust may not have non-0 base fee")150assert_equal(self.nodes[0].getrawmempool(), [])152sats_fee = 1153dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, dust_tx_fee=sats_fee)154assert_equal(int(COIN * dusty_tx["fee"]), sats_fee) # has fees155assert_greater_than(dusty_tx["tx"].vout[0].nValue, 330) # main output is not dust156assert_equal(dusty_tx["tx"].vout[1].nValue, 0) # added one is dust158# When base fee is non-0, we report dust like usual159res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])160assert_equal(res["package_msg"], "transaction failed")161assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")163# Priority is ignored: rejected even if modified fee is 0164self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-sats_fee)165self.nodes[1].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=-sats_fee)166res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])167assert_equal(res["package_msg"], "transaction failed")168assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")170# Will not be accepted if base fee is 0 with modified fee of non-0171dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3)173self.nodes[0].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=1000)174self.nodes[1].prioritisetransaction(txid=dusty_tx["txid"], dummy=0, fee_delta=1000)176# It's rejected submitted alone177test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])178assert not test_res[0]["allowed"]179assert_equal(test_res[0]["reject-reason"], "dust")181# Or as a package182res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])183assert_equal(res["package_msg"], "transaction failed")184assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust, tx with dust output must be 0-fee")186assert_mempool_contents(self, self.nodes[0], expected=[])188def test_multidust(self):189self.log.info("Test that a transaction with multiple ephemeral dusts is not allowed")191assert_mempool_contents(self, self.nodes[0], expected=[])192dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, num_dust_outputs=2)194res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])195assert_equal(res["package_msg"], "transaction failed")196assert_equal(res["tx-results"][dusty_tx["wtxid"]]["error"], "dust")197assert_equal(self.nodes[0].getrawmempool(), [])199def test_nonzero_dust(self):200self.log.info("Test that a single output of any satoshi amount is allowed, not checking spending")202# We aren't checking spending, allow it in with no fee203self.restart_node(0, extra_args=["-minrelaytxfee=0"])204self.restart_node(1, extra_args=["-minrelaytxfee=0"])205self.connect_nodes(0, 1)207# 330 is dust threshold for taproot outputs208for value in [1, 329, 330]:209assert_equal(self.nodes[0].getrawmempool(), [])210dusty_tx, _ = self.create_ephemeral_dust_package(tx_version=3, dust_value=value)211test_res = self.nodes[0].testmempoolaccept([dusty_tx["hex"]])212assert test_res[0]["allowed"]214self.restart_node(0, extra_args=[])215self.restart_node(1, extra_args=[])216self.connect_nodes(0, 1)217assert_mempool_contents(self, self.nodes[0], expected=[])219def test_non_truc(self):220self.log.info("Test that v2 dust-having transaction is also accepted if spent")222assert_equal(self.nodes[0].getrawmempool(), [])223dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=2)225res = self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])226assert_equal(res["package_msg"], "success")227assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])228self.generate(self.nodes[0], 1)230def test_unspent_ephemeral(self):231self.log.info("Test that spending from a tx with ephemeral outputs is only allowed if dust is spent as well")233assert_equal(self.nodes[0].getrawmempool(), [])234dusty_tx, sweep_tx = self.create_ephemeral_dust_package(tx_version=3, dust_value=329)236# Valid sweep we will RBF incorrectly by not spending dust as well237self.nodes[0].submitpackage([dusty_tx["hex"], sweep_tx["hex"]])238assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])240# Doesn't spend in-mempool dust output from parent241unspent_sweep_tx = self.wallet.create_self_transfer_multi(fee_per_output=2000, utxos_to_spend=[dusty_tx["new_utxos"][0]], version=3)242assert_greater_than(unspent_sweep_tx["fee"], sweep_tx["fee"])243res = self.nodes[0].submitpackage([dusty_tx["hex"], unspent_sweep_tx["hex"]])244assert_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")245assert_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"])246assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"], sweep_tx["tx"]])248# Spend works with dust spent249sweep_tx_2 = self.wallet.create_self_transfer_multi(fee_per_output=2000, utxos_to_spend=dusty_tx["new_utxos"], version=3)250assert_not_equal(sweep_tx["hex"], sweep_tx_2["hex"])
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.