diff --git a/corpus_generator/src/generators/beacon_vote.rs b/corpus_generator/src/generators/beacon_vote.rs new file mode 100644 index 0000000..509200d --- /dev/null +++ b/corpus_generator/src/generators/beacon_vote.rs @@ -0,0 +1,245 @@ +use std::path::Path; +use rand::{Rng, rngs::StdRng}; +use ssz::{Encode, Decode}; +use types::{Hash256, Checkpoint}; +use ssv_types::consensus::BeaconVote; +use crate::{CorpusGenerator, ValidationResult, CorpusUtils}; + +pub struct BeaconVoteGenerator; + +impl CorpusGenerator for BeaconVoteGenerator { + fn name(&self) -> &'static str { + "diff_fuzz_beacon_vote_decode_encode" + } + + fn description(&self) -> &'static str { + "BeaconVote - Beacon vote data encoding/decoding" + } + + fn validate_corpus_entry(&self, data: &[u8]) -> Result> { + let critical_sizes: Vec = crate::common::SizeBoundaryUtils::get_critical_size_boundaries() + .into_iter() + .map(|(size, _)| size) + .collect(); + + if critical_sizes.contains(&data.len()) { + return Ok(ValidationResult::Boundary); + } + + if data.len() != 112 { + return Ok(ValidationResult::Invalid); + } + + match BeaconVote::from_ssz_bytes(data) { + Ok(_) => Ok(ValidationResult::Valid), + Err(_) => Ok(ValidationResult::Invalid), + } + } + + fn generate(&self, output_dir: &Path, rng: &mut StdRng) -> Result> { + let mut count = 0; + + count += self.generate_valid_beacon_votes(output_dir, rng)?; + count += self.generate_malformed_beacon_votes(output_dir, rng)?; + count += self.generate_size_boundary_cases(output_dir, rng)?; + count += self.generate_ssz_structure_edges(output_dir, rng)?; + + Ok(count) + } +} + +impl BeaconVoteGenerator { + fn generate_valid_beacon_votes(&self, output_dir: &Path, rng: &mut StdRng) -> Result> { + let mut count = 0; + let vote_cases = vec![ + ("standard_vote", (10, 11)), + ("near_epoch_vote", (15, 16)), + ("far_epoch_vote", (5, 20)), + ("zero_epoch_vote", (0, 1)), + ("large_epoch_vote", (1000, 1001)), + ]; + + for (name, (source_epoch, target_epoch)) in vote_cases { + let vote = self.create_valid_beacon_vote(rng, source_epoch, target_epoch)?; + let bytes = vote.as_ssz_bytes(); + CorpusUtils::save_corpus_file(output_dir, &format!("beacon_vote_valid_{}", name), &bytes)?; + count += 1; + } + + Ok(count) + } + + fn create_valid_beacon_vote(&self, rng: &mut StdRng, source_epoch: u64, target_epoch: u64) -> Result> { + Ok(BeaconVote { + block_root: Hash256::new(rng.gen::<[u8; 32]>()), + source: Checkpoint { + epoch: types::Epoch::new(source_epoch), + root: Hash256::new(rng.gen::<[u8; 32]>()), + }, + target: Checkpoint { + epoch: types::Epoch::new(target_epoch), + root: Hash256::new(rng.gen::<[u8; 32]>()), + }, + }) + } + + fn generate_malformed_beacon_votes( + &self, + output_dir: &Path, + rng: &mut StdRng, + ) -> Result> { + let mut count = 0; + let base_vote = self.create_valid_beacon_vote(rng, 10, 11)?; + + // Precompute all malformed votes + let invalid_epoch_order = { + let mut vote = base_vote.clone(); + vote.source.epoch = types::Epoch::new(11); + vote.target.epoch = types::Epoch::new(10); + vote.as_ssz_bytes() + }; + + let zero_block_root = { + let mut vote = base_vote.clone(); + vote.block_root = Hash256::repeat_byte(0); + vote.as_ssz_bytes() + }; + + let zero_source_root = { + let mut vote = base_vote.clone(); + vote.source.root = Hash256::repeat_byte(0); + vote.as_ssz_bytes() + }; + + let zero_target_root = { + let mut vote = base_vote.clone(); + vote.target.root = Hash256::repeat_byte(0); + vote.as_ssz_bytes() + }; + + let all_ones_block_root = { + let mut vote = base_vote.clone(); + vote.block_root = Hash256::repeat_byte(0xFF); + vote.as_ssz_bytes() + }; + + let extreme_epoch = { + let mut vote = base_vote.clone(); + vote.source.epoch = types::Epoch::new(u64::MAX); + vote.as_ssz_bytes() + }; + + let random_corruption = { + let mut vote = base_vote.clone(); + let mut random_bytes = rng.gen::<[u8; 32]>(); + random_bytes[0] = 0xFF; + vote.block_root = Hash256::new(random_bytes); + vote.as_ssz_bytes() + }; + + // Vector of (name, data) + let malformed_cases = vec![ + ("invalid_epoch_order", invalid_epoch_order), + ("zero_block_root", zero_block_root), + ("zero_source_root", zero_source_root), + ("zero_target_root", zero_target_root), + ("all_ones_block_root", all_ones_block_root), + ("extreme_epoch", extreme_epoch), + ("random_corruption", random_corruption), + ]; + + for (name, bytes) in malformed_cases { + CorpusUtils::save_corpus_file(output_dir, &format!("beacon_vote_malformed_{}", name), &bytes)?; + count += 1; + } + + Ok(count) + } + + + fn generate_size_boundary_cases(&self, output_dir: &Path, rng: &mut StdRng) -> Result> { + let mut count = 0; + let boundaries = crate::common::SizeBoundaryUtils::get_critical_size_boundaries(); + + for (size, name) in boundaries { + let data = if size < 32 { + CorpusUtils::generate_random_data(size, rng) + } else { + let vote = self.create_valid_beacon_vote(rng, 10, 11)?; + let mut bytes = vote.as_ssz_bytes(); + while bytes.len() < size { + bytes.push(rng.gen()); + } + bytes.truncate(size); + bytes + }; + + CorpusUtils::save_corpus_file(output_dir, &format!("beacon_vote_boundary_{}_{}", name, size), &data)?; + count += 1; + } + + Ok(count) + } + + fn generate_ssz_structure_edges(&self, output_dir: &Path, rng: &mut StdRng) -> Result> { + let mut count = 0; + let base_vote = self.create_valid_beacon_vote(rng, 10, 11)?; + let base_bytes = base_vote.as_ssz_bytes(); + + // Precompute all edge cases to avoid macro errors + let invalid_offsets = { + let mut bytes = base_bytes.clone(); + if bytes.len() < 4 { bytes.resize(4, 0); } + bytes[0..4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); + bytes + }; + + let truncated_data = { + let len = base_bytes.len().min(100); + base_bytes[..len].to_vec() + }; + + let wrong_length_indicator = { + let mut bytes = base_bytes.clone(); + if bytes.len() < 4 { bytes.resize(4, 0); } + bytes[0..4].copy_from_slice(&0x0000_0000u32.to_le_bytes()); + bytes + }; + + let partial_field_corruption = { + let mut bytes = base_bytes.clone(); + if bytes.len() < 32 { bytes.resize(32, 0); } + bytes[16..32].fill(0xAA); + bytes + }; + + let oversized_data = { + let mut bytes = base_bytes.clone(); + bytes.extend_from_slice(&[0u8; 1024]); + bytes + }; + + let randomized_field = { + let mut bytes = base_bytes.clone(); + if bytes.len() < 32 { bytes.resize(32, 0); } + bytes[0..32].copy_from_slice(&rng.gen::<[u8; 32]>()); + bytes + }; + + let edge_cases = vec![ + ("invalid_offsets", invalid_offsets), + ("truncated_data", truncated_data), + ("wrong_length_indicator", wrong_length_indicator), + ("partial_field_corruption", partial_field_corruption), + ("oversized_data", oversized_data), + ("randomized_field", randomized_field), + ]; + + for (name, data) in edge_cases { + CorpusUtils::save_corpus_file(output_dir, &format!("beacon_vote_ssz_{}", name), &data)?; + count += 1; + } + + Ok(count) + } +} diff --git a/corpus_generator/src/generators/mod.rs b/corpus_generator/src/generators/mod.rs index 2977e20..8690f14 100644 --- a/corpus_generator/src/generators/mod.rs +++ b/corpus_generator/src/generators/mod.rs @@ -12,6 +12,7 @@ pub mod committee_quorum; pub mod message_validation_state; pub mod round_change; pub mod justification; +pub mod beacon_vote; // Re-export all generators pub use ssv_message::SSVMessageEncodeDecodeGenerator; @@ -24,4 +25,5 @@ pub use partial_signature::PartialSignatureValidationGenerator; pub use committee_quorum::CommitteeQuorumCalculationGenerator; pub use message_validation_state::MessageValidationStateGenerator; pub use round_change::RoundChangeHandlingGenerator; -pub use justification::JustificationValidationGenerator; \ No newline at end of file +pub use justification::JustificationValidationGenerator; +pub use beacon_vote::BeaconVoteGenerator; diff --git a/corpus_generator/src/main.rs b/corpus_generator/src/main.rs index 163426b..929a0e2 100644 --- a/corpus_generator/src/main.rs +++ b/corpus_generator/src/main.rs @@ -76,7 +76,8 @@ impl CorpusGeneratorRegistry { registry.register(Box::new(MessageValidationStateGenerator)); registry.register(Box::new(RoundChangeHandlingGenerator)); registry.register(Box::new(JustificationValidationGenerator)); - + registry.register(Box::new(BeaconVoteGenerator)); + registry } diff --git a/diff_fuzzing/sfuzz.go b/diff_fuzzing/sfuzz.go index a54dca6..282c038 100644 --- a/diff_fuzzing/sfuzz.go +++ b/diff_fuzzing/sfuzz.go @@ -907,3 +907,32 @@ func justification_validation(data_ptr unsafe.Pointer, data_size int, out_ptr un return 1 } + +//export beacon_vote_decode_encode_ffi +func beacon_vote_decode_encode_ffi(data_ptr unsafe.Pointer, data_size int, out_ptr unsafe.Pointer, out_size int) int { + // Convert C pointer to Go byte slice for input data + data := unsafe.Slice((*byte)(data_ptr), data_size) + + // Decode into BeaconVote + beacon_vote := new(spectypes.BeaconVote) + err := beacon_vote.UnmarshalSSZ(data) + if err != nil { + return 0 // Decode failed + } + + // Encode the beacon vote + result, err := beacon_vote.MarshalSSZ() + if err != nil { + return -1 // Encode failed after successful decode + } + + // Check if output buffer is large enough + out := unsafe.Slice((*byte)(out_ptr), out_size) + if len(result) > out_size { + return -2 // Output buffer too small + } + + // Copy encoded result to output buffer + copy(out, result) + return 1 // Success +} diff --git a/diff_fuzzing/src/lib.rs b/diff_fuzzing/src/lib.rs index 635e23e..0e79596 100644 --- a/diff_fuzzing/src/lib.rs +++ b/diff_fuzzing/src/lib.rs @@ -123,6 +123,13 @@ unsafe extern "C" { data_size: usize, committee_size: u64, ) -> isize; + + fn beacon_vote_decode_encode_ffi( + data_ptr: *const u8, + data_size: usize, + out_ptr: *mut u8, + out_size: usize, + ) -> isize; } // ============================================================================= @@ -391,3 +398,18 @@ pub fn complete_message_validation_go_adapter(data: &[u8], committee_size: u64) // Complete message validation uses real Validator instances to ensure // we're testing the actual implementations, not duplicated logic + +// FFI wrapper for Go beacon vote decode/encode +pub fn beacon_vote_decode_encode(data: &[u8]) -> (isize, Vec) { + // BeaconVote is relatively small: 32 (block_root) + 2*40 (checkpoints) = ~112 bytes + let mut out: Vec = vec![0_u8; data.len().max(256)]; + + // Prepare FFI call params + let data_ptr: *const u8 = data.as_ptr(); + let data_size: usize = data.len(); + let out_ptr: *mut u8 = out.as_mut_ptr(); + let out_size = out.len(); + + let success = unsafe { beacon_vote_decode_encode_ffi(data_ptr, data_size, out_ptr, out_size) }; + (success, out) +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 39131ef..aec8465 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -147,6 +147,13 @@ test = false doc = false bench = false +[[bin]] +name = "diff_fuzz_beacon_vote_decode_encode" +path = "fuzz_targets/differential/diff_fuzz_beacon_vote_decode_encode.rs" +test = false +doc = false +bench = false + # Medium Priority Targets - Validator/Committee Issues 🟠 [[bin]] name = "diff_fuzz_partial_signature_validation" diff --git a/fuzz/fuzz_targets/differential/diff_fuzz_beacon_vote_decode_encode.rs b/fuzz/fuzz_targets/differential/diff_fuzz_beacon_vote_decode_encode.rs new file mode 100644 index 0000000..30724d8 --- /dev/null +++ b/fuzz/fuzz_targets/differential/diff_fuzz_beacon_vote_decode_encode.rs @@ -0,0 +1,55 @@ +#[macro_use] +extern crate afl; +use ssv_types::consensus::BeaconVote; +use ssz::{Decode, DecodeError, Encode}; +use diff_fuzzing::beacon_vote_decode_encode; + +fn main() { + fuzz!(|data: &[u8]| { + // Rust decode + let rust_decode_res = BeaconVote::from_ssz_bytes(data); + // Go decode and encode + let (go_success, mut go_vec) = beacon_vote_decode_encode(data); + + // If Go fails with buffer too small, skip + if go_success == -2 { + return; + } + + match rust_decode_res { + Ok(msg) => { + // Encode the successfully decoded Rust message + let encoded_msg = msg.as_ssz_bytes(); + + // Check if Go succeeded + if go_success == 1 { + // Ensure lengths match + go_vec.truncate(encoded_msg.len()); + assert_eq!( + encoded_msg.len(), + go_vec.len(), + "Length mismatch: Rust: {} vs Go: {}", + encoded_msg.len(), + go_vec.len() + ); + // Check if encoded bytes match + assert_eq!(encoded_msg, go_vec, "Encoded bytes mismatch"); + } else { + // Go failed but Rust succeeded + assert_eq!(go_success, 1, "Rust succeeds and Go fails"); + } + } + Err(ref e) => { + // Handle cases where Rust fails due to invalid list length + if matches!(e, DecodeError::InvalidListFixedBytesLen { .. }) && go_success == 1 { + // Go may succeed but would fail validation (e.g., list too big) + return; + } + // If Go succeeds but Rust fails, panic + if go_success == 1 { + panic!("Go succeeded but Rust failed with: {:?}", e); + } + } + } + }); +} diff --git a/run_fuzzer.sh b/run_fuzzer.sh index ea36e2a..607fae2 100755 --- a/run_fuzzer.sh +++ b/run_fuzzer.sh @@ -40,6 +40,7 @@ HIGH_PRIORITY_DIFFERENTIAL=( "diff_fuzz_ssv_message_encode_decode" "diff_fuzz_complete_message_validation" "diff_fuzz_message_id_generation" + "diff_fuzz_beacon_vote_decode_encode" ) # Medium Priority Targets (Validator/Committee Issues 🟠) @@ -68,6 +69,7 @@ declare -A DIFFERENTIAL_DESCRIPTIONS=( ["diff_fuzz_ssv_message_encode_decode"]="Tests SSVMessage serialization differences that could cause network splits" ["diff_fuzz_complete_message_validation"]="Tests complete message validation pipeline using REAL Validator instances from both implementations" ["diff_fuzz_message_id_generation"]="Tests MessageID construction differences in P2P message routing" + ["diff_fuzz_beacon_vote_decode_encode"]="Compares BeaconVote SSZ encoding/decoding between Go and Rust implementations" # Medium Priority (Validator/Committee Issues) ["diff_fuzz_signed_ssv_msg_decode_encode"]="Compares basic signed message encoding between implementations"