v2.0.0: adaptive eBPF firewall with AI honeypot and P2P threat mesh

This commit is contained in:
Vladyslav Soliannikov 2026-04-07 22:28:11 +00:00
commit 37c6bbf5a1
133 changed files with 28073 additions and 0 deletions

719
hivemind/tests/battlefield.rs Executable file
View file

@ -0,0 +1,719 @@
//! Battlefield Simulation — Full-scale E2E integration tests for HiveMind Threat Mesh.
//!
//! Simulates a hostile network environment with 10 virtual HiveMind nodes
//! under Sybil attack, coordinated botnet detection, federated learning
//! with Byzantine actors, and ZKP-backed consensus verification.
//!
//! These tests exercise the real code paths at module integration boundaries,
//! NOT the libp2p transport layer (which requires a live network stack).
use common::hivemind as hm;
use hm::{IoC, IoCType, ThreatSeverity};
use hivemind::consensus::{ConsensusEngine, ConsensusResult};
use hivemind::ml::aggregator::{AggregatorError, FedAvgAggregator};
use hivemind::ml::defense::{GradientDefense, GradientVerdict};
use hivemind::ml::local_model::LocalModel;
use hivemind::reputation::ReputationStore;
use hivemind::sybil_guard::{SybilError, SybilGuard};
use hivemind::zkp::{prover, verifier};
use std::time::Instant;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Generate a deterministic peer pubkey from an ID byte.
fn peer_key(id: u8) -> [u8; 32] {
let mut key = [0u8; 32];
key[0] = id;
// Spread entropy so PoW nonce search starts differently per peer
key[31] = id.wrapping_mul(37);
key
}
/// Create a JA4 IoC for consensus testing.
fn make_ja4_ioc(ip: u32) -> IoC {
IoC {
ioc_type: IoCType::Ja4Fingerprint as u8,
severity: ThreatSeverity::High as u8,
ip,
ja4: Some("t13d1516h2_8daaf6152771_e5627efa2ab1".into()),
entropy_score: Some(7800),
description: "Suspicious JA4 fingerprint — possible C2 beacon".into(),
first_seen: now_secs(),
confirmations: 0,
zkp_proof: Vec::new(),
}
}
/// Create a malicious-IP IoC.
fn make_malicious_ip_ioc(ip: u32) -> IoC {
IoC {
ioc_type: IoCType::MaliciousIp as u8,
severity: ThreatSeverity::Critical as u8,
ip,
ja4: None,
entropy_score: None,
description: "Known C2 server".into(),
first_seen: now_secs(),
confirmations: 0,
zkp_proof: Vec::new(),
}
}
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
// ===========================================================================
// Scenario 1 — The Swarm: Spawn 10 virtual HiveMind nodes
// ===========================================================================
#[test]
fn scenario_1_swarm_spawn_10_nodes() {
let mut guard = SybilGuard::new();
let mut reputation = ReputationStore::new();
let start = Instant::now();
// Register 10 nodes via valid Proof-of-Work
for id in 1..=10u8 {
let pk = peer_key(id);
let challenge = SybilGuard::generate_pow(&pk, hm::POW_DIFFICULTY_BITS);
// PoW must verify successfully
let result = guard.verify_registration(&challenge);
assert!(
result.is_ok(),
"Node {id} PoW verification failed: {result:?}"
);
// Register peer as seed peer (bootstrap nodes get elevated stake)
reputation.register_seed_peer(&pk);
assert!(
reputation.is_trusted(&pk),
"Seed peer node {id} should be trusted (SEED_PEER_STAKE >= MIN_TRUSTED)"
);
}
let elapsed = start.elapsed();
eprintln!(
"[SWARM] 10 nodes registered via PoW in {:.2?} — all trusted (seed peers)",
elapsed
);
// All 10 peers should be tracked
assert_eq!(reputation.peer_count(), 10);
}
// ===========================================================================
// Scenario 2 — Sybil Attack: 5 malicious nodes with invalid PoW
// ===========================================================================
#[test]
fn scenario_2_sybil_attack_rejected() {
let mut guard = SybilGuard::new();
// First register 2 legitimate nodes so the SybilGuard has state
for id in 1..=2u8 {
let pk = peer_key(id);
let challenge = SybilGuard::generate_pow(&pk, hm::POW_DIFFICULTY_BITS);
guard
.verify_registration(&challenge)
.expect("Legitimate node should pass PoW");
}
// --- Attack vector 1: Wrong nonce (hash won't meet difficulty) ---
let pk = peer_key(200);
let mut forged = SybilGuard::generate_pow(&pk, hm::POW_DIFFICULTY_BITS);
forged.nonce = forged.nonce.wrapping_add(1); // corrupt the solution
let result = guard.verify_registration(&forged);
assert_eq!(
result,
Err(SybilError::InvalidProof),
"Corrupted nonce should yield InvalidProof"
);
// --- Attack vector 2: Insufficient difficulty ---
let low_diff = hm::PowChallenge {
peer_pubkey: peer_key(201),
nonce: 0,
timestamp: now_secs(),
difficulty: hm::POW_DIFFICULTY_BITS - 5, // too easy
};
let result = guard.verify_registration(&low_diff);
assert_eq!(
result,
Err(SybilError::InsufficientDifficulty),
"Low difficulty should be rejected"
);
// --- Attack vector 3: Stale timestamp ---
let pk = peer_key(202);
let mut stale = SybilGuard::generate_pow(&pk, hm::POW_DIFFICULTY_BITS);
stale.timestamp = 1_000_000; // year ~2001, way beyond TTL
let result = guard.verify_registration(&stale);
assert_eq!(
result,
Err(SybilError::StaleChallenge),
"Stale timestamp should be rejected"
);
// --- Attack vector 4: Future timestamp ---
let future = hm::PowChallenge {
peer_pubkey: peer_key(203),
nonce: 0,
timestamp: now_secs() + 3600, // 1 hour in the future
difficulty: hm::POW_DIFFICULTY_BITS,
};
let result = guard.verify_registration(&future);
assert_eq!(
result,
Err(SybilError::StaleChallenge),
"Future timestamp should be rejected"
);
// --- Attack vector 5: Replay with someone else's pubkey ---
let victim_pk = peer_key(1);
let attacker_pk = peer_key(204);
let mut replay = SybilGuard::generate_pow(&victim_pk, hm::POW_DIFFICULTY_BITS);
replay.peer_pubkey = attacker_pk; // swap pubkey — hash won't match
let result = guard.verify_registration(&replay);
assert_eq!(
result,
Err(SybilError::InvalidProof),
"Replay with swapped pubkey should fail"
);
eprintln!("[SYBIL] All 5 attack vectors rejected correctly");
}
// ===========================================================================
// Scenario 3 — The Botnet Blitz: 3 nodes detect same JA4 → consensus
// ===========================================================================
#[test]
fn scenario_3_botnet_blitz_consensus() {
let mut consensus = ConsensusEngine::new();
let mut reputation = ReputationStore::new();
// Register 10 honest peers
for id in 1..=10u8 {
reputation.register_peer(&peer_key(id));
}
let botnet_ioc = make_ja4_ioc(0xC0A80001); // 192.168.0.1
// Peer 1 submits — pending (1/3)
let r1 = consensus.submit_ioc(&botnet_ioc, &peer_key(1));
assert_eq!(r1, ConsensusResult::Pending(1));
// Peer 2 submits — pending (2/3)
let r2 = consensus.submit_ioc(&botnet_ioc, &peer_key(2));
assert_eq!(r2, ConsensusResult::Pending(2));
// Peer 1 tries again — duplicate
let dup = consensus.submit_ioc(&botnet_ioc, &peer_key(1));
assert_eq!(dup, ConsensusResult::DuplicatePeer);
// Peer 3 submits — threshold reached → Accepted(3)
let r3 = consensus.submit_ioc(&botnet_ioc, &peer_key(3));
assert_eq!(
r3,
ConsensusResult::Accepted(hm::CROSS_VALIDATION_THRESHOLD),
"Third confirmation should trigger acceptance"
);
// Drain accepted IoCs
let accepted = consensus.drain_accepted();
assert_eq!(accepted.len(), 1, "Exactly one IoC should be accepted");
assert_eq!(
accepted[0].confirmations as usize,
hm::CROSS_VALIDATION_THRESHOLD
);
assert_eq!(accepted[0].ioc_type, IoCType::Ja4Fingerprint as u8);
// Reward the 3 confirming peers
for id in 1..=3u8 {
reputation.record_accurate_report(&peer_key(id));
}
// Verify stake increased for reporters
for id in 1..=3u8 {
let stake = reputation.get_stake(&peer_key(id));
assert_eq!(
stake,
hm::INITIAL_STAKE + hm::ACCURACY_REWARD,
"Reporter {id} should have earned accuracy reward"
);
}
// Non-reporters unchanged
let stake4 = reputation.get_stake(&peer_key(4));
assert_eq!(stake4, hm::INITIAL_STAKE);
// Simulate a false reporter and verify slashing
reputation.record_false_report(&peer_key(10));
let stake10 = reputation.get_stake(&peer_key(10));
let expected_slash = hm::INITIAL_STAKE
- (hm::INITIAL_STAKE * hm::SLASHING_PENALTY_PERCENT / 100);
assert_eq!(stake10, expected_slash, "False reporter should be slashed");
// Simulate propagation to remaining 7 nodes (in-memory) and measure latency
let start = Instant::now();
for id in 4..=10u8 {
let r = consensus.submit_ioc(&accepted[0], &peer_key(id));
// After drain, re-submitting creates a fresh pending entry.
// Threshold is 3, so peers 4,5 → Pending; peer 6 → Accepted again;
// then peers 7,8 → Pending; peer 9 → Accepted; peer 10 → Pending.
match r {
ConsensusResult::Pending(_) | ConsensusResult::Accepted(_) => {}
other => panic!(
"Unexpected result for peer {id}: {other:?}"
),
}
}
let propagation = start.elapsed();
eprintln!("[BOTNET] Consensus reached in 3 confirmations, propagation sim: {propagation:.2?}");
assert!(
propagation.as_millis() < 200,
"Propagation simulation should complete in < 200ms"
);
eprintln!("[BOTNET] Consensus + reputation + propagation verified");
}
// ===========================================================================
// Scenario 4 — Federated Learning Stress: 5 rounds with Byzantine actors
// ===========================================================================
#[test]
fn scenario_4_federated_learning_stress() {
let mut aggregator = FedAvgAggregator::new();
let mut defense = GradientDefense::new();
let param_count = {
let model = LocalModel::new(0.01);
model.param_count()
};
let dim = hm::FL_FEATURE_DIM;
// Create 7 honest models and train them briefly on synthetic data
let mut honest_models: Vec<LocalModel> = (0..7)
.map(|_| LocalModel::new(0.01))
.collect();
// Simulated feature vector (mix of benign/malicious patterns)
let mut features = vec![0.0_f32; dim];
for (i, f) in features.iter_mut().enumerate() {
*f = ((i as f32) * 0.314159).sin().abs();
}
// Run 5 FL rounds
for round in 0..5u64 {
assert_eq!(aggregator.current_round(), round);
let mut honest_count = 0;
let mut malicious_rejected = 0;
// Honest peers: train model and submit gradients
for (idx, model) in honest_models.iter_mut().enumerate() {
let target = if idx % 2 == 0 { 1.0 } else { 0.0 };
let _output = model.forward(&features);
let grads = model.backward(target);
// Defense check before aggregation
let verdict = defense.check(&grads);
if verdict == GradientVerdict::Safe {
let result = aggregator.submit_gradients(
&peer_key((idx + 1) as u8),
round,
grads,
);
assert!(result.is_ok(), "Honest peer {idx} submit failed: {result:?}");
honest_count += 1;
}
}
// Byzantine peer 1: free-rider (zero gradients)
let zeros = vec![0.0_f32; param_count];
let v1 = defense.check(&zeros);
assert_eq!(
v1,
GradientVerdict::FreeRider,
"Round {round}: zero gradients should be flagged as free-rider"
);
malicious_rejected += 1;
// Byzantine peer 2: extreme norm (gradient explosion)
let extreme: Vec<f32> = (0..param_count).map(|i| (i as f32) * 100.0).collect();
let v2 = defense.check(&extreme);
assert_eq!(
v2,
GradientVerdict::NormExceeded,
"Round {round}: extreme gradients should exceed norm bound"
);
malicious_rejected += 1;
// Byzantine peer 3: NaN injection
let mut nan_grads = vec![1.0_f32; param_count];
nan_grads[param_count / 2] = f32::NAN;
let submit_nan = aggregator.submit_gradients(&peer_key(100), round, nan_grads);
assert_eq!(
submit_nan,
Err(AggregatorError::InvalidValues),
"Round {round}: NaN gradients should be rejected by aggregator"
);
malicious_rejected += 1;
// Byzantine peer 4: wrong round
let valid_grads = vec![0.5_f32; param_count];
let submit_wrong = aggregator.submit_gradients(
&peer_key(101),
round + 99,
valid_grads,
);
assert_eq!(
submit_wrong,
Err(AggregatorError::WrongRound),
"Round {round}: wrong round should be rejected"
);
assert!(
honest_count >= hm::FL_MIN_PEERS_PER_ROUND,
"Round {round}: need at least {} honest peers, got {honest_count}",
hm::FL_MIN_PEERS_PER_ROUND
);
// Aggregate with trimmed mean
let aggregated = aggregator
.aggregate()
.expect("Aggregation should succeed with enough honest peers");
// Verify aggregated gradient sanity
assert_eq!(aggregated.len(), param_count);
for (i, &val) in aggregated.iter().enumerate() {
assert!(
val.is_finite(),
"Round {round}, dim {i}: aggregated value must be finite"
);
}
// Apply aggregated gradients to each honest model
for model in &mut honest_models {
model.apply_gradients(&aggregated);
}
eprintln!(
"[FL] Round {round}: {honest_count} honest peers, \
{malicious_rejected} malicious rejected, \
aggregated {param_count} params"
);
aggregator.advance_round();
}
assert_eq!(aggregator.current_round(), 5);
eprintln!("[FL] 5 rounds completed — Byzantine resistance verified");
}
// ===========================================================================
// Scenario 5 — ZKP Proof Chain: prove → verify cycle
// ===========================================================================
#[test]
fn scenario_5_zkp_proof_chain() {
let start = Instant::now();
// Test 1: Prove a JA4 fingerprint-based threat
let ja4_fp = b"t13d1516h2_8daaf6152771_e5627efa2ab1";
let proof_ja4 = prover::prove_threat(
Some(ja4_fp),
true, // entropy exceeded
true, // classified malicious
IoCType::Ja4Fingerprint as u8,
None,
);
let result = verifier::verify_threat(&proof_ja4, None);
assert_eq!(
result,
verifier::VerifyResult::ValidStub,
"JA4 proof should verify as valid stub"
);
// Test 2: Prove an entropy anomaly without JA4
let proof_entropy = prover::prove_threat(
None,
true,
true,
IoCType::EntropyAnomaly as u8,
None,
);
let result = verifier::verify_threat(&proof_entropy, None);
assert_eq!(
result,
verifier::VerifyResult::ValidStub,
"Entropy proof should verify as valid stub"
);
// Test 3: Prove a malicious IP detection
let proof_ip = prover::prove_threat(
None,
false,
true,
IoCType::MaliciousIp as u8,
None,
);
let result = verifier::verify_threat(&proof_ip, None);
assert_eq!(
result,
verifier::VerifyResult::ValidStub,
);
// Test 4: Empty proof data
let empty_proof = hm::ThreatProof {
version: 0,
statement: hm::ProofStatement {
ja4_hash: None,
entropy_exceeded: false,
classified_malicious: false,
ioc_type: 0,
},
proof_data: Vec::new(),
created_at: now_secs(),
};
let result = verifier::verify_threat(&empty_proof, None);
assert_eq!(result, verifier::VerifyResult::EmptyProof);
// Test 5: Tampered proof data
let mut tampered = prover::prove_threat(
Some(ja4_fp),
true,
true,
IoCType::Ja4Fingerprint as u8,
None,
);
// Flip a byte in the proof
if let Some(byte) = tampered.proof_data.last_mut() {
*byte ^= 0xFF;
}
let result = verifier::verify_threat(&tampered, None);
assert_eq!(
result,
verifier::VerifyResult::CommitmentMismatch,
"Tampered proof should fail commitment check"
);
// Test 6: Unsupported version
let future_proof = hm::ThreatProof {
version: 99,
statement: hm::ProofStatement {
ja4_hash: None,
entropy_exceeded: false,
classified_malicious: false,
ioc_type: 0,
},
proof_data: vec![0u8; 100],
created_at: now_secs(),
};
let result = verifier::verify_threat(&future_proof, None);
assert_eq!(result, verifier::VerifyResult::UnsupportedVersion);
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 50,
"6 ZKP prove/verify cycles should complete in < 50ms, took {elapsed:.2?}"
);
eprintln!(
"[ZKP] 6 prove/verify cycles completed in {elapsed:.2?} — all correct"
);
}
// ===========================================================================
// Scenario 6 — Full Pipeline: PoW → Consensus → Reputation → ZKP → FL
// ===========================================================================
#[test]
fn scenario_6_full_pipeline_integration() {
let mut guard = SybilGuard::new();
let mut reputation = ReputationStore::new();
let mut consensus = ConsensusEngine::new();
let mut aggregator = FedAvgAggregator::new();
let mut defense = GradientDefense::new();
let pipeline_start = Instant::now();
// --- Phase A: Bootstrap 5 nodes via PoW ---
let node_count = 5u8;
for id in 1..=node_count {
let pk = peer_key(id);
let challenge = SybilGuard::generate_pow(&pk, hm::POW_DIFFICULTY_BITS);
guard
.verify_registration(&challenge)
.unwrap_or_else(|e| panic!("Node {id} PoW failed: {e:?}"));
reputation.register_seed_peer(&pk);
}
eprintln!("[PIPELINE] Phase A: {node_count} nodes bootstrapped");
// --- Phase B: 3 nodes detect a DNS tunnel IoC → consensus ---
let dns_ioc = IoC {
ioc_type: IoCType::DnsTunnel as u8,
severity: ThreatSeverity::Critical as u8,
ip: 0x0A000001, // 10.0.0.1
ja4: None,
entropy_score: Some(9200),
description: "DNS tunneling detected — high entropy in TXT queries".into(),
first_seen: now_secs(),
confirmations: 0,
zkp_proof: Vec::new(),
};
for id in 1..=3u8 {
let r = consensus.submit_ioc(&dns_ioc, &peer_key(id));
if id < 3 {
assert!(matches!(r, ConsensusResult::Pending(_)));
} else {
assert_eq!(r, ConsensusResult::Accepted(3));
}
}
let accepted = consensus.drain_accepted();
assert_eq!(accepted.len(), 1);
// Reward reporters
for id in 1..=3u8 {
reputation.record_accurate_report(&peer_key(id));
}
// --- Phase C: Generate ZKP for the accepted IoC ---
let proof = prover::prove_threat(
None,
true,
true,
accepted[0].ioc_type,
None,
);
let verify = verifier::verify_threat(&proof, None);
assert_eq!(verify, verifier::VerifyResult::ValidStub);
// --- Phase D: One FL round after detection ---
let dim = hm::FL_FEATURE_DIM;
let mut features = vec![0.0_f32; dim];
for (i, f) in features.iter_mut().enumerate() {
*f = ((i as f32) * 0.271828).cos().abs();
}
for id in 1..=node_count {
let mut model = LocalModel::new(0.01);
let _out = model.forward(&features);
let grads = model.backward(1.0); // all train on "malicious"
let verdict = defense.check(&grads);
assert_eq!(verdict, GradientVerdict::Safe);
aggregator
.submit_gradients(&peer_key(id), 0, grads)
.unwrap_or_else(|e| panic!("Node {id} gradient submit failed: {e}"));
}
let global_update = aggregator.aggregate().expect("Aggregation must succeed");
assert!(
global_update.iter().all(|v| v.is_finite()),
"Aggregated model must contain only finite values"
);
// --- Phase E: Verify reputation state ---
for id in 1..=3u8 {
assert!(reputation.is_trusted(&peer_key(id)));
let s = reputation.get_stake(&peer_key(id));
assert!(
s > hm::INITIAL_STAKE,
"Reporter {id} should have earned rewards"
);
}
let pipeline_elapsed = pipeline_start.elapsed();
eprintln!(
"[PIPELINE] Full pipeline (PoW→Consensus→ZKP→FL→Reputation) in {pipeline_elapsed:.2?}"
);
}
// ===========================================================================
// Scenario 7 — Multi-IoC Consensus Storm
// ===========================================================================
#[test]
fn scenario_7_multi_ioc_consensus_storm() {
let mut consensus = ConsensusEngine::new();
let ioc_count = 50;
let start = Instant::now();
// Submit 50 distinct IoCs, each from 3 different peers → all accepted
for i in 0..ioc_count {
let ioc = make_malicious_ip_ioc(0x0A000000 + i as u32);
for peer_id in 1..=3u8 {
consensus.submit_ioc(&ioc, &peer_key(peer_id + (i as u8 * 3)));
}
}
let accepted = consensus.drain_accepted();
assert_eq!(
accepted.len(),
ioc_count,
"All {ioc_count} IoCs should reach consensus"
);
let elapsed = start.elapsed();
eprintln!(
"[STORM] {ioc_count} IoCs × 3 confirmations = {} submissions in {elapsed:.2?}",
ioc_count * 3
);
assert!(
elapsed.as_millis() < 100,
"150 consensus submissions should complete in < 100ms"
);
}
// ===========================================================================
// Scenario 8 — Reputation Slashing Cascade
// ===========================================================================
#[test]
fn scenario_8_reputation_slashing_cascade() {
let mut reputation = ReputationStore::new();
// Register a peer and slash them repeatedly
let pk = peer_key(42);
reputation.register_peer(&pk);
let initial = reputation.get_stake(&pk);
assert_eq!(initial, hm::INITIAL_STAKE);
// Slash multiple times — stake should decrease each time
let mut prev_stake = initial;
let slash_rounds = 5;
for round in 0..slash_rounds {
reputation.record_false_report(&pk);
let new_stake = reputation.get_stake(&pk);
assert!(
new_stake < prev_stake,
"Round {round}: stake should decrease after slashing"
);
prev_stake = new_stake;
}
// After multiple slashings, stake should be below trusted threshold
let final_stake = reputation.get_stake(&pk);
eprintln!(
"[SLASH] Stake after {slash_rounds} slashes: {final_stake} (threshold: {})",
hm::MIN_TRUSTED_REPUTATION
);
// At 25% slashing per round on initial stake of 100:
// 100 → 75 → 56 → 42 → 31 → 23 — below 50 threshold after round 3
assert!(
!reputation.is_trusted(&pk),
"Peer should be untrusted after cascade slashing"
);
}

