From 6ea37349268580946f4dc7e8ce01c6a7113f442b Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 24 Mar 2024 15:15:04 +0000 Subject: [PATCH] checksum: introduce `PrintImpl` object to print `impl Checksum` blocks 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. --- src/lib.rs | 2 + src/primitives/checksum.rs | 194 ++++++++++++++++++++++++++++++++++++- 2 files changed, 194 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ab86aeb21..300bfc85c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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))] diff --git a/src/primitives/checksum.rs b/src/primitives/checksum.rs index 571bb526f..e52d9912a 100644 --- a/src/primitives/checksum.rs +++ b/src/primitives/checksum.rs @@ -4,10 +4,20 @@ //! //! [BCH]: +#[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. /// @@ -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. @@ -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::::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, +} + +#[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::())?; + 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 @@ -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() { @@ -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::>(); + 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::::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); + } }