Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More error correction preparation (upgrades to Field, Polynomial, InvalidResidueError and others) #202

Closed
wants to merge 10 commits into from
20 changes: 14 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,18 @@
//!
//! ```
//! # #[cfg(feature = "alloc")] {
//! use bech32::Checksum;
//! use bech32::{Checksum, Fe32, Fe1024};
//!
//! /// The codex32 checksum algorithm, defined in BIP-93.
//! #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
//! pub enum Codex32 {}
//!
//! impl Checksum for Codex32 {
//! type MidstateRepr = u128;
//! type CorrectionField = bech32::primitives::gf32_ext::Fe32Ext<2>;
//! const ROOT_GENERATOR: Self::CorrectionField = Fe1024::new([Fe32::_9, Fe32::_9]);
//! const ROOT_EXPONENTS: core::ops::RangeInclusive<usize> = 77..=84;
//!
//! const CHECKSUM_LENGTH: usize = 13;
//! const CODE_LENGTH: usize = 93;
//! // Copied from BIP-93
Expand Down Expand Up @@ -130,6 +134,8 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
// Coding conventions
#![deny(missing_docs)]
#![allow(clippy::suspicious_arithmetic_impl)] // this lint is literally always wrong
#![allow(clippy::suspicious_op_assign_impl)] // ...and "always wrong" loves company

