Skip to content

Commit 2e1b7be

Browse files
committed
implement error correction
This commit pulls everything together. The actual error correction code isn't too big: we interpret a residue as a polynomial, evaluate it at various powers of alpha to get a syndrome polynomial, call berlekeamp-massey on this to get a "connection polynomial", then use Forney's algorithm to get the actual error values. Each step in the above is encapsulated separately -- the "big" stuff, in particular Berlekamp-Massey and obtaining the relevant constants from the checksum definition, were in previous commits. This PR does need to add some more functionality to Polynomial. Specifically we need the ability to evaluate polynomials, take their formal derivatives, and multiply them modulo x^d for a given d. These are the bulk of this PR. The next commit will introduce a fuzztest which hammers on the correction logic to ensure that it's not crashing.
1 parent 6c24f98 commit 2e1b7be

File tree

4 files changed

+294
-3
lines changed

4 files changed

+294
-3
lines changed

src/primitives/correction.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66
//! equation to identify the error values, in a BCH-encoded string.
77
//!
88
9+
use core::convert::TryInto;
10+
use core::marker::PhantomData;
11+
912
use crate::primitives::decode::{
1013
CheckedHrpstringError, ChecksumError, InvalidResidueError, SegwitHrpstringError,
1114
};
15+
use crate::primitives::{Field as _, FieldVec, LfsrIter, Polynomial};
1216
#[cfg(feature = "alloc")]
1317
use crate::DecodeError;
18+
use crate::{Checksum, Fe32};
1419

1520
/// **One more than** the maximum length (in characters) of a checksum which
1621
/// can be error-corrected without an allocator.
@@ -57,6 +62,22 @@ pub trait CorrectableError {
5762
///
5863
/// This is the function that implementors should implement.
5964
fn residue_error(&self) -> Option<&InvalidResidueError>;
65+
66+
/// Wrapper around [`Self::residue_error`] that outputs a correction context.
67+
///
68+
/// Will return None if the error is not a correctable one, or if the **alloc**
69+
/// feature is disabled and the checksum is too large. See the documentation
70+
/// for [`NO_ALLOC_MAX_LENGTH`] for more information.
71+
///
72+
/// This is the function that users should call.
73+
fn correction_context<Ck: Checksum>(&self) -> Option<Corrector<Ck>> {
74+
#[cfg(not(feature = "alloc"))]
75+
if Ck::CHECKSUM_LENGTH >= NO_ALLOC_MAX_LENGTH {
76+
return None;
77+
}
78+
79+
self.residue_error().map(|e| Corrector { residue: e.residue(), phantom: PhantomData })
80+
}
6081
}
6182

