Skip to main content

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 &regular_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}