545 lines
15 KiB
Rust
545 lines
15 KiB
Rust
//! 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());
|
|
}
|
|
}
|
|
}
|