6283
impl CorrectableError for InvalidResidueError {
@@ -104,3 +125,186 @@ impl CorrectableError for DecodeError {
104125
}
105126
}
106127
}
128+
129+
/// An error-correction context.
130+
pub struct Corrector<Ck> {
131+
residue: Polynomial<Fe32>,
132+
phantom: PhantomData<Ck>,
133+
}
134+
135+
impl<Ck: Checksum> Corrector<Ck> {
136+
/// Returns an iterator over the errors in the string.
137+
///
138+
/// Returns `None` if it can be determined that there are too many errors to be
139+
/// corrected. However, returning an iterator from this function does **not**
140+
/// imply that the intended string can be determined. It only implies that there
141+
/// is a unique closest correct string to the erroneous string, and gives
142+
/// instructions for finding it.
143+
///
144+
/// If the input string has sufficiently many errors, this unique closest correct
145+
/// string may not actually be the intended string.
146+
pub fn bch_errors(&self) -> Option<ErrorIterator<Ck>> {
147+
// 1. Compute all syndromes by evaluating the residue at each power of the generator.
148+
let syndromes: FieldVec<_> = Ck::ROOT_GENERATOR
149+
.powers_range(Ck::ROOT_EXPONENTS)
150+
.map(|rt| self.residue.evaluate(&rt))
151+
.collect();
152+
153+
// 2. Use the Berlekamp-Massey algorithm to find the connection polynomial of the
154+
// LFSR that generates these syndromes. For magical reasons this will be equal
155+
// to the error locator polynomial for the syndrome.
156+
let lfsr = LfsrIter::berlekamp_massey(&syndromes[..]);
157+
let conn = lfsr.coefficient_polynomial();
158+
159+
// 3. The connection polynomial is the error locator polynomial. Use this to get
160+
// the errors.
161+
let max_correctable_errors =
162+
(Ck::ROOT_EXPONENTS.end() - Ck::ROOT_EXPONENTS.start() + 1) / 2;
163+
if conn.degree() <= max_correctable_errors {
164+
Some(ErrorIterator {
165+
evaluator: conn.mul_mod_x_d(
166+
&Polynomial::from(syndromes),
167+
Ck::ROOT_EXPONENTS.end() - Ck::ROOT_EXPONENTS.start() + 1,
168+
),
169+
locator_derivative: conn.formal_derivative(),
170+
inner: conn.find_nonzero_distinct_roots(Ck::ROOT_GENERATOR),
171+
a: Ck::ROOT_GENERATOR,
172+
c: *Ck::ROOT_EXPONENTS.start(),
173+
})
174+
} else {
175+
None
176+
}
177+
}
178+
}
179+
180+
/// An iterator over the errors in a string.
181+
///
182+
/// The errors will be yielded as `(usize, Fe32)` tuples.
183+
///
184+
/// The first component is a **negative index** into the string. So 0 represents
185+
/// the last element, 1 the second-to-last, and so on.
186+
///
187+
/// The second component is an element to **add to** the element at the given
188+
/// location in the string.
189+
///
190+
/// The maximum index is one less than [`Checksum::CODE_LENGTH`], regardless of the
191+
/// actual length of the string. Therefore it is not safe to simply subtract the
192+
/// length of the string from the returned index; you must first check that the
193+
/// index makes sense. If the index exceeds the length of the string or implies that
194+
/// an error occurred in the HRP, the string should simply be rejected as uncorrectable.
195+
///
196+
/// Out-of-bound error locations will not occur "naturally", in the sense that they
197+
/// will happen with extremely low probability for a string with a valid HRP and a
198+
/// uniform error pattern. (The probability is 32^-n, where n is the size of the
199+
/// range [`Checksum::ROOT_EXPONENTS`], so it is not neglible but is very small for
200+
/// most checksums.) However, it is easy to construct adversarial inputs that will
201+
/// exhibit this behavior, so you must take it into account.
202+
///
203+
/// Out-of-bound error locations may occur naturally in the case of a string with a
204+
/// corrupted HRP, because for checksumming purposes the HRP is treated as twice as
205+
/// many field elements as characters, plus one. If the correct HRP is known, the
206+
/// caller should fix this before attempting error correction. If it is unknown,
207+
/// the caller cannot assume anything about the intended checksum, and should not
208+
/// attempt error correction.
209+
pub struct ErrorIterator<Ck: Checksum> {
210+
evaluator: Polynomial<Ck::CorrectionField>,
211+
locator_derivative: Polynomial<Ck::CorrectionField>,
212+
inner: super::polynomial::RootIter<Ck::CorrectionField>,
213+
a: Ck::CorrectionField,
214+
c: usize,
215+
}
216+
217+
impl<Ck: Checksum> Iterator for ErrorIterator<Ck> {
218+
type Item = (usize, Fe32);
219+
220+
fn next(&mut self) -> Option<Self::Item> {
221+
// Compute -i, which is the location we will return to the user.
222+
let neg_i = match self.inner.next() {
223+
None => return None,
224+
Some(0) => 0,
225+
Some(x) => Ck::ROOT_GENERATOR.multiplicative_order() - x,
226+
};
227+
228+
// Forney's equation, as described in https://en.wikipedia.org/wiki/BCH_code#Forney_algorithm
229+
//
230+
// It is rendered as
231+
//
232+
// a^i evaluator(a^-i)
233+
// e_k = - ---------------------------------
234+
// a^(ci) locator_derivative(a^-i)
235+
//
236+
// where here a is `Ck::ROOT_GENERATOR`, c is the first element of the range
237+
// `Ck::ROOT_EXPONENTS`, and both evalutor and locator_derivative are polynomials
238+
// which are computed when constructing the ErrorIterator.
239+
240+
let a_i = self.a.powi(neg_i as i64);
241+
let a_neg_i = a_i.clone().multiplicative_inverse();
242+
243+
let num = self.evaluator.evaluate(&a_neg_i) * &a_i;
244+
let den = a_i.powi(self.c as i64) * self.locator_derivative.evaluate(&a_neg_i);
245+
let ret = -num / den;
246+
match ret.try_into() {
247+
Ok(ret) => Some((neg_i, ret)),
248+
Err(_) => unreachable!("error guaranteed to lie in base field"),
249+
}
250+
}
251+
}
252+
253+
#[cfg(test)]
254+
mod tests {
255+
use super::*;
256+
use crate::primitives::decode::SegwitHrpstring;
257+
use crate::Bech32;
258+
259+
#[test]
260+
fn bech32() {
261+
// Last x should be q
262+
let s = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdx";
263+
match SegwitHrpstring::new(s) {
264+
Ok(_) => panic!("{} successfully, and wrongly, parsed", s),
265+
Err(e) => {
266+
let ctx = e.correction_context::<Bech32>().unwrap();
267+
let mut iter = ctx.bch_errors().unwrap();
268+
269+
assert_eq!(iter.next(), Some((0, Fe32::X)));
270+
assert_eq!(iter.next(), None);
271+
}
272+
}
273+
274+
// f should be z, 6 chars from the back.
275+
let s = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzfwf5mdq";
276+
match SegwitHrpstring::new(s) {
277+
Ok(_) => panic!("{} successfully, and wrongly, parsed", s),
278+
Err(e) => {
279+
let ctx = e.correction_context::<Bech32>().unwrap();
280+
let mut iter = ctx.bch_errors().unwrap();
281+
282+
assert_eq!(iter.next(), Some((6, Fe32::T)));
283+
assert_eq!(iter.next(), None);
284+
}
285+
}
286+
287+
// 20 characters from the end there is a q which should be 3
288+
let s = "bc1qar0srrr7xfkvy5l64qlydnw9re59gtzzwf5mdq";
289+
match SegwitHrpstring::new(s) {
290+
Ok(_) => panic!("{} successfully, and wrongly, parsed", s),
291+
Err(e) => {
292+
let ctx = e.correction_context::<Bech32>().unwrap();
293+
let mut iter = ctx.bch_errors().unwrap();
294+
295+
assert_eq!(iter.next(), Some((20, Fe32::_3)));
296+
assert_eq!(iter.next(), None);
297+
}
298+
}
299+
300+
// Two errors.
301+
let s = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mxx";
302+
match SegwitHrpstring::new(s) {
303+
Ok(_) => panic!("{} successfully, and wrongly, parsed", s),
304+
Err(e) => {
305+
let ctx = e.correction_context::<Bech32>().unwrap();
306+
assert!(ctx.bch_errors().is_none());
307+
}
308+
}
309+
}
310+
}

