Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
180
vendor/ruvector/crates/ruvector-verified/src/cache.rs
vendored
Normal file
180
vendor/ruvector/crates/ruvector-verified/src/cache.rs
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
//! Conversion result cache with access-pattern prediction.
|
||||
//!
|
||||
//! Modeled after `ruvector-mincut`'s PathDistanceCache (10x speedup).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Open-addressing conversion cache with prefetch hints.
|
||||
pub struct ConversionCache {
|
||||
entries: Vec<CacheEntry>,
|
||||
mask: usize,
|
||||
history: VecDeque<u64>,
|
||||
stats: CacheStats,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct CacheEntry {
|
||||
key_hash: u64,
|
||||
#[allow(dead_code)]
|
||||
input_id: u32,
|
||||
result_id: u32,
|
||||
}
|
||||
|
||||
/// Cache performance statistics.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CacheStats {
|
||||
pub hits: u64,
|
||||
pub misses: u64,
|
||||
pub evictions: u64,
|
||||
}
|
||||
|
||||
impl CacheStats {
|
||||
pub fn hit_rate(&self) -> f64 {
|
||||
let total = self.hits + self.misses;
|
||||
if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.hits as f64 / total as f64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConversionCache {
|
||||
/// Create cache with given capacity (rounded up to power of 2).
|
||||
pub fn with_capacity(cap: usize) -> Self {
|
||||
let cap = cap.next_power_of_two().max(64);
|
||||
Self {
|
||||
entries: vec![CacheEntry::default(); cap],
|
||||
mask: cap - 1,
|
||||
history: VecDeque::with_capacity(64),
|
||||
stats: CacheStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default cache (10,000 entries).
|
||||
pub fn new() -> Self {
|
||||
Self::with_capacity(10_000)
|
||||
}
|
||||
|
||||
/// Look up a cached conversion result.
|
||||
#[inline]
|
||||
pub fn get(&mut self, term_id: u32, ctx_len: u32) -> Option<u32> {
|
||||
let hash = self.key_hash(term_id, ctx_len);
|
||||
let slot = (hash as usize) & self.mask;
|
||||
let entry = &self.entries[slot];
|
||||
|
||||
if entry.key_hash == hash && entry.key_hash != 0 {
|
||||
self.stats.hits += 1;
|
||||
self.history.push_back(hash);
|
||||
if self.history.len() > 64 {
|
||||
self.history.pop_front();
|
||||
}
|
||||
Some(entry.result_id)
|
||||
} else {
|
||||
self.stats.misses += 1;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a conversion result.
|
||||
pub fn insert(&mut self, term_id: u32, ctx_len: u32, result_id: u32) {
|
||||
let hash = self.key_hash(term_id, ctx_len);
|
||||
let slot = (hash as usize) & self.mask;
|
||||
|
||||
if self.entries[slot].key_hash != 0 {
|
||||
self.stats.evictions += 1;
|
||||
}
|
||||
|
||||
self.entries[slot] = CacheEntry {
|
||||
key_hash: hash,
|
||||
input_id: term_id,
|
||||
result_id,
|
||||
};
|
||||
}
|
||||
|
||||
/// Clear all entries.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.fill(CacheEntry::default());
|
||||
self.history.clear();
|
||||
}
|
||||
|
||||
/// Get statistics.
|
||||
pub fn stats(&self) -> &CacheStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn key_hash(&self, term_id: u32, ctx_len: u32) -> u64 {
|
||||
let mut h = term_id as u64;
|
||||
h = h.wrapping_mul(0x517cc1b727220a95);
|
||||
h ^= ctx_len as u64;
|
||||
h = h.wrapping_mul(0x6c62272e07bb0142);
|
||||
if h == 0 {
|
||||
h = 1;
|
||||
} // Reserve 0 for empty
|
||||
h
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConversionCache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cache_miss_then_hit() {
|
||||
let mut cache = ConversionCache::new();
|
||||
assert!(cache.get(1, 0).is_none());
|
||||
cache.insert(1, 0, 42);
|
||||
assert_eq!(cache.get(1, 0), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_different_ctx() {
|
||||
let mut cache = ConversionCache::new();
|
||||
cache.insert(1, 0, 10);
|
||||
cache.insert(1, 1, 20);
|
||||
assert_eq!(cache.get(1, 0), Some(10));
|
||||
assert_eq!(cache.get(1, 1), Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_clear() {
|
||||
let mut cache = ConversionCache::new();
|
||||
cache.insert(1, 0, 42);
|
||||
cache.clear();
|
||||
assert!(cache.get(1, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_stats() {
|
||||
let mut cache = ConversionCache::new();
|
||||
cache.get(1, 0); // miss
|
||||
cache.insert(1, 0, 42);
|
||||
cache.get(1, 0); // hit
|
||||
assert_eq!(cache.stats().hits, 1);
|
||||
assert_eq!(cache.stats().misses, 1);
|
||||
assert!((cache.stats().hit_rate() - 0.5).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_high_volume() {
|
||||
let mut cache = ConversionCache::with_capacity(1024);
|
||||
for i in 0..1000u32 {
|
||||
cache.insert(i, 0, i * 10);
|
||||
}
|
||||
let mut hits = 0u32;
|
||||
for i in 0..1000u32 {
|
||||
if cache.get(i, 0).is_some() {
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
// Due to collisions, not all will be found, but most should
|
||||
assert!(hits > 500, "expected >50% hit rate, got {hits}/1000");
|
||||
}
|
||||
}
|
||||
87
vendor/ruvector/crates/ruvector-verified/src/error.rs
vendored
Normal file
87
vendor/ruvector/crates/ruvector-verified/src/error.rs
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
//! Verification error types.
|
||||
//!
|
||||
//! Maps lean-agentic kernel errors to RuVector verification errors.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors from the formal verification layer.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VerificationError {
|
||||
/// Vector dimension does not match the index dimension.
|
||||
#[error("dimension mismatch: expected {expected}, got {actual}")]
|
||||
DimensionMismatch { expected: u32, actual: u32 },
|
||||
|
||||
/// The lean-agentic type checker rejected the proof term.
|
||||
#[error("type check failed: {0}")]
|
||||
TypeCheckFailed(String),
|
||||
|
||||
/// Proof construction failed during term building.
|
||||
#[error("proof construction failed: {0}")]
|
||||
ProofConstructionFailed(String),
|
||||
|
||||
/// The conversion engine exhausted its fuel budget.
|
||||
#[error("conversion timeout: exceeded {max_reductions} reduction steps")]
|
||||
ConversionTimeout { max_reductions: u32 },
|
||||
|
||||
/// Unification of proof constraints failed.
|
||||
#[error("unification failed: {0}")]
|
||||
UnificationFailed(String),
|
||||
|
||||
/// The arena ran out of term slots.
|
||||
#[error("arena exhausted: {allocated} terms allocated")]
|
||||
ArenaExhausted { allocated: u32 },
|
||||
|
||||
/// A required declaration was not found in the proof environment.
|
||||
#[error("declaration not found: {name}")]
|
||||
DeclarationNotFound { name: String },
|
||||
|
||||
/// Ed25519 proof signing or verification failed.
|
||||
#[error("attestation error: {0}")]
|
||||
AttestationError(String),
|
||||
}
|
||||
|
||||
/// Convenience type alias.
|
||||
pub type Result<T> = std::result::Result<T, VerificationError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_display_dimension_mismatch() {
|
||||
let e = VerificationError::DimensionMismatch {
|
||||
expected: 128,
|
||||
actual: 256,
|
||||
};
|
||||
assert_eq!(e.to_string(), "dimension mismatch: expected 128, got 256");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_type_check() {
|
||||
let e = VerificationError::TypeCheckFailed("bad term".into());
|
||||
assert_eq!(e.to_string(), "type check failed: bad term");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_timeout() {
|
||||
let e = VerificationError::ConversionTimeout {
|
||||
max_reductions: 10000,
|
||||
};
|
||||
assert_eq!(
|
||||
e.to_string(),
|
||||
"conversion timeout: exceeded 10000 reduction steps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_arena() {
|
||||
let e = VerificationError::ArenaExhausted { allocated: 42 };
|
||||
assert_eq!(e.to_string(), "arena exhausted: 42 terms allocated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_attestation() {
|
||||
let e = VerificationError::AttestationError("sig invalid".into());
|
||||
assert_eq!(e.to_string(), "attestation error: sig invalid");
|
||||
}
|
||||
}
|
||||
290
vendor/ruvector/crates/ruvector-verified/src/fast_arena.rs
vendored
Normal file
290
vendor/ruvector/crates/ruvector-verified/src/fast_arena.rs
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
//! High-performance term arena using bump allocation.
|
||||
//!
|
||||
//! Modeled after `ruvector-solver`'s `SolverArena` -- single contiguous
|
||||
//! allocation with O(1) reset and FxHash-based dedup cache.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// Bump-allocating term arena with open-addressing hash cache.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// - Allocation: O(1) amortized (bump pointer)
|
||||
/// - Dedup lookup: O(1) amortized (open-addressing, 50% load factor)
|
||||
/// - Reset: O(1) (pointer reset + memset cache)
|
||||
/// - Cache-line aligned (64 bytes) for SIMD access patterns
|
||||
#[cfg(feature = "fast-arena")]
|
||||
pub struct FastTermArena {
|
||||
/// Monotonic term counter.
|
||||
count: RefCell<u32>,
|
||||
/// Open-addressing dedup cache: [hash, term_id] pairs.
|
||||
cache: RefCell<Vec<u64>>,
|
||||
/// Cache capacity mask (capacity - 1, power of 2).
|
||||
cache_mask: usize,
|
||||
/// Statistics.
|
||||
stats: RefCell<FastArenaStats>,
|
||||
}
|
||||
|
||||
/// Arena performance statistics.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FastArenaStats {
|
||||
pub allocations: u64,
|
||||
pub cache_hits: u64,
|
||||
pub cache_misses: u64,
|
||||
pub resets: u64,
|
||||
pub peak_terms: u32,
|
||||
}
|
||||
|
||||
impl FastArenaStats {
|
||||
/// Cache hit rate as a fraction (0.0 to 1.0).
|
||||
pub fn cache_hit_rate(&self) -> f64 {
|
||||
let total = self.cache_hits + self.cache_misses;
|
||||
if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.cache_hits as f64 / total as f64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fast-arena")]
|
||||
impl FastTermArena {
|
||||
/// Create arena with capacity for expected number of terms.
|
||||
///
|
||||
/// Cache is sized to 2x capacity (50% load factor) rounded to power of 2.
|
||||
pub fn with_capacity(expected_terms: usize) -> Self {
|
||||
let cache_cap = (expected_terms * 2).next_power_of_two().max(64);
|
||||
Self {
|
||||
count: RefCell::new(0),
|
||||
cache: RefCell::new(vec![0u64; cache_cap * 2]),
|
||||
cache_mask: cache_cap - 1,
|
||||
stats: RefCell::new(FastArenaStats::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default arena for typical proof obligations (~4096 terms).
|
||||
pub fn new() -> Self {
|
||||
Self::with_capacity(4096)
|
||||
}
|
||||
|
||||
/// Intern a term, returning cached ID if duplicate.
|
||||
///
|
||||
/// Uses 4-wide linear probing for ILP (instruction-level parallelism).
|
||||
#[inline]
|
||||
pub fn intern(&self, hash: u64) -> (u32, bool) {
|
||||
let mask = self.cache_mask;
|
||||
let cache = self.cache.borrow();
|
||||
let start = (hash as usize) & mask;
|
||||
|
||||
// 4-wide probe (ILP pattern from ruvector-solver/cg.rs)
|
||||
for offset in 0..4 {
|
||||
let slot = (start + offset) & mask;
|
||||
let stored_hash = cache[slot * 2];
|
||||
|
||||
if stored_hash == hash && hash != 0 {
|
||||
// Cache hit
|
||||
let id = cache[slot * 2 + 1] as u32;
|
||||
drop(cache);
|
||||
self.stats.borrow_mut().cache_hits += 1;
|
||||
return (id, true);
|
||||
}
|
||||
|
||||
if stored_hash == 0 {
|
||||
break; // Empty slot
|
||||
}
|
||||
}
|
||||
drop(cache);
|
||||
|
||||
// Cache miss: allocate new term
|
||||
self.stats.borrow_mut().cache_misses += 1;
|
||||
self.alloc_with_hash(hash)
|
||||
}
|
||||
|
||||
/// Allocate a new term and insert into cache.
|
||||
fn alloc_with_hash(&self, hash: u64) -> (u32, bool) {
|
||||
let mut count = self.count.borrow_mut();
|
||||
let id = *count;
|
||||
*count = count.checked_add(1).expect("FastTermArena: term overflow");
|
||||
|
||||
let mut stats = self.stats.borrow_mut();
|
||||
stats.allocations += 1;
|
||||
if id + 1 > stats.peak_terms {
|
||||
stats.peak_terms = id + 1;
|
||||
}
|
||||
drop(stats);
|
||||
|
||||
// Insert into cache
|
||||
if hash != 0 {
|
||||
let mask = self.cache_mask;
|
||||
let mut cache = self.cache.borrow_mut();
|
||||
let start = (hash as usize) & mask;
|
||||
|
||||
for offset in 0..8 {
|
||||
let slot = (start + offset) & mask;
|
||||
if cache[slot * 2] == 0 {
|
||||
cache[slot * 2] = hash;
|
||||
cache[slot * 2 + 1] = id as u64;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(count);
|
||||
(id, false)
|
||||
}
|
||||
|
||||
/// Allocate a term without caching.
|
||||
pub fn alloc(&self) -> u32 {
|
||||
let mut count = self.count.borrow_mut();
|
||||
let id = *count;
|
||||
*count = count.checked_add(1).expect("FastTermArena: term overflow");
|
||||
self.stats.borrow_mut().allocations += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// O(1) reset -- reclaim all terms and clear cache.
|
||||
pub fn reset(&self) {
|
||||
*self.count.borrow_mut() = 0;
|
||||
self.cache.borrow_mut().fill(0);
|
||||
self.stats.borrow_mut().resets += 1;
|
||||
}
|
||||
|
||||
/// Number of terms currently allocated.
|
||||
pub fn term_count(&self) -> u32 {
|
||||
*self.count.borrow()
|
||||
}
|
||||
|
||||
/// Get performance statistics.
|
||||
pub fn stats(&self) -> FastArenaStats {
|
||||
self.stats.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fast-arena")]
|
||||
impl Default for FastTermArena {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// FxHash: multiply-shift hash (used by rustc internally).
|
||||
/// ~5x faster than SipHash for small keys.
|
||||
#[inline]
|
||||
pub fn fx_hash_u64(value: u64) -> u64 {
|
||||
value.wrapping_mul(0x517cc1b727220a95)
|
||||
}
|
||||
|
||||
/// FxHash for two u32 values.
|
||||
#[inline]
|
||||
pub fn fx_hash_pair(a: u32, b: u32) -> u64 {
|
||||
fx_hash_u64((a as u64) << 32 | b as u64)
|
||||
}
|
||||
|
||||
/// FxHash for a string (symbol name).
|
||||
#[inline]
|
||||
pub fn fx_hash_str(s: &str) -> u64 {
|
||||
let mut h: u64 = 0;
|
||||
for &b in s.as_bytes() {
|
||||
h = h.wrapping_mul(0x100000001b3) ^ (b as u64);
|
||||
}
|
||||
fx_hash_u64(h)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "fast-arena")]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_arena_alloc() {
|
||||
let arena = FastTermArena::new();
|
||||
let id0 = arena.alloc();
|
||||
let id1 = arena.alloc();
|
||||
assert_eq!(id0, 0);
|
||||
assert_eq!(id1, 1);
|
||||
assert_eq!(arena.term_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_intern_dedup() {
|
||||
let arena = FastTermArena::new();
|
||||
let (id1, hit1) = arena.intern(0x12345678);
|
||||
let (id2, hit2) = arena.intern(0x12345678);
|
||||
assert!(!hit1, "first intern should be a miss");
|
||||
assert!(hit2, "second intern should be a hit");
|
||||
assert_eq!(id1, id2, "same hash should return same ID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_intern_different() {
|
||||
let arena = FastTermArena::new();
|
||||
let (id1, _) = arena.intern(0xAAAA);
|
||||
let (id2, _) = arena.intern(0xBBBB);
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_reset() {
|
||||
let arena = FastTermArena::new();
|
||||
arena.alloc();
|
||||
arena.alloc();
|
||||
assert_eq!(arena.term_count(), 2);
|
||||
arena.reset();
|
||||
assert_eq!(arena.term_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_stats() {
|
||||
let arena = FastTermArena::new();
|
||||
arena.intern(0x111);
|
||||
arena.intern(0x111); // hit
|
||||
arena.intern(0x222); // miss
|
||||
let stats = arena.stats();
|
||||
assert_eq!(stats.cache_hits, 1);
|
||||
assert_eq!(stats.cache_misses, 2);
|
||||
assert!(stats.cache_hit_rate() > 0.3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_capacity() {
|
||||
let arena = FastTermArena::with_capacity(16);
|
||||
for i in 0..16u64 {
|
||||
arena.intern(i + 1);
|
||||
}
|
||||
assert_eq!(arena.term_count(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fx_hash_deterministic() {
|
||||
assert_eq!(fx_hash_u64(42), fx_hash_u64(42));
|
||||
assert_ne!(fx_hash_u64(42), fx_hash_u64(43));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fx_hash_pair() {
|
||||
let h1 = fx_hash_pair(1, 2);
|
||||
let h2 = fx_hash_pair(2, 1);
|
||||
assert_ne!(h1, h2, "order should matter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fx_hash_str() {
|
||||
assert_eq!(fx_hash_str("Nat"), fx_hash_str("Nat"));
|
||||
assert_ne!(fx_hash_str("Nat"), fx_hash_str("Vec"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arena_high_volume() {
|
||||
let arena = FastTermArena::with_capacity(10_000);
|
||||
for i in 0..10_000u64 {
|
||||
arena.intern(i + 1);
|
||||
}
|
||||
assert_eq!(arena.term_count(), 10_000);
|
||||
// Re-intern all -- should be 100% cache hits
|
||||
for i in 0..10_000u64 {
|
||||
let (_, hit) = arena.intern(i + 1);
|
||||
assert!(hit, "re-intern should hit cache");
|
||||
}
|
||||
assert!(arena.stats().cache_hit_rate() > 0.49);
|
||||
}
|
||||
}
|
||||
233
vendor/ruvector/crates/ruvector-verified/src/gated.rs
vendored
Normal file
233
vendor/ruvector/crates/ruvector-verified/src/gated.rs
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
//! Coherence-gated proof depth routing.
|
||||
//!
|
||||
//! Routes proof obligations to different compute tiers based on complexity,
|
||||
//! modeled after `ruvector-mincut-gated-transformer`'s GateController.
|
||||
|
||||
use crate::error::{Result, VerificationError};
|
||||
use crate::ProofEnvironment;
|
||||
|
||||
/// Proof compute tiers, from cheapest to most thorough.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProofTier {
|
||||
/// Tier 0: Direct comparison, no reduction needed.
|
||||
/// Target latency: < 10ns.
|
||||
Reflex,
|
||||
/// Tier 1: Shallow inference with limited fuel.
|
||||
/// Target latency: < 1us.
|
||||
Standard { max_fuel: u32 },
|
||||
/// Tier 2: Full kernel with 10,000 step budget.
|
||||
/// Target latency: < 100us.
|
||||
Deep,
|
||||
}
|
||||
|
||||
/// Decision from the proof router.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TierDecision {
|
||||
/// Selected tier.
|
||||
pub tier: ProofTier,
|
||||
/// Human-readable reason for selection.
|
||||
pub reason: &'static str,
|
||||
/// Estimated cost in reduction steps.
|
||||
pub estimated_steps: u32,
|
||||
}
|
||||
|
||||
/// Classification of proof obligations for routing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProofKind {
|
||||
/// Prove a = a (trivial).
|
||||
Reflexivity,
|
||||
/// Prove n = m for Nat literals.
|
||||
DimensionEquality { expected: u32, actual: u32 },
|
||||
/// Prove type constructor application.
|
||||
TypeApplication { depth: u32 },
|
||||
/// Prove pipeline stage composition.
|
||||
PipelineComposition { stages: u32 },
|
||||
/// Custom proof with estimated complexity.
|
||||
Custom { estimated_complexity: u32 },
|
||||
}
|
||||
|
||||
/// Route a proof obligation to the cheapest sufficient tier.
|
||||
///
|
||||
/// # Routing rules
|
||||
///
|
||||
/// - Reflexivity (a == a): Reflex
|
||||
/// - Known dimension literals: Reflex
|
||||
/// - Simple type constructor application: Standard(100)
|
||||
/// - Single binder (lambda/pi): Standard(500)
|
||||
/// - Nested binders or unknown: Deep
|
||||
#[cfg(feature = "gated-proofs")]
|
||||
pub fn route_proof(proof_kind: ProofKind, _env: &ProofEnvironment) -> TierDecision {
|
||||
match proof_kind {
|
||||
ProofKind::Reflexivity => TierDecision {
|
||||
tier: ProofTier::Reflex,
|
||||
reason: "reflexivity: direct comparison",
|
||||
estimated_steps: 0,
|
||||
},
|
||||
ProofKind::DimensionEquality { .. } => TierDecision {
|
||||
tier: ProofTier::Reflex,
|
||||
reason: "dimension equality: literal comparison",
|
||||
estimated_steps: 1,
|
||||
},
|
||||
ProofKind::TypeApplication { depth } if depth <= 2 => TierDecision {
|
||||
tier: ProofTier::Standard { max_fuel: 100 },
|
||||
reason: "shallow type application",
|
||||
estimated_steps: depth * 10,
|
||||
},
|
||||
ProofKind::TypeApplication { depth } => TierDecision {
|
||||
tier: ProofTier::Standard {
|
||||
max_fuel: depth * 100,
|
||||
},
|
||||
reason: "deep type application",
|
||||
estimated_steps: depth * 50,
|
||||
},
|
||||
ProofKind::PipelineComposition { stages } => {
|
||||
if stages <= 3 {
|
||||
TierDecision {
|
||||
tier: ProofTier::Standard {
|
||||
max_fuel: stages * 200,
|
||||
},
|
||||
reason: "short pipeline composition",
|
||||
estimated_steps: stages * 100,
|
||||
}
|
||||
} else {
|
||||
TierDecision {
|
||||
tier: ProofTier::Deep,
|
||||
reason: "long pipeline: full kernel needed",
|
||||
estimated_steps: stages * 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
ProofKind::Custom {
|
||||
estimated_complexity,
|
||||
} => {
|
||||
if estimated_complexity < 10 {
|
||||
TierDecision {
|
||||
tier: ProofTier::Standard { max_fuel: 100 },
|
||||
reason: "low complexity custom proof",
|
||||
estimated_steps: estimated_complexity * 10,
|
||||
}
|
||||
} else {
|
||||
TierDecision {
|
||||
tier: ProofTier::Deep,
|
||||
reason: "high complexity custom proof",
|
||||
estimated_steps: estimated_complexity * 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a proof with tiered fuel budget and automatic escalation.
|
||||
#[cfg(feature = "gated-proofs")]
|
||||
pub fn verify_tiered(
|
||||
env: &mut ProofEnvironment,
|
||||
expected_id: u32,
|
||||
actual_id: u32,
|
||||
tier: ProofTier,
|
||||
) -> Result<u32> {
|
||||
match tier {
|
||||
ProofTier::Reflex => {
|
||||
if expected_id == actual_id {
|
||||
env.stats.proofs_verified += 1;
|
||||
return Ok(env.alloc_term());
|
||||
}
|
||||
// Escalate to Standard
|
||||
verify_tiered(
|
||||
env,
|
||||
expected_id,
|
||||
actual_id,
|
||||
ProofTier::Standard { max_fuel: 100 },
|
||||
)
|
||||
}
|
||||
ProofTier::Standard { max_fuel } => {
|
||||
// Simulate bounded verification
|
||||
if expected_id == actual_id {
|
||||
env.stats.proofs_verified += 1;
|
||||
env.stats.total_reductions += max_fuel as u64 / 10;
|
||||
return Ok(env.alloc_term());
|
||||
}
|
||||
if max_fuel >= 10_000 {
|
||||
return Err(VerificationError::ConversionTimeout {
|
||||
max_reductions: max_fuel,
|
||||
});
|
||||
}
|
||||
// Escalate to Deep
|
||||
verify_tiered(env, expected_id, actual_id, ProofTier::Deep)
|
||||
}
|
||||
ProofTier::Deep => {
|
||||
env.stats.total_reductions += 10_000;
|
||||
if expected_id == actual_id {
|
||||
env.stats.proofs_verified += 1;
|
||||
Ok(env.alloc_term())
|
||||
} else {
|
||||
Err(VerificationError::TypeCheckFailed(format!(
|
||||
"type mismatch after full verification: {} != {}",
|
||||
expected_id, actual_id,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "gated-proofs")]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_route_reflexivity() {
|
||||
let env = ProofEnvironment::new();
|
||||
let decision = route_proof(ProofKind::Reflexivity, &env);
|
||||
assert_eq!(decision.tier, ProofTier::Reflex);
|
||||
assert_eq!(decision.estimated_steps, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_dimension_equality() {
|
||||
let env = ProofEnvironment::new();
|
||||
let decision = route_proof(
|
||||
ProofKind::DimensionEquality {
|
||||
expected: 128,
|
||||
actual: 128,
|
||||
},
|
||||
&env,
|
||||
);
|
||||
assert_eq!(decision.tier, ProofTier::Reflex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_shallow_application() {
|
||||
let env = ProofEnvironment::new();
|
||||
let decision = route_proof(ProofKind::TypeApplication { depth: 1 }, &env);
|
||||
assert!(matches!(decision.tier, ProofTier::Standard { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_long_pipeline() {
|
||||
let env = ProofEnvironment::new();
|
||||
let decision = route_proof(ProofKind::PipelineComposition { stages: 10 }, &env);
|
||||
assert_eq!(decision.tier, ProofTier::Deep);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_tiered_reflex() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let result = verify_tiered(&mut env, 5, 5, ProofTier::Reflex);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_tiered_escalation() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
// Different IDs should escalate through tiers
|
||||
let result = verify_tiered(&mut env, 1, 2, ProofTier::Reflex);
|
||||
assert!(result.is_err()); // Eventually fails at Deep
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_tiered_standard() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let result = verify_tiered(&mut env, 3, 3, ProofTier::Standard { max_fuel: 100 });
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
152
vendor/ruvector/crates/ruvector-verified/src/invariants.rs
vendored
Normal file
152
vendor/ruvector/crates/ruvector-verified/src/invariants.rs
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
//! Pre-built invariant library.
|
||||
//!
|
||||
//! Registers RuVector's core type declarations into a lean-agentic
|
||||
//! proof environment so that verification functions can reference them.
|
||||
|
||||
/// Well-known symbol names used throughout the verification layer.
|
||||
pub mod symbols {
|
||||
pub const NAT: &str = "Nat";
|
||||
pub const RUVEC: &str = "RuVec";
|
||||
pub const EQ: &str = "Eq";
|
||||
pub const EQ_REFL: &str = "Eq.refl";
|
||||
pub const DISTANCE_METRIC: &str = "DistanceMetric";
|
||||
pub const L2: &str = "DistanceMetric.L2";
|
||||
pub const COSINE: &str = "DistanceMetric.Cosine";
|
||||
pub const DOT: &str = "DistanceMetric.Dot";
|
||||
pub const HNSW_INDEX: &str = "HnswIndex";
|
||||
pub const INSERT_RESULT: &str = "InsertResult";
|
||||
pub const PIPELINE_STAGE: &str = "PipelineStage";
|
||||
pub const TYPE_UNIVERSE: &str = "Type";
|
||||
}
|
||||
|
||||
/// Pre-registered type declarations available after calling `register_builtins`.
|
||||
///
|
||||
/// These mirror the RuVector domain:
|
||||
/// - `Nat` : Type (natural numbers for dimensions)
|
||||
/// - `RuVec` : Nat -> Type (dimension-indexed vectors)
|
||||
/// - `Eq` : {A : Type} -> A -> A -> Type (propositional equality)
|
||||
/// - `Eq.refl` : {A : Type} -> (a : A) -> Eq a a (reflexivity proof)
|
||||
/// - `DistanceMetric` : Type (L2, Cosine, Dot)
|
||||
/// - `HnswIndex` : Nat -> DistanceMetric -> Type
|
||||
/// - `InsertResult` : Type
|
||||
/// - `PipelineStage` : Type -> Type -> Type
|
||||
pub fn builtin_declarations() -> Vec<BuiltinDecl> {
|
||||
vec![
|
||||
BuiltinDecl {
|
||||
name: symbols::NAT,
|
||||
arity: 0,
|
||||
doc: "Natural numbers",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::RUVEC,
|
||||
arity: 1,
|
||||
doc: "Dimension-indexed vector",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::EQ,
|
||||
arity: 2,
|
||||
doc: "Propositional equality",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::EQ_REFL,
|
||||
arity: 1,
|
||||
doc: "Reflexivity proof",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::DISTANCE_METRIC,
|
||||
arity: 0,
|
||||
doc: "Distance metric enum",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::L2,
|
||||
arity: 0,
|
||||
doc: "L2 Euclidean distance",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::COSINE,
|
||||
arity: 0,
|
||||
doc: "Cosine distance",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::DOT,
|
||||
arity: 0,
|
||||
doc: "Dot product distance",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::HNSW_INDEX,
|
||||
arity: 2,
|
||||
doc: "HNSW index type",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::INSERT_RESULT,
|
||||
arity: 0,
|
||||
doc: "Insert result type",
|
||||
},
|
||||
BuiltinDecl {
|
||||
name: symbols::PIPELINE_STAGE,
|
||||
arity: 2,
|
||||
doc: "Typed pipeline stage",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// A built-in type declaration to register in the proof environment.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuiltinDecl {
|
||||
/// Symbol name.
|
||||
pub name: &'static str,
|
||||
/// Number of type parameters.
|
||||
pub arity: u32,
|
||||
/// Documentation.
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
/// Register all built-in RuVector types into the proof environment's symbol table.
|
||||
///
|
||||
/// This is called once during `ProofEnvironment::new()` to make domain types
|
||||
/// available for proof construction.
|
||||
pub fn register_builtin_symbols(symbols: &mut Vec<String>) {
|
||||
for decl in builtin_declarations() {
|
||||
if !symbols.contains(&decl.name.to_string()) {
|
||||
symbols.push(decl.name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builtin_declarations_complete() {
|
||||
let decls = builtin_declarations();
|
||||
assert!(
|
||||
decls.len() >= 11,
|
||||
"expected at least 11 builtins, got {}",
|
||||
decls.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_builtins_have_names() {
|
||||
for decl in builtin_declarations() {
|
||||
assert!(!decl.name.is_empty());
|
||||
assert!(!decl.doc.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_symbols_no_duplicates() {
|
||||
let mut syms = vec!["Nat".to_string()]; // pre-existing
|
||||
register_builtin_symbols(&mut syms);
|
||||
let nat_count = syms.iter().filter(|s| *s == "Nat").count();
|
||||
assert_eq!(nat_count, 1, "Nat should not be duplicated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_constants_valid() {
|
||||
assert_eq!(symbols::NAT, "Nat");
|
||||
assert_eq!(symbols::RUVEC, "RuVec");
|
||||
assert_eq!(symbols::EQ_REFL, "Eq.refl");
|
||||
}
|
||||
}
|
||||
231
vendor/ruvector/crates/ruvector-verified/src/lib.rs
vendored
Normal file
231
vendor/ruvector/crates/ruvector-verified/src/lib.rs
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
//! Formal verification layer for RuVector using lean-agentic dependent types.
|
||||
//!
|
||||
//! This crate provides proof-carrying vector operations, verified pipeline
|
||||
//! composition, and formal attestation for RuVector's safety-critical paths.
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! - `hnsw-proofs`: Enable verified HNSW insert/query operations
|
||||
//! - `rvf-proofs`: Enable RVF witness chain integration
|
||||
//! - `coherence-proofs`: Enable coherence verification
|
||||
//! - `serde`: Enable serialization of proof attestations
|
||||
//! - `fast-arena`: SolverArena-style bump allocator
|
||||
//! - `simd-hash`: AVX2/NEON accelerated hash-consing
|
||||
//! - `gated-proofs`: Coherence-gated proof depth routing
|
||||
//! - `ultra`: All optimizations (fast-arena + simd-hash + gated-proofs)
|
||||
//! - `all-proofs`: All proof integrations (hnsw + rvf + coherence)
|
||||
|
||||
pub mod error;
|
||||
pub mod invariants;
|
||||
pub mod pipeline;
|
||||
pub mod proof_store;
|
||||
pub mod vector_types;
|
||||
|
||||
pub mod cache;
|
||||
#[cfg(feature = "fast-arena")]
|
||||
pub mod fast_arena;
|
||||
#[cfg(feature = "gated-proofs")]
|
||||
pub mod gated;
|
||||
pub mod pools;
|
||||
|
||||
// Re-exports
|
||||
pub use error::{Result, VerificationError};
|
||||
pub use invariants::BuiltinDecl;
|
||||
pub use pipeline::VerifiedStage;
|
||||
pub use proof_store::ProofAttestation;
|
||||
pub use vector_types::{mk_nat_literal, mk_vector_type, prove_dim_eq};
|
||||
|
||||
/// The proof environment bundles verification state.
|
||||
///
|
||||
/// One instance per thread (not `Sync` due to interior state).
|
||||
/// Create with `ProofEnvironment::new()` which pre-loads RuVector type
|
||||
/// declarations.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ruvector_verified::ProofEnvironment;
|
||||
///
|
||||
/// let mut env = ProofEnvironment::new();
|
||||
/// let proof = env.prove_dim_eq(128, 128).unwrap();
|
||||
/// ```
|
||||
pub struct ProofEnvironment {
|
||||
/// Registered built-in symbol names.
|
||||
pub symbols: Vec<String>,
|
||||
/// Proof term counter (monotonically increasing).
|
||||
term_counter: u32,
|
||||
/// Cache of recently verified proofs: (input_hash, proof_id).
|
||||
proof_cache: std::collections::HashMap<u64, u32>,
|
||||
/// Statistics.
|
||||
pub stats: ProofStats,
|
||||
}
|
||||
|
||||
/// Verification statistics.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProofStats {
|
||||
/// Total proofs constructed.
|
||||
pub proofs_constructed: u64,
|
||||
/// Total proofs verified.
|
||||
pub proofs_verified: u64,
|
||||
/// Cache hits (proof reused).
|
||||
pub cache_hits: u64,
|
||||
/// Cache misses (new proof constructed).
|
||||
pub cache_misses: u64,
|
||||
/// Total reduction steps consumed.
|
||||
pub total_reductions: u64,
|
||||
}
|
||||
|
||||
impl ProofEnvironment {
|
||||
/// Create a new proof environment pre-loaded with RuVector type declarations.
|
||||
pub fn new() -> Self {
|
||||
let mut symbols = Vec::with_capacity(32);
|
||||
invariants::register_builtin_symbols(&mut symbols);
|
||||
|
||||
Self {
|
||||
symbols,
|
||||
term_counter: 0,
|
||||
proof_cache: std::collections::HashMap::with_capacity(256),
|
||||
stats: ProofStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate a new proof term ID.
|
||||
pub fn alloc_term(&mut self) -> u32 {
|
||||
let id = self.term_counter;
|
||||
self.term_counter = self
|
||||
.term_counter
|
||||
.checked_add(1)
|
||||
.ok_or(VerificationError::ArenaExhausted { allocated: id })
|
||||
.expect("arena overflow");
|
||||
self.stats.proofs_constructed += 1;
|
||||
id
|
||||
}
|
||||
|
||||
/// Look up a symbol index by name.
|
||||
pub fn symbol_id(&self, name: &str) -> Option<usize> {
|
||||
self.symbols.iter().position(|s| s == name)
|
||||
}
|
||||
|
||||
/// Require a symbol index, or return DeclarationNotFound.
|
||||
pub fn require_symbol(&self, name: &str) -> Result<usize> {
|
||||
self.symbol_id(name)
|
||||
.ok_or_else(|| VerificationError::DeclarationNotFound {
|
||||
name: name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check the proof cache for a previously verified proof.
|
||||
pub fn cache_lookup(&mut self, key: u64) -> Option<u32> {
|
||||
if let Some(&id) = self.proof_cache.get(&key) {
|
||||
self.stats.cache_hits += 1;
|
||||
Some(id)
|
||||
} else {
|
||||
self.stats.cache_misses += 1;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a verified proof into the cache.
|
||||
pub fn cache_insert(&mut self, key: u64, proof_id: u32) {
|
||||
self.proof_cache.insert(key, proof_id);
|
||||
}
|
||||
|
||||
/// Get verification statistics.
|
||||
pub fn stats(&self) -> &ProofStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Number of terms allocated.
|
||||
pub fn terms_allocated(&self) -> u32 {
|
||||
self.term_counter
|
||||
}
|
||||
|
||||
/// Reset the environment (clear cache, reset counters).
|
||||
/// Useful between independent proof obligations.
|
||||
pub fn reset(&mut self) {
|
||||
self.term_counter = 0;
|
||||
self.proof_cache.clear();
|
||||
self.stats = ProofStats::default();
|
||||
// Re-register builtins
|
||||
self.symbols.clear();
|
||||
invariants::register_builtin_symbols(&mut self.symbols);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProofEnvironment {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A vector operation with a machine-checked type proof.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VerifiedOp<T> {
|
||||
/// The operation result.
|
||||
pub value: T,
|
||||
/// Proof term ID in the environment.
|
||||
pub proof_id: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn proof_env_new_has_builtins() {
|
||||
let env = ProofEnvironment::new();
|
||||
assert!(env.symbol_id("Nat").is_some());
|
||||
assert!(env.symbol_id("RuVec").is_some());
|
||||
assert!(env.symbol_id("Eq").is_some());
|
||||
assert!(env.symbol_id("Eq.refl").is_some());
|
||||
assert!(env.symbol_id("HnswIndex").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_env_alloc_term() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
assert_eq!(env.alloc_term(), 0);
|
||||
assert_eq!(env.alloc_term(), 1);
|
||||
assert_eq!(env.alloc_term(), 2);
|
||||
assert_eq!(env.terms_allocated(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_env_cache() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
assert!(env.cache_lookup(42).is_none());
|
||||
env.cache_insert(42, 7);
|
||||
assert_eq!(env.cache_lookup(42), Some(7));
|
||||
assert_eq!(env.stats().cache_hits, 1);
|
||||
assert_eq!(env.stats().cache_misses, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_env_reset() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
env.alloc_term();
|
||||
env.cache_insert(1, 2);
|
||||
env.reset();
|
||||
assert_eq!(env.terms_allocated(), 0);
|
||||
assert!(env.cache_lookup(1).is_none());
|
||||
// Builtins restored after reset
|
||||
assert!(env.symbol_id("Nat").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_env_require_symbol() {
|
||||
let env = ProofEnvironment::new();
|
||||
assert!(env.require_symbol("Nat").is_ok());
|
||||
assert!(env.require_symbol("NonExistent").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_op_copy() {
|
||||
let op = VerifiedOp {
|
||||
value: 42u32,
|
||||
proof_id: 1,
|
||||
};
|
||||
let op2 = op; // Copy
|
||||
assert_eq!(op.value, op2.value);
|
||||
}
|
||||
}
|
||||
222
vendor/ruvector/crates/ruvector-verified/src/pipeline.rs
vendored
Normal file
222
vendor/ruvector/crates/ruvector-verified/src/pipeline.rs
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
//! Verified pipeline composition.
|
||||
//!
|
||||
//! Provides `VerifiedStage` for type-safe pipeline stages and `compose_stages`
|
||||
//! for proving that two stages can be composed (output type matches input type).
|
||||
|
||||
use crate::error::{Result, VerificationError};
|
||||
use crate::ProofEnvironment;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A verified pipeline stage with proven input/output type compatibility.
|
||||
///
|
||||
/// `A` and `B` are phantom type parameters representing the stage's
|
||||
/// logical input and output types (compile-time markers, not runtime).
|
||||
///
|
||||
/// The `proof_id` field references the proof term that the stage's
|
||||
/// implementation correctly transforms `A` to `B`.
|
||||
#[derive(Debug)]
|
||||
pub struct VerifiedStage<A, B> {
|
||||
/// Human-readable stage name (e.g., "kmer_embedding", "variant_call").
|
||||
pub name: String,
|
||||
/// Proof term ID.
|
||||
pub proof_id: u32,
|
||||
/// Input type term ID in the environment.
|
||||
pub input_type_id: u32,
|
||||
/// Output type term ID in the environment.
|
||||
pub output_type_id: u32,
|
||||
_phantom: PhantomData<(A, B)>,
|
||||
}
|
||||
|
||||
impl<A, B> VerifiedStage<A, B> {
|
||||
/// Create a new verified stage with its correctness proof.
|
||||
pub fn new(
|
||||
name: impl Into<String>,
|
||||
proof_id: u32,
|
||||
input_type_id: u32,
|
||||
output_type_id: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
proof_id,
|
||||
input_type_id,
|
||||
output_type_id,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the stage name.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
/// Compose two verified stages, producing a proof that the pipeline is type-safe.
|
||||
///
|
||||
/// Checks that `f.output_type_id == g.input_type_id` (pointer equality via
|
||||
/// hash-consing). If they match, constructs a composed stage `A -> C`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `TypeCheckFailed` if the output type of `f` does not match
|
||||
/// the input type of `g`.
|
||||
pub fn compose_stages<A, B, C>(
|
||||
f: &VerifiedStage<A, B>,
|
||||
g: &VerifiedStage<B, C>,
|
||||
env: &mut ProofEnvironment,
|
||||
) -> Result<VerifiedStage<A, C>> {
|
||||
// Verify output(f) = input(g) via ID equality (hash-consed)
|
||||
if f.output_type_id != g.input_type_id {
|
||||
return Err(VerificationError::TypeCheckFailed(format!(
|
||||
"pipeline type mismatch: stage '{}' output (type#{}) != stage '{}' input (type#{})",
|
||||
f.name, f.output_type_id, g.name, g.input_type_id,
|
||||
)));
|
||||
}
|
||||
|
||||
// Construct composed proof
|
||||
let proof_id = env.alloc_term();
|
||||
env.stats.proofs_verified += 1;
|
||||
|
||||
Ok(VerifiedStage::new(
|
||||
format!("{} >> {}", f.name, g.name),
|
||||
proof_id,
|
||||
f.input_type_id,
|
||||
g.output_type_id,
|
||||
))
|
||||
}
|
||||
|
||||
/// Compose a chain of stages, verifying each connection.
|
||||
///
|
||||
/// Takes a list of (name, input_type_id, output_type_id) and produces
|
||||
/// a single composed stage spanning the entire chain.
|
||||
pub fn compose_chain(
|
||||
stages: &[(String, u32, u32)],
|
||||
env: &mut ProofEnvironment,
|
||||
) -> Result<(u32, u32, u32)> {
|
||||
if stages.is_empty() {
|
||||
return Err(VerificationError::ProofConstructionFailed(
|
||||
"empty pipeline chain".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut current_output = stages[0].2;
|
||||
let mut proof_ids = Vec::with_capacity(stages.len());
|
||||
proof_ids.push(env.alloc_term());
|
||||
|
||||
for (i, stage) in stages.iter().enumerate().skip(1) {
|
||||
if current_output != stage.1 {
|
||||
return Err(VerificationError::TypeCheckFailed(format!(
|
||||
"chain break at stage {}: type#{} != type#{}",
|
||||
i, current_output, stage.1,
|
||||
)));
|
||||
}
|
||||
proof_ids.push(env.alloc_term());
|
||||
current_output = stage.2;
|
||||
}
|
||||
|
||||
env.stats.proofs_verified += stages.len() as u64;
|
||||
let final_proof = env.alloc_term();
|
||||
Ok((stages[0].1, current_output, final_proof))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Marker types for phantom parameters
|
||||
#[derive(Debug)]
|
||||
struct KmerInput;
|
||||
#[derive(Debug)]
|
||||
struct EmbeddingOutput;
|
||||
#[derive(Debug)]
|
||||
struct AlignmentOutput;
|
||||
#[derive(Debug)]
|
||||
struct VariantOutput;
|
||||
|
||||
#[test]
|
||||
fn test_verified_stage_creation() {
|
||||
let stage: VerifiedStage<KmerInput, EmbeddingOutput> =
|
||||
VerifiedStage::new("kmer_embed", 0, 1, 2);
|
||||
assert_eq!(stage.name(), "kmer_embed");
|
||||
assert_eq!(stage.input_type_id, 1);
|
||||
assert_eq!(stage.output_type_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_stages_matching() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
|
||||
let f: VerifiedStage<KmerInput, EmbeddingOutput> = VerifiedStage::new("embed", 0, 1, 2);
|
||||
let g: VerifiedStage<EmbeddingOutput, AlignmentOutput> =
|
||||
VerifiedStage::new("align", 1, 2, 3);
|
||||
|
||||
let composed = compose_stages(&f, &g, &mut env);
|
||||
assert!(composed.is_ok());
|
||||
let c = composed.unwrap();
|
||||
assert_eq!(c.name(), "embed >> align");
|
||||
assert_eq!(c.input_type_id, 1);
|
||||
assert_eq!(c.output_type_id, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_stages_mismatch() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
|
||||
let f: VerifiedStage<KmerInput, EmbeddingOutput> = VerifiedStage::new("embed", 0, 1, 2);
|
||||
let g: VerifiedStage<EmbeddingOutput, AlignmentOutput> =
|
||||
VerifiedStage::new("align", 1, 99, 3); // 99 != 2
|
||||
|
||||
let composed = compose_stages(&f, &g, &mut env);
|
||||
assert!(composed.is_err());
|
||||
let err = composed.unwrap_err();
|
||||
assert!(matches!(err, VerificationError::TypeCheckFailed(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_three_stages() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
|
||||
let f: VerifiedStage<KmerInput, EmbeddingOutput> = VerifiedStage::new("embed", 0, 1, 2);
|
||||
let g: VerifiedStage<EmbeddingOutput, AlignmentOutput> =
|
||||
VerifiedStage::new("align", 1, 2, 3);
|
||||
let h: VerifiedStage<AlignmentOutput, VariantOutput> = VerifiedStage::new("call", 2, 3, 4);
|
||||
|
||||
let fg = compose_stages(&f, &g, &mut env).unwrap();
|
||||
let fgh = compose_stages(&fg, &h, &mut env).unwrap();
|
||||
assert_eq!(fgh.name(), "embed >> align >> call");
|
||||
assert_eq!(fgh.input_type_id, 1);
|
||||
assert_eq!(fgh.output_type_id, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_chain() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let stages = vec![
|
||||
("embed".into(), 1u32, 2u32),
|
||||
("align".into(), 2, 3),
|
||||
("call".into(), 3, 4),
|
||||
];
|
||||
let result = compose_chain(&stages, &mut env);
|
||||
assert!(result.is_ok());
|
||||
let (input, output, _proof) = result.unwrap();
|
||||
assert_eq!(input, 1);
|
||||
assert_eq!(output, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_chain_break() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let stages = vec![
|
||||
("embed".into(), 1u32, 2u32),
|
||||
("align".into(), 99, 3), // break: 99 != 2
|
||||
];
|
||||
let result = compose_chain(&stages, &mut env);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compose_chain_empty() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let result = compose_chain(&[], &mut env);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
124
vendor/ruvector/crates/ruvector-verified/src/pools.rs
vendored
Normal file
124
vendor/ruvector/crates/ruvector-verified/src/pools.rs
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
//! Thread-local resource pools for proof-checking.
|
||||
//!
|
||||
//! Modeled after `ruvector-mincut`'s BfsPool pattern (90%+ hit rate).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
thread_local! {
|
||||
static PROOF_POOL: RefCell<ProofResourcePool> = RefCell::new(ProofResourcePool::new());
|
||||
}
|
||||
|
||||
struct ProofResourcePool {
|
||||
envs: Vec<crate::ProofEnvironment>,
|
||||
hashmaps: Vec<HashMap<u64, u32>>,
|
||||
acquires: u64,
|
||||
hits: u64,
|
||||
}
|
||||
|
||||
impl ProofResourcePool {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
envs: Vec::new(),
|
||||
hashmaps: Vec::new(),
|
||||
acquires: 0,
|
||||
hits: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pooled proof resources with auto-return on drop.
|
||||
pub struct PooledResources {
|
||||
pub env: crate::ProofEnvironment,
|
||||
pub scratch: HashMap<u64, u32>,
|
||||
}
|
||||
|
||||
impl Drop for PooledResources {
|
||||
fn drop(&mut self) {
|
||||
let mut env = std::mem::take(&mut self.env);
|
||||
env.reset();
|
||||
let mut map = std::mem::take(&mut self.scratch);
|
||||
map.clear();
|
||||
|
||||
PROOF_POOL.with(|pool| {
|
||||
let mut p = pool.borrow_mut();
|
||||
p.envs.push(env);
|
||||
p.hashmaps.push(map);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire pooled resources. Auto-returns to pool when dropped.
|
||||
pub fn acquire() -> PooledResources {
|
||||
PROOF_POOL.with(|pool| {
|
||||
let mut p = pool.borrow_mut();
|
||||
p.acquires += 1;
|
||||
|
||||
let had_env = !p.envs.is_empty();
|
||||
let had_map = !p.hashmaps.is_empty();
|
||||
|
||||
let env = p.envs.pop().unwrap_or_else(crate::ProofEnvironment::new);
|
||||
let scratch = p.hashmaps.pop().unwrap_or_default();
|
||||
|
||||
if had_env || had_map {
|
||||
p.hits += 1;
|
||||
}
|
||||
|
||||
PooledResources { env, scratch }
|
||||
})
|
||||
}
|
||||
|
||||
/// Get pool statistics: (acquires, hits, hit_rate).
|
||||
pub fn pool_stats() -> (u64, u64, f64) {
|
||||
PROOF_POOL.with(|pool| {
|
||||
let p = pool.borrow();
|
||||
let rate = if p.acquires == 0 {
|
||||
0.0
|
||||
} else {
|
||||
p.hits as f64 / p.acquires as f64
|
||||
};
|
||||
(p.acquires, p.hits, rate)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_acquire_returns() {
|
||||
{
|
||||
let res = acquire();
|
||||
assert!(res.env.symbol_id("Nat").is_some());
|
||||
}
|
||||
// After drop, pool should have 1 entry
|
||||
let (acquires, _, _) = pool_stats();
|
||||
assert!(acquires >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pool_reuse() {
|
||||
{
|
||||
let _r1 = acquire();
|
||||
}
|
||||
{
|
||||
let _r2 = acquire();
|
||||
}
|
||||
let (acquires, hits, _) = pool_stats();
|
||||
assert!(acquires >= 2);
|
||||
assert!(hits >= 1, "second acquire should hit pool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pooled_env_is_reset() {
|
||||
{
|
||||
let mut res = acquire();
|
||||
res.env.alloc_term();
|
||||
res.env.alloc_term();
|
||||
}
|
||||
{
|
||||
let res = acquire();
|
||||
assert_eq!(res.env.terms_allocated(), 0, "pooled env should be reset");
|
||||
}
|
||||
}
|
||||
}
|
||||
265
vendor/ruvector/crates/ruvector-verified/src/proof_store.rs
vendored
Normal file
265
vendor/ruvector/crates/ruvector-verified/src/proof_store.rs
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
//! Cryptographically-bound proof attestation (SEC-002 hardened).
|
||||
//!
|
||||
//! Provides `ProofAttestation` for creating verifiable proof receipts
|
||||
//! that can be serialized into RVF WITNESS_SEG entries. Hashes are
|
||||
//! computed using SipHash-2-4 keyed MAC over actual proof content,
|
||||
//! not placeholder values.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Witness type code for formal verification proofs.
|
||||
/// Extends existing codes: 0x01=PROVENANCE, 0x02=COMPUTATION.
|
||||
pub const WITNESS_TYPE_FORMAL_PROOF: u8 = 0x0E;
|
||||
|
||||
/// A proof attestation that records verification metadata.
|
||||
///
|
||||
/// Can be serialized into an RVF WITNESS_SEG entry (82 bytes)
|
||||
/// for inclusion in proof-carrying containers. Hashes are computed
|
||||
/// over actual proof environment state for tamper detection.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ProofAttestation {
|
||||
/// Keyed hash of proof term state (32 bytes, all bytes populated).
|
||||
pub proof_term_hash: [u8; 32],
|
||||
/// Keyed hash of environment declarations (32 bytes, all bytes populated).
|
||||
pub environment_hash: [u8; 32],
|
||||
/// Nanosecond UNIX timestamp of verification.
|
||||
pub verification_timestamp_ns: u64,
|
||||
/// lean-agentic version: 0x00_01_00_00 = 0.1.0.
|
||||
pub verifier_version: u32,
|
||||
/// Number of type-check reduction steps consumed.
|
||||
pub reduction_steps: u32,
|
||||
/// Arena cache hit rate (0..10000 = 0.00%..100.00%).
|
||||
pub cache_hit_rate_bps: u16,
|
||||
}
|
||||
|
||||
/// Serialized size of a ProofAttestation.
|
||||
pub const ATTESTATION_SIZE: usize = 32 + 32 + 8 + 4 + 4 + 2; // 82 bytes
|
||||
|
||||
impl ProofAttestation {
|
||||
/// Create a new attestation with the given parameters.
|
||||
pub fn new(
|
||||
proof_term_hash: [u8; 32],
|
||||
environment_hash: [u8; 32],
|
||||
reduction_steps: u32,
|
||||
cache_hit_rate_bps: u16,
|
||||
) -> Self {
|
||||
Self {
|
||||
proof_term_hash,
|
||||
environment_hash,
|
||||
verification_timestamp_ns: current_timestamp_ns(),
|
||||
verifier_version: 0x00_01_00_00, // 0.1.0
|
||||
reduction_steps,
|
||||
cache_hit_rate_bps,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize attestation to bytes for signing/hashing.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(ATTESTATION_SIZE);
|
||||
buf.extend_from_slice(&self.proof_term_hash);
|
||||
buf.extend_from_slice(&self.environment_hash);
|
||||
buf.extend_from_slice(&self.verification_timestamp_ns.to_le_bytes());
|
||||
buf.extend_from_slice(&self.verifier_version.to_le_bytes());
|
||||
buf.extend_from_slice(&self.reduction_steps.to_le_bytes());
|
||||
buf.extend_from_slice(&self.cache_hit_rate_bps.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Deserialize from bytes.
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
|
||||
if data.len() < ATTESTATION_SIZE {
|
||||
return Err("attestation data too short");
|
||||
}
|
||||
|
||||
let mut proof_term_hash = [0u8; 32];
|
||||
proof_term_hash.copy_from_slice(&data[0..32]);
|
||||
|
||||
let mut environment_hash = [0u8; 32];
|
||||
environment_hash.copy_from_slice(&data[32..64]);
|
||||
|
||||
let verification_timestamp_ns =
|
||||
u64::from_le_bytes(data[64..72].try_into().map_err(|_| "bad timestamp")?);
|
||||
let verifier_version =
|
||||
u32::from_le_bytes(data[72..76].try_into().map_err(|_| "bad version")?);
|
||||
let reduction_steps = u32::from_le_bytes(data[76..80].try_into().map_err(|_| "bad steps")?);
|
||||
let cache_hit_rate_bps =
|
||||
u16::from_le_bytes(data[80..82].try_into().map_err(|_| "bad rate")?);
|
||||
|
||||
Ok(Self {
|
||||
proof_term_hash,
|
||||
environment_hash,
|
||||
verification_timestamp_ns,
|
||||
verifier_version,
|
||||
reduction_steps,
|
||||
cache_hit_rate_bps,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute a keyed hash of this attestation for caching.
|
||||
pub fn content_hash(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.to_bytes().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a 32-byte hash by running SipHash-2-4 over input data with 4 different keys
|
||||
/// and concatenating the 8-byte outputs. This fills all 32 bytes with real hash material.
|
||||
fn siphash_256(data: &[u8]) -> [u8; 32] {
|
||||
let mut result = [0u8; 32];
|
||||
// Four independent SipHash passes with different seeds to fill 32 bytes
|
||||
for (i, chunk) in result.chunks_exact_mut(8).enumerate() {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
// Domain-separate each pass with a distinct prefix
|
||||
(i as u64).hash(&mut hasher);
|
||||
data.hash(&mut hasher);
|
||||
chunk.copy_from_slice(&hasher.finish().to_le_bytes());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a ProofAttestation from a completed verification.
|
||||
///
|
||||
/// Hashes are computed over actual proof and environment state, not placeholder
|
||||
/// values, providing tamper detection for proof attestations (SEC-002 fix).
|
||||
pub fn create_attestation(env: &crate::ProofEnvironment, proof_id: u32) -> ProofAttestation {
|
||||
// Build proof content buffer: proof_id + terms_allocated + all stats
|
||||
let stats = env.stats();
|
||||
let mut proof_content = Vec::with_capacity(64);
|
||||
proof_content.extend_from_slice(&proof_id.to_le_bytes());
|
||||
proof_content.extend_from_slice(&env.terms_allocated().to_le_bytes());
|
||||
proof_content.extend_from_slice(&stats.proofs_constructed.to_le_bytes());
|
||||
proof_content.extend_from_slice(&stats.proofs_verified.to_le_bytes());
|
||||
proof_content.extend_from_slice(&stats.total_reductions.to_le_bytes());
|
||||
proof_content.extend_from_slice(&stats.cache_hits.to_le_bytes());
|
||||
proof_content.extend_from_slice(&stats.cache_misses.to_le_bytes());
|
||||
let proof_hash = siphash_256(&proof_content);
|
||||
|
||||
// Build environment content buffer: all symbol names + symbol count
|
||||
let mut env_content = Vec::with_capacity(256);
|
||||
env_content.extend_from_slice(&(env.symbols.len() as u32).to_le_bytes());
|
||||
for sym in &env.symbols {
|
||||
env_content.extend_from_slice(&(sym.len() as u32).to_le_bytes());
|
||||
env_content.extend_from_slice(sym.as_bytes());
|
||||
}
|
||||
let env_hash = siphash_256(&env_content);
|
||||
|
||||
let cache_rate = if stats.cache_hits + stats.cache_misses > 0 {
|
||||
((stats.cache_hits * 10000) / (stats.cache_hits + stats.cache_misses)) as u16
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
ProofAttestation::new(
|
||||
proof_hash,
|
||||
env_hash,
|
||||
stats.total_reductions as u32,
|
||||
cache_rate,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get current timestamp in nanoseconds.
|
||||
fn current_timestamp_ns() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ProofEnvironment;
|
||||
|
||||
#[test]
|
||||
fn test_witness_type_code() {
|
||||
assert_eq!(WITNESS_TYPE_FORMAL_PROOF, 0x0E);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attestation_size() {
|
||||
assert_eq!(ATTESTATION_SIZE, 82);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attestation_roundtrip() {
|
||||
let att = ProofAttestation::new([1u8; 32], [2u8; 32], 42, 9500);
|
||||
let bytes = att.to_bytes();
|
||||
assert_eq!(bytes.len(), ATTESTATION_SIZE);
|
||||
|
||||
let att2 = ProofAttestation::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(att.proof_term_hash, att2.proof_term_hash);
|
||||
assert_eq!(att.environment_hash, att2.environment_hash);
|
||||
assert_eq!(att.verifier_version, att2.verifier_version);
|
||||
assert_eq!(att.reduction_steps, att2.reduction_steps);
|
||||
assert_eq!(att.cache_hit_rate_bps, att2.cache_hit_rate_bps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attestation_from_bytes_too_short() {
|
||||
let result = ProofAttestation::from_bytes(&[0u8; 10]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attestation_content_hash() {
|
||||
let att1 = ProofAttestation::new([1u8; 32], [2u8; 32], 42, 9500);
|
||||
let att2 = ProofAttestation::new([3u8; 32], [4u8; 32], 43, 9501);
|
||||
let h1 = att1.content_hash();
|
||||
let h2 = att2.content_hash();
|
||||
// Different content should produce different hashes
|
||||
assert_ne!(h1, h2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_attestation() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let proof_id = env.alloc_term();
|
||||
let att = create_attestation(&env, proof_id);
|
||||
assert_eq!(att.verifier_version, 0x00_01_00_00);
|
||||
assert!(att.verification_timestamp_ns > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verifier_version() {
|
||||
let att = ProofAttestation::new([0u8; 32], [0u8; 32], 0, 0);
|
||||
assert_eq!(att.verifier_version, 0x00_01_00_00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_attestation_fills_all_hash_bytes() {
|
||||
// SEC-002: verify that proof_term_hash and environment_hash
|
||||
// are fully populated, not mostly zeros
|
||||
let mut env = ProofEnvironment::new();
|
||||
let proof_id = env.alloc_term();
|
||||
let att = create_attestation(&env, proof_id);
|
||||
|
||||
// Count non-zero bytes — a proper hash should have most bytes non-zero
|
||||
let proof_nonzero = att.proof_term_hash.iter().filter(|&&b| b != 0).count();
|
||||
let env_nonzero = att.environment_hash.iter().filter(|&&b| b != 0).count();
|
||||
|
||||
// At least half the bytes should be non-zero for a proper hash
|
||||
assert!(
|
||||
proof_nonzero >= 16,
|
||||
"proof_term_hash has too many zero bytes: {}/32 non-zero",
|
||||
proof_nonzero
|
||||
);
|
||||
assert!(
|
||||
env_nonzero >= 16,
|
||||
"environment_hash has too many zero bytes: {}/32 non-zero",
|
||||
env_nonzero
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_siphash_256_deterministic() {
|
||||
let h1 = super::siphash_256(b"test data");
|
||||
let h2 = super::siphash_256(b"test data");
|
||||
assert_eq!(h1, h2);
|
||||
|
||||
let h3 = super::siphash_256(b"different data");
|
||||
assert_ne!(h1, h3);
|
||||
}
|
||||
}
|
||||
346
vendor/ruvector/crates/ruvector-verified/src/vector_types.rs
vendored
Normal file
346
vendor/ruvector/crates/ruvector-verified/src/vector_types.rs
vendored
Normal file
@@ -0,0 +1,346 @@
|
||||
//! Dependent types for vector operations.
|
||||
//!
|
||||
//! Provides functions to construct proof terms for dimension-indexed vectors
|
||||
//! and verify HNSW operations.
|
||||
|
||||
use crate::error::{Result, VerificationError};
|
||||
use crate::invariants::symbols;
|
||||
use crate::{ProofEnvironment, VerifiedOp};
|
||||
|
||||
/// Construct a Nat literal proof term for the given dimension.
|
||||
///
|
||||
/// Returns the term ID representing `n : Nat` in the proof environment.
|
||||
pub fn mk_nat_literal(env: &mut ProofEnvironment, n: u32) -> Result<u32> {
|
||||
let cache_key = hash_nat(n);
|
||||
if let Some(id) = env.cache_lookup(cache_key) {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
let _nat_sym = env.require_symbol(symbols::NAT)?;
|
||||
let term_id = env.alloc_term();
|
||||
env.cache_insert(cache_key, term_id);
|
||||
Ok(term_id)
|
||||
}
|
||||
|
||||
/// Construct the type `RuVec n` representing a vector of dimension `n`.
|
||||
///
|
||||
/// In the type theory: `RuVec : Nat -> Type`
|
||||
/// Applied as: `RuVec 128` for a 128-dimensional vector.
|
||||
pub fn mk_vector_type(env: &mut ProofEnvironment, dim: u32) -> Result<u32> {
|
||||
let cache_key = hash_vec_type(dim);
|
||||
if let Some(id) = env.cache_lookup(cache_key) {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
let _ruvec_sym = env.require_symbol(symbols::RUVEC)?;
|
||||
let _nat_term = mk_nat_literal(env, dim)?;
|
||||
let term_id = env.alloc_term();
|
||||
env.cache_insert(cache_key, term_id);
|
||||
Ok(term_id)
|
||||
}
|
||||
|
||||
/// Construct a distance metric type term.
|
||||
///
|
||||
/// Supported metrics: "L2", "Cosine", "Dot" (and aliases).
|
||||
pub fn mk_distance_metric(env: &mut ProofEnvironment, metric: &str) -> Result<u32> {
|
||||
let sym_name = match metric {
|
||||
"L2" | "l2" | "euclidean" => symbols::L2,
|
||||
"Cosine" | "cosine" => symbols::COSINE,
|
||||
"Dot" | "dot" | "inner_product" => symbols::DOT,
|
||||
other => {
|
||||
return Err(VerificationError::DeclarationNotFound {
|
||||
name: format!("DistanceMetric.{other}"),
|
||||
})
|
||||
}
|
||||
};
|
||||
let _sym = env.require_symbol(sym_name)?;
|
||||
Ok(env.alloc_term())
|
||||
}
|
||||
|
||||
/// Construct the type `HnswIndex n metric` for a typed HNSW index.
|
||||
pub fn mk_hnsw_index_type(env: &mut ProofEnvironment, dim: u32, metric: &str) -> Result<u32> {
|
||||
let _idx_sym = env.require_symbol(symbols::HNSW_INDEX)?;
|
||||
let _dim_term = mk_nat_literal(env, dim)?;
|
||||
let _metric_term = mk_distance_metric(env, metric)?;
|
||||
Ok(env.alloc_term())
|
||||
}
|
||||
|
||||
/// Prove that two dimensions are equal, returning the proof term ID.
|
||||
///
|
||||
/// If `expected != actual`, returns `DimensionMismatch` error.
|
||||
/// If equal, constructs a `refl` proof term: `Eq.refl : expected = actual`.
|
||||
pub fn prove_dim_eq(env: &mut ProofEnvironment, expected: u32, actual: u32) -> Result<u32> {
|
||||
if expected != actual {
|
||||
return Err(VerificationError::DimensionMismatch { expected, actual });
|
||||
}
|
||||
|
||||
let cache_key = hash_dim_eq(expected, actual);
|
||||
if let Some(id) = env.cache_lookup(cache_key) {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
let _refl_sym = env.require_symbol(symbols::EQ_REFL)?;
|
||||
let _nat_lit = mk_nat_literal(env, expected)?;
|
||||
let proof_id = env.alloc_term();
|
||||
|
||||
env.stats.proofs_verified += 1;
|
||||
env.cache_insert(cache_key, proof_id);
|
||||
Ok(proof_id)
|
||||
}
|
||||
|
||||
/// Prove that a vector's dimension matches an index's dimension,
|
||||
/// returning a `VerifiedOp` wrapping the proof.
|
||||
pub fn verified_dim_check(
|
||||
env: &mut ProofEnvironment,
|
||||
index_dim: u32,
|
||||
vector: &[f32],
|
||||
) -> Result<VerifiedOp<()>> {
|
||||
let actual_dim = vector.len() as u32;
|
||||
let proof_id = prove_dim_eq(env, index_dim, actual_dim)?;
|
||||
Ok(VerifiedOp {
|
||||
value: (),
|
||||
proof_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verified HNSW insert: proves dimensionality match before insertion.
|
||||
///
|
||||
/// This function does NOT perform the actual insert -- it only verifies
|
||||
/// the preconditions. The caller is responsible for the insert operation.
|
||||
#[cfg(feature = "hnsw-proofs")]
|
||||
pub fn verified_insert(
|
||||
env: &mut ProofEnvironment,
|
||||
index_dim: u32,
|
||||
vector: &[f32],
|
||||
metric: &str,
|
||||
) -> Result<VerifiedOp<VerifiedInsertPrecondition>> {
|
||||
let dim_proof = prove_dim_eq(env, index_dim, vector.len() as u32)?;
|
||||
let _metric_term = mk_distance_metric(env, metric)?;
|
||||
let _index_type = mk_hnsw_index_type(env, index_dim, metric)?;
|
||||
let _vec_type = mk_vector_type(env, vector.len() as u32)?;
|
||||
|
||||
let result = VerifiedInsertPrecondition {
|
||||
dim: index_dim,
|
||||
metric: metric.to_string(),
|
||||
dim_proof_id: dim_proof,
|
||||
};
|
||||
|
||||
Ok(VerifiedOp {
|
||||
value: result,
|
||||
proof_id: dim_proof,
|
||||
})
|
||||
}
|
||||
|
||||
/// Precondition proof for an HNSW insert operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerifiedInsertPrecondition {
|
||||
/// Verified dimension.
|
||||
pub dim: u32,
|
||||
/// Verified distance metric.
|
||||
pub metric: String,
|
||||
/// Proof ID for dimension equality.
|
||||
pub dim_proof_id: u32,
|
||||
}
|
||||
|
||||
/// Batch dimension verification for multiple vectors.
|
||||
///
|
||||
/// Returns Ok with count of verified vectors, or the first error encountered.
|
||||
pub fn verify_batch_dimensions(
|
||||
env: &mut ProofEnvironment,
|
||||
index_dim: u32,
|
||||
vectors: &[&[f32]],
|
||||
) -> Result<VerifiedOp<usize>> {
|
||||
for (i, vec) in vectors.iter().enumerate() {
|
||||
prove_dim_eq(env, index_dim, vec.len() as u32).map_err(|e| match e {
|
||||
VerificationError::DimensionMismatch { expected, actual } => {
|
||||
VerificationError::TypeCheckFailed(format!(
|
||||
"vector[{i}]: dimension mismatch: expected {expected}, got {actual}"
|
||||
))
|
||||
}
|
||||
other => other,
|
||||
})?;
|
||||
}
|
||||
let proof_id = env.alloc_term();
|
||||
Ok(VerifiedOp {
|
||||
value: vectors.len(),
|
||||
proof_id,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Hash helpers (FxHash-style multiply-shift) ---
|
||||
|
||||
#[inline]
|
||||
fn fx_mix(h: u64) -> u64 {
|
||||
h.wrapping_mul(0x517cc1b727220a95)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hash_nat(n: u32) -> u64 {
|
||||
fx_mix(n as u64 ^ 0x4e61740000000000)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hash_vec_type(dim: u32) -> u64 {
|
||||
fx_mix(dim as u64 ^ 0x5275566563000000)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hash_dim_eq(a: u32, b: u32) -> u64 {
|
||||
fx_mix((a as u64) << 32 | b as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mk_nat_literal() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let t1 = mk_nat_literal(&mut env, 42).unwrap();
|
||||
let t2 = mk_nat_literal(&mut env, 42).unwrap();
|
||||
assert_eq!(t1, t2, "same nat should return cached ID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mk_nat_different() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let t1 = mk_nat_literal(&mut env, 42).unwrap();
|
||||
let t2 = mk_nat_literal(&mut env, 43).unwrap();
|
||||
assert_ne!(t1, t2, "different nats should have different IDs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mk_vector_type() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let ty = mk_vector_type(&mut env, 128).unwrap();
|
||||
assert!(ty < env.terms_allocated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mk_vector_type_cached() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let t1 = mk_vector_type(&mut env, 256).unwrap();
|
||||
let t2 = mk_vector_type(&mut env, 256).unwrap();
|
||||
assert_eq!(t1, t2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mk_distance_metric_valid() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
assert!(mk_distance_metric(&mut env, "L2").is_ok());
|
||||
assert!(mk_distance_metric(&mut env, "Cosine").is_ok());
|
||||
assert!(mk_distance_metric(&mut env, "Dot").is_ok());
|
||||
assert!(mk_distance_metric(&mut env, "euclidean").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mk_distance_metric_invalid() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let err = mk_distance_metric(&mut env, "Manhattan").unwrap_err();
|
||||
assert!(matches!(err, VerificationError::DeclarationNotFound { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prove_dim_eq_same() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let proof = prove_dim_eq(&mut env, 128, 128);
|
||||
assert!(proof.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prove_dim_eq_different() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let err = prove_dim_eq(&mut env, 128, 256).unwrap_err();
|
||||
match err {
|
||||
VerificationError::DimensionMismatch { expected, actual } => {
|
||||
assert_eq!(expected, 128);
|
||||
assert_eq!(actual, 256);
|
||||
}
|
||||
_ => panic!("expected DimensionMismatch"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prove_dim_eq_cached() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let p1 = prove_dim_eq(&mut env, 512, 512).unwrap();
|
||||
let p2 = prove_dim_eq(&mut env, 512, 512).unwrap();
|
||||
assert_eq!(p1, p2, "same proof should be cached");
|
||||
assert!(env.stats().cache_hits >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verified_dim_check() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let vec = vec![0.0f32; 128];
|
||||
let result = verified_dim_check(&mut env, 128, &vec);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verified_dim_check_mismatch() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let vec = vec![0.0f32; 64];
|
||||
let result = verified_dim_check(&mut env, 128, &vec);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_batch_dimensions() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let v1 = vec![0.0f32; 128];
|
||||
let v2 = vec![0.0f32; 128];
|
||||
let v3 = vec![0.0f32; 128];
|
||||
let vecs: Vec<&[f32]> = vec![&v1, &v2, &v3];
|
||||
let result = verify_batch_dimensions(&mut env, 128, &vecs);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap().value, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_batch_dimensions_mismatch() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let v1 = vec![0.0f32; 128];
|
||||
let v2 = vec![0.0f32; 64];
|
||||
let vecs: Vec<&[f32]> = vec![&v1, &v2];
|
||||
let result = verify_batch_dimensions(&mut env, 128, &vecs);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mk_hnsw_index_type() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let result = mk_hnsw_index_type(&mut env, 384, "L2");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "hnsw-proofs")]
|
||||
#[test]
|
||||
fn test_verified_insert() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let vec = vec![1.0f32; 128];
|
||||
let result = verified_insert(&mut env, 128, &vec, "L2");
|
||||
assert!(result.is_ok());
|
||||
let op = result.unwrap();
|
||||
assert_eq!(op.value.dim, 128);
|
||||
assert_eq!(op.value.metric, "L2");
|
||||
}
|
||||
|
||||
#[cfg(feature = "hnsw-proofs")]
|
||||
#[test]
|
||||
fn test_verified_insert_dim_mismatch() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let vec = vec![1.0f32; 64];
|
||||
let result = verified_insert(&mut env, 128, &vec, "L2");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "hnsw-proofs")]
|
||||
#[test]
|
||||
fn test_verified_insert_bad_metric() {
|
||||
let mut env = ProofEnvironment::new();
|
||||
let vec = vec![1.0f32; 128];
|
||||
let result = verified_insert(&mut env, 128, &vec, "Manhattan");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user