Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 3: [PATCH 03/17] feat(policy): preserve cluster accounting and repair dynamic dust

test/functional/mempool_dynamic_dust.py

Before: /dev/null · After: test/functional/mempool_dynamic_dust.py

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..de571c276440f5717bbefa99d4307e2e5f202fed

Download original patch · Release notes

Diff rows 1–108 of 108. Line numbers refer to the original before/after files.

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.

test/functional/mempool_dynamic_dust.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 The Bitcoin Roots developers
3
+ # Distributed under the MIT software license, see the accompanying
4
+ # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
+ """Dynamic dust startup, publication, immutable deficits, retry and shutdown."""
6
+
7
+ from decimal import Decimal
8
+ import shutil
9
+
10
+ from test_framework.messages import COIN, CTxOut
11
+ from test_framework.test_framework import BitcoinTestFramework
12
+ from test_framework.util import assert_equal
13
+ from test_framework.wallet import MiniWallet
14
+
15
+
16
+ class DynamicDustTest(BitcoinTestFramework):
17
+ def set_test_params(self):
18
+ self.num_nodes = 1
19
+
20
+ def run_test(self):
21
+ node = self.nodes[0]
22
+ modes = ["off", "0", "target:2", "target:1008", "1e0*target:2", "mempool:1", "mempool:2147483647", "2147483.647*mempool:1"]
23
+ for mode in modes:
24
+ offset = node.debug_log_path.stat().st_size
25
+ self.restart_node(0, extra_args=[f"-dustdynamic={mode}", "-dustrelayfee=0.00003000"])
26
+ current = node.debug_log_path.read_text()[offset:]
27
+ assert_equal(current.count("Dynamic dust updates scheduled after chainstate load"), int(mode not in ["off", "0"]))
28
+ # Unavailable estimates preserve the configured floor at legal extremes.
29
+ wallet = MiniWallet(node)
30
+ tx = self.probe(wallet, value=500)
31
+ assert_equal(node.testmempoolaccept([tx.serialize().hex()])[0]["allowed"], True)
32
+ node.mockscheduler(901)
33
+
34
+ self.restart_node(0, extra_args=["-dustdynamic=target:2", "-blocksonly=1"])
35
+ node.mockscheduler(901)
36
+ self.restart_node(0, extra_args=["-dustdynamic=1*mempool:1", "-acceptnonstdtxn=1", "-checkmempool=1"])
37
+ wallet = MiniWallet(node)
38
+ for _ in range(10):
39
+ wallet.send_self_transfer(from_node=node, confirmed_only=True, fee_rate=Decimal("0.001"))
40
+ first = self.probe(wallet, value=100)
41
+ first_id = node.sendrawtransaction(first.serialize().hex())
42
+ first_fee = node.getmempoolentry(first_id)["fees"]
43
+ assert_equal(first_fee["base"], Decimal("0.001"))
44
+ assert_equal(first_fee["modified"], Decimal(100000 - 230) / COIN)
45
+ node.prioritisetransaction(first_id, 0, 987)
46
+ first_fee = node.getmempoolentry(first_id)["fees"]
47
+ self.restart_node(0, extra_args=["-dustdynamic=1*mempool:1", "-acceptnonstdtxn=1", "-checkmempool=1"])
48
+ self.wait_until(lambda: node.getmempoolinfo()["loaded"])
49
+ assert_equal(node.getmempoolentry(first_id)["fees"], first_fee)
50
+ wallet.rescan_utxos()
51
+ with node.assert_debug_log(["Updating dust feerate to 100.000 sat/vB"]):
52
+ node.mockscheduler(901)
53
+ assert_equal(node.getmempoolentry(first_id)["fees"], first_fee)
54
+ second = self.probe(wallet, value=100)
55
+ second_id = node.sendrawtransaction(second.serialize().hex())
56
+ assert_equal(node.getmempoolentry(second_id)["fees"]["modified"], Decimal(100000 - 10900) / COIN)
57
+ # Existing entries retain their admission deficit, and valid blocks use
58
+ # actual fees even after the live policy rate changes.
59
+ self.generate(wallet, 1)
60
+ with node.assert_debug_log(["Updating dust feerate to 3.000 sat/vB"]):
61
+ node.mockscheduler(901)
62
+ wallet.rescan_utxos()
63
+ probe = self.probe(wallet, value=100)
64
+ node.sendrawtransaction(probe.serialize().hex())
65
+ self.generate(wallet, 1)
66
+
67
+ # Exercise the existing production retry branch: its first mempool is
68
+ # destroyed before any dynamic-dust task is registered.
69
+ self.stop_node(0)
70
+ shutil.rmtree(node.blocks_path / "index")
71
+ offset = node.debug_log_path.stat().st_size
72
+ self.start_node(0, extra_args=["-dustdynamic=mempool:1", "-test=reindex_after_failure_noninteractive_yes"])
73
+ current = node.debug_log_path.read_text()[offset:]
74
+ assert_equal(current.count("Dynamic dust updates scheduled after chainstate load"), 1)
75
+ node.mockscheduler(901)
76
+ self.restart_node(0, extra_args=["-dustdynamic=target:2"])
77
+ node.mockscheduler(901)
78
+ # Warm the actual long-horizon estimator with individually confirmed
79
+ # transactions; then verify node scheduling publishes its raw estimate.
80
+ wallet = MiniWallet(node)
81
+ for _ in range(32):
82
+ for _ in range(10):
83
+ wallet.send_self_transfer(from_node=node, confirmed_only=True, fee_rate=Decimal("0.001"))
84
+ self.generate(wallet, 1)
85
+ wallet.rescan_utxos()
86
+ estimate = node.estimaterawfee(2, 0.8)["long"]["feerate"]
87
+ expected = estimate * COIN * 3 / 1000
88
+ assert expected > 3
89
+ with node.assert_debug_log([f"Updating dust feerate to {expected:.3f} sat/vB"]):
90
+ node.mockscheduler(901)
91
+ # The standardness snapshot must see publication too, while consensus
92
+ # accepts a block containing the locally rejected transaction.
93
+ probe = self.probe(wallet, value=500)
94
+ result = node.testmempoolaccept([probe.serialize().hex()])[0]
95
+ assert_equal(result["reject-reason"], "dust-nonanchor")
96
+ self.generateblock(node, wallet.get_address(), [probe.serialize().hex()])
97
+
98
+ def probe(self, wallet, value):
99
+ transfer = wallet.create_self_transfer(confirmed_only=True, fee=Decimal("0.001"))
100
+ tx = transfer["tx"]
101
+ tx.vout[0].nValue -= value
102
+ tx.vout.insert(0, CTxOut(value, wallet.get_output_script()))
103
+ wallet.sign_tx(tx)
104
+ return tx
105
+
106
+
107
+ if __name__ == '__main__':
108
+ DynamicDustTest(__file__).main()