src/primitives/decode.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,17 @@ impl InvalidResidueError {
10211021
pub fn matches_bech32_checksum(&self) -> bool {
10221022
self.actual == Polynomial::from_residue(Bech32::TARGET_RESIDUE)
10231023
}
1024+
1025+
/// Accessor for the invalid residue, less the target residue.
1026+
///
1027+
/// Note that because the error type is not parameterized by a checksum (it
1028+
/// holds the target residue but this doesn't help), the caller will need
1029+
/// to obtain the checksum from somewhere else in order to make use of this.
1030+
///
1031+
/// Not public because [`Polynomial`] is a private type, and because the
1032+
/// subtraction will panic if this is called without checking has_data
1033+
/// on the FieldVecs.
1034+
pub(super) fn residue(&self) -> Polynomial<Fe32> { self.actual.clone() - &self.target }
10241035
}
10251036

10261037
#[cfg(feature = "std")]

src/primitives/lfsr.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ impl<F: Field> LfsrIter<F> {
3939
/// Accessor for the coefficients used to compute the next element.
4040
pub fn coefficients(&self) -> &[F] { &self.coeffs.as_inner()[1..] }
4141

42+
/// Accessor for the coefficients used to compute the next element.
43+
pub(super) fn coefficient_polynomial(&self) -> &Polynomial<F> { &self.coeffs }
44+
4245
/// Create a minimal LFSR iterator that generates a set of initial
4346
/// contents, using Berlekamp's algorithm.
4447
///

src/primitives/polynomial.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
//! Polynomials over Finite Fields
44
5-
use core::{fmt, iter, ops, slice};
5+
use core::{cmp, fmt, iter, ops, slice};
66

77
use super::checksum::PackedFe32;
88
use super::{ExtensionField, Field, FieldVec};
@@ -26,7 +26,7 @@ impl<F: Field> Eq for Polynomial<F> {}
2626

2727
impl Polynomial<Fe32> {
2828
pub fn from_residue<R: PackedFe32>(residue: R) -> Self {
29-
(0..R::WIDTH).rev().map(|i| Fe32(residue.unpack(i))).collect()
29+
(0..R::WIDTH).map(|i| Fe32(residue.unpack(i))).collect()
3030
}
3131
}
3232
impl<F: Field> Polynomial<F> {
@@ -70,7 +70,7 @@ impl<F: Field> Polynomial<F> {
7070
/// Panics if [`Self::has_data`] is false.
7171
pub fn iter(&self) -> slice::Iter<F> {
7272
self.assert_has_data();
73-
self.inner.iter()
73+
self.inner[..self.degree() + 1].iter()
7474
}
7575

7676
/// The leading term of the polynomial.
@@ -89,6 +89,11 @@ impl<F: Field> Polynomial<F> {
8989
/// factor of the polynomial.
9090
pub fn zero_is_root(&self) -> bool { self.inner.is_empty() || self.leading_term() == F::ZERO }
9191

92+
/// Computes the formal derivative of the polynomial
93+
pub fn formal_derivative(&self) -> Self {
94+
self.iter().enumerate().map(|(n, fe)| fe.muli(n as i64)).skip(1).collect()
95+
}
96+
9297
/// Helper function to add leading 0 terms until the polynomial has a specified
9398
/// length.
9499
fn zero_pad_up_to(&mut self, len: usize) {
@@ -128,6 +133,38 @@ impl<F: Field> Polynomial<F> {
128133
}
129134
}
130135

136+
/// Evaluate the polynomial at a given element.
137+
pub fn evaluate<E: Field + From<F>>(&self, elem: &E) -> E {
138+
let mut res = E::ZERO;
139+
for fe in self.iter().rev() {
140+
res *= elem;
141+
res += E::from(fe.clone());
142+
}
143+
res
144+
}
145+
146+
/// Multiplies two polynomials modulo x^d, for some given `d`.
147+
///
148+
/// Can be used to simply multiply two polynomials, by passing `usize::MAX` or
149+
/// some other suitably large number as `d`.
150+
pub fn mul_mod_x_d(&self, other: &Self, d: usize) -> Self {
151+
if d == 0 {
152+
return Self { inner: FieldVec::new() };
153+
}
154+
155+
let sdeg = self.degree();
156+
let odeg = other.degree();
157+
158+
let convolution_product = |exp: usize| {
159+
let sidx = exp.saturating_sub(sdeg);
160+
let eidx = cmp::min(exp, odeg);
161+
(sidx..=eidx).map(|i| self.inner[exp - i].clone() * &other.inner[i]).sum()
162+
};
163+
164+
let max_n = cmp::min(sdeg + odeg + 1, d - 1);
165+
(0..=max_n).map(convolution_product).collect()
166+
}
167+
131168
/// Given a BCH generator polynomial, find an element alpha that maximizes the
132169
/// consecutive range i..j such that `alpha^i `through `alpha^j` are all roots
133170
/// of the polynomial.
@@ -456,4 +493,40 @@ mod tests {
456493
panic!("Unexpected generator {}", elem);
457494
}
458495
}
496+
497+
#[test]
498+
fn mul_mod() {
499+
let x_minus_1: Polynomial<_> = [Fe32::P, Fe32::P].iter().copied().collect();
500+
assert_eq!(
501+
x_minus_1.mul_mod_x_d(&x_minus_1, 3),
502+
[Fe32::P, Fe32::Q, Fe32::P].iter().copied().collect(),
503+
);
504+
assert_eq!(x_minus_1.mul_mod_x_d(&x_minus_1, 2), [Fe32::P].iter().copied().collect(),);
505+
}
506+
507+
#[test]
508+
#[cfg(feature = "alloc")] // needed since `mul_mod_x_d` produces extra 0 coefficients
509+
fn factor_then_mul() {
510+
let bech32_poly: Polynomial<Fe32> = {
511+
use Fe32 as F;
512+
[F::J, F::A, F::_4, F::_5, F::K, F::A, F::P]
513+
}
514+
.iter()
515+
.copied()
516+
.collect();
517+
518+
let bech32_poly_lift = Polynomial { inner: bech32_poly.inner.lift() };
519+
520+
let factors = bech32_poly
521+
.find_nonzero_distinct_roots(Fe1024::GENERATOR)
522+
.map(|idx| Fe1024::GENERATOR.powi(idx as i64))
523+
.map(|root| [root, Fe1024::ONE].iter().copied().collect::<Polynomial<_>>())
524+
.collect::<Vec<_>>();
525+
526+
let product = factors.iter().fold(
527+
Polynomial::with_monic_leading_term(&[]),
528+
|acc: Polynomial<_>, factor: &Polynomial<_>| acc.mul_mod_x_d(factor, 100),
529+
);
530+
assert_eq!(bech32_poly_lift, product);
531+
}
459532
}

0 commit comments

Comments
 (0)