1pub const PROOF_CACHE_MAX_ENTRIES: usize = 64;
43
44pub const PROOF_CACHE_TTL_NS: u64 = 100_000_000;
46
47pub const PROOF_CACHE_TTL_MS: u32 = 100;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum CacheError {
53 CacheFull,
55 NotFound,
57 Expired,
59 NonceConsumed,
61 DuplicateEntry,
63}
64
65#[derive(Debug, Clone, Copy)]
70#[repr(C)]
71pub struct ProofCacheEntry {
72 pub proof_id: u32,
74 pub inserted_at: u64,
76 pub nonce: u64,
78 pub mutation_hash: [u8; 32],
80 pub consumed: bool,
82}
83
84impl ProofCacheEntry {
85 #[must_use]
87 pub const fn new(
88 proof_id: u32,
89 inserted_at: u64,
90 nonce: u64,
91 mutation_hash: [u8; 32],
92 ) -> Self {
93 Self {
94 proof_id,
95 inserted_at,
96 nonce,
97 mutation_hash,
98 consumed: false,
99 }
100 }
101
102 #[must_use]
104 #[inline]
105 pub const fn is_expired(&self, current_time_ns: u64) -> bool {
106 if current_time_ns < self.inserted_at {
109 return false;
110 }
111 current_time_ns - self.inserted_at > PROOF_CACHE_TTL_NS
112 }
113
114 #[must_use]
116 #[inline]
117 pub fn matches(&self, mutation_hash: &[u8; 32], nonce: u64) -> bool {
118 self.nonce == nonce && self.mutation_hash == *mutation_hash
119 }
120}
121
122#[derive(Debug)]
130pub struct ProofCache {
131 entries: [Option<ProofCacheEntry>; PROOF_CACHE_MAX_ENTRIES],
133 count: usize,
135}
136
137impl ProofCache {
138 #[must_use]
140 pub const fn new() -> Self {
141 const NONE: Option<ProofCacheEntry> = None;
142 Self {
143 entries: [NONE; PROOF_CACHE_MAX_ENTRIES],
144 count: 0,
145 }
146 }
147
148 #[must_use]
150 #[inline]
151 pub const fn len(&self) -> usize {
152 self.count
153 }
154
155 #[must_use]
157 #[inline]
158 pub const fn is_empty(&self) -> bool {
159 self.count == 0
160 }
161
162 #[must_use]
164 #[inline]
165 pub const fn is_full(&self) -> bool {
166 self.count >= PROOF_CACHE_MAX_ENTRIES
167 }
168
169 #[must_use]
183 pub fn insert(
184 &mut self,
185 mutation_hash: [u8; 32],
186 nonce: u64,
187 proof_id: u32,
188 current_time_ns: u64,
189 ) -> Result<(), CacheError> {
190 for i in 0..PROOF_CACHE_MAX_ENTRIES {
192 if let Some(ref entry) = self.entries[i] {
193 if entry.matches(&mutation_hash, nonce) && !entry.consumed {
195 return Err(CacheError::DuplicateEntry);
196 }
197
198 if entry.is_expired(current_time_ns) || entry.consumed {
200 self.entries[i] = None;
201 self.count = self.count.saturating_sub(1);
202 }
203 }
204 }
205
206 let mut slot = None;
208 for i in 0..PROOF_CACHE_MAX_ENTRIES {
209 if self.entries[i].is_none() {
210 slot = Some(i);
211 break;
212 }
213 }
214
215 match slot {
216 Some(i) => {
217 self.entries[i] = Some(ProofCacheEntry::new(
218 proof_id,
219 current_time_ns,
220 nonce,
221 mutation_hash,
222 ));
223 self.count += 1;
224 Ok(())
225 }
226 None => Err(CacheError::CacheFull),
227 }
228 }
229
230 #[must_use]
255 pub fn verify_and_consume(
256 &mut self,
257 mutation_hash: &[u8; 32],
258 nonce: u64,
259 current_time_ns: u64,
260 ) -> Result<u32, CacheError> {
261 for i in 0..PROOF_CACHE_MAX_ENTRIES {
263 if let Some(ref mut entry) = self.entries[i] {
264 if entry.matches(mutation_hash, nonce) {
265 if entry.consumed {
267 self.entries[i] = None;
269 self.count = self.count.saturating_sub(1);
270 return Err(CacheError::NonceConsumed);
271 }
272
273 if entry.is_expired(current_time_ns) {
275 self.entries[i] = None;
277 self.count = self.count.saturating_sub(1);
278 return Err(CacheError::Expired);
279 }
280
281 let proof_id = entry.proof_id;
283 self.entries[i] = None;
284 self.count = self.count.saturating_sub(1);
285
286 return Ok(proof_id);
287 }
288 }
289 }
290
291 Err(CacheError::NotFound)
292 }
293
294 #[must_use]
299 pub fn exists(&self, mutation_hash: &[u8; 32], nonce: u64, current_time_ns: u64) -> bool {
300 for entry in &self.entries {
301 if let Some(ref e) = entry {
302 if e.matches(mutation_hash, nonce) && !e.consumed && !e.is_expired(current_time_ns)
303 {
304 return true;
305 }
306 }
307 }
308 false
309 }
310
311 pub fn evict_expired(&mut self, current_time_ns: u64) {
315 for i in 0..PROOF_CACHE_MAX_ENTRIES {
316 if let Some(ref entry) = self.entries[i] {
317 if entry.is_expired(current_time_ns) || entry.consumed {
318 self.entries[i] = None;
319 self.count = self.count.saturating_sub(1);
320 }
321 }
322 }
323 }
324
325 pub fn clear(&mut self) {
327 for i in 0..PROOF_CACHE_MAX_ENTRIES {
328 self.entries[i] = None;
329 }
330 self.count = 0;
331 }
332
333 #[must_use]
335 pub fn stats(&self, current_time_ns: u64) -> ProofCacheStats {
336 let mut active = 0;
337 let mut expired = 0;
338 let mut consumed = 0;
339
340 for entry in &self.entries {
341 if let Some(ref e) = entry {
342 if e.consumed {
343 consumed += 1;
344 } else if e.is_expired(current_time_ns) {
345 expired += 1;
346 } else {
347 active += 1;
348 }
349 }
350 }
351
352 ProofCacheStats {
353 total_slots: PROOF_CACHE_MAX_ENTRIES,
354 active_entries: active,
355 expired_entries: expired,
356 consumed_entries: consumed,
357 free_slots: PROOF_CACHE_MAX_ENTRIES - (active + expired + consumed),
358 }
359 }
360}
361
362impl Default for ProofCache {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub struct ProofCacheStats {
371 pub total_slots: usize,
373 pub active_entries: usize,
375 pub expired_entries: usize,
377 pub consumed_entries: usize,
379 pub free_slots: usize,
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_cache_insert_and_verify() {
389 let mut cache = ProofCache::new();
390
391 let mutation_hash = [42u8; 32];
392 let nonce = 12345u64;
393 let proof_id = 1u32;
394 let time = 1_000_000u64;
395
396 assert!(cache.insert(mutation_hash, nonce, proof_id, time).is_ok());
398 assert_eq!(cache.len(), 1);
399
400 assert!(cache.exists(&mutation_hash, nonce, time));
402
403 let result = cache.verify_and_consume(&mutation_hash, nonce, time);
405 assert_eq!(result, Ok(proof_id));
406 assert_eq!(cache.len(), 0);
407
408 let result = cache.verify_and_consume(&mutation_hash, nonce, time);
410 assert_eq!(result, Err(CacheError::NotFound));
411 }
412
413 #[test]
414 fn test_nonce_single_use() {
415 let mut cache = ProofCache::new();
416
417 let mutation_hash = [1u8; 32];
418 let nonce = 99999u64;
419 let time = 0u64;
420
421 cache.insert(mutation_hash, nonce, 1, time).unwrap();
422
423 assert!(cache.verify_and_consume(&mutation_hash, nonce, time).is_ok());
425
426 cache.insert(mutation_hash, nonce, 2, time).unwrap();
428
429 assert!(cache.verify_and_consume(&mutation_hash, nonce, time).is_ok());
431 }
432
433 #[test]
434 fn test_ttl_expiry() {
435 let mut cache = ProofCache::new();
436
437 let mutation_hash = [2u8; 32];
438 let nonce = 1u64;
439 let proof_id = 10u32;
440 let insert_time = 1_000_000u64;
441
442 cache.insert(mutation_hash, nonce, proof_id, insert_time).unwrap();
443
444 let time_within_ttl = insert_time + 50_000_000;
446 assert!(cache.exists(&mutation_hash, nonce, time_within_ttl));
447
448 let time_after_ttl = insert_time + 150_000_000;
450 assert!(!cache.exists(&mutation_hash, nonce, time_after_ttl));
451
452 let result = cache.verify_and_consume(&mutation_hash, nonce, time_after_ttl);
454 assert_eq!(result, Err(CacheError::Expired));
455 }
456
457 #[test]
458 fn test_max_entries() {
459 let mut cache = ProofCache::new();
460
461 for i in 0..PROOF_CACHE_MAX_ENTRIES {
463 let mut hash = [0u8; 32];
464 hash[0] = i as u8;
465 cache.insert(hash, i as u64, i as u32, 0).unwrap();
466 }
467
468 assert!(cache.is_full());
469 assert_eq!(cache.len(), PROOF_CACHE_MAX_ENTRIES);
470
471 let result = cache.insert([255u8; 32], 999, 999, 0);
473 assert_eq!(result, Err(CacheError::CacheFull));
474 }
475
476 #[test]
477 fn test_eviction_of_expired() {
478 let mut cache = ProofCache::new();
479
480 for i in 0..PROOF_CACHE_MAX_ENTRIES {
482 let mut hash = [0u8; 32];
483 hash[0] = i as u8;
484 cache.insert(hash, i as u64, i as u32, 0).unwrap();
485 }
486
487 assert!(cache.is_full());
488
489 let later = PROOF_CACHE_TTL_NS + 1;
491 let result = cache.insert([254u8; 32], 9999, 9999, later);
492 assert!(result.is_ok());
493 }
494
495 #[test]
496 fn test_duplicate_entry() {
497 let mut cache = ProofCache::new();
498
499 let hash = [5u8; 32];
500 let nonce = 100u64;
501
502 cache.insert(hash, nonce, 1, 0).unwrap();
503
504 let result = cache.insert(hash, nonce, 2, 0);
506 assert_eq!(result, Err(CacheError::DuplicateEntry));
507 }
508
509 #[test]
510 fn test_stats() {
511 let mut cache = ProofCache::new();
512
513 for i in 0..10 {
515 let mut hash = [0u8; 32];
516 hash[0] = i as u8;
517 cache.insert(hash, i as u64, i as u32, 0).unwrap();
518 }
519
520 let stats = cache.stats(0);
521 assert_eq!(stats.active_entries, 10);
522 assert_eq!(stats.free_slots, PROOF_CACHE_MAX_ENTRIES - 10);
523
524 let mut hash = [0u8; 32];
526 cache.verify_and_consume(&hash, 0, 0).unwrap();
527 hash[0] = 1;
528 cache.verify_and_consume(&hash, 1, 0).unwrap();
529
530 let stats = cache.stats(0);
531 assert_eq!(stats.active_entries, 8);
532
533 let later = PROOF_CACHE_TTL_NS + 1;
535 let stats = cache.stats(later);
536 assert_eq!(stats.expired_entries, 8);
537 assert_eq!(stats.active_entries, 0);
538 }
539
540 #[test]
541 fn test_evict_expired() {
542 let mut cache = ProofCache::new();
543
544 for i in 0..5 {
545 let mut hash = [0u8; 32];
546 hash[0] = i as u8;
547 cache.insert(hash, i as u64, i as u32, 0).unwrap();
548 }
549
550 assert_eq!(cache.len(), 5);
551
552 cache.evict_expired(PROOF_CACHE_TTL_NS + 1);
554 assert_eq!(cache.len(), 0);
555 }
556
557 #[test]
558 fn test_clear() {
559 let mut cache = ProofCache::new();
560
561 for i in 0..10 {
562 let mut hash = [0u8; 32];
563 hash[0] = i as u8;
564 cache.insert(hash, i as u64, i as u32, 0).unwrap();
565 }
566
567 assert_eq!(cache.len(), 10);
568 cache.clear();
569 assert_eq!(cache.len(), 0);
570 assert!(cache.is_empty());
571 }
572
573 #[test]
574 fn test_entry_expired() {
575 let entry = ProofCacheEntry::new(1, 0, 0, [0u8; 32]);
576
577 assert!(!entry.is_expired(0));
579
580 assert!(!entry.is_expired(50_000_000));
582
583 assert!(!entry.is_expired(PROOF_CACHE_TTL_NS));
585
586 assert!(entry.is_expired(PROOF_CACHE_TTL_NS + 1));
588
589 assert!(!entry.is_expired(0));
591 }
592}