Skip to main content

ruvix_vecgraph/
graph_store.rs

1//! Kernel-resident graph store implementation.
2//!
3//! Graph stores are kernel objects containing nodes and edges.
4//! All mutations are proof-gated via the `graph_apply_proved` syscall.
5//!
6//! # Design (from ADR-087 Section 4.3)
7//!
8//! ```rust,ignore
9//! pub struct KernelGraphStore {
10//!     node_region: RegionHandle,       // slab region for graph nodes
11//!     edge_region: RegionHandle,       // slab region for adjacency lists
12//!     witness_region: RegionHandle,    // append-only mutation witness log
13//!     partition_meta: PartitionMeta,   // MinCut partition metadata
14//!     proof_policy: ProofPolicy,
15//! }
16//! ```
17//!
18//! # Supported Mutations
19//!
20//! - AddNode: Add a new node to the graph
21//! - RemoveNode: Remove a node and all its edges
22//! - AddEdge: Add a new edge between two nodes
23//! - RemoveEdge: Remove an edge between two nodes
24//! - UpdateEdgeWeight: Update the weight of an existing edge
25
26use crate::coherence::{CoherenceConfig, CoherenceTracker};
27use crate::proof_policy::{ProofPolicy, ProofVerifier};
28use crate::witness::WitnessLog;
29use crate::Result;
30
31use ruvix_region::backing::MemoryBacking;
32use ruvix_region::slab::{SlabAllocator, SlotHandle};
33use ruvix_types::{
34    CapRights, Capability, GraphHandle, GraphMutation, GraphMutationKind, KernelError,
35    ProofAttestation, ProofToken, RegionHandle,
36};
37
38/// Maximum edges per node (for slab sizing).
39const MAX_EDGES_PER_NODE: usize = 64;
40
41/// Graph node stored in the node slab.
42#[derive(Debug, Clone, Copy)]
43#[repr(C)]
44pub struct GraphNode {
45    /// Node ID.
46    pub id: u64,
47
48    /// Number of outgoing edges.
49    pub edge_count: u32,
50
51    /// Partition ID for MinCut coherence.
52    pub partition_id: u32,
53
54    /// Handle to the edge list slot.
55    pub edge_list_slot: SlotHandle,
56
57    /// Coherence score for this node.
58    pub coherence_score: u16,
59
60    /// Mutation epoch.
61    pub mutation_epoch: u64,
62
63    /// Padding for alignment.
64    _padding: [u8; 6],
65}
66
67impl GraphNode {
68    /// Size of a graph node in bytes.
69    pub const SIZE: usize = core::mem::size_of::<Self>();
70
71    /// Creates a new graph node.
72    #[inline]
73    #[must_use]
74    pub const fn new(id: u64, partition_id: u32) -> Self {
75        Self {
76            id,
77            edge_count: 0,
78            partition_id,
79            edge_list_slot: SlotHandle::invalid(),
80            coherence_score: 10000,
81            mutation_epoch: 0,
82            _padding: [0; 6],
83        }
84    }
85}
86
87/// Edge entry in an adjacency list.
88#[derive(Debug, Clone, Copy)]
89#[repr(C)]
90pub struct EdgeEntry {
91    /// Target node ID.
92    pub target_id: u64,
93
94    /// Edge weight (fixed-point: weight * 10000).
95    pub weight_fp: i32,
96
97    /// Padding for alignment.
98    _padding: [u8; 4],
99}
100
101impl EdgeEntry {
102    /// Size of an edge entry in bytes.
103    pub const SIZE: usize = core::mem::size_of::<Self>();
104
105    /// Creates a new edge entry.
106    #[inline]
107    #[must_use]
108    pub const fn new(target_id: u64, weight_fp: i32) -> Self {
109        Self {
110            target_id,
111            weight_fp,
112            _padding: [0; 4],
113        }
114    }
115
116    /// Returns the weight as a float.
117    #[inline]
118    #[must_use]
119    pub fn weight(&self) -> f32 {
120        self.weight_fp as f32 / 10000.0
121    }
122}
123
124/// Metadata for MinCut graph partitioning.
125#[derive(Debug, Clone, Copy)]
126#[repr(C)]
127pub struct PartitionMeta {
128    /// Number of partitions.
129    pub partition_count: u32,
130
131    /// Edges crossing partition boundaries (cut size).
132    pub cut_size: u32,
133
134    /// Average coherence across partitions.
135    pub average_coherence: u16,
136
137    /// Last partition update epoch.
138    pub last_update_epoch: u64,
139}
140
141impl Default for PartitionMeta {
142    fn default() -> Self {
143        Self {
144            partition_count: 1,
145            cut_size: 0,
146            average_coherence: 10000,
147            last_update_epoch: 0,
148        }
149    }
150}
151
152impl PartitionMeta {
153    /// Creates new partition metadata.
154    #[inline]
155    #[must_use]
156    pub const fn new(partition_count: u32) -> Self {
157        Self {
158            partition_count,
159            cut_size: 0,
160            average_coherence: 10000,
161            last_update_epoch: 0,
162        }
163    }
164}
165
166/// Result of a graph mutation.
167#[derive(Debug, Clone, PartialEq)]
168pub struct GraphMutationResult {
169    /// The attestation for this mutation.
170    pub attestation: ProofAttestation,
171
172    /// Whether the partition structure changed.
173    pub partition_changed: bool,
174
175    /// The affected partition IDs.
176    pub affected_partitions: [u32; 2],
177}
178
179/// Node-to-slot mapping for graph lookup.
180struct NodeMap {
181    entries: [(u64, SlotHandle); 256],
182    count: usize,
183}
184
185impl NodeMap {
186    fn new() -> Self {
187        Self {
188            entries: [(0, SlotHandle::invalid()); 256],
189            count: 0,
190        }
191    }
192
193    fn get(&self, node_id: u64) -> Option<SlotHandle> {
194        for i in 0..self.count {
195            if self.entries[i].0 == node_id {
196                return Some(self.entries[i].1);
197            }
198        }
199        None
200    }
201
202    fn insert(&mut self, node_id: u64, handle: SlotHandle) -> Result<()> {
203        // Check for existing
204        for i in 0..self.count {
205            if self.entries[i].0 == node_id {
206                self.entries[i].1 = handle;
207                return Ok(());
208            }
209        }
210
211        if self.count >= 256 {
212            return Err(KernelError::LimitExceeded);
213        }
214
215        self.entries[self.count] = (node_id, handle);
216        self.count += 1;
217        Ok(())
218    }
219
220    fn remove(&mut self, node_id: u64) -> Option<SlotHandle> {
221        for i in 0..self.count {
222            if self.entries[i].0 == node_id {
223                let handle = self.entries[i].1;
224                self.entries[i] = self.entries[self.count - 1];
225                self.entries[self.count - 1] = (0, SlotHandle::invalid());
226                self.count -= 1;
227                return Some(handle);
228            }
229        }
230        None
231    }
232
233    fn len(&self) -> usize {
234        self.count
235    }
236}
237
238/// Builder for creating kernel graph stores.
239pub struct GraphStoreBuilder {
240    capacity: u32,
241    coherence_config: CoherenceConfig,
242    proof_policy: ProofPolicy,
243    partition_count: u32,
244}
245
246impl GraphStoreBuilder {
247    /// Creates a new graph store builder.
248    #[inline]
249    #[must_use]
250    pub fn new(capacity: u32) -> Self {
251        Self {
252            capacity,
253            coherence_config: CoherenceConfig::default(),
254            proof_policy: ProofPolicy::standard(),
255            partition_count: 1,
256        }
257    }
258
259    /// Sets the coherence configuration.
260    #[inline]
261    #[must_use]
262    pub fn with_coherence_config(mut self, config: CoherenceConfig) -> Self {
263        self.coherence_config = config;
264        self
265    }
266
267    /// Sets the proof policy.
268    #[inline]
269    #[must_use]
270    pub fn with_proof_policy(mut self, policy: ProofPolicy) -> Self {
271        self.proof_policy = policy;
272        self
273    }
274
275    /// Sets the initial partition count.
276    #[inline]
277    #[must_use]
278    pub fn with_partitions(mut self, count: u32) -> Self {
279        self.partition_count = count;
280        self
281    }
282
283    /// Builds the kernel graph store.
284    pub fn build<B: MemoryBacking>(
285        self,
286        node_backing: B,
287        edge_backing: B,
288        witness_backing: B,
289        node_handle: RegionHandle,
290        edge_handle: RegionHandle,
291        witness_handle: RegionHandle,
292        store_id: u32,
293    ) -> Result<KernelGraphStore<B>> {
294        KernelGraphStore::new(
295            node_backing,
296            edge_backing,
297            witness_backing,
298            node_handle,
299            edge_handle,
300            witness_handle,
301            self.capacity,
302            self.coherence_config,
303            self.proof_policy,
304            self.partition_count,
305            store_id,
306        )
307    }
308}
309
310/// Kernel-resident graph store.
311///
312/// Implements the `graph_apply_proved` syscall interface.
313pub struct KernelGraphStore<B: MemoryBacking> {
314    /// Slab region for graph nodes.
315    node_slab: SlabAllocator<B>,
316
317    /// Slab region for edge lists.
318    edge_slab: SlabAllocator<B>,
319
320    /// Append-only witness log.
321    witness_log: WitnessLog<B>,
322
323    /// Region handles for capability checking.
324    node_handle: RegionHandle,
325    edge_handle: RegionHandle,
326
327    /// Node ID to slot mapping.
328    node_map: NodeMap,
329
330    /// Coherence tracker.
331    coherence_tracker: CoherenceTracker,
332
333    /// Proof verifier.
334    proof_verifier: ProofVerifier,
335
336    /// Partition metadata.
337    partition_meta: PartitionMeta,
338
339    /// Maximum capacity.
340    capacity: u32,
341
342    /// Store identifier.
343    store_id: u32,
344
345    /// Store handle.
346    handle: GraphHandle,
347
348    /// Total edge count.
349    edge_count: u32,
350}
351
352impl<B: MemoryBacking> KernelGraphStore<B> {
353    /// Creates a new kernel graph store.
354    #[allow(clippy::too_many_arguments)]
355    pub fn new(
356        node_backing: B,
357        edge_backing: B,
358        witness_backing: B,
359        node_handle: RegionHandle,
360        edge_handle: RegionHandle,
361        witness_handle: RegionHandle,
362        capacity: u32,
363        coherence_config: CoherenceConfig,
364        proof_policy: ProofPolicy,
365        partition_count: u32,
366        store_id: u32,
367    ) -> Result<Self> {
368        let node_slab = SlabAllocator::new(node_backing, GraphNode::SIZE, capacity as usize)?;
369
370        // Edge list slot size: header (4 bytes count) + MAX_EDGES_PER_NODE edges
371        let edge_slot_size = 4 + MAX_EDGES_PER_NODE * EdgeEntry::SIZE;
372        let edge_slab = SlabAllocator::new(edge_backing, edge_slot_size, capacity as usize)?;
373
374        let witness_log = WitnessLog::new(
375            witness_backing,
376            capacity as usize * 4, // Allow multiple mutations per node
377            witness_handle,
378            store_id,
379        )?;
380
381        Ok(Self {
382            node_slab,
383            edge_slab,
384            witness_log,
385            node_handle,
386            edge_handle,
387            node_map: NodeMap::new(),
388            coherence_tracker: CoherenceTracker::new(coherence_config),
389            proof_verifier: ProofVerifier::new(proof_policy),
390            partition_meta: PartitionMeta::new(partition_count),
391            capacity,
392            store_id,
393            handle: GraphHandle::new(store_id, 0),
394            edge_count: 0,
395        })
396    }
397
398    /// Returns the store handle.
399    #[inline]
400    #[must_use]
401    pub const fn handle(&self) -> GraphHandle {
402        self.handle
403    }
404
405    /// Returns the number of nodes.
406    #[inline]
407    #[must_use]
408    pub fn node_count(&self) -> usize {
409        self.node_map.len()
410    }
411
412    /// Returns the number of edges.
413    #[inline]
414    #[must_use]
415    pub const fn edge_count(&self) -> u32 {
416        self.edge_count
417    }
418
419    /// Returns the capacity.
420    #[inline]
421    #[must_use]
422    pub const fn capacity(&self) -> u32 {
423        self.capacity
424    }
425
426    /// Returns the partition metadata.
427    #[inline]
428    #[must_use]
429    pub const fn partition_meta(&self) -> &PartitionMeta {
430        &self.partition_meta
431    }
432
433    /// Implements the `graph_apply_proved` syscall.
434    ///
435    /// Applies a graph mutation with proof verification.
436    /// Requires PROVE right on the capability.
437    ///
438    /// # Arguments
439    ///
440    /// * `mutation` - The graph mutation to apply
441    /// * `proof` - The proof token authorizing the mutation
442    /// * `capability` - Capability authorizing the mutation
443    /// * `current_time_ns` - Current time for proof verification
444    ///
445    /// # Returns
446    ///
447    /// A mutation result including the attestation.
448    pub fn graph_apply_proved(
449        &mut self,
450        mutation: &GraphMutation,
451        proof: &ProofToken,
452        capability: &Capability,
453        current_time_ns: u64,
454    ) -> Result<GraphMutationResult> {
455        // Compute mutation hash
456        let mutation_hash = compute_graph_mutation_hash(mutation);
457
458        // Verify proof
459        let attestation =
460            self.proof_verifier
461                .verify(proof, &mutation_hash, current_time_ns, capability)?;
462
463        // Apply mutation based on kind
464        let (partition_changed, affected_partitions) = match mutation.kind {
465            GraphMutationKind::AddNode => self.apply_add_node(mutation.node_a)?,
466            GraphMutationKind::RemoveNode => self.apply_remove_node(mutation.node_a)?,
467            GraphMutationKind::AddEdge => {
468                self.apply_add_edge(mutation.node_a, mutation.node_b, mutation.weight_fp)?
469            }
470            GraphMutationKind::RemoveEdge => {
471                self.apply_remove_edge(mutation.node_a, mutation.node_b)?
472            }
473            GraphMutationKind::UpdateEdgeWeight => {
474                self.apply_update_edge_weight(mutation.node_a, mutation.node_b, mutation.weight_fp)?
475            }
476            GraphMutationKind::UpdateNodeMeta => {
477                // Update node metadata (e.g., partition hint)
478                self.apply_update_node_meta(mutation.node_a, mutation.partition_hint)?
479            }
480        };
481
482        // Record in witness log
483        self.witness_log
484            .record_graph_mutation(mutation, attestation, current_time_ns)?;
485
486        Ok(GraphMutationResult {
487            attestation,
488            partition_changed,
489            affected_partitions,
490        })
491    }
492
493    /// Applies an AddNode mutation.
494    fn apply_add_node(&mut self, node_id: u64) -> Result<(bool, [u32; 2])> {
495        // Check if node already exists
496        if self.node_map.get(node_id).is_some() {
497            return Err(KernelError::AlreadyExists);
498        }
499
500        // Allocate node slot
501        let node_slot = self.node_slab.alloc()?;
502
503        // Allocate edge list slot
504        let edge_slot = self.edge_slab.alloc()?;
505
506        // Create node
507        let mut node = GraphNode::new(node_id, 0);
508        node.edge_list_slot = edge_slot;
509        node.mutation_epoch = self.coherence_tracker.advance_epoch();
510
511        // Write node
512        self.write_node(node_slot, &node)?;
513
514        // Initialize edge list (empty)
515        self.write_edge_count(edge_slot, 0)?;
516
517        // Update map
518        self.node_map.insert(node_id, node_slot)?;
519
520        // Track coherence
521        self.coherence_tracker.on_entry_added(node.coherence_score);
522
523        Ok((false, [0, 0]))
524    }
525
526    /// Applies a RemoveNode mutation.
527    fn apply_remove_node(&mut self, node_id: u64) -> Result<(bool, [u32; 2])> {
528        // Find node
529        let node_slot = self.node_map.get(node_id).ok_or(KernelError::NotFound)?;
530
531        // Read node
532        let node = self.read_node(node_slot)?;
533
534        // Free edge list
535        if !node.edge_list_slot.is_invalid() {
536            // Count edges being removed
537            let edge_count = self.read_edge_count(node.edge_list_slot)?;
538            self.edge_count = self.edge_count.saturating_sub(edge_count);
539
540            self.edge_slab.free(node.edge_list_slot)?;
541        }
542
543        // Free node
544        self.node_slab.free(node_slot)?;
545
546        // Update map
547        self.node_map.remove(node_id);
548
549        // Track coherence
550        self.coherence_tracker
551            .on_entry_removed(node.coherence_score);
552
553        Ok((true, [node.partition_id, 0]))
554    }
555
556    /// Applies an AddEdge mutation.
557    fn apply_add_edge(
558        &mut self,
559        from_id: u64,
560        to_id: u64,
561        weight_fp: i32,
562    ) -> Result<(bool, [u32; 2])> {
563        // Find source node
564        let from_slot = self.node_map.get(from_id).ok_or(KernelError::NotFound)?;
565
566        // Verify target exists
567        if self.node_map.get(to_id).is_none() {
568            return Err(KernelError::NotFound);
569        }
570
571        // Read source node
572        let mut node = self.read_node(from_slot)?;
573
574        // Read edge list
575        let edge_count = self.read_edge_count(node.edge_list_slot)?;
576
577        if edge_count as usize >= MAX_EDGES_PER_NODE {
578            return Err(KernelError::LimitExceeded);
579        }
580
581        // Check for duplicate edge
582        for i in 0..edge_count {
583            let edge = self.read_edge(node.edge_list_slot, i)?;
584            if edge.target_id == to_id {
585                return Err(KernelError::AlreadyExists);
586            }
587        }
588
589        // Add edge
590        let edge = EdgeEntry::new(to_id, weight_fp);
591        self.write_edge(node.edge_list_slot, edge_count, &edge)?;
592        self.write_edge_count(node.edge_list_slot, edge_count + 1)?;
593
594        // Update node
595        node.edge_count = edge_count + 1;
596        node.mutation_epoch = self.coherence_tracker.advance_epoch();
597        self.write_node(from_slot, &node)?;
598
599        self.edge_count += 1;
600
601        // Check if edge crosses partitions
602        let to_slot = self.node_map.get(to_id).unwrap();
603        let to_node = self.read_node(to_slot)?;
604
605        let partition_changed = node.partition_id != to_node.partition_id;
606        if partition_changed {
607            self.partition_meta.cut_size += 1;
608        }
609
610        Ok((partition_changed, [node.partition_id, to_node.partition_id]))
611    }
612
613    /// Applies a RemoveEdge mutation.
614    fn apply_remove_edge(&mut self, from_id: u64, to_id: u64) -> Result<(bool, [u32; 2])> {
615        // Find source node
616        let from_slot = self.node_map.get(from_id).ok_or(KernelError::NotFound)?;
617
618        // Read source node
619        let mut node = self.read_node(from_slot)?;
620
621        // Find and remove edge
622        let edge_count = self.read_edge_count(node.edge_list_slot)?;
623        let mut found_idx = None;
624
625        for i in 0..edge_count {
626            let edge = self.read_edge(node.edge_list_slot, i)?;
627            if edge.target_id == to_id {
628                found_idx = Some(i);
629                break;
630            }
631        }
632
633        let idx = found_idx.ok_or(KernelError::NotFound)?;
634
635        // Swap with last and decrement count
636        if idx < edge_count - 1 {
637            let last_edge = self.read_edge(node.edge_list_slot, edge_count - 1)?;
638            self.write_edge(node.edge_list_slot, idx, &last_edge)?;
639        }
640        self.write_edge_count(node.edge_list_slot, edge_count - 1)?;
641
642        // Update node
643        node.edge_count = edge_count - 1;
644        node.mutation_epoch = self.coherence_tracker.advance_epoch();
645        self.write_node(from_slot, &node)?;
646
647        self.edge_count = self.edge_count.saturating_sub(1);
648
649        Ok((false, [node.partition_id, 0]))
650    }
651
652    /// Applies an UpdateEdgeWeight mutation.
653    fn apply_update_edge_weight(
654        &mut self,
655        from_id: u64,
656        to_id: u64,
657        weight_fp: i32,
658    ) -> Result<(bool, [u32; 2])> {
659        // Find source node
660        let from_slot = self.node_map.get(from_id).ok_or(KernelError::NotFound)?;
661
662        // Read source node
663        let mut node = self.read_node(from_slot)?;
664
665        // Find edge
666        let edge_count = self.read_edge_count(node.edge_list_slot)?;
667
668        for i in 0..edge_count {
669            let mut edge = self.read_edge(node.edge_list_slot, i)?;
670            if edge.target_id == to_id {
671                edge.weight_fp = weight_fp;
672                self.write_edge(node.edge_list_slot, i, &edge)?;
673
674                // Update node epoch
675                node.mutation_epoch = self.coherence_tracker.advance_epoch();
676                self.write_node(from_slot, &node)?;
677
678                return Ok((false, [node.partition_id, 0]));
679            }
680        }
681
682        Err(KernelError::NotFound)
683    }
684
685    /// Applies an UpdateNodeMeta mutation.
686    fn apply_update_node_meta(
687        &mut self,
688        node_id: u64,
689        new_partition: u32,
690    ) -> Result<(bool, [u32; 2])> {
691        // Find node
692        let node_slot = self.node_map.get(node_id).ok_or(KernelError::NotFound)?;
693
694        // Read node
695        let mut node = self.read_node(node_slot)?;
696        let old_partition = node.partition_id;
697
698        // Update partition
699        node.partition_id = new_partition;
700        node.mutation_epoch = self.coherence_tracker.advance_epoch();
701
702        self.write_node(node_slot, &node)?;
703
704        let partition_changed = old_partition != new_partition;
705        Ok((partition_changed, [old_partition, new_partition]))
706    }
707
708    /// Reads a node from the slab.
709    fn read_node(&self, slot: SlotHandle) -> Result<GraphNode> {
710        let mut bytes = [0u8; GraphNode::SIZE];
711        self.node_slab.read(slot, &mut bytes)?;
712        Ok(unsafe { core::ptr::read(bytes.as_ptr() as *const GraphNode) })
713    }
714
715    /// Writes a node to the slab.
716    fn write_node(&mut self, slot: SlotHandle, node: &GraphNode) -> Result<()> {
717        let bytes =
718            unsafe { core::slice::from_raw_parts(node as *const _ as *const u8, GraphNode::SIZE) };
719        self.node_slab.write(slot, bytes)?;
720        Ok(())
721    }
722
723    /// Reads the edge count from an edge list slot.
724    fn read_edge_count(&self, slot: SlotHandle) -> Result<u32> {
725        let mut bytes = [0u8; 4];
726        let ptr = self.edge_slab.slot_ptr(slot)?;
727        unsafe {
728            core::ptr::copy_nonoverlapping(ptr, bytes.as_mut_ptr(), 4);
729        }
730        Ok(u32::from_le_bytes(bytes))
731    }
732
733    /// Writes the edge count to an edge list slot.
734    fn write_edge_count(&mut self, slot: SlotHandle, count: u32) -> Result<()> {
735        let bytes = count.to_le_bytes();
736        let ptr = self.edge_slab.slot_ptr(slot)?;
737        unsafe {
738            core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, 4);
739        }
740        Ok(())
741    }
742
743    /// Reads an edge from an edge list slot.
744    fn read_edge(&self, slot: SlotHandle, index: u32) -> Result<EdgeEntry> {
745        let offset = 4 + (index as usize) * EdgeEntry::SIZE;
746        let ptr = self.edge_slab.slot_ptr(slot)?;
747        let mut bytes = [0u8; EdgeEntry::SIZE];
748        unsafe {
749            core::ptr::copy_nonoverlapping(ptr.add(offset), bytes.as_mut_ptr(), EdgeEntry::SIZE);
750        }
751        Ok(unsafe { core::ptr::read(bytes.as_ptr() as *const EdgeEntry) })
752    }
753
754    /// Writes an edge to an edge list slot.
755    fn write_edge(&mut self, slot: SlotHandle, index: u32, edge: &EdgeEntry) -> Result<()> {
756        let offset = 4 + (index as usize) * EdgeEntry::SIZE;
757        let ptr = self.edge_slab.slot_ptr(slot)?;
758        let bytes = unsafe {
759            core::slice::from_raw_parts(edge as *const _ as *const u8, EdgeEntry::SIZE)
760        };
761        unsafe {
762            core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(offset), EdgeEntry::SIZE);
763        }
764        Ok(())
765    }
766
767    /// Returns the proof policy.
768    #[inline]
769    #[must_use]
770    pub const fn proof_policy(&self) -> &ProofPolicy {
771        self.proof_verifier.policy()
772    }
773
774    /// Returns witness log statistics.
775    #[inline]
776    #[must_use]
777    pub fn witness_entry_count(&self) -> usize {
778        self.witness_log.entry_count()
779    }
780}
781
782/// Computes a hash of a graph mutation for proof verification.
783fn compute_graph_mutation_hash(mutation: &GraphMutation) -> [u8; 32] {
784    let mut hash = [0u8; 32];
785
786    hash[0] = mutation.kind as u8;
787    hash[1..9].copy_from_slice(&mutation.node_a.to_le_bytes());
788    hash[9..17].copy_from_slice(&mutation.node_b.to_le_bytes());
789    hash[17..21].copy_from_slice(&mutation.weight_fp.to_le_bytes());
790    hash[21..25].copy_from_slice(&mutation.partition_hint.to_le_bytes());
791
792    hash
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798    use ruvix_region::backing::StaticBacking;
799    use ruvix_types::{ObjectType, ProofPayload, ProofTier};
800
801    fn create_test_capability() -> Capability {
802        Capability::new(
803            1,
804            ObjectType::GraphStore,
805            CapRights::READ | CapRights::WRITE | CapRights::PROVE,
806            0,
807            1,
808        )
809    }
810
811    fn create_test_proof(mutation: &GraphMutation, valid_until_ns: u64, nonce: u64) -> ProofToken {
812        let mutation_hash = compute_graph_mutation_hash(mutation);
813        ProofToken::new(
814            mutation_hash,
815            ProofTier::Standard,
816            ProofPayload::Hash { hash: mutation_hash },
817            valid_until_ns,
818            nonce,
819        )
820    }
821
822    fn create_test_store() -> KernelGraphStore<StaticBacking<16384>> {
823        let node_backing = StaticBacking::<16384>::new();
824        let edge_backing = StaticBacking::<16384>::new();
825        let witness_backing = StaticBacking::<16384>::new();
826
827        GraphStoreBuilder::new(10) // Small capacity for tests
828            .with_proof_policy(ProofPolicy::reflex())
829            .build(
830                node_backing,
831                edge_backing,
832                witness_backing,
833                RegionHandle::new(1, 0),
834                RegionHandle::new(2, 0),
835                RegionHandle::new(3, 0),
836                1,
837            )
838            .unwrap()
839    }
840
841    #[test]
842    fn test_graph_store_creation() {
843        let store = create_test_store();
844        assert_eq!(store.node_count(), 0);
845        assert_eq!(store.edge_count(), 0);
846        assert_eq!(store.capacity(), 10);
847    }
848
849    #[test]
850    fn test_add_node() {
851        let mut store = create_test_store();
852        let cap = create_test_capability();
853
854        let mutation = GraphMutation::add_node(1);
855        let proof = create_test_proof(&mutation, 1_000_000_000, 1);
856
857        let result = store
858            .graph_apply_proved(&mutation, &proof, &cap, 500_000_000)
859            .unwrap();
860
861        assert!(!result.partition_changed);
862        assert_eq!(store.node_count(), 1);
863    }
864
865    #[test]
866    fn test_add_duplicate_node() {
867        let mut store = create_test_store();
868        let cap = create_test_capability();
869
870        // Add first node
871        let mutation1 = GraphMutation::add_node(1);
872        let proof1 = create_test_proof(&mutation1, 1_000_000_000, 1);
873        store
874            .graph_apply_proved(&mutation1, &proof1, &cap, 500_000_000)
875            .unwrap();
876
877        // Try to add duplicate
878        let mutation2 = GraphMutation::add_node(1);
879        let proof2 = create_test_proof(&mutation2, 1_000_000_000, 2);
880        let result = store.graph_apply_proved(&mutation2, &proof2, &cap, 500_000_001);
881
882        assert_eq!(result, Err(KernelError::AlreadyExists));
883    }
884
885    #[test]
886    fn test_add_edge() {
887        let mut store = create_test_store();
888        let cap = create_test_capability();
889
890        // Add nodes
891        let add1 = GraphMutation::add_node(1);
892        let proof1 = create_test_proof(&add1, 1_000_000_000, 1);
893        store
894            .graph_apply_proved(&add1, &proof1, &cap, 500_000_000)
895            .unwrap();
896
897        let add2 = GraphMutation::add_node(2);
898        let proof2 = create_test_proof(&add2, 1_000_000_000, 2);
899        store
900            .graph_apply_proved(&add2, &proof2, &cap, 500_000_001)
901            .unwrap();
902
903        // Add edge
904        let edge = GraphMutation::add_edge(1, 2, 0.75);
905        let proof3 = create_test_proof(&edge, 1_000_000_000, 3);
906        store
907            .graph_apply_proved(&edge, &proof3, &cap, 500_000_002)
908            .unwrap();
909
910        assert_eq!(store.edge_count(), 1);
911    }
912
913    #[test]
914    fn test_remove_edge() {
915        let mut store = create_test_store();
916        let cap = create_test_capability();
917
918        // Setup: add nodes and edge
919        let add1 = GraphMutation::add_node(1);
920        let add2 = GraphMutation::add_node(2);
921        let edge = GraphMutation::add_edge(1, 2, 0.75);
922
923        store
924            .graph_apply_proved(
925                &add1,
926                &create_test_proof(&add1, 1_000_000_000, 1),
927                &cap,
928                500_000_000,
929            )
930            .unwrap();
931        store
932            .graph_apply_proved(
933                &add2,
934                &create_test_proof(&add2, 1_000_000_000, 2),
935                &cap,
936                500_000_001,
937            )
938            .unwrap();
939        store
940            .graph_apply_proved(
941                &edge,
942                &create_test_proof(&edge, 1_000_000_000, 3),
943                &cap,
944                500_000_002,
945            )
946            .unwrap();
947
948        assert_eq!(store.edge_count(), 1);
949
950        // Remove edge
951        let remove = GraphMutation::remove_edge(1, 2);
952        let proof = create_test_proof(&remove, 1_000_000_000, 4);
953        store
954            .graph_apply_proved(&remove, &proof, &cap, 500_000_003)
955            .unwrap();
956
957        assert_eq!(store.edge_count(), 0);
958    }
959
960    #[test]
961    fn test_update_edge_weight() {
962        let mut store = create_test_store();
963        let cap = create_test_capability();
964
965        // Setup
966        let add1 = GraphMutation::add_node(1);
967        let add2 = GraphMutation::add_node(2);
968        let edge = GraphMutation::add_edge(1, 2, 0.5);
969
970        store
971            .graph_apply_proved(
972                &add1,
973                &create_test_proof(&add1, 1_000_000_000, 1),
974                &cap,
975                500_000_000,
976            )
977            .unwrap();
978        store
979            .graph_apply_proved(
980                &add2,
981                &create_test_proof(&add2, 1_000_000_000, 2),
982                &cap,
983                500_000_001,
984            )
985            .unwrap();
986        store
987            .graph_apply_proved(
988                &edge,
989                &create_test_proof(&edge, 1_000_000_000, 3),
990                &cap,
991                500_000_002,
992            )
993            .unwrap();
994
995        // Update weight
996        let update = GraphMutation::update_edge_weight(1, 2, 0.9);
997        let proof = create_test_proof(&update, 1_000_000_000, 4);
998        store
999            .graph_apply_proved(&update, &proof, &cap, 500_000_003)
1000            .unwrap();
1001
1002        // Edge count unchanged
1003        assert_eq!(store.edge_count(), 1);
1004    }
1005
1006    #[test]
1007    fn test_remove_node() {
1008        let mut store = create_test_store();
1009        let cap = create_test_capability();
1010
1011        // Add node
1012        let add = GraphMutation::add_node(1);
1013        let proof1 = create_test_proof(&add, 1_000_000_000, 1);
1014        store
1015            .graph_apply_proved(&add, &proof1, &cap, 500_000_000)
1016            .unwrap();
1017
1018        assert_eq!(store.node_count(), 1);
1019
1020        // Remove node
1021        let remove = GraphMutation::remove_node(1);
1022        let proof2 = create_test_proof(&remove, 1_000_000_000, 2);
1023        let result = store
1024            .graph_apply_proved(&remove, &proof2, &cap, 500_000_001)
1025            .unwrap();
1026
1027        assert!(result.partition_changed);
1028        assert_eq!(store.node_count(), 0);
1029    }
1030
1031    #[test]
1032    fn test_proof_rejected() {
1033        let mut store = create_test_store();
1034        let cap = create_test_capability();
1035
1036        let mutation = GraphMutation::add_node(1);
1037
1038        // Wrong proof hash
1039        let wrong_proof = ProofToken::new(
1040            [0u8; 32],
1041            ProofTier::Standard,
1042            ProofPayload::Hash { hash: [0u8; 32] },
1043            1_000_000_000,
1044            1,
1045        );
1046
1047        let result = store.graph_apply_proved(&mutation, &wrong_proof, &cap, 500_000_000);
1048
1049        assert_eq!(result, Err(KernelError::ProofRejected));
1050        assert_eq!(store.node_count(), 0);
1051    }
1052
1053    #[test]
1054    fn test_witness_log_recording() {
1055        let mut store = create_test_store();
1056        let cap = create_test_capability();
1057
1058        // Perform multiple mutations
1059        for i in 0..5 {
1060            let mutation = GraphMutation::add_node(i);
1061            let proof = create_test_proof(&mutation, 1_000_000_000, i);
1062            store
1063                .graph_apply_proved(&mutation, &proof, &cap, 500_000_000 + i)
1064                .unwrap();
1065        }
1066
1067        assert_eq!(store.witness_entry_count(), 5);
1068    }
1069}