#[cfg(bench)]
extern crate test;
Expand Down Expand Up @@ -212,13 +218,15 @@ const BUF_LENGTH: usize = 10;
pub fn decode(s: &str) -> Result<(Hrp, Vec<u8>), DecodeError> {
let unchecked = UncheckedHrpstring::new(s)?;

if let Err(e) = unchecked.validate_checksum::<Bech32m>() {
if !unchecked.has_valid_checksum::<Bech32>() {
match unchecked.validate_checksum::<Bech32m>() {
Ok(_) => {}
Err(ChecksumError::InvalidResidue(ref res_err)) if res_err.matches_bech32_checksum() => {}
Err(e) => {
return Err(DecodeError::Checksum(e));
}
};
// One of the checksums was valid, Ck is only for length and since
// they are both the same we can use either here.
}
// One of the checksums was valid. `Bech32m` is only used for its
// length and since it is the same as `Bech32` we can use either here.
let checked = unchecked.remove_checksum::<Bech32m>();

Ok((checked.hrp(), checked.byte_iter().collect()))
Expand Down
66 changes: 39 additions & 27 deletions src/primitives/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,23 @@
//!
//! [BCH]: <https://en.wikipedia.org/wiki/BCH_code>

#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::vec::Vec;
#[cfg(feature = "alloc")]
use core::fmt;
#[cfg(feature = "alloc")]
use core::marker::PhantomData;
use core::{mem, ops};
use core::{fmt, mem, ops};

#[cfg(feature = "alloc")]
use super::Polynomial;
use crate::primitives::hrp::Hrp;
#[cfg(feature = "alloc")]
use crate::Fe1024;
use crate::Fe32;
use crate::{Fe1024, Fe32};

/// Trait defining a particular checksum.
///
/// For users, this can be treated as a marker trait; none of the associated data
/// are end-user relevant.
///
/// For developers, implementations of this trait can be computed by starting with
/// a couple of values and using the [`PrintImpl`] object to generate the rest.
/// See the documentation for that type and its unit tests for examples.
pub trait Checksum {
/// An unsigned integer type capable of holding a packed version of the generator
/// polynomial (without its leading 1) and target residue (which will have the
Expand All @@ -37,6 +35,16 @@ pub trait Checksum {
/// be pretty efficient no matter what.
type MidstateRepr: PackedFe32;

/// The extension field in which error correction happens.
type CorrectionField: super::ExtensionField<BaseField = Fe32>;

/// The generator of the consecutive roots of the generator polynomial.
const ROOT_GENERATOR: Self::CorrectionField;

/// The consecutive powers of [`Self::ROOT_GENERATOR`] which are roots
/// of the generator polynomial.
const ROOT_EXPONENTS: core::ops::RangeInclusive<usize>;

/// The length of the code.
///
/// The length of the code is how long a coded message can be (including the
Expand Down Expand Up @@ -155,18 +163,16 @@ pub trait Checksum {
/// to compute the values yourself. The reason is that the specific values
/// used depend on the representation of extension fields, which may differ
/// between implementations (and between specifications) of your BCH code.
#[cfg(feature = "alloc")]
pub struct PrintImpl<'a, ExtField = Fe1024> {
name: &'a str,
generator: &'a [Fe32],
generator: Polynomial<Fe32>,
target: &'a [Fe32],
bit_len: usize,
hex_width: usize,
midstate_repr: &'static str,
phantom: PhantomData<ExtField>,
}

#[cfg(feature = "alloc")]
impl<'a, ExtField> PrintImpl<'a, ExtField> {
/// Constructor for an object to print an impl-block for the [`Checksum`] trait.
///
Expand Down Expand Up @@ -211,7 +217,7 @@ impl<'a, ExtField> PrintImpl<'a, ExtField> {
// End sanity checks.
PrintImpl {
name,
generator,
generator: Polynomial::with_monic_leading_term(generator),
target,
bit_len,
hex_width,
Expand All @@ -221,23 +227,16 @@ impl<'a, ExtField> PrintImpl<'a, ExtField> {
}
}

#[cfg(feature = "alloc")]
impl<'a, ExtField> fmt::Display for PrintImpl<'a, ExtField>
where
ExtField: super::ExtensionField + From<Fe32>,
ExtField: super::Bech32Field + super::ExtensionField<BaseField = Fe32>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Generator polynomial as a polynomial over GF1024
let gen_poly = {
let mut v = Vec::with_capacity(self.generator.len() + 1);
v.push(ExtField::ONE);
v.extend(self.generator.iter().cloned().map(ExtField::from));
Polynomial::new(v)
};
let (_gen, length, _exponents) = gen_poly.bch_generator_primitive_element();
let (gen, length, exponents) = self.generator.bch_generator_primitive_element::<ExtField>();

write!(f, "// Code block generated by Checksum::print_impl polynomial ")?;
for fe in self.generator {
for fe in self.generator.as_inner() {
write!(f, "{}", fe)?;
}
write!(f, " target ")?;
Expand All @@ -251,10 +250,22 @@ where
" type MidstateRepr = {}; // checksum packs into {} bits",
self.midstate_repr, self.bit_len
)?;
f.write_str("\n")?;
writeln!(f, " type CorrectionField = {};", core::any::type_name::<ExtField>())?;
f.write_str(" const ROOT_GENERATOR: Self::CorrectionField = ")?;
gen.format_as_rust_code(f)?;
f.write_str(";\n")?;
writeln!(
f,
" const ROOT_EXPONENTS: core::ops::RangeInclusive<usize> = {}..={};",
exponents.start(),
exponents.end()
)?;
f.write_str("\n")?;
writeln!(f, " const CODE_LENGTH: usize = {};", length)?;
writeln!(f, " const CHECKSUM_LENGTH: usize = {};", gen_poly.degree())?;
writeln!(f, " const CHECKSUM_LENGTH: usize = {};", self.generator.degree())?;
writeln!(f, " const GENERATOR_SH: [{}; 5] = [", self.midstate_repr)?;
let mut gen5 = self.generator.to_vec();
let mut gen5 = self.generator.clone().into_inner();
for _ in 0..5 {
let gen_packed = u128::pack(gen5.iter().copied().map(From::from));
writeln!(f, " 0x{:0width$x},", gen_packed, width = self.hex_width)?;
Expand All @@ -263,9 +274,10 @@ where
writeln!(f, " ];")?;
writeln!(
f,
" const TARGET_RESIDUE: {} = {:?};",
" const TARGET_RESIDUE: {} = 0x{:0width$x};",
self.midstate_repr,
u128::pack(self.target.iter().copied().map(From::from))
u128::pack(self.target.iter().copied().map(From::from)),
width = self.hex_width,
)?;
f.write_str("}")
}
Expand Down
35 changes: 35 additions & 0 deletions src/primitives/correction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT

//! Error Correction
//!
//! Implements the Berlekamp-Massey algorithm to locate errors, with Forney's
//! equation to identify the error values, in a BCH-encoded string.
//!

/// **One more than** the maximum length (in characters) of a checksum which
/// can be error-corrected without an allocator.
///
/// When the **alloc** feature is enabled, this constant is practically irrelevant.
/// When the feature is disabled, it represents a length beyond which this library
/// does not support error correction.
///
/// If you need this value to be increased, please file an issue describing your
/// usecase. Bear in mind that an increased value will increase memory usage for
/// all users, and the focus of this library is the Bitcoin ecosystem, so we may
/// not be able to accept your request.
// This constant is also used when comparing bech32 residues against the
// bech32/bech32m targets, which should work with no-alloc. Therefore this
// constant must be > 6 (the length of the bech32(m) checksum).
//
// Otherwise it basically represents a tradeoff between stack usage and the
// size of error types, vs functionality in a no-alloc setting. The value
// of 7 covers bech32 and bech32m. To get the descriptor checksum we need a
// value and the descriptor checksum. To also get codex32 it should be >13,
// and for "long codex32" >15 ... but consider that no-alloc contexts are
// likely to be underpowered and will struggle to do correction on these
// big codes anyway.
//
// Perhaps we will want to add a feature gate, off by default, that boosts
// this to 16, or maybe even higher. But we will wait for implementors who
// complain.
pub const NO_ALLOC_MAX_LENGTH: usize = 7;
66 changes: 58 additions & 8 deletions src/primitives/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@
use core::{fmt, iter, slice, str};

use crate::error::write_err;
use crate::primitives::checksum::{self, Checksum};
use crate::primitives::checksum::{self, Checksum, PackedFe32};
use crate::primitives::gf32::Fe32;
use crate::primitives::hrp::{self, Hrp};
use crate::primitives::iter::{Fe32IterExt, FesToBytes};
use crate::primitives::segwit::{self, WitnessLengthError, VERSION_0};
use crate::primitives::{FieldVec, Polynomial};
use crate::{Bech32, Bech32m};

/// Separator between the hrp and payload (as defined by BIP-173).
Expand Down Expand Up @@ -277,8 +278,9 @@ impl<'s> UncheckedHrpstring<'s> {
checksum_eng.input_fe(fe);
}

if checksum_eng.residue() != &Ck::TARGET_RESIDUE {
return Err(InvalidResidue);
let residue = *checksum_eng.residue();
if residue != Ck::TARGET_RESIDUE {
return Err(InvalidResidue(InvalidResidueError::new(residue, Ck::TARGET_RESIDUE)));
}

Ok(())
Expand Down Expand Up @@ -952,7 +954,7 @@ pub enum ChecksumError {
/// String exceeds maximum allowed length.
CodeLength(CodeLengthError),
/// The checksum residue is not valid for the data.
InvalidResidue,
InvalidResidue(InvalidResidueError),
/// The checksummed string is not a valid length.
InvalidLength,
}
Expand All @@ -963,7 +965,7 @@ impl fmt::Display for ChecksumError {

match *self {
CodeLength(ref e) => write_err!(f, "string exceeds maximum allowed length"; e),
InvalidResidue => write!(f, "the checksum residue is not valid for the data"),
InvalidResidue(ref e) => write_err!(f, "checksum failed"; e),
InvalidLength => write!(f, "the checksummed string is not a valid length"),
}
}
Expand All @@ -976,11 +978,56 @@ impl std::error::Error for ChecksumError {

match *self {
CodeLength(ref e) => Some(e),
InvalidResidue | InvalidLength => None,
InvalidResidue(ref e) => Some(e),
InvalidLength => None,
}
}
}

/// Residue mismatch validating the checksum. That is, "the checksum failed".
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidResidueError {
actual: Polynomial<Fe32>,
target: Polynomial<Fe32>,
}

impl fmt::Display for InvalidResidueError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.actual.has_data() {
write!(f, "residue {} did not match target {}", self.actual, self.target)
} else {
f.write_str("residue mismatch")
}
}
}

impl InvalidResidueError {
/// Constructs a new "invalid residue" error.
fn new<F: PackedFe32>(residue: F, target_residue: F) -> Self {
Self {
actual: FieldVec::from_residue(residue).into(),
target: FieldVec::from_residue(target_residue).into(),
}
}

/// Whether this "invalid residue" error actually represents a valid residue
/// for the bech32 checksum.
///
/// This method could in principle be made generic over the intended checksum,
/// but it is not clear what the purpose would be (checking bech32 vs bech32m
/// is a special case), and the method would necessarily panic if called with
/// too large a checksum without an allocator. We would like to better understand
/// the usecase for this before exposing such a footgun.
pub fn matches_bech32_checksum(&self) -> bool {
self.actual == FieldVec::from_residue(Bech32::TARGET_RESIDUE).into()
}
}

#[cfg(feature = "std")]
impl std::error::Error for InvalidResidueError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}

/// Encoding HRP and data into a bech32 string exceeds the checksum code length.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
Expand Down Expand Up @@ -1065,6 +1112,9 @@ impl std::error::Error for PaddingError {

#[cfg(test)]
mod tests {
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::vec::Vec;

use super::*;

#[test]
Expand Down Expand Up @@ -1117,7 +1167,7 @@ mod tests {
.expect("string parses correctly")
.validate_checksum::<Bech32>()
.unwrap_err();
assert_eq!(err, InvalidResidue);
assert!(matches!(err, InvalidResidue(..)));
}

#[test]
Expand Down Expand Up @@ -1178,7 +1228,7 @@ mod tests {
.unwrap()
.validate_checksum::<Bech32m>()
.unwrap_err();
assert_eq!(err, InvalidResidue);
assert!(matches!(err, InvalidResidue(..)));
}

#[test]
Expand Down
Loading
Loading