use affidavit::chain::{recompute_chain, ChainAssembler, FORMAT_VERSION};
use affidavit::types::{Blake3Hash, ObjectRef, OperationEvent, Receipt, Verdict};
use affidavit::verifier::verify;
use quickcheck::{Arbitrary, Gen, QuickCheck, TestResult};
use quickcheck_macros::quickcheck;
#[derive(Clone, Debug)]
pub struct ArbitraryOperationEvent(pub OperationEvent);
impl Arbitrary for ArbitraryOperationEvent {
fn arbitrary(g: &mut Gen) -> Self {
let id = format!("ev-{}", u64::arbitrary(g));
let seq = u64::arbitrary(g);
let event_type = match u32::arbitrary(g) % 3 {
0 => "process.exec".to_string(),
1 => "artifact.sign".to_string(),
_ => "admission.gate".to_string(),
};
let mut objects = Vec::new();
let num_objs = u32::arbitrary(g) % 4;
for i in 0..num_objs {
objects.push(ObjectRef {
id: format!("obj-{}", i),
obj_type: "blob".to_string(),
qualifier: if bool::arbitrary(g) {
Some("input".to_string())
} else {
None
},
});
}
let payload: Vec<u8> = Arbitrary::arbitrary(g);
let payload_commitment = Blake3Hash::from_bytes(&payload);
ArbitraryOperationEvent(OperationEvent {
id,
seq,
event_type,
objects,
payload_commitment,
})
}
}
#[derive(Clone, Debug)]
pub struct ArbitraryReceipt(pub Receipt);
impl Arbitrary for ArbitraryReceipt {
fn arbitrary(g: &mut Gen) -> Self {
let mut asm = ChainAssembler::new();
let num_events = (u32::arbitrary(g) % 15) + 1;
for i in 0..num_events {
let mut ev = ArbitraryOperationEvent::arbitrary(g).0;
ev.seq = i as u64;
ev.id = format!("event-{}", i);
asm.append(ev)
.expect("ChainAssembler append must succeed for arbitrary events");
}
ArbitraryReceipt(asm.finalize())
}
}
#[quickcheck]
fn prop_decidability_valid_receipts_always_accepted(arb: ArbitraryReceipt) -> bool {
let receipt = arb.0;
let verdict = verify(&receipt);
if !verdict.accepted {
eprintln!(
"FAILURE: Valid receipt rejected! Reason: {}",
verdict.reason
);
for outcome in &verdict.outcomes {
if !outcome.passed {
eprintln!(" Stage {} failed: {}", outcome.stage, outcome.detail);
}
}
}
verdict.accepted
}
#[quickcheck]
fn prop_tamper_detection_commitment_flip_breaks_chain(
arb: ArbitraryReceipt,
event_idx: usize,
seed: u64,
) -> TestResult {
let mut receipt = arb.0;
if receipt.events.is_empty() {
return TestResult::discard();
}
let idx = event_idx % receipt.events.len();
receipt.events[idx].payload_commitment = Blake3Hash::from_bytes(&seed.to_le_bytes());
let verdict = verify(&receipt);
let chain_stage = verdict
.outcomes
.iter()
.find(|o| o.stage == "chain_integrity")
.unwrap();
TestResult::from_bool(!verdict.accepted && !chain_stage.passed)
}
#[quickcheck]
fn prop_tamper_detection_event_type_mutation_breaks_chain(
arb: ArbitraryReceipt,
event_idx: usize,
) -> TestResult {
let mut receipt = arb.0;
if receipt.events.is_empty() {
return TestResult::discard();
}
let idx = event_idx % receipt.events.len();
receipt.events[idx].event_type += "_mutated";
let verdict = verify(&receipt);
let chain_stage = verdict
.outcomes
.iter()
.find(|o| o.stage == "chain_integrity")
.unwrap();
TestResult::from_bool(!verdict.accepted && !chain_stage.passed)
}
#[quickcheck]
fn prop_continuity_gap_is_rejected(arb: ArbitraryReceipt, event_idx: usize) -> TestResult {
let mut receipt = arb.0;
if receipt.events.len() < 2 {
return TestResult::discard();
}
let idx = (event_idx % (receipt.events.len() - 1)) + 1;
receipt.events[idx].seq += 1;
receipt.chain_hash = recompute_chain(&receipt.events).unwrap();
let verdict = verify(&receipt);
let continuity_stage = verdict
.outcomes
.iter()
.find(|o| o.stage == "continuity")
.unwrap();
TestResult::from_bool(!verdict.accepted && !continuity_stage.passed)
}
#[quickcheck]
fn prop_format_version_enforcement(arb: ArbitraryReceipt, mut version: String) -> TestResult {
let mut receipt = arb.0;
if version == FORMAT_VERSION || version.trim().is_empty() {
version = "unsupported/v99".to_string();
}
receipt.format_version = version;
let verdict = verify(&receipt);
let format_stage = verdict
.outcomes
.iter()
.find(|o| o.stage == "check_format")
.unwrap();
TestResult::from_bool(!verdict.accepted && !format_stage.passed)
}
#[quickcheck]
fn prop_duplicate_ids_are_rejected(arb: ArbitraryReceipt) -> TestResult {
let mut receipt = arb.0;
if receipt.events.len() < 2 {
return TestResult::discard();
}
receipt.events[1].id = receipt.events[0].id.clone();
receipt.chain_hash = recompute_chain(&receipt.events).unwrap();
let verdict = verify(&receipt);
let continuity_stage = verdict
.outcomes
.iter()
.find(|o| o.stage == "continuity")
.unwrap();
TestResult::from_bool(!verdict.accepted && !continuity_stage.passed)
}
#[quickcheck]
fn prop_empty_event_type_rejected(arb: ArbitraryReceipt, event_idx: usize) -> TestResult {
let mut receipt = arb.0;
if receipt.events.is_empty() {
return TestResult::discard();
}
let idx = event_idx % receipt.events.len();
receipt.events[idx].event_type = " ".to_string();
receipt.chain_hash = recompute_chain(&receipt.events).unwrap();
let verdict = verify(&receipt);
let profile_stage = verdict
.outcomes
.iter()
.find(|o| o.stage == "evaluate_profile")
.unwrap();
TestResult::from_bool(!verdict.accepted && !profile_stage.passed)
}
#[quickcheck]
fn prop_verdict_determinism(arb: ArbitraryReceipt) -> bool {
let receipt = arb.0;
let v1 = verify(&receipt);
let v2 = verify(&receipt);
v1 == v2
}
fn main() {
println!("Running property tests...");
}