Skip to content
Bitcoin Roots

v31.1-roots.1 · Commit 1: [PATCH 01/17] build(roots): establish v31 portability foundation

doc/feature-contracts/policy-parsing.md

Before: /dev/null · After: doc/feature-contracts/policy-parsing.md

File metadata
new file mode 100644
index 0000000000000000000000000000000000000000..c646bfdb4e044ea02e58efdfbcd61a9eae1795dd

Download original patch · Release notes

Diff rows 1–249 of 249. 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.

doc/feature-contracts/policy-parsing.md ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Roots policy input compatibility
2
+
3
+ **v31 candidate status: deferred.** This preserved contract describes the
4
+ accepted 30.3 source, including its documented gaps; the foundation does not
5
+ implement its Roots controls or RPC fields. Source code/test paths below refer
6
+ to `10d772eb7f4b00b5193428a0dde4c06558d6c297`, not current candidate behavior.
7
+ [The port review](../roots-v31-port-review.md) records ownership and current gates.
8
+
9
+ **Contract ID: policy-parsing.** Scope: startup input for the Roots-added local
10
+ policy controls listed below. This contract supplements `runtime-config` and the
11
+ policy contracts in [the catalog](../roots-features.md). It does not change
12
+ admission rules, block validity, or defaults. Scoped startup validation enforces
13
+ the decided grammar; the baseline column records the former permissive behavior
14
+ and explains migration.
15
+
16
+ ## Scope, defaults and ranges
17
+
18
+ | Options (without leading `-`) | Default | Accepted effective value |
19
+ | --- | --- | --- |
20
+ | `subdustfeepenalty`, `rejectparasites`, `acceptunknownwitness`, `datacarrierfullcount`, `permitbareanchor` | `1` | Boolean below |
21
+ | `rejecttokens`, `permitbarepubkey`, `permitbaredatacarrier`, `acceptnonstddatacarrier` | `0` | Boolean below |
22
+ | `maxscriptsize` | `1650` bytes | Integer, 0 through 4,294,967,295 |
23
+ | `bytespersigopstrict` | `20` bytes | Integer, 0 through 4,294,967,295 |
24
+ | `maxtxlegacysigops` | `2500` sigops | Integer, 0 through 4,294,967,295 |
25
+ | `minrelaycoinblocks` | `0` coin blocks | Integer, 0 through 9,223,372,036,854,775,807 |
26
+ | `minrelaymaturity` | `0` blocks | Integer, 0 through 2,147,483,647 |
27
+ | `blockmaxsize` | Unset: 4,000,000 serialized-byte cap, with 1,200,000 default weight cap | Integer, 0 through 4,000,000; retain existing template clamping |
28
+ | `blockprioritysize` | `100000` bytes | Integer, 0 through 9,223,372,036,854,775,807 |
29
+ | `datacarriercost` | `1` vbyte/data byte | Fixed point, 0 through 1,073,741,823.75; round up to quarters of a vbyte |
30
+ | `dustdynamic` | `off` | Existing structured grammar below |
31
+ | `mempoolreplacement` | `fee,-optin` (full RBF) | Mode/list below |
32
+ | `mempooltruc` | `enforce` | Mode/list below |
33
+ | `permitephemeral` | anchor allowed, dust/send disallowed | Mode/list below |
34
+ | `spkreuse` | `allow` | `allow`, `conflict`, or Boolean alias |
35
+
36
+ Inherited Core options, including `bytespersigop`, `datacarrier`,
37
+ `datacarriersize`, `permitbaremultisig`, `mempoolfullrbf`, `blockmaxweight`,
38
+ `blockreservedweight`, and fee amounts, keep their existing parsers. Changing
39
+ Roots defaults for an inherited option does not bring its parser into this scope.
40
+ Peer, wallet, RPC and runtime options are outside this contract.
41
+
42
+ **Baseline help conflict:** `blockmaxsize` help advertised `300000` as its
43
+ default, but initialized template options leave the serialized cap at 4,000,000
44
+ when it is unset. Omitting both size and weight options retains the conservative
45
+ 1,200,000 weight cap. Explicitly setting only `blockmaxsize` makes weight
46
+ unrestricted up to the consensus maximum; setting only `blockmaxweight` leaves
47
+ the serialized cap at 4,000,000. Preserve these effective defaults and option
48
+ interactions. Updated help reports the actual unset serialized cap.
49
+
50
+ ## Grammar and migration decisions
51
+
52
+ A Boolean accepts exactly `0`, `1`, or the empty string. The empty string,
53
+ including an elided CLI flag, means true. Keep `-noOPTION`, `-noOPTION=1`, and
54
+ `-noOPTION=0` (the last means true); explicit empty negation means false. A
55
+ negated value must itself be empty, `0`, or `1` before conversion, so malformed
56
+ negations cannot hide behind the inherited coercion. Other numbers, signs,
57
+ leading zeros, whitespace, textual `true`/`false`, fractions, and suffixes are
58
+ errors. JSON Boolean settings retain their corresponding Boolean meaning.
59
+
60
+ Integers consume the complete ASCII decimal string, with an optional `+` or `-`
61
+ sign and one or more digits. Leading zeros are allowed. The parsed value must
62
+ be nonnegative and fit the range above; `-0` remains zero. Empty/elided values,
63
+ whitespace, fractions, suffixes, and overflow are errors. The upper bounds
64
+ prevent narrowing/wrapping or saturating an out-of-range value; coin-block and
65
+ priority-size controls retain the nonnegative range of the former signed
66
+ 64-bit getter rather than silently expanding it. Existing `-noOPTION` and
67
+ `-noOPTION=1` retain zero; `-noOPTION=0` retains one, except fixed-point cost,
68
+ where its existing string conversion also means a cost of one vbyte. Negated
69
+ values are checked as Booleans before conversion.
70
+
71
+ Carrier cost retains the existing whole-value `ParseFixedPoint` grammar:
72
+ optional minus sign, integer part `0` or a nonzero digit followed by digits,
73
+ optional decimal point followed by at least one digit, and optional `e`/`E`
74
+ exponent with optional sign and at least one digit. The result must be exactly
75
+ representable in hundredths and within the range above. Thus `1.2300`, `1e0`,
76
+ `1e-2` and `-0` remain valid; `+1`, `01`, `.5`, `1.`, `0.001`, whitespace,
77
+ trailing junk and arithmetic overflow remain invalid. There is no new ban on
78
+ exponents or redundant fractional zeros. The existing fixed-point parser's
79
+ bounded mantissa/exponent rules remain authoritative, including for zero.
80
+
81
+ `dustdynamic` retains `off`/`0` or
82
+ `[<multiplier>*]target:<blocks>` / `[<multiplier>*]mempool:<kvB>`.
83
+ The multiplier uses the same fixed-point grammar with thousandth precision,
84
+ scaled range 1 through 2,147,483,647; default multiplier is 3. Target is an
85
+ unsigned decimal integer from 2 through the existing estimator horizon.
86
+ Mempool position is a signed decimal integer from 1 through 2,147,483,647.
87
+ Their existing `ToIntegral` grammar rejects a leading `+` and accepts leading
88
+ zeros. Empty values, extra separators, suffixes and unknown modes are errors.
89
+ `-nodustdynamic` and `-nodustdynamic=1` retain `off`/`0`; the inherited
90
+ double-negative `-nodustdynamic=0` yields `1` and remains an invalid mode.
91
+ Validate the raw negated Boolean value before conversion.
92
+
93
+ Keep these mode/list forms and their existing precedence semantics:
94
+
95
+ - `mempoolreplacement`: Boolean alias, or `fee`, `optin`, `-optin` tokens
96
+ separated by `,` or `+`. `0` disables replacement; empty/`1` enables fee-based
97
+ replacement. Repeated recognized tokens retain their current meaning;
98
+ the last `optin`/`-optin` token wins. Preserve the existing interaction with
99
+ inherited `mempoolfullrbf`, including its contradiction warning.
100
+ - `mempooltruc`: Boolean alias, or `reject`, `accept`, `enforce`, `optin`,
101
+ `-enforce`, `-optin`, `0` tokens separated by `,` or `+`. Empty/`1` means
102
+ enforce. `0`/`reject` means reject; enforce takes priority over accept;
103
+ later assignments to the same flag win. Preserve all recognized aliases.
104
+ - `permitephemeral`: Boolean alias, or `anchor`, `dust`, `send`, `reject`, `0`
105
+ tokens with an optional `-` prefix, separated by `,` or `+`. Empty/`1` enables
106
+ all three exceptions, `0`/`reject` disables them. Preserve existing implied
107
+ flags and last assignment to each flag, including signed recognized tokens.
108
+ - `spkreuse`: `allow`/`1`/empty allows reuse, `conflict`/`0` rejects conflicts.
109
+
110
+ Lists must contain recognized nonempty tokens only; unknown tokens and leading,
111
+ trailing, or doubled separators are errors. Whitespace is not token syntax.
112
+ Booleans used as aliases must consume the entire string; `2` and `1junk` are
113
+ errors. Negation retains each mode's existing Boolean alias behavior.
114
+
115
+ | Input class | Baseline behavior | Decided behavior / migration |
116
+ | --- | --- | --- |
117
+ | Omitted option | Built-in default | Unchanged |
118
+ | Empty/elided Boolean or list | Enables Boolean behavior | Unchanged; use `=0` to disable explicitly |
119
+ | Empty/elided integer | Converts to zero | Error; write `=0` |
120
+ | `+1`, `01`, `-0` integer | Numeric conversion | Retain within range |
121
+ | `+1`, `01`, `-1`, `2` Boolean | Nonzero becomes true | Error; write `=1` or `=0` |
122
+ | CLI value containing outer whitespace | Numeric getters trim; carrier cost rejects | Error for scoped values; remove whitespace |
123
+ | Config whitespace around `=`/value | Config reader trims before parsing | Retain config formatting; parser sees trimmed value |
124
+ | `12junk`, `1.5` integer | Prefix becomes 12 or 1 | Error; write the intended whole integer |
125
+ | `junk`, `false`, `0.5` Boolean | Converts to false | Error; write `=0` |
126
+ | Signed overflow | Getter saturates, then may narrow/wrap | Error; choose an in-range value |
127
+ | Negative nonzero integer | May wrap in unsigned destinations | Error; use zero or a supported positive value |
128
+ | Unknown list token / empty token | Ignored, possibly changing the selected mode | Error; remove or correct token |
129
+ | Unknown `spkreuse` string | Selects conflict rejection | Error; write `conflict` |
130
+ | Exact fractional/exponent carrier cost | Whole-value parse | Unchanged; not a partial parse |
131
+ | Malformed negated value | Converts permissively before policy sees it | Error; use an explicit `0` or `1` |
132
+
133
+ These incompatibilities replace silent guesses with actionable startup errors.
134
+ They do not authorize narrowing valid documented policy semantics or changing
135
+ how accepted values influence transactions or templates.
136
+
137
+ ## Sources, precedence and diagnostics
138
+
139
+ Preserve `runtime-config` precedence: forced settings, CLI, read/write config,
140
+ JSON settings, network config section, default config section, built-in default.
141
+ For scalar options the last CLI occurrence and first config occurrence win after
142
+ inherited negation processing. Validate the selected effective value; shadowed
143
+ ordinary values need not cause errors. Raw negations must be checked before the
144
+ argument manager converts them, even when subsequently shadowed. No change to
145
+ how config comments or outer whitespace are removed is intended. Config lines
146
+ without `=` remain syntax errors; CLI elision follows the option grammar above.
147
+
148
+ Invalid scoped effective values must produce a startup error naming the option,
149
+ the offending value, and its accepted grammar/range. Preserve the existing
150
+ `Invalid -datacarriercost value: '…'` diagnostic prefix and the specific
151
+ `dustdynamic` failure context, adding useful bounds without disguising the
152
+ reason. Errors must not report unrelated configuration values or secrets.
153
+
154
+ ## Baseline evidence and remaining acceptance gates
155
+
156
+ Characterized at `f66eca4ae466e198fecd66cac22a0033918b12ec` (same production
157
+ parser as source `17e1d484aba2a881be03b8928fba3c0125e027a2`). Paths:
158
+ argument conversion (`src/common/args.cpp`, source reference),
159
+ config reading (`src/common/config.cpp`, source reference),
160
+ merging (`src/common/settings.cpp`, source reference),
161
+ mempool options (`src/node/mempool_args.cpp`, source reference),
162
+ startup options (`src/init.cpp`, source reference),
163
+ template options (`src/node/miner.cpp`, source reference), and
164
+ fixed-point conversion (`src/util/strencodings.cpp`, source reference).
165
+
166
+ Focused baseline evidence: `getarg_tests` (10 cases),
167
+ `feature_config_args.py`, and a 160-case isolated regtest startup matrix exercising
168
+ Boolean/integer/cost classes, list aliases/junk, CLI/config pairs, repeated
169
+ options, section precedence, negation and bounds. Configuration trims outer
170
+ whitespace; CLI does not. The cost cases verify last CLI / first config /
171
+ CLI-over-config / network-over-default selection with invalid shadowed values.
172
+ The matrix produced 127 clean startups/shutdowns, 30 expected startup diagnostics,
173
+ and the three existing runtime aborts described below. All 160 outcomes matched
174
+ the source-derived characterization; an expected rejection is not a test failure.
175
+
176
+ **Existing runtime gap:** non-off `dustdynamic` modes can parse successfully but
177
+ abort startup because the mempool is constructed without its required scheduler.
178
+ Parser acceptance does not prove these modes operate. This contract records that
179
+ baseline failure; repairing scheduler wiring is a separate runtime change.
180
+
181
+ Acceptance: policy_args_tests.cpp (`src/test/policy_args_tests.cpp`, source reference)
182
+ `boolean_values`, `integer_boundaries`, `carrier_cost_compatibility`, `mode_tokens`,
183
+ `raw_negation_is_scoped`, `defaults_and_mode_semantics`,
184
+ `settings_types_and_precedence`, and `dynamic_dust_grammar_without_runtime_claim`
185
+ cover parser boundaries, exact weight rounding, defaults, types and aliases.
186
+ feature_policy_args.py (`test/functional/feature_policy_args.py`, source reference) `run_test`
187
+ covers isolated CLI/config startup parity, shadowing, negation and unchanged
188
+ inherited Core parsing. Dynamic-dust non-off grammar is unit-tested without
189
+ claiming a working startup. Configuration and affected
190
+ carrier/sub-dust/dust/replacement/TRUC/template policy suites remain regression
191
+ gates.
192
+ Retirement requires an upstream parser with the same grammar, default values,
193
+ precedence, diagnostics and covered policy behavior. No PR number is known;
194
+ a release note must be added when one is assigned.
195
+
196
+ ## Scoped fuzz evidence
197
+
198
+ `policy_args` in policy_args.cpp (`src/test/fuzz/policy_args.cpp`, source reference) exercises
199
+ the production option validator, carrier-cost and dynamic-dust parsers, and raw
200
+ Boolean negation. Its independent bounded decimal oracle checks complete integer
201
+ consumption and range limits; Boolean grammar, rejection of appended junk,
202
+ canonical cost/dynamic round trips and JSON setting types have explicit
203
+ invariants. The entire input is a value, retaining embedded NULs; only the argv
204
+ negation check skips NUL-containing values because argv strings cannot carry them.
205
+ Dynamic parsing does not construct a dynamic mempool or establish runtime support.
206
+
207
+ `subdust_fee_penalty` exercises actual admission and entry fee accounting against
208
+ a wide-integer oracle; its supported arithmetic domain and remaining gates are
209
+ in [the sub-dust contract](subdust.md). Both targets use the existing fuzz registry.
210
+
211
+ Use the existing `libfuzzer` preset when its compiler runtime is installed. A
212
+ sanitizer corpus replay can also use the supported standalone fuzz runner:
213
+
214
+ ```sh
215
+ cmake -B build_fuzz_policy -G Ninja -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ \
216
+ -DCMAKE_BUILD_TYPE=Debug -DBUILD_FOR_FUZZING=ON -DSANITIZERS=address,undefined \
217
+ -DENABLE_WALLET=OFF -DBUILD_GUI=OFF -DENABLE_IPC=OFF
218
+ cmake --build build_fuzz_policy -j4
219
+ ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 FUZZ=policy_args \
220
+ timeout 120 build_fuzz_policy/bin/fuzz /path/to/local/policy_args
221
+ ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 FUZZ=subdust_fee_penalty \
222
+ timeout 120 build_fuzz_policy/bin/fuzz /path/to/local/subdust_fee_penalty
223
+ ```
224
+
225
+ Keep inputs and artifacts outside Git. Capture the exact source revision and dirty
226
+ diff, binary/configuration, generator, PRNG seed, input counts, wall time and exits
227
+ for every campaign. The preservation run at base `b60a6e04` plus the L5 test diff
228
+ used GCC 14 ASan/UBSan, Python PRNG seed `310503`, 52 parser boundary spellings
229
+ plus 5,000 mutations, and 225 arithmetic boundaries plus 1,000 generated cases.
230
+ Both 120-second replay budgets completed: 5,052 parser inputs in 34 seconds and
231
+ 1,225 arithmetic inputs in 4 seconds, without sanitizer reports. These are bounded
232
+ smoke results, not proof of exhaustive coverage.
233
+
234
+ Parser seeds include empty/Boolean/signed/overflow integers, exact and rounded
235
+ carrier costs, exponents, embedded NUL and trailing junk, malformed lists, JSON
236
+ types, dynamic multiplier/target bounds and a 4,096-digit value. Arithmetic seeds
237
+ combine rates `0`, `1`, `3000`, `INT64_MAX/2` and `INT64_MAX`; actual fees `0`,
238
+ `1000`, `1000000`; deltas `-MAX_MONEY`, `0`, `MAX_MONEY`; zero-valued P2TR/P2A/
239
+ OP_RETURN, mixed sixteen-output fixtures and sixteen above-default-dust outputs.
240
+ Arithmetic inputs consume integers from the end using `FuzzedDataProvider`:
241
+ rate (8 bytes), fee (3), delta plus `MAX_MONEY` (7), output count minus one (1),
242
+ then each output value (2) and script index (1); integers are little endian and
243
+ the chunks are concatenated in reverse consumption order.
244
+
245
+ The installed Clang 19 lacked compiler-rt/libFuzzer/ASan archives; the compile
246
+ probe failed before testing. GCC sanitizer compilation and execution succeeded.
247
+ The executed campaigns therefore used corpus replay, including generated
248
+ mutations, with no coverage-guided engine. A coverage-guided libFuzzer campaign
249
+ remains an explicit evidence gap, and cannot be claimed from these replay counts.