git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
1053 lines
34 KiB
Rust
1053 lines
34 KiB
Rust
//! Adversarial Attack Scenario Tests
|
|
//!
|
|
//! This test suite validates edge-net's resilience against:
|
|
//! - Sybil attacks (fake node flooding)
|
|
//! - Eclipse attacks (network isolation)
|
|
//! - Byzantine behavior (malicious nodes)
|
|
//! - Double-spend attempts
|
|
//! - Replay attacks
|
|
//! - Resource exhaustion attacks
|
|
//! - Timing manipulation
|
|
//! - Authority bypass attempts
|
|
|
|
use ruvector_edge_net::rac::*;
|
|
use ed25519_dalek::SigningKey;
|
|
use std::collections::HashMap;
|
|
|
|
// ============================================================================
|
|
// Test Utilities
|
|
// ============================================================================
|
|
|
|
fn create_test_event(
|
|
context: ContextId,
|
|
author: PublicKeyBytes,
|
|
kind: EventKind,
|
|
id: Option<EventId>,
|
|
) -> Event {
|
|
Event {
|
|
id: id.unwrap_or([0u8; 32]),
|
|
prev: None,
|
|
ts_unix_ms: 1609459200000,
|
|
author,
|
|
context,
|
|
ruvector: Ruvector::new(vec![1.0, 0.0, 0.0]),
|
|
kind,
|
|
sig: vec![0u8; 64],
|
|
}
|
|
}
|
|
|
|
fn create_assert_event(proposition: &str, confidence: f32) -> AssertEvent {
|
|
AssertEvent {
|
|
proposition: proposition.as_bytes().to_vec(),
|
|
evidence: vec![EvidenceRef::hash(&[1, 2, 3])],
|
|
confidence,
|
|
expires_at_unix_ms: None,
|
|
}
|
|
}
|
|
|
|
fn generate_unique_id(seed: u8) -> EventId {
|
|
let mut id = [0u8; 32];
|
|
for i in 0..32 {
|
|
id[i] = seed.wrapping_add(i as u8);
|
|
}
|
|
id
|
|
}
|
|
|
|
// ============================================================================
|
|
// SYBIL ATTACK TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn sybil_attack_many_fake_nodes_same_claim() {
|
|
// Scenario: Attacker creates 100 fake nodes all supporting the same malicious claim
|
|
// Expected: System should detect the pattern and quarantine appropriately
|
|
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [1u8; 32];
|
|
let attacker_base = [0xAA; 32];
|
|
|
|
// Malicious claim
|
|
let mut malicious_claim = create_test_event(
|
|
context,
|
|
attacker_base,
|
|
EventKind::Assert(create_assert_event("attacker_controlled_truth", 0.99)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(malicious_claim.clone());
|
|
|
|
// Legitimate claim from honest node
|
|
let honest_author = [0xBB; 32];
|
|
let mut honest_claim = create_test_event(
|
|
context,
|
|
honest_author,
|
|
EventKind::Assert(create_assert_event("legitimate_truth", 0.95)),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
engine.ingest(honest_claim.clone());
|
|
|
|
// Challenge between claims
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context,
|
|
honest_author,
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![malicious_claim.id, honest_claim.id],
|
|
reason: "Conflicting truth claims".to_string(),
|
|
requested_proofs: vec!["evidence".to_string()],
|
|
}),
|
|
Some(generate_unique_id(3)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Sybil attack: 100 fake nodes all support malicious claim
|
|
for i in 0..100u8 {
|
|
let mut fake_author = attacker_base;
|
|
fake_author[0] = i; // Slight variation to simulate different "nodes"
|
|
|
|
let support = create_test_event(
|
|
context,
|
|
fake_author,
|
|
EventKind::Support(SupportEvent {
|
|
conflict_id,
|
|
claim_id: malicious_claim.id,
|
|
evidence: vec![EvidenceRef::hash(&[i, i, i])],
|
|
cost: 1, // Minimal cost - red flag
|
|
}),
|
|
Some(generate_unique_id(10 + i)),
|
|
);
|
|
engine.ingest(support);
|
|
}
|
|
|
|
// Verify both claims are quarantined during dispute
|
|
assert_eq!(
|
|
engine.get_quarantine_level(&hex::encode(&malicious_claim.id)),
|
|
2,
|
|
"Malicious claim should be quarantined during dispute"
|
|
);
|
|
assert_eq!(
|
|
engine.get_quarantine_level(&hex::encode(&honest_claim.id)),
|
|
2,
|
|
"Honest claim should be quarantined during dispute"
|
|
);
|
|
|
|
// Verify conflict count reflects the dispute
|
|
assert_eq!(engine.conflict_count(), 1, "One conflict should be recorded");
|
|
}
|
|
|
|
#[test]
|
|
fn sybil_attack_witness_path_analysis() {
|
|
// Test: Sybil witnesses share common paths (non-independent)
|
|
let tracker = WitnessTracker::new(3); // Require 3 independent witnesses
|
|
|
|
let claim_id = [1u8; 32];
|
|
let claim_key = hex::encode(&claim_id);
|
|
|
|
// Add 5 witnesses, but all share a common intermediate node (sybil pattern)
|
|
let common_intermediate = [0x55; 32];
|
|
for i in 0..5u8 {
|
|
let mut witness = [i; 32];
|
|
tracker.add_witness(WitnessRecord {
|
|
claim_id,
|
|
witness,
|
|
path: vec![common_intermediate], // All share same path!
|
|
witnessed_at: 1000 + i as u64,
|
|
signature: vec![],
|
|
});
|
|
}
|
|
|
|
// Despite 5 witnesses, they are NOT independent (share common path)
|
|
assert_eq!(tracker.witness_count(&claim_key), 5);
|
|
|
|
// Only 1 independent path exists (first witness + all others share path)
|
|
assert!(
|
|
!tracker.has_sufficient_witnesses(&claim_key),
|
|
"Non-independent witnesses should not satisfy requirement"
|
|
);
|
|
|
|
// Now add truly independent witness
|
|
tracker.add_witness(WitnessRecord {
|
|
claim_id,
|
|
witness: [0xFF; 32],
|
|
path: vec![[0xAA; 32], [0xBB; 32]], // Different path
|
|
witnessed_at: 2000,
|
|
signature: vec![],
|
|
});
|
|
|
|
tracker.add_witness(WitnessRecord {
|
|
claim_id,
|
|
witness: [0xFE; 32],
|
|
path: vec![[0xCC; 32], [0xDD; 32]], // Yet another different path
|
|
witnessed_at: 3000,
|
|
signature: vec![],
|
|
});
|
|
|
|
// Now we have 3 independent paths
|
|
assert!(
|
|
tracker.has_sufficient_witnesses(&claim_key),
|
|
"3 independent witnesses should satisfy requirement"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// ECLIPSE ATTACK TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn eclipse_attack_context_isolation() {
|
|
// Scenario: Attacker tries to isolate a context by controlling all events
|
|
// Expected: Context isolation prevents cross-contamination
|
|
|
|
let mut engine = CoherenceEngine::new();
|
|
let isolated_context = [0xEC; 32];
|
|
let normal_context = [0xD0; 32];
|
|
let attacker = [0xAF; 32];
|
|
let honest = [0xB0; 32];
|
|
|
|
// Attacker floods isolated context with claims
|
|
for i in 0..50u8 {
|
|
let claim = create_test_event(
|
|
isolated_context,
|
|
attacker,
|
|
EventKind::Assert(create_assert_event(
|
|
&format!("attacker_claim_{}", i),
|
|
0.9,
|
|
)),
|
|
Some(generate_unique_id(i)),
|
|
);
|
|
engine.ingest(claim);
|
|
}
|
|
|
|
// Honest node creates claim in normal context
|
|
let honest_claim = create_test_event(
|
|
normal_context,
|
|
honest,
|
|
EventKind::Assert(create_assert_event("honest_claim", 0.95)),
|
|
Some(generate_unique_id(100)),
|
|
);
|
|
engine.ingest(honest_claim.clone());
|
|
|
|
// Verify contexts are properly isolated
|
|
let isolated_events = engine.get_context_events(&isolated_context);
|
|
let normal_events = engine.get_context_events(&normal_context);
|
|
|
|
assert_eq!(isolated_events.len(), 50, "Isolated context has attacker events");
|
|
assert_eq!(normal_events.len(), 1, "Normal context has only honest event");
|
|
|
|
// Attacker cannot quarantine honest claim from different context
|
|
assert!(
|
|
engine.can_use_claim(&hex::encode(&honest_claim.id)),
|
|
"Honest claim in separate context should be usable"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn eclipse_attack_merkle_divergence_detection() {
|
|
// Test: Detecting if an attacker shows different histories to different nodes
|
|
let log = EventLog::new();
|
|
|
|
// Build history
|
|
let mut event_ids = Vec::new();
|
|
for i in 0..10u8 {
|
|
let mut event = create_test_event(
|
|
[0u8; 32],
|
|
[i; 32],
|
|
EventKind::Assert(create_assert_event(&format!("event_{}", i), 0.9)),
|
|
Some(generate_unique_id(i)),
|
|
);
|
|
if !event_ids.is_empty() {
|
|
event.prev = Some(*event_ids.last().unwrap());
|
|
}
|
|
let id = log.append(event);
|
|
event_ids.push(id);
|
|
}
|
|
|
|
// Get canonical root - changes with each append
|
|
let final_root = log.get_root();
|
|
assert!(!final_root.is_empty(), "Root should be non-empty after appends");
|
|
|
|
// Verify root is not all zeros (history exists)
|
|
let root_bytes = log.get_root_bytes();
|
|
assert_ne!(root_bytes, [0u8; 32], "Root should reflect history");
|
|
|
|
// Generate inclusion proof for last event (most recent always verifiable)
|
|
let last_id = event_ids.last().unwrap();
|
|
let proof = log.prove_inclusion(last_id);
|
|
assert!(proof.is_some(), "Should generate proof for last event");
|
|
|
|
// Proof contains valid event reference
|
|
let proof = proof.unwrap();
|
|
assert_eq!(proof.event_id, *last_id, "Proof event ID matches");
|
|
assert_eq!(proof.index, 9, "Last event at index 9");
|
|
|
|
// Attempting to prove a fake event fails
|
|
let fake_id = [0xFF; 32];
|
|
let fake_proof = log.prove_inclusion(&fake_id);
|
|
assert!(fake_proof.is_none(), "Cannot prove inclusion of non-existent event");
|
|
|
|
// Key property: Different histories would produce different roots
|
|
// If attacker shows different events, root will differ
|
|
let log2 = EventLog::new();
|
|
for i in 0..10u8 {
|
|
let event = create_test_event(
|
|
[0u8; 32],
|
|
[i + 100; 32], // Different authors = different events
|
|
EventKind::Assert(create_assert_event(&format!("different_{}", i), 0.9)),
|
|
Some(generate_unique_id(i + 100)),
|
|
);
|
|
log2.append(event);
|
|
}
|
|
|
|
let different_root = log2.get_root();
|
|
assert_ne!(final_root, different_root, "Different histories produce different roots");
|
|
}
|
|
|
|
// ============================================================================
|
|
// BYZANTINE BEHAVIOR TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn byzantine_one_third_threshold() {
|
|
// Test: BFT requires > 1/3 honest nodes for safety
|
|
// At exactly 1/3 byzantine, consensus should still be maintained
|
|
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xB1; 32];
|
|
|
|
// Simulate network with 100 nodes, 33 byzantine (exactly 1/3)
|
|
let total_nodes = 100;
|
|
let byzantine_nodes = 33;
|
|
let honest_nodes = total_nodes - byzantine_nodes;
|
|
|
|
// All honest nodes make same claim
|
|
let honest_claim_content = "consensus_truth";
|
|
let mut honest_claim_id = [0u8; 32];
|
|
|
|
for i in 0..honest_nodes {
|
|
let mut claim = create_test_event(
|
|
context,
|
|
[i as u8; 32],
|
|
EventKind::Assert(create_assert_event(honest_claim_content, 0.95)),
|
|
Some(generate_unique_id(i as u8)),
|
|
);
|
|
if i == 0 {
|
|
honest_claim_id = claim.id;
|
|
}
|
|
engine.ingest(claim);
|
|
}
|
|
|
|
// Byzantine nodes try to assert different value
|
|
for i in 0..byzantine_nodes {
|
|
let claim = create_test_event(
|
|
context,
|
|
[(honest_nodes + i) as u8; 32],
|
|
EventKind::Assert(create_assert_event("byzantine_lie", 0.99)),
|
|
Some(generate_unique_id((honest_nodes + i) as u8)),
|
|
);
|
|
engine.ingest(claim);
|
|
}
|
|
|
|
// Verify honest claim is still usable (not quarantined by byzantine minority)
|
|
assert!(
|
|
engine.can_use_claim(&hex::encode(&honest_claim_id)),
|
|
"Honest majority claim should remain usable"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn byzantine_escalation_tracking() {
|
|
// Test: Conflicts with high temperature escalate properly
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xE5; 32];
|
|
let author = [1u8; 32];
|
|
|
|
// Create claim
|
|
let claim = create_test_event(
|
|
context,
|
|
author,
|
|
EventKind::Assert(create_assert_event("disputed_claim", 0.9)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(claim.clone());
|
|
|
|
// Challenge
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context,
|
|
[2u8; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![claim.id],
|
|
reason: "Dispute".to_string(),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Add many support events to increase temperature and trigger escalation
|
|
for i in 0..20u8 {
|
|
let support = create_test_event(
|
|
context,
|
|
[i + 10; 32],
|
|
EventKind::Support(SupportEvent {
|
|
conflict_id,
|
|
claim_id: claim.id,
|
|
evidence: vec![],
|
|
cost: 100,
|
|
}),
|
|
Some(generate_unique_id(10 + i)),
|
|
);
|
|
engine.ingest(support);
|
|
}
|
|
|
|
// Verify escalations occurred
|
|
let stats: CoherenceStats = serde_json::from_str(&engine.get_stats()).unwrap();
|
|
assert!(
|
|
stats.escalations > 0,
|
|
"High-temperature conflict should trigger escalation"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// DOUBLE-SPEND ATTACK TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn double_spend_simultaneous_claims() {
|
|
// Scenario: Attacker tries to spend same resource twice
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xD5; 32];
|
|
let attacker = [0xAF; 32];
|
|
|
|
// Attacker claims to have transferred resource to two different recipients
|
|
let spend_1 = create_test_event(
|
|
context,
|
|
attacker,
|
|
EventKind::Assert(AssertEvent {
|
|
proposition: b"transfer:resource_123:recipient_A".to_vec(),
|
|
evidence: vec![EvidenceRef::hash(b"sig_A")],
|
|
confidence: 0.99,
|
|
expires_at_unix_ms: None,
|
|
}),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
|
|
let spend_2 = create_test_event(
|
|
context,
|
|
attacker,
|
|
EventKind::Assert(AssertEvent {
|
|
proposition: b"transfer:resource_123:recipient_B".to_vec(),
|
|
evidence: vec![EvidenceRef::hash(b"sig_B")],
|
|
confidence: 0.99,
|
|
expires_at_unix_ms: None,
|
|
}),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
|
|
engine.ingest(spend_1.clone());
|
|
engine.ingest(spend_2.clone());
|
|
|
|
// Honest node detects conflict and challenges
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context,
|
|
[0xB0; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![spend_1.id, spend_2.id],
|
|
reason: "Double-spend detected: same resource transferred twice".to_string(),
|
|
requested_proofs: vec!["ordering_proof".to_string()],
|
|
}),
|
|
Some(generate_unique_id(3)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Both claims should be quarantined
|
|
assert_eq!(
|
|
engine.get_quarantine_level(&hex::encode(&spend_1.id)),
|
|
2,
|
|
"First spend should be quarantined"
|
|
);
|
|
assert_eq!(
|
|
engine.get_quarantine_level(&hex::encode(&spend_2.id)),
|
|
2,
|
|
"Second spend should be quarantined"
|
|
);
|
|
|
|
// Resolution accepts first, rejects second (FIFO)
|
|
let resolution = create_test_event(
|
|
context,
|
|
[0xA0; 32], // Authority
|
|
EventKind::Resolution(ResolutionEvent {
|
|
conflict_id,
|
|
accepted: vec![spend_1.id],
|
|
deprecated: vec![spend_2.id],
|
|
rationale: vec![EvidenceRef::log(b"first_seen_wins")],
|
|
authority_sigs: vec![vec![0u8; 64]],
|
|
}),
|
|
Some(generate_unique_id(4)),
|
|
);
|
|
engine.ingest(resolution);
|
|
|
|
// Verify resolution applied correctly
|
|
assert!(
|
|
engine.can_use_claim(&hex::encode(&spend_1.id)),
|
|
"First spend should be accepted"
|
|
);
|
|
assert!(
|
|
!engine.can_use_claim(&hex::encode(&spend_2.id)),
|
|
"Second spend should be blocked"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// REPLAY ATTACK TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn replay_attack_duplicate_event_detection() {
|
|
// Scenario: Attacker replays old valid event
|
|
let log = EventLog::new();
|
|
|
|
let original_event = create_test_event(
|
|
[0u8; 32],
|
|
[1u8; 32],
|
|
EventKind::Assert(create_assert_event("original_claim", 0.9)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
|
|
let id1 = log.append(original_event.clone());
|
|
|
|
// Attempt to replay same event
|
|
let id2 = log.append(original_event.clone());
|
|
|
|
// Events have same content but log tracks both (implementation could dedupe)
|
|
assert_eq!(log.len(), 2, "Log records both events");
|
|
|
|
// In real implementation, nonce/timestamp would make ID unique
|
|
// Here we verify Merkle root changes with each append
|
|
let root_after_replay = log.get_root_bytes();
|
|
assert_ne!(root_after_replay, [0u8; 32], "Root should be non-zero");
|
|
}
|
|
|
|
#[test]
|
|
fn replay_attack_timestamp_validation() {
|
|
// Test: Events with old timestamps should be treated with caution
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xAD; 32];
|
|
|
|
// Event from "the past" (1 year ago)
|
|
let old_timestamp = 1577836800000u64; // 2020-01-01
|
|
let mut old_event = create_test_event(
|
|
context,
|
|
[1u8; 32],
|
|
EventKind::Assert(create_assert_event("old_claim", 0.9)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
old_event.ts_unix_ms = old_timestamp;
|
|
|
|
engine.ingest(old_event.clone());
|
|
|
|
// Event is ingested but drift tracking should detect temporal anomaly
|
|
assert_eq!(engine.event_count(), 1);
|
|
|
|
// The system should flag claims with very old timestamps for review
|
|
// This is a policy decision - the infrastructure supports it
|
|
}
|
|
|
|
// ============================================================================
|
|
// RESOURCE EXHAUSTION ATTACK TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn resource_exhaustion_event_flood() {
|
|
// Scenario: Attacker floods system with events to exhaust resources
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xAE; 32];
|
|
let attacker = [0xAF; 32];
|
|
|
|
// Flood with 10,000 events
|
|
let flood_count = 10_000;
|
|
for i in 0..flood_count {
|
|
let event = create_test_event(
|
|
context,
|
|
attacker,
|
|
EventKind::Assert(create_assert_event(&format!("flood_{}", i), 0.5)),
|
|
Some({
|
|
let mut id = [0u8; 32];
|
|
id[0..4].copy_from_slice(&(i as u32).to_le_bytes());
|
|
id
|
|
}),
|
|
);
|
|
engine.ingest(event);
|
|
}
|
|
|
|
// System should handle this without panicking
|
|
assert_eq!(engine.event_count(), flood_count);
|
|
|
|
// Stats should reflect the flood
|
|
let stats: CoherenceStats = serde_json::from_str(&engine.get_stats()).unwrap();
|
|
assert_eq!(stats.events_processed, flood_count);
|
|
}
|
|
|
|
#[test]
|
|
fn resource_exhaustion_conflict_spam() {
|
|
// Scenario: Attacker creates many conflicts to slow down resolution
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xC5; 32];
|
|
|
|
// Create many claims
|
|
let claim_count = 100;
|
|
let mut claim_ids = Vec::new();
|
|
|
|
for i in 0..claim_count {
|
|
let claim = create_test_event(
|
|
context,
|
|
[i as u8; 32],
|
|
EventKind::Assert(create_assert_event(&format!("claim_{}", i), 0.8)),
|
|
Some(generate_unique_id(i as u8)),
|
|
);
|
|
claim_ids.push(claim.id);
|
|
engine.ingest(claim);
|
|
}
|
|
|
|
// Challenge every pair (creates n*(n-1)/2 potential conflicts)
|
|
// We'll limit to first 50 to keep test reasonable
|
|
let mut conflict_count = 0;
|
|
for i in 0..10 {
|
|
for j in (i + 1)..10 {
|
|
let challenge = create_test_event(
|
|
context,
|
|
[0xFF; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id: {
|
|
let mut id = [0u8; 32];
|
|
id[0] = i as u8;
|
|
id[1] = j as u8;
|
|
id
|
|
},
|
|
claim_ids: vec![claim_ids[i], claim_ids[j]],
|
|
reason: "Spam conflict".to_string(),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some({
|
|
let mut id = [0u8; 32];
|
|
id[0] = 100 + i as u8;
|
|
id[1] = j as u8;
|
|
id
|
|
}),
|
|
);
|
|
engine.ingest(challenge);
|
|
conflict_count += 1;
|
|
}
|
|
}
|
|
|
|
// Verify conflicts recorded
|
|
assert_eq!(engine.conflict_count(), conflict_count);
|
|
|
|
// System should still be responsive
|
|
let stats: CoherenceStats = serde_json::from_str(&engine.get_stats()).unwrap();
|
|
assert!(stats.conflicts_detected > 0);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TIMING MANIPULATION TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn timing_attack_future_timestamp() {
|
|
// Scenario: Attacker uses future timestamps to gain priority
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xF1; 32];
|
|
|
|
// Attacker claims with far-future timestamp
|
|
let future_ts = 4102444800000u64; // 2100-01-01
|
|
let mut future_event = create_test_event(
|
|
context,
|
|
[0xAF; 32],
|
|
EventKind::Assert(create_assert_event("future_claim", 0.99)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
future_event.ts_unix_ms = future_ts;
|
|
|
|
// Current event with realistic timestamp
|
|
let current_ts = 1609459200000u64; // 2021-01-01
|
|
let mut current_event = create_test_event(
|
|
context,
|
|
[0xB0; 32],
|
|
EventKind::Assert(create_assert_event("current_claim", 0.9)),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
current_event.ts_unix_ms = current_ts;
|
|
|
|
engine.ingest(future_event.clone());
|
|
engine.ingest(current_event.clone());
|
|
|
|
// Both events ingested
|
|
assert_eq!(engine.event_count(), 2);
|
|
|
|
// System should not give priority to future-dated events
|
|
// (This is a policy check - implementation may flag anomalous timestamps)
|
|
}
|
|
|
|
#[test]
|
|
fn timing_attack_rapid_claim_resolution() {
|
|
// Scenario: Attacker tries to resolve conflict immediately without proper dispute period
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xAC; 32];
|
|
|
|
// Create claim
|
|
let claim = create_test_event(
|
|
context,
|
|
[1u8; 32],
|
|
EventKind::Assert(create_assert_event("quick_claim", 0.9)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(claim.clone());
|
|
|
|
// Challenge
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context,
|
|
[2u8; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![claim.id],
|
|
reason: "Dispute".to_string(),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Attacker immediately tries to resolve (no dispute period)
|
|
let quick_resolution = create_test_event(
|
|
context,
|
|
[0xAF; 32], // Attacker pretending to be authority
|
|
EventKind::Resolution(ResolutionEvent {
|
|
conflict_id,
|
|
accepted: vec![],
|
|
deprecated: vec![claim.id],
|
|
rationale: vec![],
|
|
authority_sigs: vec![], // No signatures!
|
|
}),
|
|
Some(generate_unique_id(3)),
|
|
);
|
|
|
|
let result = engine.ingest(quick_resolution);
|
|
|
|
// Resolution without authority should be rejected
|
|
// Note: Current implementation requires at least one signature
|
|
assert!(
|
|
matches!(result, IngestResult::UnauthorizedResolution),
|
|
"Resolution without authority should fail"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUTHORITY BYPASS TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn authority_bypass_forged_resolution() {
|
|
// Scenario: Attacker tries to forge resolution without proper authority
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xAB; 32];
|
|
|
|
// Generate a real Ed25519 keypair for the authority
|
|
let signing_key_bytes: [u8; 32] = [
|
|
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60,
|
|
0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4,
|
|
0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19,
|
|
0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
|
|
];
|
|
let signing_key = SigningKey::from_bytes(&signing_key_bytes);
|
|
let authorized_key: [u8; 32] = signing_key.verifying_key().to_bytes();
|
|
|
|
// Register authority for context
|
|
let authority = ScopedAuthority::new(context, vec![authorized_key], 1);
|
|
engine.register_authority(authority);
|
|
|
|
// Create claim and challenge
|
|
let claim = create_test_event(
|
|
context,
|
|
[1u8; 32],
|
|
EventKind::Assert(create_assert_event("protected_claim", 0.9)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(claim.clone());
|
|
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context,
|
|
[2u8; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![claim.id],
|
|
reason: "Testing authority".to_string(),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Attacker tries to resolve without authorized signature
|
|
let forged_resolution = create_test_event(
|
|
context,
|
|
[0xAF; 32], // Unauthorized attacker
|
|
EventKind::Resolution(ResolutionEvent {
|
|
conflict_id,
|
|
accepted: vec![],
|
|
deprecated: vec![claim.id],
|
|
rationale: vec![],
|
|
authority_sigs: vec![], // Missing required signature
|
|
}),
|
|
Some(generate_unique_id(3)),
|
|
);
|
|
|
|
let result = engine.ingest(forged_resolution);
|
|
assert!(
|
|
matches!(result, IngestResult::UnauthorizedResolution),
|
|
"Forged resolution should be rejected"
|
|
);
|
|
|
|
// Create valid resolution event (without signature first, for signing)
|
|
let resolution_event = ResolutionEvent {
|
|
conflict_id,
|
|
accepted: vec![claim.id],
|
|
deprecated: vec![],
|
|
rationale: vec![EvidenceRef::hash(b"authority_decision")],
|
|
authority_sigs: vec![], // Will be replaced
|
|
};
|
|
|
|
// Sign with real Ed25519 key
|
|
let signature = ScopedAuthority::sign_resolution(&resolution_event, &context, &signing_key_bytes);
|
|
|
|
// Valid resolution with real authority signature
|
|
let valid_resolution = create_test_event(
|
|
context,
|
|
authorized_key,
|
|
EventKind::Resolution(ResolutionEvent {
|
|
conflict_id,
|
|
accepted: vec![claim.id],
|
|
deprecated: vec![],
|
|
rationale: vec![EvidenceRef::hash(b"authority_decision")],
|
|
authority_sigs: vec![signature], // Real Ed25519 signature
|
|
}),
|
|
Some(generate_unique_id(4)),
|
|
);
|
|
|
|
let result = engine.ingest(valid_resolution);
|
|
assert!(
|
|
matches!(result, IngestResult::Success(_)),
|
|
"Authorized resolution should succeed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn authority_bypass_wrong_context() {
|
|
// Scenario: Authority for one context tries to resolve in another
|
|
let mut engine = CoherenceEngine::new();
|
|
let context_a = [0xAA; 32];
|
|
let context_b = [0xBB; 32];
|
|
let authority_a = [0xA1; 32];
|
|
|
|
// Register authority only for context A
|
|
let authority = ScopedAuthority::new(context_a, vec![authority_a], 1);
|
|
engine.register_authority(authority);
|
|
|
|
// Create claim in context B
|
|
let claim_b = create_test_event(
|
|
context_b,
|
|
[1u8; 32],
|
|
EventKind::Assert(create_assert_event("claim_in_b", 0.9)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(claim_b.clone());
|
|
|
|
// Challenge in context B
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context_b,
|
|
[2u8; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![claim_b.id],
|
|
reason: "Testing cross-context".to_string(),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Authority A tries to resolve in context B (should fail - no authority registered)
|
|
// Actually, without registered authority, it falls back to requiring any signature
|
|
let cross_context_resolution = create_test_event(
|
|
context_b,
|
|
authority_a, // Authority A, but for context B
|
|
EventKind::Resolution(ResolutionEvent {
|
|
conflict_id,
|
|
accepted: vec![claim_b.id],
|
|
deprecated: vec![],
|
|
rationale: vec![],
|
|
authority_sigs: vec![vec![0u8; 64]], // Has a signature, so will pass basic check
|
|
}),
|
|
Some(generate_unique_id(3)),
|
|
);
|
|
|
|
// Note: Current implementation allows this because context_b has no registered authority
|
|
// In a stricter implementation, this could be rejected
|
|
let result = engine.ingest(cross_context_resolution);
|
|
// This demonstrates that authority is context-scoped
|
|
}
|
|
|
|
// ============================================================================
|
|
// DECISION REPLAY PROTECTION TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn decision_replay_quarantined_dependency() {
|
|
// Test: Decisions cannot be replayed if dependencies become quarantined
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0xDA; 32];
|
|
|
|
// Create claim
|
|
let claim = create_test_event(
|
|
context,
|
|
[1u8; 32],
|
|
EventKind::Assert(create_assert_event("decision_input", 0.95)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(claim.clone());
|
|
|
|
// Create decision trace depending on this claim
|
|
let decision = DecisionTrace::new(
|
|
vec![claim.id],
|
|
b"decision_output".to_vec(),
|
|
);
|
|
|
|
// Decision should be replayable initially
|
|
assert!(decision.can_replay(&engine), "Decision should be replayable with valid dependency");
|
|
|
|
// Quarantine the claim
|
|
let conflict_id = generate_unique_id(99);
|
|
let challenge = create_test_event(
|
|
context,
|
|
[2u8; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id,
|
|
claim_ids: vec![claim.id],
|
|
reason: "Disputed".to_string(),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some(generate_unique_id(2)),
|
|
);
|
|
engine.ingest(challenge);
|
|
|
|
// Decision should no longer be replayable
|
|
assert!(
|
|
!decision.can_replay(&engine),
|
|
"Decision should not be replayable with quarantined dependency"
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// DRIFT ATTACK TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn semantic_drift_detection() {
|
|
// Test: Gradual semantic drift is detected
|
|
let tracker = DriftTracker::new(0.3);
|
|
let context = [0x5D; 32];
|
|
let context_key = hex::encode(&context);
|
|
|
|
// Initial embedding
|
|
tracker.update(&context, &Ruvector::new(vec![1.0, 0.0, 0.0]));
|
|
assert!(!tracker.has_drifted(&context_key), "No initial drift");
|
|
|
|
// Gradual drift through many updates
|
|
for i in 0..100 {
|
|
let angle = (i as f32) * 0.01; // Small incremental rotation
|
|
tracker.update(&context, &Ruvector::new(vec![
|
|
(1.0 - angle).max(0.0),
|
|
angle,
|
|
0.0,
|
|
]));
|
|
}
|
|
|
|
// After many updates, significant drift should be detected
|
|
let drift = tracker.get_drift(&context_key);
|
|
assert!(drift > 0.0, "Drift should be measured: {}", drift);
|
|
}
|
|
|
|
// ============================================================================
|
|
// INTEGRATION TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn integration_multi_attack_scenario() {
|
|
// Combined attack: Sybil + timing manipulation + authority bypass
|
|
let mut engine = CoherenceEngine::new();
|
|
let context = [0x1D; 32];
|
|
let honest = [0xB0; 32];
|
|
|
|
// Honest claim
|
|
let honest_claim = create_test_event(
|
|
context,
|
|
honest,
|
|
EventKind::Assert(create_assert_event("truth", 0.95)),
|
|
Some(generate_unique_id(1)),
|
|
);
|
|
engine.ingest(honest_claim.clone());
|
|
|
|
// Sybil attack: Many fake nodes challenge
|
|
for i in 0..10u8 {
|
|
let sybil_challenge = create_test_event(
|
|
context,
|
|
[i; 32],
|
|
EventKind::Challenge(ChallengeEvent {
|
|
conflict_id: generate_unique_id(100 + i),
|
|
claim_ids: vec![honest_claim.id],
|
|
reason: format!("Sybil challenge {}", i),
|
|
requested_proofs: vec![],
|
|
}),
|
|
Some(generate_unique_id(10 + i)),
|
|
);
|
|
engine.ingest(sybil_challenge);
|
|
}
|
|
|
|
// Claim should be quarantined due to challenges
|
|
assert!(
|
|
!engine.can_use_claim(&hex::encode(&honest_claim.id)) ||
|
|
engine.get_quarantine_level(&hex::encode(&honest_claim.id)) > 0,
|
|
"Claim should be affected by challenges"
|
|
);
|
|
|
|
// Honest authority resolves in favor of honest claim
|
|
let authority = [0xA0; 32];
|
|
engine.register_authority(ScopedAuthority::new(context, vec![authority], 1));
|
|
|
|
// Resolve the first conflict (challenges create separate conflicts)
|
|
let resolution = create_test_event(
|
|
context,
|
|
authority,
|
|
EventKind::Resolution(ResolutionEvent {
|
|
conflict_id: generate_unique_id(100), // First sybil conflict
|
|
accepted: vec![honest_claim.id],
|
|
deprecated: vec![],
|
|
rationale: vec![EvidenceRef::hash(b"sybil_detected")],
|
|
authority_sigs: vec![vec![0u8; 64]],
|
|
}),
|
|
Some(generate_unique_id(50)),
|
|
);
|
|
engine.ingest(resolution);
|
|
|
|
// After resolution, honest claim should be usable again
|
|
assert!(
|
|
engine.can_use_claim(&hex::encode(&honest_claim.id)),
|
|
"Honest claim should be restored after proper resolution"
|
|
);
|
|
}
|