ruvector_mincut/certificate/
audit.rs

1//! Audit trail for cut changes
2//!
3//! Logs every witness change with full provenance.
4
5use super::{CertLocalKCutQuery, LocalKCutResponse, LocalKCutResultSummary, UpdateTrigger};
6use crate::instance::WitnessHandle;
7use serde::{Deserialize, Serialize};
8use std::collections::VecDeque;
9use std::sync::{Arc, RwLock};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12/// Audit log entry
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AuditEntry {
15    /// Entry ID
16    pub id: u64,
17    /// Timestamp (seconds since UNIX epoch)
18    pub timestamp: u64,
19    /// Type of entry
20    pub entry_type: AuditEntryType,
21    /// Associated data
22    pub data: AuditData,
23}
24
25impl AuditEntry {
26    /// Create a new audit entry
27    pub fn new(id: u64, entry_type: AuditEntryType, data: AuditData) -> Self {
28        let timestamp = SystemTime::now()
29            .duration_since(UNIX_EPOCH)
30            .unwrap_or_default()
31            .as_secs();
32
33        Self {
34            id,
35            timestamp,
36            entry_type,
37            data,
38        }
39    }
40}
41
42/// Type of audit entry
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub enum AuditEntryType {
45    /// A witness was created
46    WitnessCreated,
47    /// A witness was updated
48    WitnessUpdated,
49    /// A witness was evicted from the cache
50    WitnessEvicted,
51    /// A LocalKCut query was made
52    LocalKCutQuery,
53    /// A LocalKCut response was received
54    LocalKCutResponse,
55    /// A certificate was created
56    CertificateCreated,
57    /// The minimum cut value changed
58    MinCutChanged,
59}
60
61/// Data associated with an audit entry
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum AuditData {
64    /// Witness-related data
65    Witness {
66        /// Hash of the witness
67        hash: u64,
68        /// Boundary value
69        boundary: u64,
70        /// Seed vertex
71        seed: u64,
72    },
73    /// Query data
74    Query {
75        /// Budget parameter
76        budget: u64,
77        /// Search radius
78        radius: usize,
79        /// Seed vertices
80        seeds: Vec<u64>,
81    },
82    /// Response data
83    Response {
84        /// Whether a cut was found
85        found: bool,
86        /// Cut value if found
87        value: Option<u64>,
88    },
89    /// Minimum cut change
90    MinCut {
91        /// Old minimum cut value
92        old_value: u64,
93        /// New minimum cut value
94        new_value: u64,
95        /// Update that triggered the change
96        trigger: UpdateTrigger,
97    },
98    /// Certificate creation
99    Certificate {
100        /// Number of witnesses
101        num_witnesses: usize,
102        /// Number of responses
103        num_responses: usize,
104        /// Certified value
105        certified_value: Option<u64>,
106    },
107}
108
109/// Thread-safe audit logger
110pub struct AuditLogger {
111    /// Circular buffer of entries
112    entries: Arc<RwLock<VecDeque<AuditEntry>>>,
113    /// Maximum number of entries to keep
114    max_entries: usize,
115    /// Next entry ID
116    next_id: Arc<RwLock<u64>>,
117}
118
119impl AuditLogger {
120    /// Create a new audit logger with specified capacity
121    pub fn new(max_entries: usize) -> Self {
122        Self {
123            entries: Arc::new(RwLock::new(VecDeque::with_capacity(max_entries))),
124            max_entries,
125            next_id: Arc::new(RwLock::new(0)),
126        }
127    }
128
129    /// Log a new entry
130    pub fn log(&self, entry_type: AuditEntryType, data: AuditData) {
131        let mut entries = self.entries.write().unwrap();
132        let mut next_id = self.next_id.write().unwrap();
133
134        let entry = AuditEntry::new(*next_id, entry_type, data);
135        *next_id += 1;
136
137        entries.push_back(entry);
138
139        // Maintain maximum size
140        while entries.len() > self.max_entries {
141            entries.pop_front();
142        }
143    }
144
145    /// Log witness creation
146    pub fn log_witness_created(&self, witness: &WitnessHandle) {
147        self.log(
148            AuditEntryType::WitnessCreated,
149            AuditData::Witness {
150                hash: self.compute_witness_hash(witness),
151                boundary: witness.boundary_size(),
152                seed: witness.seed(),
153            },
154        );
155    }
156
157    /// Log witness update
158    pub fn log_witness_updated(&self, witness: &WitnessHandle) {
159        self.log(
160            AuditEntryType::WitnessUpdated,
161            AuditData::Witness {
162                hash: self.compute_witness_hash(witness),
163                boundary: witness.boundary_size(),
164                seed: witness.seed(),
165            },
166        );
167    }
168
169    /// Log witness eviction
170    pub fn log_witness_evicted(&self, witness: &WitnessHandle) {
171        self.log(
172            AuditEntryType::WitnessEvicted,
173            AuditData::Witness {
174                hash: self.compute_witness_hash(witness),
175                boundary: witness.boundary_size(),
176                seed: witness.seed(),
177            },
178        );
179    }
180
181    /// Log LocalKCut query
182    pub fn log_query(&self, budget: u64, radius: usize, seeds: Vec<u64>) {
183        self.log(
184            AuditEntryType::LocalKCutQuery,
185            AuditData::Query {
186                budget,
187                radius,
188                seeds,
189            },
190        );
191    }
192
193    /// Log LocalKCut response
194    pub fn log_response(&self, response: &LocalKCutResponse) {
195        let (found, value) = match &response.result {
196            super::LocalKCutResultSummary::Found { cut_value, .. } => (true, Some(*cut_value)),
197            super::LocalKCutResultSummary::NoneInLocality => (false, None),
198        };
199
200        self.log(
201            AuditEntryType::LocalKCutResponse,
202            AuditData::Response { found, value },
203        );
204    }
205
206    /// Log minimum cut change
207    pub fn log_mincut_changed(&self, old_value: u64, new_value: u64, trigger: UpdateTrigger) {
208        self.log(
209            AuditEntryType::MinCutChanged,
210            AuditData::MinCut {
211                old_value,
212                new_value,
213                trigger,
214            },
215        );
216    }
217
218    /// Log certificate creation
219    pub fn log_certificate_created(
220        &self,
221        num_witnesses: usize,
222        num_responses: usize,
223        certified_value: Option<u64>,
224    ) {
225        self.log(
226            AuditEntryType::CertificateCreated,
227            AuditData::Certificate {
228                num_witnesses,
229                num_responses,
230                certified_value,
231            },
232        );
233    }
234
235    /// Get recent entries (up to count)
236    pub fn recent(&self, count: usize) -> Vec<AuditEntry> {
237        let entries = self.entries.read().unwrap();
238        let start = entries.len().saturating_sub(count);
239        entries.iter().skip(start).cloned().collect()
240    }
241
242    /// Get entries by type
243    pub fn by_type(&self, entry_type: AuditEntryType) -> Vec<AuditEntry> {
244        let entries = self.entries.read().unwrap();
245        entries
246            .iter()
247            .filter(|e| e.entry_type == entry_type)
248            .cloned()
249            .collect()
250    }
251
252    /// Export full log
253    pub fn export(&self) -> Vec<AuditEntry> {
254        let entries = self.entries.read().unwrap();
255        entries.iter().cloned().collect()
256    }
257
258    /// Export log to JSON
259    pub fn to_json(&self) -> Result<String, String> {
260        let entries = self.export();
261        serde_json::to_string_pretty(&entries).map_err(|e| e.to_string())
262    }
263
264    /// Clear the log
265    pub fn clear(&self) {
266        let mut entries = self.entries.write().unwrap();
267        entries.clear();
268        let mut next_id = self.next_id.write().unwrap();
269        *next_id = 0;
270    }
271
272    /// Get number of entries
273    pub fn len(&self) -> usize {
274        let entries = self.entries.read().unwrap();
275        entries.len()
276    }
277
278    /// Check if log is empty
279    pub fn is_empty(&self) -> bool {
280        self.len() == 0
281    }
282
283    /// Get maximum capacity
284    pub fn capacity(&self) -> usize {
285        self.max_entries
286    }
287
288    /// Compute a simple hash for a witness
289    fn compute_witness_hash(&self, witness: &WitnessHandle) -> u64 {
290        // Simple hash combining seed and boundary
291        let seed = witness.seed();
292        let boundary = witness.boundary_size();
293        seed.wrapping_mul(31).wrapping_add(boundary)
294    }
295}
296
297impl Default for AuditLogger {
298    fn default() -> Self {
299        Self::new(1000)
300    }
301}
302
303impl Clone for AuditLogger {
304    fn clone(&self) -> Self {
305        Self {
306            entries: Arc::clone(&self.entries),
307            max_entries: self.max_entries,
308            next_id: Arc::clone(&self.next_id),
309        }
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::certificate::{CertLocalKCutQuery, LocalKCutResultSummary, UpdateType};
317    use roaring::RoaringBitmap;
318
319    #[test]
320    fn test_new_logger() {
321        let logger = AuditLogger::new(100);
322        assert_eq!(logger.capacity(), 100);
323        assert_eq!(logger.len(), 0);
324        assert!(logger.is_empty());
325    }
326
327    #[test]
328    fn test_log_entry() {
329        let logger = AuditLogger::new(10);
330        logger.log(
331            AuditEntryType::WitnessCreated,
332            AuditData::Witness {
333                hash: 123,
334                boundary: 5,
335                seed: 1,
336            },
337        );
338
339        assert_eq!(logger.len(), 1);
340        assert!(!logger.is_empty());
341
342        let entries = logger.export();
343        assert_eq!(entries.len(), 1);
344        assert_eq!(entries[0].entry_type, AuditEntryType::WitnessCreated);
345    }
346
347    #[test]
348    fn test_log_witness_created() {
349        let logger = AuditLogger::new(10);
350        let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
351
352        logger.log_witness_created(&witness);
353        assert_eq!(logger.len(), 1);
354
355        let entries = logger.by_type(AuditEntryType::WitnessCreated);
356        assert_eq!(entries.len(), 1);
357    }
358
359    #[test]
360    fn test_log_witness_updated() {
361        let logger = AuditLogger::new(10);
362        let witness = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 3]), 3);
363
364        logger.log_witness_updated(&witness);
365        assert_eq!(logger.len(), 1);
366
367        let entries = logger.by_type(AuditEntryType::WitnessUpdated);
368        assert_eq!(entries.len(), 1);
369    }
370
371    #[test]
372    fn test_log_query() {
373        let logger = AuditLogger::new(10);
374        logger.log_query(10, 5, vec![1, 2, 3]);
375
376        let entries = logger.by_type(AuditEntryType::LocalKCutQuery);
377        assert_eq!(entries.len(), 1);
378
379        if let AuditData::Query {
380            budget,
381            radius,
382            seeds,
383        } = &entries[0].data
384        {
385            assert_eq!(*budget, 10);
386            assert_eq!(*radius, 5);
387            assert_eq!(seeds.len(), 3);
388        } else {
389            panic!("Wrong data type");
390        }
391    }
392
393    #[test]
394    fn test_log_response() {
395        let logger = AuditLogger::new(10);
396        let query = CertLocalKCutQuery::new(vec![1], 5, 2);
397        let result = LocalKCutResultSummary::Found {
398            cut_value: 3,
399            witness_hash: 999,
400        };
401        let response = LocalKCutResponse::new(query, result, 100, None);
402
403        logger.log_response(&response);
404
405        let entries = logger.by_type(AuditEntryType::LocalKCutResponse);
406        assert_eq!(entries.len(), 1);
407
408        if let AuditData::Response { found, value } = &entries[0].data {
409            assert!(found);
410            assert_eq!(*value, Some(3));
411        } else {
412            panic!("Wrong data type");
413        }
414    }
415
416    #[test]
417    fn test_log_mincut_changed() {
418        let logger = AuditLogger::new(10);
419        let trigger = UpdateTrigger::new(UpdateType::Insert, 123, (1, 2), 1000);
420
421        logger.log_mincut_changed(10, 8, trigger);
422
423        let entries = logger.by_type(AuditEntryType::MinCutChanged);
424        assert_eq!(entries.len(), 1);
425
426        if let AuditData::MinCut {
427            old_value,
428            new_value,
429            ..
430        } = &entries[0].data
431        {
432            assert_eq!(*old_value, 10);
433            assert_eq!(*new_value, 8);
434        } else {
435            panic!("Wrong data type");
436        }
437    }
438
439    #[test]
440    fn test_log_certificate_created() {
441        let logger = AuditLogger::new(10);
442        logger.log_certificate_created(5, 10, Some(8));
443
444        let entries = logger.by_type(AuditEntryType::CertificateCreated);
445        assert_eq!(entries.len(), 1);
446
447        if let AuditData::Certificate {
448            num_witnesses,
449            num_responses,
450            certified_value,
451        } = &entries[0].data
452        {
453            assert_eq!(*num_witnesses, 5);
454            assert_eq!(*num_responses, 10);
455            assert_eq!(*certified_value, Some(8));
456        } else {
457            panic!("Wrong data type");
458        }
459    }
460
461    #[test]
462    fn test_max_entries() {
463        let logger = AuditLogger::new(3);
464
465        for i in 0..5 {
466            logger.log(
467                AuditEntryType::WitnessCreated,
468                AuditData::Witness {
469                    hash: i,
470                    boundary: i,
471                    seed: i,
472                },
473            );
474        }
475
476        // Should only keep last 3 entries
477        assert_eq!(logger.len(), 3);
478
479        let entries = logger.export();
480        // First entry should have id 2 (0 and 1 were evicted)
481        assert!(entries[0].id >= 2);
482    }
483
484    #[test]
485    fn test_recent() {
486        let logger = AuditLogger::new(10);
487
488        for i in 0..5 {
489            logger.log(
490                AuditEntryType::WitnessCreated,
491                AuditData::Witness {
492                    hash: i,
493                    boundary: i,
494                    seed: i,
495                },
496            );
497        }
498
499        let recent = logger.recent(3);
500        assert_eq!(recent.len(), 3);
501
502        // Should be the last 3 entries
503        assert_eq!(recent[0].id, 2);
504        assert_eq!(recent[1].id, 3);
505        assert_eq!(recent[2].id, 4);
506    }
507
508    #[test]
509    fn test_by_type() {
510        let logger = AuditLogger::new(10);
511
512        logger.log(
513            AuditEntryType::WitnessCreated,
514            AuditData::Witness {
515                hash: 1,
516                boundary: 1,
517                seed: 1,
518            },
519        );
520        logger.log(
521            AuditEntryType::WitnessUpdated,
522            AuditData::Witness {
523                hash: 2,
524                boundary: 2,
525                seed: 2,
526            },
527        );
528        logger.log(
529            AuditEntryType::WitnessCreated,
530            AuditData::Witness {
531                hash: 3,
532                boundary: 3,
533                seed: 3,
534            },
535        );
536
537        let created = logger.by_type(AuditEntryType::WitnessCreated);
538        let updated = logger.by_type(AuditEntryType::WitnessUpdated);
539
540        assert_eq!(created.len(), 2);
541        assert_eq!(updated.len(), 1);
542    }
543
544    #[test]
545    fn test_clear() {
546        let logger = AuditLogger::new(10);
547
548        logger.log(
549            AuditEntryType::WitnessCreated,
550            AuditData::Witness {
551                hash: 1,
552                boundary: 1,
553                seed: 1,
554            },
555        );
556
557        assert_eq!(logger.len(), 1);
558
559        logger.clear();
560
561        assert_eq!(logger.len(), 0);
562        assert!(logger.is_empty());
563    }
564
565    #[test]
566    fn test_json_export() {
567        let logger = AuditLogger::new(10);
568
569        logger.log(
570            AuditEntryType::WitnessCreated,
571            AuditData::Witness {
572                hash: 1,
573                boundary: 5,
574                seed: 2,
575            },
576        );
577
578        let json = logger.to_json().unwrap();
579        assert!(json.contains("WitnessCreated"));
580        // JSON might have spaces, check for "boundary" and "5" separately
581        assert!(json.contains("boundary"));
582        assert!(json.contains("5"));
583    }
584
585    #[test]
586    fn test_clone() {
587        let logger = AuditLogger::new(10);
588        logger.log(
589            AuditEntryType::WitnessCreated,
590            AuditData::Witness {
591                hash: 1,
592                boundary: 1,
593                seed: 1,
594            },
595        );
596
597        let cloned = logger.clone();
598        assert_eq!(cloned.len(), 1);
599        assert_eq!(cloned.capacity(), 10);
600
601        // Both should share the same data
602        logger.log(
603            AuditEntryType::WitnessUpdated,
604            AuditData::Witness {
605                hash: 2,
606                boundary: 2,
607                seed: 2,
608            },
609        );
610
611        assert_eq!(cloned.len(), 2);
612    }
613
614    #[test]
615    fn test_entry_timestamps() {
616        let logger = AuditLogger::new(10);
617
618        logger.log(
619            AuditEntryType::WitnessCreated,
620            AuditData::Witness {
621                hash: 1,
622                boundary: 1,
623                seed: 1,
624            },
625        );
626
627        let entries = logger.export();
628        assert!(entries[0].timestamp > 0);
629    }
630}