Skip to content

Commit d9610e7

Browse files
tcoratgerb-wagn
andauthored
testing: more proptests (#20)
* testing: more proptests * Update src/signature/generalized_xmss.rs Co-authored-by: Benedikt Wagner <[email protected]> * fix comment * fix test --------- Co-authored-by: Benedikt Wagner <[email protected]>
1 parent adad238 commit d9610e7

File tree

5 files changed

+486
-5
lines changed

5 files changed

+486
-5
lines changed

src/inc_encoding/target_sum.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,157 @@ impl<MH: MessageHash, const TARGET_SUM: usize> IncomparableEncoding
8686
MH::internal_consistency_check();
8787
}
8888
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
use crate::F;
94+
use crate::array::FieldArray;
95+
use crate::symmetric::message_hash::MessageHash;
96+
use crate::symmetric::message_hash::poseidon::PoseidonMessageHash445;
97+
use p3_field::PrimeField32;
98+
use proptest::prelude::*;
99+
100+
const TEST_TARGET_SUM: usize = 115;
101+
type TestTargetSumEncoding = TargetSumEncoding<PoseidonMessageHash445, TEST_TARGET_SUM>;
102+
103+
#[test]
104+
fn test_internal_consistency() {
105+
TestTargetSumEncoding::internal_consistency_check();
106+
}
107+
108+
#[test]
109+
fn test_successful_encoding_fixed_message() {
110+
// keep message fixed and only resample randomness
111+
// this mirrors the actual signature scheme behavior
112+
let mut rng = rand::rng();
113+
let parameter: FieldArray<4> = FieldArray(rng.random());
114+
let message: [u8; 32] = rng.random();
115+
let epoch = 0u32;
116+
117+
// retry with different randomness until encoding succeeds
118+
for _ in 0..1_000 {
119+
let randomness = TestTargetSumEncoding::rand(&mut rng);
120+
121+
if let Ok(chunks) =
122+
TestTargetSumEncoding::encode(&parameter, &message, &randomness, epoch)
123+
{
124+
// check output has correct dimension
125+
assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION);
126+
127+
// check all chunks are in valid range [0, BASE-1]
128+
for &chunk in &chunks {
129+
assert!((chunk as usize) < TestTargetSumEncoding::BASE);
130+
}
131+
132+
// check sum equals target
133+
let sum: usize = chunks.iter().map(|&x| x as usize).sum();
134+
assert_eq!(sum, TEST_TARGET_SUM);
135+
136+
// check determinism: encoding again with same inputs produces same result
137+
let result2 =
138+
TestTargetSumEncoding::encode(&parameter, &message, &randomness, epoch);
139+
assert_eq!(chunks, result2.unwrap());
140+
141+
return;
142+
}
143+
}
144+
145+
panic!("failed to find successful encoding after 1000 attempts");
146+
}
147+
148+
#[test]
149+
fn test_successful_encoding_random_inputs() {
150+
// retry with all random inputs until encoding succeeds
151+
let mut rng = rand::rng();
152+
let epoch = 0u32;
153+
154+
for _ in 0..1_000 {
155+
let parameter: FieldArray<4> = FieldArray(rng.random());
156+
let message: [u8; 32] = rng.random();
157+
let randomness = TestTargetSumEncoding::rand(&mut rng);
158+
159+
if let Ok(chunks) =
160+
TestTargetSumEncoding::encode(&parameter, &message, &randomness, epoch)
161+
{
162+
// check output has correct dimension
163+
assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION);
164+
165+
// check all chunks are in valid range [0, BASE-1]
166+
for &chunk in &chunks {
167+
assert!((chunk as usize) < TestTargetSumEncoding::BASE);
168+
}
169+
170+
// check sum equals target
171+
let sum: usize = chunks.iter().map(|&x| x as usize).sum();
172+
assert_eq!(sum, TEST_TARGET_SUM);
173+
174+
// check determinism: encoding again with same inputs produces same result
175+
let result2 =
176+
TestTargetSumEncoding::encode(&parameter, &message, &randomness, epoch);
177+
assert_eq!(chunks, result2.unwrap());
178+
179+
return;
180+
}
181+
}
182+
183+
panic!("failed to find successful encoding after 1000 attempts");
184+
}
185+
186+
proptest! {
187+
#[test]
188+
fn proptest_encoding_determinism_and_error_reporting(
189+
message in prop::array::uniform32(any::<u8>()),
190+
randomness_values in prop::collection::vec(0u32..F::ORDER_U32, 4),
191+
parameter_values in prop::collection::vec(0u32..F::ORDER_U32, 4),
192+
epoch in any::<u32>()
193+
) {
194+
// build randomness and parameter from proptest values
195+
let randomness_arr: [F; 4] = std::array::from_fn(|i| F::new(randomness_values[i]));
196+
let randomness = FieldArray(randomness_arr);
197+
let parameter_arr: [F; 4] = std::array::from_fn(|i| F::new(parameter_values[i]));
198+
let parameter = FieldArray(parameter_arr);
199+
200+
// compute expected sum from underlying message hash
201+
let hash_chunks = PoseidonMessageHash445::apply(&parameter, epoch, &randomness, &message);
202+
let hash_sum: usize = hash_chunks.iter().map(|&x| x as usize).sum();
203+
204+
// call encode twice to check determinism
205+
let result1 = TestTargetSumEncoding::encode(&parameter, &message, &randomness, epoch);
206+
let result2 = TestTargetSumEncoding::encode(&parameter, &message, &randomness, epoch);
207+
208+
// check determinism: both calls produce same result
209+
match (&result1, &result2) {
210+
(Ok(c1), Ok(c2)) => prop_assert_eq!(c1, c2),
211+
(Err(TargetSumError::Mismatch { expected: e1, actual: a1 }),
212+
Err(TargetSumError::Mismatch { expected: e2, actual: a2 })) => {
213+
prop_assert_eq!(e1, e2);
214+
prop_assert_eq!(a1, a2);
215+
}
216+
_ => prop_assert!(false, "determinism violated"),
217+
}
218+
219+
// check properties based on success/failure
220+
match result1 {
221+
Err(TargetSumError::Mismatch { expected, actual }) => {
222+
// check error reports correct values
223+
prop_assert_eq!(expected, TEST_TARGET_SUM);
224+
prop_assert_eq!(actual, hash_sum);
225+
}
226+
Ok(chunks) => {
227+
// check output dimension
228+
prop_assert_eq!(chunks.len(), TestTargetSumEncoding::DIMENSION);
229+
230+
// check all chunks in valid range
231+
for &chunk in &chunks {
232+
prop_assert!((chunk as usize) < TestTargetSumEncoding::BASE);
233+
}
234+
235+
// check sum equals target
236+
let sum: usize = chunks.iter().map(|&x| x as usize).sum();
237+
prop_assert_eq!(sum, TEST_TARGET_SUM);
238+
}
239+
}
240+
}
241+
}
242+
}

