use crate::chain::{recompute_chain, FORMAT_VERSION};
use crate::types::{CheckOutcome, ProfileId, Receipt, Verdict};
use std::collections::BTreeSet;
const STANDARD_VERSION: &str = FORMAT_VERSION;
const BLAKE3_HEX_LEN: usize = 64;
fn is_well_formed_hash(hex: &str) -> bool {
hex.len() == BLAKE3_HEX_LEN
&& hex
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
}
pub fn verify(receipt: &Receipt) -> Verdict {
let outcomes: Vec<CheckOutcome> = vec![
stage_decode(receipt),
stage_check_format(receipt),
stage_chain_integrity(receipt),
stage_continuity(receipt),
stage_verify_commitments(receipt),
stage_evaluate_profile(receipt),
];
let first_failure = outcomes.iter().find(|o| !o.passed);
let accepted = first_failure.is_none();
let reason = match first_failure {
Some(o) => format!("{}: {}", o.stage, o.detail),
None => "all stages passed".to_string(),
};
Verdict {
accepted,
profile: ProfileId::CoreV1,
outcomes,
reason,
}
}
fn stage_decode(receipt: &Receipt) -> CheckOutcome {
let passed = !receipt.format_version.trim().is_empty();
let detail = if passed {
format!("{} event(s), format_version present", receipt.events.len())
} else {
"format_version is empty or unparseable".to_string()
};
CheckOutcome {
stage: "decode".to_string(),
passed,
detail,
}
}
fn stage_check_format(receipt: &Receipt) -> CheckOutcome {
let passed = receipt.format_version == STANDARD_VERSION;
let detail = if passed {
format!("format_version == {STANDARD_VERSION}")
} else {
format!(
"expected format_version {STANDARD_VERSION}, found {}",
receipt.format_version
)
};
CheckOutcome {
stage: "check_format".to_string(),
passed,
detail,
}
}
fn stage_chain_integrity(receipt: &Receipt) -> CheckOutcome {
match recompute_chain(&receipt.events) {
Ok(computed) => {
let passed = computed == receipt.chain_hash;
let detail = if passed {
"recomputed chain hash matches stored chain_hash".to_string()
} else {
format!(
"chain hash mismatch: stored {}, recomputed {}",
receipt.chain_hash, computed
)
};
CheckOutcome {
stage: "chain_integrity".to_string(),
passed,
detail,
}
}
Err(e) => CheckOutcome {
stage: "chain_integrity".to_string(),
passed: false,
detail: format!("could not canonicalize an event: {e}"),
},
}
}
fn stage_continuity(receipt: &Receipt) -> CheckOutcome {
let mut seen_ids: BTreeSet<&str> = BTreeSet::new();
for (index, event) in receipt.events.iter().enumerate() {
let expected_seq = index as u64;
if event.seq != expected_seq {
return CheckOutcome {
stage: "continuity".to_string(),
passed: false,
detail: format!(
"seq gap at position {index}: expected {expected_seq}, found {}",
event.seq
),
};
}
if !seen_ids.insert(event.id.as_str()) {
return CheckOutcome {
stage: "continuity".to_string(),
passed: false,
detail: format!("duplicate event id: {}", event.id),
};
}
}
CheckOutcome {
stage: "continuity".to_string(),
passed: true,
detail: format!(
"{} event(s) with contiguous seq and unique ids",
receipt.events.len()
),
}
}
fn stage_verify_commitments(receipt: &Receipt) -> CheckOutcome {
for event in &receipt.events {
let hex = event.payload_commitment.as_hex();
if !is_well_formed_hash(hex) {
return CheckOutcome {
stage: "verify_commitments".to_string(),
passed: false,
detail: format!(
"event {} has a malformed commitment (expected {BLAKE3_HEX_LEN} lowercase hex chars)",
event.id
),
};
}
}
CheckOutcome {
stage: "verify_commitments".to_string(),
passed: true,
detail: "all commitments are well-formed BLAKE3 digests".to_string(),
}
}
fn stage_evaluate_profile(receipt: &Receipt) -> CheckOutcome {
for event in &receipt.events {
if event.event_type.trim().is_empty() {
return CheckOutcome {
stage: "evaluate_profile".to_string(),
passed: false,
detail: format!("event {} has an empty event_type", event.id),
};
}
if event.payload_commitment.as_hex().is_empty() {
return CheckOutcome {
stage: "evaluate_profile".to_string(),
passed: false,
detail: format!("event {} is missing a commitment", event.id),
};
}
}
CheckOutcome {
stage: "evaluate_profile".to_string(),
passed: true,
detail: format!("profile {} satisfied", ProfileId::CoreV1.as_str()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Blake3Hash, ObjectRef, OperationEvent};
fn event(id: &str, seq: u64, event_type: &str, payload: &[u8]) -> OperationEvent {
OperationEvent {
id: id.to_string(),
seq,
event_type: event_type.to_string(),
objects: vec![ObjectRef {
id: format!("obj-{id}"),
obj_type: "artifact".to_string(),
qualifier: None,
}],
payload_commitment: Blake3Hash::from_bytes(payload),
}
}
fn valid_receipt() -> Receipt {
let mut asm = crate::chain::ChainAssembler::new();
asm.append(event("e0", 0, "emit", b"payload-zero"))
.expect("append event");
asm.append(event("e1", 1, "emit", b"payload-one"))
.expect("append event");
asm.finalize()
}
#[test]
fn verif_valid_receipt_accepts() {
let verdict = verify(&valid_receipt());
assert!(verdict.accepted, "reason: {}", verdict.reason);
assert_eq!(verdict.reason, "all stages passed");
assert!(verdict.outcomes.iter().all(|o| o.passed));
}
#[test]
fn verif_pure_deterministic() {
let r = valid_receipt();
assert_eq!(verify(&r), verify(&r));
}
#[test]
fn verif_flipped_commitment_breaks_chain_integrity() {
let mut r = valid_receipt();
r.events[1].payload_commitment = Blake3Hash::from_bytes(b"tampered");
let verdict = verify(&r);
assert!(!verdict.accepted);
let chain = verdict
.outcomes
.iter()
.find(|o| o.stage == "chain_integrity")
.expect("chain_integrity stage present");
assert!(!chain.passed, "chain integrity must catch the tamper");
}
#[test]
fn verif_seq_gap_fails_continuity() {
let mut r = valid_receipt();
r.events[1].seq = 2; let verdict = verify(&r);
assert!(!verdict.accepted);
let cont = verdict
.outcomes
.iter()
.find(|o| o.stage == "continuity")
.expect("continuity stage present");
assert!(!cont.passed);
}
#[test]
fn verif_wrong_format_fails_check_format() {
let mut r = valid_receipt();
r.format_version = "1.0.0".to_string();
let verdict = verify(&r);
assert!(!verdict.accepted);
let fmt = verdict
.outcomes
.iter()
.find(|o| o.stage == "check_format")
.expect("check_format stage present");
assert!(!fmt.passed);
}
#[test]
fn verif_duplicate_id_fails_continuity() {
let mut r = valid_receipt();
r.events[1].id = r.events[0].id.clone();
r.chain_hash = recompute_chain(&r.events).expect("canonicalize");
let verdict = verify(&r);
let cont = verdict
.outcomes
.iter()
.find(|o| o.stage == "continuity")
.expect("continuity stage present");
assert!(!cont.passed);
}
}