Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

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

View File

@@ -0,0 +1,586 @@
use serde::{Deserialize, Serialize};
use crate::epoch::{ContainerEpochBudget, EpochController, Phase};
use crate::error::{ContainerError, Result};
use crate::memory::{MemoryConfig, MemorySlab};
use crate::witness::{
CoherenceDecision, ContainerWitnessReceipt, VerificationResult, WitnessChain,
};
/// Top-level container configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerConfig {
/// Memory layout.
pub memory: MemoryConfig,
/// Per-epoch tick budgets.
pub epoch_budget: ContainerEpochBudget,
/// Unique identifier for this container instance.
pub instance_id: u64,
/// Maximum number of witness receipts retained.
pub max_receipts: usize,
}
impl Default for ContainerConfig {
fn default() -> Self {
Self {
memory: MemoryConfig::default(),
epoch_budget: ContainerEpochBudget::default(),
instance_id: 0,
max_receipts: 1024,
}
}
}
/// A graph-structure delta to apply during the ingest phase.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Delta {
EdgeAdd { u: usize, v: usize, weight: f64 },
EdgeRemove { u: usize, v: usize },
WeightUpdate { u: usize, v: usize, new_weight: f64 },
Observation { node: usize, value: f64 },
}
/// Bitmask tracking which pipeline components completed during a tick.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComponentMask(pub u8);
impl ComponentMask {
pub const INGEST: Self = Self(0b0000_0001);
pub const MINCUT: Self = Self(0b0000_0010);
pub const SPECTRAL: Self = Self(0b0000_0100);
pub const EVIDENCE: Self = Self(0b0000_1000);
pub const WITNESS: Self = Self(0b0001_0000);
pub const ALL: Self = Self(0b0001_1111);
/// Returns `true` if all bits in `other` are set in `self`.
pub fn contains(&self, other: Self) -> bool {
self.0 & other.0 == other.0
}
/// Set all bits present in `other`.
pub fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
}
/// Output of a single `tick()` invocation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TickResult {
/// The witness receipt generated for this epoch.
pub receipt: ContainerWitnessReceipt,
/// True if any pipeline phase was skipped due to budget exhaustion.
pub partial: bool,
/// Bitmask of completed components.
pub components_completed: u8,
/// Wall-clock duration in microseconds.
pub tick_time_us: u64,
}
/// Internal graph representation.
struct GraphState {
num_vertices: usize,
num_edges: usize,
edges: Vec<(usize, usize, f64)>,
min_cut_value: f64,
canonical_hash: [u8; 32],
}
impl GraphState {
fn new() -> Self {
Self {
num_vertices: 0,
num_edges: 0,
edges: Vec::new(),
min_cut_value: 0.0,
canonical_hash: [0u8; 32],
}
}
}
/// Internal spectral analysis state.
struct SpectralState {
scs: f64,
fiedler: f64,
gap: f64,
}
impl SpectralState {
fn new() -> Self {
Self {
scs: 0.0,
fiedler: 0.0,
gap: 0.0,
}
}
}
/// Internal evidence accumulation state.
struct EvidenceState {
observations: Vec<f64>,
accumulated_evidence: f64,
threshold: f64,
}
impl EvidenceState {
fn new() -> Self {
Self {
observations: Vec::new(),
accumulated_evidence: 0.0,
threshold: 1.0,
}
}
}
/// Serializable snapshot of the container state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerSnapshot {
pub epoch: u64,
pub config: ContainerConfig,
pub graph_edges: Vec<(usize, usize, f64)>,
pub spectral_scs: f64,
pub evidence_accumulated: f64,
}
/// A sealed cognitive container that orchestrates ingest, min-cut, spectral,
/// evidence, and witness phases within a memory slab and epoch budget.
pub struct CognitiveContainer {
config: ContainerConfig,
#[allow(dead_code)]
slab: MemorySlab,
epoch: EpochController,
witness: WitnessChain,
graph: GraphState,
spectral: SpectralState,
evidence: EvidenceState,
initialized: bool,
}
impl CognitiveContainer {
/// Create and initialize a new container.
pub fn new(config: ContainerConfig) -> Result<Self> {
let slab = MemorySlab::new(config.memory.clone())?;
let epoch = EpochController::new(config.epoch_budget.clone());
let witness = WitnessChain::new(config.max_receipts);
Ok(Self {
config,
slab,
epoch,
witness,
graph: GraphState::new(),
spectral: SpectralState::new(),
evidence: EvidenceState::new(),
initialized: true,
})
}
/// Execute one full epoch: ingest deltas, recompute min-cut, update spectral
/// metrics, accumulate evidence, and produce a witness receipt.
pub fn tick(&mut self, deltas: &[Delta]) -> Result<TickResult> {
if !self.initialized {
return Err(ContainerError::NotInitialized);
}
let start = std::time::Instant::now();
self.epoch.reset();
let mut completed = ComponentMask(0);
// Phase 1: Ingest
if self.epoch.try_budget(Phase::Ingest) {
for delta in deltas {
self.apply_delta(delta);
}
self.epoch.consume(deltas.len().max(1) as u64);
completed.insert(ComponentMask::INGEST);
}
// Phase 2: Min-cut
if self.epoch.try_budget(Phase::MinCut) {
self.recompute_mincut();
self.epoch.consume(self.graph.num_edges.max(1) as u64);
completed.insert(ComponentMask::MINCUT);
}
// Phase 3: Spectral
if self.epoch.try_budget(Phase::Spectral) {
self.update_spectral();
self.epoch.consume(self.graph.num_vertices.max(1) as u64);
completed.insert(ComponentMask::SPECTRAL);
}
// Phase 4: Evidence
if self.epoch.try_budget(Phase::Evidence) {
self.accumulate_evidence();
self.epoch
.consume(self.evidence.observations.len().max(1) as u64);
completed.insert(ComponentMask::EVIDENCE);
}
// Phase 5: Witness
let decision = self.make_decision();
let input_bytes = self.serialize_deltas(deltas);
let mincut_bytes = self.graph.min_cut_value.to_le_bytes();
let evidence_bytes = self.evidence.accumulated_evidence.to_le_bytes();
let receipt = self.witness.generate_receipt(
&input_bytes,
&mincut_bytes,
self.spectral.scs,
&evidence_bytes,
decision,
);
completed.insert(ComponentMask::WITNESS);
Ok(TickResult {
receipt,
partial: completed.0 != ComponentMask::ALL.0,
components_completed: completed.0,
tick_time_us: start.elapsed().as_micros() as u64,
})
}
/// Reference to the container configuration.
pub fn config(&self) -> &ContainerConfig {
&self.config
}
/// Current epoch counter (next epoch to be generated).
pub fn current_epoch(&self) -> u64 {
self.witness.current_epoch()
}
/// Slice of all retained witness receipts.
pub fn receipt_chain(&self) -> &[ContainerWitnessReceipt] {
self.witness.receipt_chain()
}
/// Verify the integrity of the internal witness chain.
pub fn verify_chain(&self) -> VerificationResult {
WitnessChain::verify_chain(self.witness.receipt_chain())
}
/// Produce a serializable snapshot of the current container state.
pub fn snapshot(&self) -> ContainerSnapshot {
ContainerSnapshot {
epoch: self.witness.current_epoch(),
config: self.config.clone(),
graph_edges: self.graph.edges.clone(),
spectral_scs: self.spectral.scs,
evidence_accumulated: self.evidence.accumulated_evidence,
}
}
// ---- Private helpers ----
fn apply_delta(&mut self, delta: &Delta) {
match delta {
Delta::EdgeAdd { u, v, weight } => {
self.graph.edges.push((*u, *v, *weight));
self.graph.num_edges += 1;
let max_node = (*u).max(*v) + 1;
if max_node > self.graph.num_vertices {
self.graph.num_vertices = max_node;
}
}
Delta::EdgeRemove { u, v } => {
self.graph.edges.retain(|(a, b, _)| !(*a == *u && *b == *v));
self.graph.num_edges = self.graph.edges.len();
}
Delta::WeightUpdate { u, v, new_weight } => {
for edge in &mut self.graph.edges {
if edge.0 == *u && edge.1 == *v {
edge.2 = *new_weight;
}
}
}
Delta::Observation { value, .. } => {
self.evidence.observations.push(*value);
}
}
}
/// Simplified Stoer-Wagner-style min-cut: find the minimum total weight
/// among all vertex partitions. For small graphs this uses the minimum
/// weighted vertex degree as a fast approximation.
fn recompute_mincut(&mut self) {
if self.graph.edges.is_empty() {
self.graph.min_cut_value = 0.0;
self.graph.canonical_hash = [0u8; 32];
return;
}
// Approximate min-cut via minimum weighted degree.
let n = self.graph.num_vertices;
let mut degree = vec![0.0f64; n];
for &(u, v, w) in &self.graph.edges {
if u < n {
degree[u] += w;
}
if v < n {
degree[v] += w;
}
}
self.graph.min_cut_value = degree
.iter()
.copied()
.filter(|&d| d > 0.0)
.fold(f64::MAX, f64::min);
if self.graph.min_cut_value == f64::MAX {
self.graph.min_cut_value = 0.0;
}
// Canonical hash: hash sorted edges.
let mut sorted = self.graph.edges.clone();
sorted.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
let bytes: Vec<u8> = sorted
.iter()
.flat_map(|(u, v, w)| {
let mut b = Vec::with_capacity(24);
b.extend_from_slice(&u.to_le_bytes());
b.extend_from_slice(&v.to_le_bytes());
b.extend_from_slice(&w.to_le_bytes());
b
})
.collect();
self.graph.canonical_hash = crate::witness::deterministic_hash_public(&bytes);
}
/// Simplified spectral metrics: SCS is the ratio of min-cut to total weight.
fn update_spectral(&mut self) {
let total_weight: f64 = self.graph.edges.iter().map(|e| e.2).sum();
if total_weight > 0.0 {
self.spectral.scs = self.graph.min_cut_value / total_weight;
self.spectral.fiedler = self.spectral.scs;
self.spectral.gap = 1.0 - self.spectral.scs;
} else {
self.spectral.scs = 0.0;
self.spectral.fiedler = 0.0;
self.spectral.gap = 0.0;
}
}
/// Simple sequential probability ratio test (SPRT) style accumulation.
fn accumulate_evidence(&mut self) {
if self.evidence.observations.is_empty() {
return;
}
let mean: f64 = self.evidence.observations.iter().sum::<f64>()
/ self.evidence.observations.len() as f64;
self.evidence.accumulated_evidence += mean.abs();
}
/// Decision logic based on spectral coherence and accumulated evidence.
fn make_decision(&self) -> CoherenceDecision {
if self.graph.edges.is_empty() {
return CoherenceDecision::Inconclusive;
}
if self.spectral.scs >= 0.5 && self.evidence.accumulated_evidence < self.evidence.threshold
{
return CoherenceDecision::Pass;
}
if self.spectral.scs < 0.2 {
let severity = ((1.0 - self.spectral.scs) * 10.0).min(255.0) as u8;
return CoherenceDecision::Fail { severity };
}
CoherenceDecision::Inconclusive
}
fn serialize_deltas(&self, deltas: &[Delta]) -> Vec<u8> {
serde_json::to_vec(deltas).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_container() -> CognitiveContainer {
CognitiveContainer::new(ContainerConfig::default()).unwrap()
}
#[test]
fn test_container_lifecycle() {
let mut container = default_container();
assert_eq!(container.current_epoch(), 0);
let result = container.tick(&[]).unwrap();
assert_eq!(result.receipt.epoch, 0);
assert_eq!(container.current_epoch(), 1);
match container.verify_chain() {
VerificationResult::Valid { chain_length, .. } => {
assert_eq!(chain_length, 1);
}
other => panic!("Expected Valid, got {other:?}"),
}
}
#[test]
fn test_container_tick_with_deltas() {
let mut container = default_container();
let deltas = vec![
Delta::EdgeAdd {
u: 0,
v: 1,
weight: 1.0,
},
Delta::EdgeAdd {
u: 1,
v: 2,
weight: 2.0,
},
Delta::EdgeAdd {
u: 2,
v: 0,
weight: 1.5,
},
Delta::Observation {
node: 0,
value: 0.8,
},
];
let result = container.tick(&deltas).unwrap();
assert!(!result.partial);
assert_eq!(result.components_completed, ComponentMask::ALL.0);
// Graph should reflect the edges.
let snap = container.snapshot();
assert_eq!(snap.graph_edges.len(), 3);
assert!(snap.spectral_scs > 0.0);
}
#[test]
fn test_container_snapshot_restore() {
let mut container = default_container();
container
.tick(&[Delta::EdgeAdd {
u: 0,
v: 1,
weight: 3.0,
}])
.unwrap();
let snap = container.snapshot();
let json = serde_json::to_string(&snap).expect("serialize snapshot");
let restored: ContainerSnapshot =
serde_json::from_str(&json).expect("deserialize snapshot");
assert_eq!(restored.epoch, snap.epoch);
assert_eq!(restored.graph_edges.len(), snap.graph_edges.len());
assert!((restored.spectral_scs - snap.spectral_scs).abs() < f64::EPSILON);
}
#[test]
fn test_container_decision_logic() {
let mut container = default_container();
// Empty graph => Inconclusive
let r = container.tick(&[]).unwrap();
assert_eq!(r.receipt.decision, CoherenceDecision::Inconclusive);
// Single edge: min-cut/total = 1.0 (high scs), no evidence => Pass
let r = container
.tick(&[Delta::EdgeAdd {
u: 0,
v: 1,
weight: 5.0,
}])
.unwrap();
assert_eq!(r.receipt.decision, CoherenceDecision::Pass);
}
#[test]
fn test_container_multiple_epochs() {
let mut container = default_container();
for i in 0..10 {
container
.tick(&[Delta::EdgeAdd {
u: i,
v: i + 1,
weight: 1.0,
}])
.unwrap();
}
assert_eq!(container.current_epoch(), 10);
match container.verify_chain() {
VerificationResult::Valid {
chain_length,
first_epoch,
last_epoch,
} => {
assert_eq!(chain_length, 10);
assert_eq!(first_epoch, 0);
assert_eq!(last_epoch, 9);
}
other => panic!("Expected Valid, got {other:?}"),
}
}
#[test]
fn test_container_edge_remove() {
let mut container = default_container();
container
.tick(&[
Delta::EdgeAdd {
u: 0,
v: 1,
weight: 1.0,
},
Delta::EdgeAdd {
u: 1,
v: 2,
weight: 2.0,
},
])
.unwrap();
container.tick(&[Delta::EdgeRemove { u: 0, v: 1 }]).unwrap();
let snap = container.snapshot();
assert_eq!(snap.graph_edges.len(), 1);
assert_eq!(snap.graph_edges[0], (1, 2, 2.0));
}
#[test]
fn test_container_weight_update() {
let mut container = default_container();
container
.tick(&[Delta::EdgeAdd {
u: 0,
v: 1,
weight: 1.0,
}])
.unwrap();
container
.tick(&[Delta::WeightUpdate {
u: 0,
v: 1,
new_weight: 5.0,
}])
.unwrap();
let snap = container.snapshot();
assert_eq!(snap.graph_edges[0].2, 5.0);
}
#[test]
fn test_component_mask() {
let mut mask = ComponentMask(0);
assert!(!mask.contains(ComponentMask::INGEST));
mask.insert(ComponentMask::INGEST);
assert!(mask.contains(ComponentMask::INGEST));
assert!(!mask.contains(ComponentMask::MINCUT));
mask.insert(ComponentMask::MINCUT);
assert!(mask.contains(ComponentMask::INGEST));
assert!(mask.contains(ComponentMask::MINCUT));
assert!(!mask.contains(ComponentMask::ALL));
}
}

View File

@@ -0,0 +1,187 @@
use serde::{Deserialize, Serialize};
/// Per-phase tick budgets for a single container epoch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerEpochBudget {
/// Maximum total ticks for the entire epoch.
pub total: u64,
/// Ticks allocated to the ingest phase.
pub ingest: u64,
/// Ticks allocated to the min-cut phase.
pub mincut: u64,
/// Ticks allocated to the spectral analysis phase.
pub spectral: u64,
/// Ticks allocated to the evidence accumulation phase.
pub evidence: u64,
/// Ticks allocated to the witness receipt phase.
pub witness: u64,
}
impl Default for ContainerEpochBudget {
fn default() -> Self {
Self {
total: 10_000,
ingest: 2_000,
mincut: 3_000,
spectral: 2_000,
evidence: 2_000,
witness: 1_000,
}
}
}
/// Processing phases within a single epoch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Ingest,
MinCut,
Spectral,
Evidence,
Witness,
}
/// Controls compute-tick budgeting across phases within an epoch.
pub struct EpochController {
budget: ContainerEpochBudget,
ticks_used: u64,
phase_used: [u64; 5],
current_phase: Phase,
}
impl EpochController {
/// Create a new controller with the given budget.
pub fn new(budget: ContainerEpochBudget) -> Self {
Self {
budget,
ticks_used: 0,
phase_used: [0; 5],
current_phase: Phase::Ingest,
}
}
/// Check whether `phase` still has budget remaining.
/// If yes, sets the current phase and returns `true`.
pub fn try_budget(&mut self, phase: Phase) -> bool {
let idx = Self::phase_index(phase);
let limit = self.phase_budget(phase);
if self.phase_used[idx] < limit && self.ticks_used < self.budget.total {
self.current_phase = phase;
true
} else {
false
}
}
/// Consume `ticks` from both the total budget and the current phase budget.
pub fn consume(&mut self, ticks: u64) {
let idx = Self::phase_index(self.current_phase);
self.ticks_used += ticks;
self.phase_used[idx] += ticks;
}
/// Ticks remaining in the total epoch budget.
pub fn remaining(&self) -> u64 {
self.budget.total.saturating_sub(self.ticks_used)
}
/// Reset the controller for a new epoch.
pub fn reset(&mut self) {
self.ticks_used = 0;
self.phase_used = [0; 5];
self.current_phase = Phase::Ingest;
}
/// Total tick budget allocated to `phase`.
pub fn phase_budget(&self, phase: Phase) -> u64 {
match phase {
Phase::Ingest => self.budget.ingest,
Phase::MinCut => self.budget.mincut,
Phase::Spectral => self.budget.spectral,
Phase::Evidence => self.budget.evidence,
Phase::Witness => self.budget.witness,
}
}
/// Ticks consumed so far by `phase`.
pub fn phase_used(&self, phase: Phase) -> u64 {
self.phase_used[Self::phase_index(phase)]
}
fn phase_index(phase: Phase) -> usize {
match phase {
Phase::Ingest => 0,
Phase::MinCut => 1,
Phase::Spectral => 2,
Phase::Evidence => 3,
Phase::Witness => 4,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_epoch_budgeting() {
let budget = ContainerEpochBudget {
total: 100,
ingest: 30,
mincut: 25,
spectral: 20,
evidence: 15,
witness: 10,
};
let mut ctl = EpochController::new(budget);
assert!(ctl.try_budget(Phase::Ingest));
ctl.consume(30);
assert_eq!(ctl.phase_used(Phase::Ingest), 30);
// Phase is now exhausted.
assert!(!ctl.try_budget(Phase::Ingest));
assert_eq!(ctl.remaining(), 70);
assert!(ctl.try_budget(Phase::MinCut));
ctl.consume(25);
assert!(!ctl.try_budget(Phase::MinCut));
assert_eq!(ctl.remaining(), 45);
assert!(ctl.try_budget(Phase::Spectral));
ctl.consume(20);
assert!(ctl.try_budget(Phase::Evidence));
ctl.consume(15);
assert!(ctl.try_budget(Phase::Witness));
ctl.consume(10);
assert_eq!(ctl.remaining(), 0);
}
#[test]
fn test_epoch_reset() {
let mut ctl = EpochController::new(ContainerEpochBudget::default());
assert!(ctl.try_budget(Phase::Ingest));
ctl.consume(500);
assert_eq!(ctl.phase_used(Phase::Ingest), 500);
ctl.reset();
assert_eq!(ctl.phase_used(Phase::Ingest), 0);
assert_eq!(ctl.remaining(), 10_000);
}
#[test]
fn test_total_budget_caps_phase() {
let budget = ContainerEpochBudget {
total: 10,
ingest: 100,
mincut: 100,
spectral: 100,
evidence: 100,
witness: 100,
};
let mut ctl = EpochController::new(budget);
assert!(ctl.try_budget(Phase::Ingest));
ctl.consume(10);
// Total is exhausted even though phase still has room.
assert!(!ctl.try_budget(Phase::MinCut));
}
}

View File

@@ -0,0 +1,66 @@
use thiserror::Error;
/// Errors that can occur during cognitive container operations.
#[derive(Error, Debug)]
pub enum ContainerError {
#[error("Memory allocation failed: requested {requested} bytes, available {available}")]
AllocationFailed { requested: usize, available: usize },
#[error("Epoch budget exhausted: used {used} of {budget} ticks")]
EpochExhausted { used: u64, budget: u64 },
#[error("Witness chain broken at epoch {epoch}")]
BrokenChain { epoch: u64 },
#[error("Invalid configuration: {reason}")]
InvalidConfig { reason: String },
#[error("Container not initialized")]
NotInitialized,
#[error("Slab overflow: component {component} exceeded budget")]
SlabOverflow { component: String },
}
/// Convenience alias for container results.
pub type Result<T> = std::result::Result<T, ContainerError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = ContainerError::AllocationFailed {
requested: 1024,
available: 512,
};
assert!(err.to_string().contains("1024"));
assert!(err.to_string().contains("512"));
}
#[test]
fn test_error_variants() {
let err = ContainerError::EpochExhausted {
used: 100,
budget: 50,
};
assert!(err.to_string().contains("100"));
let err = ContainerError::BrokenChain { epoch: 7 };
assert!(err.to_string().contains("7"));
let err = ContainerError::InvalidConfig {
reason: "bad value".into(),
};
assert!(err.to_string().contains("bad value"));
let err = ContainerError::NotInitialized;
assert!(err.to_string().contains("not initialized"));
let err = ContainerError::SlabOverflow {
component: "graph".into(),
};
assert!(err.to_string().contains("graph"));
}
}

