Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
630
vendor/ruvector/crates/ruvector-mincut/src/certificate/audit.rs
vendored
Normal file
630
vendor/ruvector/crates/ruvector-mincut/src/certificate/audit.rs
vendored
Normal file
@@ -0,0 +1,630 @@
|
||||
//! Audit trail for cut changes
|
||||
//!
|
||||
//! Logs every witness change with full provenance.
|
||||
|
||||
use super::{CertLocalKCutQuery, LocalKCutResponse, LocalKCutResultSummary, UpdateTrigger};
|
||||
use crate::instance::WitnessHandle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Audit log entry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuditEntry {
|
||||
/// Entry ID
|
||||
pub id: u64,
|
||||
/// Timestamp (seconds since UNIX epoch)
|
||||
pub timestamp: u64,
|
||||
/// Type of entry
|
||||
pub entry_type: AuditEntryType,
|
||||
/// Associated data
|
||||
pub data: AuditData,
|
||||
}
|
||||
|
||||
impl AuditEntry {
|
||||
/// Create a new audit entry
|
||||
pub fn new(id: u64, entry_type: AuditEntryType, data: AuditData) -> Self {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Self {
|
||||
id,
|
||||
timestamp,
|
||||
entry_type,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of audit entry
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AuditEntryType {
|
||||
/// A witness was created
|
||||
WitnessCreated,
|
||||
/// A witness was updated
|
||||
WitnessUpdated,
|
||||
/// A witness was evicted from the cache
|
||||
WitnessEvicted,
|
||||
/// A LocalKCut query was made
|
||||
LocalKCutQuery,
|
||||
/// A LocalKCut response was received
|
||||
LocalKCutResponse,
|
||||
/// A certificate was created
|
||||
CertificateCreated,
|
||||
/// The minimum cut value changed
|
||||
MinCutChanged,
|
||||
}
|
||||
|
||||
/// Data associated with an audit entry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum AuditData {
|
||||
/// Witness-related data
|
||||
Witness {
|
||||
/// Hash of the witness
|
||||
hash: u64,
|
||||
/// Boundary value
|
||||
boundary: u64,
|
||||
/// Seed vertex
|
||||
seed: u64,
|
||||
},
|
||||
/// Query data
|
||||
Query {
|
||||
/// Budget parameter
|
||||
budget: u64,
|
||||
/// Search radius
|
||||
radius: usize,
|
||||
/// Seed vertices
|
||||
seeds: Vec<u64>,
|
||||
},
|
||||
/// Response data
|
||||
Response {
|
||||
/// Whether a cut was found
|
||||
found: bool,
|
||||
/// Cut value if found
|
||||
value: Option<u64>,
|
||||
},
|
||||
/// Minimum cut change
|
||||
MinCut {
|
||||
/// Old minimum cut value
|
||||
old_value: u64,
|
||||
/// New minimum cut value
|
||||
new_value: u64,
|
||||
/// Update that triggered the change
|
||||
trigger: UpdateTrigger,
|
||||
},
|
||||
/// Certificate creation
|
||||
Certificate {
|
||||
/// Number of witnesses
|
||||
num_witnesses: usize,
|
||||
/// Number of responses
|
||||
num_responses: usize,
|
||||
/// Certified value
|
||||
certified_value: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Thread-safe audit logger
|
||||
pub struct AuditLogger {
|
||||
/// Circular buffer of entries
|
||||
entries: Arc<RwLock<VecDeque<AuditEntry>>>,
|
||||
/// Maximum number of entries to keep
|
||||
max_entries: usize,
|
||||
/// Next entry ID
|
||||
next_id: Arc<RwLock<u64>>,
|
||||
}
|
||||
|
||||
impl AuditLogger {
|
||||
/// Create a new audit logger with specified capacity
|
||||
pub fn new(max_entries: usize) -> Self {
|
||||
Self {
|
||||
entries: Arc::new(RwLock::new(VecDeque::with_capacity(max_entries))),
|
||||
max_entries,
|
||||
next_id: Arc::new(RwLock::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a new entry
|
||||
pub fn log(&self, entry_type: AuditEntryType, data: AuditData) {
|
||||
let mut entries = self.entries.write().unwrap();
|
||||
let mut next_id = self.next_id.write().unwrap();
|
||||
|
||||
let entry = AuditEntry::new(*next_id, entry_type, data);
|
||||
*next_id += 1;
|
||||
|
||||
entries.push_back(entry);
|
||||
|
||||
// Maintain maximum size
|
||||
while entries.len() > self.max_entries {
|
||||
entries.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// Log witness creation
|
||||
pub fn log_witness_created(&self, witness: &WitnessHandle) {
|
||||
self.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: self.compute_witness_hash(witness),
|
||||
boundary: witness.boundary_size(),
|
||||
seed: witness.seed(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Log witness update
|
||||
pub fn log_witness_updated(&self, witness: &WitnessHandle) {
|
||||
self.log(
|
||||
AuditEntryType::WitnessUpdated,
|
||||
AuditData::Witness {
|
||||
hash: self.compute_witness_hash(witness),
|
||||
boundary: witness.boundary_size(),
|
||||
seed: witness.seed(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Log witness eviction
|
||||
pub fn log_witness_evicted(&self, witness: &WitnessHandle) {
|
||||
self.log(
|
||||
AuditEntryType::WitnessEvicted,
|
||||
AuditData::Witness {
|
||||
hash: self.compute_witness_hash(witness),
|
||||
boundary: witness.boundary_size(),
|
||||
seed: witness.seed(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Log LocalKCut query
|
||||
pub fn log_query(&self, budget: u64, radius: usize, seeds: Vec<u64>) {
|
||||
self.log(
|
||||
AuditEntryType::LocalKCutQuery,
|
||||
AuditData::Query {
|
||||
budget,
|
||||
radius,
|
||||
seeds,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Log LocalKCut response
|
||||
pub fn log_response(&self, response: &LocalKCutResponse) {
|
||||
let (found, value) = match &response.result {
|
||||
super::LocalKCutResultSummary::Found { cut_value, .. } => (true, Some(*cut_value)),
|
||||
super::LocalKCutResultSummary::NoneInLocality => (false, None),
|
||||
};
|
||||
|
||||
self.log(
|
||||
AuditEntryType::LocalKCutResponse,
|
||||
AuditData::Response { found, value },
|
||||
);
|
||||
}
|
||||
|
||||
/// Log minimum cut change
|
||||
pub fn log_mincut_changed(&self, old_value: u64, new_value: u64, trigger: UpdateTrigger) {
|
||||
self.log(
|
||||
AuditEntryType::MinCutChanged,
|
||||
AuditData::MinCut {
|
||||
old_value,
|
||||
new_value,
|
||||
trigger,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Log certificate creation
|
||||
pub fn log_certificate_created(
|
||||
&self,
|
||||
num_witnesses: usize,
|
||||
num_responses: usize,
|
||||
certified_value: Option<u64>,
|
||||
) {
|
||||
self.log(
|
||||
AuditEntryType::CertificateCreated,
|
||||
AuditData::Certificate {
|
||||
num_witnesses,
|
||||
num_responses,
|
||||
certified_value,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Get recent entries (up to count)
|
||||
pub fn recent(&self, count: usize) -> Vec<AuditEntry> {
|
||||
let entries = self.entries.read().unwrap();
|
||||
let start = entries.len().saturating_sub(count);
|
||||
entries.iter().skip(start).cloned().collect()
|
||||
}
|
||||
|
||||
/// Get entries by type
|
||||
pub fn by_type(&self, entry_type: AuditEntryType) -> Vec<AuditEntry> {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries
|
||||
.iter()
|
||||
.filter(|e| e.entry_type == entry_type)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Export full log
|
||||
pub fn export(&self) -> Vec<AuditEntry> {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries.iter().cloned().collect()
|
||||
}
|
||||
|
||||
/// Export log to JSON
|
||||
pub fn to_json(&self) -> Result<String, String> {
|
||||
let entries = self.export();
|
||||
serde_json::to_string_pretty(&entries).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Clear the log
|
||||
pub fn clear(&self) {
|
||||
let mut entries = self.entries.write().unwrap();
|
||||
entries.clear();
|
||||
let mut next_id = self.next_id.write().unwrap();
|
||||
*next_id = 0;
|
||||
}
|
||||
|
||||
/// Get number of entries
|
||||
pub fn len(&self) -> usize {
|
||||
let entries = self.entries.read().unwrap();
|
||||
entries.len()
|
||||
}
|
||||
|
||||
/// Check if log is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Get maximum capacity
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.max_entries
|
||||
}
|
||||
|
||||
/// Compute a simple hash for a witness
|
||||
fn compute_witness_hash(&self, witness: &WitnessHandle) -> u64 {
|
||||
// Simple hash combining seed and boundary
|
||||
let seed = witness.seed();
|
||||
let boundary = witness.boundary_size();
|
||||
seed.wrapping_mul(31).wrapping_add(boundary)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AuditLogger {
|
||||
fn default() -> Self {
|
||||
Self::new(1000)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for AuditLogger {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
entries: Arc::clone(&self.entries),
|
||||
max_entries: self.max_entries,
|
||||
next_id: Arc::clone(&self.next_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::certificate::{CertLocalKCutQuery, LocalKCutResultSummary, UpdateType};
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
#[test]
|
||||
fn test_new_logger() {
|
||||
let logger = AuditLogger::new(100);
|
||||
assert_eq!(logger.capacity(), 100);
|
||||
assert_eq!(logger.len(), 0);
|
||||
assert!(logger.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry() {
|
||||
let logger = AuditLogger::new(10);
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 123,
|
||||
boundary: 5,
|
||||
seed: 1,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(logger.len(), 1);
|
||||
assert!(!logger.is_empty());
|
||||
|
||||
let entries = logger.export();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].entry_type, AuditEntryType::WitnessCreated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_witness_created() {
|
||||
let logger = AuditLogger::new(10);
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
|
||||
logger.log_witness_created(&witness);
|
||||
assert_eq!(logger.len(), 1);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::WitnessCreated);
|
||||
assert_eq!(entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_witness_updated() {
|
||||
let logger = AuditLogger::new(10);
|
||||
let witness = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 3]), 3);
|
||||
|
||||
logger.log_witness_updated(&witness);
|
||||
assert_eq!(logger.len(), 1);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::WitnessUpdated);
|
||||
assert_eq!(entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_query() {
|
||||
let logger = AuditLogger::new(10);
|
||||
logger.log_query(10, 5, vec![1, 2, 3]);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::LocalKCutQuery);
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
if let AuditData::Query {
|
||||
budget,
|
||||
radius,
|
||||
seeds,
|
||||
} = &entries[0].data
|
||||
{
|
||||
assert_eq!(*budget, 10);
|
||||
assert_eq!(*radius, 5);
|
||||
assert_eq!(seeds.len(), 3);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_response() {
|
||||
let logger = AuditLogger::new(10);
|
||||
let query = CertLocalKCutQuery::new(vec![1], 5, 2);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 3,
|
||||
witness_hash: 999,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
|
||||
logger.log_response(&response);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::LocalKCutResponse);
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
if let AuditData::Response { found, value } = &entries[0].data {
|
||||
assert!(found);
|
||||
assert_eq!(*value, Some(3));
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_mincut_changed() {
|
||||
let logger = AuditLogger::new(10);
|
||||
let trigger = UpdateTrigger::new(UpdateType::Insert, 123, (1, 2), 1000);
|
||||
|
||||
logger.log_mincut_changed(10, 8, trigger);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::MinCutChanged);
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
if let AuditData::MinCut {
|
||||
old_value,
|
||||
new_value,
|
||||
..
|
||||
} = &entries[0].data
|
||||
{
|
||||
assert_eq!(*old_value, 10);
|
||||
assert_eq!(*new_value, 8);
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_certificate_created() {
|
||||
let logger = AuditLogger::new(10);
|
||||
logger.log_certificate_created(5, 10, Some(8));
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::CertificateCreated);
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
if let AuditData::Certificate {
|
||||
num_witnesses,
|
||||
num_responses,
|
||||
certified_value,
|
||||
} = &entries[0].data
|
||||
{
|
||||
assert_eq!(*num_witnesses, 5);
|
||||
assert_eq!(*num_responses, 10);
|
||||
assert_eq!(*certified_value, Some(8));
|
||||
} else {
|
||||
panic!("Wrong data type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_entries() {
|
||||
let logger = AuditLogger::new(3);
|
||||
|
||||
for i in 0..5 {
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: i,
|
||||
boundary: i,
|
||||
seed: i,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Should only keep last 3 entries
|
||||
assert_eq!(logger.len(), 3);
|
||||
|
||||
let entries = logger.export();
|
||||
// First entry should have id 2 (0 and 1 were evicted)
|
||||
assert!(entries[0].id >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recent() {
|
||||
let logger = AuditLogger::new(10);
|
||||
|
||||
for i in 0..5 {
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: i,
|
||||
boundary: i,
|
||||
seed: i,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let recent = logger.recent(3);
|
||||
assert_eq!(recent.len(), 3);
|
||||
|
||||
// Should be the last 3 entries
|
||||
assert_eq!(recent[0].id, 2);
|
||||
assert_eq!(recent[1].id, 3);
|
||||
assert_eq!(recent[2].id, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_by_type() {
|
||||
let logger = AuditLogger::new(10);
|
||||
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 1,
|
||||
boundary: 1,
|
||||
seed: 1,
|
||||
},
|
||||
);
|
||||
logger.log(
|
||||
AuditEntryType::WitnessUpdated,
|
||||
AuditData::Witness {
|
||||
hash: 2,
|
||||
boundary: 2,
|
||||
seed: 2,
|
||||
},
|
||||
);
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 3,
|
||||
boundary: 3,
|
||||
seed: 3,
|
||||
},
|
||||
);
|
||||
|
||||
let created = logger.by_type(AuditEntryType::WitnessCreated);
|
||||
let updated = logger.by_type(AuditEntryType::WitnessUpdated);
|
||||
|
||||
assert_eq!(created.len(), 2);
|
||||
assert_eq!(updated.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let logger = AuditLogger::new(10);
|
||||
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 1,
|
||||
boundary: 1,
|
||||
seed: 1,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(logger.len(), 1);
|
||||
|
||||
logger.clear();
|
||||
|
||||
assert_eq!(logger.len(), 0);
|
||||
assert!(logger.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_export() {
|
||||
let logger = AuditLogger::new(10);
|
||||
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 1,
|
||||
boundary: 5,
|
||||
seed: 2,
|
||||
},
|
||||
);
|
||||
|
||||
let json = logger.to_json().unwrap();
|
||||
assert!(json.contains("WitnessCreated"));
|
||||
// JSON might have spaces, check for "boundary" and "5" separately
|
||||
assert!(json.contains("boundary"));
|
||||
assert!(json.contains("5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone() {
|
||||
let logger = AuditLogger::new(10);
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 1,
|
||||
boundary: 1,
|
||||
seed: 1,
|
||||
},
|
||||
);
|
||||
|
||||
let cloned = logger.clone();
|
||||
assert_eq!(cloned.len(), 1);
|
||||
assert_eq!(cloned.capacity(), 10);
|
||||
|
||||
// Both should share the same data
|
||||
logger.log(
|
||||
AuditEntryType::WitnessUpdated,
|
||||
AuditData::Witness {
|
||||
hash: 2,
|
||||
boundary: 2,
|
||||
seed: 2,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(cloned.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entry_timestamps() {
|
||||
let logger = AuditLogger::new(10);
|
||||
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 1,
|
||||
boundary: 1,
|
||||
seed: 1,
|
||||
},
|
||||
);
|
||||
|
||||
let entries = logger.export();
|
||||
assert!(entries[0].timestamp > 0);
|
||||
}
|
||||
}
|
||||
546
vendor/ruvector/crates/ruvector-mincut/src/certificate/mod.rs
vendored
Normal file
546
vendor/ruvector/crates/ruvector-mincut/src/certificate/mod.rs
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
//! Certificate system for cut verification
|
||||
//!
|
||||
//! Provides provable certificates that a minimum cut is correct.
|
||||
//! Each certificate includes:
|
||||
//! - The witnesses that define the cut
|
||||
//! - The LocalKCut responses that prove no smaller cut exists
|
||||
//! - A proof structure for verification
|
||||
|
||||
use crate::graph::{EdgeId, VertexId};
|
||||
use crate::instance::WitnessHandle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub mod audit;
|
||||
|
||||
pub use audit::{AuditData, AuditEntry, AuditEntryType, AuditLogger};
|
||||
|
||||
/// Witness summary for serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WitnessSummary {
|
||||
/// Seed vertex
|
||||
pub seed: u64,
|
||||
/// Boundary size
|
||||
pub boundary: u64,
|
||||
/// Number of vertices in the cut
|
||||
pub cardinality: u64,
|
||||
}
|
||||
|
||||
/// A certificate proving a minimum cut value
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CutCertificate {
|
||||
/// The witnesses (candidate cuts) that were maintained (non-serializable)
|
||||
#[serde(skip)]
|
||||
pub witnesses: Vec<WitnessHandle>,
|
||||
/// Witness summaries for serialization
|
||||
pub witness_summaries: Vec<WitnessSummary>,
|
||||
/// LocalKCut responses that prove no smaller cut exists
|
||||
pub localkcut_responses: Vec<LocalKCutResponse>,
|
||||
/// Index of the best witness (smallest boundary)
|
||||
pub best_witness_idx: Option<usize>,
|
||||
/// Timestamp when certificate was created
|
||||
#[serde(with = "system_time_serde")]
|
||||
pub timestamp: SystemTime,
|
||||
/// Certificate version for compatibility
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
/// Serde serialization for SystemTime
|
||||
mod system_time_serde {
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
|
||||
duration.as_secs().serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let secs = u64::deserialize(deserializer)?;
|
||||
Ok(UNIX_EPOCH + std::time::Duration::from_secs(secs))
|
||||
}
|
||||
}
|
||||
|
||||
/// A response from the LocalKCut oracle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LocalKCutResponse {
|
||||
/// The query that was made
|
||||
pub query: CertLocalKCutQuery,
|
||||
/// The result returned
|
||||
pub result: LocalKCutResultSummary,
|
||||
/// Timestamp of the query
|
||||
pub timestamp: u64,
|
||||
/// Optional provenance (which update triggered this)
|
||||
pub trigger: Option<UpdateTrigger>,
|
||||
}
|
||||
|
||||
impl LocalKCutResponse {
|
||||
/// Create a new LocalKCut response
|
||||
pub fn new(
|
||||
query: CertLocalKCutQuery,
|
||||
result: LocalKCutResultSummary,
|
||||
timestamp: u64,
|
||||
trigger: Option<UpdateTrigger>,
|
||||
) -> Self {
|
||||
Self {
|
||||
query,
|
||||
result,
|
||||
timestamp,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A query to the LocalKCut oracle (certificate version)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CertLocalKCutQuery {
|
||||
/// Seed vertices for the search
|
||||
pub seed_vertices: Vec<VertexId>,
|
||||
/// Budget k for cut size
|
||||
pub budget_k: u64,
|
||||
/// Search radius
|
||||
pub radius: usize,
|
||||
}
|
||||
|
||||
impl CertLocalKCutQuery {
|
||||
/// Create a new LocalKCut query
|
||||
pub fn new(seed_vertices: Vec<VertexId>, budget_k: u64, radius: usize) -> Self {
|
||||
Self {
|
||||
seed_vertices,
|
||||
budget_k,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary of LocalKCut result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum LocalKCutResultSummary {
|
||||
/// Found a cut within the budget
|
||||
Found {
|
||||
/// The cut value found
|
||||
cut_value: u64,
|
||||
/// Hash of the witness for verification
|
||||
witness_hash: u64,
|
||||
},
|
||||
/// No cut found in the local neighborhood
|
||||
NoneInLocality,
|
||||
}
|
||||
|
||||
/// Trigger for an update operation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateTrigger {
|
||||
/// Type of update (insert or delete)
|
||||
pub update_type: UpdateType,
|
||||
/// Edge ID involved
|
||||
pub edge_id: EdgeId,
|
||||
/// Endpoints of the edge
|
||||
pub endpoints: (VertexId, VertexId),
|
||||
/// Timestamp of the update
|
||||
pub time: u64,
|
||||
}
|
||||
|
||||
impl UpdateTrigger {
|
||||
/// Create a new update trigger
|
||||
pub fn new(
|
||||
update_type: UpdateType,
|
||||
edge_id: EdgeId,
|
||||
endpoints: (VertexId, VertexId),
|
||||
time: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
update_type,
|
||||
edge_id,
|
||||
endpoints,
|
||||
time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of graph update
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum UpdateType {
|
||||
/// Edge insertion
|
||||
Insert,
|
||||
/// Edge deletion
|
||||
Delete,
|
||||
}
|
||||
|
||||
/// Current certificate version
|
||||
pub const CERTIFICATE_VERSION: u32 = 1;
|
||||
|
||||
impl CutCertificate {
|
||||
/// Create a new empty certificate
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
witnesses: Vec::new(),
|
||||
witness_summaries: Vec::new(),
|
||||
localkcut_responses: Vec::new(),
|
||||
best_witness_idx: None,
|
||||
timestamp: SystemTime::now(),
|
||||
version: CERTIFICATE_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a certificate with initial witnesses
|
||||
pub fn with_witnesses(witnesses: Vec<WitnessHandle>) -> Self {
|
||||
let mut cert = Self::new();
|
||||
let summaries: Vec<WitnessSummary> = witnesses
|
||||
.iter()
|
||||
.map(|w| WitnessSummary {
|
||||
seed: w.seed(),
|
||||
boundary: w.boundary_size(),
|
||||
cardinality: w.cardinality(),
|
||||
})
|
||||
.collect();
|
||||
cert.witnesses = witnesses;
|
||||
cert.witness_summaries = summaries;
|
||||
cert
|
||||
}
|
||||
|
||||
/// Add a LocalKCut response to the certificate
|
||||
pub fn add_response(&mut self, response: LocalKCutResponse) {
|
||||
self.localkcut_responses.push(response);
|
||||
}
|
||||
|
||||
/// Update the best witness
|
||||
pub fn set_best_witness(&mut self, idx: usize, witness: WitnessHandle) {
|
||||
let summary = WitnessSummary {
|
||||
seed: witness.seed(),
|
||||
boundary: witness.boundary_size(),
|
||||
cardinality: witness.cardinality(),
|
||||
};
|
||||
|
||||
if idx < self.witnesses.len() {
|
||||
self.witnesses[idx] = witness;
|
||||
self.witness_summaries[idx] = summary;
|
||||
self.best_witness_idx = Some(idx);
|
||||
} else {
|
||||
self.witnesses.push(witness);
|
||||
self.witness_summaries.push(summary);
|
||||
self.best_witness_idx = Some(self.witnesses.len() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the certificate is internally consistent
|
||||
pub fn verify(&self) -> Result<(), CertificateError> {
|
||||
// Check version compatibility
|
||||
if self.version > CERTIFICATE_VERSION {
|
||||
return Err(CertificateError::IncompatibleVersion {
|
||||
found: self.version,
|
||||
expected: CERTIFICATE_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we have at least one witness summary (for deserialized certs)
|
||||
// or witness (for in-memory certs)
|
||||
if self.witnesses.is_empty() && self.witness_summaries.is_empty() {
|
||||
return Err(CertificateError::NoWitness);
|
||||
}
|
||||
|
||||
// Verify best witness index is valid
|
||||
if let Some(idx) = self.best_witness_idx {
|
||||
let max_idx = self.witnesses.len().max(self.witness_summaries.len());
|
||||
if max_idx > 0 && idx >= max_idx {
|
||||
return Err(CertificateError::InvalidWitnessIndex {
|
||||
index: idx,
|
||||
max: max_idx - 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Verify consistency of LocalKCut responses
|
||||
for response in &self.localkcut_responses {
|
||||
if response.query.budget_k == 0 {
|
||||
return Err(CertificateError::InvalidQuery {
|
||||
reason: "Budget k must be positive".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the certified minimum cut value
|
||||
pub fn certified_value(&self) -> Option<u64> {
|
||||
self.best_witness_idx
|
||||
.and_then(|idx| self.witnesses.get(idx).map(|w| w.boundary_size()))
|
||||
}
|
||||
|
||||
/// Get the best witness
|
||||
pub fn best_witness(&self) -> Option<&WitnessHandle> {
|
||||
self.best_witness_idx
|
||||
.and_then(|idx| self.witnesses.get(idx))
|
||||
}
|
||||
|
||||
/// Export to JSON for external verification
|
||||
pub fn to_json(&self) -> Result<String, CertificateError> {
|
||||
serde_json::to_string_pretty(self)
|
||||
.map_err(|e| CertificateError::SerializationError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Import from JSON
|
||||
pub fn from_json(json: &str) -> Result<Self, CertificateError> {
|
||||
serde_json::from_str(json)
|
||||
.map_err(|e| CertificateError::DeserializationError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Get number of witnesses
|
||||
pub fn num_witnesses(&self) -> usize {
|
||||
self.witnesses.len()
|
||||
}
|
||||
|
||||
/// Get number of LocalKCut responses
|
||||
pub fn num_responses(&self) -> usize {
|
||||
self.localkcut_responses.len()
|
||||
}
|
||||
|
||||
/// Get all witnesses
|
||||
pub fn witnesses(&self) -> &[WitnessHandle] {
|
||||
&self.witnesses
|
||||
}
|
||||
|
||||
/// Get all LocalKCut responses
|
||||
pub fn responses(&self) -> &[LocalKCutResponse] {
|
||||
&self.localkcut_responses
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CutCertificate {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur during certificate operations
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CertificateError {
|
||||
/// No witness available in certificate
|
||||
NoWitness,
|
||||
/// Inconsistent boundary calculation
|
||||
InconsistentBoundary {
|
||||
/// Expected boundary value
|
||||
expected: u64,
|
||||
/// Actual boundary value
|
||||
actual: u64,
|
||||
},
|
||||
/// Missing LocalKCut proof for a required operation
|
||||
MissingLocalKCutProof {
|
||||
/// Description of missing proof
|
||||
operation: String,
|
||||
},
|
||||
/// Invalid witness index
|
||||
InvalidWitnessIndex {
|
||||
/// The invalid index
|
||||
index: usize,
|
||||
/// Maximum valid index
|
||||
max: usize,
|
||||
},
|
||||
/// Invalid query parameters
|
||||
InvalidQuery {
|
||||
/// Reason for invalidity
|
||||
reason: String,
|
||||
},
|
||||
/// Incompatible certificate version
|
||||
IncompatibleVersion {
|
||||
/// Version found in certificate
|
||||
found: u32,
|
||||
/// Expected version
|
||||
expected: u32,
|
||||
},
|
||||
/// Serialization error
|
||||
SerializationError(String),
|
||||
/// Deserialization error
|
||||
DeserializationError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CertificateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoWitness => write!(f, "No witness available in certificate"),
|
||||
Self::InconsistentBoundary { expected, actual } => {
|
||||
write!(
|
||||
f,
|
||||
"Inconsistent boundary: expected {}, got {}",
|
||||
expected, actual
|
||||
)
|
||||
}
|
||||
Self::MissingLocalKCutProof { operation } => {
|
||||
write!(f, "Missing LocalKCut proof for operation: {}", operation)
|
||||
}
|
||||
Self::InvalidWitnessIndex { index, max } => {
|
||||
write!(f, "Invalid witness index {} (max: {})", index, max)
|
||||
}
|
||||
Self::InvalidQuery { reason } => {
|
||||
write!(f, "Invalid query: {}", reason)
|
||||
}
|
||||
Self::IncompatibleVersion { found, expected } => {
|
||||
write!(
|
||||
f,
|
||||
"Incompatible version: found {}, expected {}",
|
||||
found, expected
|
||||
)
|
||||
}
|
||||
Self::SerializationError(msg) => {
|
||||
write!(f, "Serialization error: {}", msg)
|
||||
}
|
||||
Self::DeserializationError(msg) => {
|
||||
write!(f, "Deserialization error: {}", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CertificateError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
#[test]
|
||||
fn test_new_certificate() {
|
||||
let cert = CutCertificate::new();
|
||||
assert_eq!(cert.num_witnesses(), 0);
|
||||
assert_eq!(cert.num_responses(), 0);
|
||||
assert_eq!(cert.version, CERTIFICATE_VERSION);
|
||||
assert!(cert.best_witness_idx.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_witness() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
|
||||
cert.set_best_witness(0, witness.clone());
|
||||
assert_eq!(cert.num_witnesses(), 1);
|
||||
assert_eq!(cert.best_witness_idx, Some(0));
|
||||
assert_eq!(cert.certified_value(), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_response() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let query = CertLocalKCutQuery::new(vec![1, 2], 10, 5);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 5,
|
||||
witness_hash: 12345,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
|
||||
cert.add_response(response);
|
||||
assert_eq!(cert.num_responses(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_empty() {
|
||||
let cert = CutCertificate::new();
|
||||
assert!(matches!(cert.verify(), Err(CertificateError::NoWitness)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_valid() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 3);
|
||||
cert.set_best_witness(0, witness);
|
||||
|
||||
assert!(cert.verify().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_invalid_index() {
|
||||
let mut cert = CutCertificate::new();
|
||||
// Add a witness so the certificate is not empty
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 5);
|
||||
cert.set_best_witness(0, witness);
|
||||
// Now set an invalid index
|
||||
cert.best_witness_idx = Some(5);
|
||||
|
||||
let result = cert.verify();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(CertificateError::InvalidWitnessIndex { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_roundtrip() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
cert.set_best_witness(0, witness);
|
||||
|
||||
let query = CertLocalKCutQuery::new(vec![1], 5, 2);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 3,
|
||||
witness_hash: 999,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
cert.add_response(response);
|
||||
|
||||
let json = cert.to_json().unwrap();
|
||||
let cert2 = CutCertificate::from_json(&json).unwrap();
|
||||
|
||||
// Witnesses are not serialized, only summaries
|
||||
assert_eq!(cert2.witness_summaries.len(), 1);
|
||||
assert_eq!(cert2.num_responses(), 1);
|
||||
assert_eq!(cert2.version, cert.version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_best_witness() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness1 = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 10);
|
||||
let witness2 = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 3, 4]), 5);
|
||||
|
||||
cert.set_best_witness(0, witness1);
|
||||
cert.set_best_witness(1, witness2);
|
||||
|
||||
let best = cert.best_witness().unwrap();
|
||||
assert_eq!(best.boundary_size(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_trigger() {
|
||||
let trigger = UpdateTrigger::new(UpdateType::Insert, 123, (1, 2), 1000);
|
||||
assert_eq!(trigger.update_type, UpdateType::Insert);
|
||||
assert_eq!(trigger.edge_id, 123);
|
||||
assert_eq!(trigger.endpoints, (1, 2));
|
||||
assert_eq!(trigger.time, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_kcut_query() {
|
||||
let query = CertLocalKCutQuery::new(vec![1, 2, 3], 10, 5);
|
||||
assert_eq!(query.seed_vertices.len(), 3);
|
||||
assert_eq!(query.budget_k, 10);
|
||||
assert_eq!(query.radius, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_kcut_response() {
|
||||
let query = CertLocalKCutQuery::new(vec![1], 5, 2);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 3,
|
||||
witness_hash: 999,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 500, None);
|
||||
|
||||
assert_eq!(response.timestamp, 500);
|
||||
assert!(response.trigger.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_error_display() {
|
||||
let err = CertificateError::NoWitness;
|
||||
assert!(err.to_string().contains("No witness"));
|
||||
|
||||
let err = CertificateError::InvalidWitnessIndex { index: 5, max: 3 };
|
||||
assert!(err.to_string().contains("Invalid witness index"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user