src/signature/generalized_xmss.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,10 @@ mod tests {
994994

995995
use super::*;
996996

997+
use crate::array::FieldArray;
998+
use p3_field::PrimeField32;
999+
use proptest::prelude::*;
1000+
9971001
use crate::{F, symmetric::tweak_hash::poseidon::PoseidonTweakHash};
9981002
use p3_field::RawDataSerializable;
9991003
use rand::rng;
@@ -1535,4 +1539,71 @@ mod tests {
15351539
// Verify signature from decoded key validates
15361540
assert!(Sig::verify(&pk, epoch + 1, &message, &sig2));
15371541
}
1542+
1543+
proptest! {
1544+
#[test]
1545+
fn proptest_expand_activation_time_invariants(
1546+
desired_start in 0usize..256,
1547+
desired_duration in 1usize..256
1548+
) {
1549+
const LOG_LIFETIME: usize = 8;
1550+
const C: usize = 1 << (LOG_LIFETIME / 2);
1551+
const LIFETIME: usize = 1 << LOG_LIFETIME;
1552+
1553+
let desired_end = (desired_start + desired_duration).min(LIFETIME);
1554+
1555+
let (start, end) = expand_activation_time::<LOG_LIFETIME>(desired_start, desired_duration);
1556+
1557+
let actual_start = start * C;
1558+
let actual_end = end * C;
1559+
1560+
// check minimum duration of 2 bottom trees (each tree has C leaves)
1561+
prop_assert!(actual_end - actual_start >= 2 * C);
1562+
1563+
// check result fits within lifetime
1564+
prop_assert!(actual_end <= LIFETIME);
1565+
1566+
// check result contains the desired interval
1567+
prop_assert!(actual_start <= desired_start);
1568+
prop_assert!(actual_end >= desired_end);
1569+
1570+
// check determinism by calling twice
1571+
let (start2, end2) = expand_activation_time::<LOG_LIFETIME>(desired_start, desired_duration);
1572+
prop_assert_eq!((start, end), (start2, end2));
1573+
}
1574+
1575+
#[test]
1576+
fn proptest_ssz_public_key_roundtrip_and_determinism(
1577+
root_values in prop::collection::vec(0u32..F::ORDER_U32, 7),
1578+
param_values in prop::collection::vec(0u32..F::ORDER_U32, 5)
1579+
) {
1580+
// build public key from random field element values
1581+
let root_arr: [F; 7] = std::array::from_fn(|i| F::new(root_values[i]));
1582+
let param_arr: [F; 5] = std::array::from_fn(|i| F::new(param_values[i]));
1583+
1584+
let original = GeneralizedXMSSPublicKey::<TestTH> {
1585+
root: FieldArray(root_arr),
1586+
parameter: FieldArray(param_arr),
1587+
};
1588+
1589+
// encode to SSZ bytes
1590+
let encoded1 = original.as_ssz_bytes();
1591+
let encoded2 = original.as_ssz_bytes();
1592+
1593+
// check encoding is deterministic
1594+
prop_assert_eq!(&encoded1, &encoded2);
1595+
1596+
// check size matches expected (7 + 5 field elements * 4 bytes)
1597+
let expected_size = 12 * F::NUM_BYTES;
1598+
prop_assert_eq!(encoded1.len(), expected_size);
1599+
prop_assert_eq!(original.ssz_bytes_len(), expected_size);
1600+
1601+
// decode and check roundtrip preserves data
1602+
let decoded = GeneralizedXMSSPublicKey::<TestTH>::from_ssz_bytes(&encoded1)
1603+
.expect("valid SSZ bytes should decode");
1604+
1605+
prop_assert_eq!(original.root, decoded.root);
1606+
prop_assert_eq!(original.parameter, decoded.parameter);
1607+
}
1608+
}
15381609
}

