Skip to main content

oxilean_kernel/proof_cert/
functions.rs

1//! Functions for the Proof Certificate System.
2//!
3//! Provides creation, verification, serialization, and deserialization of
4//! `ProofCertificate` values using FNV-1a structural hashing.
5
6use std::collections::HashMap;
7
8use crate::declaration::ConstantInfo;
9use crate::env::Environment;
10use crate::Expr;
11
12use super::types::{CertCheckResult, CertificateStore, ProofCertId, ProofCertificate, ProofStep};
13
14// ---------------------------------------------------------------------------
15// FNV-1a constants
16// ---------------------------------------------------------------------------
17
18const FNV1A_OFFSET: u64 = 14695981039346656037;
19const FNV1A_PRIME: u64 = 1099511628211;
20
21/// Update a FNV-1a running hash with a single byte.
22#[inline]
23fn fnv1a_byte(hash: u64, byte: u8) -> u64 {
24    (hash ^ (byte as u64)).wrapping_mul(FNV1A_PRIME)
25}
26
27/// Update a FNV-1a running hash with a u32.
28#[inline]
29fn fnv1a_u32(hash: u64, val: u32) -> u64 {
30    let bytes = val.to_le_bytes();
31    let h = fnv1a_byte(hash, bytes[0]);
32    let h = fnv1a_byte(h, bytes[1]);
33    let h = fnv1a_byte(h, bytes[2]);
34    fnv1a_byte(h, bytes[3])
35}
36
37/// Update a FNV-1a running hash with a u64.
38#[inline]
39fn fnv1a_u64(hash: u64, val: u64) -> u64 {
40    let bytes = val.to_le_bytes();
41    bytes.iter().fold(hash, |h, &b| fnv1a_byte(h, b))
42}
43
44/// Update a FNV-1a running hash with a string slice.
45#[inline]
46fn fnv1a_str(hash: u64, s: &str) -> u64 {
47    s.as_bytes().iter().fold(hash, |h, &b| fnv1a_byte(h, b))
48}
49
50// ---------------------------------------------------------------------------
51// Discriminants for Expr variants (for structurally hashing without Display)
52// ---------------------------------------------------------------------------
53
54const DISC_SORT: u8 = 0;
55const DISC_BVAR: u8 = 1;
56const DISC_FVAR: u8 = 2;
57const DISC_CONST: u8 = 3;
58const DISC_APP: u8 = 4;
59const DISC_LAM: u8 = 5;
60const DISC_PI: u8 = 6;
61const DISC_LET: u8 = 7;
62const DISC_LIT: u8 = 8;
63const DISC_PROJ: u8 = 9;
64
65// ---------------------------------------------------------------------------
66// Public API
67// ---------------------------------------------------------------------------
68
69/// Compute a structural FNV-1a hash of an expression.
70///
71/// The hash is deterministic and depends only on the structure of `e`.
72/// It is designed to distinguish structurally distinct expressions efficiently,
73/// though it is not a cryptographic hash.
74pub fn hash_expr(e: &Expr) -> u64 {
75    hash_expr_impl(e, FNV1A_OFFSET)
76}
77
78fn hash_expr_impl(e: &Expr, seed: u64) -> u64 {
79    match e {
80        Expr::Sort(level) => {
81            let h = fnv1a_byte(seed, DISC_SORT);
82            // Hash the level via its debug representation as a stable string
83            let repr = format!("{:?}", level);
84            fnv1a_str(h, &repr)
85        }
86        Expr::BVar(idx) => {
87            let h = fnv1a_byte(seed, DISC_BVAR);
88            fnv1a_u32(h, *idx)
89        }
90        Expr::FVar(fvar_id) => {
91            let h = fnv1a_byte(seed, DISC_FVAR);
92            let repr = format!("{:?}", fvar_id);
93            fnv1a_str(h, &repr)
94        }
95        Expr::Const(name, levels) => {
96            let h = fnv1a_byte(seed, DISC_CONST);
97            let h = fnv1a_str(h, &format!("{}", name));
98            levels
99                .iter()
100                .fold(h, |acc, lvl| fnv1a_str(acc, &format!("{:?}", lvl)))
101        }
102        Expr::App(f, arg) => {
103            let h = fnv1a_byte(seed, DISC_APP);
104            let h = hash_expr_impl(f, h);
105            hash_expr_impl(arg, h)
106        }
107        Expr::Lam(binfo, name, ty, body) => {
108            let h = fnv1a_byte(seed, DISC_LAM);
109            let h = fnv1a_byte(h, *binfo as u8);
110            let h = fnv1a_str(h, &format!("{}", name));
111            let h = hash_expr_impl(ty, h);
112            hash_expr_impl(body, h)
113        }
114        Expr::Pi(binfo, name, ty, body) => {
115            let h = fnv1a_byte(seed, DISC_PI);
116            let h = fnv1a_byte(h, *binfo as u8);
117            let h = fnv1a_str(h, &format!("{}", name));
118            let h = hash_expr_impl(ty, h);
119            hash_expr_impl(body, h)
120        }
121        Expr::Let(name, ty, val, body) => {
122            let h = fnv1a_byte(seed, DISC_LET);
123            let h = fnv1a_str(h, &format!("{}", name));
124            let h = hash_expr_impl(ty, h);
125            let h = hash_expr_impl(val, h);
126            hash_expr_impl(body, h)
127        }
128        Expr::Lit(lit) => {
129            let h = fnv1a_byte(seed, DISC_LIT);
130            fnv1a_str(h, &format!("{:?}", lit))
131        }
132        Expr::Proj(name, idx, expr) => {
133            let h = fnv1a_byte(seed, DISC_PROJ);
134            let h = fnv1a_str(h, &format!("{}", name));
135            let h = fnv1a_u32(h, *idx);
136            hash_expr_impl(expr, h)
137        }
138    }
139}
140
141/// Compute a structural FNV-1a hash of a `ConstantInfo` declaration.
142///
143/// The hash covers the declaration name, its type expression, and its
144/// optional value expression (for definitions/theorems).
145pub fn hash_declaration(decl: &ConstantInfo) -> u64 {
146    let mut h = FNV1A_OFFSET;
147    h = fnv1a_str(h, &format!("{}", decl.name()));
148    h = hash_expr_impl(decl.ty(), h);
149    if let Some(val) = decl.value() {
150        h = hash_expr_impl(val, h);
151    }
152    h
153}
154
155/// Create a new `ProofCertificate` for the given declaration.
156///
157/// The certificate ID is derived from the combined hash of the type and proof
158/// expressions. The `verified_at` timestamp is set to 0 in no-std/deterministic
159/// contexts; callers may override it by replacing the field.
160pub fn create_certificate(
161    decl_name: &str,
162    type_expr: &Expr,
163    proof_expr: &Expr,
164) -> ProofCertificate {
165    let type_hash = hash_expr(type_expr);
166    let proof_hash = hash_expr(proof_expr);
167    // Derive a stable certificate ID from the two hashes.
168    let id_raw = fnv1a_u64(fnv1a_u64(FNV1A_OFFSET, type_hash), proof_hash);
169    ProofCertificate {
170        id: ProofCertId::new(id_raw),
171        decl_name: decl_name.to_string(),
172        type_hash,
173        proof_hash,
174        reduction_steps: Vec::new(),
175        verified_at: 0,
176    }
177}
178
179/// Verify a certificate against the live environment.
180///
181/// Checks that:
182/// 1. The declaration named in the certificate exists in `env`.
183/// 2. The stored type hash matches the recomputed hash of the live type.
184/// 3. The stored proof hash matches the recomputed hash of the live value (if any).
185/// 4. The reduction step sequence is well-formed (no Iota steps with ctor_idx == u32::MAX).
186pub fn verify_certificate(cert: &ProofCertificate, env: &Environment) -> CertCheckResult {
187    use crate::Name;
188
189    let name = Name::from_str(cert.decl_name.as_str());
190    let ci = match env.find(&name) {
191        Some(ci) => ci,
192        None => return CertCheckResult::MissingDecl(cert.decl_name.clone()),
193    };
194
195    // Verify type hash.
196    let live_type_hash = hash_expr(ci.ty());
197    if live_type_hash != cert.type_hash {
198        return CertCheckResult::HashMismatch {
199            expected: cert.type_hash,
200            actual: live_type_hash,
201        };
202    }
203
204    // Verify proof hash (if the declaration carries a value).
205    if let Some(val) = ci.value() {
206        let live_proof_hash = hash_expr(val);
207        if live_proof_hash != cert.proof_hash {
208            return CertCheckResult::HashMismatch {
209                expected: cert.proof_hash,
210                actual: live_proof_hash,
211            };
212        }
213    }
214
215    // Validate reduction step well-formedness.
216    if !validate_steps(&cert.reduction_steps) {
217        return CertCheckResult::InvalidSteps;
218    }
219
220    CertCheckResult::Valid
221}
222
223/// Validate that a sequence of reduction steps is internally well-formed.
224///
225/// Currently checks:
226/// - `Iota` steps must not use `u32::MAX` as a constructor index (sentinel).
227/// - `Delta` and `Iota` step names must be non-empty.
228fn validate_steps(steps: &[ProofStep]) -> bool {
229    for step in steps {
230        match step {
231            ProofStep::Iota { recursor, ctor_idx }
232                if recursor.is_empty() || *ctor_idx == u32::MAX =>
233            {
234                return false;
235            }
236            ProofStep::Delta { name } if name.is_empty() => {
237                return false;
238            }
239            _ => {}
240        }
241    }
242    true
243}
244
245impl CertificateStore {
246    /// Add a certificate to the store, overwriting any existing entry
247    /// for the same declaration name.
248    pub fn add(&mut self, cert: ProofCertificate) {
249        self.certs.insert(cert.decl_name.clone(), cert);
250    }
251
252    /// Look up a certificate by declaration name.
253    pub fn get(&self, name: &str) -> Option<&ProofCertificate> {
254        self.certs.get(name)
255    }
256
257    /// Verify every certificate in the store against the given environment.
258    ///
259    /// Returns a map from declaration name to verification result.
260    pub fn verify_all(&self, env: &Environment) -> HashMap<String, CertCheckResult> {
261        self.certs
262            .iter()
263            .map(|(name, cert)| (name.clone(), verify_certificate(cert, env)))
264            .collect()
265    }
266}
267
268// ---------------------------------------------------------------------------
269// Serialization / deserialization
270// ---------------------------------------------------------------------------
271
272/// Serialize a `ProofCertificate` to a compact text format.
273///
274/// Format:
275/// ```text
276/// OXICERT v1
277/// id:<id>
278/// decl:<decl_name>
279/// type_hash:<hex>
280/// proof_hash:<hex>
281/// verified_at:<timestamp>
282/// steps:<count>
283/// <step_0>
284/// <step_1>
285/// ...
286/// ```
287pub fn serialize_cert(cert: &ProofCertificate) -> String {
288    let mut out = String::with_capacity(256);
289    out.push_str("OXICERT v1\n");
290    out.push_str(&format!("id:{}\n", cert.id.raw()));
291    out.push_str(&format!("decl:{}\n", cert.decl_name));
292    out.push_str(&format!("type_hash:{:016x}\n", cert.type_hash));
293    out.push_str(&format!("proof_hash:{:016x}\n", cert.proof_hash));
294    out.push_str(&format!("verified_at:{}\n", cert.verified_at));
295    out.push_str(&format!("steps:{}\n", cert.reduction_steps.len()));
296    for step in &cert.reduction_steps {
297        out.push_str(&serialize_step(step));
298        out.push('\n');
299    }
300    out
301}
302
303/// Serialize a single `ProofStep` to its text representation.
304fn serialize_step(step: &ProofStep) -> String {
305    match step {
306        ProofStep::Beta { redex_depth } => format!("Beta {}", redex_depth),
307        ProofStep::Delta { name } => format!("Delta {}", name),
308        ProofStep::Zeta => "Zeta".to_string(),
309        ProofStep::Iota { recursor, ctor_idx } => format!("Iota {} {}", recursor, ctor_idx),
310        ProofStep::Eta => "Eta".to_string(),
311        ProofStep::SubstLevel { params } => format!("SubstLevel {}", params.join(",")),
312        ProofStep::Assumption => "Assumption".to_string(),
313    }
314}
315
316/// Deserialize a `ProofCertificate` from the compact text format produced by
317/// [`serialize_cert`].
318///
319/// Returns `Err(String)` with a human-readable message on any parse failure.
320pub fn deserialize_cert(s: &str) -> Result<ProofCertificate, String> {
321    let mut lines = s.lines();
322
323    // Header
324    let header = lines.next().ok_or("missing header line")?;
325    if header.trim() != "OXICERT v1" {
326        return Err(format!("unrecognised header: {:?}", header));
327    }
328
329    macro_rules! next_field {
330        ($prefix:literal) => {{
331            let line = lines
332                .next()
333                .ok_or_else(|| format!("missing field '{}'", $prefix))?;
334            if !line.starts_with($prefix) {
335                return Err(format!("expected '{}', got {:?}", $prefix, line));
336            }
337            &line[$prefix.len()..]
338        }};
339    }
340
341    let id_raw: u64 = next_field!("id:")
342        .parse()
343        .map_err(|e| format!("bad id: {}", e))?;
344    let decl_name = next_field!("decl:").to_string();
345    let type_hash = u64::from_str_radix(next_field!("type_hash:"), 16)
346        .map_err(|e| format!("bad type_hash: {}", e))?;
347    let proof_hash = u64::from_str_radix(next_field!("proof_hash:"), 16)
348        .map_err(|e| format!("bad proof_hash: {}", e))?;
349    let verified_at: u64 = next_field!("verified_at:")
350        .parse()
351        .map_err(|e| format!("bad verified_at: {}", e))?;
352    let step_count: usize = next_field!("steps:")
353        .parse()
354        .map_err(|e| format!("bad steps count: {}", e))?;
355
356    let mut reduction_steps = Vec::with_capacity(step_count);
357    for i in 0..step_count {
358        let line = lines.next().ok_or_else(|| format!("missing step {}", i))?;
359        let step = deserialize_step(line).map_err(|e| format!("error in step {}: {}", i, e))?;
360        reduction_steps.push(step);
361    }
362
363    Ok(ProofCertificate {
364        id: ProofCertId::new(id_raw),
365        decl_name,
366        type_hash,
367        proof_hash,
368        reduction_steps,
369        verified_at,
370    })
371}
372
373/// Deserialize a single `ProofStep` from a text line.
374fn deserialize_step(line: &str) -> Result<ProofStep, String> {
375    let mut parts = line.splitn(2, ' ');
376    let tag = parts.next().unwrap_or("");
377    let rest = parts.next().unwrap_or("").trim();
378
379    match tag {
380        "Beta" => {
381            let depth: u32 = rest.parse().map_err(|e| format!("bad Beta depth: {}", e))?;
382            Ok(ProofStep::Beta { redex_depth: depth })
383        }
384        "Delta" => {
385            if rest.is_empty() {
386                return Err("Delta requires a name".to_string());
387            }
388            Ok(ProofStep::Delta {
389                name: rest.to_string(),
390            })
391        }
392        "Zeta" => Ok(ProofStep::Zeta),
393        "Iota" => {
394            let mut iota_parts = rest.splitn(2, ' ');
395            let recursor = iota_parts
396                .next()
397                .filter(|s| !s.is_empty())
398                .ok_or("Iota requires recursor name")?
399                .to_string();
400            let ctor_idx: u32 = iota_parts
401                .next()
402                .ok_or("Iota requires ctor_idx")?
403                .parse()
404                .map_err(|e| format!("bad Iota ctor_idx: {}", e))?;
405            Ok(ProofStep::Iota { recursor, ctor_idx })
406        }
407        "Eta" => Ok(ProofStep::Eta),
408        "SubstLevel" => {
409            let params = if rest.is_empty() {
410                Vec::new()
411            } else {
412                rest.split(',').map(|s| s.to_string()).collect()
413            };
414            Ok(ProofStep::SubstLevel { params })
415        }
416        "Assumption" => Ok(ProofStep::Assumption),
417        other => Err(format!("unknown proof step tag: {:?}", other)),
418    }
419}
420
421// ---------------------------------------------------------------------------
422// Tests
423// ---------------------------------------------------------------------------
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use crate::{Expr, Level, Name};
429
430    fn prop() -> Expr {
431        Expr::Sort(Level::Zero)
432    }
433
434    fn type0() -> Expr {
435        Expr::Sort(Level::succ(Level::Zero))
436    }
437
438    fn const_expr(name: &str) -> Expr {
439        Expr::Const(Name::from_str(name), vec![])
440    }
441
442    fn bvar(n: u32) -> Expr {
443        Expr::BVar(n)
444    }
445
446    // --- hash_expr tests ---
447
448    #[test]
449    fn test_hash_sort_prop() {
450        let h = hash_expr(&prop());
451        assert_ne!(h, 0);
452    }
453
454    #[test]
455    fn test_hash_sort_type0() {
456        let h0 = hash_expr(&prop());
457        let h1 = hash_expr(&type0());
458        assert_ne!(h0, h1, "Prop and Type should hash differently");
459    }
460
461    #[test]
462    fn test_hash_bvar_distinct() {
463        let h0 = hash_expr(&bvar(0));
464        let h1 = hash_expr(&bvar(1));
465        assert_ne!(h0, h1);
466    }
467
468    #[test]
469    fn test_hash_const() {
470        let h_nat = hash_expr(&const_expr("Nat"));
471        let h_int = hash_expr(&const_expr("Int"));
472        assert_ne!(h_nat, h_int);
473    }
474
475    #[test]
476    fn test_hash_app() {
477        let f = const_expr("f");
478        let a = const_expr("a");
479        let app = Expr::App(Box::new(f), Box::new(a));
480        let h = hash_expr(&app);
481        assert_ne!(h, 0);
482    }
483
484    #[test]
485    fn test_hash_lam() {
486        let ty = prop();
487        let body = bvar(0);
488        let lam = Expr::Lam(
489            crate::BinderInfo::Default,
490            Name::from_str("x"),
491            Box::new(ty),
492            Box::new(body),
493        );
494        let h = hash_expr(&lam);
495        assert_ne!(h, 0);
496    }
497
498    #[test]
499    fn test_hash_pi() {
500        let ty = prop();
501        let body = bvar(0);
502        let pi = Expr::Pi(
503            crate::BinderInfo::Default,
504            Name::from_str("x"),
505            Box::new(ty),
506            Box::new(body),
507        );
508        let h = hash_expr(&pi);
509        assert_ne!(h, 0);
510    }
511
512    #[test]
513    fn test_hash_deterministic() {
514        let e = Expr::App(Box::new(const_expr("Nat.succ")), Box::new(bvar(0)));
515        assert_eq!(hash_expr(&e), hash_expr(&e));
516    }
517
518    #[test]
519    fn test_hash_let() {
520        let e = Expr::Let(
521            Name::from_str("x"),
522            Box::new(prop()),
523            Box::new(bvar(0)),
524            Box::new(bvar(1)),
525        );
526        let h = hash_expr(&e);
527        assert_ne!(h, 0);
528    }
529
530    #[test]
531    fn test_hash_proj() {
532        let e = Expr::Proj(Name::from_str("Prod.fst"), 0, Box::new(const_expr("p")));
533        let h = hash_expr(&e);
534        assert_ne!(h, 0);
535    }
536
537    // --- create_certificate tests ---
538
539    #[test]
540    fn test_create_certificate_fields() {
541        let ty = prop();
542        let pf = bvar(0);
543        let cert = create_certificate("MyThm", &ty, &pf);
544        assert_eq!(cert.decl_name, "MyThm");
545        assert_eq!(cert.type_hash, hash_expr(&ty));
546        assert_eq!(cert.proof_hash, hash_expr(&pf));
547        assert!(cert.reduction_steps.is_empty());
548    }
549
550    #[test]
551    fn test_create_certificate_id_stable() {
552        let ty = prop();
553        let pf = bvar(0);
554        let c1 = create_certificate("T", &ty, &pf);
555        let c2 = create_certificate("T", &ty, &pf);
556        assert_eq!(c1.id, c2.id);
557    }
558
559    #[test]
560    fn test_create_certificate_distinct_names() {
561        let ty = prop();
562        let pf = bvar(0);
563        let c1 = create_certificate("Thm1", &ty, &pf);
564        let c2 = create_certificate("Thm2", &ty, &pf);
565        // Same type/proof → same type_hash/proof_hash, but decl_name differs.
566        assert_eq!(c1.type_hash, c2.type_hash);
567        assert_ne!(c1.decl_name, c2.decl_name);
568    }
569
570    // --- serialize / deserialize tests ---
571
572    #[test]
573    fn test_roundtrip_empty_steps() {
574        let cert = create_certificate("RoundTripThm", &prop(), &bvar(0));
575        let s = serialize_cert(&cert);
576        let cert2 = deserialize_cert(&s).expect("deserialize should succeed");
577        assert_eq!(cert, cert2);
578    }
579
580    #[test]
581    fn test_roundtrip_with_steps() {
582        let mut cert = create_certificate("StepThm", &type0(), &const_expr("Nat"));
583        cert.reduction_steps = vec![
584            ProofStep::Beta { redex_depth: 2 },
585            ProofStep::Delta {
586                name: "Nat.add".to_string(),
587            },
588            ProofStep::Zeta,
589            ProofStep::Iota {
590                recursor: "Nat.rec".to_string(),
591                ctor_idx: 1,
592            },
593            ProofStep::Eta,
594            ProofStep::SubstLevel {
595                params: vec!["u".to_string(), "v".to_string()],
596            },
597            ProofStep::Assumption,
598        ];
599        let s = serialize_cert(&cert);
600        let cert2 = deserialize_cert(&s).expect("deserialize should succeed");
601        assert_eq!(cert, cert2);
602    }
603
604    #[test]
605    fn test_serialize_header() {
606        let cert = create_certificate("T", &prop(), &bvar(0));
607        let s = serialize_cert(&cert);
608        assert!(s.starts_with("OXICERT v1\n"));
609    }
610
611    #[test]
612    fn test_deserialize_bad_header() {
613        let result = deserialize_cert("BADHEADER\nid:0\ndecl:X\ntype_hash:0000000000000000\nproof_hash:0000000000000000\nverified_at:0\nsteps:0\n");
614        assert!(result.is_err());
615    }
616
617    #[test]
618    fn test_deserialize_missing_field() {
619        // Missing proof_hash and later fields.
620        let result = deserialize_cert("OXICERT v1\nid:1\ndecl:T\ntype_hash:0000000000000001\n");
621        assert!(result.is_err());
622    }
623
624    // --- CertificateStore tests ---
625
626    #[test]
627    fn test_store_add_get() {
628        let mut store = CertificateStore::new();
629        let cert = create_certificate("Thm", &prop(), &bvar(0));
630        store.add(cert.clone());
631        let got = store.get("Thm").expect("should find cert");
632        assert_eq!(got.decl_name, "Thm");
633    }
634
635    #[test]
636    fn test_store_get_missing() {
637        let store = CertificateStore::new();
638        assert!(store.get("Nonexistent").is_none());
639    }
640
641    #[test]
642    fn test_store_overwrite() {
643        let mut store = CertificateStore::new();
644        let c1 = create_certificate("Thm", &prop(), &bvar(0));
645        let c2 = create_certificate("Thm", &type0(), &bvar(1));
646        store.add(c1);
647        store.add(c2.clone());
648        let got = store.get("Thm").expect("should find updated cert");
649        assert_eq!(got.type_hash, c2.type_hash);
650    }
651
652    #[test]
653    fn test_store_len() {
654        let mut store = CertificateStore::new();
655        assert_eq!(store.len(), 0);
656        store.add(create_certificate("A", &prop(), &bvar(0)));
657        store.add(create_certificate("B", &type0(), &bvar(0)));
658        assert_eq!(store.len(), 2);
659    }
660
661    #[test]
662    fn test_store_is_empty() {
663        let store = CertificateStore::new();
664        assert!(store.is_empty());
665    }
666
667    // --- verify_certificate tests ---
668
669    #[test]
670    fn test_verify_missing_decl() {
671        let env = Environment::new();
672        let cert = create_certificate("Missing", &prop(), &bvar(0));
673        let result = verify_certificate(&cert, &env);
674        assert_eq!(result, CertCheckResult::MissingDecl("Missing".to_string()));
675    }
676
677    // --- validate_steps tests ---
678
679    #[test]
680    fn test_validate_steps_empty() {
681        assert!(validate_steps(&[]));
682    }
683
684    #[test]
685    fn test_validate_steps_invalid_iota() {
686        let steps = vec![ProofStep::Iota {
687            recursor: "Nat.rec".to_string(),
688            ctor_idx: u32::MAX,
689        }];
690        assert!(!validate_steps(&steps));
691    }
692
693    #[test]
694    fn test_validate_steps_empty_delta_name() {
695        let steps = vec![ProofStep::Delta {
696            name: String::new(),
697        }];
698        assert!(!validate_steps(&steps));
699    }
700
701    #[test]
702    fn test_validate_steps_valid_mixed() {
703        let steps = vec![
704            ProofStep::Beta { redex_depth: 0 },
705            ProofStep::Delta {
706                name: "foo".to_string(),
707            },
708            ProofStep::Zeta,
709            ProofStep::Eta,
710            ProofStep::Assumption,
711        ];
712        assert!(validate_steps(&steps));
713    }
714
715    // --- CertCheckResult display ---
716
717    #[test]
718    fn test_cert_check_result_display_valid() {
719        let r = CertCheckResult::Valid;
720        assert_eq!(format!("{}", r), "Valid");
721    }
722
723    #[test]
724    fn test_cert_check_result_display_mismatch() {
725        let r = CertCheckResult::HashMismatch {
726            expected: 0xdeadbeef,
727            actual: 0xcafe,
728        };
729        let s = format!("{}", r);
730        assert!(s.contains("HashMismatch"));
731    }
732}