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_graph_policy.py

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

File metadata
new file mode 100755
index 0000000000000000000000000000000000000000..2ceeeaf485c311cab009a6ab3ab7c2de094024a4

Download original patch · Release notes

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

+ Added− RemovedUnmarked lines are context.

Scroll the code area horizontally to read long lines. The two line-number columns refer to before and after.

test/functional/mempool_graph_policy.py ADDED
@@ -0,0 +1,112 @@
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
+ """Native cluster policy controls, script claims and reorg exceptions."""
6
+
7
+ from decimal import Decimal
8
+
9
+ from test_framework.messages import CTxOut
10
+ from test_framework.test_framework import BitcoinTestFramework
11
+ from test_framework.util import assert_equal, assert_raises_rpc_error
12
+ from test_framework.wallet import MiniWallet
13
+
14
+
15
+ class GraphPolicyTest(BitcoinTestFramework):
16
+ def set_test_params(self):
17
+ self.num_nodes = 1
18
+ self.extra_args = [["-checkmempool=1"]]
19
+
20
+ def make_tx(self, wallet, destination, fee_rate=Decimal("0.001")):
21
+ result = wallet.create_self_transfer(fee_rate=fee_rate)
22
+ result["tx"].vout[0].scriptPubKey = destination.get_output_script()
23
+ wallet.sign_tx(result["tx"])
24
+ return result["tx"]
25
+
26
+ def run_test(self):
27
+ node = self.nodes[0]
28
+ funding = MiniWallet(node)
29
+ other = MiniWallet(node, tag_name="graph-other")
30
+ destination = MiniWallet(node, tag_name="graph-destination")
31
+ self.generate(other, 101)
32
+ funding.rescan_utxos()
33
+ other.rescan_utxos()
34
+
35
+ for mode in ["reject", "accept", "enforce"]:
36
+ for replacement_mode in ["0", "optin", "fee,-optin"]:
37
+ self.restart_node(0, extra_args=[f"-mempooltruc={mode}", f"-mempoolreplacement={replacement_mode}", "-checkmempool=1"])
38
+ tx = funding.create_self_transfer(version=3, sequence=0xffffffff)
39
+ if mode == "reject":
40
+ assert_equal(node.testmempoolaccept([tx["hex"]])[0]["reject-reason"], "version")
41
+ # Local version rejection cannot reject a valid block.
42
+ self.generateblock(node, output=funding.get_address(), transactions=[tx["hex"]])
43
+ block_hash = node.getbestblockhash()
44
+ node.invalidateblock(block_hash)
45
+ assert_equal(node.getrawmempool(), [])
46
+ node.reconsiderblock(block_hash)
47
+ else:
48
+ node.sendrawtransaction(tx["hex"])
49
+ # A direct non-signaling v3 conflict is replaceable in OptIn.
50
+ replacement = tx["tx"]
51
+ replacement.vout[0].nValue -= 10000
52
+ funding.sign_tx(replacement)
53
+ if replacement_mode == "0":
54
+ assert_raises_rpc_error(-26, "txn-mempool-conflict", node.sendrawtransaction, replacement.serialize().hex())
55
+ else:
56
+ node.sendrawtransaction(replacement.serialize().hex())
57
+ self.generate(funding, 1)
58
+ funding.rescan_utxos()
59
+
60
+ # Accept mode permits a non-TRUC parent; Enforce mode checks inheritance.
61
+ if mode != "reject":
62
+ parent = funding.create_self_transfer(version=2)
63
+ node.sendrawtransaction(parent["hex"])
64
+ child = funding.create_self_transfer(utxo_to_spend=parent["new_utxo"], version=3)
65
+ result = node.testmempoolaccept([child["hex"]])[0]
66
+ assert_equal(result["allowed"], mode == "accept")
67
+ if mode == "enforce":
68
+ assert_equal(result["reject-reason"], "TRUC-violation")
69
+ self.generate(funding, 1)
70
+ funding.rescan_utxos()
71
+
72
+ self.restart_node(0, extra_args=["-spkreuse=conflict", "-checkmempool=1"])
73
+ change = funding.create_self_transfer()
74
+ assert_equal(node.testmempoolaccept([change["hex"]])[0]["reject-reason"], "txn-spk-reused-change")
75
+ twin = self.make_tx(funding, destination)
76
+ twin.vout.append(CTxOut(1000, destination.get_output_script()))
77
+ twin.vout[0].nValue -= 1000
78
+ funding.sign_tx(twin)
79
+ assert_equal(node.testmempoolaccept([twin.serialize().hex()])[0]["reject-reason"], "txn-spk-reused-twinoutputs")
80
+
81
+ original = self.make_tx(funding, destination)
82
+ original_id = node.sendrawtransaction(original.serialize().hex())
83
+ # Same output script, independent input, fee-based policy replacement.
84
+ replacement = self.make_tx(other, destination, Decimal("0.01"))
85
+ replacement_id = node.sendrawtransaction(replacement.serialize().hex())
86
+ assert_equal(node.getrawmempool(), [replacement_id])
87
+ assert original_id != replacement_id
88
+ assert_raises_rpc_error(-26, "insufficient fee", node.sendrawtransaction, original.serialize().hex())
89
+ self.generate(funding, 1)
90
+
91
+ # Reorg admission retains local reuse checks. A consensus-valid block
92
+ # with conflicting output scripts is accepted; only the first original
93
+ # returns because the second cannot pay for policy replacement.
94
+ funding.rescan_utxos()
95
+ other.rescan_utxos()
96
+ first = self.make_tx(funding, destination)
97
+ second = self.make_tx(other, destination)
98
+ block = self.generateblock(node, output=funding.get_address(), transactions=[first.serialize().hex(), second.serialize().hex()])
99
+ node.invalidateblock(block["hash"])
100
+ assert_equal(node.getrawmempool(), [first.txid_hex])
101
+ other.rescan_utxos()
102
+ probe = self.make_tx(other, destination)
103
+ assert_equal(node.testmempoolaccept([probe.serialize().hex()])[0]["allowed"], False)
104
+ self.generateblock(node, output=funding.get_address(), transactions=[first.txid_hex])
105
+ assert_equal(node.testmempoolaccept([probe.serialize().hex()])[0]["allowed"], True)
106
+
107
+ self.restart_node(0, extra_args=["-spkreuse=allow", "-checkmempool=1"])
108
+ assert_equal(node.testmempoolaccept([twin.serialize().hex()])[0]["allowed"], True)
109
+
110
+
111
+ if __name__ == '__main__':
112
+ GraphPolicyTest(__file__).main()