Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions corpus_generator/src/generators/beacon_vote.rs
Original file line number Diff line number Diff line change
@@ -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<ValidationResult, Box<dyn std::error::Error>> {
let critical_sizes: Vec<usize> = crate::common::SizeBoundaryUtils::get_critical_size_boundaries()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but not sure get_critical_size_boundaries is a descriptive name

.into_iter()
.map(|(size, _)| size)
.collect();

if critical_sizes.contains(&data.len()) {
return Ok(ValidationResult::Boundary);
}
Comment on lines +25 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we return this here and stop validation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was, when the input length matches a critical boundary size, we don’t mark it as valid or invalid right away. Instead, we label it as ValidationResult::Boundary and keep it for testing edge cases about size. By returning at this point, we stop normal validation. This way, the input isn’t thrown away or used up, but saved in the corpus as a special boundary case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by critical? Why (1_000, "small_message"), would be relevant for a BeaconVote?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By critical, I mean sizes that are interesting for testing, not normal BeaconVotes. We keep very small, very large, or exact boundary sizes to check if the decoder crashes or has off-by-one bugs. That’s why we return Boundary and stop normal validation. I followed the style of previous generators, but we can change it if you have a different idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this one

fn validate_corpus_entry(&self, data: &[u8]) -> Result<ValidationResult, Box<dyn std::error::Error>> {
and it doesn't seem to do that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I'm not sure we can generalize boundaries for different types. Wouldn't they be like type_size, type_size - 1, type_size + 1?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, the ssv_message generator does not use this boundary logic. I followed what I saw in some other generators, but maybe it is not the best fit here.


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<usize, Box<dyn std::error::Error>> {
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<usize, Box<dyn std::error::Error>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What usize means here? What kind of error could this function return if it's supposed to generate valid beacon votes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usize in the return type is the count of files generated (how many valid beacon votes were written to the corpus). I just follow the previous generator implementations because all of them return the same output (

fn generate_validator_key_boundaries(&self, output_dir: &Path) -> Result<usize, Box<dyn std::error::Error>> {
)

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<BeaconVote, Box<dyn std::error::Error>> {
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<usize, Box<dyn std::error::Error>> {
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<usize, Box<dyn std::error::Error>> {
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<usize, Box<dyn std::error::Error>> {
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)
}
}
4 changes: 3 additions & 1 deletion corpus_generator/src/generators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
pub use justification::JustificationValidationGenerator;
pub use beacon_vote::BeaconVoteGenerator;
3 changes: 2 additions & 1 deletion corpus_generator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
29 changes: 29 additions & 0 deletions diff_fuzzing/sfuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions diff_fuzzing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

// =============================================================================
Expand Down Expand Up @@ -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<u8>) {
// BeaconVote is relatively small: 32 (block_root) + 2*40 (checkpoints) = ~112 bytes
let mut out: Vec<u8> = 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)
}
7 changes: 7 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading