1use 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
14const FNV1A_OFFSET: u64 = 14695981039346656037;
19const FNV1A_PRIME: u64 = 1099511628211;
20
21#[inline]
23fn fnv1a_byte(hash: u64, byte: u8) -> u64 {
24 (hash ^ (byte as u64)).wrapping_mul(FNV1A_PRIME)
25}
26
27#[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#[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#[inline]
46fn fnv1a_str(hash: u64, s: &str) -> u64 {
47 s.as_bytes().iter().fold(hash, |h, &b| fnv1a_byte(h, b))
48}
49
50const 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
65pub 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 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
141pub 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
155pub 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 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
179pub 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 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 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 if !validate_steps(&cert.reduction_steps) {
217 return CertCheckResult::InvalidSteps;
218 }
219
220 CertCheckResult::Valid
221}
222
223fn 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 pub fn add(&mut self, cert: ProofCertificate) {
249 self.certs.insert(cert.decl_name.clone(), cert);
250 }
251
252 pub fn get(&self, name: &str) -> Option<&ProofCertificate> {
254 self.certs.get(name)
255 }
256
257 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
268pub 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
303fn 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
316pub fn deserialize_cert(s: &str) -> Result<ProofCertificate, String> {
321 let mut lines = s.lines();
322
323 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
373fn 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#[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 #[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 #[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 assert_eq!(c1.type_hash, c2.type_hash);
567 assert_ne!(c1.decl_name, c2.decl_name);
568 }
569
570 #[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 let result = deserialize_cert("OXICERT v1\nid:1\ndecl:T\ntype_hash:0000000000000001\n");
621 assert!(result.is_err());
622 }
623
624 #[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 #[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 #[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 #[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}