125
hivemind/tests/ioc_format.rs Executable file
View file

@ -0,0 +1,125 @@
//! Integration tests for the enriched IoC file IPC format.
//!
//! Tests that the JSON Lines format produced by hivemind is correctly parsed
//! and that legacy raw u32 format is still supported.
//!
//! Run: `cargo test -p hivemind --test ioc_format -- --nocapture`
#[test]
fn enriched_ioc_json_format() {
// Verify the JSON format produced by append_accepted_ioc
let test_entries = [
// severity 2 → 1800s
(0x0A000001u32, 2u8, 3u8, 1800u32),
// severity 5 → 3600s
(0x0A000002, 5, 4, 3600),
// severity 7 → 7200s
(0x0A000003, 7, 5, 7200),
// severity 9 → 14400s
(0x0A000004, 9, 3, 14400),
];
for (ip, severity, confirmations, expected_duration) in &test_entries {
// Compute duration the same way as append_accepted_ioc
let duration_secs: u32 = match severity {
0..=2 => 1800,
3..=5 => 3600,
6..=8 => 7200,
_ => 14400,
};
assert_eq!(
duration_secs, *expected_duration,
"severity {} should map to {} seconds",
severity, expected_duration
);
// Verify JSON serialization format
let json = format!(
r#"{{"ip":{},"severity":{},"confirmations":{},"duration_secs":{}}}"#,
ip, severity, confirmations, duration_secs,
);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["ip"], *ip);
assert_eq!(parsed["severity"], *severity);
assert_eq!(parsed["confirmations"], *confirmations);
assert_eq!(parsed["duration_secs"], duration_secs);
}
}
#[test]
fn legacy_u32_format_still_parseable() {
// Old format: one u32 per line
let legacy_content = "167772161\n167772162\n167772163\n";
let mut ips = Vec::new();
for line in legacy_content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
if let Ok(ip) = trimmed.parse::<u32>() {
ips.push(ip);
}
}
}
assert_eq!(ips.len(), 3);
assert_eq!(ips[0], 167772161); // 10.0.0.1
}
#[test]
fn mixed_format_lines() {
// Content with both legacy and enriched lines (during upgrade transition)
let content = r#"167772161
{"ip":167772162,"severity":5,"confirmations":3,"duration_secs":3600}
167772163
{"ip":167772164,"severity":9,"confirmations":5,"duration_secs":14400}
"#;
let mut entries = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('{') {
let parsed: serde_json::Value = serde_json::from_str(trimmed).unwrap();
entries.push((
parsed["ip"].as_u64().unwrap() as u32,
parsed["duration_secs"].as_u64().unwrap() as u32,
));
} else if let Ok(ip) = trimmed.parse::<u32>() {
entries.push((ip, 3600)); // default duration
}
}
assert_eq!(entries.len(), 4);
assert_eq!(entries[0], (167772161, 3600)); // legacy → default
assert_eq!(entries[1], (167772162, 3600)); // enriched
assert_eq!(entries[3], (167772164, 14400)); // enriched high severity
}
#[test]
fn malformed_json_line_skipped() {
let content = r#"{"ip":123,"severity":5
{"ip":167772162,"severity":5,"confirmations":3,"duration_secs":3600}
not_a_number
"#;
let mut valid = 0u32;
let mut invalid = 0u32;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('{') {
if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
valid += 1;
} else {
invalid += 1;
}
} else if trimmed.parse::<u32>().is_ok() {
valid += 1;
} else {
invalid += 1;
}
}
assert_eq!(valid, 1);
assert_eq!(invalid, 2);
}

