md_codec/bch_decode.rs
1//! Syndrome-based BCH decoder for the MD regular code.
2//!
3//! Forked from `mk-codec` v0.3.1 (`crates/mk-codec/src/string_layer/bch_decode.rs`)
4//! at v0.34.0 per plan §1 D22 + §2.B.1. The algorithm is constant-agnostic —
5//! the caller XORs the polymod residue against the per-HRP target constant
6//! ([`crate::bch::MD_REGULAR_CONST`]) before invoking [`decode_regular_errors`].
7//! The fork copy is expected to be retired once the `mc-codex32` shared-crate
8//! extraction lands (closure Q-9 trigger: both formats v1.0 with cross-
9//! validated conformance vectors).
10//!
11//! Drops the mk1-specific long-code path: md1 only defines the regular
12//! `BCH(93,80,8)` variant. The internal `decode_errors` helper is therefore
13//! also dropped — there is only one public entry point.
14//!
15//! ## Position indexing
16//!
17//! The polymod consumes symbols in the order
18//! `hrp_expand(hrp) || data || checksum`. If `n` is the total number of
19//! symbols fed, then symbol `i` (in feed order) is the coefficient of
20//! `x^{n-1-i}` in the input polynomial. Errors are constrained to the
21//! `data_with_checksum` segment (the HRP prefix is fixed-and-known).
22//! For `data_with_checksum.len() = L` (`L ≤ 93` regular), an error at
23//! index `k` of `data_with_checksum` lies at polynomial degree
24//! `d = L - 1 - k`. The Chien search returns degrees `d` and we translate
25//! to indices via `k = (L - 1) - d`.
26//!
27//! ## Local constants (Q3 lock, plan §2.B.1)
28//!
29//! `POLYMOD_INIT` / `REGULAR_SHIFT` / `REGULAR_MASK` from `bch.rs:19-21`
30//! are re-declared locally here rather than importing from `bch.rs`. Per
31//! the Q3 lock decision the bare-private internals of `bch.rs` stay
32//! bare-private; this module re-declares the small set it needs to keep
33//! the public API surface minimal. (These three values are not currently
34//! used by `bch_decode` itself — the polymod is run by the caller — but
35//! are kept here for parity with the mk-codec source and to support any
36//! future internal verification needs.)
37
38// ---------------------------------------------------------------------------
39// GF(32) — same field as `crate::bch::GEN_REGULAR` symbols.
40// ---------------------------------------------------------------------------
41
42/// One element of `GF(32) = GF(2)[α] / (α⁵ + α³ + 1)`, encoded as a
43/// 5-bit integer `0..32` whose binary digits are the polynomial
44/// coefficients (low bit = constant term).
45type Gf32 = u8;
46
47/// Primitive polynomial reduction mask for `GF(32)`: when a `GF(32)`
48/// multiplication overflows into bit 5, XOR with `0b00_1001 = 9` to fold
49/// `α⁵ ≡ α³ + 1` back into the residue.
50const GF32_REDUCE: u8 = 0b0_1001;
51
52/// Multiply two `GF(32)` elements (carryless multiply with reduction).
53const fn gf32_mul(a: Gf32, b: Gf32) -> Gf32 {
54 let mut result: u8 = 0;
55 let mut a = a;
56 let mut i = 0;
57 while i < 5 {
58 if (b >> i) & 1 != 0 {
59 result ^= a;
60 }
61 // Multiply a by α; reduce if it leaves the 5-bit window.
62 let carry = (a >> 4) & 1;
63 a = (a << 1) & 0x1F;
64 if carry != 0 {
65 a ^= GF32_REDUCE;
66 }
67 i += 1;
68 }
69 result
70}
71
72// ---------------------------------------------------------------------------
73// GF(1024) — built as GF(32²) via ζ² = ζ + 1
74// ---------------------------------------------------------------------------
75
76/// One element of `GF(1024)` as a pair `(lo, hi)` of `GF(32)` elements
77/// representing `lo + hi·ζ` where `ζ² = ζ + 1` (i.e., `ζ` is a
78/// primitive cube root of unity in `GF(1024)*`).
79#[derive(Copy, Clone, Debug, PartialEq, Eq)]
80struct Gf1024 {
81 lo: Gf32,
82 hi: Gf32,
83}
84
85impl Gf1024 {
86 const ZERO: Gf1024 = Gf1024 { lo: 0, hi: 0 };
87 const ONE: Gf1024 = Gf1024 { lo: 1, hi: 0 };
88
89 /// Embed a `GF(32)` element as the constant term.
90 const fn from_gf32(v: Gf32) -> Self {
91 Gf1024 { lo: v, hi: 0 }
92 }
93
94 fn add(self, other: Self) -> Self {
95 Gf1024 {
96 lo: self.lo ^ other.lo,
97 hi: self.hi ^ other.hi,
98 }
99 }
100
101 fn is_zero(self) -> bool {
102 self.lo == 0 && self.hi == 0
103 }
104
105 /// Multiply two `GF(1024)` elements using the field relation
106 /// `ζ² = ζ + 1`. Concretely:
107 ///
108 /// ```text
109 /// (lo + hi·ζ) · (lo' + hi'·ζ)
110 /// = lo·lo' + (lo·hi' + hi·lo')·ζ + hi·hi'·ζ²
111 /// = lo·lo' + (lo·hi' + hi·lo')·ζ + hi·hi'·(ζ + 1)
112 /// = (lo·lo' + hi·hi') + (lo·hi' + hi·lo' + hi·hi')·ζ
113 /// ```
114 fn mul(self, other: Self) -> Self {
115 let ll = gf32_mul(self.lo, other.lo);
116 let lh = gf32_mul(self.lo, other.hi);
117 let hl = gf32_mul(self.hi, other.lo);
118 let hh = gf32_mul(self.hi, other.hi);
119 Gf1024 {
120 lo: ll ^ hh,
121 hi: lh ^ hl ^ hh,
122 }
123 }
124
125 fn pow(self, mut exp: u32) -> Self {
126 let mut base = self;
127 let mut result = Gf1024::ONE;
128 while exp > 0 {
129 if exp & 1 == 1 {
130 result = result.mul(base);
131 }
132 base = base.mul(base);
133 exp >>= 1;
134 }
135 result
136 }
137
138 fn inv(self) -> Self {
139 // Fermat: a^(2^10 - 2) = a^1022 = a^-1 in GF(1024)*.
140 debug_assert!(!self.is_zero(), "inv of zero in GF(1024)");
141 self.pow(1022)
142 }
143}
144
145/// `β = G·ζ = 8·ζ`, the primitive element for the **regular code**'s
146/// BCH-defining group. `β` has order 93. (BIP 93 §"Generation of valid
147/// checksum".)
148const BETA: Gf1024 = Gf1024 { lo: 0, hi: 8 };
149
150/// Smallest exponent in the 8-consecutive-roots window of the regular
151/// code's generator polynomial: `g_regular(β^j) = 0` for `j = 77, …, 84`.
152const REGULAR_J_START: u32 = 77;
153
154/// Regular-code BCH checksum length (in 5-bit symbols).
155const REGULAR_CHECKSUM_SYMBOLS: u32 = 13;
156
157// ---------------------------------------------------------------------------
158// Horner-form polynomial evaluation
159// ---------------------------------------------------------------------------
160
161/// Horner-form polynomial evaluation: GF(32)-coefficient polynomial at
162/// a GF(1024) point. `coeffs[i]` is the coefficient of `x^i`.
163fn horner(coeffs: &[Gf32], x: Gf1024) -> Gf1024 {
164 let mut acc = Gf1024::ZERO;
165 for &c in coeffs.iter().rev() {
166 acc = acc.mul(x).add(Gf1024::from_gf32(c));
167 }
168 acc
169}
170
171/// Horner-form polynomial evaluation: GF(1024)-coefficient polynomial
172/// at a GF(1024) point. `coeffs[i]` is the coefficient of `x^i`.
173fn horner_ext(coeffs: &[Gf1024], x: Gf1024) -> Gf1024 {
174 let mut acc = Gf1024::ZERO;
175 for &c in coeffs.iter().rev() {
176 acc = acc.mul(x).add(c);
177 }
178 acc
179}
180
181// ---------------------------------------------------------------------------
182// Syndromes
183// ---------------------------------------------------------------------------
184
185/// Compute the eight syndromes `S_m = E(β^{j_start + m - 1})` for
186/// `m = 1, …, 8`, where `E(x)` is the error polynomial (recoverable as
187/// the polymod residue minus the MD target constant). The remainder is
188/// already congruent to `E(x)` modulo `g_regular(x)`, so evaluating it at
189/// the generator's roots is equivalent to evaluating `E(x)` itself.
190fn compute_syndromes_regular(residue_xor_const: u128) -> [Gf1024; 8] {
191 // Unpack the remainder: 13 GF(32) coefficients packed with the
192 // highest-order coefficient (x^12) at bit 60 and the constant term
193 // (x^0) at bits 0..5.
194 let mut coeffs = [0u8; REGULAR_CHECKSUM_SYMBOLS as usize];
195 for (i, slot) in coeffs.iter_mut().enumerate() {
196 *slot = ((residue_xor_const >> (5 * i)) & 0x1F) as u8;
197 }
198
199 let mut syndromes = [Gf1024::ZERO; 8];
200 let alpha_j_start = BETA.pow(REGULAR_J_START);
201 let mut alpha_j = alpha_j_start;
202 for s in &mut syndromes {
203 *s = horner(&coeffs, alpha_j);
204 alpha_j = alpha_j.mul(BETA);
205 }
206 syndromes
207}
208
209// ---------------------------------------------------------------------------
210// Berlekamp–Massey
211// ---------------------------------------------------------------------------
212
213/// Berlekamp–Massey for BCH over `GF(1024)`. Returns the error-locator
214/// polynomial `Λ(x)` with `Λ(0) = 1`. `Λ` has degree equal to the
215/// number of errors when the received word is correctable.
216fn berlekamp_massey(syndromes: &[Gf1024; 8]) -> Vec<Gf1024> {
217 // Standard formulation (Massey 1969 / Lin & Costello §6.3, adapted
218 // for 0-indexed syndromes where syndromes[k] = S_{j_start + k}).
219 let n = syndromes.len();
220 let mut lam: Vec<Gf1024> = vec![Gf1024::ONE]; // current connection poly
221 let mut prev: Vec<Gf1024> = vec![Gf1024::ONE]; // last-updated connection poly
222 let mut l: usize = 0; // current LFSR length
223 let mut m: usize = 1; // shift since last update
224 let mut b = Gf1024::ONE; // discrepancy from last update
225
226 for k in 0..n {
227 // Discrepancy: d = syndromes[k] + sum_{i=1..L} lam[i] * syndromes[k-i]
228 let mut d = syndromes[k];
229 for i in 1..=l {
230 // i > k means k - i would underflow; skip rather than wrap.
231 // i >= lam.len() means lam[i] doesn't exist yet; same skip.
232 if i <= k && i < lam.len() {
233 d = d.add(lam[i].mul(syndromes[k - i]));
234 }
235 }
236
237 if d.is_zero() {
238 m += 1;
239 } else if 2 * l <= k {
240 // Length increases. New lam = lam - (d/b) * x^m * prev.
241 let t = lam.clone();
242 let scale = d.mul(b.inv());
243 let new_len = (lam.len()).max(prev.len() + m);
244 lam.resize(new_len, Gf1024::ZERO);
245 for (i, &p) in prev.iter().enumerate() {
246 let idx = i + m;
247 lam[idx] = lam[idx].add(scale.mul(p));
248 }
249 l = k + 1 - l;
250 prev = t;
251 b = d;
252 m = 1;
253 } else {
254 // Length stays the same. lam = lam - (d/b) * x^m * prev.
255 let scale = d.mul(b.inv());
256 let new_len = (lam.len()).max(prev.len() + m);
257 lam.resize(new_len, Gf1024::ZERO);
258 for (i, &p) in prev.iter().enumerate() {
259 let idx = i + m;
260 lam[idx] = lam[idx].add(scale.mul(p));
261 }
262 m += 1;
263 }
264 }
265
266 while lam.len() > 1 && lam.last().is_some_and(|x| x.is_zero()) {
267 lam.pop();
268 }
269 lam
270}
271
272// ---------------------------------------------------------------------------
273// Chien search + Forney
274// ---------------------------------------------------------------------------
275
276/// Search for the roots of `Λ(x)` among `β⁰, β⁻¹, …, β⁻⁽ᴸ⁻¹⁾`, where
277/// `L = data_with_checksum_len` (we restrict the search to legitimate
278/// error positions; HRP-prefix positions are not transmitted).
279///
280/// Returns the list of polynomial degrees `d ∈ [0, L)` such that
281/// `Λ(β⁻ᵈ) = 0`. Each such `d` is the polynomial degree of an error.
282/// Returns `None` if the number of distinct roots found does not equal
283/// `deg(Λ)`.
284fn chien_search(lambda: &[Gf1024], data_with_checksum_len: usize) -> Option<Vec<usize>> {
285 let deg = lambda.len() - 1;
286 if deg == 0 {
287 return Some(Vec::new());
288 }
289
290 let mut error_degrees = Vec::with_capacity(deg);
291 let beta_inv = BETA.inv();
292 let mut current = Gf1024::ONE; // β^0
293 for d in 0..data_with_checksum_len {
294 if horner_ext(lambda, current).is_zero() {
295 error_degrees.push(d);
296 }
297 current = current.mul(beta_inv);
298 }
299
300 if error_degrees.len() != deg {
301 return None;
302 }
303 Some(error_degrees)
304}
305
306/// Shifted Forney's algorithm: given `Λ(x)`, the syndromes (at
307/// `β^{j_start}, …, β^{j_start + 7}`), and the error degrees `d_k` such
308/// that `β^{-d_k}` are the roots of `Λ`, compute the GF(32) error
309/// magnitudes at each position.
310///
311/// Formula (with `j_start` shift):
312///
313/// ```text
314/// e_k = X_k^{1 - j_start} · Ω(X_k^{-1}) / Λ'(X_k^{-1})
315/// ```
316///
317/// where `X_k = β^{d_k}`, `Ω(x) ≡ S(x)·Λ(x) mod x^8`, and `Λ'(x)` is
318/// the formal derivative.
319///
320/// Returns `None` if any computed magnitude does not lie in the symbol
321/// field `GF(32)`.
322fn forney(
323 syndromes: &[Gf1024; 8],
324 lambda: &[Gf1024],
325 error_degrees: &[usize],
326) -> Option<Vec<Gf32>> {
327 // Ω(x) = S(x) * Λ(x) mod x^8, where S(x) = sum_{m=0..7} S_{j_start + m} * x^m.
328 let s_poly: Vec<Gf1024> = syndromes.to_vec();
329 let mut omega = vec![Gf1024::ZERO; 8];
330 for i in 0..s_poly.len().min(8) {
331 for j in 0..lambda.len() {
332 if i + j < 8 {
333 omega[i + j] = omega[i + j].add(s_poly[i].mul(lambda[j]));
334 }
335 }
336 }
337
338 // Λ'(x) = formal derivative. In characteristic 2 only odd-power
339 // terms survive: Λ'(x) = sum_{i odd} lambda[i] * x^{i-1}.
340 let mut lambda_prime = vec![Gf1024::ZERO; lambda.len().saturating_sub(1)];
341 for i in 1..lambda.len() {
342 if i % 2 == 1 {
343 lambda_prime[i - 1] = lambda[i];
344 }
345 }
346
347 let mut magnitudes = Vec::with_capacity(error_degrees.len());
348 for &d in error_degrees {
349 // X_k = β^d.
350 let x_k = BETA.pow(d as u32);
351 let x_k_inv = x_k.inv();
352 let omega_val = horner_ext(&omega, x_k_inv);
353 let lam_p_val = horner_ext(&lambda_prime, x_k_inv);
354 if lam_p_val.is_zero() {
355 return None;
356 }
357
358 // Compute X_k^{1 - j_start}. Note `1 - j_start` is negative;
359 // since X_k has order ord(β) = 93, we use
360 // X_k^{1 - j_start} = X_k^{(93 - j_start + 1) mod 93}.
361 // But we handle this generically via x_k_inv^{j_start - 1}.
362 let shift = REGULAR_J_START.saturating_sub(1);
363 let x_k_shift = x_k_inv.pow(shift); // = X_k^{-(j_start - 1)} = X_k^{1 - j_start}
364
365 let mag = x_k_shift.mul(omega_val.mul(lam_p_val.inv()));
366
367 // Magnitude must lie in GF(32) (the high coefficient must be zero).
368 if mag.hi != 0 {
369 return None;
370 }
371 if mag.lo == 0 {
372 // Zero magnitude is not a real error — typically signals
373 // more than 4 actual errors that fooled BM.
374 return None;
375 }
376 magnitudes.push(mag.lo);
377 }
378 Some(magnitudes)
379}
380
381// ---------------------------------------------------------------------------
382// Public entry point
383// ---------------------------------------------------------------------------
384
385/// Decode a regular-code BCH error pattern. Inputs:
386///
387/// - `residue_xor_const`: the value
388/// `polymod(hrp_expand("md") || data_with_checksum) ⊕ MD_REGULAR_CONST`.
389/// By the BCH syndrome property, this is congruent to the error
390/// polynomial `E(x)` modulo `g_regular(x)`. The caller is responsible
391/// for running [`crate::bch::polymod_run`] on the full
392/// `hrp_expand(...) || data_with_checksum` slice and XOR-ing the
393/// per-HRP target constant before passing the result here.
394/// - `data_with_checksum_len`: the total symbol count of
395/// `data_with_checksum` (in the `0..=93` range for the regular code).
396///
397/// Returns `Some((positions, magnitudes))` if the algorithm finds a
398/// consistent error pattern of weight `≤ 4`. Each `positions[k]` is an
399/// index into `data_with_checksum` (post-HRP-prefix); each
400/// `magnitudes[k]` is a `GF(32)` symbol that must be XORed into
401/// `data_with_checksum[positions[k]]` to repair the codeword. Returns
402/// `None` if the pattern is uncorrectable (> t = 4 errors).
403pub fn decode_regular_errors(
404 residue_xor_const: u128,
405 data_with_checksum_len: usize,
406) -> Option<(Vec<usize>, Vec<Gf32>)> {
407 let syndromes = compute_syndromes_regular(residue_xor_const);
408
409 // All-zero syndromes ⇒ no errors (caller usually detects earlier).
410 if syndromes.iter().all(|s| s.is_zero()) {
411 return Some((Vec::new(), Vec::new()));
412 }
413
414 let lambda = berlekamp_massey(&syndromes);
415 let deg = lambda.len() - 1;
416 if deg == 0 || deg > 4 {
417 // > 4 errors is above the BCH(93, 80, 8) / t = 4 capacity.
418 return None;
419 }
420
421 let error_degrees = chien_search(&lambda, data_with_checksum_len)?;
422 if error_degrees.len() != deg {
423 return None;
424 }
425
426 let magnitudes = forney(&syndromes, &lambda, &error_degrees)?;
427
428 // Translate polynomial degrees back to data_with_checksum indices.
429 // For data_with_checksum[k] (k = 0..L-1), polynomial degree d = L - 1 - k.
430 // So k = L - 1 - d.
431 let mut positions = Vec::with_capacity(error_degrees.len());
432 for &d in &error_degrees {
433 if d >= data_with_checksum_len {
434 // Should not happen since chien_search bounds d to [0, L).
435 return None;
436 }
437 let k = data_with_checksum_len - 1 - d;
438 positions.push(k);
439 }
440
441 // Sort ascending by position for deterministic output. Magnitudes
442 // need to be reordered along with the positions.
443 let mut paired: Vec<(usize, Gf32)> = positions.into_iter().zip(magnitudes).collect();
444 paired.sort_by_key(|p| p.0);
445 let positions: Vec<usize> = paired.iter().map(|p| p.0).collect();
446 let magnitudes: Vec<Gf32> = paired.iter().map(|p| p.1).collect();
447
448 Some((positions, magnitudes))
449}
450
451// ---------------------------------------------------------------------------
452// Unit tests (algorithmic sanity; integration cells live in tests/bch_decode.rs)
453// ---------------------------------------------------------------------------
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::bch::{MD_REGULAR_CONST, bch_create_checksum_regular, hrp_expand, polymod_run};
459
460 #[test]
461 fn gf32_mul_identity() {
462 for v in 0..32u8 {
463 assert_eq!(gf32_mul(v, 1), v);
464 assert_eq!(gf32_mul(1, v), v);
465 }
466 }
467
468 #[test]
469 fn gf32_mul_zero() {
470 for v in 0..32u8 {
471 assert_eq!(gf32_mul(v, 0), 0);
472 assert_eq!(gf32_mul(0, v), 0);
473 }
474 }
475
476 #[test]
477 fn beta_has_order_93_regular() {
478 // β = G·ζ has order 93 (BIP 93 §"Generation of valid checksum").
479 let mut p = Gf1024::ONE;
480 for j in 1..=93 {
481 p = p.mul(BETA);
482 if p == Gf1024::ONE {
483 assert_eq!(j, 93, "β prematurely returned to 1 at exponent {}", j);
484 }
485 }
486 assert_eq!(p, Gf1024::ONE, "β^93 should equal 1");
487 }
488
489 #[test]
490 fn one_error_decodes_correctly_regular() {
491 let hrp = "md";
492 let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
493 let checksum = bch_create_checksum_regular(hrp, &data);
494 let mut codeword = data.clone();
495 codeword.extend_from_slice(&checksum);
496 let original = codeword.clone();
497
498 let err_pos = 5;
499 let err_mag: u8 = 0b10101;
500 codeword[err_pos] ^= err_mag;
501
502 let mut input = hrp_expand(hrp);
503 input.extend_from_slice(&codeword);
504 let polymod = polymod_run(&input);
505 let residue = polymod ^ MD_REGULAR_CONST;
506
507 let (positions, magnitudes) =
508 decode_regular_errors(residue, codeword.len()).expect("1-error must decode");
509 assert_eq!(positions, vec![err_pos]);
510 assert_eq!(magnitudes, vec![err_mag]);
511
512 let mut corrected = codeword.clone();
513 for (p, m) in positions.iter().zip(&magnitudes) {
514 corrected[*p] ^= m;
515 }
516 assert_eq!(corrected, original);
517 }
518
519 #[test]
520 fn two_errors_decode_correctly_regular() {
521 let hrp = "md";
522 let data: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
523 let checksum = bch_create_checksum_regular(hrp, &data);
524 let mut codeword = data.clone();
525 codeword.extend_from_slice(&checksum);
526 let original = codeword.clone();
527
528 let positions_in: [usize; 2] = [3, 17];
529 let mags_in: [u8; 2] = [0b11001, 0b00111];
530 for (&p, &m) in positions_in.iter().zip(&mags_in) {
531 codeword[p] ^= m;
532 }
533
534 let mut input = hrp_expand(hrp);
535 input.extend_from_slice(&codeword);
536 let polymod = polymod_run(&input);
537 let residue = polymod ^ MD_REGULAR_CONST;
538
539 let (positions, magnitudes) =
540 decode_regular_errors(residue, codeword.len()).expect("2-error must decode");
541 assert_eq!(positions, vec![3, 17]);
542 assert_eq!(magnitudes, vec![mags_in[0], mags_in[1]]);
543
544 let mut corrected = codeword.clone();
545 for (p, m) in positions.iter().zip(&magnitudes) {
546 corrected[*p] ^= m;
547 }
548 assert_eq!(corrected, original);
549 }
550}