ruvector_mincut/certificate/
audit.rs

1//! Audit trail for cut changes
2//!
3//! Logs every witness change with full provenance.
4
5use super::{LocalKCutResponse, UpdateTrigger, CertLocalKCutQuery, LocalKCutResultSummary};
6use crate::instance::WitnessHandle;
7use std::collections::VecDeque;
8use std::sync::{Arc, RwLock};
9use std::time::{SystemTime, UNIX_EPOCH};
10use serde::{Deserialize, Serialize};
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 { budget, radius, seeds } = &entries[0].data {
380            assert_eq!(*budget, 10);
381            assert_eq!(*radius, 5);
382            assert_eq!(seeds.len(), 3);
383        } else {
384            panic!("Wrong data type");
385        }
386    }
387
388    #[test]
389    fn test_log_response() {
390        let logger = AuditLogger::new(10);
391        let query = CertLocalKCutQuery::new(vec![1], 5, 2);
392        let result = LocalKCutResultSummary::Found {
393            cut_value: 3,
394            witness_hash: 999,
395        };
396        let response = LocalKCutResponse::new(query, result, 100, None);
397
398        logger.log_response(&response);
399
400        let entries = logger.by_type(AuditEntryType::LocalKCutResponse);
401        assert_eq!(entries.len(), 1);
402
403        if let AuditData::Response { found, value } = &entries[0].data {
404            assert!(found);
405            assert_eq!(*value, Some(3));
406        } else {
407            panic!("Wrong data type");
408        }
409    }
410
411    #[test]
412    fn test_log_mincut_changed() {
413        let logger = AuditLogger::new(10);
414        let trigger = UpdateTrigger::new(UpdateType::Insert, 123, (1, 2), 1000);
415
416        logger.log_mincut_changed(10, 8, trigger);
417
418        let entries = logger.by_type(AuditEntryType::MinCutChanged);
419        assert_eq!(entries.len(), 1);
420
421        if let AuditData::MinCut { old_value, new_value, .. } = &entries[0].data {
422            assert_eq!(*old_value, 10);
423            assert_eq!(*new_value, 8);
424        } else {
425            panic!("Wrong data type");
426        }
427    }
428
429    #[test]
430    fn test_log_certificate_created() {
431        let logger = AuditLogger::new(10);
432        logger.log_certificate_created(5, 10, Some(8));
433
434        let entries = logger.by_type(AuditEntryType::CertificateCreated);
435        assert_eq!(entries.len(), 1);
436
437        if let AuditData::Certificate {
438            num_witnesses,
439            num_responses,
440            certified_value,
441        } = &entries[0].data
442        {
443            assert_eq!(*num_witnesses, 5);
444            assert_eq!(*num_responses, 10);
445            assert_eq!(*certified_value, Some(8));
446        } else {
447            panic!("Wrong data type");
448        }
449    }
450
451    #[test]
452    fn test_max_entries() {
453        let logger = AuditLogger::new(3);
454
455        for i in 0..5 {
456            logger.log(
457                AuditEntryType::WitnessCreated,
458                AuditData::Witness {
459                    hash: i,
460                    boundary: i,
461                    seed: i,
462                },
463            );
464        }
465
466        // Should only keep last 3 entries
467        assert_eq!(logger.len(), 3);
468
469        let entries = logger.export();
470        // First entry should have id 2 (0 and 1 were evicted)
471        assert!(entries[0].id >= 2);
472    }
473
474    #[test]
475    fn test_recent() {
476        let logger = AuditLogger::new(10);
477
478        for i in 0..5 {
479            logger.log(
480                AuditEntryType::WitnessCreated,
481                AuditData::Witness {
482                    hash: i,
483                    boundary: i,
484                    seed: i,
485                },
486            );
487        }
488
489        let recent = logger.recent(3);
490        assert_eq!(recent.len(), 3);
491
492        // Should be the last 3 entries
493        assert_eq!(recent[0].id, 2);
494        assert_eq!(recent[1].id, 3);
495        assert_eq!(recent[2].id, 4);
496    }
497
498    #[test]
499    fn test_by_type() {
500        let logger = AuditLogger::new(10);
501
502        logger.log(
503            AuditEntryType::WitnessCreated,
504            AuditData::Witness { hash: 1, boundary: 1, seed: 1 },
505        );
506        logger.log(
507            AuditEntryType::WitnessUpdated,
508            AuditData::Witness { hash: 2, boundary: 2, seed: 2 },
509        );
510        logger.log(
511            AuditEntryType::WitnessCreated,
512            AuditData::Witness { hash: 3, boundary: 3, seed: 3 },
513        );
514
515        let created = logger.by_type(AuditEntryType::WitnessCreated);
516        let updated = logger.by_type(AuditEntryType::WitnessUpdated);
517
518        assert_eq!(created.len(), 2);
519        assert_eq!(updated.len(), 1);
520    }
521
522    #[test]
523    fn test_clear() {
524        let logger = AuditLogger::new(10);
525
526        logger.log(
527            AuditEntryType::WitnessCreated,
528            AuditData::Witness { hash: 1, boundary: 1, seed: 1 },
529        );
530
531        assert_eq!(logger.len(), 1);
532
533        logger.clear();
534
535        assert_eq!(logger.len(), 0);
536        assert!(logger.is_empty());
537    }
538
539    #[test]
540    fn test_json_export() {
541        let logger = AuditLogger::new(10);
542
543        logger.log(
544            AuditEntryType::WitnessCreated,
545            AuditData::Witness { hash: 1, boundary: 5, seed: 2 },
546        );
547
548        let json = logger.to_json().unwrap();
549        assert!(json.contains("WitnessCreated"));
550        // JSON might have spaces, check for "boundary" and "5" separately
551        assert!(json.contains("boundary"));
552        assert!(json.contains("5"));
553    }
554
555    #[test]
556    fn test_clone() {
557        let logger = AuditLogger::new(10);
558        logger.log(
559            AuditEntryType::WitnessCreated,
560            AuditData::Witness { hash: 1, boundary: 1, seed: 1 },
561        );
562
563        let cloned = logger.clone();
564        assert_eq!(cloned.len(), 1);
565        assert_eq!(cloned.capacity(), 10);
566
567        // Both should share the same data
568        logger.log(
569            AuditEntryType::WitnessUpdated,
570            AuditData::Witness { hash: 2, boundary: 2, seed: 2 },
571        );
572
573        assert_eq!(cloned.len(), 2);
574    }
575
576    #[test]
577    fn test_entry_timestamps() {
578        let logger = AuditLogger::new(10);
579
580        logger.log(
581            AuditEntryType::WitnessCreated,
582            AuditData::Witness { hash: 1, boundary: 1, seed: 1 },
583        );
584
585        let entries = logger.export();
586        assert!(entries[0].timestamp > 0);
587    }
588}