Skip to content

Commit

Permalink
fuzz: add fuzztests that try to correct bech32 and codex32 errors
Browse files Browse the repository at this point in the history
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..)
  • Loading branch information
apoelstra committed Sep 30, 2024
1 parent 9ef5b14 commit b5fdff3
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 0 deletions.
8 changes: 8 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ members = ["."]
name = "berlekamp_massey"
path = "fuzz_targets/berlekamp_massey.rs"

[[bin]]
name = "correct_bech32"
path = "fuzz_targets/correct_bech32.rs"

[[bin]]
name = "correct_codex32"
path = "fuzz_targets/correct_codex32.rs"

[[bin]]
name = "decode_rnd"
path = "fuzz_targets/decode_rnd.rs"
Expand Down
93 changes: 93 additions & 0 deletions fuzz/fuzz_targets/correct_bech32.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use std::collections::HashMap;

use bech32::primitives::correction::CorrectableError as _;
use bech32::primitives::decode::CheckedHrpstring;
use bech32::{Bech32, Fe32};
use honggfuzz::fuzz;

// coinbase output of block 862290
static CORRECT: &[u8; 62] = b"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38";

fn do_test(data: &[u8]) {
if data.is_empty() || data.len() % 2 == 1 {
return;
}

// Start with a correct string
let mut hrpstring = *CORRECT;
// ..then mangle it
let mut errors = HashMap::with_capacity(data.len() / 2);
for sl in data.chunks_exact(2) {
let idx = usize::from(sl[0]);
if idx >= CORRECT.len() - 3 {
return;
}
let offs = match Fe32::try_from(sl[1]) {
Ok(Fe32::Q) => return,
Ok(fe) => fe,
Err(_) => return,
};

hrpstring[idx + 3] =
(Fe32::from_char(hrpstring[idx + 3].into()).unwrap() + offs).to_char() as u8;
if errors.insert(idx + 3, offs).is_some() {
return;
}
}

let s = unsafe { core::str::from_utf8_unchecked(&hrpstring) };
/*
println!("{}", unsafe { core::str::from_utf8_unchecked(CORRECT) });
println!("{}", s);
*/
let corrections = CheckedHrpstring::new::<Bech32>(s)
.unwrap_err()
.correction_context::<Bech32>()
.unwrap()
.bch_errors();

if errors.len() <= 4 {
for (idx, fe) in corrections.unwrap() {
let idx = s.len() - idx - 1;
//println!("Errors: {:?}", errors);
//println!("Remove: {} {}", idx, fe);
assert_eq!(errors.remove(&idx), Some(fe));
}
assert_eq!(errors.len(), 0);
}
}

fn main() {
loop {
fuzz!(|data| {
do_test(data);
});
}
}

#[cfg(test)]
mod tests {
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
let mut b = 0;
for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() {
b <<= 4;
match *c {
b'A'..=b'F' => b |= c - b'A' + 10,
b'a'..=b'f' => b |= c - b'a' + 10,
b'0'..=b'9' => b |= c - b'0',
_ => panic!("Bad hex"),
}
if (idx & 1) == 1 {
out.push(b);
b = 0;
}
}
}

#[test]
fn duplicate_crash() {
let mut a = Vec::new();
extend_vec_from_hex("", &mut a);
super::do_test(&a);
}
}
118 changes: 118 additions & 0 deletions fuzz/fuzz_targets/correct_codex32.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::collections::HashMap;

use bech32::primitives::correction::CorrectableError as _;
use bech32::primitives::decode::CheckedHrpstring;
use bech32::{Checksum, Fe1024, Fe32};
use honggfuzz::fuzz;

/// The codex32 checksum algorithm, defined in BIP-93.
///
/// Used in this fuzztest because it can correct up to 4 errors, vs bech32 which
/// can correct only 1. Should exhibit more interesting behavior.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Codex32 {}

impl Checksum for Codex32 {
type MidstateRepr = u128;
type CorrectionField = Fe1024;
const ROOT_GENERATOR: Self::CorrectionField = Fe1024::new([Fe32::_9, Fe32::_9]);
const ROOT_EXPONENTS: core::ops::RangeInclusive<usize> = 9..=16;

const CHECKSUM_LENGTH: usize = 13;
const CODE_LENGTH: usize = 93;
// Copied from BIP-93
const GENERATOR_SH: [u128; 5] = [
0x19dc500ce73fde210,
0x1bfae00def77fe529,
0x1fbd920fffe7bee52,
0x1739640bdeee3fdad,
0x07729a039cfc75f5a,
];
const TARGET_RESIDUE: u128 = 0x10ce0795c2fd1e62a;
}

static CORRECT: &[u8; 48] = b"ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw";

fn do_test(data: &[u8]) {
if data.is_empty() || data.len() % 2 == 1 {
return;
}

// Start with a correct string
let mut hrpstring = *CORRECT;
// ..then mangle it
let mut errors = HashMap::with_capacity(data.len() / 2);
for sl in data.chunks_exact(2) {
let idx = usize::from(sl[0]);
if idx >= CORRECT.len() - 3 {
return;
}
let offs = match Fe32::try_from(sl[1]) {
Ok(Fe32::Q) => return,
Ok(fe) => fe,
Err(_) => return,
};

hrpstring[idx + 3] =
(Fe32::from_char(hrpstring[idx + 3].into()).unwrap() + offs).to_char() as u8;
if errors.insert(idx + 3, offs).is_some() {
return;
}
}

let s = unsafe { core::str::from_utf8_unchecked(&hrpstring) };
/*
println!("{}", unsafe { core::str::from_utf8_unchecked(CORRECT) });
println!("{}", s);
*/
let corrections = CheckedHrpstring::new::<Codex32>(s)
.unwrap_err()
.correction_context::<Codex32>()
.unwrap()
.bch_errors();

if errors.len() <= 4 {
for (idx, fe) in corrections.unwrap() {
let idx = s.len() - idx - 1;
//println!("Errors: {:?}", errors);
//println!("Remove: {} {}", idx, fe);
assert_eq!(errors.remove(&idx), Some(fe));
}
assert_eq!(errors.len(), 0);
}
}

fn main() {
loop {
fuzz!(|data| {
do_test(data);
});
}
}

#[cfg(test)]
mod tests {
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
let mut b = 0;
for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() {
b <<= 4;
match *c {
b'A'..=b'F' => b |= c - b'A' + 10,
b'a'..=b'f' => b |= c - b'a' + 10,
b'0'..=b'9' => b |= c - b'0',
_ => panic!("Bad hex"),
}
if (idx & 1) == 1 {
out.push(b);
b = 0;
}
}
}

#[test]
fn duplicate_crash() {
let mut a = Vec::new();
extend_vec_from_hex("", &mut a);
super::do_test(&a);
}
}

0 comments on commit b5fdff3

Please sign in to comment.