Skip to content

Commit

Permalink
primitives: introduce the Berlekamp-Massey algorithm for computing li…
Browse files Browse the repository at this point in the history
…near shift registers

This provides a general-purpose implementation of the Berlekamp-Massey
algorithm for finding a linear shift register that generates a given
sequence prefix.

If compiled without an allocator, it will run less efficiently (and be
limited to a maximum size) but it will work.

Also introduces a fuzz test to check that it works properly and does not
crash.
  • Loading branch information
apoelstra committed Sep 30, 2024
1 parent fc903d6 commit 6c24f98
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
fuzz_target: [decode_rnd, encode_decode, parse_hrp]
fuzz_target: [berlekamp_massey, decode_rnd, encode_decode, parse_hrp]
steps:
- name: Install test dependencies
run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev
Expand Down
4 changes: 4 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ bech32 = { path = ".." }
[workspace]
members = ["."]

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

[[bin]]
name = "decode_rnd"
path = "fuzz_targets/decode_rnd.rs"
Expand Down
58 changes: 58 additions & 0 deletions fuzz/fuzz_targets/berlekamp_massey.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use bech32::primitives::LfsrIter;
use bech32::Fe32;
use honggfuzz::fuzz;

fn do_test(data: &[u8]) {
for ch in data {
if *ch >= 32 {
return;
}
}
if data.is_empty() || data.len() > 1_000 {
return;
}

let mut iv = Vec::with_capacity(data.len());
for ch in data {
iv.push(Fe32::try_from(*ch).unwrap());
}

for (i, d) in LfsrIter::berlekamp_massey(&iv).take(data.len()).enumerate() {
assert_eq!(data[i], d.to_u8());
}
}

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("00", &mut a);
super::do_test(&a);
}
}
90 changes: 90 additions & 0 deletions src/primitives/fieldvec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ impl<F> FieldVec<F> {
#[inline]
pub fn is_empty(&self) -> bool { self.len == 0 }

/// Reverses the contents of the vector in-place.
pub fn reverse(&mut self) {
self.assert_has_data();

#[cfg(not(feature = "alloc"))]
{
self.inner_a[..self.len].reverse();
}

#[cfg(feature = "alloc")]
if self.len > NO_ALLOC_MAX_LENGTH {
self.inner_v.reverse();
} else {
self.inner_a[..self.len].reverse();
}
}

/// Returns an immutable iterator over the elements in the vector.
///
/// # Panics
Expand Down Expand Up @@ -186,7 +203,48 @@ impl<F: Field> FieldVec<F> {
}
}

impl<F: Default> Default for FieldVec<F> {
fn default() -> Self { Self::new() }
}

impl<F: Default> FieldVec<F> {
/// Constructs a new empty field vector.
pub fn new() -> Self {
FieldVec {
inner_a: Default::default(),
len: 0,
#[cfg(feature = "alloc")]
inner_v: Vec::new(),
}
}

/// Constructs a new field vector with the given capacity.
pub fn with_capacity(cap: usize) -> Self {
#[cfg(not(feature = "alloc"))]
{
let mut ret = Self::new();
ret.len = cap;
ret.assert_has_data();
ret.len = 0;
ret
}

#[cfg(feature = "alloc")]
if cap > NO_ALLOC_MAX_LENGTH {
let mut ret = Self::new();
ret.inner_v = Vec::with_capacity(cap);
ret
} else {
Self::new()
}
}

/// Pushes an item onto the end of the vector.
///
/// Synonym for [`Self::push`] used to simplify code where a
/// [`FieldVec`] is used in place of a `VecDeque`.
pub fn push_back(&mut self, item: F) { self.push(item) }

/// Pushes an item onto the end of the vector.
///
/// # Panics
Expand All @@ -213,6 +271,38 @@ impl<F: Default> FieldVec<F> {
}
}

/// Pops an item off the front of the vector.
///
/// This operation is always O(n).
pub fn pop_front(&mut self) -> Option<F> {
self.assert_has_data();
if self.len == 0 {
return None;
}

#[cfg(not(feature = "alloc"))]
{
// Not the most efficient algorithm, but it is safe code,
// easily seen to be correct, and is only used with very
// small vectors.
self.reverse();
let ret = self.pop();
self.reverse();
ret
}

#[cfg(feature = "alloc")]
if self.len > NO_ALLOC_MAX_LENGTH + 1 {
self.len -= 1;
Some(self.inner_v.remove(0))
} else {
self.reverse();
let ret = self.pop();
self.reverse();
ret
}
}

/// Pops an item off the end of the vector.
///
/// # Panics
Expand Down
Loading

0 comments on commit 6c24f98

Please sign in to comment.