Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View 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);
}
}

View 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"));
}
}