Skip to main content

ruvix_vecgraph/
proof_policy.rs

1//! Proof policy and verification for kernel vector/graph stores.
2//!
3//! In RuVix, proof-gated mutation (ADR-047) is a kernel invariant.
4//! The kernel physically prevents state mutation without a valid proof token.
5//!
6//! # Verification Steps (from ADR-087 Section 8.2)
7//!
8//! 1. Verify proof token matches mutation hash
9//! 2. Verify token has not expired
10//! 3. Verify nonce has not been used
11//! 4. Verify calling task holds PROVE rights on the object
12//!
13//! If all checks pass: apply mutation, emit attestation to witness log.
14//! If any check fails: return Err(ProofRejected).
15
16use crate::Result;
17use ruvix_types::{
18    CapRights, Capability, KernelError, ProofAttestation, ProofPayload, ProofTier, ProofToken,
19};
20
21/// Policy for proof verification on a store.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[repr(C)]
24pub struct ProofPolicy {
25    /// Required proof tier for mutations.
26    pub required_tier: ProofTier,
27
28    /// Maximum allowed verification time in microseconds.
29    /// Proofs exceeding this are rejected.
30    pub max_verification_time_us: u32,
31
32    /// Maximum proof validity window in nanoseconds.
33    /// Proofs with longer validity are rejected.
34    pub max_validity_window_ns: u64,
35
36    /// Whether to require coherence certificates for Deep tier.
37    pub require_coherence_cert: bool,
38
39    /// Minimum coherence score in proof (for CoherenceCert payloads).
40    pub min_coherence_in_proof: u16,
41}
42
43impl Default for ProofPolicy {
44    fn default() -> Self {
45        Self::standard()
46    }
47}
48
49impl ProofPolicy {
50    /// Creates a Reflex-tier policy (sub-microsecond hash checks).
51    #[inline]
52    #[must_use]
53    pub const fn reflex() -> Self {
54        Self {
55            required_tier: ProofTier::Reflex,
56            max_verification_time_us: 1,
57            max_validity_window_ns: 1_000_000_000, // 1s (relaxed for testing)
58            require_coherence_cert: false,
59            min_coherence_in_proof: 0,
60        }
61    }
62
63    /// Creates a Standard-tier policy (Merkle witness verification).
64    #[inline]
65    #[must_use]
66    pub const fn standard() -> Self {
67        Self {
68            required_tier: ProofTier::Standard,
69            max_verification_time_us: 100,
70            max_validity_window_ns: 1_000_000_000, // 1s
71            require_coherence_cert: false,
72            min_coherence_in_proof: 0,
73        }
74    }
75
76    /// Creates a Deep-tier policy (full coherence verification).
77    #[inline]
78    #[must_use]
79    pub const fn deep() -> Self {
80        Self {
81            required_tier: ProofTier::Deep,
82            max_verification_time_us: 10_000,       // 10ms
83            max_validity_window_ns: 5_000_000_000,  // 5s
84            require_coherence_cert: true,
85            min_coherence_in_proof: 5000, // 0.5
86        }
87    }
88
89    /// Sets the required proof tier.
90    #[inline]
91    #[must_use]
92    pub const fn with_tier(mut self, tier: ProofTier) -> Self {
93        self.required_tier = tier;
94        self
95    }
96
97    /// Sets the maximum verification time.
98    #[inline]
99    #[must_use]
100    pub const fn with_max_verification_time_us(mut self, time_us: u32) -> Self {
101        self.max_verification_time_us = time_us;
102        self
103    }
104
105    /// Sets the maximum validity window.
106    #[inline]
107    #[must_use]
108    pub const fn with_max_validity_ns(mut self, validity_ns: u64) -> Self {
109        self.max_validity_window_ns = validity_ns;
110        self
111    }
112
113    /// Checks if a proof tier satisfies this policy.
114    #[inline]
115    #[must_use]
116    pub const fn tier_satisfies(&self, proof_tier: ProofTier) -> bool {
117        // Higher tiers satisfy lower requirements
118        (proof_tier as u8) >= (self.required_tier as u8)
119    }
120}
121
122/// Tracks used nonces to prevent replay attacks.
123#[derive(Debug)]
124pub struct NonceTracker {
125    /// Ring buffer of recent nonces.
126    /// We only need to track nonces within the validity window.
127    recent_nonces: [u64; 64],
128
129    /// Write position in the ring buffer.
130    write_pos: usize,
131
132    /// Number of nonces stored.
133    count: usize,
134
135    /// Total nonces ever tracked.
136    total_tracked: u64,
137}
138
139impl NonceTracker {
140    /// Creates a new nonce tracker.
141    #[inline]
142    #[must_use]
143    pub const fn new() -> Self {
144        Self {
145            recent_nonces: [0u64; 64],
146            write_pos: 0,
147            count: 0,
148            total_tracked: 0,
149        }
150    }
151
152    /// Checks if a nonce has been used and marks it as used.
153    ///
154    /// Returns `true` if the nonce was new (valid), `false` if it was a replay.
155    pub fn check_and_mark(&mut self, nonce: u64) -> bool {
156        // Check if nonce exists in our ring buffer
157        for i in 0..self.count.min(64) {
158            if self.recent_nonces[i] == nonce {
159                return false; // Replay detected
160            }
161        }
162
163        // Add nonce to ring buffer
164        self.recent_nonces[self.write_pos] = nonce;
165        self.write_pos = (self.write_pos + 1) % 64;
166        if self.count < 64 {
167            self.count += 1;
168        }
169        self.total_tracked = self.total_tracked.wrapping_add(1);
170
171        true
172    }
173
174    /// Returns the number of nonces tracked.
175    #[inline]
176    #[must_use]
177    pub const fn count(&self) -> usize {
178        self.count
179    }
180
181    /// Returns the total nonces ever tracked.
182    #[inline]
183    #[must_use]
184    pub const fn total_tracked(&self) -> u64 {
185        self.total_tracked
186    }
187
188    /// Clears old nonces (should be called periodically).
189    pub fn clear_old(&mut self) {
190        self.recent_nonces = [0u64; 64];
191        self.write_pos = 0;
192        self.count = 0;
193    }
194}
195
196impl Default for NonceTracker {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202/// Verifies proof tokens for store mutations.
203pub struct ProofVerifier {
204    /// The proof policy for this verifier.
205    policy: ProofPolicy,
206
207    /// Nonce tracker to prevent replays.
208    nonce_tracker: NonceTracker,
209
210    /// Verifier version for attestations.
211    verifier_version: u32,
212
213    /// Statistics.
214    #[cfg(feature = "stats")]
215    proofs_verified: u64,
216    #[cfg(feature = "stats")]
217    proofs_rejected: u64,
218}
219
220impl ProofVerifier {
221    /// Creates a new proof verifier with the given policy.
222    #[inline]
223    #[must_use]
224    pub fn new(policy: ProofPolicy) -> Self {
225        Self {
226            policy,
227            nonce_tracker: NonceTracker::new(),
228            verifier_version: 0x00_01_00_00, // 0.1.0
229            #[cfg(feature = "stats")]
230            proofs_verified: 0,
231            #[cfg(feature = "stats")]
232            proofs_rejected: 0,
233        }
234    }
235
236    /// Returns the proof policy.
237    #[inline]
238    #[must_use]
239    pub const fn policy(&self) -> &ProofPolicy {
240        &self.policy
241    }
242
243    /// Verifies a proof token for a mutation.
244    ///
245    /// # Arguments
246    ///
247    /// * `proof` - The proof token to verify
248    /// * `expected_mutation_hash` - Hash of the mutation being authorized
249    /// * `current_time_ns` - Current time in nanoseconds
250    /// * `capability` - The capability being used (must have PROVE right)
251    ///
252    /// # Returns
253    ///
254    /// On success, returns a `ProofAttestation` to be logged.
255    /// On failure, returns `ProofRejected`.
256    pub fn verify(
257        &mut self,
258        proof: &ProofToken,
259        expected_mutation_hash: &[u8; 32],
260        current_time_ns: u64,
261        capability: &Capability,
262    ) -> Result<ProofAttestation> {
263        // Step 1: Check capability has PROVE right
264        if !capability.has_rights(CapRights::PROVE) {
265            #[cfg(feature = "stats")]
266            {
267                self.proofs_rejected += 1;
268            }
269            return Err(KernelError::InsufficientRights);
270        }
271
272        // Step 2: Verify mutation hash matches
273        if proof.mutation_hash != *expected_mutation_hash {
274            #[cfg(feature = "stats")]
275            {
276                self.proofs_rejected += 1;
277            }
278            return Err(KernelError::ProofRejected);
279        }
280
281        // Step 3: Check proof tier satisfies policy
282        if !self.policy.tier_satisfies(proof.tier) {
283            #[cfg(feature = "stats")]
284            {
285                self.proofs_rejected += 1;
286            }
287            return Err(KernelError::ProofRejected);
288        }
289
290        // Step 4: Check expiry
291        if proof.is_expired(current_time_ns) {
292            #[cfg(feature = "stats")]
293            {
294                self.proofs_rejected += 1;
295            }
296            return Err(KernelError::ProofRejected);
297        }
298
299        // Step 5: Check validity window is not too large
300        let validity_window = proof.valid_until_ns.saturating_sub(current_time_ns);
301        if validity_window > self.policy.max_validity_window_ns {
302            #[cfg(feature = "stats")]
303            {
304                self.proofs_rejected += 1;
305            }
306            return Err(KernelError::ProofRejected);
307        }
308
309        // Step 6: Check nonce is not reused
310        if !self.nonce_tracker.check_and_mark(proof.nonce) {
311            #[cfg(feature = "stats")]
312            {
313                self.proofs_rejected += 1;
314            }
315            return Err(KernelError::ProofRejected);
316        }
317
318        // Step 7: Verify payload according to tier
319        self.verify_payload(&proof.payload)?;
320
321        // All checks passed - create attestation
322        #[cfg(feature = "stats")]
323        {
324            self.proofs_verified += 1;
325        }
326
327        Ok(self.create_attestation(proof, current_time_ns))
328    }
329
330    /// Verifies the proof payload according to its type.
331    fn verify_payload(&self, payload: &ProofPayload) -> Result<()> {
332        match payload {
333            ProofPayload::Hash { hash: _ } => {
334                // Reflex tier: just check the hash is non-zero
335                // Actual hash verification is done by comparing mutation_hash
336                Ok(())
337            }
338            ProofPayload::MerkleWitness {
339                root: _,
340                leaf_index: _,
341                path_len,
342                path: _,
343            } => {
344                // Standard tier: verify path length is reasonable
345                if *path_len > 32 {
346                    return Err(KernelError::ProofRejected);
347                }
348                // Full Merkle verification would go here
349                // For now, we trust the proof engine that generated this
350                Ok(())
351            }
352            ProofPayload::CoherenceCert {
353                score_before: _,
354                score_after,
355                partition_id: _,
356                signature: _,
357            } => {
358                // Deep tier: check coherence requirements
359                if self.policy.require_coherence_cert
360                    && *score_after < self.policy.min_coherence_in_proof
361                {
362                    return Err(KernelError::CoherenceViolation);
363                }
364                // Full signature verification would go here
365                Ok(())
366            }
367        }
368    }
369
370    /// Creates a proof attestation for a verified proof.
371    fn create_attestation(&self, proof: &ProofToken, current_time_ns: u64) -> ProofAttestation {
372        // Environment hash would include: policy, capability, mutation type
373        // For now, use a placeholder
374        let environment_hash = [0u8; 32];
375
376        ProofAttestation::new(
377            proof.mutation_hash,
378            environment_hash,
379            current_time_ns,
380            self.verifier_version,
381            1, // reduction_steps
382            0, // cache_hit_rate
383        )
384    }
385
386    /// Returns the verifier version.
387    #[inline]
388    #[must_use]
389    pub const fn verifier_version(&self) -> u32 {
390        self.verifier_version
391    }
392
393    /// Returns the nonce tracker.
394    #[inline]
395    #[must_use]
396    pub const fn nonce_tracker(&self) -> &NonceTracker {
397        &self.nonce_tracker
398    }
399}
400
401impl Default for ProofVerifier {
402    fn default() -> Self {
403        Self::new(ProofPolicy::default())
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use ruvix_types::ObjectType;
411
412    fn create_test_capability() -> Capability {
413        Capability::new(
414            1,
415            ObjectType::VectorStore,
416            CapRights::READ | CapRights::WRITE | CapRights::PROVE,
417            0,
418            1,
419        )
420    }
421
422    fn create_test_proof(
423        mutation_hash: [u8; 32],
424        tier: ProofTier,
425        valid_until_ns: u64,
426        nonce: u64,
427    ) -> ProofToken {
428        ProofToken::new(
429            mutation_hash,
430            tier,
431            ProofPayload::Hash { hash: mutation_hash },
432            valid_until_ns,
433            nonce,
434        )
435    }
436
437    #[test]
438    fn test_proof_policy_tiers() {
439        let reflex = ProofPolicy::reflex();
440        assert_eq!(reflex.required_tier, ProofTier::Reflex);
441        assert!(reflex.tier_satisfies(ProofTier::Reflex));
442        assert!(reflex.tier_satisfies(ProofTier::Standard));
443        assert!(reflex.tier_satisfies(ProofTier::Deep));
444
445        let deep = ProofPolicy::deep();
446        assert!(!deep.tier_satisfies(ProofTier::Reflex));
447        assert!(!deep.tier_satisfies(ProofTier::Standard));
448        assert!(deep.tier_satisfies(ProofTier::Deep));
449    }
450
451    #[test]
452    fn test_nonce_tracker_replay() {
453        let mut tracker = NonceTracker::new();
454
455        assert!(tracker.check_and_mark(1));
456        assert!(tracker.check_and_mark(2));
457        assert!(tracker.check_and_mark(3));
458
459        // Replay should fail
460        assert!(!tracker.check_and_mark(1));
461        assert!(!tracker.check_and_mark(2));
462
463        // New nonces should work
464        assert!(tracker.check_and_mark(4));
465    }
466
467    #[test]
468    fn test_proof_verifier_success() {
469        let mut verifier = ProofVerifier::new(ProofPolicy::reflex());
470        let cap = create_test_capability();
471
472        let mutation_hash = [1u8; 32];
473        let proof = create_test_proof(mutation_hash, ProofTier::Standard, 1_000_000_000, 1);
474
475        let result = verifier.verify(&proof, &mutation_hash, 500_000_000, &cap);
476        assert!(result.is_ok());
477    }
478
479    #[test]
480    fn test_proof_verifier_wrong_hash() {
481        let mut verifier = ProofVerifier::new(ProofPolicy::reflex());
482        let cap = create_test_capability();
483
484        let mutation_hash = [1u8; 32];
485        let wrong_hash = [2u8; 32];
486        let proof = create_test_proof(mutation_hash, ProofTier::Standard, 1_000_000_000, 1);
487
488        let result = verifier.verify(&proof, &wrong_hash, 500_000_000, &cap);
489        assert_eq!(result, Err(KernelError::ProofRejected));
490    }
491
492    #[test]
493    fn test_proof_verifier_expired() {
494        let mut verifier = ProofVerifier::new(ProofPolicy::reflex());
495        let cap = create_test_capability();
496
497        let mutation_hash = [1u8; 32];
498        let proof = create_test_proof(mutation_hash, ProofTier::Standard, 500_000_000, 1);
499
500        // Current time is after expiry
501        let result = verifier.verify(&proof, &mutation_hash, 1_000_000_000, &cap);
502        assert_eq!(result, Err(KernelError::ProofRejected));
503    }
504
505    #[test]
506    fn test_proof_verifier_nonce_reuse() {
507        let mut verifier = ProofVerifier::new(ProofPolicy::reflex());
508        let cap = create_test_capability();
509
510        let mutation_hash = [1u8; 32];
511        let proof1 = create_test_proof(mutation_hash, ProofTier::Standard, 1_000_000_000, 42);
512        let proof2 = create_test_proof(mutation_hash, ProofTier::Standard, 1_000_000_000, 42);
513
514        // First use should succeed
515        let result1 = verifier.verify(&proof1, &mutation_hash, 500_000_000, &cap);
516        assert!(result1.is_ok());
517
518        // Second use with same nonce should fail
519        let result2 = verifier.verify(&proof2, &mutation_hash, 500_000_001, &cap);
520        assert_eq!(result2, Err(KernelError::ProofRejected));
521    }
522
523    #[test]
524    fn test_proof_verifier_insufficient_rights() {
525        let mut verifier = ProofVerifier::new(ProofPolicy::reflex());
526
527        // Capability without PROVE right
528        let cap = Capability::new(1, ObjectType::VectorStore, CapRights::READ, 0, 1);
529
530        let mutation_hash = [1u8; 32];
531        let proof = create_test_proof(mutation_hash, ProofTier::Standard, 1_000_000_000, 1);
532
533        let result = verifier.verify(&proof, &mutation_hash, 500_000_000, &cap);
534        assert_eq!(result, Err(KernelError::InsufficientRights));
535    }
536
537    #[test]
538    fn test_proof_verifier_tier_mismatch() {
539        let mut verifier = ProofVerifier::new(ProofPolicy::deep());
540        let cap = create_test_capability();
541
542        let mutation_hash = [1u8; 32];
543        // Proof is Standard tier, policy requires Deep
544        let proof = create_test_proof(mutation_hash, ProofTier::Standard, 1_000_000_000, 1);
545
546        let result = verifier.verify(&proof, &mutation_hash, 500_000_000, &cap);
547        assert_eq!(result, Err(KernelError::ProofRejected));
548    }
549}