Skip to content

Commit 5244932

Browse files
committed
Expose bounded full-string correction
Add frozen, slotted correction context, edit, and candidate records plus a deterministic public API over the existing fixed BCH decoder. Keep the HRP and separator immutable, reparse every candidate, constrain wallet context before return, and keep BIP39 correction API-only. Collapse private decoder diagnostics into the public no-candidate result, route the CLI through the public boundary, and add all-profile, malformed-corpus, differential, and bounded fuzz evidence while preserving the 3,000-line package limit.
1 parent a9b9778 commit 5244932

19 files changed

Lines changed: 557 additions & 177 deletions

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ assert isinstance(secret, MasterSeed)
103103
additional = derive_share([a, c], "d")
104104
```
105105

106+
Full-string correction returns immutable, untrusted candidates and requires an
107+
explicit registered profile:
108+
109+
```python
110+
from codex32 import CorrectionContext, Profile, correct
111+
112+
candidates = correct(CorrectionContext(Profile.MS), "ms10tests?xxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw")
113+
for candidate in candidates:
114+
print(candidate.artifact)
115+
```
116+
117+
The HRP and separator are never corrected. A candidate is checksum-valid, not
118+
proof that it belongs to the intended wallet; compare it with the physical
119+
backup.
120+
106121
Worksheet residue correction is also available without disclosing the backup's
107122
profile or length:
108123

‎docs/architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ semantics. No artifact crosses the parsing boundary until every stage passes.
5454
hidden state.
5555

5656
Private Python names are convention rather than access control. The supported
57-
surface is the 20-name package `__all__`; direct use of private helpers is
57+
surface is the 25-name package `__all__`; direct use of private helpers is
5858
unsupported but remains in the review scope.
5959

6060
## Size budget

‎docs/correction.md‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,34 @@ boundary. A bounded structural adapter is a cuttable pre-v1 gate in
66
the plan's completeness, performance, size, and audit requirements, v1 remains
77
fixed-length-only and structural correction moves to v1.1.
88

9+
## Public full-string API
10+
11+
`correct(CorrectionContext(...), damaged_text)` accepts every fixed registered
12+
profile. Its context may constrain the complete canonical length, the
13+
five-symbol threshold-plus-identifier header, and ordinary share indices that
14+
are already in use. The HRP and separator are immutable. A differing expected
15+
length receives no structural search in the current fixed-length
16+
implementation.
17+
18+
The function returns an immutable tuple of `CorrectionCandidate` records in
19+
deterministic rank order. A valid unchanged string returns one candidate with
20+
no edits; no valid correction returns `()`. Malformed context raises
21+
`InvalidCorrectionInput`. Every candidate contains an ordinary parsed `Share`
22+
or `Secret`; the decoder never exposes a checksum-only result.
23+
24+
Fixed correction emits only `substitution` and `erasure` edits. Insertions,
25+
deletions, and transpositions are reserved record kinds for the cuttable
26+
structural gate. Edit coordinates are zero-based from the end of the
27+
data/checksum body. `observed` and `replacement` preserve string case.
28+
`estimated_search_bits` is zero for fixed decoding, `erasures_filled` and
29+
`addend_hamming_weight` expose the deterministic secondary ranking inputs, and
30+
`crc_padding_match` is a boolean only for an `ms` S candidate. It is `None` for
31+
shares and other profiles and never affects validity.
32+
33+
The API completes its fixed search without a deadline or provisional result.
34+
All four profiles are available to API callers. The CLI deliberately offers
35+
full-string correction only for `ms` and `cl`.
36+
937
## Candidate structural-correction gate
1038

1139
A future `indel.py` may search bounded insertion and deletion candidates, but
@@ -75,8 +103,8 @@ All algebra uses PR #70's native zero-based reverse coordinates: index 0 is the
75103
last data/checksum character. This convention is never converted using an HRP,
76104
separator, payload length, or complete-string length.
77105

78-
Full-string correction requires a `suspected_profile`. The adapter accepts the
79-
exact registered HRP and its separator as an immutable prefix. Printable
106+
Full-string correction requires a `CorrectionContext` with an exact registered
107+
profile. The adapter accepts that HRP and its separator as an immutable prefix. Printable
80108
non-Bech32 characters after that boundary are erasures, including a later `1`.
81109
The format layer owns checksum selection. A correction outside the visible body is
82110
rejected, and every candidate must pass the ordinary `parse_codex32` boundary.
@@ -122,7 +150,7 @@ displays positions as one-based and performs only `position - 1` conversion.
122150
| Error-plus-erasure BCH decoding | `_bch_error_corrections` |
123151
| Unique arbitrary/consecutive erasure fallback | `_solve_linear`, `_linear_error_corrections` |
124152
| Root-subgroup and final target verification | `_bch_error_corrections`, `_corrections_reach_target` |
125-
| Fixed registered-profile adapter | `_correct_fixed` |
153+
| Public registered-profile adapter | `correct`, over `_correct_fixed` |
126154
| Application-agnostic residue adapter | `correct_worksheet_residue` |
127155

128156
The target constants are checked against the imported checksum constants. No
@@ -139,10 +167,9 @@ for 13 consecutive regular-checksum erasures and 15 consecutive Long-checksum
139167
erasures. Other uniquely solvable erasure patterns may succeed but are not a
140168
guarantee.
141169

142-
Failure records distinguish lexical text, immutable prefix, selected-profile
143-
shape, algebra, correction outside the visible body, and final semantic reparse.
144-
Algebra failures retain both BCH and linear-stage diagnostics rather than
145-
claiming that a mixed-corruption input failed for one inferred reason.
170+
Lexical, prefix, profile-shape, algebra, visible-body, and semantic-reparse
171+
failures all collapse to no public candidate. This avoids presenting one
172+
internal decoder stage as a diagnosis of the physical transcription error.
146173

147174
Decoder success is fail-closed twice: every synthesized locator root must map
148175
to a legal reverse position in the selected checksum period, and the complete
@@ -164,3 +191,10 @@ The implementation carries the MIT notice from Blockstream's PR #70 head
164191
The source-derived offline corpus and its limitations are recorded in
165192
`source-manifest.md`; `tools/differential_correction.py --verify` checks it
166193
without network or Haskell dependencies.
194+
195+
The frozen malformed corpus exercises parsing, checksum completion,
196+
interpolation, correction, and CLI tokenization. The dependency-free structured
197+
targets at `tools/fuzz_untrusted_boundaries.py` and
198+
`tools/fuzz_correction_context.py` bound each input to 4,096 bytes;
199+
`tests/test_fuzz_targets.py` supplies deterministic boundary seeds and
200+
Hypothesis smoke campaigns.

‎docs/production-ready-v1.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Production-ready v1 completion plan
22

33
Status: active implementation roadmap. The mandatory new-session scan
4-
precondition and Gate 0 passed on 2026-08-24; Gate 1 is next.
4+
precondition and Gates 0--1 passed on 2026-08-24; Gate 2 is next.
55

66
This plan turns the current reference implementation into a narrowly scoped
77
real-funds release. It does not add a GUI, networking, RPC, secret storage,
@@ -192,6 +192,12 @@ Success criteria:
192192
- fixed correction remains differential-compatible with the frozen P70 corpus;
193193
- all four registered profiles are covered by public API tests.
194194

195+
Completion evidence (2026-08-24): 499 ordinary, optimized, and
196+
Hypothesis-statistics tests pass; the two 4,096-byte fuzz targets complete 250
197+
generated examples each; 28 frozen malformed cases and all 57 differential PR
198+
#70 cases pass; mypy and Ruff pass; and the installed package remains at 2,999
199+
physical Python lines.
200+
195201
Dependency: Gate 0.
196202

197203
## Gate 2 -- Generation and dependency assurance

‎docs/source-manifest.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ SHA-256 `6aa552b34c0bb2878d45dee2655c331d52e40e41e61cef523415d314ad9948e5`.
2525
`tools/differential_correction.py --verify` checks it offline. The repository
2626
does not claim that the upstream Haskell property suite was executed locally.
2727

28+
The independent rejection literals in `tests/data/malformed_inputs.json` have
29+
SHA-256 `966403685d979999524318d752537b5fe2ff01c0189a8e919e040dfd0de3978f`.
30+
They are fixed abuse cases, not outputs derived from production code. The two
31+
checked-in fuzz targets accept at most 4,096 bytes and add no runtime
32+
dependency.
33+
2834
Generalized-HRP PR #2040 and length-restriction PR #2077 are non-authoritative
2935
research context. The README is user documentation, never requirements evidence.
3036

‎docs/traceability.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Every implemented claim identifies one code owner and direct evidence.
1515
| R08 | derive only a fresh ordinary share | `derive_share` | every target and existing/S rejection | Implemented |
1616
| R09 | fresh shared S uses k uniform u5 masks | `generation._masks` and basis loop | mask invariants, recovery, no entropy injection | Implemented |
1717
| R10 | splitting S uses S plus k−1 masks | `split_secret` | exact recovery and threshold properties | Implemented for `ms` and `cl` |
18-
| R11 | four errors, `2e+v≤8`, eight erasures, bursts | `correction.py` | P70 corpus and Hypothesis positions | Implemented fixed-length only |
18+
| R11 | four errors, `2e+v≤8`, eight erasures, bursts | public `correct` over `correction.py` | P70 corpus, all-profile API tests, and Hypothesis positions | Implemented fixed-length only |
1919
| R12 | correction is an untrusted suggestion | CLI `correct` | stderr/nonzero and no-correction tests | Implemented |
2020
| R13 | subsequent share input uses known prefix/header | `_cli_input.read_artifacts`, BIP93 prefix validators | suffix/full paste, retry, duplicate/mismatch and stream tests | Implemented |
2121
| R14 | structural correction/timeout UX | none | [cuttable v1 gate](production-ready-v1.md) | Missing; Gate 3 candidate |
@@ -32,8 +32,9 @@ Every implemented claim identifies one code owner and direct evidence.
3232
| R25 | xprv, coordinator xpub, descriptors in reusable API | `wallet.py`; goal-oriented CLI tree | official xprv, frozen BIP48/descriptor and nested-command tests | Implemented |
3333
| R26 | explicit account/timestamp, mandatory Core mode, root-xprv warning | wallet API and CLI | deterministic records, public/private separation and warning tests | Implemented |
3434
| R27 | no arbitrary security parser for descriptors | fixed templates in `wallet.py` | module/API absence and template fixtures | Implemented by removal |
35-
| R28 | safe typed installable reference surface | 20-name `__all__`, project script | public abuse tests, mypy, wheel/CLI checks | Implemented |
35+
| R28 | safe typed installable reference surface | 25-name `__all__`, project script | public abuse tests, mypy, wheel/CLI checks | Implemented |
3636
| R29 | explicit share-index selectors are bounded before copying or normalizing elements | `generation._indices` | oversized string pre-normalization regression across all three public generation APIs | Implemented from standard security scan |
37+
| R30 | malformed untrusted boundaries fail closed under bounded work | parser, completion, interpolation, correction, and CLI adapters | frozen malformed corpus and 4,096-byte structured fuzz target | Implemented in Gate 1 |
3738

3839
The expanded checksum rule from PR #2258 is the only pending-upstream behavior.
3940
Its 44--46-byte compatibility exposure is explicitly accepted, has direct

‎src/codex32/__init__.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313
recover_secret,
1414
)
1515
from .correction import (
16+
CorrectionCandidate,
17+
CorrectionContext,
18+
CorrectionEdit,
1619
WorksheetCorrection,
20+
correct,
1721
correct_worksheet_residue,
1822
)
19-
from .errors import CodexError
23+
from .errors import CodexError, InvalidCorrectionInput
2024
from .generation import (
2125
generate_core_lightning_secret,
2226
generate_master_seed,
@@ -33,14 +37,19 @@
3337
"Bip39Secret",
3438
"CodexError",
3539
"CoreLightningSecret",
40+
"CorrectionCandidate",
41+
"CorrectionContext",
42+
"CorrectionEdit",
3643
"Header",
44+
"InvalidCorrectionInput",
3745
"MasterSeed",
3846
"Profile",
3947
"Secret",
4048
"Share",
4149
"WorksheetCorrection",
4250
"complete_checksum",
4351
"core_descriptors",
52+
"correct",
4453
"correct_worksheet_residue",
4554
"derive_share",
4655
"generate_core_lightning_secret",

‎src/codex32/cli.py‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
from codex32._cli_parser import parser as _parser
3434
from codex32.bip93 import IDX_SORT, _normalize_target
3535
from codex32.correction import (
36-
_correct_fixed,
37-
_FixedCorrectionSuccess,
36+
CorrectionContext,
37+
correct,
3838
correct_worksheet_residue,
3939
)
4040
from codex32.errors import CodexError, HeaderCollision, InvalidCorrectionInput
@@ -311,15 +311,11 @@ def _correct(
311311
raise _UsageError(
312312
"The string must begin with an undamaged ms1 or cl1 prefix; prefix correction is not attempted."
313313
)
314-
fixed = _correct_fixed(value, suspected_profile=profile)
315-
if not isinstance(fixed, _FixedCorrectionSuccess):
316-
messages = {
317-
"algebra": "No correction found. Check the original backup.",
318-
"body": "The proposed correction falls outside the supplied string.",
319-
"reparse": "No valid correction was found for this backup.",
320-
}
321-
raise _CommandError(messages.get(fixed.stage, fixed.detail))
322-
if not fixed.addends:
314+
candidates = correct(CorrectionContext(profile), value)
315+
if not candidates:
316+
raise _CommandError("No valid correction found. Check the original backup.")
317+
fixed = candidates[0]
318+
if not fixed.edits:
323319
_print("The codex32 string is already valid.")
324320
return 0
325321
warning = (

0 commit comments

Comments
 (0)