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()
|