302
hivemind/tests/stress_mesh.rs Executable file
View file

@ -0,0 +1,302 @@
//! Stress benchmark: concurrent IoC consensus, ZKP proof+verify,
//! FHE encrypt+decrypt, reputation cascades.
//!
//! Run: `cargo test -p hivemind --test stress_mesh -- --nocapture`
use std::time::Instant;
use common::hivemind::IoC;
use hivemind::consensus::{ConsensusEngine, ConsensusResult};
use hivemind::crypto::fhe::FheContext;
use hivemind::reputation::ReputationStore;
use hivemind::zkp::{prover, verifier};
/// Deterministic 32-byte key for peer `id`.
fn peer_key(id: u16) -> [u8; 32] {
let mut key = [0u8; 32];
key[0] = (id >> 8) as u8;
key[1] = (id & 0xFF) as u8;
key[31] = 0xAA;
key
}
fn make_ioc(idx: u16) -> IoC {
IoC {
ioc_type: 0, // MaliciousIp
severity: 7,
ip: 0x0A630000 | idx as u32, // 10.99.x.x
ja4: Some(format!("t13d1517h2_stress_{:04x}", idx)),
entropy_score: Some(7500),
description: format!("stress-ioc-{idx}"),
first_seen: 1_700_000_000 + idx as u64,
confirmations: 0,
zkp_proof: Vec::new(),
}
}
#[test]
fn stress_100_peer_reputation_registration() {
let mut reputation = ReputationStore::new();
let start = Instant::now();
for id in 0..120u16 {
reputation.register_peer(&peer_key(id));
}
let elapsed = start.elapsed();
println!("\n=== 120-PEER REPUTATION REGISTRATION ===");
println!(" Registered: {}", reputation.peer_count());
println!(" Duration: {elapsed:?}");
println!(
" Per-peer: {:.2}µs",
elapsed.as_micros() as f64 / 120.0
);
assert_eq!(reputation.peer_count(), 120);
}
#[test]
fn stress_concurrent_ioc_consensus() {
let mut engine = ConsensusEngine::new();
let start = Instant::now();
let mut accepted = 0u32;
// Submit 200 IoCs, each from 3+ different peers to reach quorum
for ioc_idx in 0..200u16 {
let ioc = make_ioc(ioc_idx);
for voter in 0..4u16 {
let peer = peer_key(voter);
let result = engine.submit_ioc(&ioc, &peer);
if matches!(result, ConsensusResult::Accepted(_)) {
accepted += 1;
}
}
}
let elapsed = start.elapsed();
println!("\n=== IoC CONSENSUS STRESS (200 IoCs × 4 peers) ===");
println!(" IoCs submitted: 200");
println!(" Accepted: {accepted}");
println!(" Duration: {elapsed:?}");
println!(
" Per-IoC: {:.2}µs",
elapsed.as_micros() as f64 / 200.0
);
assert_eq!(accepted, 200, "all IoCs should reach consensus with 4 voters");
}
#[test]
fn stress_zkp_proof_verify_throughput() {
let start = Instant::now();
let iterations = 500u32;
let mut proofs_valid = 0u32;
for i in 0..iterations {
let ja4 = format!("t13d1517h2_8daaf6152771_{:04x}", i);
let proof = prover::prove_threat(
Some(ja4.as_bytes()),
true, // entropy_exceeded
true, // classified_malicious
0, // ioc_type: MaliciousIp
None, // no signing key (v0 stub)
);
if matches!(
verifier::verify_threat(&proof, None),
verifier::VerifyResult::ValidStub
) {
proofs_valid += 1;
}
}
let elapsed = start.elapsed();
println!("\n=== ZKP v0 STUB THROUGHPUT ===");
println!(" Iterations: {iterations}");
println!(" Valid: {proofs_valid}");
println!(" Duration: {elapsed:?}");
println!(
" Per-cycle: {:.2}µs",
elapsed.as_micros() as f64 / iterations as f64
);
assert_eq!(proofs_valid, iterations);
}
#[test]
fn stress_zkp_signed_proof_verify() {
use ring::signature::{Ed25519KeyPair, KeyPair};
use ring::rand::SystemRandom;
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).expect("keygen");
let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).expect("parse");
let pub_key = key_pair.public_key().as_ref();
let start = Instant::now();
let iterations = 200u32;
let mut proofs_valid = 0u32;
for i in 0..iterations {
let ja4 = format!("t13d1517h2_signed_{:04x}", i);
let proof = prover::prove_threat(
Some(ja4.as_bytes()),
true,
true,
0,
Some(&key_pair),
);
if matches!(
verifier::verify_threat(&proof, Some(pub_key)),
verifier::VerifyResult::Valid
) {
proofs_valid += 1;
}
}
let elapsed = start.elapsed();
println!("\n=== ZKP v1 SIGNED THROUGHPUT ===");
println!(" Iterations: {iterations}");
println!(" Valid: {proofs_valid}");
println!(" Duration: {elapsed:?}");
println!(
" Per-cycle: {:.2}µs",
elapsed.as_micros() as f64 / iterations as f64
);
assert_eq!(proofs_valid, iterations);
}
#[test]
fn stress_fhe_encrypt_decrypt_throughput() {
let ctx = FheContext::new_encrypted().expect("fhe init");
let start = Instant::now();
let iterations = 1000u32;
let mut valid = 0u32;
for i in 0..iterations {
// Simulate gradient vectors (10 floats each)
let gradients: Vec<f32> = (0..10)
.map(|j| (i as f32 * 0.01) + (j as f32 * 0.001))
.collect();
let encrypted = ctx.encrypt_gradients(&gradients).expect("encrypt");
let decrypted = ctx.decrypt_gradients(&encrypted).expect("decrypt");
if decrypted.len() == gradients.len() {
valid += 1;
}
}
let elapsed = start.elapsed();
println!("\n=== FHE (AES-256-GCM) GRADIENT THROUGHPUT ===");
println!(" Iterations: {iterations}");
println!(" Payload: 10 floats = 40 bytes each");
println!(" Valid: {valid}");
println!(" Duration: {elapsed:?}");
println!(
" Per-cycle: {:.2}µs",
elapsed.as_micros() as f64 / iterations as f64
);
assert_eq!(valid, iterations);
}
#[test]
fn stress_reputation_slashing_cascade() {
let mut store = ReputationStore::new();
// Register 100 peers
for id in 0..100u16 {
store.register_peer(&peer_key(id));
}
let start = Instant::now();
let mut expelled = 0u32;
// Slash half the peers repeatedly with false reports
for id in 0..50u16 {
let key = peer_key(id);
for _ in 0..10 {
store.record_false_report(&key);
}
if !store.is_trusted(&key) {
expelled += 1;
}
}
// Reward the other half
for id in 50..100u16 {
let key = peer_key(id);
for _ in 0..5 {
store.record_accurate_report(&key);
}
}
let elapsed = start.elapsed();
println!("\n=== REPUTATION CASCADE ===");
println!(" Total peers: 100");
println!(" Slashed: 50 (10× false reports each)");
println!(" Rewarded: 50 (5× accurate reports each)");
println!(" Expelled: {expelled}");
println!(" Duration: {elapsed:?}");
assert!(expelled >= 30, "heavily-slashed peers should lose trust");
}
#[test]
fn stress_full_pipeline_ioc_to_proof() {
use ring::signature::{Ed25519KeyPair, KeyPair};
use ring::rand::SystemRandom;
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).expect("keygen");
let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).expect("parse");
let pub_key = key_pair.public_key().as_ref();
let mut engine = ConsensusEngine::new();
let mut reputation = ReputationStore::new();
// Register 20 peers
for id in 0..20u16 {
reputation.register_peer(&peer_key(id));
}
let start = Instant::now();
let mut end_to_end_valid = 0u32;
// Full pipeline: IoC → consensus → ZKP proof → verify
for ioc_idx in 0..100u16 {
let ioc = make_ioc(ioc_idx);
// Submit from 3 peers — 3rd should trigger acceptance (threshold=3)
for voter in 0..3u16 {
let result = engine.submit_ioc(&ioc, &peer_key(voter));
if matches!(result, ConsensusResult::Accepted(_)) {
// Generate signed ZKP proof for the accepted IoC
let proof = prover::prove_threat(
ioc.ja4.as_ref().map(|s| s.as_bytes()),
ioc.entropy_score.map_or(false, |e| e > 7000),
true,
ioc.ioc_type,
Some(&key_pair),
);
// Verify the proof
if matches!(
verifier::verify_threat(&proof, Some(pub_key)),
verifier::VerifyResult::Valid
) {
end_to_end_valid += 1;
for v in 0..3u16 {
reputation.record_accurate_report(&peer_key(v));
}
}
}
}
}
let elapsed = start.elapsed();
println!("\n=== FULL PIPELINE: IoC → CONSENSUS → ZKP ===");
println!(" IoCs processed: 100");
println!(" E2E valid: {end_to_end_valid}");
println!(" Duration: {elapsed:?}");
println!(
" Per-pipeline: {:.2}µs",
elapsed.as_micros() as f64 / 100.0
);
assert_eq!(end_to_end_valid, 100);
}