Skip to content

Commit b5fdff3

Browse files
committed
fuzz: add fuzztests that try to correct bech32 and codex32 errors
The codex32 test will more thoroughly exercise the algebra, since there we can correct up to 4 errors. The bech32 test on the other hand should work without an allocator (though to exercise this you need to manually edit fuzz/Cargo.toml to disable the alloc feature -- this is rust-lang/cargo#2980 which has been open for 10 years and counting..)
1 parent 9ef5b14 commit b5fdff3

3 files changed

Lines changed: 219 additions & 0 deletions

File tree

fuzz/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ members = ["."]
2121
name = "berlekamp_massey"
2222
path = "fuzz_targets/berlekamp_massey.rs"
2323

24+
[[bin]]
25+
name = "correct_bech32"
26+
path = "fuzz_targets/correct_bech32.rs"
27+
28+
[[bin]]
29+
name = "correct_codex32"
30+
path = "fuzz_targets/correct_codex32.rs"
31+
2432
[[bin]]
2533
name = "decode_rnd"
2634
path = "fuzz_targets/decode_rnd.rs"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use std::collections::HashMap;
2+
3+
use bech32::primitives::correction::CorrectableError as _;
4+
use bech32::primitives::decode::CheckedHrpstring;
5+
use bech32::{Bech32, Fe32};
6+
use honggfuzz::fuzz;
7+
8+
// coinbase output of block 862290
9+
static CORRECT: &[u8; 62] = b"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38";
10+
11+
fn do_test(data: &[u8]) {
12+
if data.is_empty() || data.len() % 2 == 1 {
13+
return;
14+
}
15+
16+
// Start with a correct string
17+
let mut hrpstring = *CORRECT;
18+
// ..then mangle it
19+
let mut errors = HashMap::with_capacity(data.len() / 2);
20+
for sl in data.chunks_exact(2) {
21+
let idx = usize::from(sl[0]);
22+
if idx >= CORRECT.len() - 3 {
23+
return;
24+
}
25+
let offs = match Fe32::try_from(sl[1]) {
26+
Ok(Fe32::Q) => return,
27+
Ok(fe) => fe,
28+
Err(_) => return,
29+
};
30+
31+
hrpstring[idx + 3] =
32+
(Fe32::from_char(hrpstring[idx + 3].into()).unwrap() + offs).to_char() as u8;
33+
if errors.insert(idx + 3, offs).is_some() {
34+
return;
35+
}
36+
}
37+
38+
let s = unsafe { core::str::from_utf8_unchecked(&hrpstring) };
39+
/*
40+
println!("{}", unsafe { core::str::from_utf8_unchecked(CORRECT) });
41+
println!("{}", s);
42+
*/
43+
let corrections = CheckedHrpstring::new::<Bech32>(s)
44+
.unwrap_err()
45+
.correction_context::<Bech32>()
46+
.unwrap()
47+
.bch_errors();
48+
49+
if errors.len() <= 4 {
50+
for (idx, fe) in corrections.unwrap() {
51+
let idx = s.len() - idx - 1;
52+
//println!("Errors: {:?}", errors);
53+
//println!("Remove: {} {}", idx, fe);
54+
assert_eq!(errors.remove(&idx), Some(fe));
55+
}
56+
assert_eq!(errors.len(), 0);
57+
}
58+
}
59+
60+
fn main() {
61+
loop {
62+
fuzz!(|data| {
63+
do_test(data);
64+
});
65+
}
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
71+
let mut b = 0;
72+
for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() {
73+
b <<= 4;
74+
match *c {
75+
b'A'..=b'F' => b |= c - b'A' + 10,
76+
b'a'..=b'f' => b |= c - b'a' + 10,
77+
b'0'..=b'9' => b |= c - b'0',
78+
_ => panic!("Bad hex"),
79+
}
80+
if (idx & 1) == 1 {
81+
out.push(b);
82+
b = 0;
83+
}
84+
}
85+
}
86+
87+
#[test]
88+
fn duplicate_crash() {
89+
let mut a = Vec::new();
90+
extend_vec_from_hex("", &mut a);
91+
super::do_test(&a);
92+
}
93+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use std::collections::HashMap;
2+
3+
use bech32::primitives::correction::CorrectableError as _;
4+
use bech32::primitives::decode::CheckedHrpstring;
5+
use bech32::{Checksum, Fe1024, Fe32};
6+
use honggfuzz::fuzz;
7+
8+
/// The codex32 checksum algorithm, defined in BIP-93.
9+
///
10+
/// Used in this fuzztest because it can correct up to 4 errors, vs bech32 which
11+
/// can correct only 1. Should exhibit more interesting behavior.
12+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13+
pub enum Codex32 {}
14+
15+
impl Checksum for Codex32 {
16+
type MidstateRepr = u128;
17+
type CorrectionField = Fe1024;
18+
const ROOT_GENERATOR: Self::CorrectionField = Fe1024::new([Fe32::_9, Fe32::_9]);
19+
const ROOT_EXPONENTS: core::ops::RangeInclusive<usize> = 9..=16;
20+
21+
const CHECKSUM_LENGTH: usize = 13;
22+
const CODE_LENGTH: usize = 93;
23+
// Copied from BIP-93
24+
const GENERATOR_SH: [u128; 5] = [
25+
0x19dc500ce73fde210,
26+
0x1bfae00def77fe529,
27+
0x1fbd920fffe7bee52,
28+
0x1739640bdeee3fdad,
29+
0x07729a039cfc75f5a,
30+
];
31+
const TARGET_RESIDUE: u128 = 0x10ce0795c2fd1e62a;
32+
}
33+
34+
static CORRECT: &[u8; 48] = b"ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw";
35+
36+
fn do_test(data: &[u8]) {
37+
if data.is_empty() || data.len() % 2 == 1 {
38+
return;
39+
}
40+
41+
// Start with a correct string
42+
let mut hrpstring = *CORRECT;
43+
// ..then mangle it
44+
let mut errors = HashMap::with_capacity(data.len() / 2);
45+
for sl in data.chunks_exact(2) {
46+
let idx = usize::from(sl[0]);
47+
if idx >= CORRECT.len() - 3 {
48+
return;
49+
}
50+
let offs = match Fe32::try_from(sl[1]) {
51+
Ok(Fe32::Q) => return,
52+
Ok(fe) => fe,
53+
Err(_) => return,
54+
};
55+
56+
hrpstring[idx + 3] =
57+
(Fe32::from_char(hrpstring[idx + 3].into()).unwrap() + offs).to_char() as u8;
58+
if errors.insert(idx + 3, offs).is_some() {
59+
return;
60+
}
61+
}
62+
63+
let s = unsafe { core::str::from_utf8_unchecked(&hrpstring) };
64+
/*
65+
println!("{}", unsafe { core::str::from_utf8_unchecked(CORRECT) });
66+
println!("{}", s);
67+
*/
68+
let corrections = CheckedHrpstring::new::<Codex32>(s)
69+
.unwrap_err()
70+
.correction_context::<Codex32>()
71+
.unwrap()
72+
.bch_errors();
73+
74+
if errors.len() <= 4 {
75+
for (idx, fe) in corrections.unwrap() {
76+
let idx = s.len() - idx - 1;
77+
//println!("Errors: {:?}", errors);
78+
//println!("Remove: {} {}", idx, fe);
79+
assert_eq!(errors.remove(&idx), Some(fe));
80+
}
81+
assert_eq!(errors.len(), 0);
82+
}
83+
}
84+
85+
fn main() {
86+
loop {
87+
fuzz!(|data| {
88+
do_test(data);
89+
});
90+
}
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
96+
let mut b = 0;
97+
for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() {
98+
b <<= 4;
99+
match *c {
100+
b'A'..=b'F' => b |= c - b'A' + 10,
101+
b'a'..=b'f' => b |= c - b'a' + 10,
102+
b'0'..=b'9' => b |= c - b'0',
103+
_ => panic!("Bad hex"),
104+
}
105+
if (idx & 1) == 1 {
106+
out.push(b);
107+
b = 0;
108+
}
109+
}
110+
}
111+
112+
#[test]
113+
fn duplicate_crash() {
114+
let mut a = Vec::new();
115+
extend_vec_from_hex("", &mut a);
116+
super::do_test(&a);
117+
}
118+
}

0 commit comments

Comments
 (0)