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 { 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, 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 = 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 = 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); } }