1use std::path::PathBuf;
2
3use substrate::CmnEntry;
4
5use crate::sink::HyphaError;
6
7use super::{
8 locked_update_file, locked_write_file, CacheStatus, DomainStatePin, FetchStatus, KeyTrustEntry,
9 TasteVerdictCache,
10};
11
12pub const DOMAIN_STATE_JUMP_THRESHOLD: u64 = 1000;
13
14pub struct DomainCache {
16 pub root: PathBuf,
17 pub domain: String,
18}
19
20impl DomainCache {
21 pub fn mycelium_dir(&self) -> PathBuf {
23 self.root.join("mycelium")
24 }
25
26 pub fn spore_dir(&self) -> PathBuf {
28 self.root.join("spore")
29 }
30
31 pub fn spore_path(&self, hash: &str) -> PathBuf {
33 self.spore_dir().join(hash)
34 }
35
36 pub fn repos_dir(&self) -> PathBuf {
38 self.root.join("repos")
39 }
40
41 pub fn repo_path(&self, root_commit: &str) -> PathBuf {
43 self.repos_dir().join(root_commit)
44 }
45
46 pub fn cmn_path(&self) -> PathBuf {
48 self.mycelium_dir().join("cmn.json")
49 }
50
51 pub fn domain_state_path(&self) -> PathBuf {
53 self.mycelium_dir().join("domain_state.json")
54 }
55
56 pub fn load_cmn(&self) -> Option<CmnEntry> {
58 let path = self.cmn_path();
59 if path.exists() {
60 std::fs::read_to_string(&path)
61 .ok()
62 .and_then(|s| serde_json::from_str(&s).ok())
63 } else {
64 None
65 }
66 }
67
68 pub fn save_cmn(&self, entry: &CmnEntry) -> Result<(), crate::sink::HyphaError> {
70 let dir = self.mycelium_dir();
71 std::fs::create_dir_all(&dir).map_err(|e| {
72 HyphaError::new(
73 "cache_write_failed",
74 format!("Failed to create mycelium dir: {}", e),
75 )
76 })?;
77
78 let content = serde_json::to_string_pretty(entry).map_err(|e| {
79 HyphaError::new(
80 "cache_write_failed",
81 format!("Failed to serialize cmn entry: {}", e),
82 )
83 })?;
84
85 locked_write_file(&self.cmn_path(), &content)
86 }
87
88 pub fn load_domain_state(&self) -> Option<DomainStatePin> {
90 let path = self.domain_state_path();
91 if path.exists() {
92 std::fs::read_to_string(&path)
93 .ok()
94 .and_then(|s| serde_json::from_str(&s).ok())
95 } else {
96 None
97 }
98 }
99
100 pub fn validate_and_pin_cmn_state(
102 &self,
103 entry: &CmnEntry,
104 ) -> Result<(), crate::sink::HyphaError> {
105 let capsule = entry.primary_capsule().map_err(|e| {
106 HyphaError::new("domain_state_invalid", format!("Invalid cmn.json: {e}"))
107 })?;
108 let expected_uri = substrate::build_domain_uri(&self.domain);
109 if capsule.uri != expected_uri {
110 return Err(HyphaError::new(
111 "domain_state_invalid",
112 format!(
113 "cmn.json primary capsule uri {} does not match domain {}",
114 capsule.uri, self.domain
115 ),
116 ));
117 }
118 let digest = entry.capsules_digest().map_err(|e| {
119 HyphaError::new(
120 "domain_state_invalid",
121 format!("Failed to digest cmn.json capsules: {e}"),
122 )
123 })?;
124
125 if let Some(pin) = self.load_domain_state() {
126 if capsule.serial < pin.serial {
127 return Err(HyphaError::new(
128 "domain_state_rollback",
129 format!(
130 "cmn.json serial {} is lower than pinned serial {} for {}",
131 capsule.serial, pin.serial, self.domain
132 ),
133 ));
134 }
135 if capsule.serial == pin.serial && digest != pin.capsules_digest {
136 return Err(HyphaError::new(
137 "domain_state_equivocation",
138 format!(
139 "cmn.json serial {} for {} has a different capsules digest than the local pin",
140 capsule.serial, self.domain
141 ),
142 ));
143 }
144 if capsule.serial.saturating_sub(pin.serial) > DOMAIN_STATE_JUMP_THRESHOLD {
145 return Err(HyphaError::new(
146 "domain_state_jump",
147 format!(
148 "cmn.json serial jumped from {} to {} for {}",
149 pin.serial, capsule.serial, self.domain
150 ),
151 ));
152 }
153 if capsule.key != pin.current_key {
154 capsule
155 .verify_rotation_chain_from(&pin.current_key)
156 .map_err(|e| {
157 HyphaError::new(
158 "domain_key_rotation_unproven",
159 format!(
160 "cmn.json key changed for {} without a valid rotation chain: {e}",
161 self.domain
162 ),
163 )
164 })?;
165 }
166 }
167
168 self.save_domain_state(&DomainStatePin {
169 serial: capsule.serial,
170 capsules_digest: digest,
171 current_key: capsule.key.clone(),
172 pinned_at_epoch_ms: crate::time::now_epoch_ms(),
173 })
174 }
175
176 fn save_domain_state(&self, pin: &DomainStatePin) -> Result<(), crate::sink::HyphaError> {
177 let dir = self.mycelium_dir();
178 std::fs::create_dir_all(&dir).map_err(|e| {
179 HyphaError::new(
180 "cache_write_failed",
181 format!("Failed to create mycelium dir: {}", e),
182 )
183 })?;
184
185 let content = serde_json::to_string_pretty(pin).map_err(|e| {
186 HyphaError::new(
187 "cache_write_failed",
188 format!("Failed to serialize domain state: {}", e),
189 )
190 })?;
191
192 locked_write_file(&self.domain_state_path(), &content)
193 }
194
195 pub fn mycelium_path(&self) -> PathBuf {
197 self.mycelium_dir().join("mycelium.json")
198 }
199
200 pub fn load_mycelium(&self) -> Option<serde_json::Value> {
202 let path = self.mycelium_path();
203 if path.exists() {
204 std::fs::read_to_string(&path)
205 .ok()
206 .and_then(|s| serde_json::from_str(&s).ok())
207 } else {
208 None
209 }
210 }
211
212 pub fn save_mycelium(
214 &self,
215 mycelium: &serde_json::Value,
216 ) -> Result<(), crate::sink::HyphaError> {
217 let dir = self.mycelium_dir();
218 std::fs::create_dir_all(&dir).map_err(|e| {
219 HyphaError::new(
220 "cache_write_failed",
221 format!("Failed to create mycelium dir: {}", e),
222 )
223 })?;
224
225 let content = crate::mycelium::format_mycelium(mycelium).map_err(|e| {
226 HyphaError::new(
227 "cache_write_failed",
228 format!("Failed to serialize mycelium: {}", e),
229 )
230 })?;
231
232 locked_write_file(&self.mycelium_path(), &content)
233 }
234
235 pub fn status_path(&self) -> PathBuf {
237 self.mycelium_dir().join("status.json")
238 }
239
240 pub fn load_status(&self) -> CacheStatus {
242 let path = self.status_path();
243 if path.exists() {
244 std::fs::read_to_string(&path)
245 .ok()
246 .and_then(|s| serde_json::from_str(&s).ok())
247 .unwrap_or_default()
248 } else {
249 CacheStatus::default()
250 }
251 }
252
253 pub fn save_status(&self, status: &CacheStatus) -> Result<(), crate::sink::HyphaError> {
255 let dir = self.mycelium_dir();
256 std::fs::create_dir_all(&dir).map_err(|e| {
257 HyphaError::new(
258 "cache_write_failed",
259 format!("Failed to create mycelium dir: {}", e),
260 )
261 })?;
262
263 let content = serde_json::to_string_pretty(status).map_err(|e| {
264 HyphaError::new(
265 "cache_write_failed",
266 format!("Failed to serialize status: {}", e),
267 )
268 })?;
269
270 locked_write_file(&self.status_path(), &content)
271 }
272
273 pub fn domain_taste_path(&self) -> PathBuf {
275 self.mycelium_dir().join("taste.json")
276 }
277
278 pub fn load_domain_taste(&self) -> Option<TasteVerdictCache> {
280 let path = self.domain_taste_path();
281 if path.exists() {
282 std::fs::read_to_string(&path)
283 .ok()
284 .and_then(|s| serde_json::from_str(&s).ok())
285 } else {
286 None
287 }
288 }
289
290 pub fn save_domain_taste(
292 &self,
293 verdict: &TasteVerdictCache,
294 ) -> Result<(), crate::sink::HyphaError> {
295 let dir = self.mycelium_dir();
296 std::fs::create_dir_all(&dir).map_err(|e| {
297 HyphaError::new(
298 "cache_write_failed",
299 format!("Failed to create mycelium dir: {}", e),
300 )
301 })?;
302
303 let content = serde_json::to_string_pretty(verdict).map_err(|e| {
304 HyphaError::new(
305 "cache_write_failed",
306 format!("Failed to serialize domain taste verdict: {}", e),
307 )
308 })?;
309
310 locked_write_file(&self.domain_taste_path(), &content)
311 }
312
313 pub fn taste_path(&self, hash: &str) -> PathBuf {
315 self.spore_path(hash).join("taste.json")
316 }
317
318 pub fn load_taste(&self, hash: &str) -> Option<TasteVerdictCache> {
320 let path = self.taste_path(hash);
321 if path.exists() {
322 std::fs::read_to_string(&path)
323 .ok()
324 .and_then(|s| serde_json::from_str(&s).ok())
325 } else {
326 None
327 }
328 }
329
330 pub fn save_taste(
332 &self,
333 hash: &str,
334 verdict: &TasteVerdictCache,
335 ) -> Result<(), crate::sink::HyphaError> {
336 let dir = self.spore_path(hash);
337 std::fs::create_dir_all(&dir).map_err(|e| {
338 HyphaError::new(
339 "cache_write_failed",
340 format!("Failed to create spore dir: {}", e),
341 )
342 })?;
343
344 let content = serde_json::to_string_pretty(verdict).map_err(|e| {
345 HyphaError::new(
346 "cache_write_failed",
347 format!("Failed to serialize taste verdict: {}", e),
348 )
349 })?;
350
351 locked_write_file(&self.taste_path(hash), &content)
352 }
353
354 pub fn key_trust_path(&self) -> PathBuf {
356 self.mycelium_dir().join("key_trust.json")
357 }
358
359 pub fn load_key_trust(&self) -> Vec<KeyTrustEntry> {
361 let path = self.key_trust_path();
362 if path.exists() {
363 std::fs::read_to_string(&path)
364 .ok()
365 .and_then(|s| serde_json::from_str(&s).ok())
366 .unwrap_or_default()
367 } else {
368 Vec::new()
369 }
370 }
371
372 pub fn save_key_trust(&self, key: &str) -> Result<(), crate::sink::HyphaError> {
377 self.save_key_trust_with_retirement(key, None)
378 }
379
380 pub fn save_key_trust_with_retirement(
382 &self,
383 key: &str,
384 retired_at_epoch_ms: Option<u64>,
385 ) -> Result<(), crate::sink::HyphaError> {
386 locked_update_file(&self.key_trust_path(), |existing| {
387 let mut entries: Vec<KeyTrustEntry> = existing
388 .and_then(|s| serde_json::from_str(&s).ok())
389 .unwrap_or_default();
390 if let Some(entry) = entries.iter_mut().find(|e| e.key == key) {
391 entry.confirmed_at_epoch_ms = crate::time::now_epoch_ms();
392 entry.retired_at_epoch_ms = retired_at_epoch_ms;
393 } else {
394 entries.push(KeyTrustEntry {
395 key: key.to_string(),
396 confirmed_at_epoch_ms: crate::time::now_epoch_ms(),
397 retired_at_epoch_ms,
398 });
399 }
400 serde_json::to_string_pretty(&entries).map_err(|e| {
401 HyphaError::new(
402 "cache_write_failed",
403 format!("Failed to serialize key trust: {}", e),
404 )
405 })
406 })
407 }
408
409 pub fn is_key_trusted(&self, key: &str, ttl_ms: u64, clock_skew_tolerance_ms: u64) -> bool {
412 self.is_key_trusted_entry(key, ttl_ms, clock_skew_tolerance_ms)
413 .is_some()
414 }
415
416 pub fn is_key_trusted_for_time(
418 &self,
419 key: &str,
420 signed_at_epoch_ms: u64,
421 ttl_ms: u64,
422 clock_skew_tolerance_ms: u64,
423 ) -> bool {
424 self.is_key_trusted_entry(key, ttl_ms, clock_skew_tolerance_ms)
425 .map(|entry| {
426 entry
427 .retired_at_epoch_ms
428 .map(|retired_at| signed_at_epoch_ms <= retired_at)
429 .unwrap_or(true)
430 })
431 .unwrap_or(false)
432 }
433
434 fn is_key_trusted_entry(
435 &self,
436 key: &str,
437 ttl_ms: u64,
438 clock_skew_tolerance_ms: u64,
439 ) -> Option<KeyTrustEntry> {
440 let entries = self.load_key_trust();
441 let now = crate::time::now_epoch_ms();
442 let effective_ttl = ttl_ms.saturating_add(clock_skew_tolerance_ms);
443 entries
444 .into_iter()
445 .find(|e| e.key == key && now.saturating_sub(e.confirmed_at_epoch_ms) < effective_ttl)
446 }
447
448 pub fn update_cmn_status(&self, success: bool, error: Option<&str>) {
450 let _ = locked_update_file(&self.status_path(), |existing| {
451 let mut status: CacheStatus = existing
452 .and_then(|s| serde_json::from_str(&s).ok())
453 .unwrap_or_default();
454 if success {
455 status.cmn = FetchStatus::success();
456 } else {
457 status.cmn =
458 FetchStatus::failure(error.unwrap_or("Unknown error"), Some(&status.cmn));
459 }
460 serde_json::to_string_pretty(&status).map_err(|e| {
461 HyphaError::new(
462 "cache_write_failed",
463 format!("Failed to serialize status: {}", e),
464 )
465 })
466 });
467 }
468}