Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 2: [PATCH 02/17] feat(policy): preserve Roots parsing and local transaction checks

test/functional/feature_policy_args.py

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

File metadata
new file mode 100755
index 0000000000000000000000000000000000000000..bedfb67366a4793e5050a6d962bc7c9d073c2894

Download original patch · Release notes

Diff rows 1–178 of 178. 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/feature_policy_args.py ADDED
@@ -0,0 +1,178 @@
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
+ """Check Roots policy grammar at startup and preserve configuration precedence."""
6
+
7
+ import json
8
+ import re
9
+
10
+ from test_framework.test_framework import BitcoinTestFramework
11
+ from test_framework.test_node import ErrorMatch
12
+ from test_framework.util import assert_equal
13
+
14
+
15
+ class PolicyArgsTest(BitcoinTestFramework):
16
+ def set_test_params(self):
17
+ self.setup_clean_chain = True
18
+ self.num_nodes = 1
19
+ self.uses_wallet = None
20
+ self.wallet_names = []
21
+
22
+ def setup_network(self):
23
+ self.add_nodes(self.num_nodes)
24
+
25
+ def run_test(self):
26
+ node = self.nodes[0]
27
+ config_path = node.datadir_path / "bitcoin.conf"
28
+ base_config = config_path.read_text(encoding="utf-8")
29
+ passed_startups = 0
30
+ rejected_startups = 0
31
+
32
+ def check_start(cli=None, config="", *, default_config=""):
33
+ nonlocal passed_startups
34
+ config_path.write_text(default_config + base_config + config, encoding="utf-8")
35
+ self.start_node(0, extra_args=cli or [])
36
+ assert_equal(node.getblockchaininfo()["chain"], "regtest")
37
+ # Staged controls must visibly warn; verify the complete diagnostic.
38
+ staged = ["datacarriercost", "subdustfeepenalty", "mempoolreplacement", "mempoolfullrbf", "mempooltruc", "spkreuse", "dustdynamic", "blockmaxsize", "blockprioritysize"]
39
+ supplied = set()
40
+ for arg in cli or []:
41
+ name = arg.split("=", 1)[0].lstrip("-")
42
+ supplied.add(name[2:] if name.startswith("no") else name)
43
+ for line in (default_config + config).splitlines():
44
+ name = line.split("=", 1)[0].strip()
45
+ supplied.add(name[2:] if name.startswith("no") else name)
46
+ settings_path = node.chain_path / "settings.json"
47
+ if settings_path.exists():
48
+ supplied.update(json.loads(settings_path.read_text(encoding="utf-8")))
49
+ expected = "\n".join(
50
+ f"Warning: -{name} is parsed but not enforced in this staged candidate; graph policy and mining implementations are pending."
51
+ for name in staged if name in supplied
52
+ )
53
+ self.stop_node(0, expected_stderr=expected)
54
+ passed_startups += 1
55
+
56
+ def check_error(option, value, *, config=False, negated=False, cli=None):
57
+ nonlocal rejected_startups
58
+ key = ("no" if negated else "") + option
59
+ config_path.write_text(base_config + (f"{key}={value}\n" if config else ""), encoding="utf-8")
60
+ args = ([] if config else [f"-{key}={value}"]) + (cli or [])
61
+ node.assert_start_raises_init_error(
62
+ extra_args=args,
63
+ expected_msg=re.escape(f"Invalid -{key} value: '{value}'"),
64
+ match=ErrorMatch.PARTIAL_REGEX,
65
+ )
66
+ rejected_startups += 1
67
+
68
+ self.log.info("Default policy configuration starts unchanged")
69
+ check_start()
70
+
71
+ self.log.info("Malformed effective values fail equally on CLI and in config")
72
+ invalid = {
73
+ "subdustfeepenalty": "1junk",
74
+ "rejectparasites": "2",
75
+ "acceptunknownwitness": "+1",
76
+ "datacarrierfullcount": "01",
77
+ "permitbareanchor": "false",
78
+ "rejecttokens": "-1",
79
+ "permitbarepubkey": "0.5",
80
+ "permitbaredatacarrier": "junk",
81
+ "acceptnonstddatacarrier": "1.0",
82
+ "maxscriptsize": "12junk",
83
+ "bytespersigopstrict": "4294967296",
84
+ "maxtxlegacysigops": "-1",
85
+ "minrelaycoinblocks": "9223372036854775808",
86
+ "minrelaymaturity": "2147483648",
87
+ "blockmaxsize": "4000001",
88
+ "blockprioritysize": "1.5",
89
+ "datacarriercost": "1073741823.76",
90
+ "mempoolreplacement": "fee,junk",
91
+ "mempooltruc": "accept,,enforce",
92
+ "permitephemeral": "anchor,",
93
+ "spkreuse": "garbage",
94
+ }
95
+ for option, value in invalid.items():
96
+ check_error(option, value)
97
+ check_error(option, value, config=True)
98
+
99
+ self.log.info("Negation is checked before coercion, even when shadowed")
100
+ for option in [*invalid, "dustdynamic"]:
101
+ check_error(option, "junk", negated=True, cli=[f"-{option}=0"])
102
+ check_error(option, "junk", config=True, negated=True, cli=[f"-{option}=0"])
103
+
104
+ self.log.info("Empty integer values and CLI whitespace cannot partially parse")
105
+ check_error("maxscriptsize", "")
106
+ check_error("maxscriptsize", "", config=True)
107
+ check_error("subdustfeepenalty", " 1")
108
+ check_error("datacarriercost", "1 ")
109
+ config_path.write_text(base_config, encoding="utf-8")
110
+ node.assert_start_raises_init_error(
111
+ extra_args=["-maxscriptsize"],
112
+ expected_msg="Invalid -maxscriptsize value: ''",
113
+ match=ErrorMatch.PARTIAL_REGEX,
114
+ )
115
+ rejected_startups += 1
116
+
117
+ self.log.info("Valid boundaries, aliases, exponent precision and whitespace formatting remain supported")
118
+ valid = {
119
+ "subdustfeepenalty": "", "rejectparasites": "0", "acceptunknownwitness": "1",
120
+ "datacarrierfullcount": "1", "permitbareanchor": "1", "rejecttokens": "0",
121
+ "permitbarepubkey": "0", "permitbaredatacarrier": "0", "acceptnonstddatacarrier": "0",
122
+ "maxscriptsize": "+1650", "bytespersigopstrict": "020", "maxtxlegacysigops": "4294967295",
123
+ "minrelaycoinblocks": "9223372036854775807", "minrelaymaturity": "2147483647",
124
+ "blockmaxsize": "4000000", "blockprioritysize": "9223372036854775807",
125
+ "datacarriercost": "1.2300", "dustdynamic": "off", "mempoolreplacement": "fee+-optin",
126
+ "mempooltruc": "accept,enforce", "permitephemeral": "0,anchor", "spkreuse": "conflict",
127
+ }
128
+ check_start([f"-{option}={value}" for option, value in valid.items()])
129
+ check_start(config="".join(f"{option} = {value} \n" for option, value in valid.items()))
130
+ for cost in ["-0", "0.01", "0.25", "0.26", "1e0", "1e-2", "1073741823.75"]:
131
+ check_start([f"-datacarriercost={cost}"])
132
+ check_start(["-subdustfeepenalty", "-mempoolreplacement", "-mempooltruc", "-permitephemeral", "-spkreuse"])
133
+ check_start(["-nosubdustfeepenalty", "-nomaxscriptsize=0", "-nodatacarriercost", "-nomempoolreplacement", "-nomempooltruc", "-nopermitephemeral", "-nospkreuse"])
134
+ check_start(["-nosubdustfeepenalty=0", "-maxscriptsize=-0"])
135
+
136
+ self.log.info("Last CLI, first config, network section and CLI-over-config selection remain unchanged")
137
+ check_start(["-subdustfeepenalty=junk", "-subdustfeepenalty=1"], config="subdustfeepenalty=junk\n")
138
+ check_start(config="subdustfeepenalty=1\nsubdustfeepenalty=junk\n")
139
+ check_start(default_config="subdustfeepenalty=junk\n", config="subdustfeepenalty=1\n")
140
+ config_path.write_text(base_config, encoding="utf-8")
141
+ node.assert_start_raises_init_error(extra_args=["-subdustfeepenalty=1", "-subdustfeepenalty=junk"], expected_msg="Invalid -subdustfeepenalty value: 'junk'", match=ErrorMatch.PARTIAL_REGEX)
142
+ rejected_startups += 1
143
+ config_path.write_text(base_config + "subdustfeepenalty=junk\nsubdustfeepenalty=1\n", encoding="utf-8")
144
+ node.assert_start_raises_init_error(extra_args=[], expected_msg="Invalid -subdustfeepenalty value: 'junk'", match=ErrorMatch.PARTIAL_REGEX)
145
+ rejected_startups += 1
146
+
147
+ self.log.info("Inherited Core numeric and Boolean parsers retain permissive input")
148
+ check_start(["-bytespersigop=20junk", "-datacarriersize=83junk", "-datacarrier=1junk", "-permitbaremultisig=2", "-mempoolfullrbf=1junk"])
149
+ check_start(config="bytespersigop=20junk\ndatacarriersize=83junk\ndatacarrier=1junk\npermitbaremultisig=2\nmempoolfullrbf=1junk\n")
150
+
151
+ # Only malformed dynamic modes are startup-tested. Valid non-off grammar
152
+ # is unit-tested without constructing a mempool: production scheduler
153
+ # wiring is an existing separate runtime gap, not a successful startup.
154
+ config_path.write_text(base_config, encoding="utf-8")
155
+ node.assert_start_raises_init_error(extra_args=["-dustdynamic=target:2junk"], expected_msg="Invalid mode for dustdynamic: failed to parse target block count", match=ErrorMatch.PARTIAL_REGEX)
156
+ rejected_startups += 1
157
+
158
+ self.log.info("JSON settings use the same effective grammar; confrw integration belongs L12")
159
+ settings_path = node.chain_path / "settings.json"
160
+ for value in [True, False]:
161
+ settings_path.write_text(json.dumps({"subdustfeepenalty": value}), encoding="utf-8")
162
+ check_start(config="subdustfeepenalty=junk\n")
163
+ settings_path.write_text(json.dumps({"subdustfeepenalty": 2}), encoding="utf-8")
164
+ config_path.write_text(base_config, encoding="utf-8")
165
+ node.assert_start_raises_init_error(extra_args=[], expected_msg="Invalid -subdustfeepenalty value: '2'", match=ErrorMatch.PARTIAL_REGEX)
166
+ rejected_startups += 1
167
+ for value in [[], {}]:
168
+ settings_path.write_text(json.dumps({"subdustfeepenalty": value}), encoding="utf-8")
169
+ node.assert_start_raises_init_error(extra_args=[], expected_msg="Invalid -subdustfeepenalty value: expected a scalar policy value", match=ErrorMatch.PARTIAL_REGEX)
170
+ rejected_startups += 1
171
+ check_start(["-subdustfeepenalty=1"])
172
+ settings_path.write_text("{}", encoding="utf-8")
173
+ config_path.write_text(base_config, encoding="utf-8")
174
+ self.log.info("Policy startup cases: %d passed starts, %d expected diagnostic rejections", passed_startups, rejected_startups)
175
+
176
+
177
+ if __name__ == '__main__':
178
+ PolicyArgsTest(__file__).main()