src/symmetric/message_hash/poseidon.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ pub type PoseidonMessageHashW1 = PoseidonMessageHash<5, 5, 5, 163, 2, 2, 9>;
232232
mod tests {
233233
use super::*;
234234
use num_traits::Zero;
235+
use p3_field::PrimeField32;
236+
use proptest::prelude::*;
235237
use rand::Rng;
236238
use std::collections::HashMap;
237239

@@ -610,4 +612,100 @@ mod tests {
610612
"Reconstructed bigint from chunks does not match bigint from field elements"
611613
);
612614
}
615+
616+
proptest! {
617+
#[test]
618+
fn proptest_apply_determinism_and_output_validity(
619+
message in prop::array::uniform32(any::<u8>()),
620+
param_values in prop::collection::vec(0u32..F::ORDER_U32, 4),
621+
rand_values in prop::collection::vec(0u32..F::ORDER_U32, 4),
622+
epoch in any::<u32>()
623+
) {
624+
// build parameter and randomness from proptest values
625+
let param_arr: [F; 4] = std::array::from_fn(|i| F::new(param_values[i]));
626+
let parameter = FieldArray(param_arr);
627+
let rand_arr: [F; 4] = std::array::from_fn(|i| F::new(rand_values[i]));
628+
let randomness = FieldArray(rand_arr);
629+
630+
// call apply twice to check determinism
631+
let result1 = PoseidonMessageHash445::apply(&parameter, epoch, &randomness, &message);
632+
let result2 = PoseidonMessageHash445::apply(&parameter, epoch, &randomness, &message);
633+
634+
// check determinism
635+
prop_assert_eq!(&result1, &result2);
636+
637+
// check output dimension
638+
prop_assert_eq!(result1.len(), PoseidonMessageHash445::DIMENSION);
639+
640+
// check all chunks are in valid range [0, BASE-1]
641+
for &chunk in &result1 {
642+
prop_assert!((chunk as usize) < PoseidonMessageHash445::BASE);
643+
}
644+
645+
// check different epochs produce different results
646+
let other_epoch = PoseidonMessageHash445::apply(
647+
&parameter,
648+
epoch.wrapping_add(1),
649+
&randomness,
650+
&message,
651+
);
652+
prop_assert_ne!(&result1[..], &other_epoch[..]);
653+
}
654+
655+
#[test]
656+
fn proptest_encode_epoch_properties(
657+
epoch1 in any::<u32>(),
658+
epoch2 in any::<u32>()
659+
) {
660+
// check encoding is deterministic
661+
let result1 = encode_epoch::<4>(epoch1);
662+
let result2 = encode_epoch::<4>(epoch1);
663+
prop_assert_eq!(result1, result2);
664+
665+
// check output has correct length
666+
prop_assert_eq!(result1.len(), 4);
667+
668+
// check different epochs produce different encodings
669+
let other = encode_epoch::<4>(epoch2);
670+
if epoch1 == epoch2 {
671+
prop_assert_eq!(result1, other);
672+
} else {
673+
prop_assert_ne!(result1, other);
674+
}
675+
676+
// check zero epoch produces encoding with separator only (first element non-zero)
677+
if epoch1 == 0 {
678+
// epoch=0 should still produce non-trivial encoding due to separator
679+
let has_nonzero = result1.iter().any(|&x| x != F::ZERO);
680+
prop_assert!(has_nonzero);
681+
}
682+
}
683+
684+
#[test]
685+
fn proptest_encode_message_properties(
686+
message1 in prop::array::uniform32(any::<u8>()),
687+
message2 in prop::array::uniform32(any::<u8>())
688+
) {
689+
// check encoding is deterministic
690+
let result1 = encode_message::<9>(&message1);
691+
let result2 = encode_message::<9>(&message1);
692+
prop_assert_eq!(result1, result2);
693+
694+
// check output has correct length
695+
prop_assert_eq!(result1.len(), 9);
696+
697+
// check different messages produce different encodings
698+
let other = encode_message::<9>(&message2);
699+
if message1 == message2 {
700+
prop_assert_eq!(result1, other);
701+
} else {
702+
prop_assert_ne!(result1, other);
703+
}
704+
705+
// check zero message produces zero encoding
706+
let zero_msg = [0u8; 32];
707+
let zero_result = encode_message::<9>(&zero_msg);
708+
prop_assert!(zero_result.iter().all(|&x| x == F::ZERO));
709+
}
710+
}
613711
}

0 commit comments

Comments
 (0)