mk_codec/string_layer/bch_decode.rs
1//! Syndrome-based BCH decoder for the MK regular and long codes.
2//!
3//! Forked from `md-codec` v0.4.x (`crates/md-codec/src/encoding/bch_decode.rs`)
4//! at the start of the mk1 v0.1 implementation per `design/DECISIONS.md` D-13.
5//! The algorithm is shared with the sibling md1 format because both formats
6//! share BIP 93's BCH polynomials; only the target residue constants
7//! ([`crate::consts::MK_REGULAR_CONST`] / [`crate::consts::MK_LONG_CONST`])
8//! and the HRP differ. The fork copy is expected to be retired once the
9//! `mc-codex32` shared-crate extraction lands (closure Q-9 trigger: both
10//! formats v1.0 with cross-validated conformance vectors).
11//!
12//! Implements the textbook decoder pipeline:
13//!
14//! 1. **Syndrome computation**: compute eight syndromes
15//! `S_m = E(α^{j_start - 1 + m})` for `m = 1, …, 8` where `α` is the
16//! primitive element of the BCH-defining field and `j_start` is the
17//! smallest integer in the 8-consecutive-roots window of the
18//! generator polynomial. For the regular code `α = β = G·ζ` (order 93)
19//! with `j_start = 77`; for the long code `α = γ = E + X·ζ` (order
20//! 1023) with `j_start = 1019`.
21//! 2. **Berlekamp–Massey**: derive the error-locator polynomial `Λ(x)`
22//! from the eight syndromes. Runs in `O(t²)` for `t = 4`.
23//! 3. **Chien search**: enumerate `Λ` over every codeword position to
24//! locate error positions.
25//! 4. **Forney's algorithm** (shifted form): derive each error magnitude
26//! `e_k = X_k^{1 - j_start} · Ω(X_k^{-1}) / Λ'(X_k^{-1})` from the
27//! syndrome polynomial `S(x)`, the error-evaluator polynomial
28//! `Ω(x) ≡ S(x)·Λ(x) mod x⁸`, and the formal derivative `Λ'(x)`.
29//! The `X_k^{1 - j_start}` factor accounts for syndromes starting at
30//! `α^{j_start}` rather than `α^1`; cf. Lin & Costello §6.3 eq. (6.21)
31//! with the substitution `S_j → S_{j_start + j - 1}`.
32//! 5. **Apply corrections**: XOR the error magnitudes into the received
33//! word at the recovered positions.
34//! 6. **Verify** (caller's responsibility): defensive re-check via the
35//! polymod primitive guards against pathological inputs (≥ 5 errors
36//! that happen to produce a degree-≤ 4 `Λ` with 4 valid roots).
37//!
38//! # Field and root structure (BIP 93 §"Generation of valid checksum")
39//!
40//! `GF(32)` uses the codex32/BIP 93 primitive polynomial `x⁵ + x³ + 1`,
41//! with the multiplicative generator being the bit value `0b00010 = 2`
42//! (the bech32 `"z"` character). This matches the `bech32` crate's
43//! `Fe32` representation.
44//!
45//! `GF(1024) = GF(32)[ζ] / (ζ² - ζ - P)` where `P = 1` (so `ζ² = ζ + 1`).
46//! `ζ` is a primitive cube root of unity. For the **regular code**:
47//!
48//! ```text
49//! β = G·ζ (G = 8, so β = (0, 8) in our (lo, hi) form)
50//! ord(β) = 93
51//! roots of g_regular(x) are { β^17, β^20, β^46, β^49, β^52,
52//! β^77, β^78, β^79, β^80, β^81,
53//! β^82, β^83, β^84 }
54//! 8-consecutive window: { β^77, …, β^84 } ⇒ j_start = 77
55//! ```
56//!
57//! For the **long code**:
58//!
59//! ```text
60//! γ = E + X·ζ (E = 25, X = 6, so γ = (25, 6))
61//! ord(γ) = 1023
62//! roots of g_long(x) are { γ^32, γ^64, γ^96,
63//! γ^895, γ^927, γ^959, γ^991,
64//! γ^1019, γ^1020, γ^1021, γ^1022,
65//! γ^1023, γ^1024, γ^1025, γ^1026 }
66//! 8-consecutive window: { γ^1019, …, γ^1026 } ⇒ j_start = 1019
67//! ```
68//!
69//! Both windows are 8 consecutive integer powers of the chosen primitive
70//! element, satisfying the BCH bound and giving `t = 4` correction.
71//!
72//! # Position indexing
73//!
74//! The polymod consumes symbols in the order
75//! `hrp_expand(hrp) || data || checksum`. If `n` is the total number of
76//! symbols fed, then symbol `i` (in feed order) is the coefficient of
77//! `x^{n-1-i}` in the input polynomial. Errors are constrained to the
78//! `data_with_checksum` segment (the HRP prefix is fixed-and-known).
79//! For `data_with_checksum.len() = L` (`L ≤ 93` regular, `96 ≤ L ≤ 108`
80//! long), an error at index `k` of `data_with_checksum` lies at
81//! polynomial degree `d = L - 1 - k`. The Chien search returns degrees
82//! `d` and we translate to indices via `k = (L - 1) - d`.
83
84#[cfg(test)]
85use super::bch::{GEN_LONG, GEN_REGULAR};
86
87// ---------------------------------------------------------------------------
88// GF(32) — same field as `crate::string_layer::bch::ALPHABET` codes
89// ---------------------------------------------------------------------------
90
91/// One element of `GF(32) = GF(2)[α] / (α⁵ + α³ + 1)`, encoded as a
92/// 5-bit integer `0..32` whose binary digits are the polynomial
93/// coefficients (low bit = constant term).
94type Gf32 = u8;
95
96/// Primitive polynomial reduction mask for `GF(32)`: when a `GF(32)`
97/// multiplication overflows into bit 5, XOR with `0b00_1001 = 9` to fold
98/// `α⁵ ≡ α³ + 1` back into the residue.
99const GF32_REDUCE: u8 = 0b0_1001;
100
101/// Multiply two `GF(32)` elements (carryless multiply with reduction).
102const fn gf32_mul(a: Gf32, b: Gf32) -> Gf32 {
103 let mut result: u8 = 0;
104 let mut a = a;
105 let mut i = 0;
106 while i < 5 {
107 if (b >> i) & 1 != 0 {
108 result ^= a;
109 }
110 // Multiply a by α; reduce if it leaves the 5-bit window.
111 let carry = (a >> 4) & 1;
112 a = (a << 1) & 0x1F;
113 if carry != 0 {
114 a ^= GF32_REDUCE;
115 }
116 i += 1;
117 }
118 result
119}
120
121// ---------------------------------------------------------------------------
122// GF(1024) — built as GF(32²) via ζ² = ζ + 1
123// ---------------------------------------------------------------------------
124
125/// One element of `GF(1024)` as a pair `(lo, hi)` of `GF(32)` elements
126/// representing `lo + hi·ζ` where `ζ² = ζ + 1` (i.e., `ζ` is a
127/// primitive cube root of unity in `GF(1024)*`).
128#[derive(Copy, Clone, Debug, PartialEq, Eq)]
129struct Gf1024 {
130 lo: Gf32,
131 hi: Gf32,
132}
133
134impl Gf1024 {
135 const ZERO: Gf1024 = Gf1024 { lo: 0, hi: 0 };
136 const ONE: Gf1024 = Gf1024 { lo: 1, hi: 0 };
137
138 /// Embed a `GF(32)` element as the constant term.
139 const fn from_gf32(v: Gf32) -> Self {
140 Gf1024 { lo: v, hi: 0 }
141 }
142
143 fn add(self, other: Self) -> Self {
144 Gf1024 {
145 lo: self.lo ^ other.lo,
146 hi: self.hi ^ other.hi,
147 }
148 }
149
150 fn is_zero(self) -> bool {
151 self.lo == 0 && self.hi == 0
152 }
153
154 /// Multiply two `GF(1024)` elements using the field relation
155 /// `ζ² = ζ + 1`. Concretely:
156 ///
157 /// ```text
158 /// (lo + hi·ζ) · (lo' + hi'·ζ)
159 /// = lo·lo' + (lo·hi' + hi·lo')·ζ + hi·hi'·ζ²
160 /// = lo·lo' + (lo·hi' + hi·lo')·ζ + hi·hi'·(ζ + 1)
161 /// = (lo·lo' + hi·hi') + (lo·hi' + hi·lo' + hi·hi')·ζ
162 /// ```
163 fn mul(self, other: Self) -> Self {
164 let ll = gf32_mul(self.lo, other.lo);
165 let lh = gf32_mul(self.lo, other.hi);
166 let hl = gf32_mul(self.hi, other.lo);
167 let hh = gf32_mul(self.hi, other.hi);
168 Gf1024 {
169 lo: ll ^ hh,
170 hi: lh ^ hl ^ hh,
171 }
172 }
173
174 fn pow(self, mut exp: u32) -> Self {
175 let mut base = self;
176 let mut result = Gf1024::ONE;
177 while exp > 0 {
178 if exp & 1 == 1 {
179 result = result.mul(base);
180 }
181 base = base.mul(base);
182 exp >>= 1;
183 }
184 result
185 }
186
187 fn inv(self) -> Self {
188 // Fermat: a^(2^10 - 2) = a^1022 = a^-1 in GF(1024)*.
189 debug_assert!(!self.is_zero(), "inv of zero in GF(1024)");
190 self.pow(1022)
191 }
192}
193
194/// `ζ`, a primitive cube root of unity. With our `Y² = Y + 1` quadratic,
195/// `ζ` is `Y` itself, encoded as `(0, 1)` in `(lo, hi)` form. Used in
196/// tests to verify the field relation `ζ² = ζ + 1`; the runtime
197/// arithmetic uses [`BETA`] and [`GAMMA`] directly.
198#[cfg(test)]
199const ZETA: Gf1024 = Gf1024 { lo: 0, hi: 1 };
200
201/// `β = G·ζ = 8·ζ`, the primitive element for the **regular code**'s
202/// BCH-defining group. `β` has order 93. (BIP 93 §"Generation of valid
203/// checksum".)
204const BETA: Gf1024 = Gf1024 { lo: 0, hi: 8 };
205
206/// `γ = E + X·ζ = 25 + 6·ζ`, the primitive element for the **long
207/// code**'s BCH-defining group. `γ` has order 1023.
208const GAMMA: Gf1024 = Gf1024 { lo: 25, hi: 6 };
209
210/// Smallest exponent in the 8-consecutive-roots window of the regular
211/// code's generator polynomial: `g_regular(β^j) = 0` for `j = 77, …, 84`.
212const REGULAR_J_START: u32 = 77;
213
214/// Smallest exponent in the 8-consecutive-roots window of the long
215/// code's generator polynomial: `g_long(γ^j) = 0` for
216/// `j = 1019, 1020, …, 1026`.
217const LONG_J_START: u32 = 1019;
218
219// `β` and `γ` orders (93 and 1023, respectively) are tested directly
220// in the unit-test module via inline integer constants; we don't need
221// run-time symbols for them.
222
223// ---------------------------------------------------------------------------
224// Generator polynomial reconstruction (used only for self-test)
225// ---------------------------------------------------------------------------
226
227/// Reconstruct `g_regular(x)`, the degree-13 BCH generator polynomial,
228/// from `GEN_REGULAR[0]`. Returns coefficients with `result[i]` being
229/// the coefficient of `x^i`. The leading coefficient (`result[13]`) is 1.
230///
231/// **Why this works**: `polymod_step` computes
232/// `(residue · x + symbol) mod g(x)` in `GF(32)[x] / g(x)`. The constant
233/// `GEN_REGULAR[0]` is, by construction,
234/// `1 · x^13 mod g(x) = x^13 mod g(x)`, packed as 13 5-bit GF(32)
235/// coefficients (high coeff = `x^12`, low coeff = `x^0`). Since
236/// `g(x) = x^13 + (x^13 - g(x))`, and reduction in characteristic 2
237/// gives `g_low = x^13 mod g(x)`, we have
238/// `g(x) = x^13 + GEN_REGULAR[0]_packed_as_polynomial`.
239#[cfg(test)]
240fn generator_polynomial_regular() -> [Gf32; 14] {
241 let mut g = [0u8; 14];
242 g[13] = 1;
243 for (i, slot) in g.iter_mut().enumerate().take(13) {
244 *slot = ((GEN_REGULAR[0] >> (5 * i)) & 0x1F) as u8;
245 }
246 g
247}
248
249/// Reconstruct `g_long(x)`, the degree-15 BCH generator polynomial,
250/// from `GEN_LONG[0]`. Same packing convention as
251/// [`generator_polynomial_regular`].
252#[cfg(test)]
253fn generator_polynomial_long() -> [Gf32; 16] {
254 let mut g = [0u8; 16];
255 g[15] = 1;
256 for (i, slot) in g.iter_mut().enumerate().take(15) {
257 *slot = ((GEN_LONG[0] >> (5 * i)) & 0x1F) as u8;
258 }
259 g
260}
261
262/// Horner-form polynomial evaluation: GF(32)-coefficient polynomial at
263/// a GF(1024) point. `coeffs[i]` is the coefficient of `x^i`.
264fn horner(coeffs: &[Gf32], x: Gf1024) -> Gf1024 {
265 let mut acc = Gf1024::ZERO;
266 for &c in coeffs.iter().rev() {
267 acc = acc.mul(x).add(Gf1024::from_gf32(c));
268 }
269 acc
270}
271
272/// Horner-form polynomial evaluation: GF(1024)-coefficient polynomial
273/// at a GF(1024) point. `coeffs[i]` is the coefficient of `x^i`.
274fn horner_ext(coeffs: &[Gf1024], x: Gf1024) -> Gf1024 {
275 let mut acc = Gf1024::ZERO;
276 for &c in coeffs.iter().rev() {
277 acc = acc.mul(x).add(c);
278 }
279 acc
280}
281
282// ---------------------------------------------------------------------------
283// Syndromes
284// ---------------------------------------------------------------------------
285
286/// Compute the eight syndromes
287/// `S_m = E(α^{j_start + m - 1})` for `m = 1, …, 8`, where `E(x)` is the
288/// error polynomial (recoverable as the polymod residue minus the MD
289/// target constant). The remainder is already congruent to `E(x)`
290/// modulo `g(x)`, so evaluating it at the generator's roots is
291/// equivalent to evaluating `E(x)` itself.
292fn compute_syndromes(
293 residue_xor_const: u128,
294 checksum_len: u32,
295 alpha: Gf1024,
296 j_start: u32,
297) -> [Gf1024; 8] {
298 // Unpack the remainder: `checksum_len` GF(32) coefficients packed
299 // with the highest-order coefficient (x^{checksum_len-1}) at bit
300 // 5*(checksum_len-1) and the constant term (x^0) at bits 0..5.
301 // Stack-allocate at the maximum (Long code = 15); the active slice
302 // is `&coeffs[..checksum_len]`.
303 let mut coeffs = [0u8; 15];
304 let len = checksum_len as usize;
305 for i in 0..checksum_len {
306 coeffs[i as usize] = ((residue_xor_const >> (5 * i)) & 0x1F) as u8;
307 }
308 let coeffs = &coeffs[..len];
309
310 let mut syndromes = [Gf1024::ZERO; 8];
311 let alpha_j_start = alpha.pow(j_start);
312 let mut alpha_j = alpha_j_start;
313 for s in &mut syndromes {
314 *s = horner(coeffs, alpha_j);
315 alpha_j = alpha_j.mul(alpha);
316 }
317 syndromes
318}
319
320// ---------------------------------------------------------------------------
321// Berlekamp–Massey
322// ---------------------------------------------------------------------------
323
324/// Berlekamp–Massey for BCH over `GF(1024)`. Returns the error-locator
325/// polynomial `Λ(x)` with `Λ(0) = 1`. `Λ` has degree equal to the
326/// number of errors when the received word is correctable.
327fn berlekamp_massey(syndromes: &[Gf1024; 8]) -> Vec<Gf1024> {
328 // Standard formulation (Massey 1969 / Lin & Costello §6.3, adapted
329 // for 0-indexed syndromes where syndromes[k] = S_{j_start + k}).
330 let n = syndromes.len();
331 let mut lam: Vec<Gf1024> = vec![Gf1024::ONE]; // current connection poly
332 let mut prev: Vec<Gf1024> = vec![Gf1024::ONE]; // last-updated connection poly
333 let mut l: usize = 0; // current LFSR length
334 let mut m: usize = 1; // shift since last update
335 let mut b = Gf1024::ONE; // discrepancy from last update
336
337 for k in 0..n {
338 // Discrepancy: d = syndromes[k] + sum_{i=1..L} lam[i] * syndromes[k-i]
339 let mut d = syndromes[k];
340 for i in 1..=l {
341 // i > k means k - i would underflow; skip rather than wrap.
342 // i >= lam.len() means lam[i] doesn't exist yet; same skip.
343 if i <= k && i < lam.len() {
344 d = d.add(lam[i].mul(syndromes[k - i]));
345 }
346 }
347
348 if d.is_zero() {
349 m += 1;
350 } else if 2 * l <= k {
351 // Length increases. New lam = lam - (d/b) * x^m * prev.
352 let t = lam.clone();
353 let scale = d.mul(b.inv());
354 let new_len = (lam.len()).max(prev.len() + m);
355 lam.resize(new_len, Gf1024::ZERO);
356 for (i, &p) in prev.iter().enumerate() {
357 let idx = i + m;
358 lam[idx] = lam[idx].add(scale.mul(p));
359 }
360 l = k + 1 - l;
361 prev = t;
362 b = d;
363 m = 1;
364 } else {
365 // Length stays the same. lam = lam - (d/b) * x^m * prev.
366 let scale = d.mul(b.inv());
367 let new_len = (lam.len()).max(prev.len() + m);
368 lam.resize(new_len, Gf1024::ZERO);
369 for (i, &p) in prev.iter().enumerate() {
370 let idx = i + m;
371 lam[idx] = lam[idx].add(scale.mul(p));
372 }
373 m += 1;
374 }
375 }
376
377 while lam.len() > 1 && lam.last().is_some_and(|x| x.is_zero()) {
378 lam.pop();
379 }
380 lam
381}
382
383// ---------------------------------------------------------------------------
384// Chien search + Forney
385// ---------------------------------------------------------------------------
386
387/// Search for the roots of `Λ(x)` among `α⁰, α⁻¹, …, α⁻⁽ᴸ⁻¹⁾`, where
388/// `L = data_with_checksum_len` (we restrict the search to legitimate
389/// error positions; HRP-prefix positions are not transmitted).
390///
391/// Returns the list of polynomial degrees `d ∈ [0, L)` such that
392/// `Λ(α⁻ᵈ) = 0`. Each such `d` is the polynomial degree of an error.
393/// Returns `None` if the number of distinct roots found does not equal
394/// `deg(Λ)`.
395fn chien_search(
396 lambda: &[Gf1024],
397 data_with_checksum_len: usize,
398 alpha: Gf1024,
399) -> Option<Vec<usize>> {
400 let deg = lambda.len() - 1;
401 if deg == 0 {
402 return Some(Vec::new());
403 }
404
405 let mut error_degrees = Vec::with_capacity(deg);
406 let alpha_inv = alpha.inv();
407 let mut current = Gf1024::ONE; // α^0
408 for d in 0..data_with_checksum_len {
409 if horner_ext(lambda, current).is_zero() {
410 error_degrees.push(d);
411 }
412 current = current.mul(alpha_inv);
413 }
414
415 if error_degrees.len() != deg {
416 return None;
417 }
418 Some(error_degrees)
419}
420
421/// Shifted Forney's algorithm: given `Λ(x)`, the syndromes (at
422/// `α^{j_start}, …, α^{j_start + 7}`), and the error degrees `d_k` such
423/// that `α^{-d_k}` are the roots of `Λ`, compute the GF(32) error
424/// magnitudes at each position.
425///
426/// Formula (with `j_start` shift):
427///
428/// ```text
429/// e_k = X_k^{1 - j_start} · Ω(X_k^{-1}) / Λ'(X_k^{-1})
430/// ```
431///
432/// where `X_k = α^{d_k}`, `Ω(x) ≡ S(x)·Λ(x) mod x^8`, and `Λ'(x)` is
433/// the formal derivative.
434///
435/// Returns `None` if any computed magnitude does not lie in the symbol
436/// field `GF(32)`.
437fn forney(
438 syndromes: &[Gf1024; 8],
439 lambda: &[Gf1024],
440 error_degrees: &[usize],
441 alpha: Gf1024,
442 j_start: u32,
443) -> Option<Vec<Gf32>> {
444 // Ω(x) = S(x) * Λ(x) mod x^8, where S(x) = sum_{m=0..7} S_{j_start + m} * x^m.
445 let s_poly: Vec<Gf1024> = syndromes.to_vec();
446 let mut omega = vec![Gf1024::ZERO; 8];
447 for i in 0..s_poly.len().min(8) {
448 for j in 0..lambda.len() {
449 if i + j < 8 {
450 omega[i + j] = omega[i + j].add(s_poly[i].mul(lambda[j]));
451 }
452 }
453 }
454
455 // Λ'(x) = formal derivative. In characteristic 2 only odd-power
456 // terms survive: Λ'(x) = sum_{i odd} lambda[i] * x^{i-1}.
457 let mut lambda_prime = vec![Gf1024::ZERO; lambda.len().saturating_sub(1)];
458 for i in 1..lambda.len() {
459 if i % 2 == 1 {
460 lambda_prime[i - 1] = lambda[i];
461 }
462 }
463
464 let mut magnitudes = Vec::with_capacity(error_degrees.len());
465 for &d in error_degrees {
466 // X_k = α^d.
467 let x_k = alpha.pow(d as u32);
468 let x_k_inv = x_k.inv();
469 let omega_val = horner_ext(&omega, x_k_inv);
470 let lam_p_val = horner_ext(&lambda_prime, x_k_inv);
471 if lam_p_val.is_zero() {
472 return None;
473 }
474
475 // Compute X_k^{1 - j_start}. Note `1 - j_start` is negative;
476 // since X_k has order ord(α) (93 or 1023), we use
477 // X_k^{1 - j_start} = X_k^{(ord - j_start + 1) mod ord}.
478 // But we handle this generically via x_k_inv^{j_start - 1}.
479 let shift = j_start.saturating_sub(1);
480 let x_k_shift = x_k_inv.pow(shift); // = X_k^{-(j_start - 1)} = X_k^{1 - j_start}
481
482 let mag = x_k_shift.mul(omega_val.mul(lam_p_val.inv()));
483
484 // Magnitude must lie in GF(32) (the high coefficient must be zero).
485 if mag.hi != 0 {
486 return None;
487 }
488 if mag.lo == 0 {
489 // Zero magnitude is not a real error — typically signals
490 // more than 4 actual errors that fooled BM.
491 return None;
492 }
493 magnitudes.push(mag.lo);
494 }
495 Some(magnitudes)
496}
497
498// ---------------------------------------------------------------------------
499// Public entry points
500// ---------------------------------------------------------------------------
501
502/// Decode a regular-code BCH error pattern. Inputs:
503///
504/// - `residue_xor_const`: the value
505/// `polymod(hrp_expand(hrp) || data_with_checksum) ⊕ MK_REGULAR_CONST`.
506/// By the BCH syndrome property, this is congruent to the error
507/// polynomial `E(x)` modulo `g_regular(x)`.
508/// - `data_with_checksum_len`: the total symbol count of
509/// `data_with_checksum` (in the `0..=93` range for the regular code).
510///
511/// Returns `Some((positions, magnitudes))` if the algorithm finds a
512/// consistent error pattern of weight `≤ 4`. Each `positions[k]` is an
513/// index into `data_with_checksum` (post-HRP-prefix); each
514/// `magnitudes[k]` is a `GF(32)` symbol that must be XORed into
515/// `data_with_checksum[positions[k]]` to repair the codeword. Returns
516/// `None` if the pattern is uncorrectable.
517// v0.3.1: promoted from `pub(super)` so downstream consumers (toolkit
518// `repair` feature) can compute corrections for ms / md HRPs using their
519// own target-residue constants (all 3 share the BIP-93 BCH(93,80,8) code).
520pub fn decode_regular_errors(
521 residue_xor_const: u128,
522 data_with_checksum_len: usize,
523) -> Option<(Vec<usize>, Vec<Gf32>)> {
524 decode_errors(
525 residue_xor_const,
526 data_with_checksum_len,
527 13,
528 BETA,
529 REGULAR_J_START,
530 )
531}
532
533/// Long-code analog of [`decode_regular_errors`].
534///
535/// v0.3.1: promoted from `pub(super)` for downstream-consumer access (see
536/// `decode_regular_errors` docs).
537pub fn decode_long_errors(
538 residue_xor_const: u128,
539 data_with_checksum_len: usize,
540) -> Option<(Vec<usize>, Vec<Gf32>)> {
541 decode_errors(
542 residue_xor_const,
543 data_with_checksum_len,
544 15,
545 GAMMA,
546 LONG_J_START,
547 )
548}
549
550fn decode_errors(
551 residue_xor_const: u128,
552 data_with_checksum_len: usize,
553 checksum_len: u32,
554 alpha: Gf1024,
555 j_start: u32,
556) -> Option<(Vec<usize>, Vec<Gf32>)> {
557 let syndromes = compute_syndromes(residue_xor_const, checksum_len, alpha, j_start);
558
559 // All-zero syndromes ⇒ no errors (caller usually detects earlier).
560 if syndromes.iter().all(|s| s.is_zero()) {
561 return Some((Vec::new(), Vec::new()));
562 }
563
564 let lambda = berlekamp_massey(&syndromes);
565 let deg = lambda.len() - 1;
566 if deg == 0 || deg > 4 {
567 // > 4 errors is above the BCH(•, •, 8) / t = 4 capacity.
568 return None;
569 }
570
571 let error_degrees = chien_search(&lambda, data_with_checksum_len, alpha)?;
572 if error_degrees.len() != deg {
573 return None;
574 }
575
576 let magnitudes = forney(&syndromes, &lambda, &error_degrees, alpha, j_start)?;
577
578 // Translate polynomial degrees back to data_with_checksum indices.
579 // For data_with_checksum[k] (k = 0..L-1), polynomial degree d = L - 1 - k.
580 // So k = L - 1 - d.
581 let mut positions = Vec::with_capacity(error_degrees.len());
582 for &d in &error_degrees {
583 if d >= data_with_checksum_len {
584 // Should not happen since chien_search bounds d to [0, L).
585 return None;
586 }
587 let k = data_with_checksum_len - 1 - d;
588 positions.push(k);
589 }
590
591 // Sort ascending by position for deterministic output. Magnitudes
592 // need to be reordered along with the positions.
593 let mut paired: Vec<(usize, Gf32)> = positions.into_iter().zip(magnitudes).collect();
594 paired.sort_by_key(|p| p.0);
595 let positions: Vec<usize> = paired.iter().map(|p| p.0).collect();
596 let magnitudes: Vec<Gf32> = paired.iter().map(|p| p.1).collect();
597
598 Some((positions, magnitudes))
599}
600
601// ---------------------------------------------------------------------------
602// Unit tests
603// ---------------------------------------------------------------------------
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use crate::consts::{MK_LONG_CONST, MK_REGULAR_CONST};
609 use crate::string_layer::bch::{
610 GEN_LONG, GEN_REGULAR, LONG_MASK, LONG_SHIFT, REGULAR_MASK, REGULAR_SHIFT,
611 bch_create_checksum_long, bch_create_checksum_regular, hrp_expand,
612 };
613
614 #[test]
615 fn gf32_mul_identity() {
616 for v in 0..32u8 {
617 assert_eq!(gf32_mul(v, 1), v);
618 assert_eq!(gf32_mul(1, v), v);
619 }
620 }
621
622 #[test]
623 fn gf32_mul_zero() {
624 for v in 0..32u8 {
625 assert_eq!(gf32_mul(v, 0), 0);
626 assert_eq!(gf32_mul(0, v), 0);
627 }
628 }
629
630 #[test]
631 fn gf32_alpha_powers_match_bech32_log_inv_table() {
632 // Cross-check: alpha = 2 (= "z"). Powers of alpha must match the
633 // LOG_INV table from the bech32 crate.
634 let mut a: u8 = 1;
635 let expected: [u8; 31] = [
636 1, 2, 4, 8, 16, 9, 18, 13, 26, 29, 19, 15, 30, 21, 3, 6, 12, 24, 25, 27, 31, 23, 7, 14,
637 28, 17, 11, 22, 5, 10, 20,
638 ];
639 for &exp in &expected {
640 assert_eq!(a, exp);
641 a = gf32_mul(a, 2);
642 }
643 // After 31 multiplications by alpha, we should be back to 1.
644 assert_eq!(a, 1);
645 }
646
647 #[test]
648 fn zeta_is_primitive_cube_root_of_unity() {
649 // ζ² = ζ + 1, ζ³ = ζ·(ζ + 1) = ζ² + ζ = 2ζ + 1 = 1 (in char 2).
650 let zeta_sq = ZETA.mul(ZETA);
651 assert_eq!(zeta_sq, ZETA.add(Gf1024::ONE), "ζ² should equal ζ + 1");
652 let zeta_cu = zeta_sq.mul(ZETA);
653 assert_eq!(zeta_cu, Gf1024::ONE, "ζ³ should equal 1");
654 }
655
656 #[test]
657 fn beta_has_order_93_regular() {
658 // β = G·ζ has order 93 (BIP 93 §"Generation of valid checksum").
659 let mut p = Gf1024::ONE;
660 for j in 1..=93 {
661 p = p.mul(BETA);
662 if p == Gf1024::ONE {
663 assert_eq!(j, 93, "β prematurely returned to 1 at exponent {}", j);
664 }
665 }
666 assert_eq!(p, Gf1024::ONE, "β^93 should equal 1");
667 }
668
669 #[test]
670 fn gamma_has_order_1023_long() {
671 // γ = E + X·ζ has order 1023 (BIP 93 §"Generation of valid checksum").
672 // Quick-check at the 3 prime divisors of 1023 = 3·11·31.
673 for &q in &[341u32, 93u32, 33u32] {
674 // 1023/3, 1023/11, 1023/31
675 assert_ne!(GAMMA.pow(q), Gf1024::ONE, "γ^(1023/p) = 1 for some p");
676 }
677 assert_eq!(GAMMA.pow(1023), Gf1024::ONE, "γ^1023 should equal 1");
678 }
679
680 #[test]
681 fn generator_polynomial_evaluates_to_zero_at_specified_roots() {
682 // Cross-check the BIP 93 §"Generation of valid checksum" claim
683 // that g_regular(β^i) = 0 for i ∈ {17, 20, 46, 49, 52, 77..84}
684 // and g_long(γ^i) = 0 for i ∈ {32, 64, 96, 895, 927, 959, 991,
685 // 1019..1026}. Reconstructs g(x) from GEN_*[0] and verifies.
686 let g_reg = generator_polynomial_regular();
687 let g_long = generator_polynomial_long();
688
689 let regular_roots: [u32; 13] = [17, 20, 46, 49, 52, 77, 78, 79, 80, 81, 82, 83, 84];
690 for &i in ®ular_roots {
691 assert!(
692 horner(&g_reg, BETA.pow(i)).is_zero(),
693 "g_regular(β^{}) != 0",
694 i
695 );
696 }
697
698 let long_roots: [u32; 15] = [
699 32, 64, 96, 895, 927, 959, 991, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026,
700 ];
701 for &i in &long_roots {
702 assert!(
703 horner(&g_long, GAMMA.pow(i)).is_zero(),
704 "g_long(γ^{}) != 0",
705 i
706 );
707 }
708 }
709
710 // Re-export the production polymod_run so tests validate field arithmetic
711 // against the same code path the codec actually runs. A local duplicate
712 // (which used to live here) would let polymod_run bugs go undetected if
713 // both copies agreed on the wrong answer.
714 use crate::string_layer::bch::polymod_run;
715
716 #[test]
717 fn one_error_decodes_correctly_regular() {
718 let hrp = "mk";
719 let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
720 let checksum = bch_create_checksum_regular(hrp, &data);
721 let mut codeword = data.clone();
722 codeword.extend_from_slice(&checksum);
723 let original = codeword.clone();
724
725 let err_pos = 5;
726 let err_mag: u8 = 0b10101;
727 codeword[err_pos] ^= err_mag;
728
729 let mut input = hrp_expand(hrp);
730 input.extend_from_slice(&codeword);
731 let polymod = polymod_run(&input, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK);
732 let residue = polymod ^ MK_REGULAR_CONST;
733
734 let (positions, magnitudes) =
735 decode_regular_errors(residue, codeword.len()).expect("1-error must decode");
736 assert_eq!(positions, vec![err_pos]);
737 assert_eq!(magnitudes, vec![err_mag]);
738
739 let mut corrected = codeword.clone();
740 for (p, m) in positions.iter().zip(&magnitudes) {
741 corrected[*p] ^= m;
742 }
743 assert_eq!(corrected, original);
744 }
745
746 #[test]
747 fn two_errors_decode_correctly_regular() {
748 let hrp = "mk";
749 let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
750 let checksum = bch_create_checksum_regular(hrp, &data);
751 let mut codeword = data.clone();
752 codeword.extend_from_slice(&checksum);
753 let original = codeword.clone();
754
755 let positions_in: [usize; 2] = [3, 17];
756 let mags_in: [u8; 2] = [0b11001, 0b00111];
757 for (&p, &m) in positions_in.iter().zip(&mags_in) {
758 codeword[p] ^= m;
759 }
760
761 let mut input = hrp_expand(hrp);
762 input.extend_from_slice(&codeword);
763 let polymod = polymod_run(&input, &GEN_REGULAR, REGULAR_SHIFT, REGULAR_MASK);
764 let residue = polymod ^ MK_REGULAR_CONST;
765
766 let (positions, magnitudes) =
767 decode_regular_errors(residue, codeword.len()).expect("2-error must decode");
768 assert_eq!(positions, vec![3, 17]);
769 assert_eq!(magnitudes, vec![mags_in[0], mags_in[1]]);
770
771 let mut corrected = codeword.clone();
772 for (p, m) in positions.iter().zip(&magnitudes) {
773 corrected[*p] ^= m;
774 }
775 assert_eq!(corrected, original);
776 }
777
778 #[test]
779 fn four_errors_decode_correctly_long() {
780 let hrp = "mk";
781 let data: Vec<u8> = (0..16).collect();
782 let checksum = bch_create_checksum_long(hrp, &data);
783 let mut codeword = data.clone();
784 codeword.extend_from_slice(&checksum);
785 let original = codeword.clone();
786
787 let positions_in: [usize; 4] = [0, 5, 18, 28];
788 let mags_in: [u8; 4] = [0b00001, 0b10000, 0b11111, 0b01010];
789 for (&p, &m) in positions_in.iter().zip(&mags_in) {
790 codeword[p] ^= m;
791 }
792
793 let mut input = hrp_expand(hrp);
794 input.extend_from_slice(&codeword);
795 let polymod = polymod_run(&input, &GEN_LONG, LONG_SHIFT, LONG_MASK);
796 let residue = polymod ^ MK_LONG_CONST;
797
798 let (positions, magnitudes) =
799 decode_long_errors(residue, codeword.len()).expect("4-error must decode");
800 assert_eq!(positions, vec![0, 5, 18, 28]);
801 assert_eq!(magnitudes, mags_in.to_vec());
802
803 let mut corrected = codeword.clone();
804 for (p, m) in positions.iter().zip(&magnitudes) {
805 corrected[*p] ^= m;
806 }
807 assert_eq!(corrected, original);
808 }
809
810 #[test]
811 fn five_errors_either_rejects_or_returns_bogus_recovery() {
812 // The decoder doesn't detect 5+ errors directly. It may return
813 // None or return Some() with bogus positions/magnitudes that
814 // fail to reproduce the original. The caller's responsibility
815 // is to re-verify via `bch_verify_*`.
816 let hrp = "mk";
817 let data: Vec<u8> = (0..16).collect();
818 let checksum = bch_create_checksum_long(hrp, &data);
819 let mut codeword = data.clone();
820 codeword.extend_from_slice(&checksum);
821
822 let positions_in: [usize; 5] = [0, 5, 10, 15, 20];
823 let mags_in: [u8; 5] = [1, 2, 3, 4, 5];
824 for (&p, &m) in positions_in.iter().zip(&mags_in) {
825 codeword[p] ^= m;
826 }
827
828 let mut input = hrp_expand(hrp);
829 input.extend_from_slice(&codeword);
830 let polymod = polymod_run(&input, &GEN_LONG, LONG_SHIFT, LONG_MASK);
831 let residue = polymod ^ MK_LONG_CONST;
832
833 if let Some((positions, magnitudes)) = decode_long_errors(residue, codeword.len()) {
834 let original = {
835 let mut o = data.clone();
836 o.extend_from_slice(&checksum);
837 o
838 };
839 let mut corrected = codeword.clone();
840 for (p, m) in positions.iter().zip(&magnitudes) {
841 corrected[*p] ^= m;
842 }
843 assert_ne!(
844 corrected, original,
845 "5-error decode should not produce the original codeword"
846 );
847 }
848 }
849}