//! Fuzzing utilities for [Hasher] implementations. //! //! For any hasher, the one-shot [Hasher::hash], [Hasher::hash_across], //! [Hasher::hash_pair], and [Hasher::hash_many] entrypoints must agree with //! streaming the same bytes through [Hasher::update]. Implementations are free //! to specialize the one-shot entrypoints for fixed shapes (e.g. with assembly //! kernels), so the inputs generated here are biased toward the shapes and //! lengths those specializations match on. use crate::Hasher; use arbitrary::{Arbitrary, Unstructured}; use commonware_parallel::{Sequential, Strategy}; use core::{fmt::Debug, marker::PhantomData}; /// Pick a contiguous message length biased toward the boundaries of /// SHA-256's specialized paths (the pair kernels at 64 and 72 bytes and the /// two-block fixed path limit at 119 bytes), which are harmless biases for /// other hashers. fn arbitrary_len(u: &mut Unstructured<'_>) -> arbitrary::Result { Ok(match u.int_in_range(0..=6)? { 0 => 55, 1 => 64, 2 => 72, 3 => 119, 4 => 120, 5 => 1024, _ => u.int_in_range(0..=1024)?, }) } /// Pick a batch message length while keeping the complete plan bounded. fn arbitrary_batch_len(u: &mut Unstructured<'_>) -> arbitrary::Result { Ok(match u.int_in_range(0..=13)? { 0 => 0, 1 => 55, 2 => 56, 3 => 63, 4 => 64, 5 => 65, 6 => 72, 7 => 119, 8 => 120, 9 => 127, 10 => 128, 11 => 129, 12 => 256, _ => u.int_in_range(0..=256)?, }) } /// Generate a message as parts: either one of the fixed merkle shapes that /// implementations specialize (e.g. position || left || right), or a /// length-biased contiguous message split at an arbitrary point. fn arbitrary_message(u: &mut Unstructured<'_>) -> arbitrary::Result>> { fn part(u: &mut Unstructured<'_>, len: usize) -> arbitrary::Result> { Ok(u.bytes(len)?.to_vec()) } match u.int_in_range(0..=4)? { 0 => Ok(vec![part(u, 8)?, part(u, 32)?, part(u, 32)?]), 1 => Ok(vec![part(u, 32)?, part(u, 32)?]), 2 => Ok(vec![part(u, 8)?, part(u, 32)?]), 3 => Ok(vec![part(u, 4)?, part(u, 32)?]), _ => { let len = arbitrary_len(u)?; let split = u.int_in_range(0..=len)?; let data = part(u, len)?; Ok(vec![data[..split].to_vec(), data[split..].to_vec()]) } } } /// A pair of multi-part messages to hash through every [Hasher] entrypoint. pub struct Plan { left: Vec>, right: Vec>, _hasher: PhantomData, } impl Debug for Plan { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Plan") .field("left", &self.left) .field("right", &self.right) .finish() } } impl Arbitrary<'_> for Plan { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { Ok(Self { left: arbitrary_message(u)?, right: arbitrary_message(u)?, _hasher: PhantomData, }) } } impl Plan { /// Construct a plan for two fixed messages, each given as parts. /// /// Use this to guarantee coverage of a specific shape (e.g. one an /// implementation specializes) independent of what the fuzz generators /// happen to sample. pub const fn new(left: Vec>, right: Vec>) -> Self { Self { left, right, _hasher: PhantomData, } } /// Check that every entrypoint agrees with a single [Hasher::update] /// over the concatenated message. pub fn run(self) { let left: Vec<&[u8]> = self.left.iter().map(Vec::as_slice).collect(); let right: Vec<&[u8]> = self.right.iter().map(Vec::as_slice).collect(); let reference = |parts: &[&[u8]]| { let mut hasher = H::default(); hasher.update(&parts.concat()); hasher.finalize().1 }; let expected_left = reference(&left); let expected_right = reference(&right); // Stream each part separately, reusing the reset hasher returned by // finalize for the second message. let mut hasher = H::default(); for part in &left { hasher.update(part); } let (mut hasher, streamed_left) = hasher.finalize(); for part in &right { hasher.update(part); } let (_, streamed_right) = hasher.finalize(); assert_eq!(streamed_left, expected_left); assert_eq!(streamed_right, expected_right); assert_eq!(H::hash(&left), expected_left); assert_eq!(H::hash(&right), expected_right); assert_eq!(H::hash_across(&left, &Sequential), expected_left); assert_eq!(H::hash_across(&right, &Sequential), expected_right); let (left_digest, right_digest) = H::hash_pair(&left, &right, &Sequential); assert_eq!(left_digest, expected_left); assert_eq!(right_digest, expected_right); } } /// Contiguous messages to hash through [Hasher::hash_many]. pub struct BatchPlan { messages: Vec>, _hasher: PhantomData, } impl Debug for BatchPlan { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("BatchPlan") .field("messages", &self.messages) .finish() } } impl Arbitrary<'_> for BatchPlan { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let count = u.int_in_range(0..=40)?; let equal_lengths = u.arbitrary::()?; let common_len = arbitrary_batch_len(u)?; let mut messages = Vec::with_capacity(count); for lane in 0..count { let len = if equal_lengths { common_len } else { arbitrary_batch_len(u)? }; let mut message = u.bytes(len)?.to_vec(); if let Some(first) = message.first_mut() { *first = lane as u8; } messages.push(message); } Ok(Self { messages, _hasher: PhantomData, }) } } impl BatchPlan { /// Check that batch output positions agree with independent streaming /// hashes, both on the calling thread and across `strategy`. pub fn run(self, strategy: &impl Strategy) { let expected = self .messages .iter() .map(|message| { let mut hasher = H::default(); hasher.update(message); hasher.finalize().1 }) .collect::>(); assert_eq!(H::hash_many(&self.messages, &Sequential), expected); assert_eq!(H::hash_many(&self.messages, strategy), expected); } } #[cfg(test)] mod tests { use super::*; use crate::{Blake3, Sha256}; use commonware_invariants::minifuzz; use commonware_parallel::mocks::explore; use std::sync::Arc; fn test_fuzz() { // The generators below always emit at least one part, so pin the // zero-parts one-shot to the empty-message digest separately. Plan::::new(vec![], vec![]).run(); minifuzz::Builder::default() .with_seed(0) .with_search_limit(512) .test(|u| { u.arbitrary::>()?.run(); Ok(()) }); } fn test_fuzz_hash_many() { explore(|strategy| { let mut saw_empty_batch = false; let mut saw_equal_lengths = false; let mut saw_partial_batch = false; let mut saw_equal_full_blocks = false; let mut saw_equal_two_block_padding = false; let mut saw_unequal_lengths = false; minifuzz::Builder::default() .with_seed(0) .with_search_limit(512) .test(|u| { let plan = u.arbitrary::>()?; let first_len = plan.messages.first().map_or(0, Vec::len); let equal_lengths = plan .messages .iter() .all(|message| message.len() == first_len); for (lane, message) in plan.messages.iter().enumerate() { if let Some(first) = message.first() { assert_eq!(*first, lane as u8); } } saw_empty_batch |= plan.messages.is_empty(); saw_equal_lengths |= equal_lengths && plan.messages.len() >= 16; saw_partial_batch |= equal_lengths && (7..16).contains(&plan.messages.len()); saw_equal_full_blocks |= equal_lengths && plan.messages.len() >= 16 && first_len >= 64; saw_equal_two_block_padding |= equal_lengths && plan.messages.len() >= 16 && first_len % 64 >= 56; saw_unequal_lengths |= !equal_lengths; plan.run(strategy); Ok(()) }); assert!(saw_empty_batch); assert!(saw_equal_lengths); assert!(saw_partial_batch); assert!(saw_equal_full_blocks); assert!(saw_equal_two_block_padding); assert!(saw_unequal_lengths); }); } #[test] fn test_fuzz_sha256() { test_fuzz::(); } #[test] fn test_fuzz_hash_many_sha256() { test_fuzz_hash_many::(); } #[test] fn test_hash_many_default_matches_individual_hashes() { let messages = (0..33) .map(|lane| Arc::<[u8]>::from(vec![lane as u8; lane])) .collect::>(); let expected = messages .iter() .map(|message| { let mut hasher = Blake3::default(); hasher.update(message); hasher.finalize().1 }) .collect::>(); for count in 0..=messages.len() { let messages = &messages[..count]; assert_eq!( explore(|strategy| Blake3::hash_many(messages, strategy)), expected[..count] ); } } #[test] fn test_fuzz_blake3() { test_fuzz::(); } #[cfg(feature = "std")] #[test] fn test_fuzz_crc32() { test_fuzz::(); } }