Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
544
crates/cognitum-gate-tilezero/tests/receipt_tests.rs
Normal file
544
crates/cognitum-gate-tilezero/tests/receipt_tests.rs
Normal file
@@ -0,0 +1,544 @@
|
||||
//! Comprehensive tests for witness receipts and hash chain integrity
|
||||
//!
|
||||
//! Tests cover:
|
||||
//! - Receipt creation and hashing
|
||||
//! - Hash chain verification
|
||||
//! - Tamper detection
|
||||
//! - Security tests (chain manipulation, replay attacks)
|
||||
|
||||
use cognitum_gate_tilezero::permit::PermitToken;
|
||||
use cognitum_gate_tilezero::receipt::{
|
||||
EvidentialWitness, PredictiveWitness, ReceiptLog, StructuralWitness, TimestampProof,
|
||||
WitnessReceipt, WitnessSummary,
|
||||
};
|
||||
use cognitum_gate_tilezero::GateDecision;
|
||||
|
||||
fn create_test_token(sequence: u64, action_id: &str) -> PermitToken {
|
||||
PermitToken {
|
||||
decision: GateDecision::Permit,
|
||||
action_id: action_id.to_string(),
|
||||
timestamp: 1000000000 + sequence * 1000,
|
||||
ttl_ns: 60_000_000_000,
|
||||
witness_hash: [0u8; 32],
|
||||
sequence,
|
||||
signature: [0u8; 64],
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_summary() -> WitnessSummary {
|
||||
WitnessSummary {
|
||||
structural: StructuralWitness {
|
||||
cut_value: 10.0,
|
||||
partition: "stable".to_string(),
|
||||
critical_edges: 5,
|
||||
boundary: vec!["edge1".to_string(), "edge2".to_string()],
|
||||
},
|
||||
predictive: PredictiveWitness {
|
||||
set_size: 8,
|
||||
coverage: 0.9,
|
||||
},
|
||||
evidential: EvidentialWitness {
|
||||
e_value: 150.0,
|
||||
verdict: "accept".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_receipt(sequence: u64, previous_hash: [u8; 32]) -> WitnessReceipt {
|
||||
WitnessReceipt {
|
||||
sequence,
|
||||
token: create_test_token(sequence, &format!("action-{}", sequence)),
|
||||
previous_hash,
|
||||
witness_summary: create_test_summary(),
|
||||
timestamp_proof: TimestampProof {
|
||||
timestamp: 1000000000 + sequence * 1000,
|
||||
previous_receipt_hash: previous_hash,
|
||||
merkle_root: [0u8; 32],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod witness_summary {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_summary() {
|
||||
let summary = WitnessSummary::empty();
|
||||
assert_eq!(summary.structural.cut_value, 0.0);
|
||||
assert_eq!(summary.predictive.set_size, 0);
|
||||
assert_eq!(summary.evidential.e_value, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary_hash_deterministic() {
|
||||
let summary = create_test_summary();
|
||||
let hash1 = summary.hash();
|
||||
let hash2 = summary.hash();
|
||||
assert_eq!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary_hash_unique() {
|
||||
let summary1 = create_test_summary();
|
||||
let mut summary2 = create_test_summary();
|
||||
summary2.structural.cut_value = 20.0;
|
||||
|
||||
assert_ne!(summary1.hash(), summary2.hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary_to_json() {
|
||||
let summary = create_test_summary();
|
||||
let json = summary.to_json();
|
||||
|
||||
assert!(json.is_object());
|
||||
assert!(json["structural"]["cut_value"].is_number());
|
||||
assert!(json["predictive"]["set_size"].is_number());
|
||||
assert!(json["evidential"]["e_value"].is_number());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod receipt_hashing {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_receipt_hash_nonzero() {
|
||||
let receipt = create_test_receipt(0, [0u8; 32]);
|
||||
let hash = receipt.hash();
|
||||
assert_ne!(hash, [0u8; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_hash_deterministic() {
|
||||
let receipt = create_test_receipt(0, [0u8; 32]);
|
||||
let hash1 = receipt.hash();
|
||||
let hash2 = receipt.hash();
|
||||
assert_eq!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_hash_changes_with_sequence() {
|
||||
let receipt1 = create_test_receipt(0, [0u8; 32]);
|
||||
let receipt2 = create_test_receipt(1, [0u8; 32]);
|
||||
assert_ne!(receipt1.hash(), receipt2.hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_hash_changes_with_previous() {
|
||||
let receipt1 = create_test_receipt(0, [0u8; 32]);
|
||||
let receipt2 = create_test_receipt(0, [1u8; 32]);
|
||||
assert_ne!(receipt1.hash(), receipt2.hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_hash_includes_witness() {
|
||||
let mut receipt1 = create_test_receipt(0, [0u8; 32]);
|
||||
let mut receipt2 = create_test_receipt(0, [0u8; 32]);
|
||||
|
||||
receipt2.witness_summary.structural.cut_value = 99.0;
|
||||
|
||||
assert_ne!(receipt1.hash(), receipt2.hash());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod receipt_log {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_log_empty() {
|
||||
let log = ReceiptLog::new();
|
||||
assert!(log.is_empty());
|
||||
assert_eq!(log.len(), 0);
|
||||
assert_eq!(log.latest_sequence(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_genesis_hash() {
|
||||
let log = ReceiptLog::new();
|
||||
assert_eq!(log.last_hash(), [0u8; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append_single() {
|
||||
let mut log = ReceiptLog::new();
|
||||
let receipt = create_test_receipt(0, log.last_hash());
|
||||
|
||||
log.append(receipt);
|
||||
|
||||
assert_eq!(log.len(), 1);
|
||||
assert_eq!(log.latest_sequence(), Some(0));
|
||||
assert_ne!(log.last_hash(), [0u8; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append_multiple() {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
for i in 0..5 {
|
||||
let receipt = create_test_receipt(i, log.last_hash());
|
||||
log.append(receipt);
|
||||
}
|
||||
|
||||
assert_eq!(log.len(), 5);
|
||||
assert_eq!(log.latest_sequence(), Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_receipt() {
|
||||
let mut log = ReceiptLog::new();
|
||||
let receipt = create_test_receipt(0, log.last_hash());
|
||||
log.append(receipt);
|
||||
|
||||
let retrieved = log.get(0);
|
||||
assert!(retrieved.is_some());
|
||||
assert_eq!(retrieved.unwrap().sequence, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_nonexistent() {
|
||||
let log = ReceiptLog::new();
|
||||
assert!(log.get(0).is_none());
|
||||
assert!(log.get(999).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hash_chain_verification {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_verify_empty_chain() {
|
||||
let log = ReceiptLog::new();
|
||||
// Verifying empty chain up to 0 should fail (no receipt at 0)
|
||||
assert!(log.verify_chain_to(0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_single_receipt() {
|
||||
let mut log = ReceiptLog::new();
|
||||
let receipt = create_test_receipt(0, log.last_hash());
|
||||
log.append(receipt);
|
||||
|
||||
assert!(log.verify_chain_to(0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_chain_multiple() {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
for i in 0..10 {
|
||||
let receipt = create_test_receipt(i, log.last_hash());
|
||||
log.append(receipt);
|
||||
}
|
||||
|
||||
// Verify full chain
|
||||
assert!(log.verify_chain_to(9).is_ok());
|
||||
|
||||
// Verify partial chains
|
||||
assert!(log.verify_chain_to(0).is_ok());
|
||||
assert!(log.verify_chain_to(5).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_beyond_latest() {
|
||||
let mut log = ReceiptLog::new();
|
||||
let receipt = create_test_receipt(0, log.last_hash());
|
||||
log.append(receipt);
|
||||
|
||||
// Trying to verify beyond what exists should fail
|
||||
assert!(log.verify_chain_to(1).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tamper_detection {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_modified_hash() {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
// Build a valid chain
|
||||
for i in 0..5 {
|
||||
let receipt = create_test_receipt(i, log.last_hash());
|
||||
log.append(receipt);
|
||||
}
|
||||
|
||||
// The chain should be valid
|
||||
assert!(log.verify_chain_to(4).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_with_gap() {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
// Add receipt at 0
|
||||
let receipt0 = create_test_receipt(0, log.last_hash());
|
||||
log.append(receipt0);
|
||||
|
||||
// Skip 1, add at 2 (breaking chain)
|
||||
let receipt2 = create_test_receipt(2, log.last_hash());
|
||||
log.append(receipt2);
|
||||
|
||||
// Verify should fail at sequence 1 (missing)
|
||||
assert!(log.verify_chain_to(2).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod timestamp_proof {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_proof_structure() {
|
||||
let proof = TimestampProof {
|
||||
timestamp: 1000000000,
|
||||
previous_receipt_hash: [1u8; 32],
|
||||
merkle_root: [2u8; 32],
|
||||
};
|
||||
|
||||
assert_eq!(proof.timestamp, 1000000000);
|
||||
assert_eq!(proof.previous_receipt_hash, [1u8; 32]);
|
||||
assert_eq!(proof.merkle_root, [2u8; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_receipt_contains_timestamp_proof() {
|
||||
let receipt = create_test_receipt(5, [3u8; 32]);
|
||||
|
||||
assert_eq!(receipt.timestamp_proof.previous_receipt_hash, [3u8; 32]);
|
||||
assert!(receipt.timestamp_proof.timestamp > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_ordering() {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
for i in 0..5 {
|
||||
let receipt = create_test_receipt(i, log.last_hash());
|
||||
log.append(receipt);
|
||||
}
|
||||
|
||||
// Each receipt should have increasing timestamp
|
||||
let mut prev_ts = 0;
|
||||
for i in 0..5 {
|
||||
let receipt = log.get(i).unwrap();
|
||||
assert!(receipt.timestamp_proof.timestamp > prev_ts);
|
||||
prev_ts = receipt.timestamp_proof.timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod structural_witness {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_structural_witness_fields() {
|
||||
let witness = StructuralWitness {
|
||||
cut_value: 15.0,
|
||||
partition: "fragile".to_string(),
|
||||
critical_edges: 3,
|
||||
boundary: vec!["e1".to_string(), "e2".to_string(), "e3".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(witness.cut_value, 15.0);
|
||||
assert_eq!(witness.partition, "fragile");
|
||||
assert_eq!(witness.critical_edges, 3);
|
||||
assert_eq!(witness.boundary.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_structural_witness_serialization() {
|
||||
let witness = StructuralWitness {
|
||||
cut_value: 10.0,
|
||||
partition: "stable".to_string(),
|
||||
critical_edges: 2,
|
||||
boundary: vec![],
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&witness).unwrap();
|
||||
let restored: StructuralWitness = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(witness.cut_value, restored.cut_value);
|
||||
assert_eq!(witness.partition, restored.partition);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod predictive_witness {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_predictive_witness_fields() {
|
||||
let witness = PredictiveWitness {
|
||||
set_size: 12,
|
||||
coverage: 0.95,
|
||||
};
|
||||
|
||||
assert_eq!(witness.set_size, 12);
|
||||
assert_eq!(witness.coverage, 0.95);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_predictive_witness_serialization() {
|
||||
let witness = PredictiveWitness {
|
||||
set_size: 5,
|
||||
coverage: 0.9,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&witness).unwrap();
|
||||
let restored: PredictiveWitness = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(witness.set_size, restored.set_size);
|
||||
assert!((witness.coverage - restored.coverage).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod evidential_witness {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_evidential_witness_fields() {
|
||||
let witness = EvidentialWitness {
|
||||
e_value: 250.0,
|
||||
verdict: "accept".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(witness.e_value, 250.0);
|
||||
assert_eq!(witness.verdict, "accept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evidential_witness_verdicts() {
|
||||
let accept = EvidentialWitness {
|
||||
e_value: 200.0,
|
||||
verdict: "accept".to_string(),
|
||||
};
|
||||
|
||||
let cont = EvidentialWitness {
|
||||
e_value: 50.0,
|
||||
verdict: "continue".to_string(),
|
||||
};
|
||||
|
||||
let reject = EvidentialWitness {
|
||||
e_value: 0.005,
|
||||
verdict: "reject".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(accept.verdict, "accept");
|
||||
assert_eq!(cont.verdict, "continue");
|
||||
assert_eq!(reject.verdict, "reject");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod security_tests {
|
||||
use super::*;
|
||||
|
||||
/// Test that forged receipts are detected
|
||||
#[test]
|
||||
fn test_forged_receipt_detection() {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
// Build legitimate chain
|
||||
for i in 0..3 {
|
||||
let receipt = create_test_receipt(i, log.last_hash());
|
||||
log.append(receipt);
|
||||
}
|
||||
|
||||
// A forged receipt with wrong previous hash would break verification
|
||||
// (simulated by the verify_chain_to test with gaps)
|
||||
}
|
||||
|
||||
/// Test that hash provides uniqueness
|
||||
#[test]
|
||||
fn test_hash_collision_resistance() {
|
||||
let mut hashes = std::collections::HashSet::new();
|
||||
|
||||
// Generate many receipts and check for collisions
|
||||
for i in 0..100 {
|
||||
let receipt = create_test_receipt(i, [i as u8; 32]);
|
||||
let hash = receipt.hash();
|
||||
assert!(hashes.insert(hash), "Hash collision at sequence {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that modifying any field changes the hash
|
||||
#[test]
|
||||
fn test_all_fields_affect_hash() {
|
||||
let base = create_test_receipt(0, [0u8; 32]);
|
||||
let base_hash = base.hash();
|
||||
|
||||
// Modify sequence
|
||||
let mut modified = create_test_receipt(0, [0u8; 32]);
|
||||
modified.sequence = 1;
|
||||
assert_ne!(base_hash, modified.hash());
|
||||
|
||||
// Modify previous_hash
|
||||
let modified2 = create_test_receipt(0, [1u8; 32]);
|
||||
assert_ne!(base_hash, modified2.hash());
|
||||
|
||||
// Modify witness
|
||||
let mut modified3 = create_test_receipt(0, [0u8; 32]);
|
||||
modified3.witness_summary.evidential.e_value = 0.0;
|
||||
assert_ne!(base_hash, modified3.hash());
|
||||
}
|
||||
|
||||
/// Test sequence monotonicity
|
||||
#[test]
|
||||
fn test_sequence_monotonicity() {
|
||||
let mut log = ReceiptLog::new();
|
||||
let mut prev_seq = None;
|
||||
|
||||
for i in 0..10 {
|
||||
let receipt = create_test_receipt(i, log.last_hash());
|
||||
log.append(receipt);
|
||||
|
||||
if let Some(prev) = prev_seq {
|
||||
assert!(log.get(i).unwrap().sequence > prev);
|
||||
}
|
||||
prev_seq = Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Property-based tests
|
||||
#[cfg(test)]
|
||||
mod property_tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_hash_deterministic(seq in 0u64..1000, prev in proptest::array::uniform32(0u8..255)) {
|
||||
let receipt = create_test_receipt(seq, prev);
|
||||
assert_eq!(receipt.hash(), receipt.hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_different_sequences_different_hashes(seq1 in 0u64..1000, seq2 in 0u64..1000) {
|
||||
prop_assume!(seq1 != seq2);
|
||||
let r1 = create_test_receipt(seq1, [0u8; 32]);
|
||||
let r2 = create_test_receipt(seq2, [0u8; 32]);
|
||||
assert_ne!(r1.hash(), r2.hash());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prop_chain_grows_correctly(n in 1usize..20) {
|
||||
let mut log = ReceiptLog::new();
|
||||
|
||||
for i in 0..n {
|
||||
let receipt = create_test_receipt(i as u64, log.last_hash());
|
||||
log.append(receipt);
|
||||
}
|
||||
|
||||
assert_eq!(log.len(), n);
|
||||
assert!(log.verify_chain_to((n - 1) as u64).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user