use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::policy::{compose_policy_outcomes, PolicyContribution, PolicyDecision, PolicyOutcome};
use crate::{CoreError, CoreResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProofState {
FullChainVerified,
Partial,
Broken,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProofEdgeKind {
HashChain,
Signature,
IdentityRotation,
ExternalAnchor,
LineageClosure,
AuthorityFold,
ContextPackLink,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProofEdge {
pub kind: ProofEdgeKind,
pub from_ref: String,
pub to_ref: String,
pub evidence_ref: Option<String>,
}
impl ProofEdge {
#[must_use]
pub fn new(
kind: ProofEdgeKind,
from_ref: impl Into<String>,
to_ref: impl Into<String>,
) -> Self {
Self {
kind,
from_ref: from_ref.into(),
to_ref: to_ref.into(),
evidence_ref: None,
}
}
#[must_use]
pub fn with_evidence_ref(mut self, evidence_ref: impl Into<String>) -> Self {
self.evidence_ref = Some(evidence_ref.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProofEdgeFailure {
Missing,
Unresolved,
Mismatch,
InvalidSignature,
AnchorMismatch,
AuthorityMismatch,
}
impl ProofEdgeFailure {
#[must_use]
pub const fn is_broken(self) -> bool {
match self {
Self::Missing | Self::Unresolved => false,
Self::Mismatch
| Self::InvalidSignature
| Self::AnchorMismatch
| Self::AuthorityMismatch => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct FailingEdge {
pub kind: ProofEdgeKind,
pub from_ref: String,
pub to_ref: Option<String>,
pub failure: ProofEdgeFailure,
pub reason: String,
}
impl FailingEdge {
#[must_use]
pub fn missing(
kind: ProofEdgeKind,
from_ref: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self {
kind,
from_ref: from_ref.into(),
to_ref: None,
failure: ProofEdgeFailure::Missing,
reason: reason.into(),
}
}
#[must_use]
pub fn unresolved(
kind: ProofEdgeKind,
from_ref: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self {
kind,
from_ref: from_ref.into(),
to_ref: None,
failure: ProofEdgeFailure::Unresolved,
reason: reason.into(),
}
}
#[must_use]
pub fn broken(
kind: ProofEdgeKind,
from_ref: impl Into<String>,
to_ref: impl Into<String>,
failure: ProofEdgeFailure,
reason: impl Into<String>,
) -> Self {
debug_assert!(failure.is_broken());
let failure = if failure.is_broken() {
failure
} else {
ProofEdgeFailure::Mismatch
};
Self {
kind,
from_ref: from_ref.into(),
to_ref: Some(to_ref.into()),
failure,
reason: reason.into(),
}
}
#[must_use]
pub const fn is_broken(&self) -> bool {
self.failure.is_broken()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProofClosureReport {
#[serde(rename = "proof_state")]
state: ProofState,
verified_edges: Vec<ProofEdge>,
failing_edges: Vec<FailingEdge>,
}
impl ProofClosureReport {
#[must_use]
pub fn full_chain_verified(verified_edges: Vec<ProofEdge>) -> Self {
Self {
state: ProofState::FullChainVerified,
verified_edges,
failing_edges: Vec::new(),
}
}
#[must_use]
pub fn from_edges(verified_edges: Vec<ProofEdge>, failing_edges: Vec<FailingEdge>) -> Self {
let state = classify_failures(&failing_edges);
Self {
state,
verified_edges,
failing_edges,
}
}
#[must_use]
pub const fn state(&self) -> ProofState {
self.state
}
#[must_use]
pub fn verified_edges(&self) -> &[ProofEdge] {
&self.verified_edges
}
#[must_use]
pub fn failing_edges(&self) -> &[FailingEdge] {
&self.failing_edges
}
#[must_use]
pub const fn is_full_chain_verified(&self) -> bool {
matches!(self.state, ProofState::FullChainVerified)
}
#[must_use]
pub const fn is_broken(&self) -> bool {
matches!(self.state, ProofState::Broken)
}
pub fn push_failing_edge(&mut self, edge: FailingEdge) {
self.failing_edges.push(edge);
self.state = classify_failures(&self.failing_edges);
}
#[must_use]
pub fn with_failing_edge(mut self, edge: FailingEdge) -> Self {
self.push_failing_edge(edge);
self
}
#[must_use]
pub fn policy_decision(&self) -> PolicyDecision {
let outcome = match self.state {
ProofState::FullChainVerified => PolicyOutcome::Allow,
ProofState::Partial => PolicyOutcome::Quarantine,
ProofState::Broken => PolicyOutcome::Reject,
};
let reason = match self.state {
ProofState::FullChainVerified => "proof closure is fully verified",
ProofState::Partial => {
"proof closure is partial and cannot be treated as clean authority"
}
ProofState::Broken => "proof closure is broken and fails closed",
};
compose_policy_outcomes(
vec![
PolicyContribution::new("proof_closure.state", outcome, reason)
.expect("static policy contribution is valid"),
],
None,
)
}
pub fn require_current_use_allowed(&self) -> CoreResult<()> {
let policy = self.policy_decision();
match policy.final_outcome {
PolicyOutcome::Reject | PolicyOutcome::Quarantine => {
Err(CoreError::Validation(format!(
"proof closure current use blocked by policy outcome {:?}",
policy.final_outcome
)))
}
PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
}
}
}
const fn classify_failures(failing_edges: &[FailingEdge]) -> ProofState {
if failing_edges.is_empty() {
return ProofState::FullChainVerified;
}
let mut i = 0;
while i < failing_edges.len() {
if failing_edges[i].is_broken() {
return ProofState::Broken;
}
i += 1;
}
ProofState::Partial
}
#[cfg(test)]
mod tests {
use super::*;
fn edge() -> ProofEdge {
ProofEdge::new(ProofEdgeKind::HashChain, "evt_a", "evt_b").with_evidence_ref("hash_ab")
}
#[test]
fn full_report_cannot_carry_failures() {
let report = ProofClosureReport::full_chain_verified(vec![edge()]);
assert_eq!(report.state(), ProofState::FullChainVerified);
assert!(report.is_full_chain_verified());
assert!(report.failing_edges().is_empty());
}
#[test]
fn missing_edge_downgrades_to_partial() {
let report = ProofClosureReport::from_edges(
vec![edge()],
vec![FailingEdge::missing(
ProofEdgeKind::ExternalAnchor,
"tip_1",
"anchor not available",
)],
);
assert_eq!(report.state(), ProofState::Partial);
assert!(!report.is_full_chain_verified());
assert!(!report.is_broken());
}
#[test]
fn broken_edge_downgrades_to_broken() {
let report = ProofClosureReport::from_edges(
vec![edge()],
vec![FailingEdge::broken(
ProofEdgeKind::Signature,
"evt_a",
"sig_a",
ProofEdgeFailure::InvalidSignature,
"signature verification failed",
)],
);
assert_eq!(report.state(), ProofState::Broken);
assert!(report.is_broken());
}
#[test]
fn adding_failure_recomputes_state() {
let mut report = ProofClosureReport::full_chain_verified(vec![edge()]);
report.push_failing_edge(FailingEdge::unresolved(
ProofEdgeKind::LineageClosure,
"mem_a",
"source event not loaded",
));
assert_eq!(report.state(), ProofState::Partial);
assert!(!report.is_full_chain_verified());
}
#[test]
fn proof_state_wire_strings_are_stable() {
assert_eq!(
serde_json::to_value(ProofState::FullChainVerified).unwrap(),
serde_json::json!("full_chain_verified")
);
assert_eq!(
serde_json::to_value(ProofState::Partial).unwrap(),
serde_json::json!("partial")
);
assert_eq!(
serde_json::to_value(ProofState::Broken).unwrap(),
serde_json::json!("broken")
);
}
#[test]
fn proof_report_serializes_proof_state_field() {
let report = ProofClosureReport::from_edges(
Vec::new(),
vec![FailingEdge::missing(
ProofEdgeKind::ExternalAnchor,
"tip_1",
"anchor not available",
)],
);
let serialized = serde_json::to_value(report).unwrap();
assert_eq!(serialized["proof_state"], serde_json::json!("partial"));
assert!(serialized.get("state").is_none());
}
#[test]
fn proof_state_derives_policy_decision() {
let full = ProofClosureReport::full_chain_verified(Vec::new());
let partial = ProofClosureReport::from_edges(
Vec::new(),
vec![FailingEdge::missing(
ProofEdgeKind::LineageClosure,
"memory:mem_01",
"source missing",
)],
);
let broken = ProofClosureReport::from_edges(
Vec::new(),
vec![FailingEdge::broken(
ProofEdgeKind::HashChain,
"event:a",
"event:b",
ProofEdgeFailure::Mismatch,
"hash mismatch",
)],
);
assert_eq!(full.policy_decision().final_outcome, PolicyOutcome::Allow);
assert_eq!(
partial.policy_decision().final_outcome,
PolicyOutcome::Quarantine
);
assert_eq!(
broken.policy_decision().final_outcome,
PolicyOutcome::Reject
);
}
#[test]
fn partial_or_broken_proof_fails_closed_for_current_use() {
let partial = ProofClosureReport::from_edges(
Vec::new(),
vec![FailingEdge::missing(
ProofEdgeKind::LineageClosure,
"memory:mem_01",
"source missing",
)],
);
let broken = ProofClosureReport::from_edges(
Vec::new(),
vec![FailingEdge::broken(
ProofEdgeKind::HashChain,
"event:a",
"event:b",
ProofEdgeFailure::Mismatch,
"hash mismatch",
)],
);
assert!(partial.require_current_use_allowed().is_err());
assert!(broken.require_current_use_allowed().is_err());
ProofClosureReport::full_chain_verified(Vec::new())
.require_current_use_allowed()
.expect("full chain proof supports current use");
}
}