diff --git a/Cargo.toml b/Cargo.toml index 1e152c0..404b047 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,18 @@ with-gen-benches-poseidon = [] name = "benchmark" harness = false +[[test]] +name = "inc_encoding_target_sum" +path = "tests/inc_encoding/target_sum.rs" + +[[test]] +name = "shake_to_field" +path = "tests/symmetric/prf/shake_to_field.rs" + +[[test]] +name = "tweak_hash" +path = "tests/symmetric/tweak_hash.rs" + [profile.profiling] inherits = "release" debug = true \ No newline at end of file diff --git a/src/array.rs b/src/array.rs index 0ef0b3a..208b060 100644 --- a/src/array.rs +++ b/src/array.rs @@ -148,331 +148,3 @@ impl<'de, const N: usize> Deserialize<'de> for FieldArray { deserializer.deserialize_tuple(N, FieldArrayVisitor::) } } - -#[cfg(test)] -mod tests { - use super::*; - use proptest::prelude::*; - use rand::RngExt; - - /// Small parameter arrays - const SMALL_SIZE: usize = 5; - /// Hash output size - const MEDIUM_SIZE: usize = 7; - /// Larger parameter arrays - const LARGE_SIZE: usize = 44; - - #[test] - fn test_ssz_roundtrip_zero_values() { - // Start with an array of zeros - let original = FieldArray([F::ZERO; SMALL_SIZE]); - - // Encode to bytes using SSZ - let encoded = original.as_ssz_bytes(); - - // Decode back from bytes - let decoded = FieldArray::::from_ssz_bytes(&encoded) - .expect("Failed to decode valid SSZ bytes"); - - // Verify round-trip preserves the value - assert_eq!(original, decoded, "Round-trip failed for zero values"); - } - - #[test] - fn test_ssz_roundtrip_max_values() { - // Create array with maximum valid field values - let max_val = F::ORDER_U32 - 1; - let original = FieldArray([F::new(max_val); MEDIUM_SIZE]); - - // Perform round-trip encoding/decoding - let encoded = original.as_ssz_bytes(); - let decoded = FieldArray::::from_ssz_bytes(&encoded) - .expect("Failed to decode max values"); - - // Verify the values survived the round-trip - assert_eq!(original, decoded, "Round-trip failed for max values"); - } - - #[test] - fn test_ssz_roundtrip_specific_values() { - // Create an array with sequential values for easy verification - let original = FieldArray([F::new(1), F::new(2), F::new(3), F::new(4), F::new(5)]); - - // Encode and verify the byte representation - let encoded = original.as_ssz_bytes(); - - // Each u32 should be encoded as F::NUM_BYTES bytes in little-endian - assert_eq!( - &encoded[0..F::NUM_BYTES], - &[1, 0, 0, 0], - "First element encoding incorrect" - ); - assert_eq!( - &encoded[F::NUM_BYTES..2 * F::NUM_BYTES], - &[2, 0, 0, 0], - "Second element encoding incorrect" - ); - assert_eq!( - &encoded[2 * F::NUM_BYTES..3 * F::NUM_BYTES], - &[3, 0, 0, 0], - "Third element encoding incorrect" - ); - - // Decode and verify round-trip - let decoded = FieldArray::::from_ssz_bytes(&encoded) - .expect("Failed to decode specific values"); - - assert_eq!(original, decoded, "Round-trip failed for specific values"); - } - - #[test] - fn test_ssz_encoding_deterministic() { - let mut rng = rand::rng(); - - // Create a random field array - let field_array = FieldArray(rng.random::<[F; SMALL_SIZE]>()); - - // Encode it multiple times - let encoding1 = field_array.as_ssz_bytes(); - let encoding2 = field_array.as_ssz_bytes(); - let encoding3 = field_array.as_ssz_bytes(); - - // All encodings should be identical - assert_eq!(encoding1, encoding2, "Encoding not deterministic (1 vs 2)"); - assert_eq!(encoding2, encoding3, "Encoding not deterministic (2 vs 3)"); - } - - #[test] - fn test_ssz_encoded_size() { - let field_array = FieldArray([F::ZERO; LARGE_SIZE]); - let encoded = field_array.as_ssz_bytes(); - - // Verify the encoded size matches expectations - let expected_size = LARGE_SIZE * F::NUM_BYTES; - assert_eq!( - encoded.len(), - expected_size, - "Encoded size should be {} bytes (array of {} elements, {} bytes each)", - expected_size, - LARGE_SIZE, - F::NUM_BYTES - ); - - // Also verify the trait method reports the same size - assert_eq!( - field_array.ssz_bytes_len(), - expected_size, - "ssz_bytes_len() should match actual encoded size" - ); - } - - #[test] - fn test_ssz_decode_rejects_wrong_length() { - let expected_len = SMALL_SIZE * F::NUM_BYTES; - - // Test buffer that's too short (missing one byte) - let too_short = vec![0u8; expected_len - 1]; - let result = FieldArray::::from_ssz_bytes(&too_short); - assert!(result.is_err(), "Should reject buffer that's too short"); - if let Err(DecodeError::InvalidByteLength { len, expected }) = result { - assert_eq!(len, expected_len - 1); - assert_eq!(expected, expected_len); - } else { - panic!("Expected InvalidByteLength error"); - } - - // Test buffer that's too long (extra byte) - let too_long = vec![0u8; expected_len + 1]; - let result = FieldArray::::from_ssz_bytes(&too_long); - assert!(result.is_err(), "Should reject buffer that's too long"); - if let Err(DecodeError::InvalidByteLength { len, expected }) = result { - assert_eq!(len, expected_len + 1); - assert_eq!(expected, expected_len); - } else { - panic!("Expected InvalidByteLength error"); - } - } - - #[test] - fn test_ssz_fixed_len_trait_methods() { - // Arrays are always fixed-length in SSZ - assert!( - as Encode>::is_ssz_fixed_len(), - "FieldArray should report as fixed-length (Encode)" - ); - assert!( - as Decode>::is_ssz_fixed_len(), - "FieldArray should report as fixed-length (Decode)" - ); - - // The fixed length should be N * F::NUM_BYTES - let expected_len = SMALL_SIZE * F::NUM_BYTES; - assert_eq!( - as Encode>::ssz_fixed_len(), - expected_len, - "Encode::ssz_fixed_len() incorrect" - ); - assert_eq!( - as Decode>::ssz_fixed_len(), - expected_len, - "Decode::ssz_fixed_len() incorrect" - ); - } - - proptest! { - #[test] - fn proptest_ssz_roundtrip_large( - values in prop::collection::vec(0u32..F::ORDER_U32, LARGE_SIZE) - ) { - // Convert Vec to array for large sizes - let arr: [F; LARGE_SIZE] = std::array::from_fn(|i| F::new(values[i])); - let original = FieldArray(arr); - - let encoded = original.as_ssz_bytes(); - let decoded = FieldArray::::from_ssz_bytes(&encoded) - .expect("Valid SSZ bytes should always decode"); - - prop_assert_eq!(original, decoded); - } - - #[test] - fn proptest_ssz_deterministic( - values in prop::array::uniform5(0u32..F::ORDER_U32) - ) { - let arr = values.map(F::new); - let field_array = FieldArray(arr); - - // Encode twice and verify both encodings are identical - let encoding1 = field_array.as_ssz_bytes(); - let encoding2 = field_array.as_ssz_bytes(); - - prop_assert_eq!(encoding1, encoding2); - } - - #[test] - fn proptest_ssz_size_invariant( - values in prop::array::uniform5(0u32..F::ORDER_U32) - ) { - let arr = values.map(F::new); - let field_array = FieldArray(arr); - - let encoded = field_array.as_ssz_bytes(); - let expected_size = SMALL_SIZE * F::NUM_BYTES; - - prop_assert_eq!(encoded.len(), expected_size); - prop_assert_eq!(field_array.ssz_bytes_len(), expected_size); - } - - #[test] - fn proptest_serde_roundtrip( - values in prop::collection::vec(0u32..F::ORDER_U32, LARGE_SIZE) - ) { - let arr: [F; LARGE_SIZE] = std::array::from_fn(|i| F::new(values[i])); - let original = FieldArray(arr); - - let config = bincode::config::standard().with_fixed_int_encoding(); - let encoded = bincode::serde::encode_to_vec(original, config) - .expect("Failed to serialize"); - let decoded: FieldArray = bincode::serde::decode_from_slice(&encoded, config) - .expect("Failed to deserialize") - .0; - - prop_assert_eq!(original, decoded); - } - - #[test] - fn proptest_serde_deterministic( - values in prop::array::uniform5(0u32..F::ORDER_U32) - ) { - let arr = values.map(F::new); - let field_array = FieldArray(arr); - - let config = bincode::config::standard().with_fixed_int_encoding(); - - // Encode twice and verify both encodings are identical - let encoding1 = bincode::serde::encode_to_vec(field_array, config) - .expect("Failed to serialize"); - let encoding2 = bincode::serde::encode_to_vec(field_array, config) - .expect("Failed to serialize"); - - prop_assert_eq!(encoding1, encoding2); - } - } - - #[test] - fn test_equality() { - let arr1 = FieldArray([F::new(1), F::new(2), F::new(3)]); - let arr2 = FieldArray([F::new(1), F::new(2), F::new(3)]); - let arr3 = FieldArray([F::new(1), F::new(2), F::new(4)]); - - // Equal arrays should be equal - assert_eq!(arr1, arr2); - - // Different arrays should not be equal - assert_ne!(arr1, arr3); - assert_ne!(arr2, arr3); - } - - #[test] - fn test_bincode_no_size_prefix() { - let config = bincode::config::standard().with_fixed_int_encoding(); - let arr = FieldArray([F::new(1), F::new(2), F::new(3)]); - let encoded = bincode::serde::encode_to_vec(arr, config).unwrap(); - assert_eq!(encoded.len(), arr.len() * F::NUM_BYTES); - } - - #[test] - fn test_serde_uses_montgomery_form() { - // Create a field array with known values - let arr = FieldArray([F::new(1), F::new(2), F::new(3)]); - - // Serialize using bincode - let config = bincode::config::standard().with_fixed_int_encoding(); - let encoded = bincode::serde::encode_to_vec(arr, config).unwrap(); - - // Extract the raw u32 values from the encoded bytes - let mut raw_values = Vec::new(); - for i in 0..arr.len() { - let start = i * F::NUM_BYTES; - let chunk = &encoded[start..start + F::NUM_BYTES]; - let value = u32::from_le_bytes(chunk.try_into().unwrap()); - raw_values.push(value); - } - - // Verify that the serialized values are in Montgomery form, not canonical form. - // - // - If they were in canonical form, we would see [1, 2, 3] - // - In Montgomery form, they should be different values - // - // We check this to confirm the serialization is using Montgomery form as in Plonky3. - // - // This is for consistency with other serializations including field elements over the codebase. - assert_ne!( - raw_values, - vec![1, 2, 3], - "Values should be in Montgomery form, not canonical form" - ); - - // Verify that when we access the internal value directly, it matches what was serialized - // This confirms we're serializing the Montgomery representation - for (i, &expected_monty) in raw_values.iter().enumerate() { - // Access the internal Montgomery value through unsafe (for testing only) - let actual_monty = unsafe { - // SAFETY: MontyField31 is repr(transparent) with a u32 value field - std::ptr::read((&raw const arr[i]).cast::()) - }; - - assert_eq!( - actual_monty, expected_monty, - "Element {} should serialize its internal Montgomery form", - i - ); - } - - // Verify roundtrip works correctly - let decoded: FieldArray<3> = bincode::serde::decode_from_slice(&encoded, config) - .expect("Failed to deserialize") - .0; - assert_eq!(arr, decoded, "Roundtrip should preserve values"); - } -} diff --git a/src/inc_encoding/target_sum.rs b/src/inc_encoding/target_sum.rs index 50b1930..0df0e02 100644 --- a/src/inc_encoding/target_sum.rs +++ b/src/inc_encoding/target_sum.rs @@ -142,152 +142,3 @@ impl IncomparableEncoding } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::F; - use crate::array::FieldArray; - use crate::symmetric::message_hash::poseidon::PoseidonMessageHash445; - use p3_field::PrimeField32; - use proptest::prelude::*; - use rand::RngExt; - - const TEST_TARGET_SUM: usize = 115; - type TestTargetSumEncoding = TargetSumEncoding; - - #[test] - fn test_successful_encoding_fixed_message() { - // keep message fixed and only resample randomness - // this mirrors the actual signature scheme behavior - let mut rng = rand::rng(); - let parameter: FieldArray<4> = FieldArray(rng.random()); - let message: [u8; 32] = rng.random(); - let epoch = 0u32; - - // retry with different randomness until encoding succeeds - for _ in 0..1_000 { - let randomness = TestTargetSumEncoding::rand(&mut rng); - - if let Ok(chunks) = - TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch) - { - // check output has correct dimension - assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION); - - // check all chunks are in valid range [0, BASE-1] - for &chunk in &chunks { - assert!((chunk as usize) < TestTargetSumEncoding::BASE); - } - - // check sum equals target - let sum: usize = chunks.iter().map(|&x| x as usize).sum(); - assert_eq!(sum, TEST_TARGET_SUM); - - // check determinism: encoding again with same inputs produces same result - let result2 = - TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); - assert_eq!(chunks, result2.unwrap()); - - return; - } - } - - panic!("failed to find successful encoding after 1000 attempts"); - } - - #[test] - fn test_successful_encoding_random_inputs() { - // retry with all random inputs until encoding succeeds - let mut rng = rand::rng(); - let epoch = 0u32; - - for _ in 0..1_000 { - let parameter: FieldArray<4> = FieldArray(rng.random()); - let message: [u8; 32] = rng.random(); - let randomness = TestTargetSumEncoding::rand(&mut rng); - - if let Ok(chunks) = - TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch) - { - // check output has correct dimension - assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION); - - // check all chunks are in valid range [0, BASE-1] - for &chunk in &chunks { - assert!((chunk as usize) < TestTargetSumEncoding::BASE); - } - - // check sum equals target - let sum: usize = chunks.iter().map(|&x| x as usize).sum(); - assert_eq!(sum, TEST_TARGET_SUM); - - // check determinism: encoding again with same inputs produces same result - let result2 = - TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); - assert_eq!(chunks, result2.unwrap()); - - return; - } - } - - panic!("failed to find successful encoding after 1000 attempts"); - } - - proptest! { - #[test] - fn proptest_encoding_determinism_and_error_reporting( - message in prop::array::uniform32(any::()), - randomness_values in prop::collection::vec(0u32..F::ORDER_U32, 4), - parameter_values in prop::collection::vec(0u32..F::ORDER_U32, 4), - epoch in any::() - ) { - // build randomness and parameter from proptest values - let randomness_arr: [F; 4] = std::array::from_fn(|i| F::new(randomness_values[i])); - let randomness = FieldArray(randomness_arr); - let parameter_arr: [F; 4] = std::array::from_fn(|i| F::new(parameter_values[i])); - let parameter = FieldArray(parameter_arr); - - // compute expected sum from underlying message hash - let hash_chunks = PoseidonMessageHash445::apply(¶meter, epoch, &randomness, &message).unwrap(); - let hash_sum: usize = hash_chunks.iter().map(|&x| x as usize).sum(); - - // call encode twice to check determinism - let result1 = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); - let result2 = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); - - // check determinism: both calls produce same result - match (&result1, &result2) { - (Ok(c1), Ok(c2)) => prop_assert_eq!(c1, c2), - (Err(TargetSumError::Mismatch { expected: e1, actual: a1 }), - Err(TargetSumError::Mismatch { expected: e2, actual: a2 })) => { - prop_assert_eq!(e1, e2); - prop_assert_eq!(a1, a2); - } - _ => prop_assert!(false, "determinism violated"), - } - - // check properties based on success/failure - match result1 { - Err(TargetSumError::Mismatch { expected, actual }) => { - // check error reports correct values - prop_assert_eq!(expected, TEST_TARGET_SUM); - prop_assert_eq!(actual, hash_sum); - } - Ok(chunks) => { - // check output dimension - prop_assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION); - - // check all chunks in valid range - for &chunk in &chunks { - prop_assert!((chunk as usize) < TestTargetSumEncoding::BASE); - } - - // check sum equals target - let sum: usize = chunks.iter().map(|&x| x as usize).sum(); - prop_assert_eq!(sum, TEST_TARGET_SUM); - } - } - } - } -} diff --git a/src/symmetric/prf/shake_to_field.rs b/src/symmetric/prf/shake_to_field.rs index 28c794f..255a284 100644 --- a/src/symmetric/prf/shake_to_field.rs +++ b/src/symmetric/prf/shake_to_field.rs @@ -116,98 +116,3 @@ where }) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::MESSAGE_LENGTH; - use proptest::prelude::*; - - const DOMAIN_LEN: usize = 4; - const RAND_LEN: usize = 4; - type PRF = ShakePRFtoF; - - #[test] - fn test_shake_to_field_prf_key_not_all_same() { - const K: usize = 10; - - let mut rng = rand::rng(); - let mut all_same_count = 0; - - for _ in 0..K { - let key = PRF::key_gen(&mut rng); - - let first = key[0]; - if key.iter().all(|&x| x == first) { - all_same_count += 1; - } - } - - assert!( - all_same_count < K, - "PRF key had identical elements in all {} trials", - K - ); - } - - proptest! { - #[test] - fn proptest_get_domain_element_properties( - key in prop::array::uniform32(any::()), - epoch in any::(), - index1 in any::(), - index2 in any::() - ) { - // check output has correct length - let result1 = PRF::get_domain_element(&key, epoch, index1); - prop_assert_eq!(result1.len(), DOMAIN_LEN); - - // check determinism: same inputs produce same output - let result2 = PRF::get_domain_element(&key, epoch, index1); - prop_assert_eq!(result1, result2); - - // check uniqueness: different indices produce different outputs - let other = PRF::get_domain_element(&key, epoch, index2); - if index1 == index2 { - prop_assert_eq!(result1, other); - } else { - prop_assert_ne!(result1, other); - } - - // check different epochs produce different outputs - let other_epoch = PRF::get_domain_element(&key, epoch.wrapping_add(1), index1); - prop_assert_ne!(result1, other_epoch); - } - - #[test] - fn proptest_get_randomness_properties( - key in prop::array::uniform32(any::()), - epoch in any::(), - message in prop::array::uniform32(any::()), - counter1 in any::(), - counter2 in any::() - ) { - let msg: [u8; MESSAGE_LENGTH] = message; - - // check output has correct length - let result1 = PRF::get_randomness(&key, epoch, &msg, counter1); - prop_assert_eq!(result1.len(), RAND_LEN); - - // check determinism: same inputs produce same output - let result2 = PRF::get_randomness(&key, epoch, &msg, counter1); - prop_assert_eq!(result1, result2); - - // check uniqueness: different counters produce different outputs - let other = PRF::get_randomness(&key, epoch, &msg, counter2); - if counter1 == counter2 { - prop_assert_eq!(result1, other); - } else { - prop_assert_ne!(result1, other); - } - - // check different epochs produce different outputs - let other_epoch = PRF::get_randomness(&key, epoch.wrapping_add(1), &msg, counter1); - prop_assert_ne!(result1, other_epoch); - } - } -} diff --git a/src/symmetric/tweak_hash.rs b/src/symmetric/tweak_hash.rs index 952b1f7..ae28f01 100644 --- a/src/symmetric/tweak_hash.rs +++ b/src/symmetric/tweak_hash.rs @@ -133,133 +133,3 @@ pub fn chain( } pub mod poseidon; - -#[cfg(test)] -mod tests { - use crate::symmetric::tweak_hash::poseidon::PoseidonTweak44; - - use super::*; - use proptest::prelude::*; - - type TestTH = PoseidonTweak44; - - #[test] - fn test_chain_associative() { - let mut rng = rand::rng(); - - // we test that first walking k steps, and then walking the remaining steps - // is the same as directly walking all steps. - - let epoch = 9; - let chain_index = 20; - let parameter = TestTH::rand_parameter(&mut rng); - let start = TestTH::rand_domain(&mut rng); - let total_steps = 16; - - // walking directly - let end_direct = chain::(¶meter, epoch, chain_index, 0, total_steps, &start); - - for split in 0..=total_steps { - let steps_a = split; - let steps_b = total_steps - split; - - // walking indirectly - let intermediate = chain::(¶meter, epoch, chain_index, 0, steps_a, &start); - let end_indirect = chain::( - ¶meter, - epoch, - chain_index, - steps_a as u8, - steps_b, - &intermediate, - ); - - // should be the same - assert_eq!(end_direct, end_indirect); - } - } - - #[test] - fn test_chain_associative_max_value() { - let mut rng = rand::rng(); - - // we test that first walking k steps, and then walking the remaining steps - // is the same as directly walking all steps. - - let epoch = 12; - let chain_index = 210; - let parameter = TestTH::rand_parameter(&mut rng); - let start = TestTH::rand_domain(&mut rng); - let total_steps = u8::MAX as usize; // max if we say that pos_in_chain is u8 - - // walking directly - let end_direct = chain::(¶meter, epoch, chain_index, 0, total_steps, &start); - - for split in 0..=total_steps { - let steps_a = split; - let steps_b = total_steps - split; - - // walking indirectly - let intermediate = chain::(¶meter, epoch, chain_index, 0, steps_a, &start); - let end_indirect = chain::( - ¶meter, - epoch, - chain_index, - steps_a as u8, - steps_b, - &intermediate, - ); - - // should be the same - assert_eq!(end_direct, end_indirect); - } - } - - proptest! { - #[test] - fn proptest_chain_associative( - // Random epoch for domain separation (small range to keep tests fast) - epoch in 0u32..100, - - // Random chain index to simulate different chains (small range to keep tests fast) - chain_index in 0u8..10, - - // Total number of steps to walk along the chain (bounded to keep tests fast) - total_steps in 0usize..16, - ) { - // Random number generator for generating parameters and start point - let mut rng = rand::rng(); - - // Generate a random public parameter for the tweakable hash function - let parameter = TestTH::rand_parameter(&mut rng); - - // Generate a random starting domain element (initial hash state) - let start = TestTH::rand_domain(&mut rng); - - // Compute the result of walking the entire chain in one go - let end_direct = chain::(¶meter, epoch, chain_index, 0, total_steps, &start); - - // For every way of splitting the walk into two segments... - for split in 0..=total_steps { - let steps_a = split; // First segment length - let steps_b = total_steps - split; // Second segment length - - // First walk: from start, walk `steps_a` steps - let intermediate = chain::(¶meter, epoch, chain_index, 0, steps_a, &start); - - // Second walk: continue from intermediate point for `steps_b` steps - let end_indirect = chain::( - ¶meter, - epoch, - chain_index, - steps_a as u8, // Start position for second segment - steps_b, - &intermediate, - ); - - // Check that walking in one go or in two segments gives the same result - prop_assert_eq!(end_direct, end_indirect); - } - } - } -} diff --git a/tests/array.rs b/tests/array.rs new file mode 100644 index 0000000..3c4b181 --- /dev/null +++ b/tests/array.rs @@ -0,0 +1,327 @@ +use leansig::array::FieldArray; +use p3_field::{PrimeCharacteristicRing, PrimeField32, RawDataSerializable}; +use p3_koala_bear::KoalaBear as F; +use proptest::prelude::*; +use rand::RngExt; +use ssz::{Decode, DecodeError, Encode}; + +/// Small parameter arrays +const SMALL_SIZE: usize = 5; +/// Hash output size +const MEDIUM_SIZE: usize = 7; +/// Larger parameter arrays +const LARGE_SIZE: usize = 44; + +#[test] +fn test_ssz_roundtrip_zero_values() { + // Start with an array of zeros + let original = FieldArray([F::ZERO; SMALL_SIZE]); + + // Encode to bytes using SSZ + let encoded = original.as_ssz_bytes(); + + // Decode back from bytes + let decoded = FieldArray::::from_ssz_bytes(&encoded) + .expect("Failed to decode valid SSZ bytes"); + + // Verify round-trip preserves the value + assert_eq!(original, decoded, "Round-trip failed for zero values"); +} + +#[test] +fn test_ssz_roundtrip_max_values() { + // Create array with maximum valid field values + let max_val = F::ORDER_U32 - 1; + let original = FieldArray([F::new(max_val); MEDIUM_SIZE]); + + // Perform round-trip encoding/decoding + let encoded = original.as_ssz_bytes(); + let decoded = + FieldArray::::from_ssz_bytes(&encoded).expect("Failed to decode max values"); + + // Verify the values survived the round-trip + assert_eq!(original, decoded, "Round-trip failed for max values"); +} + +#[test] +fn test_ssz_roundtrip_specific_values() { + // Create an array with sequential values for easy verification + let original = FieldArray([F::new(1), F::new(2), F::new(3), F::new(4), F::new(5)]); + + // Encode and verify the byte representation + let encoded = original.as_ssz_bytes(); + + // Each u32 should be encoded as F::NUM_BYTES bytes in little-endian + assert_eq!( + &encoded[0..F::NUM_BYTES], + &[1, 0, 0, 0], + "First element encoding incorrect" + ); + assert_eq!( + &encoded[F::NUM_BYTES..2 * F::NUM_BYTES], + &[2, 0, 0, 0], + "Second element encoding incorrect" + ); + assert_eq!( + &encoded[2 * F::NUM_BYTES..3 * F::NUM_BYTES], + &[3, 0, 0, 0], + "Third element encoding incorrect" + ); + + // Decode and verify round-trip + let decoded = FieldArray::::from_ssz_bytes(&encoded) + .expect("Failed to decode specific values"); + + assert_eq!(original, decoded, "Round-trip failed for specific values"); +} + +#[test] +fn test_ssz_encoding_deterministic() { + let mut rng = rand::rng(); + + // Create a random field array + let field_array = FieldArray(rng.random::<[F; SMALL_SIZE]>()); + + // Encode it multiple times + let encoding1 = field_array.as_ssz_bytes(); + let encoding2 = field_array.as_ssz_bytes(); + let encoding3 = field_array.as_ssz_bytes(); + + // All encodings should be identical + assert_eq!(encoding1, encoding2, "Encoding not deterministic (1 vs 2)"); + assert_eq!(encoding2, encoding3, "Encoding not deterministic (2 vs 3)"); +} + +#[test] +fn test_ssz_encoded_size() { + let field_array = FieldArray([F::ZERO; LARGE_SIZE]); + let encoded = field_array.as_ssz_bytes(); + + // Verify the encoded size matches expectations + let expected_size = LARGE_SIZE * F::NUM_BYTES; + assert_eq!( + encoded.len(), + expected_size, + "Encoded size should be {} bytes (array of {} elements, {} bytes each)", + expected_size, + LARGE_SIZE, + F::NUM_BYTES + ); + + // Also verify the trait method reports the same size + assert_eq!( + field_array.ssz_bytes_len(), + expected_size, + "ssz_bytes_len() should match actual encoded size" + ); +} + +#[test] +fn test_ssz_decode_rejects_wrong_length() { + let expected_len = SMALL_SIZE * F::NUM_BYTES; + + // Test buffer that's too short (missing one byte) + let too_short = vec![0u8; expected_len - 1]; + let result = FieldArray::::from_ssz_bytes(&too_short); + assert!(result.is_err(), "Should reject buffer that's too short"); + if let Err(DecodeError::InvalidByteLength { len, expected }) = result { + assert_eq!(len, expected_len - 1); + assert_eq!(expected, expected_len); + } else { + panic!("Expected InvalidByteLength error"); + } + + // Test buffer that's too long (extra byte) + let too_long = vec![0u8; expected_len + 1]; + let result = FieldArray::::from_ssz_bytes(&too_long); + assert!(result.is_err(), "Should reject buffer that's too long"); + if let Err(DecodeError::InvalidByteLength { len, expected }) = result { + assert_eq!(len, expected_len + 1); + assert_eq!(expected, expected_len); + } else { + panic!("Expected InvalidByteLength error"); + } +} + +#[test] +fn test_ssz_fixed_len_trait_methods() { + // Arrays are always fixed-length in SSZ + assert!( + as Encode>::is_ssz_fixed_len(), + "FieldArray should report as fixed-length (Encode)" + ); + assert!( + as Decode>::is_ssz_fixed_len(), + "FieldArray should report as fixed-length (Decode)" + ); + + // The fixed length should be N * F::NUM_BYTES + let expected_len = SMALL_SIZE * F::NUM_BYTES; + assert_eq!( + as Encode>::ssz_fixed_len(), + expected_len, + "Encode::ssz_fixed_len() incorrect" + ); + assert_eq!( + as Decode>::ssz_fixed_len(), + expected_len, + "Decode::ssz_fixed_len() incorrect" + ); +} + +proptest! { + #[test] + fn proptest_ssz_roundtrip_large( + values in prop::collection::vec(0u32..F::ORDER_U32, LARGE_SIZE) + ) { + // Convert Vec to array for large sizes + let arr: [F; LARGE_SIZE] = std::array::from_fn(|i| F::new(values[i])); + let original = FieldArray(arr); + + let encoded = original.as_ssz_bytes(); + let decoded = FieldArray::::from_ssz_bytes(&encoded) + .expect("Valid SSZ bytes should always decode"); + + prop_assert_eq!(original, decoded); + } + + #[test] + fn proptest_ssz_deterministic( + values in prop::array::uniform5(0u32..F::ORDER_U32) + ) { + let arr = values.map(F::new); + let field_array = FieldArray(arr); + + // Encode twice and verify both encodings are identical + let encoding1 = field_array.as_ssz_bytes(); + let encoding2 = field_array.as_ssz_bytes(); + + prop_assert_eq!(encoding1, encoding2); + } + + #[test] + fn proptest_ssz_size_invariant( + values in prop::array::uniform5(0u32..F::ORDER_U32) + ) { + let arr = values.map(F::new); + let field_array = FieldArray(arr); + + let encoded = field_array.as_ssz_bytes(); + let expected_size = SMALL_SIZE * F::NUM_BYTES; + + prop_assert_eq!(encoded.len(), expected_size); + prop_assert_eq!(field_array.ssz_bytes_len(), expected_size); + } + + #[test] + fn proptest_serde_roundtrip( + values in prop::collection::vec(0u32..F::ORDER_U32, LARGE_SIZE) + ) { + let arr: [F; LARGE_SIZE] = std::array::from_fn(|i| F::new(values[i])); + let original = FieldArray(arr); + + let config = bincode::config::standard().with_fixed_int_encoding(); + let encoded = bincode::serde::encode_to_vec(original, config) + .expect("Failed to serialize"); + let decoded: FieldArray = bincode::serde::decode_from_slice(&encoded, config) + .expect("Failed to deserialize") + .0; + + prop_assert_eq!(original, decoded); + } + + #[test] + fn proptest_serde_deterministic( + values in prop::array::uniform5(0u32..F::ORDER_U32) + ) { + let arr = values.map(F::new); + let field_array = FieldArray(arr); + + let config = bincode::config::standard().with_fixed_int_encoding(); + + // Encode twice and verify both encodings are identical + let encoding1 = bincode::serde::encode_to_vec(field_array, config) + .expect("Failed to serialize"); + let encoding2 = bincode::serde::encode_to_vec(field_array, config) + .expect("Failed to serialize"); + + prop_assert_eq!(encoding1, encoding2); + } +} + +#[test] +fn test_equality() { + let arr1 = FieldArray([F::new(1), F::new(2), F::new(3)]); + let arr2 = FieldArray([F::new(1), F::new(2), F::new(3)]); + let arr3 = FieldArray([F::new(1), F::new(2), F::new(4)]); + + // Equal arrays should be equal + assert_eq!(arr1, arr2); + + // Different arrays should not be equal + assert_ne!(arr1, arr3); + assert_ne!(arr2, arr3); +} + +#[test] +fn test_bincode_no_size_prefix() { + let config = bincode::config::standard().with_fixed_int_encoding(); + let arr = FieldArray([F::new(1), F::new(2), F::new(3)]); + let encoded = bincode::serde::encode_to_vec(arr, config).unwrap(); + assert_eq!(encoded.len(), arr.len() * F::NUM_BYTES); +} + +#[test] +fn test_serde_uses_montgomery_form() { + // Create a field array with known values + let arr = FieldArray([F::new(1), F::new(2), F::new(3)]); + + // Serialize using bincode + let config = bincode::config::standard().with_fixed_int_encoding(); + let encoded = bincode::serde::encode_to_vec(arr, config).unwrap(); + + // Extract the raw u32 values from the encoded bytes + let mut raw_values = Vec::new(); + for i in 0..arr.len() { + let start = i * F::NUM_BYTES; + let chunk = &encoded[start..start + F::NUM_BYTES]; + let value = u32::from_le_bytes(chunk.try_into().unwrap()); + raw_values.push(value); + } + + // Verify that the serialized values are in Montgomery form, not canonical form. + // + // If they were in canonical form, we would see [1, 2, 3] + // In Montgomery form, they should be different values + // + // We check this to confirm the serialization is using Montgomery form as in Plonky3. + // + // This is for consistency with other serializations including field elements over the codebase. + assert_ne!( + raw_values, + vec![1, 2, 3], + "Values should be in Montgomery form, not canonical form" + ); + + // Verify that when we access the internal value directly, it matches what was serialized + // This confirms we're serializing the Montgomery representation + for (i, &expected_monty) in raw_values.iter().enumerate() { + // Access the internal Montgomery value through unsafe (for testing only) + let actual_monty = unsafe { + // SAFETY: MontyField31 is repr(transparent) with a u32 value field + std::ptr::read((&raw const arr[i]).cast::()) + }; + + assert_eq!( + actual_monty, expected_monty, + "Element {} should serialize its internal Montgomery form", + i + ); + } + + // Verify roundtrip works correctly + let decoded: FieldArray<3> = bincode::serde::decode_from_slice(&encoded, config) + .expect("Failed to deserialize") + .0; + assert_eq!(arr, decoded, "Roundtrip should preserve values"); +} diff --git a/tests/inc_encoding/target_sum.rs b/tests/inc_encoding/target_sum.rs new file mode 100644 index 0000000..cb1578c --- /dev/null +++ b/tests/inc_encoding/target_sum.rs @@ -0,0 +1,145 @@ +use leansig::array::FieldArray; +use leansig::inc_encoding::IncomparableEncoding; +use leansig::inc_encoding::target_sum::{TargetSumEncoding, TargetSumError}; +use leansig::symmetric::message_hash::MessageHash; +use leansig::symmetric::message_hash::poseidon::PoseidonMessageHash; +use p3_field::PrimeField32; +use p3_koala_bear::KoalaBear as F; +use proptest::prelude::*; +use rand::RngExt; + +type PoseidonMessageHash445 = PoseidonMessageHash<4, 4, 5, 128, 4, 2, 9>; + +const TEST_TARGET_SUM: usize = 115; +type TestTargetSumEncoding = TargetSumEncoding; + +#[test] +fn test_successful_encoding_fixed_message() { + // keep message fixed and only resample randomness + // this mirrors the actual signature scheme behavior + let mut rng = rand::rng(); + let parameter: FieldArray<4> = FieldArray(rng.random()); + let message: [u8; 32] = rng.random(); + let epoch = 0u32; + + // retry with different randomness until encoding succeeds + for _ in 0..1_000 { + let randomness = TestTargetSumEncoding::rand(&mut rng); + + if let Ok(chunks) = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch) + { + // check output has correct dimension + assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION); + + // check all chunks are in valid range [0, BASE-1] + for &chunk in &chunks { + assert!((chunk as usize) < TestTargetSumEncoding::BASE); + } + + // check sum equals target + let sum: usize = chunks.iter().map(|&x| x as usize).sum(); + assert_eq!(sum, TEST_TARGET_SUM); + + // check determinism: encoding again with same inputs produces same result + let result2 = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); + assert_eq!(chunks, result2.unwrap()); + + return; + } + } + + panic!("failed to find successful encoding after 1000 attempts"); +} + +#[test] +fn test_successful_encoding_random_inputs() { + // retry with all random inputs until encoding succeeds + let mut rng = rand::rng(); + let epoch = 0u32; + + for _ in 0..1_000 { + let parameter: FieldArray<4> = FieldArray(rng.random()); + let message: [u8; 32] = rng.random(); + let randomness = TestTargetSumEncoding::rand(&mut rng); + + if let Ok(chunks) = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch) + { + // check output has correct dimension + assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION); + + // check all chunks are in valid range [0, BASE-1] + for &chunk in &chunks { + assert!((chunk as usize) < TestTargetSumEncoding::BASE); + } + + // check sum equals target + let sum: usize = chunks.iter().map(|&x| x as usize).sum(); + assert_eq!(sum, TEST_TARGET_SUM); + + // check determinism: encoding again with same inputs produces same result + let result2 = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); + assert_eq!(chunks, result2.unwrap()); + + return; + } + } + + panic!("failed to find successful encoding after 1000 attempts"); +} + +proptest! { + #[test] + fn proptest_encoding_determinism_and_error_reporting( + message in prop::array::uniform32(any::()), + randomness_values in prop::collection::vec(0u32..F::ORDER_U32, 4), + parameter_values in prop::collection::vec(0u32..F::ORDER_U32, 4), + epoch in any::() + ) { + // build randomness and parameter from proptest values + let randomness_arr: [F; 4] = std::array::from_fn(|i| F::new(randomness_values[i])); + let randomness = FieldArray(randomness_arr); + let parameter_arr: [F; 4] = std::array::from_fn(|i| F::new(parameter_values[i])); + let parameter = FieldArray(parameter_arr); + + // compute expected sum from underlying message hash + let hash_chunks = PoseidonMessageHash445::apply(¶meter, epoch, &randomness, &message).unwrap(); + let hash_sum: usize = hash_chunks.iter().map(|&x| x as usize).sum(); + + // call encode twice to check determinism + let result1 = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); + let result2 = TestTargetSumEncoding::encode(¶meter, &message, &randomness, epoch); + + // check determinism: both calls produce same result + match (&result1, &result2) { + (Ok(c1), Ok(c2)) => prop_assert_eq!(c1, c2), + (Err(TargetSumError::Mismatch { expected: e1, actual: a1 }), + Err(TargetSumError::Mismatch { expected: e2, actual: a2 })) => { + prop_assert_eq!(e1, e2); + prop_assert_eq!(a1, a2); + } + _ => prop_assert!(false, "determinism violated"), + } + + // check properties based on success/failure + match result1 { + Err(TargetSumError::Mismatch { expected, actual }) => { + // check error reports correct values + prop_assert_eq!(expected, TEST_TARGET_SUM); + prop_assert_eq!(actual, hash_sum); + } + Ok(chunks) => { + // check output dimension + prop_assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION); + + // check all chunks in valid range + for &chunk in &chunks { + prop_assert!((chunk as usize) < TestTargetSumEncoding::BASE); + } + + // check sum equals target + let sum: usize = chunks.iter().map(|&x| x as usize).sum(); + prop_assert_eq!(sum, TEST_TARGET_SUM); + } + } + } +} diff --git a/tests/symmetric/prf/shake_to_field.rs b/tests/symmetric/prf/shake_to_field.rs new file mode 100644 index 0000000..f95bdf2 --- /dev/null +++ b/tests/symmetric/prf/shake_to_field.rs @@ -0,0 +1,92 @@ +use leansig::MESSAGE_LENGTH; +use leansig::symmetric::prf::Pseudorandom; +use leansig::symmetric::prf::shake_to_field::ShakePRFtoF; +use proptest::prelude::*; + +const DOMAIN_LEN: usize = 4; +const RAND_LEN: usize = 4; +type PRF = ShakePRFtoF; + +#[test] +fn test_shake_to_field_prf_key_not_all_same() { + const K: usize = 10; + + let mut rng = rand::rng(); + let mut all_same_count = 0; + + for _ in 0..K { + let key = PRF::key_gen(&mut rng); + + let first = key[0]; + if key.iter().all(|&x| x == first) { + all_same_count += 1; + } + } + + assert!( + all_same_count < K, + "PRF key had identical elements in all {} trials", + K + ); +} + +proptest! { + #[test] + fn proptest_get_domain_element_properties( + key in prop::array::uniform32(any::()), + epoch in any::(), + index1 in any::(), + index2 in any::() + ) { + // check output has correct length + let result1 = PRF::get_domain_element(&key, epoch, index1); + prop_assert_eq!(result1.len(), DOMAIN_LEN); + + // check determinism: same inputs produce same output + let result2 = PRF::get_domain_element(&key, epoch, index1); + prop_assert_eq!(result1, result2); + + // check uniqueness: different indices produce different outputs + let other = PRF::get_domain_element(&key, epoch, index2); + if index1 == index2 { + prop_assert_eq!(result1, other); + } else { + prop_assert_ne!(result1, other); + } + + // check different epochs produce different outputs + let other_epoch = PRF::get_domain_element(&key, epoch.wrapping_add(1), index1); + prop_assert_ne!(result1, other_epoch); + } + + #[test] + fn proptest_get_randomness_properties( + key in prop::array::uniform32(any::()), + epoch in any::(), + message in prop::array::uniform32(any::()), + counter1 in any::(), + counter2 in any::() + ) { + let msg: [u8; MESSAGE_LENGTH] = message; + + // check output has correct length + let result1 = PRF::get_randomness(&key, epoch, &msg, counter1); + prop_assert_eq!(result1.len(), RAND_LEN); + + // check determinism: same inputs produce same output + let result2 = PRF::get_randomness(&key, epoch, &msg, counter1); + prop_assert_eq!(result1, result2); + + // check uniqueness: different counters produce different outputs + let other = PRF::get_randomness(&key, epoch, &msg, counter2); + if counter1 == counter2 { + prop_assert_eq!(result1, other); + } else { + prop_assert_ne!(result1, other); + } + + // check different epochs produce different outputs + let other_epoch = PRF::get_randomness(&key, epoch.wrapping_add(1), &msg, counter1); + prop_assert_ne!(result1, other_epoch); + } +} diff --git a/tests/symmetric/tweak_hash.rs b/tests/symmetric/tweak_hash.rs new file mode 100644 index 0000000..48030bd --- /dev/null +++ b/tests/symmetric/tweak_hash.rs @@ -0,0 +1,128 @@ +use leansig::symmetric::tweak_hash::TweakableHash; +use leansig::symmetric::tweak_hash::chain; +use leansig::symmetric::tweak_hash::poseidon::PoseidonTweakHash; +use proptest::prelude::*; + +type PoseidonTweak44 = PoseidonTweakHash<4, 4, 3, 9, 128>; + +type TestTH = PoseidonTweak44; + +#[test] +fn test_chain_associative() { + let mut rng = rand::rng(); + + // we test that first walking k steps, and then walking the remaining steps + // is the same as directly walking all steps. + + let epoch = 9; + let chain_index = 20; + let parameter = TestTH::rand_parameter(&mut rng); + let start = TestTH::rand_domain(&mut rng); + let total_steps = 16; + + // walking directly + let end_direct = chain::(¶meter, epoch, chain_index, 0, total_steps, &start); + + for split in 0..=total_steps { + let steps_a = split; + let steps_b = total_steps - split; + + // walking indirectly + let intermediate = chain::(¶meter, epoch, chain_index, 0, steps_a, &start); + let end_indirect = chain::( + ¶meter, + epoch, + chain_index, + steps_a as u8, + steps_b, + &intermediate, + ); + + // should be the same + assert_eq!(end_direct, end_indirect); + } +} + +#[test] +fn test_chain_associative_max_value() { + let mut rng = rand::rng(); + + // we test that first walking k steps, and then walking the remaining steps + // is the same as directly walking all steps. + + let epoch = 12; + let chain_index = 210; + let parameter = TestTH::rand_parameter(&mut rng); + let start = TestTH::rand_domain(&mut rng); + let total_steps = u8::MAX as usize; // max if we say that pos_in_chain is u8 + + // walking directly + let end_direct = chain::(¶meter, epoch, chain_index, 0, total_steps, &start); + + for split in 0..=total_steps { + let steps_a = split; + let steps_b = total_steps - split; + + // walking indirectly + let intermediate = chain::(¶meter, epoch, chain_index, 0, steps_a, &start); + let end_indirect = chain::( + ¶meter, + epoch, + chain_index, + steps_a as u8, + steps_b, + &intermediate, + ); + + // should be the same + assert_eq!(end_direct, end_indirect); + } +} + +proptest! { + #[test] + fn proptest_chain_associative( + // Random epoch for domain separation (small range to keep tests fast) + epoch in 0u32..100, + + // Random chain index to simulate different chains (small range to keep tests fast) + chain_index in 0u8..10, + + // Total number of steps to walk along the chain (bounded to keep tests fast) + total_steps in 0usize..16, + ) { + // Random number generator for generating parameters and start point + let mut rng = rand::rng(); + + // Generate a random public parameter for the tweakable hash function + let parameter = TestTH::rand_parameter(&mut rng); + + // Generate a random starting domain element (initial hash state) + let start = TestTH::rand_domain(&mut rng); + + // Compute the result of walking the entire chain in one go + let end_direct = chain::(¶meter, epoch, chain_index, 0, total_steps, &start); + + // For every way of splitting the walk into two segments... + for split in 0..=total_steps { + let steps_a = split; // First segment length + let steps_b = total_steps - split; // Second segment length + + // First walk: from start, walk `steps_a` steps + let intermediate = chain::(¶meter, epoch, chain_index, 0, steps_a, &start); + + // Second walk: continue from intermediate point for `steps_b` steps + let end_indirect = chain::( + ¶meter, + epoch, + chain_index, + steps_a as u8, // Start position for second segment + steps_b, + &intermediate, + ); + + // Check that walking in one go or in two segments gives the same result + prop_assert_eq!(end_direct, end_indirect); + } + } +}