Skip to content

Commit

Permalink
checksum: introduce PrintImpl object to print impl Checksum blocks
Browse files Browse the repository at this point in the history
For the most part, BCH codes used in the ecosystem are defined as
generator polynomials over GF32 and a target residue polynomial,
and the remaining code parameters are not specified, or are only
specified indirectly.

It is difficult for non-experts to compute or even validate parameters
such as the length of the code or the shifted+packed generator
polynomials. It will be even more difficult in the sequel when we extend
the Checksum trait to also cover error-correction parameters (some of
which will depend on our particular choice of representation for GF1024
elements, which is not canonical and differs between different
documents).

So we introduce an object `PrintImpl` and a unit test and doctest
demonstrating its use, which will generate all of the needed parameters
and output a block of Rust code.
  • Loading branch information
apoelstra committed Mar 25, 2024
1 parent f1871f7 commit 6ea3734
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ pub use {
crate::primitives::iter::{ByteIterExt, Fe32IterExt},
crate::primitives::{Bech32, Bech32m, NoChecksum},
};
#[cfg(feature = "alloc")]
pub use crate::primitives::checksum::PrintImpl;

// Write to fmt buffer, small during testing to exercise full code path.
#[cfg(not(test))]
Expand Down
194 changes: 192 additions & 2 deletions src/primitives/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@
//!
//! [BCH]: <https://en.wikipedia.org/wiki/BCH_code>

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

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

/// Trait defining a particular checksum.
///
Expand Down Expand Up @@ -49,7 +59,7 @@ pub trait Checksum {
///
/// These cannot be usefully pre-computed because of Rust's limited constfn support
/// as of 1.67, so they must be specified manually for each checksum. To check the
/// values for consistency, run `Self::sanity_check()`.
/// values for consistency, run [`Self::sanity_check`].
const GENERATOR_SH: [Self::MidstateRepr; 5];

/// The residue, modulo the generator polynomial, that a valid codeword will have.
Expand Down Expand Up @@ -83,6 +93,144 @@ pub trait Checksum {
}
}

/// Given a polynomial representation for your generator polynomial and your
/// target residue, outputs a `impl Checksum` block.
///
/// Used like
///
/// ```
/// # #[cfg(feature = "alloc")] {
/// use bech32::{Bech32, Fe32, PrintImpl};
/// println!(
/// "{}",
/// PrintImpl::<Bech32>::new(
/// &[Fe32::A, Fe32::K, Fe32::_5, Fe32::_4, Fe32::A, Fe32::J],
/// &[Fe32::Q, Fe32::Q, Fe32::Q, Fe32::Q, Fe32::Q, Fe32::P],
/// ),
/// );
/// # }
/// ```
///
/// The awkward API is to allow this type to be used in the widest set of
/// circumstances, including in nostd settings. (However, the underlying
/// polynomial math requires the `alloc` feature.)
///
/// Both polynomial representations should be in little-endian order, so that
/// the coefficient of x^i appears in the ith slot. The generator polynomial
/// should be a monic polynomial but you should not include the monic term,
/// so that both `generator` and `target` are arrays of the same length.
///
/// **This function should never need to be called by users, but will be helpful
/// for developers.**
///
/// In general, when defining a checksum, it is best to call this method (and
/// to add a unit test that calls [`Checksum::sanity_check`] rather than trying
/// 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, T> {
generator: &'a [Fe32],
target: &'a [Fe32],
bit_len: usize,
hex_width: usize,
midstate_repr: &'static str,
phantom: PhantomData<T>,
}

#[cfg(feature = "alloc")]
impl<'a, T: Checksum> PrintImpl<'a, T> {
/// Constructor for an object to print an impl-block for the [`Checksum`] trait.
///
/// # Panics
///
/// Panics if any of the input values fail various sanity checks.
pub fn new(generator: &'a [Fe32], target: &'a [Fe32]) -> Self {
// Sanity checks.
assert_ne!(
generator.len(),
0,
"generator polynomial cannot be the empty string (constant 1)"
);
assert_ne!(target.len(), 0, "target residue cannot be the empty string");
if generator.len() != target.len() {
let hint = if generator.len() == target.len() + 1 {
" (you should not include the monic term of the generator polynomial"
} else if generator.len() > target.len() {
" (you may need to zero-pad your target residue)"
} else {
""
};
panic!(
"Generator length {} does not match target residue length {}{}",
generator.len(),
target.len(),
hint
);
}

let bit_len = 5 * target.len();
let (hex_width, midstate_repr) = if bit_len <= 32 {
(8, "u32")
} else if bit_len <= 64 {
(16, "u64")
} else if bit_len <= 128 {
(32, "u128")
} else {
panic!("Generator length {} cannot exceed 25, as we cannot represent it by packing bits into a Rust numeric type", generator.len());
};
// End sanity checks.
PrintImpl { generator, target, bit_len, hex_width, midstate_repr, phantom: PhantomData }
}
}

#[cfg(feature = "alloc")]
impl<'a, T: Checksum> fmt::Display for PrintImpl<'a, T> {
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(Fe1024::ONE);
v.extend(self.generator.iter().cloned().map(Fe1024::from));
Polynomial::new(v)
};
let (_gen, length, _exponents) = gen_poly.bch_generator_primitive_element();

write!(f, "// Code block generated by Checksum::print_impl polynomial ")?;
for fe in self.generator {
write!(f, "{}", fe)?;
}
write!(f, " target ")?;
for fe in self.target {
write!(f, "{}", fe)?;
}
f.write_str("")?;
writeln!(f, "impl Checksum for {} {{", core::any::type_name::<T>())?;
writeln!(
f,
" type MidstateRepr = {}; // checksum packs into {} bits",
self.midstate_repr, self.bit_len
)?;
writeln!(f, " const CODE_LENGTH: usize = {};", length)?;
writeln!(f, " const CHECKSUM_LENGTH: usize = {};", gen_poly.degree())?;
writeln!(f, " const GENERATOR_SH: [{}; 5] = [", self.midstate_repr)?;
let mut gen5 = self.generator.to_vec();
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)?;
gen5.iter_mut().for_each(|x| *x *= Fe32::Z);
}
writeln!(f, " ];")?;
writeln!(
f,
" const TARGET_RESIDUE: {} = {:?};",
self.midstate_repr,
u128::pack(self.target.iter().copied().map(From::from))
)?;
f.write_str("}")
}
}

/// A checksum engine, which can be used to compute or verify a checksum.
///
/// Use this to verify a checksum, feed it the data to be checksummed using
Expand Down Expand Up @@ -311,7 +459,12 @@ impl<'hrp> Iterator for HrpFe32Iter<'hrp> {

#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
use core::convert::TryFrom;

use super::*;
#[cfg(feature = "alloc")]
use crate::Bech32;

#[test]
fn pack_unpack() {
Expand All @@ -327,4 +480,41 @@ mod tests {
assert_eq!(packed.unpack(2), 2);
assert_eq!(packed.unpack(3), 1);
}

#[test]
#[cfg(feature = "alloc")]
fn bech32() {
// In codes that Pieter specifies typically the generator polynomial is
// only given indirectly, in the reference code which encodes it in a
// packed form (shifted multiple times).
//
// For example in the BIP173 Python reference code you will see an array
// called `generator` whose first entry is 0x3b6a57b2. This first entry
// is the generator polynomial in packed form.
//
// To get the expanded polynomial form you can use `u128::unpack` like so:
let unpacked_poly = (0..6)
.rev() // Note .rev() to convert from BE integer literal to LE polynomial!
.map(|i| 0x3b6a57b2u128.unpack(i))
.map(|u| Fe32::try_from(u).unwrap())
.collect::<Vec<_>>();
assert_eq!(unpacked_poly, [Fe32::A, Fe32::K, Fe32::_5, Fe32::_4, Fe32::A, Fe32::J],);
// To get a version of the above with bech32 chars instead of Fe32s, which
// can be a bit hard to print, just stick a `.map(Fe32::to_char)` into the
// above iterator chain.

// Ok, exposition over. The actual unit test follows.

// Run with -- --nocapture to see the output of this. This unit test
// does not check the exact output because it is not deterministic,
// and cannot check the code semantics because Rust does not have
// any sort of `eval`, but you can manually check the output works.
let _s = PrintImpl::<Bech32>::new(
&[Fe32::A, Fe32::K, Fe32::_5, Fe32::_4, Fe32::A, Fe32::J],
&[Fe32::Q, Fe32::Q, Fe32::Q, Fe32::Q, Fe32::Q, Fe32::P],
)
.to_string();
#[cfg(feature = "std")]
println!("{}", _s);
}
}

0 comments on commit 6ea3734

Please sign in to comment.