Skip to content

Commit 3f98190

Browse files
committed
Merge rust-bitcoin#204: Introduce FieldVec and improve Polynomial and InvalidResidueError
c209ce8 polynomial: expand unit tests (Andrew Poelstra) dfc29ad fieldvec: add a bunch of unit tests (Andrew Poelstra) 0e3d954 polynomial: make Eq/PartialEq take leading zeros into account (Andrew Poelstra) d08da46 correction: introduce CorrectableError trait (Andrew Poelstra) 4c6010e primitives: introduce InvalidResidueError (Andrew Poelstra) b76adbe polynomial: add some extra functionality enabled by FieldVec (Andrew Poelstra) 690fc7d polynomial: use FieldVec in Polynomial (Andrew Poelstra) f2ec582 primitives: introduce FieldVec type (Andrew Poelstra) Pull request description: This introduces a new internal type `FieldVec` which is basically a backing array for checksum residues with some fiddly logic to work in a noalloc context. Unlike ArrayVec or other similar types, this one's semantics are "an unbounded vector if you have an allocator, a bounded vector otherwise". If you go outside of the bounds without an allocator the code will panic, but our use of it ensures that this is never exposed to a user. It also assumes that it's holding a `Default` type which lets us avoid unsafety (and dismisses the potential performance cost because this array is never expected to have more than 10 or 20 elements, even when it is "unbounded"). Using this backing, it improves `Polynomial` and removes the alloc bound from it; using this, it puts `Polynomial` into a new `InvalidResidueError` type, which will allow users to extract the necessary information from a checksum error to run the error correction algorithm. (The resulting change to an error type makes this an API-breaking change, though I don't think that anything else here is API-breaking.) The next PR will actually implement correction :). ACKs for top commit: tcharding: ACK c209ce8 clarkmoody: ACK c209ce8 Tree-SHA512: 16a23bd88101be549c9d50f6b32f669688dfb1efbd6e842c1c3db2203d1775e647f0909c4a65191fd332cd07778c0431ae7b4635909a00f7c2d86135fae042e2
2 parents 86a5bdc + c209ce8 commit 3f98190

File tree

9 files changed

+1061
-167
lines changed

9 files changed

+1061
-167
lines changed

Diff for: src/lib.rs

+10-5
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@
134134
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
135135
// Coding conventions
136136
#![deny(missing_docs)]
137+
#![allow(clippy::suspicious_arithmetic_impl)] // this lint is literally always wrong
138+
#![allow(clippy::suspicious_op_assign_impl)] // ...and "always wrong" loves company
137139

138140
#[cfg(bench)]
139141
extern crate test;
@@ -164,6 +166,7 @@ use crate::primitives::decode::{ChecksumError, UncheckedHrpstring, UncheckedHrps
164166
#[doc(inline)]
165167
pub use {
166168
crate::primitives::checksum::Checksum,
169+
crate::primitives::correction::CorrectableError,
167170
crate::primitives::gf32::Fe32,
168171
crate::primitives::gf32_ext::{Fe1024, Fe32768},
169172
crate::primitives::hrp::Hrp,
@@ -216,13 +219,15 @@ const BUF_LENGTH: usize = 10;
216219
pub fn decode(s: &str) -> Result<(Hrp, Vec<u8>), DecodeError> {
217220
let unchecked = UncheckedHrpstring::new(s)?;
218221

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

228233
Ok((checked.hrp(), checked.byte_iter().collect()))

Diff for: src/primitives/checksum.rs

+9-24
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,14 @@
44
//!
55
//! [BCH]: <https://en.wikipedia.org/wiki/BCH_code>
66
7-
#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
7+
#[cfg(all(feature = "alloc", not(feature = "std")))]
88
use alloc::vec::Vec;
9-
#[cfg(feature = "alloc")]
10-
use core::fmt;
11-
#[cfg(feature = "alloc")]
129
use core::marker::PhantomData;
13-
use core::{mem, ops};
10+
use core::{fmt, mem, ops};
1411

15-
#[cfg(feature = "alloc")]
1612
use super::Polynomial;
1713
use crate::primitives::hrp::Hrp;
18-
#[cfg(feature = "alloc")]
19-
use crate::Fe1024;
20-
use crate::Fe32;
14+
use crate::{Fe1024, Fe32};
2115

2216
/// Trait defining a particular checksum.
2317
///
@@ -169,18 +163,16 @@ pub trait Checksum {
169163
/// to compute the values yourself. The reason is that the specific values
170164
/// used depend on the representation of extension fields, which may differ
171165
/// between implementations (and between specifications) of your BCH code.
172-
#[cfg(feature = "alloc")]
173166
pub struct PrintImpl<'a, ExtField = Fe1024> {
174167
name: &'a str,
175-
generator: &'a [Fe32],
168+
generator: Polynomial<Fe32>,
176169
target: &'a [Fe32],
177170
bit_len: usize,
178171
hex_width: usize,
179172
midstate_repr: &'static str,
180173
phantom: PhantomData<ExtField>,
181174
}
182175

183-
#[cfg(feature = "alloc")]
184176
impl<'a, ExtField> PrintImpl<'a, ExtField> {
185177
/// Constructor for an object to print an impl-block for the [`Checksum`] trait.
186178
///
@@ -225,7 +217,7 @@ impl<'a, ExtField> PrintImpl<'a, ExtField> {
225217
// End sanity checks.
226218
PrintImpl {
227219
name,
228-
generator,
220+
generator: Polynomial::with_monic_leading_term(generator),
229221
target,
230222
bit_len,
231223
hex_width,
@@ -235,23 +227,16 @@ impl<'a, ExtField> PrintImpl<'a, ExtField> {
235227
}
236228
}
237229

238-
#[cfg(feature = "alloc")]
239230
impl<'a, ExtField> fmt::Display for PrintImpl<'a, ExtField>
240231
where
241232
ExtField: super::Bech32Field + super::ExtensionField<BaseField = Fe32>,
242233
{
243234
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
244235
// Generator polynomial as a polynomial over GF1024
245-
let gen_poly = {
246-
let mut v = Vec::with_capacity(self.generator.len() + 1);
247-
v.push(ExtField::ONE);
248-
v.extend(self.generator.iter().cloned().map(ExtField::from));
249-
Polynomial::new(v)
250-
};
251-
let (gen, length, exponents) = gen_poly.bch_generator_primitive_element();
236+
let (gen, length, exponents) = self.generator.bch_generator_primitive_element::<ExtField>();
252237

253238
write!(f, "// Code block generated by Checksum::print_impl polynomial ")?;
254-
for fe in self.generator {
239+
for fe in self.generator.as_inner() {
255240
write!(f, "{}", fe)?;
256241
}
257242
write!(f, " target ")?;
@@ -278,9 +263,9 @@ where
278263
)?;
279264
f.write_str("\n")?;
280265
writeln!(f, " const CODE_LENGTH: usize = {};", length)?;
281-
writeln!(f, " const CHECKSUM_LENGTH: usize = {};", gen_poly.degree())?;
266+
writeln!(f, " const CHECKSUM_LENGTH: usize = {};", self.generator.degree())?;
282267
writeln!(f, " const GENERATOR_SH: [{}; 5] = [", self.midstate_repr)?;
283-
let mut gen5 = self.generator.to_vec();
268+
let mut gen5 = self.generator.clone().into_inner();
284269
for _ in 0..5 {
285270
let gen_packed = u128::pack(gen5.iter().copied().map(From::from));
286271
writeln!(f, " 0x{:0width$x},", gen_packed, width = self.hex_width)?;

Diff for: src/primitives/correction.rs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
//! Error Correction
4+
//!
5+
//! Implements the Berlekamp-Massey algorithm to locate errors, with Forney's
6+
//! equation to identify the error values, in a BCH-encoded string.
7+
//!
8+
9+
use crate::primitives::decode::{
10+
CheckedHrpstringError, ChecksumError, InvalidResidueError, SegwitHrpstringError,
11+
};
12+
#[cfg(feature = "alloc")]
13+
use crate::DecodeError;
14+
15+
/// **One more than** the maximum length (in characters) of a checksum which
16+
/// can be error-corrected without an allocator.
17+
///
18+
/// When the **alloc** feature is enabled, this constant is practically irrelevant.
19+
/// When the feature is disabled, it represents a length beyond which this library
20+
/// does not support error correction.
21+
///
22+
/// If you need this value to be increased, please file an issue describing your
23+
/// usecase. Bear in mind that an increased value will increase memory usage for
24+
/// all users, and the focus of this library is the Bitcoin ecosystem, so we may
25+
/// not be able to accept your request.
26+
// This constant is also used when comparing bech32 residues against the
27+
// bech32/bech32m targets, which should work with no-alloc. Therefore this
28+
// constant must be > 6 (the length of the bech32(m) checksum).
29+
//
30+
// Otherwise it basically represents a tradeoff between stack usage and the
31+
// size of error types, vs functionality in a no-alloc setting. The value
32+
// of 7 covers bech32 and bech32m. To get the descriptor checksum we need a
33+
// value and the descriptor checksum. To also get codex32 it should be >13,
34+
// and for "long codex32" >15 ... but consider that no-alloc contexts are
35+
// likely to be underpowered and will struggle to do correction on these
36+
// big codes anyway.
37+
//
38+
// Perhaps we will want to add a feature gate, off by default, that boosts
39+
// this to 16, or maybe even higher. But we will wait for implementors who
40+
// complain.
41+
pub const NO_ALLOC_MAX_LENGTH: usize = 7;
42+
43+
/// Trait describing an error for which an error correction algorithm is applicable.
44+
///
45+
/// Essentially, this trait represents an error which contains an [`InvalidResidueError`]
46+
/// variant.
47+
pub trait CorrectableError {
48+
/// Given a decoding error, if this is a "checksum failed" error, extract
49+
/// that specific error type.
50+
///
51+
/// There are many ways in which decoding a checksummed string might fail.
52+
/// If the string was well-formed in all respects except that the final
53+
/// checksum characters appear to be wrong, it is possible to run an
54+
/// error correction algorithm to attempt to extract errors.
55+
///
56+
/// In all other cases we do not have enough information to do correction.
57+
///
58+
/// This is the function that implementors should implement.
59+
fn residue_error(&self) -> Option<&InvalidResidueError>;
60+
}
61+
62+
impl CorrectableError for InvalidResidueError {
63+
fn residue_error(&self) -> Option<&InvalidResidueError> { Some(self) }
64+
}
65+
66+
impl CorrectableError for ChecksumError {
67+
fn residue_error(&self) -> Option<&InvalidResidueError> {
68+
match self {
69+
ChecksumError::InvalidResidue(ref e) => Some(e),
70+
_ => None,
71+
}
72+
}
73+
}
74+
75+
impl CorrectableError for SegwitHrpstringError {
76+
fn residue_error(&self) -> Option<&InvalidResidueError> {
77+
match self {
78+
SegwitHrpstringError::Checksum(ref e) => e.residue_error(),
79+
_ => None,
80+
}
81+
}
82+
}
83+
84+
impl CorrectableError for CheckedHrpstringError {
85+
fn residue_error(&self) -> Option<&InvalidResidueError> {
86+
match self {
87+
CheckedHrpstringError::Checksum(ref e) => e.residue_error(),
88+
_ => None,
89+
}
90+
}
91+
}
92+
93+
#[cfg(feature = "alloc")]
94+
impl CorrectableError for crate::segwit::DecodeError {
95+
fn residue_error(&self) -> Option<&InvalidResidueError> { self.0.residue_error() }
96+
}
97+
98+
#[cfg(feature = "alloc")]
99+
impl CorrectableError for DecodeError {
100+
fn residue_error(&self) -> Option<&InvalidResidueError> {
101+
match self {
102+
DecodeError::Checksum(ref e) => e.residue_error(),
103+
_ => None,
104+
}
105+
}
106+
}

Diff for: src/primitives/decode.rs

+67-8
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,12 @@
7676
use core::{fmt, iter, slice, str};
7777

7878
use crate::error::write_err;
79-
use crate::primitives::checksum::{self, Checksum};
79+
use crate::primitives::checksum::{self, Checksum, PackedFe32};
8080
use crate::primitives::gf32::Fe32;
8181
use crate::primitives::hrp::{self, Hrp};
8282
use crate::primitives::iter::{Fe32IterExt, FesToBytes};
8383
use crate::primitives::segwit::{self, WitnessLengthError, VERSION_0};
84+
use crate::primitives::Polynomial;
8485
use crate::{Bech32, Bech32m};
8586

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

280-
if checksum_eng.residue() != &Ck::TARGET_RESIDUE {
281-
return Err(InvalidResidue);
281+
let residue = *checksum_eng.residue();
282+
if residue != Ck::TARGET_RESIDUE {
283+
return Err(InvalidResidue(InvalidResidueError::new(residue, Ck::TARGET_RESIDUE)));
282284
}
283285

284286
Ok(())
@@ -952,7 +954,7 @@ pub enum ChecksumError {
952954
/// String exceeds maximum allowed length.
953955
CodeLength(CodeLengthError),
954956
/// The checksum residue is not valid for the data.
955-
InvalidResidue,
957+
InvalidResidue(InvalidResidueError),
956958
/// The checksummed string is not a valid length.
957959
InvalidLength,
958960
}
@@ -963,7 +965,7 @@ impl fmt::Display for ChecksumError {
963965

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

977979
match *self {
978980
CodeLength(ref e) => Some(e),
979-
InvalidResidue | InvalidLength => None,
981+
InvalidResidue(ref e) => Some(e),
982+
InvalidLength => None,
980983
}
981984
}
982985
}
983986

987+
/// Residue mismatch validating the checksum. That is, "the checksum failed".
988+
#[derive(Debug, Clone, PartialEq, Eq)]
989+
pub struct InvalidResidueError {
990+
actual: Polynomial<Fe32>,
991+
target: Polynomial<Fe32>,
992+
}
993+
994+
impl fmt::Display for InvalidResidueError {
995+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
996+
if self.actual.has_data() {
997+
write!(f, "residue {} did not match target {}", self.actual, self.target)
998+
} else {
999+
f.write_str("residue mismatch")
1000+
}
1001+
}
1002+
}
1003+
1004+
impl InvalidResidueError {
1005+
/// Constructs a new "invalid residue" error.
1006+
fn new<F: PackedFe32>(residue: F, target_residue: F) -> Self {
1007+
Self {
1008+
actual: Polynomial::from_residue(residue),
1009+
target: Polynomial::from_residue(target_residue),
1010+
}
1011+
}
1012+
1013+
/// Whether this "invalid residue" error actually represents a valid residue
1014+
/// for the bech32 checksum.
1015+
///
1016+
/// This method could in principle be made generic over the intended checksum,
1017+
/// but it is not clear what the purpose would be (checking bech32 vs bech32m
1018+
/// is a special case), and the method would necessarily panic if called with
1019+
/// too large a checksum without an allocator. We would like to better understand
1020+
/// the usecase for this before exposing such a footgun.
1021+
pub fn matches_bech32_checksum(&self) -> bool {
1022+
self.actual == Polynomial::from_residue(Bech32::TARGET_RESIDUE)
1023+
}
1024+
}
1025+
1026+
#[cfg(feature = "std")]
1027+
impl std::error::Error for InvalidResidueError {
1028+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
1029+
}
1030+
9841031
/// Encoding HRP and data into a bech32 string exceeds the checksum code length.
9851032
#[derive(Debug, Clone, PartialEq, Eq)]
9861033
#[non_exhaustive]
@@ -1065,6 +1112,9 @@ impl std::error::Error for PaddingError {
10651112

10661113
#[cfg(test)]
10671114
mod tests {
1115+
#[cfg(all(feature = "alloc", not(feature = "std")))]
1116+
use alloc::vec::Vec;
1117+
10681118
use super::*;
10691119

10701120
#[test]
@@ -1117,7 +1167,7 @@ mod tests {
11171167
.expect("string parses correctly")
11181168
.validate_checksum::<Bech32>()
11191169
.unwrap_err();
1120-
assert_eq!(err, InvalidResidue);
1170+
assert!(matches!(err, InvalidResidue(..)));
11211171
}
11221172

11231173
#[test]
@@ -1178,7 +1228,7 @@ mod tests {
11781228
.unwrap()
11791229
.validate_checksum::<Bech32m>()
11801230
.unwrap_err();
1181-
assert_eq!(err, InvalidResidue);
1231+
assert!(matches!(err, InvalidResidue(..)));
11821232
}
11831233

11841234
#[test]
@@ -1212,6 +1262,15 @@ mod tests {
12121262
}
12131263
}
12141264

1265+
#[test]
1266+
#[allow(clippy::assertions_on_constants)]
1267+
fn constant_sanity() {
1268+
assert!(
1269+
crate::primitives::correction::NO_ALLOC_MAX_LENGTH > 6,
1270+
"The NO_ALLOC_MAX_LENGTH constant must be > 6. See its documentation for why.",
1271+
);
1272+
}
1273+
12151274
macro_rules! check_invalid_segwit_addresses {
12161275
($($test_name:ident, $reason:literal, $address:literal);* $(;)?) => {
12171276
$(

0 commit comments

Comments
 (0)