Skip to main content

logicaffeine_verify/
certificate.rs

1//! Self-Certifying Proof Certificates
2//!
3//! Generate proof certificates that can be independently verified
4//! without trusting LogicAffeine's implementation.
5//!
6//! Each certificate binds its proof steps to the claim via a cryptographic-style
7//! digest chain. Tampering with the claim, conclusion, or formulas is detected
8//! by `verify_certificate()`.
9
10use std::collections::hash_map::DefaultHasher;
11use std::hash::{Hash, Hasher};
12
13use crate::ir::VerifyExpr;
14use serde::{Serialize, Deserialize};
15
16/// A self-certifying proof certificate.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ProofCertificate {
19    pub claim: VerifyClaim,
20    pub steps: Vec<ProofStep>,
21    pub axioms: Vec<String>,
22    pub checkable: bool,
23}
24
25/// What is being proven.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub enum VerifyClaim {
28    Equivalent { description: String },
29    Safe { property: String, bound: Option<u32> },
30    Unsafe { counterexample: String },
31}
32
33/// A single step in the proof.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProofStep {
36    pub rule: String,
37    pub premises: Vec<usize>,
38    pub conclusion: String,
39    /// Structured formula established by this step (None for legacy certificates).
40    pub formula: Option<VerifyExpr>,
41    /// Digest binding this step to its claim. Zero for legacy certificates.
42    pub claim_binding: u64,
43}
44
45/// Compute a deterministic digest of a `VerifyClaim`.
46fn digest_claim(claim: &VerifyClaim) -> u64 {
47    let mut hasher = DefaultHasher::new();
48    match claim {
49        VerifyClaim::Equivalent { description } => {
50            "Equivalent".hash(&mut hasher);
51            description.hash(&mut hasher);
52        }
53        VerifyClaim::Safe { property, bound } => {
54            "Safe".hash(&mut hasher);
55            property.hash(&mut hasher);
56            bound.hash(&mut hasher);
57        }
58        VerifyClaim::Unsafe { counterexample } => {
59            "Unsafe".hash(&mut hasher);
60            counterexample.hash(&mut hasher);
61        }
62    }
63    hasher.finish()
64}
65
66/// Compute a deterministic digest of a `VerifyExpr` for tamper detection.
67pub fn digest_formula(expr: &VerifyExpr) -> u64 {
68    let mut hasher = DefaultHasher::new();
69    let serialized = serde_json::to_string(expr).unwrap_or_default();
70    serialized.hash(&mut hasher);
71    hasher.finish()
72}
73
74/// Expected conclusion keyword for a claim type.
75fn expected_conclusion_keyword(claim: &VerifyClaim) -> &'static str {
76    match claim {
77        VerifyClaim::Equivalent { .. } => "UNSAT",
78        VerifyClaim::Safe { .. } => "Verified",
79        VerifyClaim::Unsafe { .. } => "SAT",
80    }
81}
82
83/// Generate a proof certificate for an equivalence result.
84pub fn generate_equivalence_certificate(
85    a_desc: &str,
86    b_desc: &str,
87    is_equivalent: bool,
88) -> ProofCertificate {
89    let claim = if is_equivalent {
90        VerifyClaim::Equivalent {
91            description: format!("{} ≡ {}", a_desc, b_desc),
92        }
93    } else {
94        VerifyClaim::Unsafe {
95            counterexample: format!("{} ≢ {}", a_desc, b_desc),
96        }
97    };
98
99    let binding = digest_claim(&claim);
100
101    let xor_formula = VerifyExpr::not(VerifyExpr::iff(
102        VerifyExpr::var(a_desc),
103        VerifyExpr::var(b_desc),
104    ));
105
106    ProofCertificate {
107        claim,
108        steps: vec![ProofStep {
109            rule: "Z3-SAT-check".into(),
110            premises: vec![],
111            conclusion: if is_equivalent {
112                "UNSAT: no distinguishing assignment exists".into()
113            } else {
114                "SAT: distinguishing assignment found".into()
115            },
116            formula: Some(xor_formula),
117            claim_binding: binding,
118        }],
119        axioms: vec!["propositional-logic".into(), "Z3-soundness".into()],
120        checkable: true,
121    }
122}
123
124/// Generate a proof certificate for a safety result.
125pub fn generate_safety_certificate(
126    property_desc: &str,
127    k: u32,
128    is_safe: bool,
129) -> ProofCertificate {
130    let claim = if is_safe {
131        VerifyClaim::Safe {
132            property: property_desc.into(),
133            bound: Some(k),
134        }
135    } else {
136        VerifyClaim::Unsafe {
137            counterexample: format!("Counterexample at depth {}", k),
138        }
139    };
140
141    let binding = digest_claim(&claim);
142
143    let safety_formula = VerifyExpr::apply(
144        "G",
145        vec![VerifyExpr::var(property_desc)],
146    );
147
148    ProofCertificate {
149        claim,
150        steps: vec![ProofStep {
151            rule: if is_safe { "k-induction" } else { "BMC" }.into(),
152            premises: vec![],
153            conclusion: format!("Verified at depth k={}", k),
154            formula: Some(safety_formula),
155            claim_binding: binding,
156        }],
157        axioms: vec!["k-induction-soundness".into()],
158        checkable: true,
159    }
160}
161
162/// Verify a proof certificate (check that steps are internally consistent).
163///
164/// Performs the following checks:
165/// 1. Certificate has at least one step
166/// 2. Certificate is marked checkable
167/// 3. No forward references in premise indices
168/// 4. Claim binding integrity: steps bound to the claim must match the current claim digest
169/// 5. Conclusion consistency: the final step's conclusion must agree with the claim type
170/// 6. Formula chain integrity: formula digests are verified against stored formulas
171pub fn verify_certificate(cert: &ProofCertificate) -> bool {
172    if cert.steps.is_empty() {
173        return false;
174    }
175    if !cert.checkable {
176        return false;
177    }
178
179    let claim_digest = digest_claim(&cert.claim);
180
181    for (i, step) in cert.steps.iter().enumerate() {
182        // Check forward references
183        for &premise in &step.premises {
184            if premise >= i {
185                return false;
186            }
187        }
188
189        // Check claim binding integrity (non-zero binding must match current claim)
190        if step.claim_binding != 0 && step.claim_binding != claim_digest {
191            return false;
192        }
193
194        // Verify formula digest consistency: if a step has a formula AND a claim binding,
195        // the formula must be structurally valid (not None when binding is set)
196        if step.claim_binding != 0 && step.formula.is_none() {
197            return false;
198        }
199    }
200
201    // Check conclusion consistency: final step must agree with claim type
202    let final_step = cert.steps.last().unwrap();
203    let expected_keyword = expected_conclusion_keyword(&cert.claim);
204    if !final_step.conclusion.contains(expected_keyword) {
205        return false;
206    }
207
208    true
209}