use crate::digest::{decision_digest, digest_to_hex, input_digest, policy_digest};
use crate::kernel::{KernelDecision, KernelInput, PolicySnapshot};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VerifyResult {
Valid,
Mismatch {
expected: KernelDecision,
actual: KernelDecision,
},
DigestMismatch {
expected_hex: String,
actual_hex: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestDecodeError {
InvalidHexCharacter { digit: u8, index: usize },
OddLength,
InvalidStringLength,
}
impl std::fmt::Display for DigestDecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidHexCharacter { digit, index } => {
write!(f, "invalid hex digit 0x{digit:02x} at index {index}")
}
Self::OddLength => write!(f, "odd hex string length"),
Self::InvalidStringLength => write!(f, "expected 64 hex characters"),
}
}
}
impl std::error::Error for DigestDecodeError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AuditBundle {
pub policy_digest_hex: String,
pub input_digest_hex: String,
pub decision_digest_hex: String,
pub replay_valid: bool,
}
impl AuditBundle {
pub fn policy_digest(&self) -> Result<[u8; 32], DigestDecodeError> {
decode_hex32(&self.policy_digest_hex)
}
pub fn input_digest(&self) -> Result<[u8; 32], DigestDecodeError> {
decode_hex32(&self.input_digest_hex)
}
pub fn decision_digest(&self) -> Result<[u8; 32], DigestDecodeError> {
decode_hex32(&self.decision_digest_hex)
}
}
pub fn audit_bundle(
snapshot: &PolicySnapshot,
input: KernelInput,
decision: &KernelDecision,
) -> AuditBundle {
let policy = policy_digest(snapshot);
let input_d = input_digest(&input);
let decision_d = decision_digest(decision);
let replayed = snapshot.prescribe(input);
AuditBundle {
policy_digest_hex: digest_to_hex(&policy),
input_digest_hex: digest_to_hex(&input_d),
decision_digest_hex: digest_to_hex(&decision_d),
replay_valid: replayed == *decision,
}
}
pub fn verify_decision(
snapshot: &PolicySnapshot,
input: KernelInput,
decision: &KernelDecision,
) -> VerifyResult {
let replayed = snapshot.prescribe(input);
let expected_d = decision_digest(&replayed);
let actual_d = decision_digest(decision);
if expected_d != actual_d {
return VerifyResult::DigestMismatch {
expected_hex: digest_to_hex(&expected_d),
actual_hex: digest_to_hex(&actual_d),
};
}
if replayed == *decision {
VerifyResult::Valid
} else {
VerifyResult::Mismatch {
expected: replayed,
actual: *decision,
}
}
}
pub fn snapshot_fingerprint(snapshot: &PolicySnapshot) -> String {
digest_to_hex(&policy_digest(snapshot))
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CorrectnessCertificate {
pub policy_fingerprint: String,
pub input_fingerprint: String,
pub decision_fingerprint: String,
pub decision_sequence: u64,
pub selected_model_id: u32,
pub action: String,
pub reason: String,
pub replay_valid: bool,
pub evaluated_models: u16,
pub eligible_models: u16,
pub counterfactual_model_id: u32,
pub counterfactual_utility_microunits: i64,
}
pub fn certify_decision(
snapshot: &PolicySnapshot,
input: KernelInput,
decision: &KernelDecision,
) -> CorrectnessCertificate {
let bundle = audit_bundle(snapshot, input, decision);
CorrectnessCertificate {
policy_fingerprint: bundle.policy_digest_hex,
input_fingerprint: bundle.input_digest_hex,
decision_fingerprint: bundle.decision_digest_hex,
decision_sequence: decision.request_sequence,
selected_model_id: decision.selected_model_id,
action: format!("{}", decision.action),
reason: format!("{}", decision.reason),
replay_valid: bundle.replay_valid,
evaluated_models: decision.evaluated_models,
eligible_models: decision.eligible_models,
counterfactual_model_id: decision.counterfactual_model_id,
counterfactual_utility_microunits: decision.counterfactual_utility_microunits,
}
}
pub fn counterfactual_utility(
snapshot: &PolicySnapshot,
input: KernelInput,
alt_model_id: u32,
) -> Option<i64> {
snapshot.utility_for_model(input, alt_model_id)
}
fn decode_hex32(hex: &str) -> Result<[u8; 32], DigestDecodeError> {
if hex.len() % 2 != 0 {
return Err(DigestDecodeError::OddLength);
}
let mut out = Vec::with_capacity(hex.len() / 2);
let bytes = hex.as_bytes();
for i in (0..bytes.len()).step_by(2) {
let hi = from_hex_digit(bytes[i], i)?;
let lo = from_hex_digit(bytes[i + 1], i + 1)?;
out.push((hi << 4) | lo);
}
if out.len() != 32 {
return Err(DigestDecodeError::InvalidStringLength);
}
let mut digest = [0_u8; 32];
digest.copy_from_slice(&out);
Ok(digest)
}
fn from_hex_digit(byte: u8, index: usize) -> Result<u8, DigestDecodeError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(DigestDecodeError::InvalidHexCharacter { digit: byte, index }),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::*;
fn test_snapshot() -> PolicySnapshot {
PolicySnapshot::try_new(
1,
1,
9600,
5500,
3500,
2,
vec![
KernelModel {
model_id: 1,
provider_id: 0,
quality_bps: 9500,
risk_ceiling_bps: 9500,
enabled: 1,
p95_latency_ms: 450,
capabilities: 0,
region_mask: ALL_REGIONS,
input_cost_microunits_per_million_tokens: 250,
output_cost_microunits_per_million_tokens: 1000,
},
KernelModel {
model_id: 2,
provider_id: 1,
quality_bps: 7000,
risk_ceiling_bps: 9500,
enabled: 1,
p95_latency_ms: 90,
capabilities: 0,
region_mask: ALL_REGIONS,
input_cost_microunits_per_million_tokens: 25,
output_cost_microunits_per_million_tokens: 125,
},
],
)
.expect("valid snapshot")
}
fn test_input() -> KernelInput {
KernelInput {
request_sequence: 1,
requested_model_id: 1,
input_tokens: 1000,
output_tokens: 500,
business_value_microunits: 100_000,
budget_limit_microunits: 50_000_000,
risk_bps: 1000,
confidence_bps: 9000,
minimum_quality_bps: 5000,
max_p95_latency_ms: 1000,
required_capabilities: 0,
allowed_provider_mask: ALL_PROVIDERS,
required_region_mask: 0,
}
}
#[test]
fn valid_decision_verifies() {
let snap = test_snapshot();
let input = test_input();
let decision = snap.prescribe(input);
assert_eq!(
verify_decision(&snap, input, &decision),
VerifyResult::Valid
);
}
#[test]
fn tampered_counterfactual_detected() {
let snap = test_snapshot();
let input = test_input();
let mut decision = snap.prescribe(input);
decision.counterfactual_utility_microunits += 1;
assert!(matches!(
verify_decision(&snap, input, &decision),
VerifyResult::DigestMismatch { .. } | VerifyResult::Mismatch { .. }
));
}
#[test]
fn audit_bundle_binds_input() {
let snap = test_snapshot();
let input = test_input();
let decision = snap.prescribe(input);
let bundle = audit_bundle(&snap, input, &decision);
assert!(bundle.replay_valid);
assert_eq!(bundle.policy_digest_hex.len(), 64);
assert_eq!(bundle.input_digest_hex.len(), 64);
assert_eq!(bundle.decision_digest_hex.len(), 64);
}
#[test]
fn fingerprint_matches_policy_digest() {
let snap = test_snapshot();
assert_eq!(
snapshot_fingerprint(&snap),
digest_to_hex(&policy_digest(&snap))
);
}
#[test]
fn audit_bundle_decodes_digests() {
let snap = test_snapshot();
let input = test_input();
let decision = snap.prescribe(input);
let bundle = audit_bundle(&snap, input, &decision);
assert_eq!(bundle.policy_digest().unwrap().len(), 32);
assert_eq!(bundle.input_digest().unwrap().len(), 32);
assert_eq!(bundle.decision_digest().unwrap().len(), 32);
}
#[test]
fn certificate_includes_input_fingerprint() {
let snap = test_snapshot();
let input = test_input();
let decision = snap.prescribe(input);
let cert = certify_decision(&snap, input, &decision);
assert!(cert.replay_valid);
assert_eq!(cert.input_fingerprint.len(), 64);
assert_eq!(cert.decision_fingerprint.len(), 64);
}
#[test]
fn decode_hex32_rejects_odd_length() {
let bundle = AuditBundle {
policy_digest_hex: "abc".into(),
input_digest_hex: "00".repeat(32),
decision_digest_hex: "00".repeat(32),
replay_valid: true,
};
assert_eq!(bundle.policy_digest(), Err(DigestDecodeError::OddLength));
}
#[test]
fn decode_hex32_rejects_invalid_characters() {
let bundle = AuditBundle {
policy_digest_hex: format!("{}g", "a".repeat(63)),
input_digest_hex: "00".repeat(32),
decision_digest_hex: "00".repeat(32),
replay_valid: true,
};
assert!(matches!(
bundle.policy_digest(),
Err(DigestDecodeError::InvalidHexCharacter { .. })
));
}
#[test]
fn decode_hex32_rejects_wrong_length() {
let bundle = AuditBundle {
policy_digest_hex: "00".repeat(30),
input_digest_hex: "00".repeat(32),
decision_digest_hex: "00".repeat(32),
replay_valid: true,
};
assert_eq!(
bundle.policy_digest(),
Err(DigestDecodeError::InvalidStringLength)
);
}
#[test]
fn counterfactual_utility_for_eligible_model() {
let snap = test_snapshot();
let input = test_input();
let utility = counterfactual_utility(&snap, input, 2);
assert!(utility.is_some());
assert!(utility.unwrap() > 0);
}
#[test]
fn counterfactual_utility_none_for_missing_model() {
let snap = test_snapshot();
let input = test_input();
assert!(counterfactual_utility(&snap, input, 999).is_none());
}
#[test]
fn verify_decision_wrong_policy_epoch() {
let snap = test_snapshot();
let input = test_input();
let decision = snap.prescribe(input);
let mut other = snap.clone();
other.policy_epoch = snap.policy_epoch + 1;
assert_ne!(
verify_decision(&other, input, &decision),
VerifyResult::Valid
);
}
use proptest::prelude::*;
proptest! {
#[test]
fn decode_hex32_rejects_non_hex_strings(s in "[^0-9a-fA-F]{1,20}") {
let bundle = AuditBundle {
policy_digest_hex: s,
input_digest_hex: "00".repeat(32),
decision_digest_hex: "00".repeat(32),
replay_valid: true,
};
prop_assert!(bundle.policy_digest().is_err());
}
}
}