ggen_core/codegen/
proof_archive.rs1use crate::codegen::ExecutionProof;
2use ggen_utils::error::Result;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Serialize, Deserialize, Clone)]
8pub struct ProofEntry {
9 pub execution_id: String,
10 pub timestamp_ms: u64,
11 pub proof: ExecutionProof,
12}
13
14pub struct ProofArchive {
15 archive_dir: PathBuf,
16}
17
18impl ProofArchive {
19 pub fn new(archive_dir: PathBuf) -> Result<Self> {
20 fs::create_dir_all(&archive_dir)?;
21 Ok(Self { archive_dir })
22 }
23
24 pub fn store_proof(&self, proof: &ExecutionProof) -> Result<()> {
25 let proof_file = self
26 .archive_dir
27 .join(format!("{}.json", proof.execution_id));
28 let json = serde_json::to_string_pretty(proof)?;
29 fs::write(proof_file, json)?;
30 Ok(())
31 }
32
33 pub fn load_proof(&self, execution_id: &str) -> Result<Option<ExecutionProof>> {
34 let proof_file = self.archive_dir.join(format!("{}.json", execution_id));
35 if !proof_file.exists() {
36 return Ok(None);
37 }
38
39 let json = fs::read_to_string(proof_file)?;
40 let proof = serde_json::from_str(&json)?;
41 Ok(Some(proof))
42 }
43
44 pub fn list_proofs(&self) -> Result<Vec<ProofEntry>> {
45 let mut entries = vec![];
46
47 for entry in fs::read_dir(&self.archive_dir)? {
48 let entry = entry?;
49 let path = entry.path();
50
51 if path.extension().is_some_and(|ext| ext == "json") {
52 if let Ok(json) = fs::read_to_string(&path) {
53 if let Ok(proof) = serde_json::from_str::<ExecutionProof>(&json) {
54 entries.push(ProofEntry {
55 execution_id: proof.execution_id.clone(),
56 timestamp_ms: proof.timestamp_ms,
57 proof,
58 });
59 }
60 }
61 }
62 }
63
64 entries.sort_by_key(|e| e.timestamp_ms);
65 Ok(entries)
66 }
67
68 pub fn verify_chain(&self) -> Result<ChainVerification> {
69 let entries = self.list_proofs()?;
70
71 if entries.is_empty() {
72 return Ok(ChainVerification {
73 is_valid: true,
74 proof_count: 0,
75 determinism_violations: vec![],
76 last_manifest_hash: None,
77 last_output_hash: None,
78 });
79 }
80
81 let mut determinism_violations = vec![];
82 let mut last_manifest = entries[0].proof.manifest_hash.clone();
83 let mut last_output = entries[0].proof.output_hash.clone();
84
85 for i in 1..entries.len() {
86 let current = &entries[i];
87 let previous = &entries[i - 1];
88
89 if previous.proof.manifest_hash == current.proof.manifest_hash
90 && previous.proof.ontology_hash == current.proof.ontology_hash
91 && previous.proof.output_hash != current.proof.output_hash
92 {
93 determinism_violations.push(DeterminismViolation {
94 execution_id_previous: previous.execution_id.clone(),
95 execution_id_current: current.execution_id.clone(),
96 reason: "Same manifest/ontology produced different output".to_string(),
97 });
98 }
99
100 last_manifest = current.proof.manifest_hash.clone();
101 last_output = current.proof.output_hash.clone();
102 }
103
104 let is_valid = determinism_violations.is_empty();
105
106 Ok(ChainVerification {
107 is_valid,
108 proof_count: entries.len(),
109 determinism_violations,
110 last_manifest_hash: Some(last_manifest),
111 last_output_hash: Some(last_output),
112 })
113 }
114
115 pub fn get_latest_proof(&self) -> Result<Option<ExecutionProof>> {
116 let entries = self.list_proofs()?;
117 Ok(entries.last().map(|e| e.proof.clone()))
118 }
119}
120
121pub struct ChainVerification {
122 pub is_valid: bool,
123 pub proof_count: usize,
124 pub determinism_violations: Vec<DeterminismViolation>,
125 pub last_manifest_hash: Option<String>,
126 pub last_output_hash: Option<String>,
127}
128
129pub struct DeterminismViolation {
130 pub execution_id_previous: String,
131 pub execution_id_current: String,
132 pub reason: String,
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use tempfile::TempDir;
139
140 #[test]
141 fn test_proof_archive_creation() {
142 let temp = TempDir::new().unwrap();
143 let _archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
144 assert!(temp.path().exists());
145 }
146
147 #[test]
148 fn test_proof_persistence() {
149 let temp = TempDir::new().unwrap();
150 let archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
151
152 let proof = ExecutionProof {
153 execution_id: "test-1".to_string(),
154 timestamp_ms: 1000,
155 manifest_hash: "hash1".to_string(),
156 ontology_hash: "ohash1".to_string(),
157 rules_executed: vec![],
158 output_hash: "out1".to_string(),
159 execution_duration_ms: 100,
160 determinism_signature: "sig1".to_string(),
161 };
162
163 archive.store_proof(&proof).unwrap();
164 let loaded = archive.load_proof("test-1").unwrap().unwrap();
165
166 assert_eq!(loaded.execution_id, proof.execution_id);
167 assert_eq!(loaded.manifest_hash, proof.manifest_hash);
168 }
169
170 #[test]
171 fn test_chain_verification_empty() {
172 let temp = TempDir::new().unwrap();
173 let archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
174 let verification = archive.verify_chain().unwrap();
175
176 assert!(verification.is_valid);
177 assert_eq!(verification.proof_count, 0);
178 }
179
180 #[test]
181 fn test_determinism_violation_detection() {
182 let temp = TempDir::new().unwrap();
183 let archive = ProofArchive::new(temp.path().to_path_buf()).unwrap();
184
185 let proof1 = ExecutionProof {
186 execution_id: "exec-1".to_string(),
187 timestamp_ms: 1000,
188 manifest_hash: "same-manifest".to_string(),
189 ontology_hash: "same-ontology".to_string(),
190 rules_executed: vec![],
191 output_hash: "output-1".to_string(),
192 execution_duration_ms: 100,
193 determinism_signature: "sig1".to_string(),
194 };
195
196 let proof2 = ExecutionProof {
197 execution_id: "exec-2".to_string(),
198 timestamp_ms: 2000,
199 manifest_hash: "same-manifest".to_string(),
200 ontology_hash: "same-ontology".to_string(),
201 rules_executed: vec![],
202 output_hash: "output-2".to_string(),
203 execution_duration_ms: 100,
204 determinism_signature: "sig2".to_string(),
205 };
206
207 archive.store_proof(&proof1).unwrap();
208 archive.store_proof(&proof2).unwrap();
209
210 let verification = archive.verify_chain().unwrap();
211
212 assert!(!verification.is_valid);
213 assert_eq!(verification.determinism_violations.len(), 1);
214 }
215}