#![cfg(feature = "projection")]
use proptest::prelude::*;
use rustbinary::{prove, verify, ProjectedRecord, Projection, ProjectionLimits, RecordBuilder};
fn record_strategy() -> impl Strategy<Value = Vec<(u32, Vec<u8>)>> {
prop::collection::vec(
(any::<u32>(), prop::collection::vec(any::<u8>(), 0..64)),
1..64,
)
.prop_map(|mut pairs| {
pairs.sort_unstable_by_key(|(id, _)| *id);
pairs.dedup_by_key(|(id, _)| *id);
pairs
})
}
fn build(pairs: &[(u32, Vec<u8>)]) -> (Vec<u8>, [u8; 32]) {
let mut builder = RecordBuilder::new(7);
for (id, payload) in pairs {
builder.insert(*id, payload).unwrap();
}
let record = builder.finish().unwrap();
let parsed = ProjectedRecord::parse(&record, &ProjectionLimits::new()).unwrap();
let anchor = *parsed.root();
drop(parsed);
(record, anchor)
}
proptest! {
#[test]
fn prove_verify_returns_exact_projection(
pairs in record_strategy(),
query_mask in any::<u64>(),
) {
let (record, anchor) = build(&pairs);
let all_ids: Vec<u32> = pairs.iter().map(|(id, _)| *id).collect();
let query = Projection::new(
all_ids
.iter()
.enumerate()
.filter(|(i, _)| query_mask & (1u64 << (i % 64)) != 0)
.map(|(_, id)| *id),
);
if query.is_empty() {
prop_assert!(matches!(
prove(&record, &query, &ProjectionLimits::new()),
Err(rustbinary::ProjectionError::InvalidQuery)
));
return Ok(());
}
let proof = prove(&record, &query, &ProjectionLimits::new()).unwrap();
let verified = verify(&proof, &anchor, 7).unwrap();
let expected: Vec<(u32, &[u8])> = pairs
.iter()
.filter(|(id, _)| query.contains(*id))
.map(|(id, payload)| (*id, payload.as_slice()))
.collect();
prop_assert_eq!(verified.len(), expected.len());
for (v, (eid, epayload)) in verified.iter().zip(expected.iter()) {
prop_assert_eq!(v.field_id, *eid);
prop_assert_eq!(v.payload, *epayload);
}
}
#[test]
fn any_single_byte_tamper_is_detected(
pairs in record_strategy(),
pos in any::<usize>(),
) {
let (mut record, anchor) = build(&pairs);
let body_len = record.len() - 32;
if body_len == 0 {
return Ok(());
}
let pos = pos % body_len;
record[pos] ^= 0x01;
let query = Projection::new(pairs.iter().map(|(id, _)| *id));
if let Ok(parsed) = ProjectedRecord::parse(&record, &ProjectionLimits::new()) {
match parsed.prove(&query) {
Ok(proof) => {
let result = verify(&proof, &anchor, 7);
prop_assert!(
result.is_err(),
"tamper at byte {pos} was not detected"
);
}
Err(_) => {
}
}
}
}
#[test]
fn arbitrary_bytes_never_panic(data in prop::collection::vec(any::<u8>(), 0..512)) {
let result = std::panic::catch_unwind(|| {
ProjectedRecord::parse(&data, &ProjectionLimits::new())
});
prop_assert!(result.is_ok(), "parse panicked on arbitrary bytes");
}
#[test]
fn stored_root_matches_recomputed_root(pairs in record_strategy()) {
let (record, _) = build(&pairs);
let parsed = ProjectedRecord::parse(&record, &ProjectionLimits::new()).unwrap();
prop_assert!(parsed.verify_root().is_ok());
let proof = parsed
.prove(&Projection::new(pairs.iter().map(|(id, _)| *id)))
.unwrap();
prop_assert_eq!(proof.claimed_root(), parsed.root());
}
}