View File

@@ -0,0 +1,19 @@
//! Verifiable WASM cognitive container with canonical witness chains.
//!
//! This crate composes cognitive primitives (graph ingest, min-cut, spectral
//! analysis, evidence accumulation) into a sealed container that produces a
//! tamper-evident witness chain linking every epoch to its predecessor.
pub mod container;
pub mod epoch;
pub mod error;
pub mod memory;
pub mod witness;
pub use container::{
CognitiveContainer, ComponentMask, ContainerConfig, ContainerSnapshot, Delta, TickResult,
};
pub use epoch::{ContainerEpochBudget, EpochController, Phase};
pub use error::{ContainerError, Result};
pub use memory::{Arena, MemoryConfig, MemorySlab};
pub use witness::{CoherenceDecision, ContainerWitnessReceipt, VerificationResult, WitnessChain};

View File

@@ -0,0 +1,213 @@
use serde::{Deserialize, Serialize};
use crate::error::{ContainerError, Result};
/// Configuration for memory slab layout.
///
/// Each budget defines the byte size of a sub-arena within the slab.
/// The total slab size is the sum of all budgets.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
/// Total slab size in bytes (must equal sum of budgets).
pub slab_size: usize,
/// Bytes reserved for graph adjacency data.
pub graph_budget: usize,
/// Bytes reserved for feature / embedding storage.
pub feature_budget: usize,
/// Bytes reserved for solver scratch space.
pub solver_budget: usize,
/// Bytes reserved for witness receipt storage.
pub witness_budget: usize,
/// Bytes reserved for evidence accumulation.
pub evidence_budget: usize,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
slab_size: 4 * 1024 * 1024, // 4 MB total
graph_budget: 1024 * 1024, // 1 MB
feature_budget: 1024 * 1024, // 1 MB
solver_budget: 512 * 1024, // 512 KB
witness_budget: 512 * 1024, // 512 KB
evidence_budget: 1024 * 1024, // 1 MB
}
}
}
impl MemoryConfig {
/// Validate that budget components sum to `slab_size`.
pub fn validate(&self) -> Result<()> {
let sum = self.graph_budget
+ self.feature_budget
+ self.solver_budget
+ self.witness_budget
+ self.evidence_budget;
if sum != self.slab_size {
return Err(ContainerError::InvalidConfig {
reason: format!(
"budget sum ({sum}) does not equal slab_size ({})",
self.slab_size
),
});
}
Ok(())
}
}
/// A contiguous block of memory backing all container arenas.
pub struct MemorySlab {
data: Vec<u8>,
config: MemoryConfig,
}
impl MemorySlab {
/// Allocate a new slab according to `config`.
pub fn new(config: MemoryConfig) -> Result<Self> {
config.validate()?;
Ok(Self {
data: vec![0u8; config.slab_size],
config,
})
}
/// Total slab size in bytes.
pub fn total_size(&self) -> usize {
self.data.len()
}
/// Immutable view of the raw slab bytes.
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// Reference to the underlying config.
pub fn config(&self) -> &MemoryConfig {
&self.config
}
}
/// A bump-allocator arena within a `MemorySlab`.
///
/// `base_offset` is the starting position inside the slab.
/// Allocations grow upward; `reset()` reclaims all space.
pub struct Arena {
base_offset: usize,
size: usize,
offset: usize,
}
impl Arena {
/// Create a new arena starting at `base_offset` with the given `size`.
pub fn new(base_offset: usize, size: usize) -> Self {
Self {
base_offset,
size,
offset: 0,
}
}
/// Bump-allocate `size` bytes with the given `align`ment.
///
/// Returns the absolute offset within the slab on success.
pub fn alloc(&mut self, size: usize, align: usize) -> Result<usize> {
let align = align.max(1);
let current = self.base_offset + self.offset;
let aligned = (current + align - 1) & !(align - 1);
let padding = aligned - current;
let total = padding + size;
if self.offset + total > self.size {
return Err(ContainerError::AllocationFailed {
requested: size,
available: self.remaining(),
});
}
self.offset += total;
Ok(aligned)
}
/// Reset the arena, reclaiming all allocated space.
pub fn reset(&mut self) {
self.offset = 0;
}
/// Number of bytes currently consumed (including alignment padding).
pub fn used(&self) -> usize {
self.offset
}
/// Number of bytes still available.
pub fn remaining(&self) -> usize {
self.size.saturating_sub(self.offset)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_slab_creation() {
let config = MemoryConfig::default();
let slab = MemorySlab::new(config).expect("slab should allocate");
assert_eq!(slab.total_size(), 4 * 1024 * 1024);
assert_eq!(slab.as_bytes().len(), slab.total_size());
// Fresh slab is zero-filled.
assert!(slab.as_bytes().iter().all(|&b| b == 0));
}
#[test]
fn test_memory_config_validation_fails_on_mismatch() {
let config = MemoryConfig {
slab_size: 100,
graph_budget: 10,
feature_budget: 10,
solver_budget: 10,
witness_budget: 10,
evidence_budget: 10,
};
assert!(MemorySlab::new(config).is_err());
}
#[test]
fn test_arena_allocation() {
let mut arena = Arena::new(0, 256);
assert_eq!(arena.remaining(), 256);
assert_eq!(arena.used(), 0);
let off1 = arena.alloc(64, 8).expect("alloc 64");
assert_eq!(off1, 0); // base 0, align 8 => 0
assert_eq!(arena.used(), 64);
assert_eq!(arena.remaining(), 192);
let off2 = arena.alloc(32, 16).expect("alloc 32");
// 64 already used, align to 16 => 64 (already aligned)
assert_eq!(off2, 64);
assert_eq!(arena.used(), 96);
arena.reset();
assert_eq!(arena.used(), 0);
assert_eq!(arena.remaining(), 256);
}
#[test]
fn test_arena_allocation_overflow() {
let mut arena = Arena::new(0, 64);
assert!(arena.alloc(128, 1).is_err());
}
#[test]
fn test_arena_alignment_padding() {
let mut arena = Arena::new(0, 256);
// Allocate 1 byte at alignment 1
let _ = arena.alloc(1, 1).unwrap();
assert_eq!(arena.used(), 1);
// Next allocation with align 16: from offset 1, aligned to 16 => 16
let off = arena.alloc(8, 16).unwrap();
assert_eq!(off, 16);
// used = 1 (first) + 15 (padding) + 8 = 24
assert_eq!(arena.used(), 24);
}
}

View File

@@ -0,0 +1,353 @@
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
/// Coherence decision emitted after each epoch.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CoherenceDecision {
Pass,
Fail { severity: u8 },
Inconclusive,
}
/// A single witness receipt linking an epoch to its predecessor via hashes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerWitnessReceipt {
/// Epoch number this receipt covers.
pub epoch: u64,
/// Hash of the previous receipt (zero for the genesis receipt).
pub prev_hash: [u8; 32],
/// Hash of the input deltas for this epoch.
pub input_hash: [u8; 32],
/// Hash of the min-cut result.
pub mincut_hash: [u8; 32],
/// Spectral coherence score in fixed-point 32.32 representation.
pub spectral_scs: u64,
/// Hash of the evidence accumulation state.
pub evidence_hash: [u8; 32],
/// Decision for this epoch.
pub decision: CoherenceDecision,
/// Hash of this receipt (covers all fields above).
pub receipt_hash: [u8; 32],
}
impl ContainerWitnessReceipt {
/// Serialize all fields except `receipt_hash` into a byte vector for hashing.
pub fn signable_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
buf.extend_from_slice(&self.epoch.to_le_bytes());
buf.extend_from_slice(&self.prev_hash);
buf.extend_from_slice(&self.input_hash);
buf.extend_from_slice(&self.mincut_hash);
buf.extend_from_slice(&self.spectral_scs.to_le_bytes());
buf.extend_from_slice(&self.evidence_hash);
match self.decision {
CoherenceDecision::Pass => buf.push(0),
CoherenceDecision::Fail { severity } => {
buf.push(1);
buf.push(severity);
}
CoherenceDecision::Inconclusive => buf.push(2),
}
buf
}
/// Compute and set `receipt_hash` from the signable portion of this receipt.
pub fn compute_hash(&mut self) {
self.receipt_hash = deterministic_hash(&self.signable_bytes());
}
}
/// Result of verifying a witness chain.
#[derive(Debug, Clone)]
pub enum VerificationResult {
/// Chain is valid.
Valid {
chain_length: usize,
first_epoch: u64,
last_epoch: u64,
},
/// Chain is empty (no receipts).
Empty,
/// A receipt's `prev_hash` does not match the preceding receipt's `receipt_hash`.
BrokenChain { epoch: u64 },
/// Epoch numbers are not strictly monotonic.
EpochGap { expected: u64, got: u64 },
}
/// Append-only chain of witness receipts with hash linking.
pub struct WitnessChain {
current_epoch: u64,
prev_hash: [u8; 32],
receipts: Vec<ContainerWitnessReceipt>,
max_receipts: usize,
}
impl WitnessChain {
/// Create a new empty chain that retains at most `max_receipts` entries.
pub fn new(max_receipts: usize) -> Self {
Self {
current_epoch: 0,
prev_hash: [0u8; 32],
receipts: Vec::with_capacity(max_receipts.min(1024)),
max_receipts,
}
}
/// Generate a new receipt, append it to the chain, and return a clone.
pub fn generate_receipt(
&mut self,
input_deltas: &[u8],
mincut_data: &[u8],
spectral_scs: f64,
evidence_data: &[u8],
decision: CoherenceDecision,
) -> ContainerWitnessReceipt {
let scs_fixed = f64_to_fixed_32_32(spectral_scs);
let mut receipt = ContainerWitnessReceipt {
epoch: self.current_epoch,
prev_hash: self.prev_hash,
input_hash: deterministic_hash(input_deltas),
mincut_hash: deterministic_hash(mincut_data),
spectral_scs: scs_fixed,
evidence_hash: deterministic_hash(evidence_data),
decision,
receipt_hash: [0u8; 32],
};
receipt.compute_hash();
self.prev_hash = receipt.receipt_hash;
self.current_epoch += 1;
// Ring-buffer behavior: drop oldest when full.
if self.receipts.len() >= self.max_receipts {
self.receipts.remove(0);
}
self.receipts.push(receipt.clone());
receipt
}
/// Current epoch counter (next epoch to be generated).
pub fn current_epoch(&self) -> u64 {
self.current_epoch
}
/// Most recent receipt, if any.
pub fn latest_receipt(&self) -> Option<&ContainerWitnessReceipt> {
self.receipts.last()
}
/// Slice of all retained receipts.
pub fn receipt_chain(&self) -> &[ContainerWitnessReceipt] {
&self.receipts
}
/// Verify hash-chain integrity and epoch monotonicity for a slice of receipts.
pub fn verify_chain(receipts: &[ContainerWitnessReceipt]) -> VerificationResult {
if receipts.is_empty() {
return VerificationResult::Empty;
}
// Verify each receipt's self-hash.
for r in receipts {
let expected = deterministic_hash(&r.signable_bytes());
if expected != r.receipt_hash {
return VerificationResult::BrokenChain { epoch: r.epoch };
}
}
// Verify prev_hash linkage and epoch ordering.
for i in 1..receipts.len() {
let prev = &receipts[i - 1];
let curr = &receipts[i];
if curr.prev_hash != prev.receipt_hash {
return VerificationResult::BrokenChain { epoch: curr.epoch };
}
let expected_epoch = prev.epoch + 1;
if curr.epoch != expected_epoch {
return VerificationResult::EpochGap {
expected: expected_epoch,
got: curr.epoch,
};
}
}
VerificationResult::Valid {
chain_length: receipts.len(),
first_epoch: receipts[0].epoch,
last_epoch: receipts[receipts.len() - 1].epoch,
}
}
}
/// Convert an f64 to a 32.32 fixed-point representation.
fn f64_to_fixed_32_32(value: f64) -> u64 {
let clamped = value.clamp(0.0, (u32::MAX as f64) + 0.999_999_999);
(clamped * (1u64 << 32) as f64) as u64
}
/// Public wrapper for deterministic hashing, used by other modules.
pub fn deterministic_hash_public(data: &[u8]) -> [u8; 32] {
deterministic_hash(data)
}
/// Deterministic hash producing 32 bytes.
///
/// Uses `std::hash::DefaultHasher` (SipHash-2-4) run with four different seeds
/// to fill 32 bytes. This is NOT cryptographic but fully deterministic across
/// runs on the same platform.
fn deterministic_hash(data: &[u8]) -> [u8; 32] {
let mut result = [0u8; 32];
for i in 0u64..4 {
let mut hasher = DefaultHasher::new();
i.hash(&mut hasher);
data.hash(&mut hasher);
let h = hasher.finish();
let offset = (i as usize) * 8;
result[offset..offset + 8].copy_from_slice(&h.to_le_bytes());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deterministic_hash_consistency() {
let a = deterministic_hash(b"hello world");
let b = deterministic_hash(b"hello world");
assert_eq!(a, b);
}
#[test]
fn test_deterministic_hash_differs_for_different_inputs() {
let a = deterministic_hash(b"alpha");
let b = deterministic_hash(b"beta");
assert_ne!(a, b);
}
#[test]
fn test_witness_chain_integrity() {
let mut chain = WitnessChain::new(100);
for i in 0..5 {
let data = format!("epoch-{i}");
chain.generate_receipt(
data.as_bytes(),
b"mincut",
0.95,
b"evidence",
CoherenceDecision::Pass,
);
}
assert_eq!(chain.current_epoch(), 5);
match WitnessChain::verify_chain(chain.receipt_chain()) {
VerificationResult::Valid {
chain_length,
first_epoch,
last_epoch,
} => {
assert_eq!(chain_length, 5);
assert_eq!(first_epoch, 0);
assert_eq!(last_epoch, 4);
}
other => panic!("Expected Valid, got {other:?}"),
}
}
#[test]
fn test_witness_chain_epoch_monotonicity() {
let mut chain = WitnessChain::new(100);
for _ in 0..3 {
chain.generate_receipt(
b"input",
b"mincut",
1.0,
b"evidence",
CoherenceDecision::Pass,
);
}
let receipts = chain.receipt_chain();
for i in 1..receipts.len() {
assert_eq!(receipts[i].epoch, receipts[i - 1].epoch + 1);
}
}
#[test]
fn test_verification_detects_tampering() {
let mut chain = WitnessChain::new(100);
for _ in 0..3 {
chain.generate_receipt(
b"input",
b"mincut",
0.5,
b"evidence",
CoherenceDecision::Inconclusive,
);
}
// Tamper with the second receipt's input_hash.
let mut tampered: Vec<ContainerWitnessReceipt> = chain.receipt_chain().to_vec();
tampered[1].input_hash[0] ^= 0xFF;
match WitnessChain::verify_chain(&tampered) {
VerificationResult::BrokenChain { epoch } => {
assert_eq!(epoch, 1);
}
other => panic!("Expected BrokenChain, got {other:?}"),
}
}
#[test]
fn test_empty_chain_verification() {
let receipts: Vec<ContainerWitnessReceipt> = vec![];
match WitnessChain::verify_chain(&receipts) {
VerificationResult::Empty => {}
other => panic!("Expected Empty, got {other:?}"),
}
}
#[test]
fn test_ring_buffer_eviction() {
let mut chain = WitnessChain::new(3);
for _ in 0..5 {
chain.generate_receipt(b"data", b"mc", 0.1, b"ev", CoherenceDecision::Pass);
}
assert_eq!(chain.receipt_chain().len(), 3);
assert_eq!(chain.receipt_chain()[0].epoch, 2);
assert_eq!(chain.receipt_chain()[2].epoch, 4);
}
#[test]
fn test_f64_to_fixed() {
assert_eq!(f64_to_fixed_32_32(1.0), 1u64 << 32);
assert_eq!(f64_to_fixed_32_32(0.0), 0);
let half = f64_to_fixed_32_32(0.5);
assert_eq!(half, 1u64 << 31);
}
#[test]
fn test_signable_bytes_determinism() {
let receipt = ContainerWitnessReceipt {
epoch: 42,
prev_hash: [1u8; 32],
input_hash: [2u8; 32],
mincut_hash: [3u8; 32],
spectral_scs: 100,
evidence_hash: [4u8; 32],
decision: CoherenceDecision::Fail { severity: 7 },
receipt_hash: [0u8; 32],
};
let a = receipt.signable_bytes();
let b = receipt.signable_bytes();
assert_eq!(a, b);
}
}