#![cfg(feature = "simulation_tests")]
use freenet::config::GlobalTestMetrics;
use freenet::config::{GlobalRng, GlobalSimulationTime, SimulationTransportOpt};
use freenet::dev_tool::{
RequestId, SimNetwork, StreamId, VirtualTime, check_convergence_from_logs,
reset_channel_id_counter, reset_event_id_counter, reset_global_node_index, reset_nonce_counter,
};
use freenet::simulation::TimeSource;
use freenet::transport::in_memory_socket::{
SimulationSocket, clear_all_socket_registries, register_address_network,
register_network_time_source,
};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::net::{Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
struct TestConfig {
name: &'static str,
seed: u64,
gateways: usize,
nodes: usize,
ring_max_htl: usize,
rnd_if_htl_above: usize,
max_connections: usize,
min_connections: usize,
max_contracts: usize,
iterations: usize,
duration: Duration,
event_wait: Duration,
sleep_after_events: Duration,
require_convergence: bool,
latency_range: Option<std::ops::Range<Duration>>,
message_loss_rate: f64,
use_mock_wasm: bool,
}
impl TestConfig {
fn small(name: &'static str, seed: u64) -> Self {
Self {
name,
seed,
gateways: 1,
nodes: 3,
ring_max_htl: 7,
rnd_if_htl_above: 3,
max_connections: 10,
min_connections: 2,
max_contracts: 3,
iterations: 15,
duration: Duration::from_secs(20),
event_wait: Duration::from_millis(200),
sleep_after_events: Duration::from_secs(1),
require_convergence: true,
latency_range: None,
message_loss_rate: 0.0,
use_mock_wasm: false,
}
}
fn medium(name: &'static str, seed: u64) -> Self {
Self {
name,
seed,
gateways: 2,
nodes: 6,
ring_max_htl: 10,
rnd_if_htl_above: 7,
max_connections: 15,
min_connections: 2,
max_contracts: 8,
iterations: 100,
duration: Duration::from_secs(120),
event_wait: Duration::from_millis(200),
sleep_after_events: Duration::from_secs(3),
require_convergence: true,
latency_range: None,
message_loss_rate: 0.0,
use_mock_wasm: false,
}
}
fn large(name: &'static str, seed: u64) -> Self {
Self {
name,
seed,
gateways: 3,
nodes: 12,
ring_max_htl: 12,
rnd_if_htl_above: 8,
max_connections: 12,
min_connections: 6,
max_contracts: 15,
iterations: 150,
duration: Duration::from_secs(300),
event_wait: Duration::from_millis(200),
sleep_after_events: Duration::from_secs(10),
require_convergence: true,
latency_range: None,
message_loss_rate: 0.0,
use_mock_wasm: false,
}
}
#[allow(dead_code)]
fn long_running(name: &'static str, seed: u64) -> Self {
Self {
name,
seed,
gateways: 2,
nodes: 6,
ring_max_htl: 10,
rnd_if_htl_above: 5,
max_connections: 15,
min_connections: 3,
max_contracts: 8,
iterations: 360, duration: Duration::from_secs(3700), event_wait: Duration::from_secs(10), sleep_after_events: Duration::from_secs(10), require_convergence: true,
latency_range: Some(Duration::from_millis(10)..Duration::from_millis(50)),
message_loss_rate: 0.0,
use_mock_wasm: false,
}
}
fn with_nodes(mut self, nodes: usize) -> Self {
self.nodes = nodes;
self
}
fn with_gateways(mut self, gateways: usize) -> Self {
self.gateways = gateways;
self
}
fn with_iterations(mut self, iterations: usize) -> Self {
self.iterations = iterations;
self
}
fn with_max_contracts(mut self, max_contracts: usize) -> Self {
self.max_contracts = max_contracts;
self
}
fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
fn with_sleep(mut self, sleep: Duration) -> Self {
self.sleep_after_events = sleep;
self
}
fn with_connections(mut self, min: usize, max: usize) -> Self {
self.min_connections = min;
self.max_connections = max;
self
}
fn with_htl(mut self, max_htl: usize, rnd_above: usize) -> Self {
self.ring_max_htl = max_htl;
self.rnd_if_htl_above = rnd_above;
self
}
fn require_convergence(mut self) -> Self {
self.require_convergence = true;
self
}
fn no_convergence_wait(mut self) -> Self {
self.require_convergence = false;
self
}
#[allow(dead_code)]
fn with_latency(mut self, min: Duration, max: Duration) -> Self {
self.latency_range = Some(min..max);
self
}
fn with_event_wait(mut self, event_wait: Duration) -> Self {
self.event_wait = event_wait;
self
}
fn with_message_loss(mut self, rate: f64) -> Self {
self.message_loss_rate = rate.clamp(0.0, 1.0);
self
}
fn with_mock_wasm(mut self) -> Self {
self.use_mock_wasm = true;
self
}
fn run(self) -> TestResult {
use freenet::simulation::FaultConfig;
setup_deterministic_state(self.seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let mut sim = SimNetwork::new(
self.name,
self.gateways,
self.nodes,
self.ring_max_htl,
self.rnd_if_htl_above,
self.max_connections,
self.min_connections,
self.seed,
)
.await;
let has_latency = self.latency_range.is_some();
let has_loss = self.message_loss_rate > 0.0;
if has_latency || has_loss {
let mut builder = FaultConfig::builder();
if let Some(ref latency) = self.latency_range {
builder = builder.latency_range(latency.clone());
tracing::info!(
"Latency jitter enabled: {:?} - {:?}",
latency.start,
latency.end
);
}
if has_loss {
builder = builder.message_loss_rate(self.message_loss_rate);
tracing::info!(
"Message loss enabled: {:.1}%",
self.message_loss_rate * 100.0
);
}
sim.with_fault_injection(builder.build());
}
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let sleep_duration = self.sleep_after_events;
let event_wait = self.event_wait;
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
self.seed,
self.max_contracts,
self.iterations,
self.duration,
event_wait,
move || async move {
tokio::time::sleep(sleep_duration).await;
Ok(())
},
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let event_count = rt.block_on(async { logs_handle.lock().await.len() });
TestResult {
seed: self.seed,
name: self.name,
simulation_result: result,
convergence,
event_count,
require_convergence: self.require_convergence,
logs_handle,
}
}
#[allow(dead_code)] fn run_direct(self) -> TestResult {
use freenet::simulation::FaultConfig;
setup_deterministic_state(self.seed);
let rt = create_runtime();
let use_mock_wasm = self.use_mock_wasm;
let skip_convergence_wait = !self.require_convergence;
let (sim, logs_handle) = rt.block_on(async {
let mut sim = SimNetwork::new(
self.name,
self.gateways,
self.nodes,
self.ring_max_htl,
self.rnd_if_htl_above,
self.max_connections,
self.min_connections,
self.seed,
)
.await;
sim.use_mock_wasm = use_mock_wasm;
sim.skip_convergence_wait = skip_convergence_wait;
let has_latency = self.latency_range.is_some();
let has_loss = self.message_loss_rate > 0.0;
if has_latency || has_loss {
let mut builder = FaultConfig::builder();
if let Some(ref latency) = self.latency_range {
builder = builder.latency_range(latency.clone());
tracing::info!(
"Latency jitter enabled: {:?} - {:?}",
latency.start,
latency.end
);
}
if has_loss {
builder = builder.message_loss_rate(self.message_loss_rate);
tracing::info!(
"Message loss enabled: {:.1}%",
self.message_loss_rate * 100.0
);
}
sim.with_fault_injection(builder.build());
}
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
drop(rt);
let direct_result = sim.run_simulation_direct::<rand::rngs::SmallRng>(
self.seed,
self.max_contracts,
self.iterations,
self.event_wait,
);
let simulation_result: turmoil::Result = match direct_result {
Ok(()) => Ok(()),
Err(e) => Err(Box::new(std::io::Error::other(e.to_string()))),
};
let rt = create_runtime();
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let event_count = rt.block_on(async { logs_handle.lock().await.len() });
TestResult {
seed: self.seed,
name: self.name,
simulation_result,
convergence,
event_count,
require_convergence: self.require_convergence,
logs_handle,
}
}
}
struct TestResult {
seed: u64,
name: &'static str,
simulation_result: turmoil::Result,
convergence: freenet::dev_tool::ConvergenceResult,
event_count: usize,
require_convergence: bool,
logs_handle: Arc<Mutex<Vec<freenet::tracing::NetLogMessage>>>,
}
impl TestResult {
fn assert_ok(self) -> Self {
if let Err(e) = &self.simulation_result {
tracing::error!("============================================================");
tracing::error!("SIMULATION TEST FAILED: {}", self.name);
tracing::error!("============================================================");
tracing::error!("Seed for reproduction: 0x{:X}", self.seed);
tracing::error!("Error: {:?}", e);
tracing::error!("============================================================");
}
assert!(
self.simulation_result.is_ok(),
"{} failed: {:?}",
self.name,
self.simulation_result.err()
);
self
}
fn check_convergence(self) -> Self {
tracing::info!("=== CONVERGENCE CHECK: {} ===", self.name);
tracing::info!(
"Result: {} converged, {} diverged, {} events",
self.convergence.converged.len(),
self.convergence.diverged.len(),
self.event_count
);
let rt = create_runtime();
let logs = rt.block_on(async { self.logs_handle.lock().await.clone() });
for diverged in &self.convergence.diverged {
tracing::warn!(
"DIVERGED: {} - {} unique states across {} peers",
diverged.contract_key,
diverged.unique_state_count(),
diverged.peer_states.len()
);
for (peer, hash) in &diverged.peer_states {
tracing::warn!(" peer {}: {}", peer, hash);
}
tracing::debug!(
"Stored state events for contract {}:",
diverged.contract_key
);
for log in &logs {
let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
if contract_key.as_ref() == Some(&diverged.contract_key) {
if let Some(state_hash) = log.kind.stored_state_hash() {
let variant = log.kind.variant_name();
tracing::debug!(
" {} @ {}: {} -> {}",
log.tx,
log.peer_id.socket_addr(),
variant,
&state_hash[..16]
);
}
}
}
tracing::debug!("Subscribe events for contract {}:", diverged.contract_key);
for log in &logs {
let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
if contract_key.as_ref() == Some(&diverged.contract_key) {
let variant = log.kind.variant_name();
if variant.contains("Subscribe") {
tracing::debug!(
" {} @ {}: {}",
log.tx,
log.peer_id.socket_addr(),
variant
);
}
}
}
}
if self.require_convergence {
assert!(
self.convergence.is_converged(),
"{} convergence failed: {} converged, {} diverged",
self.name,
self.convergence.converged.len(),
self.convergence.diverged.len()
);
}
tracing::info!("{} PASSED", self.name);
self
}
fn verify_operation_coverage(self) -> Self {
let rt = create_runtime();
let logs = rt.block_on(async { self.logs_handle.lock().await.clone() });
let mut put_count = 0;
let mut get_count = 0;
let mut update_count = 0;
let mut subscribe_count = 0;
let mut contracts_with_puts: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut puts_per_contract: std::collections::HashMap<
String,
std::collections::HashSet<(String, String)>,
> = std::collections::HashMap::new();
for log in &logs {
let variant = log.kind.variant_name();
if variant.starts_with("Put") {
put_count += 1;
if let Some(key) = log.kind.contract_key() {
let key_str = format!("{:?}", key);
contracts_with_puts.insert(key_str.clone());
let peer_addr = format!("{}", log.peer_id.socket_addr());
let state_hash = log
.kind
.state_hash()
.map(|h| h[..8].to_string())
.unwrap_or_default();
puts_per_contract
.entry(key_str)
.or_default()
.insert((peer_addr, state_hash));
}
} else if variant.starts_with("Get") {
get_count += 1;
} else if variant.starts_with("Update") {
update_count += 1;
} else if variant.starts_with("Subscribe") {
subscribe_count += 1;
}
}
let contracts_with_concurrent_puts: Vec<_> = puts_per_contract
.iter()
.filter(|(_, puts)| puts.len() > 1)
.collect();
tracing::info!("=== OPERATION COVERAGE: {} ===", self.name);
tracing::info!("PUT operations: {}", put_count);
tracing::info!("GET operations: {}", get_count);
tracing::info!("UPDATE operations: {}", update_count);
tracing::info!("SUBSCRIBE operations: {}", subscribe_count);
tracing::info!("Contracts with PUTs: {}", contracts_with_puts.len());
tracing::info!(
"Contracts with concurrent PUTs: {}",
contracts_with_concurrent_puts.len()
);
for (contract, puts) in &contracts_with_concurrent_puts {
tracing::debug!(
" Contract {} has {} concurrent puts from peers:",
&contract[..20],
puts.len()
);
for (peer, hash) in puts.iter().take(5) {
tracing::debug!(" peer={} state={}", peer, hash);
}
}
assert!(
put_count > 0,
"{}: No PUT operations detected - test not exercising puts",
self.name
);
assert!(
get_count > 0,
"{}: No GET operations detected - test not exercising gets",
self.name
);
assert!(
update_count > 0,
"{}: No UPDATE operations detected - test not exercising updates",
self.name
);
assert!(
subscribe_count > 0,
"{}: No SUBSCRIBE operations detected - test not exercising subscribes",
self.name
);
assert!(
!contracts_with_puts.is_empty(),
"{}: No contracts with PUTs - test not exercising contract creation",
self.name
);
tracing::info!(
"{}: Operation coverage verified (PUT={}, GET={}, UPDATE={}, SUBSCRIBE={}, contracts={})",
self.name,
put_count,
get_count,
update_count,
subscribe_count,
contracts_with_puts.len()
);
self
}
fn verify_state_report(self) -> Self {
let rt = create_runtime();
let report = rt.block_on(async {
let logs = self.logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!("=== ANOMALY DETECTION REPORT: {} ===", self.name);
tracing::info!(
"Events analyzed: {} total, {} state-mutating",
report.total_events,
report.state_events
);
tracing::info!("Contracts analyzed: {}", report.contracts_analyzed);
tracing::info!("Total anomalies: {}", report.anomalies.len());
if report.anomalies.is_empty() {
tracing::info!(
"{}: State verification CLEAN - no anomalies detected",
self.name
);
} else {
let divergences = report.divergences();
let missing = report.missing_broadcasts();
let unapplied = report.unapplied_broadcasts();
let partitions = report.suspected_partitions();
let stale = report.stale_peers();
let oscillations = report.state_oscillations();
let zombies = report.zombie_transactions();
let storms = report.broadcast_storms();
let cascades = report.delta_sync_cascades();
tracing::warn!(
"{}: {} anomalies detected: divergences={}, missing_broadcasts={}, \
unapplied_broadcasts={}, partitions={}, stale_peers={}, oscillations={}, \
zombies={}, storms={}, cascades={}",
self.name,
report.anomalies.len(),
divergences.len(),
missing.len(),
unapplied.len(),
partitions.len(),
stale.len(),
oscillations.len(),
zombies.len(),
storms.len(),
cascades.len(),
);
for (i, anomaly) in report.anomalies.iter().enumerate() {
tracing::debug!("{}: anomaly[{}] = {:?}", self.name, i, anomaly);
}
}
self
}
fn router_snapshots(&self) -> Vec<(usize, usize, bool)> {
let rt = create_runtime();
rt.block_on(async {
let logs = self.logs_handle.lock().await;
logs.iter()
.filter_map(|log| log.kind.router_snapshot_summary())
.collect()
})
}
}
fn setup_deterministic_state(seed: u64) {
GlobalRng::set_seed(seed);
const BASE_EPOCH_MS: u64 = 1577836800000; const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + (seed % RANGE_MS));
GlobalTestMetrics::reset();
SimulationTransportOpt::disable();
freenet::dev_tool::clear_crdt_contracts();
RequestId::reset_counter();
freenet::dev_tool::ClientId::reset_counter();
reset_event_id_counter();
reset_channel_id_counter();
StreamId::reset_counter();
reset_nonce_counter();
reset_global_node_index();
}
fn create_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TraceFingerprint {
total_events: usize,
sequence_hash: u64,
counts_hash: u64,
}
impl TraceFingerprint {
fn from_events(events: &[String], event_counts: &HashMap<String, usize>) -> Self {
let mut seq_hasher = std::collections::hash_map::DefaultHasher::new();
for event in events {
event.hash(&mut seq_hasher);
}
let sorted_counts: BTreeMap<&String, &usize> = event_counts.iter().collect();
let mut counts_hasher = std::collections::hash_map::DefaultHasher::new();
for (k, v) in &sorted_counts {
k.hash(&mut counts_hasher);
v.hash(&mut counts_hasher);
}
TraceFingerprint {
total_events: events.len(),
sequence_hash: seq_hasher.finish(),
counts_hash: counts_hasher.finish(),
}
}
}
#[test_log::test]
fn test_strict_determinism_exact_event_equality() {
const SEED: u64 = 0xDE7E_2A1E_1234;
#[derive(Debug, PartialEq)]
struct SimulationTrace {
event_counts: HashMap<String, usize>,
event_sequence: Vec<String>, total_events: usize,
}
fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, SimulationTrace) {
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
name, 2, 18, 10, 3, 15, 5, seed,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
seed,
10, 40, Duration::from_secs(30), Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
let trace = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut event_counts: HashMap<String, usize> = HashMap::new();
let mut event_sequence: Vec<String> = Vec::new();
for log in logs.iter() {
let kind_name = log.kind.variant_name().to_string();
*event_counts.entry(kind_name.clone()).or_insert(0) += 1;
event_sequence.push(kind_name);
}
SimulationTrace {
total_events: logs.len(),
event_counts,
event_sequence,
}
});
(result, trace)
}
let (result1, trace1) = run_and_trace("strict-det-run1", SEED);
let (result2, trace2) = run_and_trace("strict-det-run2", SEED);
let (result3, trace3) = run_and_trace("strict-det-run3", SEED);
assert_eq!(
result1.is_ok(),
result2.is_ok(),
"STRICT DETERMINISM FAILURE: Simulation outcomes differ!\nRun 1: {:?}\nRun 2: {:?}",
result1,
result2
);
assert_eq!(
result2.is_ok(),
result3.is_ok(),
"STRICT DETERMINISM FAILURE: Simulation outcomes differ!\nRun 2: {:?}\nRun 3: {:?}",
result2,
result3
);
if trace1.total_events != trace2.total_events || trace2.total_events != trace3.total_events {
tracing::info!("\n=== DETERMINISM DEBUG ===");
tracing::info!(
"Run 1 total: {}, Run 2 total: {}, Run 3 total: {}",
trace1.total_events,
trace2.total_events,
trace3.total_events
);
let mut all_types: std::collections::BTreeSet<&String> =
trace1.event_counts.keys().collect();
all_types.extend(trace2.event_counts.keys());
all_types.extend(trace3.event_counts.keys());
for event_type in all_types {
let count1 = trace1.event_counts.get(event_type).unwrap_or(&0);
let count2 = trace2.event_counts.get(event_type).unwrap_or(&0);
let count3 = trace3.event_counts.get(event_type).unwrap_or(&0);
if count1 != count2 || count2 != count3 {
tracing::info!(
" {} : {} vs {} vs {} (DIFFERS)",
event_type,
count1,
count2,
count3
);
}
}
tracing::info!("=========================\n");
}
assert_eq!(
trace1.total_events, trace2.total_events,
"STRICT DETERMINISM FAILURE: Total event counts differ!"
);
assert_eq!(
trace2.total_events, trace3.total_events,
"STRICT DETERMINISM FAILURE: Total event counts differ!"
);
assert_eq!(
trace1.event_counts, trace2.event_counts,
"STRICT DETERMINISM FAILURE: Event counts per type differ!"
);
assert_eq!(
trace2.event_counts, trace3.event_counts,
"STRICT DETERMINISM FAILURE: Event counts per type differ!"
);
for (i, ((e1, e2), e3)) in trace1
.event_sequence
.iter()
.zip(trace2.event_sequence.iter())
.zip(trace3.event_sequence.iter())
.enumerate()
{
assert_eq!(e1, e2, "Event sequence differs at index {}!", i);
assert_eq!(e2, e3, "Event sequence differs at index {}!", i);
}
let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");
tracing::info!(
"STRICT DETERMINISM TEST PASSED: {} events, fingerprint={:#018x}",
trace1.total_events,
fp1.sequence_hash
);
}
#[test_log::test]
fn test_strict_determinism_multi_gateway() {
const SEED: u64 = 0xAB17_6A7E_1234;
#[derive(Debug, PartialEq)]
struct SimulationTrace {
event_counts: HashMap<String, usize>,
event_sequence: Vec<String>,
total_events: usize,
}
fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, SimulationTrace) {
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
name, 2, 6, 7, 3, 10, 2, seed,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
seed,
5,
20,
Duration::from_secs(30),
Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
let trace = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut event_counts: HashMap<String, usize> = HashMap::new();
let mut event_sequence: Vec<String> = Vec::new();
for log in logs.iter() {
let kind_name = log.kind.variant_name().to_string();
*event_counts.entry(kind_name.clone()).or_insert(0) += 1;
event_sequence.push(kind_name);
}
SimulationTrace {
total_events: logs.len(),
event_counts,
event_sequence,
}
});
(result, trace)
}
let (result1, trace1) = run_and_trace("multi-gw-det-run1", SEED);
let (result2, trace2) = run_and_trace("multi-gw-det-run2", SEED);
let (result3, trace3) = run_and_trace("multi-gw-det-run3", SEED);
assert_eq!(result1.is_ok(), result2.is_ok());
assert_eq!(result2.is_ok(), result3.is_ok());
assert_eq!(trace1.total_events, trace2.total_events);
assert_eq!(trace2.total_events, trace3.total_events);
assert_eq!(trace1.event_counts, trace2.event_counts);
assert_eq!(trace2.event_counts, trace3.event_counts);
for (i, ((e1, e2), e3)) in trace1
.event_sequence
.iter()
.zip(trace2.event_sequence.iter())
.zip(trace3.event_sequence.iter())
.enumerate()
{
assert_eq!(e1, e2, "Event sequence differs at index {}!", i);
assert_eq!(e2, e3, "Event sequence differs at index {}!", i);
}
let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");
tracing::info!(
"MULTI-GATEWAY DETERMINISM TEST PASSED: {} events (2 gateways), fingerprint={:#018x}",
trace1.total_events,
fp1.sequence_hash
);
}
#[test_log::test]
fn test_deterministic_replay_events() {
const SEED: u64 = 0xDEAD_BEEF_1234;
#[derive(Debug, PartialEq)]
struct ReplayTrace {
event_counts: HashMap<String, usize>,
total_events: usize,
}
fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, ReplayTrace) {
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(name, 1, 3, 7, 3, 10, 2, seed).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
seed,
3,
10,
Duration::from_secs(20),
Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
let trace = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut event_counts: HashMap<String, usize> = HashMap::new();
for log in logs.iter() {
let kind_name = log.kind.variant_name().to_string();
*event_counts.entry(kind_name).or_insert(0) += 1;
}
ReplayTrace {
total_events: logs.len(),
event_counts,
}
});
(result, trace)
}
let (result1, trace1) = run_and_trace("replay-run1", SEED);
let (result2, trace2) = run_and_trace("replay-run2", SEED);
assert_eq!(result1.is_ok(), result2.is_ok());
assert!(trace1.total_events > 0);
assert_eq!(trace1.event_counts, trace2.event_counts);
assert_eq!(trace1.total_events, trace2.total_events);
let fp1 = TraceFingerprint::from_events(&[], &trace1.event_counts);
let fp2 = TraceFingerprint::from_events(&[], &trace2.event_counts);
assert_eq!(
fp1.counts_hash, fp2.counts_hash,
"Counts fingerprint mismatch between replay runs"
);
tracing::info!(
"Deterministic replay test passed - {} events, counts_hash={:#018x}",
trace1.total_events,
fp1.counts_hash
);
}
#[test_log::test]
fn test_sim_network_basic_setup() {
#[allow(clippy::unusual_byte_groupings)]
const SEED: u64 = 0xBA51C_5E70_0001;
setup_deterministic_state(SEED);
let rt = create_runtime();
rt.block_on(async {
let mut sim = SimNetwork::new("basic-setup", 1, 5, 7, 3, 10, 2, SEED).await;
sim.with_start_backoff(Duration::from_millis(100));
let peers = sim.build_peers();
assert_eq!(peers.len(), 6, "Expected 1 gateway + 5 nodes = 6 peers");
let gateway_count = peers.iter().filter(|(l, _)| !l.is_node()).count();
let node_count = peers.iter().filter(|(l, _)| l.is_node()).count();
assert_eq!(gateway_count, 1, "Expected 1 gateway");
assert_eq!(node_count, 5, "Expected 5 regular nodes");
});
}
#[test_log::test]
fn test_sim_network_peer_startup() {
TestConfig::small("peer-startup", 0xBEE2_5747_0001)
.with_nodes(2)
.with_iterations(5)
.with_duration(Duration::from_secs(15))
.run()
.assert_ok();
}
#[test_log::test]
fn test_sim_network_connectivity() {
TestConfig::small("connectivity-test", 0xC0EE_3C70_0001)
.with_max_contracts(2)
.with_iterations(10)
.run()
.assert_ok();
}
#[test_log::test]
fn ci_quick_simulation() {
TestConfig::small("ci-quick-sim", 0xC1F1_ED5E_ED00)
.with_nodes(4)
.with_max_contracts(5)
.with_iterations(50)
.with_duration(Duration::from_secs(45))
.with_sleep(Duration::from_secs(2))
.run()
.assert_ok()
.verify_operation_coverage()
.check_convergence()
.verify_state_report();
}
#[test_log::test]
fn ci_medium_simulation() {
TestConfig::medium("ci-medium-sim", 0xC1F1_ED7E_ED01)
.run()
.assert_ok()
.verify_operation_coverage()
.check_convergence()
.verify_state_report();
}
#[test_log::test]
fn mock_wasm_runtime_simulation() {
TestConfig::small("mock-wasm-sim", 0xA0C1_1A5E_0001)
.with_mock_wasm()
.run_direct()
.assert_ok()
.verify_state_report();
}
#[test_log::test]
fn replica_validation_and_stepwise_consistency() {
TestConfig::medium("replica-validation", 0xBEE1_1CA5_0001)
.with_gateways(2)
.with_nodes(8)
.with_htl(8, 4)
.with_connections(4, 8)
.with_max_contracts(10)
.with_iterations(90)
.with_duration(Duration::from_secs(180))
.with_sleep(Duration::from_secs(5))
.require_convergence()
.run()
.assert_ok()
.verify_operation_coverage()
.check_convergence()
.verify_state_report();
}
#[test_log::test]
fn dense_network_replication() {
TestConfig::large("dense-network", 0xDE05_E0F0_0001)
.require_convergence()
.run()
.assert_ok()
.verify_operation_coverage()
.check_convergence()
.verify_state_report();
}
#[test_log::test]
fn test_turmoil_determinism_verification() {
const SEED: u64 = 0xDE7E_2A11_0001;
fn run_simulation(name: &'static str, seed: u64) -> Vec<String> {
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(name, 1, 3, 7, 3, 10, 2, seed).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
seed,
3,
15,
Duration::from_secs(20),
Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
assert!(result.is_ok(), "Simulation failed: {:?}", result.err());
rt.block_on(async {
let logs = logs_handle.lock().await;
logs.iter()
.map(|log| format!("{:?}", log.kind.variant_name()))
.collect()
})
}
let events1 = run_simulation("det-verify-1", SEED);
let events2 = run_simulation("det-verify-2", SEED);
assert_eq!(
events1.len(),
events2.len(),
"Event counts differ: {} vs {}",
events1.len(),
events2.len()
);
for (i, (e1, e2)) in events1.iter().zip(events2.iter()).enumerate() {
assert_eq!(e1, e2, "Event {} differs: {:?} vs {:?}", i, e1, e2);
}
let counts1: HashMap<String, usize> = {
let mut m = HashMap::new();
for e in &events1 {
*m.entry(e.clone()).or_insert(0) += 1;
}
m
};
let counts2: HashMap<String, usize> = {
let mut m = HashMap::new();
for e in &events2 {
*m.entry(e.clone()).or_insert(0) += 1;
}
m
};
let fp1 = TraceFingerprint::from_events(&events1, &counts1);
let fp2 = TraceFingerprint::from_events(&events2, &counts2);
assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
tracing::info!(
"Determinism verified: {} identical events, fingerprint={:#018x}",
events1.len(),
fp1.sequence_hash
);
}
#[test_log::test]
fn test_graceful_shutdown_no_deadlock() {
const SEED: u64 = 0xDEAD_BEEF_CAFE;
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new("graceful-shutdown-test", 1, 2, 7, 3, 10, 2, SEED).await
});
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
10,
3,
Duration::from_secs(30),
Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(5)).await;
tracing::info!("Test client: simulation will end gracefully");
Ok(())
},
);
assert!(
result.is_ok(),
"Simulation should complete without deadlock: {:?}",
result.err()
);
tracing::info!("Graceful shutdown test passed - no deadlock under Turmoil");
}
#[test_log::test]
fn test_graceful_shutdown_typed_error() {
use freenet::EventLoopExitReason;
let graceful = EventLoopExitReason::GracefulShutdown;
let unexpected = EventLoopExitReason::UnexpectedStreamEnd;
assert_eq!(graceful.to_string(), "Graceful shutdown");
assert_eq!(
unexpected.to_string(),
"Network event stream ended unexpectedly"
);
tracing::info!("Typed error test passed");
}
#[test_log::test]
fn test_high_latency_timeout_regression() {
use freenet::simulation::FaultConfig;
const SEED: u64 = 0xDEAD_BEEF_1234;
tracing::info!(
"Starting high-latency regression test with seed: 0x{:X}",
SEED
);
setup_deterministic_state(SEED);
let rt = create_runtime();
rt.block_on(async {
let mut sim = SimNetwork::new(
"high-latency-regression",
1, 3, 7, 3, 10, 2, SEED,
)
.await;
let fault_config = FaultConfig::builder()
.latency_range(Duration::from_millis(150)..Duration::from_millis(200))
.build();
sim.with_fault_injection(fault_config);
sim.with_start_backoff(Duration::from_millis(100));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 1, 1)
.await;
for _ in 0..50 {
sim.advance_time(Duration::from_millis(100));
tokio::task::yield_now().await;
}
match sim
.check_partial_connectivity(Duration::from_secs(60), 0.5)
.await
{
Ok(()) => {
tracing::info!("High-latency network established connectivity");
}
Err(e) => {
tracing::warn!(
"High-latency connectivity check incomplete: {} (this may be acceptable)",
e
);
}
}
let summary = sim.get_operation_summary().await;
let total_timeouts = summary.timeouts;
tracing::info!(
"High-latency test results: {} total timeouts, {:.1}% success rate",
total_timeouts,
summary.overall_success_rate() * 100.0
);
assert!(
total_timeouts < 50,
"Expected <50 timeouts with MIN_RTO=300ms fix, got {} (timeout storm detected!)",
total_timeouts
);
tracing::info!("High-latency regression test PASSED - no timeout storm detected");
});
}
#[test]
fn test_turmoil_with_real_simulation_socket() -> turmoil::Result {
clear_all_socket_registries();
let network_name = "turmoil-test";
let virtual_time = VirtualTime::new();
register_network_time_source(network_name, virtual_time);
let server_addr: SocketAddr = (Ipv6Addr::LOCALHOST, 18000).into();
let client_addr: SocketAddr = (Ipv6Addr::LOCALHOST, 19000).into();
register_address_network(server_addr, network_name);
register_address_network(client_addr, network_name);
let mut sim = turmoil::Builder::new()
.simulation_duration(Duration::from_secs(10))
.rng_seed(0xDEAD_BEEF_CAFE_1234)
.build();
sim.host("server", move || async move {
let socket = SimulationSocket::bind(server_addr)
.await
.expect("Server bind failed");
tracing::info!("Server bound to {:?}", server_addr);
let mut buf = [0u8; 1024];
match socket.recv_from(&mut buf).await {
Ok((len, from)) => {
let msg = &buf[..len];
tracing::info!("Server received {:?} from {:?}", msg, from);
socket.send_to(b"pong", from).await.ok();
}
Err(e) => {
tracing::error!("Server recv error: {:?}", e);
}
}
Ok(())
});
sim.client("client", async move {
tokio::time::sleep(Duration::from_millis(100)).await;
let socket = SimulationSocket::bind(client_addr)
.await
.expect("Client bind failed");
tracing::info!("Client bound to {:?}", client_addr);
socket.send_to(b"ping", server_addr).await.ok();
tracing::info!("Client sent ping");
let mut buf = [0u8; 1024];
match tokio::time::timeout(Duration::from_secs(5), socket.recv_from(&mut buf)).await {
Ok(Ok((len, from))) => {
let msg = &buf[..len];
tracing::info!("Client received {:?} from {:?}", msg, from);
assert_eq!(msg, b"pong");
assert_eq!(from, server_addr);
}
Ok(Err(e)) => {
tracing::error!("Client recv error: {:?}", e);
}
Err(_) => {
tracing::error!("Client recv timeout");
}
}
Ok(())
});
sim.run()
}
const SOURCE_THRESHOLD: f64 = 0.05;
const SINGLE_HOSTER_NETWORK: &str = "single-hoster-test";
fn ring_distance(a: f64, b: f64) -> f64 {
let diff = (a - b).abs();
diff.min(1.0 - diff)
}
#[test_log::test]
fn test_topology_single_hoster() {
use freenet::dev_tool::{
Location, NodeLabel, ScheduledOperation, SimOperation, validate_topology_from_snapshots,
};
const SEED: u64 = 0x5EED_0001_CAFE;
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, gateway_location) = rt.block_on(async {
let sim = SimNetwork::new(
SINGLE_HOSTER_NETWORK,
1, 2, 7, 3, 10, 2, SEED,
)
.await;
let locations = sim.get_peer_locations();
let gateway_loc = locations
.first()
.copied()
.expect("Should have at least one gateway");
(sim, gateway_loc)
});
let _ = gateway_location;
let contract_seed = 42u8;
let contract = SimOperation::create_test_contract(contract_seed);
let contract_id = *contract.key().id();
let initial_state = SimOperation::create_test_state(contract_seed);
let operations = vec![ScheduledOperation::new(
NodeLabel::gateway(SINGLE_HOSTER_NETWORK, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state.clone(),
subscribe: true,
},
)];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120), Duration::from_secs(45), );
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete successfully: {:?}",
result.turmoil_result.err()
);
let snapshots = result.topology_snapshots;
tracing::info!("Captured {} topology snapshots", snapshots.len());
let gateway_with_contract = snapshots
.iter()
.find(|snap| snap.contracts.contains_key(&contract_id));
assert!(
gateway_with_contract.is_some(),
"Gateway should have the contract in its topology snapshot. \
Available snapshots: {} peers",
snapshots.len()
);
let gateway_snap = gateway_with_contract.unwrap();
let contract_sub = gateway_snap.contracts.get(&contract_id).unwrap();
let contract_location = Location::from(&contract_id).as_f64();
let gateway_location = gateway_snap.location;
let gateway_distance = ring_distance(gateway_location, contract_location);
tracing::info!(
"Contract location: {:.4}, Gateway location: {:.4}, Distance: {:.4}, Threshold: {:.4}",
contract_location,
gateway_location,
gateway_distance,
SOURCE_THRESHOLD
);
assert!(
contract_sub.is_hosting,
"Gateway should be hosting the contract after PUT with subscribe=true"
);
tracing::info!(
"Gateway {} is hosting contract: upstream={:?}, downstream={:?}",
gateway_snap.peer_addr,
contract_sub.upstream,
contract_sub.downstream
);
let result = validate_topology_from_snapshots(&snapshots, &contract_id, contract_location);
tracing::info!(
"Topology validation: cycles={}, orphans={}, disconnected={}, unreachable={}, proximity={}",
result.bidirectional_cycles.len(),
result.orphan_hosters.len(),
result.disconnected_upstream.len(),
result.unreachable_hosters.len(),
result.proximity_violations.len()
);
assert!(
result.bidirectional_cycles.is_empty(),
"Single hoster should have no bidirectional cycles, found: {:?}",
result.bidirectional_cycles
);
let gateway_is_source = gateway_distance < SOURCE_THRESHOLD;
if gateway_is_source {
tracing::info!(
"Gateway IS source (distance={:.4} < threshold={:.4}) - validating healthy topology",
gateway_distance,
SOURCE_THRESHOLD
);
assert!(
result.orphan_hosters.is_empty(),
"Source hoster (within threshold) should not be marked as orphan, found: {:?}",
result.orphan_hosters
);
assert!(
result.disconnected_upstream.is_empty(),
"Source hoster (within threshold) should not be disconnected, found: {:?}",
result.disconnected_upstream
);
assert!(
result.unreachable_hosters.is_empty(),
"Source hoster should have no unreachable hosters, found: {:?}",
result.unreachable_hosters
);
assert!(
result.is_healthy(),
"Single source hoster topology should be healthy, got {} issues",
result.issue_count
);
} else {
tracing::warn!(
"Gateway is NOT source (distance={:.4} >= threshold={:.4}) - orphan/disconnected issues are expected",
gateway_distance,
SOURCE_THRESHOLD
);
if !result.orphan_hosters.is_empty() {
tracing::info!(
"Expected: Gateway flagged as orphan (not a source, no upstream): {:?}",
result.orphan_hosters
);
}
if !result.disconnected_upstream.is_empty() {
tracing::info!(
"Expected: Gateway flagged as disconnected upstream (not a source, has downstream): {:?}",
result.disconnected_upstream
);
}
}
tracing::info!(
"Single hoster topology test passed - gateway_is_source={}, cycles=0, other_issues={}",
gateway_is_source,
result.issue_count
);
}
#[test_log::test]
fn test_concurrent_updates_convergence() {
TestConfig::medium("concurrent-updates-convergence", 0xC0C0_BEEF_1234)
.with_gateways(2) .with_nodes(6) .with_max_contracts(3) .with_iterations(80) .with_duration(Duration::from_secs(90))
.with_sleep(Duration::from_secs(2)) .require_convergence() .run()
.assert_ok()
.verify_operation_coverage()
.check_convergence()
.verify_state_report();
}
#[test_log::test]
fn test_full_state_send_no_incorrect_caching() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x2763_CAFE_0001;
const NETWORK_NAME: &str = "full-state-cache-test";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 3, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x63);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let initial_state = SimOperation::create_test_state(1);
let update_state_2 = SimOperation::create_test_state(2);
let update_state_3 = SimOperation::create_test_state(3);
let update_state_4 = SimOperation::create_test_state(4);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 3),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: update_state_2,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Update {
key: contract_key,
data: update_state_3,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Update {
key: contract_key,
data: update_state_4,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120), Duration::from_secs(60), );
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete successfully: {:?}",
result.turmoil_result.err()
);
let convergence =
rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"Convergence check: {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
for diverged in &convergence.diverged {
tracing::error!(
"DIVERGED: {} - {} unique states across {} peers",
diverged.contract_key,
diverged.unique_state_count(),
diverged.peer_states.len()
);
for (peer, hash) in &diverged.peer_states {
tracing::error!(" peer {}: {}", peer, hash);
}
}
assert!(
convergence.is_converged(),
"PR #2763 REGRESSION: State divergence detected! \
This indicates incorrect summary caching after full state sends. \
{} contracts converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
let resync_count = GlobalTestMetrics::resync_requests();
let delta_sends = GlobalTestMetrics::delta_sends();
let full_state_sends = GlobalTestMetrics::full_state_sends();
tracing::info!(
"Broadcast stats - delta_sends: {}, full_state_sends: {}, resync_requests: {}",
delta_sends,
full_state_sends,
resync_count
);
assert_eq!(
resync_count, 0,
"PR #2763 REGRESSION: {} ResyncRequests detected! \
This indicates deltas are failing due to incorrect summary caching. \
With the fix, no resyncs should be needed in this scenario.",
resync_count
);
let total_broadcasts = delta_sends + full_state_sends;
assert!(
total_broadcasts > 0,
"Expected at least one broadcast to occur during the simulation"
);
tracing::info!(
"test_full_state_send_no_incorrect_caching PASSED: \
{} peers converged, {} broadcasts ({} delta, {} full state), {} resyncs",
convergence
.converged
.iter()
.map(|c| c.replica_count)
.sum::<usize>(),
total_broadcasts,
delta_sends,
full_state_sends,
resync_count
);
}
#[test_log::test]
fn test_subscribe_forwarding_ack_relay() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x5CB6_F37C_0001;
const NETWORK_NAME: &str = "subscribe-fwd-ack";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 4, 7, 3, 5, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xAC);
let contract_id = *contract.key().id();
let initial_state = SimOperation::create_test_state(1);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 3),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 4),
SimOperation::Subscribe { contract_id },
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed (ForwardingAck regression?): {:?}",
result.turmoil_result.err()
);
let (success_count, not_found_count) = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut successes = 0usize;
let mut not_found = 0usize;
for log in logs.iter() {
match log.kind.subscribe_outcome() {
Some(true) => successes += 1,
Some(false) => not_found += 1,
None => {}
}
}
(successes, not_found)
});
tracing::info!(
success_count,
not_found_count,
"Subscribe telemetry summary"
);
assert!(
success_count >= 4,
"Expected at least 4 successful subscribes, got {} (not_found={})",
success_count,
not_found_count,
);
}
#[test_log::test]
fn test_client_disconnect_emits_upstream_unsubscribe() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x5CB6_F37C_0003;
const NETWORK_NAME: &str = "client-disconnect-unsub";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 4, 7, 3, 5, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xDC);
let contract_id = *contract.key().id();
let initial_state = SimOperation::create_test_state(1);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: false,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(NodeLabel::node(NETWORK_NAME, 1), SimOperation::Disconnect),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let (sent_count, received_count) = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut sent = 0usize;
let mut received = 0usize;
for log in logs.iter() {
if log
.kind
.unsubscribe_sent_instance_id()
.is_some_and(|id| *id == contract_id)
{
sent += 1;
}
if log
.kind
.unsubscribe_received_instance_id()
.is_some_and(|id| *id == contract_id)
{
received += 1;
}
}
(sent, received)
});
tracing::info!(
sent_count,
received_count,
"Unsubscribe telemetry after client disconnect"
);
assert!(
sent_count > 0,
"Disconnecting node MUST emit at least one UnsubscribeSent for the \
contract it was subscribed to (sent={sent_count}, received={received_count})"
);
assert!(
received_count > 0,
"Upstream node MUST log UnsubscribeReceived after client disconnect \
(sent={sent_count}, received={received_count})"
);
}
#[test_log::test]
fn test_client_disconnect_without_subscriptions_is_noop() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x5CB6_F37C_0004;
const NETWORK_NAME: &str = "disconnect-no-subs";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, 4, 7, 3, 5, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let operations = vec![ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Disconnect,
)];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(60),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let sent_count = rt.block_on(async {
let logs = logs_handle.lock().await;
logs.iter()
.filter(|log| log.kind.unsubscribe_sent_instance_id().is_some())
.count()
});
assert_eq!(
sent_count, 0,
"Disconnecting a client with no subscriptions must not emit any \
UnsubscribeSent events (got {sent_count})"
);
}
#[test_log::test]
fn test_repeat_subscribe_then_disconnect_emits_single_unsubscribe() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x5CB6_F37C_0005;
const NETWORK_NAME: &str = "repeat-subscribe-disconnect";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, 4, 7, 3, 5, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xDD);
let contract_id = *contract.key().id();
let initial_state = SimOperation::create_test_state(1);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: false,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(NodeLabel::node(NETWORK_NAME, 1), SimOperation::Disconnect),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let sent_count = rt.block_on(async {
let logs = logs_handle.lock().await;
logs.iter()
.filter(|log| {
log.kind
.unsubscribe_sent_instance_id()
.is_some_and(|id| *id == contract_id)
})
.count()
});
assert_eq!(
sent_count, 1,
"Repeat-subscribe + disconnect must emit exactly one UnsubscribeSent \
(got {sent_count})"
);
}
#[test_log::test]
fn test_relay_does_not_pollute_active_subscriptions() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x5CB6_F37C_0002;
const NETWORK_NAME: &str = "subscribe-relay-pollution";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new(
NETWORK_NAME,
2, 18, 10, 3, 3, 2, SEED,
)
.await
});
let contract = SimOperation::create_test_contract(0xAD);
let contract_id = *contract.key().id();
let initial_state = SimOperation::create_test_state(1);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 15),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 16),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 17),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 18),
SimOperation::Subscribe { contract_id },
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let snapshots = &result.topology_snapshots;
assert!(
!snapshots.is_empty(),
"No topology snapshots captured — background snapshot task didn't run"
);
let mut subscribed_peers: Vec<SocketAddr> = snapshots
.iter()
.filter(|snap| snap.active_subscription_keys.contains(&contract_id))
.map(|snap| snap.peer_addr)
.collect();
subscribed_peers.sort();
tracing::info!(
subscribed_peer_count = subscribed_peers.len(),
?subscribed_peers,
total_snapshots = snapshots.len(),
"Peers with contract in active_subscriptions"
);
assert!(
subscribed_peers.len() <= 5,
"Relay pollution regression: {} peer(s) have the contract in \
`active_subscriptions`, expected at most 5 (gateway + 4 subscribing \
nodes). Polluted peers: {:?}. \
See subscribe.rs::SubscribeMsgResult::Subscribed — `ring.subscribe()` \
must only run on the originator branch, not on relays.",
subscribed_peers.len(),
subscribed_peers,
);
assert!(
!subscribed_peers.is_empty(),
"No peers have the contract in active_subscriptions — \
subscribes didn't complete, test is degenerate"
);
}
#[test_log::test]
fn test_crdt_mode_version_tracking() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x0276_3CD0_0001;
const NETWORK_NAME: &str = "crdt-version-test";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 2, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xCD);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let state_v1 = SimOperation::create_crdt_state(1, 0x11);
let state_v2 = SimOperation::create_crdt_state(2, 0x22);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: state_v1,
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: state_v2,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(60),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence =
rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
let delta_sends = GlobalTestMetrics::delta_sends();
let full_state_sends = GlobalTestMetrics::full_state_sends();
tracing::info!(
"CRDT mode test - convergence: {}/{}, delta_sends: {}, full_state_sends: {}, resyncs: {}",
convergence.converged.len(),
convergence.total_contracts(),
delta_sends,
full_state_sends,
resync_count
);
assert!(
convergence.is_converged(),
"CRDT mode: all peers should converge. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
tracing::info!(
"test_crdt_mode_version_tracking PASSED: converged={}, resyncs={}",
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_pr2763_crdt_convergence_with_resync() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x0276_3B06_0001;
const NETWORK_NAME: &str = "pr2763-crdt-resync";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 2, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xBB);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let state_v1_gw = SimOperation::create_crdt_state(1, 0xAA); let state_v5_node = SimOperation::create_crdt_state(5, 0xBB); let state_v10_gw = SimOperation::create_crdt_state(10, 0xCC);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: state_v1_gw,
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Update {
key: contract_key,
data: state_v5_node,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: state_v10_gw,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(90),
Duration::from_secs(45),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence =
rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
let delta_sends = GlobalTestMetrics::delta_sends();
let full_state_sends = GlobalTestMetrics::full_state_sends();
tracing::info!(
"CRDT resync test - convergence: {}/{}, delta_sends: {}, full_state_sends: {}, resyncs: {}",
convergence.converged.len(),
convergence.total_contracts(),
delta_sends,
full_state_sends,
resync_count
);
for diverged in &convergence.diverged {
tracing::warn!(
"DIVERGED: {} - {} unique states",
diverged.contract_key,
diverged.unique_state_count()
);
for (peer, hash) in &diverged.peer_states {
tracing::warn!(" peer {}: {}", peer, hash);
}
}
assert!(
convergence.is_converged(),
"CRDT resync test: peers MUST converge via ResyncRequest recovery. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
if resync_count > 0 {
tracing::info!(
"CRDT resync test: {} ResyncRequests occurred (expected for version mismatch recovery)",
resync_count
);
}
tracing::info!(
"test_pr2763_crdt_convergence_with_resync PASSED: converged={}, resyncs={}",
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_sustained_update_fanout_no_full_state_storm() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x4233_5701_0001;
const NETWORK_NAME: &str = "i4233-fanout-storm";
const SUBSCRIBERS: usize = 2;
const SUSTAINED_UPDATES: usize = 20;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, SUBSCRIBERS, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x42);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let initial_state = SimOperation::create_crdt_state(1, 0x10);
let mut operations = Vec::new();
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: true,
},
));
for node_idx in 1..=SUBSCRIBERS {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, node_idx),
SimOperation::Subscribe { contract_id },
));
}
for v in 0..SUSTAINED_UPDATES {
let version = (v as u64) + 2; operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(version, 0x20 + v as u8),
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(240), Duration::from_secs(90), );
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence =
rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });
let delta_sends = GlobalTestMetrics::delta_sends();
let full_state_sends = GlobalTestMetrics::full_state_sends();
let resync_count = GlobalTestMetrics::resync_requests();
let pending_op_hwm = GlobalTestMetrics::pending_op_high_water_mark();
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
let stale = report.stale_peers();
tracing::info!(
"i4233 fanout: delta_sends={}, full_state_sends={}, resyncs={}, pending_op_hwm={}, converged={}/{}, stale_peers={}",
delta_sends,
full_state_sends,
resync_count,
pending_op_hwm,
convergence.converged.len(),
convergence.total_contracts(),
stale.len(),
);
assert!(
delta_sends + full_state_sends > 0,
"no broadcasts were recorded — the fan-out scenario did not run"
);
assert!(
stale.is_empty(),
"#4233 liveness: {} subscriber(s) went stale under sustained UPDATE \
fan-out (event loop starved by the broadcast storm): {:?}",
stale.len(),
stale,
);
const PENDING_OP_BACKLOG_BOUND: u64 = 200;
assert!(
pending_op_hwm < PENDING_OP_BACKLOG_BOUND,
"#4233 liveness: event-loop backlog high-water mark {pending_op_hwm} \
reached {PENDING_OP_BACKLOG_BOUND}+ in-flight op-results under sustained \
UPDATE fan-out — the event loop is falling behind (replies piling up), \
a sign the broadcast path is pinning the loop."
);
assert!(
delta_sends > full_state_sends,
"#4145 REGRESSION: deltas must dominate after bootstrap, but \
delta_sends={delta_sends} <= full_state_sends={full_state_sends}. \
A reverted summary-cache gate traps every subscriber on full state \
(the #4233 storm): every update re-sends full state to every subscriber \
instead of a delta."
);
assert!(
convergence.is_converged(),
"#4233: broadcast delivery degraded — peers failed to converge under \
sustained fan-out. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len(),
);
tracing::info!(
"test_sustained_update_fanout_no_full_state_storm PASSED: \
delta_sends={} > full_state_sends={}, converged, no stale peers",
delta_sends,
full_state_sends,
);
}
#[cfg(feature = "simulation_tests")]
fn run_fanout_liveness_scenario(network_name: &str, seed: u64, subscribers: usize) {
run_fanout_liveness_scenario_inner(network_name, seed, subscribers, false, false);
}
#[cfg(feature = "simulation_tests")]
fn run_fanout_liveness_scenario_inner(
network_name: &str,
seed: u64,
subscribers: usize,
preseed: bool,
enable_migration: bool,
) {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SUSTAINED_UPDATES: usize = 40;
GlobalTestMetrics::reset();
setup_deterministic_state(seed);
let rt = create_runtime();
let max_connections = subscribers + 16;
let (sim, logs_handle) = rt.block_on(async {
let mut sim = SimNetwork::new(
network_name,
1, subscribers, 7, 3, max_connections,
2, seed,
)
.await;
if enable_migration {
sim.enable_placement_migration();
} else {
sim.disable_placement_migration();
}
if preseed {
let host = NodeLabel::gateway(network_name, 0);
let subs: Vec<NodeLabel> = (1..=subscribers)
.map(|idx| NodeLabel::node(network_name, idx))
.collect();
sim.preseed_direct_star(&host, &subs);
}
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x42);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let initial_state = SimOperation::create_crdt_state(1, 0x10);
let mut operations = Vec::new();
operations.push(ScheduledOperation::new(
NodeLabel::gateway(network_name, 0),
SimOperation::Put {
contract: contract.clone(),
state: initial_state,
subscribe: true,
},
));
for node_idx in 1..=subscribers {
operations.push(ScheduledOperation::new(
NodeLabel::node(network_name, node_idx),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: true,
},
));
}
for v in 0..SUSTAINED_UPDATES {
let version = (v as u64) + 2; operations.push(ScheduledOperation::new(
NodeLabel::gateway(network_name, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(version, 0x20 + v as u8),
},
));
}
const PER_OP_PACING_SECS: u64 = 3;
const POST_OP_WAIT_SECS: u64 = 120;
let op_count = (1 + subscribers + SUSTAINED_UPDATES) as u64;
let sim_duration_secs = op_count * PER_OP_PACING_SECS + POST_OP_WAIT_SECS + 90;
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(sim_duration_secs), Duration::from_secs(POST_OP_WAIT_SECS), );
assert!(
result.turmoil_result.is_ok(),
"#4233 fan-out (N={subscribers}): simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence =
rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });
let delta_sends = GlobalTestMetrics::delta_sends();
let full_state_sends = GlobalTestMetrics::full_state_sends();
let resync_count = GlobalTestMetrics::resync_requests();
let pending_op_hwm = GlobalTestMetrics::pending_op_high_water_mark();
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
let stale = report.stale_peers();
let missing = report.missing_broadcasts();
let unapplied = report.unapplied_broadcasts();
tracing::info!(
"#4233 fanout N={}: delta_sends={}, full_state_sends={}, resyncs={}, \
pending_op_hwm={}, converged={}/{}, stale_peers={}, missing_broadcasts={}, \
unapplied_broadcasts={}",
subscribers,
delta_sends,
full_state_sends,
resync_count,
pending_op_hwm,
convergence.converged.len(),
convergence.total_contracts(),
stale.len(),
missing.len(),
unapplied.len(),
);
assert!(
delta_sends + full_state_sends > 0,
"#4233 fan-out (N={subscribers}): no broadcasts were recorded — the \
fan-out scenario did not run"
);
assert!(
stale.is_empty(),
"#4233 liveness (N={subscribers}): {} subscriber(s) went stale under \
sustained UPDATE fan-out — the event loop wedged and subscribers went \
silent (the v0.2.73 / #4145 production symptom): {:?}",
stale.len(),
stale,
);
let pending_op_bound = (subscribers as u64) * 8 + 256;
assert!(
pending_op_hwm < pending_op_bound,
"#4233 liveness (N={subscribers}): event-loop backlog high-water mark \
{pending_op_hwm} reached the bound {pending_op_bound} of in-flight \
op-results under sustained UPDATE fan-out — the event loop is falling \
behind (replies piling up), a sign the broadcast path is pinning the \
loop (the #4145 wedge condition)."
);
if !missing.is_empty() || !unapplied.is_empty() {
tracing::info!(
"#4233 (N={}): broadcast-bookkeeping diagnostics (NOT a failure): \
missing_broadcasts={}, unapplied_broadcasts={} — these are \
per-(tx,target) verifier artifacts (summary-skip / relay-hop), see \
the rationale above; liveness is gated on stale_peers + convergence",
subscribers,
missing.len(),
unapplied.len(),
);
}
assert!(
convergence.is_converged(),
"#4233 (N={subscribers}): broadcast delivery degraded — peers failed to \
converge under sustained fan-out. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len(),
);
if preseed && !enable_migration {
let expected_replicas = subscribers + 1; assert!(
!convergence.converged.is_empty(),
"#4233 participation (N={subscribers}): no contract reached the >=2-replica \
convergence threshold — the fan-out star did not form"
);
for c in &convergence.converged {
assert_eq!(
c.replica_count, expected_replicas,
"#4233 participation (N={subscribers}): converged contract {} has {} \
replicas, expected {} (host + all subscribers). A subscriber never \
logged contract state, so it silently dropped out of the fan-out \
(preseed cap-rejection or a subscribe that never reached the host); \
the liveness gate would otherwise pass vacuously.",
c.contract_key, c.replica_count, expected_replicas,
);
}
}
tracing::info!(
"#4233 fan-out liveness (N={}) PASSED: no stale peers, \
pending_op_hwm={} < {}, converged ({} delta / {} full sends)",
subscribers,
pending_op_hwm,
pending_op_bound,
delta_sends,
full_state_sends,
);
}
#[test_log::test]
#[cfg(feature = "simulation_tests")]
fn test_fanout_liveness_8_subscribers() {
run_fanout_liveness_scenario("i4233-fanout-8", 0x4233_0801_0001, 8);
}
#[test_log::test]
#[cfg(feature = "simulation_tests")]
fn test_fanout_liveness_16_subscribers() {
run_fanout_liveness_scenario("i4233-fanout-16", 0x4233_1601_0001, 16);
}
#[test_log::test]
#[cfg(feature = "simulation_tests")]
#[ignore = "#4233 incident-scale diagnostic (preseeded N=40/60/80, 40 UPDATEs each); too heavy for default CI. Run with --ignored. Always-on gate is test_fanout_liveness_8/16_subscribers."]
fn test_fanout_liveness_incident_scale_migration_off() {
for (n, seed) in [
(40usize, 0x4233_4001_0001u64),
(60, 0x4233_6001_0001),
(80, 0x4233_8001_0001),
] {
let name = format!("i4233-fanout-preseed-off-{n}");
run_fanout_liveness_scenario_inner(
&name, seed, n, true, false,
);
}
}
#[test_log::test]
#[cfg(feature = "simulation_tests")]
#[ignore = "#4233 incident-scale diagnostic (preseeded N=40/60/80, 40 UPDATEs each, migration ON); too heavy for default CI. Run with --ignored. Always-on gate is test_fanout_liveness_8/16_subscribers."]
fn test_fanout_liveness_incident_scale_migration_on() {
for (n, seed) in [
(40usize, 0x4233_4001_0002u64),
(60, 0x4233_6001_0002),
(80, 0x4233_8001_0002),
] {
let name = format!("i4233-fanout-preseed-on-{n}");
run_fanout_liveness_scenario_inner(
&name, seed, n, true, true,
);
}
}
#[test_log::test]
#[cfg(feature = "simulation_tests")]
fn test_fanout_liveness_preseed_24_subscribers() {
run_fanout_liveness_scenario_inner(
"i4233-fanout-preseed-24",
0x4233_2401_0001,
24,
true,
false,
);
}
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
#[rstest::rstest]
#[case::n3_g1_s1("crdt-3n-1gw-s1", 0x2773_0003_0001, 1, 3)]
#[case::n3_g1_s2("crdt-3n-1gw-s2", 0x2773_0003_0002, 1, 3)]
#[case::n3_g1_s3("crdt-3n-1gw-s3", 0x2773_0003_0003, 1, 3)]
#[case::n3_g1_s4("crdt-3n-1gw-s4", 0x2773_0003_0007, 1, 3)]
#[case::n3_g1_s5("crdt-3n-1gw-s5", 0x2773_0003_0006, 1, 3)]
#[case::n5_g2_s1("crdt-5n-2gw-s1", 0x2773_0005_1021, 2, 5)]
#[case::n5_g2_s2("crdt-5n-2gw-s2", 0x2773_0005_1012, 2, 5)]
#[case::n5_g2_s3("crdt-5n-2gw-s3", 0x2773_0005_1003, 2, 5)]
#[case::n5_g2_s4("crdt-5n-2gw-s4", 0x2773_0005_2001, 2, 5)]
#[case::n5_g2_s5("crdt-5n-2gw-s5", 0x2773_0005_1005, 2, 5)]
#[case::n6_g2_s1("crdt-6n-2gw-s1", 0x2773_0006_1001, 2, 6)]
#[case::n6_g2_s2("crdt-6n-2gw-s2", 0x2773_0006_1002, 2, 6)]
#[case::n6_g2_s3("crdt-6n-2gw-s3", 0x2773_0006_1003, 2, 6)]
#[case::n6_g2_s4("crdt-6n-2gw-s4", 0x2773_0006_1004, 2, 6)]
#[case::n6_g2_s5("crdt-6n-2gw-s5", 0x2773_0006_1005, 2, 6)]
#[case::n4_g1_s1("crdt-4n-1gw-s1", 0x2773_0004_0001, 1, 4)]
#[case::n4_g1_s2("crdt-4n-1gw-s2", 0x2773_0004_0002, 1, 4)]
#[case::n4_g1_s3("crdt-4n-1gw-s3", 0x2773_0004_0003, 1, 4)]
#[case::n4_g1_s4("crdt-4n-1gw-s4", 0x2773_0004_0004, 1, 4)]
#[case::n4_g1_s5("crdt-4n-1gw-s5", 0x2773_0004_0005, 1, 4)]
#[case::n5_g1_s1("crdt-5n-1gw-s1", 0x2773_0005_0008, 1, 5)]
#[case::n5_g1_s2("crdt-5n-1gw-s2", 0x2773_0005_0012, 1, 5)]
#[case::n5_g1_s3("crdt-5n-1gw-s3", 0x2773_0005_0003, 1, 5)]
#[case::n5_g1_s4("crdt-5n-1gw-s4", 0x2773_0005_0004, 1, 5)]
#[case::n5_g1_s5("crdt-5n-1gw-s5", 0x2773_0005_0005, 1, 5)]
#[case::n6_g1_s1("crdt-6n-1gw-s1", 0x2773_0006_0001, 1, 6)]
#[case::n6_g1_s2("crdt-6n-1gw-s2", 0x2773_0006_0002, 1, 6)]
#[case::n6_g1_s3("crdt-6n-1gw-s3", 0x2773_0006_0003, 1, 6)]
#[case::n6_g1_s4("crdt-6n-1gw-s4", 0x2773_0006_0004, 1, 6)]
#[case::n6_g1_s5("crdt-6n-1gw-s5", 0x2773_0006_0005, 1, 6)]
#[case::n7_g1_s1("crdt-7n-1gw-s1", 0x2773_0007_0001, 1, 7)]
#[case::n7_g1_s2("crdt-7n-1gw-s2", 0x2773_0007_0002, 1, 7)]
#[case::n7_g1_s3("crdt-7n-1gw-s3", 0x2773_0007_0023, 1, 7)]
#[case::n7_g1_s4("crdt-7n-1gw-s4", 0x2773_0007_0010, 1, 7)]
#[case::n7_g1_s5("crdt-7n-1gw-s5", 0x2773_0007_0005, 1, 7)]
#[case::n8_g1_s1("crdt-8n-1gw-s1", 0x2773_0008_0001, 1, 8)]
#[case::n8_g1_s2("crdt-8n-1gw-s2", 0x2773_0008_0002, 1, 8)]
#[case::n8_g1_s3("crdt-8n-1gw-s3", 0x2773_0008_0003, 1, 8)]
#[case::n8_g1_s4("crdt-8n-1gw-s4", 0x2773_0008_0004, 1, 8)]
#[case::n8_g1_s5("crdt-8n-1gw-s5", 0x2773_0008_0005, 1, 8)]
fn test_crdt_convergence(
#[case] name: &'static str,
#[case] seed: u64,
#[case] gateways: usize,
#[case] nodes: usize,
) {
GlobalTestMetrics::reset();
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(name, gateways, nodes, 7, 3, 10, 4, seed).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x5F);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(name, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x00),
subscribe: true,
},
)];
for i in 1..=nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(name, i),
SimOperation::Subscribe { contract_id },
));
}
for i in 1..=nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(name, i),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
},
));
}
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(180),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"[{}] Simulation should complete: {:?}",
name,
result.turmoil_result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
tracing::info!(
"[{}] nodes={}, gateways={}, converged={}/{}, resyncs={}",
name,
nodes,
gateways,
convergence.converged.len(),
convergence.total_contracts(),
resync_count
);
for diverged in &convergence.diverged {
tracing::warn!(
"[{}] DIVERGED: {} - {} unique states across {} peers",
name,
diverged.contract_key,
diverged.unique_state_count(),
diverged.peer_states.len()
);
}
assert!(
convergence.is_converged(),
"[{}] {} nodes must converge. {} converged, {} diverged",
name,
nodes,
convergence.converged.len(),
convergence.diverged.len()
);
tracing::info!(
"[{}] PASSED: converged={}, resyncs={}",
name,
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_concurrent_updates_from_n_sources() {
const SEED: u64 = 0xC0C0_BEEF_0002;
const NETWORK_NAME: &str = "concurrent-updates-n";
const NODE_COUNT: usize = 6;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 2, NODE_COUNT, 10, 5, 15, 4, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xC0);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x00),
subscribe: true,
},
)];
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
tracing::info!(
"Concurrent N-source updates: converged={}/{}, resyncs={}",
convergence.converged.len(),
convergence.total_contracts(),
resync_count
);
for diverged in &convergence.diverged {
tracing::warn!(
"DIVERGED: {} - {} unique states across {} peers",
diverged.contract_key,
diverged.unique_state_count(),
diverged.peer_states.len()
);
}
assert!(
convergence.is_converged(),
"Concurrent N-way updates MUST converge. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
tracing::info!(
"test_concurrent_updates_from_n_sources PASSED: converged={}, resyncs={}",
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_stale_summary_cache_multiple_branches() {
const SEED: u64 = 0x57A1_E001_0001;
const NETWORK_NAME: &str = "stale-summaries";
const NODE_COUNT: usize = 4;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, NODE_COUNT, 7, 3, 10, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x57);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x01),
subscribe: true,
},
)];
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
tracing::info!(
"Stale summary test: converged={}/{}, resyncs={}",
convergence.converged.len(),
convergence.total_contracts(),
resync_count
);
for diverged in &convergence.diverged {
tracing::warn!(
"DIVERGED: {} - {} unique states across {} peers",
diverged.contract_key,
diverged.unique_state_count(),
diverged.peer_states.len()
);
}
assert!(
convergence.is_converged(),
"Divergent state must converge via CRDT merge + ResyncRequest. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
tracing::info!(
"test_stale_summary_cache_multiple_branches PASSED: converged={}, resyncs={}",
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_max_downstream_limit_reached() {
const SEED: u64 = 0x0A0D_0001_0001;
const NETWORK_NAME: &str = "large-network";
const NODE_COUNT: usize = 12;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 2, NODE_COUNT, 10, 5, 15, 3, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x0A);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x00),
subscribe: true,
},
)];
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(300),
Duration::from_secs(150),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
tracing::info!(
"Large network test: converged={}/{}, resyncs={}",
convergence.converged.len(),
convergence.total_contracts(),
resync_count
);
for diverged in &convergence.diverged {
tracing::warn!(
"DIVERGED: {} - {} unique states across {} peers",
diverged.contract_key,
diverged.unique_state_count(),
diverged.peer_states.len()
);
}
assert!(
convergence.is_converged(),
"Large network must converge. {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
tracing::info!(
"test_max_downstream_limit_reached PASSED: converged={}, resyncs={}",
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_chain_topology_formation() {
const SEED: u64 = 0xC4A1_0001_0001;
const NETWORK_NAME: &str = "chain-topology";
const NODE_COUNT: usize = 4;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, NODE_COUNT, 10, 5, 10, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xC4);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x00),
subscribe: true,
},
)];
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for i in 1..=NODE_COUNT {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
let resync_count = GlobalTestMetrics::resync_requests();
tracing::info!(
"Chain/sequential test: converged={}/{}, resyncs={}",
convergence.converged.len(),
convergence.total_contracts(),
resync_count
);
assert!(
convergence.is_converged(),
"All {} contracts should converge. Converged: {}, Diverged: {:?}",
convergence.total_contracts(),
convergence.converged.len(),
convergence.diverged
);
tracing::info!(
"test_chain_topology_formation PASSED: converged={}, resyncs={}",
convergence.converged.len(),
resync_count
);
}
#[test_log::test]
fn test_subscription_broadcast_propagation() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0xBCAD_C057_0001; const NETWORK_NAME: &str = "broadcast-test";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 2, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xBC);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x01),
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(100, 0xFF), },
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120), Duration::from_secs(60), );
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"Broadcast propagation test: converged={}, diverged={}",
convergence.converged.len(),
convergence.diverged.len()
);
let contract_key_str = format!("{:?}", contract_key);
let peer_states: std::collections::BTreeMap<std::net::SocketAddr, String> =
rt.block_on(async {
let logs = logs_handle.lock().await;
let mut states = std::collections::BTreeMap::new();
for log in logs.iter() {
if let Some(key) = log.kind.contract_key() {
if format!("{:?}", key) == contract_key_str {
if let Some(hash) = log.kind.stored_state_hash() {
states.insert(log.peer_id.socket_addr(), hash.to_string());
}
}
}
}
states
});
tracing::info!(
"Contract {} has state on {} peers: {:?}",
contract_key_str,
peer_states.len(),
peer_states.keys().collect::<Vec<_>>()
);
assert!(
peer_states.len() >= 2,
"BUG: Update broadcast failed! Only {} peer(s) have state for contract {}. \
Expected at least 2 (gateway + subscriber). \
This indicates subscribers aren't receiving broadcasts (proximity_sources=0). \
Peers with state: {:?}",
peer_states.len(),
contract_key_str,
peer_states.keys().collect::<Vec<_>>()
);
let unique_states: std::collections::HashSet<&String> = peer_states.values().collect();
assert!(
unique_states.len() == 1,
"BUG: State divergence! {} unique states across {} peers for contract {}. \
States: {:?}",
unique_states.len(),
peer_states.len(),
contract_key_str,
peer_states
);
let broadcast_emitted_count: usize = rt.block_on(async {
let logs = logs_handle.lock().await;
logs.iter()
.filter(|log| log.kind.is_update_broadcast_emitted())
.count()
});
assert!(
broadcast_emitted_count > 0,
"Expected at least one UpdateEvent::BroadcastEmitted telemetry event, got 0. \
This means the emission wiring from #3622 is broken."
);
tracing::info!(
"test_subscription_broadcast_propagation PASSED: {} peers converged to same state, \
{} BroadcastEmitted events",
peer_states.len(),
broadcast_emitted_count
);
}
#[test_log::test]
fn test_subscription_relay_propagation() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0xBEAD_FEED_3390;
const NETWORK_NAME: &str = "relay-sub-test";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 3, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x39);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let operations = vec![
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0x01),
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(100, 0xFE),
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(60),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation should complete: {:?}",
result.turmoil_result.err()
);
let contract_key_str = format!("{:?}", contract_key);
let peer_states: std::collections::BTreeMap<std::net::SocketAddr, String> =
rt.block_on(async {
let logs = logs_handle.lock().await;
let mut states = std::collections::BTreeMap::new();
for log in logs.iter() {
if let Some(key) = log.kind.contract_key() {
if format!("{:?}", key) == contract_key_str {
if let Some(hash) = log.kind.stored_state_hash() {
states.insert(log.peer_id.socket_addr(), hash.to_string());
}
}
}
}
states
});
tracing::info!(
"Relay subscription test: contract {} has state on {} peers: {:?}",
contract_key_str,
peer_states.len(),
peer_states.keys().collect::<Vec<_>>()
);
assert!(
peer_states.len() >= 3,
"BUG (#3390): Relay subscription failed! Only {} peer(s) have state for contract {}. \
Expected at least 3 (hoster + gateway relay + subscriber). \
The relay node (gateway) likely didn't register the subscriber as downstream. \
Peers with state: {:?}",
peer_states.len(),
contract_key_str,
peer_states.keys().collect::<Vec<_>>()
);
let unique_states: std::collections::HashSet<&String> = peer_states.values().collect();
assert!(
unique_states.len() == 1,
"State divergence in relay test! {} unique states across {} peers: {:?}",
unique_states.len(),
peer_states.len(),
peer_states
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"Anomaly report: {} anomalies across {} contracts",
report.anomalies.len(),
report.contracts_analyzed
);
tracing::info!(
"test_subscription_relay_propagation PASSED: {} peers converged via relay",
peer_states.len()
);
}
#[test_log::test]
#[cfg(feature = "nightly_tests")]
fn test_long_running_deterministic() {
const SEED: u64 = 0x1A00_2C00_72AC;
tracing::info!("=== Starting Long-Running Deterministic Simulation ===");
tracing::info!("Seed: 0x{:X}", SEED);
tracing::info!("Virtual time target: 3600 seconds (1 hour)");
let start_time = std::time::Instant::now();
TestConfig::long_running("long-running", SEED)
.run_direct()
.assert_ok()
.verify_operation_coverage()
.check_convergence();
let wall_clock = start_time.elapsed();
tracing::info!("=== Long-Running Simulation Complete ===");
tracing::info!("Wall clock time: {:.1} seconds", wall_clock.as_secs_f64());
tracing::info!(
"Time acceleration: {:.1}x",
3600.0 / wall_clock.as_secs_f64()
);
tracing::info!("test_long_running_deterministic PASSED");
}
#[test_log::test]
fn test_partition_heal_convergence() {
use freenet::dev_tool::NodeLabel;
use freenet::simulation::Partition;
const SEED: u64 = 0xDA27_0EA1_0001;
const NETWORK_NAME: &str = "partition-heal";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle, node_addrs) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 5, 7,
3,
10,
2,
SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
(sim, logs_handle, node_addrs)
});
let addrs: Vec<std::net::SocketAddr> = node_addrs.values().copied().collect();
let mid = addrs.len() / 2;
let side_a: std::collections::HashSet<_> = addrs[..mid].iter().copied().collect();
let side_b: std::collections::HashSet<_> = addrs[mid..].iter().copied().collect();
let side_a_len = side_a.len();
let side_b_len = side_b.len();
let network_name = NETWORK_NAME.to_string();
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
5, 100, Duration::from_secs(120), Duration::from_millis(500), move || async move {
tracing::info!(
"Injecting partition: side_a={} nodes, side_b={} nodes",
side_a_len,
side_b_len,
);
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
let partition = Partition::new(side_a, side_b).permanent(0);
state.config.add_partition(partition);
}
tokio::time::sleep(Duration::from_secs(15)).await;
tracing::info!("Partition active for 15s, now healing");
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
state.config.partitions.clear();
}
tokio::time::sleep(Duration::from_secs(15)).await;
tracing::info!("Post-heal convergence period complete");
Ok(())
},
);
assert!(
result.is_ok(),
"Partition-heal simulation failed: {:?}",
result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"Partition-heal: {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"=== PARTITION-HEAL ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
report.total_events,
report.state_events,
report.contracts_analyzed,
report.anomalies.len()
);
let divergences = report.divergences();
let missing = report.missing_broadcasts();
let partitions = report.suspected_partitions();
let stale = report.stale_peers();
let oscillations = report.state_oscillations();
tracing::warn!(
" divergences={}, missing_broadcasts={}, partitions={}, stale={}, oscillations={}",
divergences.len(),
missing.len(),
partitions.len(),
stale.len(),
oscillations.len(),
);
for (i, anomaly) in report.anomalies.iter().enumerate() {
tracing::debug!(" anomaly[{}] = {:?}", i, anomaly);
}
}
#[test_log::test]
fn test_crash_recover_convergence() {
use freenet::dev_tool::NodeLabel;
const SEED: u64 = 0x2011_2E57_0002;
const NETWORK_NAME: &str = "crash-recover";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle, node_addrs) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, 5, 7, 3, 10, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
(sim, logs_handle, node_addrs)
});
let crash_addrs: Vec<std::net::SocketAddr> = node_addrs
.iter()
.filter(|(label, _)| label.is_node())
.take(2)
.map(|(_, addr)| *addr)
.collect();
assert_eq!(crash_addrs.len(), 2, "Need at least 2 non-gateway nodes");
let network_name = NETWORK_NAME.to_string();
let crash_addrs_clone = crash_addrs.clone();
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
3, 100, Duration::from_secs(120), Duration::from_millis(500), move || async move {
tracing::info!("Crashing 2 nodes: {:?}", crash_addrs_clone);
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
for addr in &crash_addrs_clone {
state.config.crash_node(*addr);
}
}
tokio::time::sleep(Duration::from_secs(10)).await;
tracing::info!("Degraded period over, recovering nodes");
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
for addr in &crash_addrs_clone {
state.config.recover_node(addr);
}
}
tokio::time::sleep(Duration::from_secs(15)).await;
tracing::info!("Post-recovery convergence period complete");
Ok(())
},
);
assert!(
result.is_ok(),
"Crash-recover simulation failed: {:?}",
result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"Crash-recover: {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"=== CRASH-RECOVER ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
report.total_events,
report.state_events,
report.contracts_analyzed,
report.anomalies.len()
);
let divergences = report.divergences();
let stale = report.stale_peers();
let zombies = report.zombie_transactions();
let oscillations = report.state_oscillations();
tracing::warn!(
" divergences={}, stale_peers={}, zombies={}, oscillations={}",
divergences.len(),
stale.len(),
zombies.len(),
oscillations.len(),
);
for (i, anomaly) in report.anomalies.iter().enumerate() {
tracing::debug!(" anomaly[{}] = {:?}", i, anomaly);
}
}
#[test_log::test]
fn test_multi_step_churn() {
use freenet::dev_tool::NodeLabel;
const SEED: u64 = 0xC402_0000_0002;
const NETWORK_NAME: &str = "multi-churn";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle, node_addrs) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, 4, 7, 3, 10, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
(sim, logs_handle, node_addrs)
});
let churn_addrs: Vec<std::net::SocketAddr> = node_addrs
.iter()
.filter(|(label, _)| label.is_node())
.map(|(_, addr)| *addr)
.collect();
let network_name = NETWORK_NAME.to_string();
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
3, 100, Duration::from_secs(150), Duration::from_millis(500), move || async move {
const CHURN_ROUNDS: usize = 3;
for round in 0..CHURN_ROUNDS {
let target = churn_addrs[round % churn_addrs.len()];
tracing::info!("Churn round {}: crashing {:?}", round, target);
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
state.config.crash_node(target);
}
tokio::time::sleep(Duration::from_secs(5)).await;
tracing::info!("Churn round {}: recovering {:?}", round, target);
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
state.config.recover_node(&target);
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
tokio::time::sleep(Duration::from_secs(10)).await;
tracing::info!("Multi-step churn: all {} rounds complete", CHURN_ROUNDS);
Ok(())
},
);
assert!(
result.is_ok(),
"Multi-step churn simulation failed: {:?}",
result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"Multi-step churn: {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"=== MULTI-STEP CHURN ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
report.total_events,
report.state_events,
report.contracts_analyzed,
report.anomalies.len()
);
let divergences = report.divergences();
let missing = report.missing_broadcasts();
let stale = report.stale_peers();
let zombies = report.zombie_transactions();
let oscillations = report.state_oscillations();
let cascades = report.delta_sync_cascades();
tracing::warn!(
" divergences={}, missing={}, stale={}, zombies={}, oscillations={}, cascades={}",
divergences.len(),
missing.len(),
stale.len(),
zombies.len(),
oscillations.len(),
cascades.len(),
);
for (i, anomaly) in report.anomalies.iter().enumerate() {
tracing::debug!(" anomaly[{}] = {:?}", i, anomaly);
}
}
#[test_log::test]
fn test_determinism_parallel_safe() {
const SEED: u64 = 0x0A2A_11E1_0001;
#[derive(Debug, PartialEq)]
struct Trace {
event_counts: HashMap<String, usize>,
event_sequence: Vec<String>,
total_events: usize,
}
fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, Trace) {
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(name, 1, 5, 7, 3, 10, 2, seed).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
seed,
3,
15,
Duration::from_secs(20),
Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
let trace = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut event_counts: HashMap<String, usize> = HashMap::new();
let mut event_sequence: Vec<String> = Vec::new();
for log in logs.iter() {
let kind_name = log.kind.variant_name().to_string();
*event_counts.entry(kind_name.clone()).or_insert(0) += 1;
event_sequence.push(kind_name);
}
Trace {
total_events: logs.len(),
event_counts,
event_sequence,
}
});
(result, trace)
}
let (result1, trace1) = run_and_trace("parallel-safe-run1", SEED);
let (result2, trace2) = run_and_trace("parallel-safe-run2", SEED);
let (result3, trace3) = run_and_trace("parallel-safe-run3", SEED);
assert_eq!(
result1.is_ok(),
result2.is_ok(),
"Simulation outcomes differ: run1={:?}, run2={:?}",
result1,
result2
);
assert_eq!(result2.is_ok(), result3.is_ok());
assert!(trace1.total_events > 0, "No events captured");
assert_eq!(
trace1.total_events, trace2.total_events,
"Total events differ: {} vs {}",
trace1.total_events, trace2.total_events
);
assert_eq!(trace2.total_events, trace3.total_events);
assert_eq!(trace1.event_counts, trace2.event_counts);
assert_eq!(trace2.event_counts, trace3.event_counts);
assert_eq!(trace1.event_sequence, trace2.event_sequence);
assert_eq!(trace2.event_sequence, trace3.event_sequence);
let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");
tracing::info!(
"PARALLEL-SAFE DETERMINISM PASSED: {} events, fingerprint={:#018x}",
trace1.total_events,
fp1.sequence_hash
);
}
#[test_log::test]
fn test_determinism_across_threads() {
const SEED: u64 = 0xCE05_7EAD_0001;
fn run_on_thread(name: &'static str, seed: u64) -> std::thread::JoinHandle<usize> {
std::thread::spawn(move || {
GlobalRng::set_seed(seed);
const BASE_EPOCH_MS: u64 = 1577836800000;
const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000;
GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + (seed % RANGE_MS));
GlobalTestMetrics::reset();
RequestId::reset_counter();
freenet::dev_tool::ClientId::reset_counter();
reset_event_id_counter();
reset_channel_id_counter();
StreamId::reset_counter();
reset_nonce_counter();
reset_global_node_index();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create runtime");
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(name, 1, 3, 7, 3, 10, 2, seed).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let _result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
seed,
3,
10,
Duration::from_secs(20),
Duration::from_millis(200),
|| async {
tokio::time::sleep(Duration::from_secs(1)).await;
Ok(())
},
);
rt.block_on(async { logs_handle.lock().await.len() })
})
}
let handle1 = run_on_thread("cross-thread-run1", SEED);
let handle2 = run_on_thread("cross-thread-run2", SEED);
let events1 = handle1.join().expect("Thread 1 panicked");
let events2 = handle2.join().expect("Thread 2 panicked");
assert!(events1 > 0, "Thread 1 should produce events, got 0");
assert!(events2 > 0, "Thread 2 should produce events, got 0");
tracing::info!(
"CROSS-THREAD ISOLATION PASSED: thread1={} events, thread2={} events",
events1,
events2
);
}
#[test_log::test]
fn test_direct_runner_determinism() {
const SEED: u64 = 0xD12E_C7DE_7000;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct EventKey {
tx: freenet::dev_tool::Transaction,
peer_addr: std::net::SocketAddr,
event_kind_name: String,
contract_key: Option<String>,
state_hash: Option<String>,
}
#[derive(Debug)]
struct SimulationTrace {
event_counts: HashMap<String, usize>,
event_sequence: Vec<String>,
event_keys: Vec<EventKey>,
total_events: usize,
}
fn run_and_trace(name: &str, seed: u64) -> SimulationTrace {
setup_deterministic_state(seed);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
name, 2, 4, 10, 3, 10, 3, seed,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
drop(rt);
sim.run_simulation_direct::<rand::rngs::SmallRng>(
seed,
10, 30, Duration::from_millis(200),
)
.expect("Direct simulation should succeed");
let rt = create_runtime();
rt.block_on(async {
let logs = logs_handle.lock().await;
let mut event_counts: HashMap<String, usize> = HashMap::new();
let mut event_sequence: Vec<String> = Vec::new();
let mut event_keys: Vec<EventKey> = logs
.iter()
.map(|log| {
let event_kind_name = log.kind.variant_name().to_string();
let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
let state_hash = log.kind.state_hash().map(String::from);
*event_counts.entry(event_kind_name.clone()).or_insert(0) += 1;
event_sequence.push(event_kind_name.clone());
EventKey {
tx: log.tx,
peer_addr: log.peer_id.socket_addr(),
event_kind_name,
contract_key,
state_hash,
}
})
.collect();
event_keys.sort();
SimulationTrace {
total_events: logs.len(),
event_counts,
event_sequence,
event_keys,
}
})
}
let trace1 = run_and_trace("direct-det-run1", SEED);
let trace2 = run_and_trace("direct-det-run2", SEED);
let trace3 = run_and_trace("direct-det-run3", SEED);
assert!(trace1.total_events > 0, "Run 1 should produce events");
assert!(trace2.total_events > 0, "Run 2 should produce events");
assert!(trace3.total_events > 0, "Run 3 should produce events");
assert_eq!(
trace1.total_events, trace2.total_events,
"DIRECT DETERMINISM FAILURE: Total event counts differ (run1 vs run2)!"
);
assert_eq!(
trace2.total_events, trace3.total_events,
"DIRECT DETERMINISM FAILURE: Total event counts differ (run2 vs run3)!"
);
assert_eq!(
trace1.event_counts, trace2.event_counts,
"DIRECT DETERMINISM FAILURE: Event type counts differ (run1 vs run2)!"
);
assert_eq!(
trace2.event_counts, trace3.event_counts,
"DIRECT DETERMINISM FAILURE: Event type counts differ (run2 vs run3)!"
);
for (i, ((e1, e2), e3)) in trace1
.event_sequence
.iter()
.zip(trace2.event_sequence.iter())
.zip(trace3.event_sequence.iter())
.enumerate()
{
assert_eq!(
e1, e2,
"Event sequence differs at index {} (run1 vs run2)!",
i
);
assert_eq!(
e2, e3,
"Event sequence differs at index {} (run2 vs run3)!",
i
);
}
assert_eq!(
trace1.event_keys, trace2.event_keys,
"DIRECT DETERMINISM FAILURE: Sorted event keys differ (run1 vs run2)!"
);
assert_eq!(
trace2.event_keys, trace3.event_keys,
"DIRECT DETERMINISM FAILURE: Sorted event keys differ (run2 vs run3)!"
);
let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");
tracing::info!(
"DIRECT RUNNER DETERMINISM PASSED: {} events, fingerprint={:#018x}",
trace1.total_events,
fp1.sequence_hash
);
}
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_suspend_resume_zombie_connections() {
const SEED: u64 = 0xDEAD_BEEF_3005;
const NETWORK_NAME: &str = "zombie-connections";
tracing::info!("=== Testing Zombie Connection Bug (PR #3005) ===");
let mut sim = SimNetwork::new(NETWORK_NAME, 1, 2, 7, 3, 10, 2, SEED).await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 5, 10)
.await;
tracing::info!("Phase 1: Network startup and stabilization");
tokio::time::sleep(Duration::from_secs(2)).await;
let initial_time = sim.virtual_time().now_nanos();
tracing::info!("Initial virtual time: {}ns", initial_time);
sim.check_connectivity(Duration::from_secs(10))
.await
.expect("Initial connectivity check should pass");
tracing::info!("✓ Network connectivity established");
let node_to_suspend = sim
.all_node_addresses()
.keys()
.find(|label| label.is_node())
.cloned()
.expect("Should have at least one node");
tracing::info!(?node_to_suspend, "Phase 2: Simulating suspend (crash node)");
let crashed = sim.crash_node(&node_to_suspend);
assert!(crashed, "Node should crash successfully");
assert!(sim.is_node_crashed(&node_to_suspend));
tracing::info!("✓ Node crashed (suspend simulated)");
tracing::info!("Phase 3: Simulating time passage during suspend");
let suspend_duration = Duration::from_secs(3600);
sim.virtual_time().advance(suspend_duration);
let time_after_suspend = sim.virtual_time().now_nanos();
tracing::info!(
"✓ Advanced virtual time by {} seconds (now: {}ns)",
suspend_duration.as_secs(),
time_after_suspend
);
tokio::time::sleep(Duration::from_millis(100)).await;
tracing::info!(
?node_to_suspend,
"Phase 4: Simulating resume (restart node)"
);
let restart_seed = SEED.wrapping_add(0x1000);
let handle = sim
.restart_node::<rand::rngs::SmallRng>(&node_to_suspend, restart_seed, 5, 5)
.await;
assert!(handle.is_some(), "Node should restart successfully");
assert!(!sim.is_node_crashed(&node_to_suspend));
tracing::info!("✓ Node restarted (resume simulated)");
tokio::time::sleep(Duration::from_secs(3)).await;
tracing::info!("Phase 5: Checking for zombie connections");
let connectivity_result = sim.check_connectivity(Duration::from_secs(15)).await;
match connectivity_result {
Ok(()) => {
tracing::info!("✓ Connectivity restored after restart");
tracing::info!(" FIX WORKING: No zombie connections blocking reconnection");
tracing::info!(" (or DropAllConnections was called on resume)");
}
Err(e) => {
tracing::error!("✗ Connectivity check failed after restart: {}", e);
tracing::error!("BUG DETECTED: Zombie connections blocking reconnection");
tracing::error!("");
tracing::error!("Root cause analysis:");
tracing::error!(" - Node crashed without proper cleanup");
tracing::error!(" - Old connection HashMap entries still exist");
tracing::error!(" - CONNECT messages sent through dead transport sockets");
tracing::error!(" - Gateway doesn't respond (socket handle invalid)");
tracing::error!(" - Bootstrap hangs waiting for response");
tracing::error!("");
tracing::error!("Expected fix: Call DropAllConnections in ops_after_resume()");
tracing::error!("See PR #3005 for details");
panic!("Zombie connection bug detected! Connectivity failed after node restart");
}
}
tracing::info!("=== Zombie Connection Test Complete ===");
}
#[test_log::test]
#[cfg(feature = "nightly_tests")]
fn test_subscription_renewal_at_scale() {
const SEED: u64 = 0x2995_CAFE_BABE;
tracing::info!("=== Subscription Renewal at Scale (250+ contracts) ===");
let start_time = std::time::Instant::now();
let config = TestConfig {
name: "subscription-renewal-scale",
seed: SEED,
gateways: 1,
nodes: 2,
ring_max_htl: 7,
rnd_if_htl_above: 3,
max_connections: 10,
min_connections: 2,
max_contracts: 250, iterations: 300, duration: Duration::from_secs(120), event_wait: Duration::from_millis(200),
sleep_after_events: Duration::from_secs(40), require_convergence: false,
latency_range: None,
message_loss_rate: 0.0,
use_mock_wasm: false,
};
let max_contracts = config.max_contracts;
tracing::info!(
"Testing with {} contracts, {} operations",
max_contracts,
config.iterations
);
let result = config.run();
let rt = create_runtime();
let logs = rt.block_on(async { result.logs_handle.lock().await.clone() });
let mut renewal_attempts = 0;
let mut channel_warnings = 0;
for log in &logs {
let msg = format!("{:?}", log);
if msg.contains("Subscription renewal: attempted") {
renewal_attempts += 1;
}
if msg.contains("Notification channel") && msg.contains("full") {
channel_warnings += 1;
tracing::error!("Channel saturation detected: {}", msg);
}
}
let wall_clock = start_time.elapsed();
tracing::info!(
"Completed in {:.1}s: {} renewal cycles, {} channel warnings",
wall_clock.as_secs_f64(),
renewal_attempts,
channel_warnings
);
result.assert_ok();
tracing::info!("Subscription renewal cycles observed: {}", renewal_attempts);
assert_eq!(
channel_warnings, 0,
"Channel saturation detected - regression in PR #2995/#3146 fix! \
The subscription renewal rate-limiting may not be working correctly."
);
tracing::info!(
"✓ No channel saturation with {} contracts (PR #2995/#3146 fix working)",
max_contracts
);
}
#[test_log::test]
fn test_router_accumulates_feedback_events() {
let result = TestConfig::small("router-feedback", 0x0FEE_DBAC_0001)
.with_nodes(4)
.with_max_contracts(5)
.with_iterations(50)
.with_duration(Duration::from_secs(60))
.with_sleep(Duration::from_secs(2))
.run()
.assert_ok();
let rt = create_runtime();
let (route_count, route_by_peer) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let route_count = logs
.iter()
.filter(|log| log.kind.variant_name() == "Route")
.count();
let mut by_peer: HashMap<String, usize> = HashMap::new();
for log in logs.iter().filter(|l| l.kind.variant_name() == "Route") {
*by_peer
.entry(format!("{}", log.peer_id.socket_addr()))
.or_default() += 1;
}
(route_count, by_peer)
});
tracing::info!("=== ROUTER FEEDBACK TEST ===");
tracing::info!(
"Total Route events (routing_finished calls): {}",
route_count
);
for (peer, count) in &route_by_peer {
tracing::info!(" peer {}: {} route events", peer, count);
}
assert!(
route_count > 0,
"No Route events in logs - routing_finished never called. \
The router is not receiving any feedback from completed operations."
);
tracing::info!(
"ROUTER FEEDBACK TEST PASSED: {} route events across {} peers",
route_count,
route_by_peer.len()
);
}
#[test_log::test]
fn test_router_accumulates_failure_events_with_message_loss() {
let result = TestConfig::small("router-faults", 0xFA17_0055_0001)
.with_nodes(4)
.with_max_contracts(5)
.with_iterations(60)
.with_duration(Duration::from_secs(60))
.with_sleep(Duration::from_secs(2))
.with_message_loss(0.15)
.run()
.assert_ok();
let rt = create_runtime();
let (route_count, timeout_count) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let route_count = logs
.iter()
.filter(|log| log.kind.variant_name() == "Route")
.count();
let timeout_count = logs
.iter()
.filter(|log| log.kind.variant_name() == "Timeout")
.count();
(route_count, timeout_count)
});
tracing::info!("=== ROUTER FAULT INJECTION TEST ===");
tracing::info!("Route events (routing_finished): {}", route_count);
tracing::info!("Timeout events: {}", timeout_count);
assert!(
route_count > 0,
"No Route events with 15% message loss - router not receiving any feedback"
);
tracing::info!(
"ROUTER FAULT TEST PASSED: route_events={}, timeouts={}",
route_count,
timeout_count
);
}
#[test_log::test]
fn test_router_prediction_threshold_activation() {
let result = TestConfig::small("router-threshold", 0xACE5_0FD0_0001)
.with_nodes(2)
.with_max_contracts(6)
.with_iterations(1500)
.with_event_wait(Duration::from_millis(500))
.with_duration(Duration::from_secs(900))
.with_sleep(Duration::from_secs(2))
.run()
.assert_ok();
let rt = create_runtime();
let (route_count, route_by_peer) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let route_count = logs
.iter()
.filter(|log| log.kind.variant_name() == "Route")
.count();
let mut by_peer: HashMap<String, usize> = HashMap::new();
for log in logs.iter().filter(|l| l.kind.variant_name() == "Route") {
*by_peer
.entry(format!("{}", log.peer_id.socket_addr()))
.or_default() += 1;
}
(route_count, by_peer)
});
tracing::info!("=== ROUTER THRESHOLD ACTIVATION TEST ===");
tracing::info!("Total Route events: {}", route_count);
for (peer, count) in &route_by_peer {
tracing::info!(" peer {}: {} route events", peer, count);
}
let snapshots = result.router_snapshots();
tracing::info!("RouterSnapshot events captured: {}", snapshots.len());
for (i, (failure, success, active)) in snapshots.iter().enumerate() {
tracing::info!(
" snapshot[{}]: failure_events={}, success_events={}, prediction_active={}",
i,
failure,
success,
active
);
}
let max_per_peer = route_by_peer.values().max().copied().unwrap_or(0);
tracing::info!("Max route events on a single peer: {}", max_per_peer);
assert!(
route_count >= 50,
"Only {} total Route events from 300 iterations - \
not enough feedback flowing to the router",
route_count
);
if !snapshots.is_empty() {
let any_active = snapshots.iter().any(|(_, _, active)| *active);
let max_failure_events = snapshots
.iter()
.map(|(failure, _, _)| *failure)
.max()
.unwrap();
if any_active {
tracing::info!(
"ROUTER THRESHOLD TEST PASSED: prediction activated with {} failure_events (threshold=50)",
max_failure_events
);
} else {
tracing::info!(
"RouterSnapshot shows failure_events={} (threshold=50). \
Route events per peer: {:?}. \
Note: Route events may not all be reflected in failure_events \
if the snapshot was captured before all operations completed.",
max_failure_events,
route_by_peer
);
}
}
tracing::info!(
"ROUTER THRESHOLD TEST PASSED: {} total route events, max {} per peer (threshold=50)",
route_count,
max_per_peer
);
}
#[test]
#[cfg(feature = "simulation_tests")]
fn test_pending_op_results_bounded() {
let result = TestConfig::medium("pending-op-bounded", 0x3100_0001).run();
result.assert_ok().verify_state_report();
let inserts = freenet::config::GlobalTestMetrics::pending_op_inserts();
let removes = freenet::config::GlobalTestMetrics::pending_op_removes();
let hwm = freenet::config::GlobalTestMetrics::pending_op_high_water_mark();
tracing::info!(inserts, removes, hwm, "pending_op_results resource metrics");
if inserts == 0 {
tracing::info!(
"pending_op_results path not exercised (no production OpCtx caller yet — see #1454 phase 2b)"
);
}
assert!(
hwm <= 100,
"pending_op_results high-water mark ({hwm}) exceeded bound of 100 — \
regression of #3100 (unbounded HashMap growth)"
);
let leak = inserts.saturating_sub(removes);
assert_eq!(
leak, 0,
"pending_op_results leak at shutdown: {leak} entries \
(inserts={inserts}, removes={removes})"
);
}
#[test]
#[cfg(feature = "simulation_tests")]
fn test_neighbor_hosting_bounded() {
let result = TestConfig::medium("neighbor-hosting-bounded", 0x3100_0002).run();
result.assert_ok().verify_state_report();
let updates = freenet::config::GlobalTestMetrics::neighbor_hosting_updates();
tracing::info!(updates, "neighbor hosting resource metrics");
assert!(
updates > 0,
"Neighbor hosting should have been exercised (updates = 0)"
);
assert!(
updates <= 500,
"neighbor_hosting_updates ({updates}) exceeded bound of 500 — \
excessive cache churn for an 8-peer / 8-contract network"
);
}
#[test]
#[cfg(feature = "simulation_tests")]
fn test_anti_starvation_exercised() {
let result = TestConfig::medium("anti-starvation", 0x3094_0002)
.with_iterations(150)
.with_max_contracts(10)
.run();
let triggers = freenet::config::GlobalTestMetrics::anti_starvation_triggers();
tracing::info!(triggers, "anti-starvation trigger count");
result.assert_ok().verify_state_report().check_convergence();
}
fn verify_contract_propagation(
rt: &tokio::runtime::Runtime,
logs_handle: &Arc<Mutex<Vec<freenet::tracing::NetLogMessage>>>,
contract_key: freenet_stdlib::prelude::ContractKey,
min_peers: usize,
) -> BTreeMap<SocketAddr, String> {
let contract_key_str = format!("{:?}", contract_key);
let peer_states: BTreeMap<SocketAddr, String> = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut states = BTreeMap::new();
for log in logs.iter() {
if let Some(key) = log.kind.contract_key() {
if format!("{:?}", key) == contract_key_str {
if let Some(hash) = log.kind.stored_state_hash() {
states.insert(log.peer_id.socket_addr(), hash.to_string());
}
}
}
}
states
});
tracing::info!(
"Contract {} has state on {} peers (need {}): {:?}",
contract_key_str,
peer_states.len(),
min_peers,
peer_states.keys().collect::<Vec<_>>()
);
assert!(
peer_states.len() >= min_peers,
"Contract {} propagation failed: only {} peer(s) have state, expected at least {}. \
Peers with state: {:?}",
contract_key_str,
peer_states.len(),
min_peers,
peer_states.keys().collect::<Vec<_>>()
);
let unique_states: std::collections::HashSet<&String> = peer_states.values().collect();
assert!(
unique_states.len() == 1,
"Contract {} state divergence: {} unique states across {} peers. States: {:?}",
contract_key_str,
unique_states.len(),
peer_states.len(),
peer_states
);
peer_states
}
#[test_log::test]
fn test_six_peer_contract_lifecycle() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x3151_0001_0001;
const NETWORK_NAME: &str = "six-peer-lifecycle";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 2, 4, 10, 5, 15, 3, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract_a = SimOperation::create_test_contract(0xA1);
let contract_a_id = *contract_a.key().id();
let contract_a_key = contract_a.key();
register_crdt_contract(contract_a_id);
let contract_b = SimOperation::create_test_contract(0xB2);
let contract_b_id = *contract_b.key().id();
let contract_b_key = contract_b.key();
register_crdt_contract(contract_b_id);
let mut operations = Vec::new();
for (contract, contract_id, contract_key, seed_byte) in [
(contract_a.clone(), contract_a_id, contract_a_key, 0xA1u8),
(contract_b.clone(), contract_b_id, contract_b_key, 0xB2u8),
] {
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, seed_byte),
subscribe: true,
},
));
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
));
for i in 2..=5 {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for v in [10u64, 20, 30] {
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(v, seed_byte),
},
));
}
for v in [40u64, 50] {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(v, seed_byte),
},
));
}
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(240),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Six-peer lifecycle simulation failed: {:?}",
result.turmoil_result.err()
);
let states_a = verify_contract_propagation(&rt, &logs_handle, contract_a_key, 4);
let states_b = verify_contract_propagation(&rt, &logs_handle, contract_b_key, 4);
tracing::info!(
"test_six_peer_contract_lifecycle PASSED: contract_a on {} peers, contract_b on {} peers",
states_a.len(),
states_b.len()
);
}
#[test_log::test]
fn test_six_peer_lifecycle_mock_wasm() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x3151_0002_0001;
const NETWORK_NAME: &str = "six-peer-mock-wasm";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (mut sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 2, 4, 10, 5, 15, 3, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
sim.use_mock_wasm = true;
let contract = SimOperation::create_test_contract(0xC3);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0xC3),
subscribe: true,
},
)];
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
));
for i in 2..=5 {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for v in [10u64, 20, 30] {
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(v, 0xC3),
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(240),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"MockWasm lifecycle simulation failed: {:?}",
result.turmoil_result.err()
);
verify_contract_propagation(&rt, &logs_handle, contract_key, 4);
tracing::info!("test_six_peer_lifecycle_mock_wasm PASSED");
}
#[test_log::test]
fn test_late_joiner_receives_current_state() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x3151_0003_0001;
const NETWORK_NAME: &str = "late-joiner";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(NETWORK_NAME, 1, 3, 7, 3, 10, 2, SEED).await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xD4);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0xD4),
subscribe: true,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 2),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(10, 0xD4),
},
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(20, 0xD4),
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 3),
SimOperation::Subscribe { contract_id },
),
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(30, 0xD4),
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Late joiner simulation failed: {:?}",
result.turmoil_result.err()
);
let peer_states = verify_contract_propagation(&rt, &logs_handle, contract_key, 3);
let node3_label = NodeLabel::node(NETWORK_NAME, 3);
let node3_storage = result
.node_storages
.get(&node3_label)
.expect("node 3 should have a storage handle");
let node3_state = node3_storage.get_stored_state(&contract_key);
assert!(
node3_state.is_some(),
"Late joiner (node 3) should have contract state, but storage is empty. \
This indicates the subscribe-after-updates path failed to fetch current state."
);
tracing::info!(
"test_late_joiner_receives_current_state PASSED: {} peers converged, \
late joiner confirmed with state",
peer_states.len()
);
}
#[test_log::test]
#[cfg(feature = "nightly_tests")]
fn test_subscribe_renewal_long_running() {
const SEED: u64 = 0x3151_0004_0001;
tracing::info!("=== Starting Subscription Renewal Long-Running Test ===");
TestConfig::long_running("sub-renewal-long", SEED)
.with_mock_wasm()
.run_direct()
.assert_ok()
.verify_operation_coverage()
.verify_state_report();
tracing::info!("test_subscribe_renewal_long_running PASSED");
}
#[test_log::test]
fn test_topology_subscribe_health() {
const SEED: u64 = 0x3152_0001_0001;
tracing::info!("=== Starting Topology Subscribe Health Test ===");
let result = TestConfig::medium("sub-health", SEED)
.with_iterations(100)
.with_duration(Duration::from_secs(60))
.run()
.assert_ok();
let rt = create_runtime();
let (successes, failures) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let mut successes = 0u64;
let mut failures = 0u64;
for log in logs.iter() {
match log.kind.subscribe_outcome() {
Some(true) => successes += 1,
Some(false) => failures += 1,
None => {}
}
}
(successes, failures)
});
let total = successes + failures;
assert!(
total >= 5,
"Only {} subscribe outcome events in {} total events — \
too few for meaningful success rate measurement",
total,
result.event_count
);
let success_rate = successes as f64 / total as f64;
tracing::info!(
"Subscribe health: {}/{} succeeded ({:.1}%)",
successes,
total,
success_rate * 100.0
);
assert!(
success_rate >= 0.70,
"Subscribe success rate {:.1}% is below 70% threshold \
({} succeeded, {} failed out of {} total). \
See #3138 for context on subscribe rate regressions.",
success_rate * 100.0,
successes,
failures,
total
);
tracing::info!(
"test_topology_subscribe_health PASSED: {:.1}% success rate",
success_rate * 100.0
);
}
#[test_log::test]
fn test_router_learning() {
const SEED: u64 = 0x3152_0002_0001;
tracing::info!("=== Starting Router Learning Test ===");
let result = TestConfig::small("router-learn", SEED)
.with_iterations(150)
.with_duration(Duration::from_secs(200))
.run()
.assert_ok();
let snapshots = result.router_snapshots();
let any_prediction_active = snapshots.iter().any(|(_f, _s, active)| *active);
tracing::info!(
"Router snapshots: {} total, prediction_active in any: {}",
snapshots.len(),
any_prediction_active
);
let rt = create_runtime();
let outcomes: Vec<bool> = rt.block_on(async {
let logs = result.logs_handle.lock().await;
logs.iter()
.filter_map(|log| log.kind.route_outcome_is_success())
.collect()
});
assert!(
outcomes.len() >= 10,
"Only {} route outcome events — too few for meaningful comparison. \
Expected at least 10.",
outcomes.len()
);
let mid = outcomes.len() / 2;
let (first_half, second_half) = outcomes.split_at(mid);
let first_failures = first_half.iter().filter(|&&s| !s).count();
let second_failures = second_half.iter().filter(|&&s| !s).count();
let first_rate = if first_half.is_empty() {
0.0
} else {
first_failures as f64 / first_half.len() as f64
};
let second_rate = second_failures as f64 / second_half.len() as f64;
tracing::info!(
"Route failure rates: first half {:.1}% ({}/{}), second half {:.1}% ({}/{})",
first_rate * 100.0,
first_failures,
first_half.len(),
second_rate * 100.0,
second_failures,
second_half.len()
);
assert!(
second_rate <= first_rate + 0.15,
"Router is degrading: second-half failure rate ({:.1}%) exceeds \
first-half ({:.1}%) by more than 15pp. \
This suggests the router is not learning from feedback. See #3128/#3136.",
second_rate * 100.0,
first_rate * 100.0
);
tracing::info!("test_router_learning PASSED");
}
#[test_log::test]
fn test_interest_renewal() {
const SEED: u64 = 0x3152_0003_0001;
tracing::info!("=== Starting Interest Renewal Test ===");
let result = TestConfig::medium("interest-renew", SEED)
.with_nodes(4)
.with_iterations(400)
.with_event_wait(Duration::from_secs(3))
.no_convergence_wait()
.run_direct()
.assert_ok();
let rt = create_runtime();
let (early_broadcasts, late_broadcasts) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let log_count = logs.len();
let mid_index = log_count / 2;
let mut early = 0usize;
let mut late = 0usize;
for (i, log) in logs.iter().enumerate() {
if log.kind.is_update_broadcast_received() {
if i < mid_index {
early += 1;
} else {
late += 1;
}
}
}
(early, late)
});
let total = early_broadcasts + late_broadcasts;
tracing::info!(
"Broadcast received events: {} total (early: {}, late: {})",
total,
early_broadcasts,
late_broadcasts
);
assert!(
total > 0,
"No BroadcastReceived events found in {} events — \
simulation may not be generating updates",
result.event_count
);
assert!(
late_broadcasts > 0,
"No BroadcastReceived events in second half of simulation. \
Subscriptions may have expired without renewal. See #3093."
);
let ratio = late_broadcasts as f64 / early_broadcasts.max(1) as f64;
tracing::info!(
"Late/early broadcast ratio: {:.2} ({}/{})",
ratio,
late_broadcasts,
early_broadcasts
);
assert!(
ratio > 0.2,
"Late broadcast ratio ({:.2}) is below 0.2 threshold — \
subscriptions are likely degrading across lease cycles. See #3093.",
ratio
);
tracing::info!(
"test_interest_renewal PASSED: {:.2} late/early ratio",
ratio
);
}
#[test_log::test]
fn test_interest_ttl_refresh_on_broadcast() {
const SEED: u64 = 0x3107_0BCA_0001;
tracing::info!("=== Starting Interest TTL Refresh on Broadcast Test ===");
tracing::info!("Virtual time target: 1800s (~1.5x INTEREST_TTL of 1200s)");
let result = TestConfig::small("ttl-refresh-bcast", SEED)
.with_gateways(1)
.with_nodes(2) .with_max_contracts(2) .with_iterations(300) .with_event_wait(Duration::from_secs(6))
.run_direct()
.assert_ok();
let rt = create_runtime();
let (early_broadcasts, mid_broadcasts, late_broadcasts) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let log_count = logs.len();
let third = log_count / 3;
let mut early = 0usize;
let mut mid = 0usize;
let mut late = 0usize;
for (i, log) in logs.iter().enumerate() {
if log.kind.is_update_broadcast_received() {
if i < third {
early += 1;
} else if i < third * 2 {
mid += 1;
} else {
late += 1;
}
}
}
(early, mid, late)
});
let total = early_broadcasts + mid_broadcasts + late_broadcasts;
tracing::info!(
"Broadcast received events: {} total (early: {}, mid: {}, late: {})",
total,
early_broadcasts,
mid_broadcasts,
late_broadcasts
);
assert!(
total > 0,
"No BroadcastReceived events found in {} logged events — \
simulation may not be generating updates. Seed: 0x{:X}",
result.event_count,
SEED
);
assert!(
late_broadcasts > 0,
"No BroadcastReceived events in the final third of simulation \
(after INTEREST_TTL boundary). Interest TTL is NOT being refreshed \
on broadcast send — subscriptions have silently expired. \
See #3093, #3107. Seed: 0x{:X}",
SEED
);
let late_ratio = late_broadcasts as f64 / early_broadcasts.max(1) as f64;
tracing::info!(
"Late/early broadcast ratio: {:.2} ({}/{})",
late_ratio,
late_broadcasts,
early_broadcasts
);
assert!(
late_ratio > 0.1,
"Late broadcast ratio ({:.2}) dropped below 0.1 — \
interest TTL refresh may be partially broken. \
Early: {}, Mid: {}, Late: {}. See #3093, #3107. Seed: 0x{:X}",
late_ratio,
early_broadcasts,
mid_broadcasts,
late_broadcasts,
SEED
);
tracing::info!(
"test_interest_ttl_refresh_on_broadcast PASSED: late/early ratio {:.2}, \
total broadcasts: {} (early: {}, mid: {}, late: {})",
late_ratio,
total,
early_broadcasts,
mid_broadcasts,
late_broadcasts
);
}
async fn let_network_run(sim: &mut SimNetwork, duration: Duration) {
let step = Duration::from_millis(100);
let mut elapsed = Duration::ZERO;
while elapsed < duration {
sim.advance_time(step);
tokio::task::yield_now().await;
tokio::time::sleep(Duration::from_millis(10)).await;
elapsed += step;
}
}
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_thundering_herd_connect_storm() {
const SEED: u64 = 0x3207_0000_3208;
const NETWORK_NAME: &str = "thundering-herd-connect";
tracing::info!("=== Thundering Herd CONNECT Storm Test (Issue #3207, PR #3208) ===");
let mut sim = SimNetwork::new(NETWORK_NAME, 1, 20, 7, 3, 10, 2, SEED).await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 5, 10)
.await;
let logs_handle = sim.event_logs_handle();
tracing::info!("Phase 1: Stabilizing network (5s)");
let_network_run(&mut sim, Duration::from_secs(5)).await;
sim.check_partial_connectivity(Duration::from_secs(20), 0.8)
.await
.expect("Network should reach ≥80% connectivity before crash");
tracing::info!("Phase 1 complete: network connected");
let baseline_connects = {
let logs = logs_handle.lock().await;
logs.iter().filter(|log| log.kind.is_connect()).count()
};
tracing::info!("Baseline Connect events: {}", baseline_connects);
let gateway_label = sim
.all_node_addresses()
.keys()
.find(|label| label.is_gateway())
.cloned()
.expect("Should have a gateway");
tracing::info!(?gateway_label, "Phase 2: Crashing gateway");
let crashed = sim.crash_node(&gateway_label);
assert!(crashed, "Gateway should crash successfully");
tracing::info!("Phase 3: Peers detecting dead gateway (10s)");
let_network_run(&mut sim, Duration::from_secs(10)).await;
tracing::info!("Phase 4: Restarting gateway (thundering herd begins)");
let restart_seed = SEED.wrapping_add(0x1000);
let handle = sim
.restart_node::<rand::rngs::SmallRng>(&gateway_label, restart_seed, 5, 5)
.await;
assert!(handle.is_some(), "Gateway should restart successfully");
tracing::info!("Phase 5: Reconnection storm playing out (30s)");
let_network_run(&mut sim, Duration::from_secs(30)).await;
tracing::info!("Phase 6: Verifying network recovery");
let final_connects = {
let logs = logs_handle.lock().await;
logs.iter().filter(|log| log.kind.is_connect()).count()
};
let storm_connects = final_connects - baseline_connects;
tracing::info!(
"Connect events after restart: {} (baseline: {}, storm: {})",
final_connects,
baseline_connects,
storm_connects
);
sim.check_partial_connectivity(Duration::from_secs(20), 0.8)
.await
.expect(
"Network should recover to ≥80% connectivity after gateway restart. \
The rate limiter should throttle but not prevent reconnection.",
);
tracing::info!(
"test_thundering_herd_connect_storm PASSED: {} Connect events after restart, \
network recovered to ≥80% connectivity",
storm_connects
);
}
#[test_log::test]
fn test_isolated_node_rebootstraps_via_gateway() {
use freenet::dev_tool::NodeLabel;
use freenet::simulation::Partition;
const SEED: u64 = 0x3219_B007_0001;
const NETWORK_NAME: &str = "isolated-rebootstrap";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle, node_addrs) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 4, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
(sim, logs_handle, node_addrs)
});
let (isolated_label, isolated_addr) = node_addrs
.iter()
.find(|(label, _)| label.is_node())
.map(|(l, a)| (l.clone(), *a))
.expect("Need at least one non-gateway node");
let side_a: std::collections::HashSet<_> = [isolated_addr].into_iter().collect();
let side_b: std::collections::HashSet<_> = node_addrs
.iter()
.filter(|(label, _)| **label != isolated_label)
.map(|(_, addr)| *addr)
.collect();
tracing::info!(
isolated = %isolated_label,
isolated_addr = %isolated_addr,
rest_count = side_b.len(),
"Will isolate one node from all others"
);
let network_name = NETWORK_NAME.to_string();
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
3, 80, Duration::from_secs(120), Duration::from_millis(500), move || async move {
tracing::info!("Injecting partition: isolating one node from all others");
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
let partition = Partition::new(side_a, side_b).permanent(0);
state.config.add_partition(partition);
}
tokio::time::sleep(Duration::from_secs(20)).await;
tracing::info!("Isolation period over — node should have zero connections now");
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
state.config.partitions.clear();
}
tracing::info!("Partition healed — waiting for re-bootstrap");
tokio::time::sleep(Duration::from_secs(30)).await;
tracing::info!("Post-heal convergence period complete");
Ok(())
},
);
assert!(
result.is_ok(),
"Isolated-node re-bootstrap simulation failed: {:?}",
result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"Isolated-rebootstrap: {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
assert!(
convergence.diverged.is_empty(),
"Isolated node failed to re-bootstrap: {} contracts diverged (expected 0). \
This indicates the gateway bootstrap fallback in connection_maintenance \
did not recover the node after partition heal.",
convergence.diverged.len()
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"=== ISOLATED-REBOOTSTRAP ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
report.total_events,
report.state_events,
report.contracts_analyzed,
report.anomalies.len()
);
let divergences = report.divergences();
let stale = report.stale_peers();
tracing::warn!(" divergences={}, stale={}", divergences.len(), stale.len(),);
for (i, anomaly) in report.anomalies.iter().enumerate() {
tracing::debug!(" anomaly[{}] = {:?}", i, anomaly);
}
}
#[test]
fn test_direct_runner_churn() {
use freenet::dev_tool::ChurnConfig;
const SEED: u64 = 0xC1_0055_FEED;
setup_deterministic_state(SEED);
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new(
"test-churn",
2, 8, 10, 5, 15, 3, SEED,
)
.await
});
sim.with_churn(ChurnConfig {
crash_probability: 0.20,
tick_interval: Duration::from_millis(200),
recovery_delay: Duration::from_millis(500),
max_simultaneous_crashes: Some(2),
permanent_crash_rate: 0.05,
warmup_delay: Duration::from_secs(5),
});
let logs_handle = sim.event_logs_handle();
drop(rt);
sim.run_simulation_direct::<rand::rngs::SmallRng>(
SEED,
10, 30, Duration::from_millis(200),
)
.expect("Direct simulation with churn should complete without panic");
let rt = create_runtime();
let event_count = rt.block_on(async {
let logs = logs_handle.lock().await;
logs.len()
});
assert!(
event_count > 0,
"Simulation with churn should produce events, got 0"
);
tracing::info!(
"CHURN TEST PASSED: {} events produced with node churn enabled",
event_count
);
}
#[test]
fn test_direct_runner_churn_actually_drops_packets() {
use freenet::dev_tool::ChurnConfig;
const SEED: u64 = 0xC1_0055_D009;
const NETWORK_NAME: &str = "test-churn-drops";
setup_deterministic_state(SEED);
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new(
NETWORK_NAME,
2, 8, 10, 5, 15, 3, SEED,
)
.await
});
sim.with_churn(ChurnConfig {
crash_probability: 0.5,
tick_interval: Duration::from_millis(200),
recovery_delay: Duration::from_millis(500),
max_simultaneous_crashes: Some(3),
permanent_crash_rate: 1.0, warmup_delay: Duration::from_secs(5),
});
let injector = freenet::dev_tool::get_fault_injector(NETWORK_NAME)
.expect("fault injector is registered at SimNetwork construction");
let logs_handle = sim.event_logs_handle();
drop(rt);
sim.run_simulation_direct::<rand::rngs::SmallRng>(
SEED,
10, 30, Duration::from_millis(200),
)
.expect("Direct simulation with churn should complete without panic");
let crash_drops = injector.lock().unwrap().stats.messages_dropped_crash;
let rt = create_runtime();
let event_count = rt.block_on(async { logs_handle.lock().await.len() });
assert!(event_count > 0, "churn simulation produced no events");
assert!(
crash_drops > 0,
"expected crashed-node packets to be DROPPED (messages_dropped_crash > 0), \
but got {crash_drops}. Fault injection is inert on the direct runner — the \
packet-delivery callback was not installed / enforced (regression of #4694)."
);
tracing::info!(
"CHURN DROP TEST PASSED: {crash_drops} packets dropped for crashed nodes \
({event_count} events produced)"
);
}
#[test_log::test]
fn test_unhealthy_peer_eviction() {
use freenet::dev_tool::NodeLabel;
use freenet::simulation::Partition;
const SEED: u64 = 0xDEAD_BEEF_0042;
const NETWORK_NAME: &str = "unhealthy-eviction";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle, node_addrs) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 4, 7,
3,
10,
2,
SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
let node_addrs: HashMap<NodeLabel, SocketAddr> = sim.all_node_addresses().clone();
(sim, logs_handle, node_addrs)
});
let victim_addr: SocketAddr = *node_addrs
.iter()
.find(|(label, _)| label.is_node())
.expect("Need at least 1 non-gateway node")
.1;
let all_addrs: HashSet<_> = node_addrs.values().copied().collect();
let victim_set: HashSet<_> = [victim_addr].into_iter().collect();
let healthy_set: HashSet<_> = all_addrs.difference(&victim_set).copied().collect();
let network_name = NETWORK_NAME.to_string();
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
5, 150, Duration::from_secs(600), Duration::from_secs(2), move || async move {
tracing::info!("Partitioning victim node: {}", victim_addr);
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
let partition = Partition::new(victim_set, healthy_set).permanent(0);
state.config.add_partition(partition);
}
tokio::time::sleep(Duration::from_secs(120)).await;
tracing::info!("Partition active for 120s virtual time");
Ok(())
},
);
assert!(
result.is_ok(),
"Unhealthy-peer eviction simulation failed: {:?}",
result.err()
);
let event_count = rt.block_on(async { logs_handle.lock().await.len() });
assert!(event_count > 0, "Simulation should produce events, got 0");
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"=== UNHEALTHY EVICTION REPORT: {} events, {} state, {} contracts, {} anomalies ===",
report.total_events,
report.state_events,
report.contracts_analyzed,
report.anomalies.len()
);
}
#[test]
fn test_readiness_gating_production_config() {
const SEED: u64 = 0xBEAD_10A7_E001;
setup_deterministic_state(SEED);
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new(
"test-readiness-gating",
1, 4, 10, 5, 10, 3, SEED,
)
.await
});
sim.with_readiness_gating(3);
let logs_handle = sim.event_logs_handle();
drop(rt);
sim.run_simulation_direct::<rand::rngs::SmallRng>(
SEED,
5, 20, Duration::from_millis(200),
)
.expect("Simulation with readiness gating should complete without panic");
let rt = create_runtime();
let event_count = rt.block_on(async {
let logs = logs_handle.lock().await;
logs.len()
});
assert!(
event_count > 0,
"Simulation with readiness gating should produce events, got 0"
);
tracing::info!(
"READINESS GATING TEST PASSED: {} events produced with gating(3)",
event_count
);
}
#[test]
fn test_readiness_gating_with_message_loss() {
use freenet::simulation::FaultConfig;
const SEED: u64 = 0xBEAD_10A7_E002;
setup_deterministic_state(SEED);
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new(
"test-readiness-gating-loss",
1, 4, 10, 5, 10, 3, SEED,
)
.await
});
sim.with_readiness_gating(3);
sim.with_fault_injection(FaultConfig::builder().message_loss_rate(0.05).build());
let logs_handle = sim.event_logs_handle();
drop(rt);
sim.run_simulation_direct::<rand::rngs::SmallRng>(
SEED,
5, 25, Duration::from_millis(200),
)
.expect("Simulation with readiness gating + message loss should complete");
let rt = create_runtime();
let event_count = rt.block_on(async {
let logs = logs_handle.lock().await;
logs.len()
});
assert!(
event_count > 0,
"Simulation with readiness gating + message loss should produce events, got 0"
);
tracing::info!(
"READINESS GATING + MESSAGE LOSS TEST PASSED: {} events",
event_count
);
}
#[test]
fn test_gateway_version_probe_fires() {
const SEED: u64 = 0x6A7E_7AE9_0001;
setup_deterministic_state(SEED);
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new(
"test-gw-version-probe",
1, 3, 7, 3, 10, 2, SEED,
)
.await
});
let logs_handle = sim.event_logs_handle();
drop(rt);
sim.run_simulation_direct::<rand::rngs::SmallRng>(SEED, 3, 30, Duration::from_secs(1))
.expect("Simulation should complete without panic");
let rt = create_runtime();
let unique_connect_txs = rt.block_on(async {
let logs = logs_handle.lock().await;
let mut seen = std::collections::HashSet::new();
for log in logs.iter() {
if log.kind.variant_name() == "Connect" {
seen.insert(log.tx);
}
}
seen.len()
});
assert!(
unique_connect_txs > 6,
"Expected bootstrap (~3) + at least 4 probe CONNECTs, got {unique_connect_txs}"
);
}
#[test_log::test]
fn test_connect_despite_nat_partition() {
use freenet::dev_tool::NodeLabel;
use freenet::simulation::Partition;
const SEED: u64 = 0xC0DE_FA11_0001;
const NETWORK_NAME: &str = "nat-partition";
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle, node_addrs) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 5, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
(sim, logs_handle, node_addrs)
});
let mut non_gw_nodes: Vec<_> = node_addrs
.iter()
.filter(|(label, _)| label.is_node())
.collect();
non_gw_nodes.sort_by_key(|(label, _)| label.number());
assert!(
non_gw_nodes.len() >= 2,
"Need at least 2 non-gateway nodes for partition"
);
let node_a_addr = *non_gw_nodes[0].1;
let node_b_addr = *non_gw_nodes[1].1;
let side_a: HashSet<std::net::SocketAddr> = [node_a_addr].into_iter().collect();
let side_b: HashSet<std::net::SocketAddr> = [node_b_addr].into_iter().collect();
let network_name = NETWORK_NAME.to_string();
tracing::info!(
node_a = %node_a_addr,
node_b = %node_b_addr,
"Will inject NAT partition between two nodes"
);
let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
SEED,
3, 100, Duration::from_secs(120), Duration::from_millis(500), move || async move {
if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
let mut state = injector.lock().unwrap();
let partition = Partition::new(side_a, side_b).permanent(0);
state.config.add_partition(partition);
tracing::info!("NAT partition injected between two nodes");
}
tokio::time::sleep(Duration::from_secs(30)).await;
tracing::info!("NAT partition test: observation period complete");
Ok(())
},
);
assert!(
result.is_ok(),
"NAT partition simulation failed (nodes may have gotten stuck retrying \
unreachable acceptor): {:?}",
result.err()
);
let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
tracing::info!(
"NAT partition: {} converged, {} diverged",
convergence.converged.len(),
convergence.diverged.len()
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"=== NAT PARTITION ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
report.total_events,
report.state_events,
report.contracts_analyzed,
report.anomalies.len()
);
let divergences = report.divergences();
let stale = report.stale_peers();
tracing::warn!(" divergences={}, stale={}", divergences.len(), stale.len(),);
for (i, anomaly) in report.anomalies.iter().enumerate() {
tracing::debug!(" anomaly[{}] = {:?}", i, anomaly);
}
}
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_connection_growth_stall_regression() {
use freenet::dev_tool::NodeLabel;
use freenet::simulation::FaultConfig;
const SEED: u64 = 0x3408_3398_0001;
const NETWORK_NAME: &str = "connection-growth-stall";
const GATEWAYS: usize = 2;
const NODES: usize = 15;
const RING_MAX_HTL: usize = 7;
const RND_IF_HTL_ABOVE: usize = 3;
const MAX_CONN: usize = 10;
const MIN_CONN: usize = 5;
tracing::info!("=== Connection Growth Stall Regression Test ===");
tracing::info!("Verifies fixes: #3408, #3398, #3396, #3380");
setup_deterministic_state(SEED);
let mut sim = SimNetwork::new(
NETWORK_NAME,
GATEWAYS,
NODES,
RING_MAX_HTL,
RND_IF_HTL_ABOVE,
MAX_CONN,
MIN_CONN,
SEED,
)
.await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
.await;
tracing::info!("Phase 1: Connection growth — 20 virtual minutes, no faults");
let_network_run(&mut sim, Duration::from_secs(1200)).await;
let mut node_counts: Vec<usize> = (0..NODES)
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
node_counts.sort_unstable();
let num_sampled = node_counts.len();
assert!(num_sampled > 0, "No connection_managers available");
let median_conn = node_counts[num_sampled / 2];
let nodes_above_min = node_counts.iter().filter(|&&c| c >= MIN_CONN).count();
let fraction_above_min = nodes_above_min as f64 / num_sampled as f64;
tracing::info!("Phase 1 connection counts: {:?}", node_counts);
tracing::info!(
"Median={}, nodes at min_connections={}/{} ({:.0}%)",
median_conn,
nodes_above_min,
num_sampled,
fraction_above_min * 100.0
);
let connectivity = sim.node_connectivity();
let mut nodes_with_peer_connections = 0usize;
for (label, (_key, conns)) in &connectivity {
if !label.is_gateway() {
let has_non_gw_conn = conns.keys().any(|peer| !peer.is_gateway());
if has_non_gw_conn {
nodes_with_peer_connections += 1;
}
}
}
tracing::info!(
"Nodes with non-gateway peer connections: {}/{}",
nodes_with_peer_connections,
NODES
);
assert!(
median_conn >= MIN_CONN - 1,
"Connection growth stall: median={} must be >= {} (MIN_CONN - 1). \
Counts: {:?}. Seed: 0x{:X}",
median_conn,
MIN_CONN - 1,
node_counts,
SEED
);
assert!(
fraction_above_min >= 0.10,
"Too few nodes reached min_connections: {:.0}% ({}/{}) — expected >= 10%. \
Counts: {:?}. Seed: 0x{:X}",
fraction_above_min * 100.0,
nodes_above_min,
num_sampled,
node_counts,
SEED
);
let peer_conn_fraction = nodes_with_peer_connections as f64 / NODES as f64;
assert!(
peer_conn_fraction >= 0.50,
"Only {:.0}% of nodes have non-gateway peer connections (expected >= 50%). \
CONNECT forwarding is insufficient. Seed: 0x{:X}",
peer_conn_fraction * 100.0,
SEED
);
tracing::info!("Phase 2: NAT failure simulation — 20% message loss for 1 min");
sim.with_fault_injection(FaultConfig::builder().message_loss_rate(0.20).build());
let_network_run(&mut sim, Duration::from_secs(60)).await;
let mut node_counts_after: Vec<usize> = (0..NODES)
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
node_counts_after.sort_unstable();
let median_after = node_counts_after
.get(node_counts_after.len() / 2)
.copied()
.unwrap_or(0);
let nodes_isolated = node_counts_after.iter().filter(|&&c| c == 0).count();
let fraction_isolated = nodes_isolated as f64 / NODES as f64;
tracing::info!(
"Phase 2 connection counts after NAT failures: {:?}",
node_counts_after
);
tracing::info!(
"Median={}, isolated={}/{} ({:.0}%)",
median_after,
nodes_isolated,
NODES,
fraction_isolated * 100.0
);
assert!(
median_after >= MIN_CONN.saturating_sub(2).max(2),
"Death spiral: median connections after NAT failures = {} (expected >= {}). \
Counts: {:?}. Seed: 0x{:X}",
median_after,
MIN_CONN.saturating_sub(2).max(2),
node_counts_after,
SEED
);
assert!(
fraction_isolated < 0.10,
"{:.0}% of nodes isolated after NAT failures (threshold: 10%). \
Counts: {:?}. Seed: 0x{:X}",
fraction_isolated * 100.0,
node_counts_after,
SEED
);
tracing::info!(
"PASSED: Phase1[median={}, above_min={}/{}, peer_conns={}/{}] \
Phase2[median={}, isolated={}/{}]",
median_conn,
nodes_above_min,
num_sampled,
nodes_with_peer_connections,
NODES,
median_after,
nodes_isolated,
NODES
);
}
#[test_log::test]
fn test_get_succeeds_despite_readiness_gating() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0xAE7F_1A00_0001;
const NETWORK_NAME: &str = "get-readiness-fallback";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (mut sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, 3, 7, 3, 10, 2, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let total_nodes = 1 + 3; sim.with_readiness_gating(total_nodes + 1);
let contract = SimOperation::create_test_contract(0xAE);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: vec![1, 2, 3, 4],
subscribe: false,
},
),
ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, 1),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"GET readiness fallback simulation failed: {:?}",
result.turmoil_result.err()
);
let node1_label = NodeLabel::node(NETWORK_NAME, 1);
let node1_storage = result
.node_storages
.get(&node1_label)
.expect("node 1 should have a storage handle");
let node1_state = node1_storage.get_stored_state(&contract_key);
assert!(
node1_state.is_some(),
"Node 1 should have contract state after GET, but storage is empty. \
This indicates k_closest_potentially_hosting returned empty due to \
readiness gating (the bug from #3356/#3423)."
);
let rt = create_runtime();
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"Anomaly report: {} anomalies across {} contracts",
report.anomalies.len(),
report.contracts_analyzed
);
tracing::info!(
"test_get_succeeds_despite_readiness_gating PASSED: \
node 1 has contract state, {} events analyzed",
report.total_events
);
}
#[test_log::test]
fn test_get_routing_coverage_low_htl() {
use std::sync::atomic::Ordering;
use freenet::dev_tool::{
GET_RELAY_DRIVER_CALL_COUNT, NodeLabel, RELAY_PUT_DRIVER_CALL_COUNT,
RELAY_SUBSCRIBE_DRIVER_CALL_COUNT, ScheduledOperation, SimOperation,
register_crdt_contract,
};
const SEED: u64 = 0xC0DE_B0CA_0032;
const NETWORK_NAME: &str = "get-routing-coverage";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let relay_baseline = GET_RELAY_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
let relay_put_baseline = RELAY_PUT_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
let relay_subscribe_baseline = RELAY_SUBSCRIBE_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
let rt = create_runtime();
let num_nodes = 15;
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, num_nodes, 3, 1, 7, 3, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xC0);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0xC0),
subscribe: true,
},
),
];
for i in 1..=12 {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(42, 0xC0),
},
));
for i in 1..=num_nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(300),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let mut nodes_without_state = Vec::new();
for i in 1..=num_nodes {
let label = NodeLabel::node(NETWORK_NAME, i);
let storage = result
.node_storages
.get(&label)
.unwrap_or_else(|| panic!("node {i} should have a storage handle"));
if storage.get_stored_state(&contract_key).is_none() {
nodes_without_state.push(format!("node-{i}"));
}
}
assert!(
nodes_without_state.is_empty(),
"GET routing exhaustion: {} nodes failed to get contract state \
(contract only cached at ~5 nodes due to HTL=4). \
Failed nodes: {:?}. See #3431.",
nodes_without_state.len(),
nodes_without_state
);
let relay_after = GET_RELAY_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
let relay_delta = relay_after.saturating_sub(relay_baseline);
assert!(
relay_delta > 0,
"RELAY_DRIVER_CALL_COUNT did not advance during the test — the \
relay task-per-tx driver never ran. With {num_nodes} nodes, HTL=3, \
and ~5 caching nodes, every non-caching requester should hit at \
least one relay hop. Dispatch gate in \
handle_pure_network_message_v1 has likely regressed. Baseline: {relay_baseline}, after: {relay_after}."
);
let relay_put_after = RELAY_PUT_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
let relay_put_delta = relay_put_after.saturating_sub(relay_put_baseline);
assert!(
relay_put_delta > 0,
"RELAY_PUT_DRIVER_CALL_COUNT did not advance during the test — \
the relay PUT task-per-tx driver never ran. Gateway PUT at HTL=3 \
in a {num_nodes}-node network must traverse at least one relay hop. \
Dispatch gate for PUT has likely regressed. \
Baseline: {relay_put_baseline}, after: {relay_put_after}."
);
let relay_subscribe_after = RELAY_SUBSCRIBE_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
let relay_subscribe_delta = relay_subscribe_after.saturating_sub(relay_subscribe_baseline);
assert!(
relay_subscribe_delta > 0,
"RELAY_SUBSCRIBE_DRIVER_CALL_COUNT did not advance during the \
test — the relay SUBSCRIBE task-per-tx driver never ran. With \
12 node subscribes at HTL=3 and ~5 caching nodes, at least one \
subscribe must traverse a relay hop. Dispatch gate for \
SUBSCRIBE has likely regressed. \
Baseline: {relay_subscribe_baseline}, after: {relay_subscribe_after}."
);
let rt = create_runtime();
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"test_get_routing_coverage_low_htl PASSED: {} anomalies, {} events, \
relay_driver_calls={relay_delta}",
report.anomalies.len(),
report.total_events,
);
}
#[test_log::test]
fn test_relay_route_events_multihop() {
use std::sync::atomic::Ordering;
use freenet::dev_tool::{
NodeLabel, RELAY_GET_ROUTE_EVENT_COUNT, RELAY_PUT_ROUTE_EVENT_COUNT, ScheduledOperation,
SimOperation, register_crdt_contract,
};
const SEED: u64 = 0xC0DE_B0CA_0040;
const NETWORK_NAME: &str = "relay-route-events";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let get_route_baseline = RELAY_GET_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
let put_route_baseline = RELAY_PUT_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
let rt = create_runtime();
let num_nodes = 15;
let sim = rt.block_on(async {
SimNetwork::new(
NETWORK_NAME,
1, num_nodes, 3, 1, 4, 3, SEED,
)
.await
});
let contract = SimOperation::create_test_contract(0xC0);
let contract_id = *contract.key().id();
register_crdt_contract(contract_id);
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0xC0),
subscribe: true,
},
)];
for i in 1..=4 {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
for i in 1..=num_nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(300),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let get_route_after = RELAY_GET_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
let put_route_after = RELAY_PUT_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
assert!(
get_route_after > get_route_baseline,
"RELAY_GET_ROUTE_EVENT_COUNT did not advance — in a sparse {num_nodes}-node \
mesh (max_connections=4) at least one GET must forward through a relay that \
feeds a route event to the local Router (a NotFound reply counts too). \
Baseline: {get_route_baseline}, after: {get_route_after}."
);
assert!(
put_route_after > put_route_baseline,
"RELAY_PUT_ROUTE_EVENT_COUNT did not advance — the gateway PUT at HTL=3 in a \
sparse {num_nodes}-node mesh must forward through at least one relay. \
Baseline: {put_route_baseline}, after: {put_route_after}."
);
}
#[test_log::test]
fn test_get_retry_with_alternatives_sparse_topology() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x3506_000B_0001;
const NETWORK_NAME: &str = "get-retry-sparse";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let num_nodes = 10;
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, num_nodes, 2, 1, 6, 3, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xBE);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let mut operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: vec![10, 20, 30, 40],
subscribe: false,
},
),
];
for i in 1..=num_nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(300), Duration::from_secs(90), );
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let mut nodes_without_state = Vec::new();
for i in 1..=num_nodes {
let label = NodeLabel::node(NETWORK_NAME, i);
let storage = result
.node_storages
.get(&label)
.unwrap_or_else(|| panic!("node {i} should have a storage handle"));
if storage.get_stored_state(&contract_key).is_none() {
nodes_without_state.push(format!("node-{i}"));
}
}
assert!(
nodes_without_state.is_empty(),
"GET retry regression: {} of {} nodes failed to get contract state \
(contract cached at only ~2-3 nodes due to HTL=2, retry should find it). \
Failed nodes: {:?}. See PR #3444.",
nodes_without_state.len(),
num_nodes,
nodes_without_state
);
let rt = create_runtime();
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"test_get_retry_with_alternatives_sparse_topology PASSED: \
all {} nodes got contract state, {} anomalies, {} events",
num_nodes,
report.anomalies.len(),
report.total_events
);
}
#[test_log::test]
fn test_auto_fetch_from_update_sender() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0xFE7C_A100_0001;
const NETWORK_NAME: &str = "auto-fetch-update";
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let num_nodes = 8;
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
1, num_nodes, 2, 1, 6, 3, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0xFE);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let mut operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_crdt_state(1, 0xFE),
subscribe: true,
},
),
];
for i in 1..=num_nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Subscribe { contract_id },
));
}
operations.push(ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Update {
key: contract_key,
data: SimOperation::create_crdt_state(42, 0xFE),
},
));
for i in 1..=num_nodes {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(300), Duration::from_secs(90), );
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let mut nodes_without_state = Vec::new();
for i in 1..=num_nodes {
let label = NodeLabel::node(NETWORK_NAME, i);
let storage = result
.node_storages
.get(&label)
.unwrap_or_else(|| panic!("node {i} should have a storage handle"));
if storage.get_stored_state(&contract_key).is_none() {
nodes_without_state.push(format!("node-{i}"));
}
}
let success_count = num_nodes - nodes_without_state.len();
let success_rate = success_count as f64 / num_nodes as f64;
assert!(
success_rate >= 0.75,
"Auto-fetch regression: only {} of {} nodes ({:.0}%) got contract state \
after UPDATE broadcast. Without auto-fetch, nodes that didn't receive \
PUT due to low HTL would never get the contract. \
Failed nodes: {:?}. See PR #3444.",
success_count,
num_nodes,
success_rate * 100.0,
nodes_without_state
);
let rt = create_runtime();
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"test_auto_fetch_from_update_sender PASSED: \
{}/{} nodes got contract state ({:.0}%), {} anomalies, {} events",
success_count,
num_nodes,
success_rate * 100.0,
report.anomalies.len(),
report.total_events
);
}
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_connection_growth_plateau_diagnostic() {
use freenet::dev_tool::NodeLabel;
const SEED: u64 = 0x3555_D1A6_0002;
const NETWORK_NAME: &str = "growth-plateau-diag";
const GATEWAYS: usize = 2;
const NODES: usize = 50;
const RING_MAX_HTL: usize = 7;
const RND_IF_HTL_ABOVE: usize = 3;
const MAX_CONN: usize = 20;
const MIN_CONN: usize = 10;
tracing::info!("=== Connection Growth Plateau Diagnostic ===");
setup_deterministic_state(SEED);
let mut sim = SimNetwork::new(
NETWORK_NAME,
GATEWAYS,
NODES,
RING_MAX_HTL,
RND_IF_HTL_ABOVE,
MAX_CONN,
MIN_CONN,
SEED,
)
.await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
.await;
let logs_handle = sim.event_logs_handle();
let phases = [
("5min", Duration::from_secs(300)),
("10min", Duration::from_secs(300)),
("15min", Duration::from_secs(300)),
("20min", Duration::from_secs(300)),
];
for (phase_name, duration) in &phases {
let_network_run(&mut sim, *duration).await;
let mut node_counts: Vec<usize> = (0..NODES)
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
node_counts.sort_unstable();
let n = node_counts.len();
if n == 0 {
tracing::info!("[{}] No nodes sampled", phase_name);
continue;
}
let median = node_counts[n / 2];
let min = node_counts[0];
let max = node_counts[n - 1];
let above_min = node_counts.iter().filter(|&&c| c >= MIN_CONN).count();
let zero = node_counts.iter().filter(|&&c| c == 0).count();
let avg = node_counts.iter().sum::<usize>() as f64 / n as f64;
let (initiated, terminus_accepted, terminus_rejected, forwarded, connected, disconnected) = {
let logs = logs_handle.lock().await;
let initiated = logs
.iter()
.filter(|l| l.kind.is_connect_initiated())
.count();
let terminus_accepted = logs
.iter()
.filter(|l| l.kind.is_connect_terminus_accepted())
.count();
let terminus_rejected = logs
.iter()
.filter(|l| l.kind.is_connect_terminus_rejected())
.count();
let forwarded = logs
.iter()
.filter(|l| l.kind.is_connect_forwarded())
.count();
let connected = logs
.iter()
.filter(|l| l.kind.is_connect_connected())
.count();
let disconnected = logs.iter().filter(|l| l.kind.is_disconnected()).count();
(
initiated,
terminus_accepted,
terminus_rejected,
forwarded,
connected,
disconnected,
)
};
let terminus_total = terminus_accepted + terminus_rejected;
let terminus_accept_rate = if terminus_total > 0 {
terminus_accepted as f64 / terminus_total as f64 * 100.0
} else {
0.0
};
tracing::info!(
"[{}] Connections: median={}, avg={:.1}, min={}, max={}, \
above_min={}/{} ({:.0}%), zero={}",
phase_name,
median,
avg,
min,
max,
above_min,
n,
above_min as f64 / n as f64 * 100.0,
zero,
);
tracing::info!(
"[{}] CONNECT pipeline: initiated={}, forwarded={}, \
terminus_accepted={}, terminus_rejected={}, \
terminus_accept_rate={:.0}%, connected={}, disconnected={}",
phase_name,
initiated,
forwarded,
terminus_accepted,
terminus_rejected,
terminus_accept_rate,
connected,
disconnected,
);
}
let final_counts: Vec<usize> = (0..NODES)
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
let mut histogram: BTreeMap<usize, usize> = BTreeMap::new();
for &c in &final_counts {
*histogram.entry(c).or_insert(0) += 1;
}
tracing::info!("Final connection distribution: {:?}", histogram);
for i in 0..GATEWAYS {
let label = NodeLabel::gateway(NETWORK_NAME, i);
if let Some(count) = sim.connection_count(&label) {
tracing::info!("Gateway {} connections: {}", i, count);
}
}
let total_connected = final_counts.iter().filter(|&&c| c > 0).count();
assert!(
total_connected > NODES / 2,
"Fewer than half the nodes have any connections: {}/{}",
total_connected,
NODES,
);
}
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_get_reliability_diagnostic() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x3570_D1A6_0001;
const NETWORK_NAME: &str = "get-reliability-diag";
const NUM_NODES: usize = 100;
const NUM_GATEWAYS: usize = 3;
const RING_MAX_HTL: usize = 10;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
NUM_GATEWAYS,
NUM_NODES,
RING_MAX_HTL, 7, 12, 4, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x35);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let mut operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: vec![0x35; 64],
subscribe: true,
},
),
];
for i in 0..NUM_NODES {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(600), Duration::from_secs(120), );
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let rt = create_runtime();
let (summary, dispatched_nodes) = rt.block_on(async {
let logs = logs_handle.lock().await;
let summary = freenet::tracing::summarize_get_outcomes_per_tx(&logs);
let dispatched_nodes: HashSet<_> = logs
.iter()
.filter(|log| log.kind.get_request_htl() == Some(RING_MAX_HTL))
.map(|log| log.peer_id.clone())
.collect();
(summary, dispatched_nodes.len())
});
let (successes, not_found, failures, timeouts) = (
summary.successes,
summary.not_found,
summary.failures,
summary.timeouts,
);
let network_successes = summary.network_successes;
let total_outcomes = summary.total();
let sorted_latencies = &summary.success_elapsed_ms;
let p50 = sorted_latencies
.get(sorted_latencies.len() / 2)
.copied()
.unwrap_or(0);
let p90 = sorted_latencies
.get(sorted_latencies.len() * 9 / 10)
.copied()
.unwrap_or(0);
let p99 = sorted_latencies
.get(sorted_latencies.len() * 99 / 100)
.copied()
.unwrap_or(0);
let max_latency = sorted_latencies.last().copied().unwrap_or(0);
let success_rate = if total_outcomes > 0 {
successes as f64 / total_outcomes as f64
} else {
0.0
};
let (response_sent_count, ack_received_count, response_lost_txs, retry_storm_txs) = rt
.block_on(async {
let logs = logs_handle.lock().await;
let mut response_sent_txs: HashSet<String> = HashSet::new();
let mut success_txs: HashSet<String> = HashSet::new();
let mut ack_received = 0u64;
let mut request_count_per_tx: HashMap<String, usize> = HashMap::new();
for log in logs.iter() {
let tx_str = log.tx.to_string();
if log.kind.is_get_response_sent() {
response_sent_txs.insert(tx_str.clone());
}
if log.kind.get_outcome() == Some(true) {
success_txs.insert(tx_str.clone());
}
if log.kind.is_forwarding_ack_received() {
ack_received += 1;
}
if log.kind.get_outcome().is_some()
|| log.kind.is_get_response_sent()
|| log.kind.is_forwarding_ack_received()
{
} else if log.kind.is_get_request() {
*request_count_per_tx.entry(tx_str).or_insert(0) += 1;
}
}
let response_lost: Vec<_> = response_sent_txs
.difference(&success_txs)
.cloned()
.collect();
let retry_storms: Vec<_> = request_count_per_tx
.iter()
.filter(|&(_, &count)| count > 10)
.map(|(tx, count)| (tx.clone(), *count))
.collect();
(
response_sent_txs.len(),
ack_received,
response_lost,
retry_storms,
)
});
tracing::info!("=== GET Reliability Diagnostic (#3570) ===");
tracing::info!(
"Network: {} gateways + {} nodes = {} total peers",
NUM_GATEWAYS,
NUM_NODES,
NUM_GATEWAYS + NUM_NODES
);
tracing::info!(
"GET outcomes (per attempt tx): {} total — {} success ({} network-traversed), \
{} not_found, {} failures, {} timeouts",
total_outcomes,
successes,
network_successes,
not_found,
failures,
timeouts
);
tracing::info!(
"GET success rate: {:.1}% ({}/{})",
success_rate * 100.0,
successes,
total_outcomes
);
tracing::info!(
"GET dispatch: {}/{} nodes dispatched at least one GET attempt",
dispatched_nodes,
NUM_NODES
);
tracing::info!(
"Latency (successful GETs): p50={}ms, p90={}ms, p99={}ms, max={}ms",
p50,
p90,
p99,
max_latency
);
tracing::info!(
"ForwardingAck: {} ACKs received, {} response_sent events, {} response-lost txs",
ack_received_count,
response_sent_count,
response_lost_txs.len()
);
if !retry_storm_txs.is_empty() {
tracing::info!(
"Retry storms (>10 requests per tx): {} txs, max {} requests",
retry_storm_txs.len(),
retry_storm_txs.iter().map(|(_, c)| c).max().unwrap_or(&0)
);
}
let mut nodes_with_state = 0;
let mut nodes_without_state = Vec::new();
for i in 0..NUM_NODES {
let label = NodeLabel::node(NETWORK_NAME, i);
if let Some(storage) = result.node_storages.get(&label) {
if storage.get_stored_state(&contract_key).is_some() {
nodes_with_state += 1;
} else {
nodes_without_state.push(i);
}
} else {
nodes_without_state.push(i);
}
}
tracing::info!(
"Storage verification: {}/{} nodes have contract state",
nodes_with_state,
NUM_NODES
);
if !nodes_without_state.is_empty() {
tracing::info!(
"Nodes missing state ({} total): {:?}{}",
nodes_without_state.len(),
&nodes_without_state[..nodes_without_state.len().min(20)],
if nodes_without_state.len() > 20 {
"..."
} else {
""
}
);
}
assert!(
total_outcomes >= 10,
"Only {} GET outcome transactions — too few for meaningful analysis",
total_outcomes
);
assert!(
success_rate >= 0.90,
"GET success rate {:.1}% below 0.90 floor (#4362 fixed should keep this high). \
{} succeeded, {} not_found, {} failures, {} timeouts out of {} total. \
See #3570 for context.",
success_rate * 100.0,
successes,
not_found,
failures,
timeouts,
total_outcomes
);
assert!(
dispatched_nodes >= NUM_NODES * 8 / 10,
"Only {}/{} scheduled GETs dispatched an operation — the test \
exercised only a fraction of its workload; the bootstrap acceptance \
collapse (#4362) appears to have regressed (#4361)",
dispatched_nodes,
NUM_NODES
);
assert!(
network_successes >= 5,
"Only {} of {} successful GETs traversed the network (hop_count >= 1) \
— the success metric is measuring local availability, not routing (#4361)",
network_successes,
successes
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"Anomaly report: {} anomalies across {} contracts, {} total events",
report.anomalies.len(),
report.contracts_analyzed,
report.total_events
);
tracing::info!(
"test_get_reliability_diagnostic DONE: {:.1}% success rate, \
{}/{} nodes have state",
success_rate * 100.0,
nodes_with_state,
NUM_NODES
);
}
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_bootstrap_acceptance_no_dead_zone() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x3570_D1A6_0001;
const NETWORK_NAME: &str = "bootstrap-no-dead-zone";
const NUM_NODES: usize = 100;
const NUM_GATEWAYS: usize = 3;
const RING_MAX_HTL: usize = 10;
const MIN_CONNECTIONS: usize = 4;
const MAX_CONNECTIONS: usize = 12;
const CHECKPOINT_MS: u64 = 120_000;
const REACHED_MIN_FRACTION: f64 = 0.90;
const BASE_EPOCH_MS: u64 = 1577836800000; const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; let epoch_ms = BASE_EPOCH_MS + (SEED % RANGE_MS);
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
NUM_GATEWAYS,
NUM_NODES,
RING_MAX_HTL,
7, MAX_CONNECTIONS,
MIN_CONNECTIONS,
SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
let contract = SimOperation::create_test_contract(0x35);
let contract_id = *contract.key().id();
let mut operations = vec![ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: vec![0x35; 64],
subscribe: true,
},
)];
for i in 0..NUM_NODES {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(600),
Duration::from_secs(120),
);
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let rt = create_runtime();
let (connect_rejected, reached_min_by_checkpoint, total_peers_seen) = rt.block_on(async {
let logs = logs_handle.lock().await;
let connect_rejected = logs
.iter()
.filter(|log| log.kind.is_connect_rejected())
.count();
let checkpoint_at_ms = epoch_ms.saturating_add(CHECKPOINT_MS);
let mut max_count_by_peer: HashMap<_, usize> = HashMap::new();
let mut peers_seen: HashSet<_> = HashSet::new();
for log in logs.iter() {
if let Some(count) = log.kind.connect_peer_connection_count() {
peers_seen.insert(log.peer_id.clone());
if log.tx.created_at_ms() <= checkpoint_at_ms {
let entry = max_count_by_peer.entry(log.peer_id.clone()).or_insert(0);
*entry = (*entry).max(count);
}
}
}
let reached_min = max_count_by_peer
.values()
.filter(|&&c| c >= MIN_CONNECTIONS)
.count();
(connect_rejected, reached_min, peers_seen.len())
});
tracing::info!(
"=== Bootstrap acceptance timeline (#4362) ===\n\
connect_rejected={} (diagnostic only — NOT asserted; see note above)\n\
nodes reaching min_connections({}) by T+{}s: {}/{} peers seen",
connect_rejected,
MIN_CONNECTIONS,
CHECKPOINT_MS / 1000,
reached_min_by_checkpoint,
total_peers_seen,
);
let required = (NUM_NODES as f64 * REACHED_MIN_FRACTION).ceil() as usize;
assert!(
reached_min_by_checkpoint >= required,
"only {}/{} nodes reached min_connections({}) by T+{}s (needed >= {}) — \
the bootstrap dead zone still parks late joiners below min (#4362)",
reached_min_by_checkpoint,
NUM_NODES,
MIN_CONNECTIONS,
CHECKPOINT_MS / 1000,
required,
);
}
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_get_reliability_with_latency() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
use freenet::simulation::FaultConfig;
const SEED: u64 = 0x3570_D1A6_0002;
const NETWORK_NAME: &str = "get-reliability-latency";
const NUM_NODES: usize = 100;
const NUM_GATEWAYS: usize = 3;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (mut sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
NUM_GATEWAYS,
NUM_NODES,
10, 7, 12, 4, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
sim.with_fault_injection(
FaultConfig::builder()
.latency_range(Duration::from_millis(50)..Duration::from_millis(200))
.message_loss_rate(0.05)
.build(),
);
let contract = SimOperation::create_test_contract(0x36);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let mut operations = vec![
ScheduledOperation::new(
NodeLabel::gateway(NETWORK_NAME, 0),
SimOperation::Put {
contract: contract.clone(),
state: vec![0x36; 64],
subscribe: true,
},
),
];
for i in 0..NUM_NODES {
operations.push(ScheduledOperation::new(
NodeLabel::node(NETWORK_NAME, i),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(900), Duration::from_secs(180), );
assert!(
result.turmoil_result.is_ok(),
"Simulation failed: {:?}",
result.turmoil_result.err()
);
let rt = create_runtime();
let summary = rt.block_on(async {
let logs = logs_handle.lock().await;
freenet::tracing::summarize_get_outcomes_per_tx(&logs)
});
let (successes, not_found, failures, timeouts) = (
summary.successes,
summary.not_found,
summary.failures,
summary.timeouts,
);
let total_outcomes = summary.total();
let sorted_latencies = &summary.success_elapsed_ms;
let p50 = sorted_latencies
.get(sorted_latencies.len() / 2)
.copied()
.unwrap_or(0);
let p90 = sorted_latencies
.get(sorted_latencies.len() * 9 / 10)
.copied()
.unwrap_or(0);
let p99 = sorted_latencies
.get(sorted_latencies.len() * 99 / 100)
.copied()
.unwrap_or(0);
let max_latency = sorted_latencies.last().copied().unwrap_or(0);
let success_rate = if total_outcomes > 0 {
successes as f64 / total_outcomes as f64
} else {
0.0
};
tracing::info!("=== GET Reliability with Latency (#3570) ===");
tracing::info!(
"Network: {} gateways + {} nodes, latency=50-200ms, loss=5%",
NUM_GATEWAYS,
NUM_NODES
);
tracing::info!(
"GET outcomes (per attempt tx): {} total — {} success ({} network-traversed), \
{} not_found, {} failures, {} timeouts",
total_outcomes,
successes,
summary.network_successes,
not_found,
failures,
timeouts
);
tracing::info!(
"GET success rate: {:.1}% ({}/{})",
success_rate * 100.0,
successes,
total_outcomes
);
tracing::info!(
"Latency (successful GETs): p50={}ms, p90={}ms, p99={}ms, max={}ms",
p50,
p90,
p99,
max_latency
);
let mut nodes_with_state = 0;
let mut nodes_without_state = Vec::new();
for i in 0..NUM_NODES {
let label = NodeLabel::node(NETWORK_NAME, i);
if let Some(storage) = result.node_storages.get(&label) {
if storage.get_stored_state(&contract_key).is_some() {
nodes_with_state += 1;
} else {
nodes_without_state.push(i);
}
} else {
nodes_without_state.push(i);
}
}
tracing::info!(
"Storage verification: {}/{} nodes have contract state",
nodes_with_state,
NUM_NODES
);
if !nodes_without_state.is_empty() {
tracing::info!(
"Nodes missing state ({} total): {:?}{}",
nodes_without_state.len(),
&nodes_without_state[..nodes_without_state.len().min(20)],
if nodes_without_state.len() > 20 {
"..."
} else {
""
}
);
}
tracing::info!(
"=== Comparison with no-latency baseline ===\n\
Baseline (no latency): 88.3% success, 72/100 nodes, p50=1ms, p90=754ms\n\
This run (50-200ms latency, 5% loss): {:.1}% success, {}/{} nodes, p50={}ms, p90={}ms",
success_rate * 100.0,
nodes_with_state,
NUM_NODES,
p50,
p90
);
assert!(
total_outcomes >= 10,
"Only {} GET outcome events — too few for meaningful analysis",
total_outcomes
);
let report = rt.block_on(async {
let logs = logs_handle.lock().await;
let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
verifier.verify()
});
tracing::info!(
"Anomaly report: {} anomalies across {} contracts, {} total events",
report.anomalies.len(),
report.contracts_analyzed,
report.total_events
);
tracing::info!(
"test_get_reliability_with_latency DONE: {:.1}% success rate, \
{}/{} nodes have state",
success_rate * 100.0,
nodes_with_state,
NUM_NODES
);
}
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_get_reliability_with_churn() {
use freenet::dev_tool::ChurnConfig;
use freenet::simulation::FaultConfig;
const SEED: u64 = 0x3570_D1A6_0003;
const NETWORK_NAME: &str = "get-reliability-churn";
const NUM_NODES: usize = 100;
const NUM_GATEWAYS: usize = 3;
GlobalTestMetrics::reset();
setup_deterministic_state(SEED);
let rt = create_runtime();
let (mut sim, logs_handle) = rt.block_on(async {
let sim = SimNetwork::new(
NETWORK_NAME,
NUM_GATEWAYS,
NUM_NODES,
10, 7, 12, 4, SEED,
)
.await;
let logs_handle = sim.event_logs_handle();
(sim, logs_handle)
});
sim.with_fault_injection(
FaultConfig::builder()
.latency_range(Duration::from_millis(50)..Duration::from_millis(200))
.message_loss_rate(0.05)
.build(),
);
sim.with_churn(ChurnConfig {
crash_probability: 0.10,
tick_interval: Duration::from_secs(5),
recovery_delay: Duration::from_secs(3),
max_simultaneous_crashes: Some(5),
permanent_crash_rate: 0.02, warmup_delay: Duration::from_secs(10),
});
drop(rt);
let direct_result = sim.run_simulation_direct::<rand::rngs::SmallRng>(
SEED,
15, 300, Duration::from_millis(500),
);
if let Err(e) = &direct_result {
tracing::warn!("Direct simulation completed with error (may be expected under churn): {e}");
}
let rt = create_runtime();
let summary = rt.block_on(async {
let logs = logs_handle.lock().await;
freenet::tracing::summarize_get_outcomes_per_tx(&logs)
});
let (successes, not_found, failures, timeouts) = (
summary.successes,
summary.not_found,
summary.failures,
summary.timeouts,
);
let total_outcomes = summary.total();
let sorted_latencies = &summary.success_elapsed_ms;
let p50 = sorted_latencies
.get(sorted_latencies.len() / 2)
.copied()
.unwrap_or(0);
let p90 = sorted_latencies
.get(sorted_latencies.len() * 9 / 10)
.copied()
.unwrap_or(0);
let p99 = sorted_latencies
.get(sorted_latencies.len() * 99 / 100)
.copied()
.unwrap_or(0);
let max_latency = sorted_latencies.last().copied().unwrap_or(0);
let success_rate = if total_outcomes > 0 {
successes as f64 / total_outcomes as f64
} else {
0.0
};
tracing::info!("=== GET Reliability with Churn (#3570) ===");
tracing::info!(
"Network: {} gateways + {} nodes, latency=50-200ms, loss=5%, churn=10%/5s",
NUM_GATEWAYS,
NUM_NODES
);
tracing::info!(
"GET outcomes (per attempt tx): {} total — {} success ({} network-traversed), \
{} not_found, {} failures, {} timeouts",
total_outcomes,
successes,
summary.network_successes,
not_found,
failures,
timeouts
);
tracing::info!(
"GET success rate: {:.1}% ({}/{})",
success_rate * 100.0,
successes,
total_outcomes
);
tracing::info!(
"Latency (successful GETs): p50={}ms, p90={}ms, p99={}ms, max={}ms",
p50,
p90,
p99,
max_latency
);
tracing::info!(
"=== Comparison ===\n\
Baseline (no latency): 88.3% success, p90=754ms\n\
With latency (50-200ms, 5%): 81.9% success, p90=1833ms\n\
With churn + latency: {:.1}% success, p90={}ms",
success_rate * 100.0,
p90
);
if total_outcomes >= 10 {
tracing::info!(
"test_get_reliability_with_churn DONE: {:.1}% GET success rate \
({} outcomes from 300 random operations under churn)",
success_rate * 100.0,
total_outcomes
);
} else {
tracing::warn!(
"test_get_reliability_with_churn: only {} GET outcomes — \
random event generation may not have produced enough GETs",
total_outcomes
);
}
}
#[cfg(feature = "nightly_tests")]
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_nightly_50_node_topology_formation() {
use freenet::dev_tool::NodeLabel;
const SEED: u64 = 0x3511_5000_0001;
const NETWORK_NAME: &str = "nightly-50-topology";
const GATEWAYS: usize = 4;
const NODES: usize = 50;
const RING_MAX_HTL: usize = 10;
const RND_IF_HTL_ABOVE: usize = 5;
const MAX_CONN: usize = 20;
const MIN_CONN: usize = 10;
const VIRTUAL_DURATION: Duration = Duration::from_secs(3600);
tracing::info!("=== Nightly: 50-Node Topology Formation ===");
setup_deterministic_state(SEED);
let mut sim = SimNetwork::new(
NETWORK_NAME,
GATEWAYS,
NODES,
RING_MAX_HTL,
RND_IF_HTL_ABOVE,
MAX_CONN,
MIN_CONN,
SEED,
)
.await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
.await;
tracing::info!("Running 1 virtual hour of topology formation...");
let_network_run(&mut sim, VIRTUAL_DURATION).await;
let mut node_counts: Vec<usize> = (0..NODES)
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
node_counts.sort_unstable();
let num_sampled = node_counts.len();
assert!(num_sampled > 0, "No connection managers available");
let median_conn = node_counts[num_sampled / 2];
let nodes_above_min = node_counts.iter().filter(|&&c| c >= MIN_CONN).count();
let fraction_above_min = nodes_above_min as f64 / num_sampled as f64;
tracing::info!("Connection counts: {:?}", node_counts);
tracing::info!(
"Median={}, nodes at min_connections={}/{} ({:.0}%)",
median_conn,
nodes_above_min,
num_sampled,
fraction_above_min * 100.0
);
let connectivity = sim.node_connectivity();
let mut nodes_with_peer_connections = 0usize;
for (label, (_key, conns)) in &connectivity {
if !label.is_gateway() && conns.keys().any(|peer| !peer.is_gateway()) {
nodes_with_peer_connections += 1;
}
}
let peer_conn_fraction = nodes_with_peer_connections as f64 / NODES as f64;
tracing::info!(
"Nodes with non-gateway peer connections: {}/{} ({:.0}%)",
nodes_with_peer_connections,
NODES,
peer_conn_fraction * 100.0
);
assert!(
median_conn >= MIN_CONN,
"Topology formation stall: median={} < min_connections={}. \
Counts: {:?}. Seed: 0x{:X}",
median_conn,
MIN_CONN,
node_counts,
SEED
);
assert!(
fraction_above_min >= 0.90,
"Only {:.0}% of nodes reached min_connections (expected >= 90%). \
{}/{} nodes. Counts: {:?}. Seed: 0x{:X}",
fraction_above_min * 100.0,
nodes_above_min,
num_sampled,
node_counts,
SEED
);
assert!(
peer_conn_fraction >= 0.80,
"Only {:.0}% of nodes have non-gateway peer connections (expected >= 80%). \
CONNECT forwarding is insufficient. Seed: 0x{:X}",
peer_conn_fraction * 100.0,
SEED
);
tracing::info!(
"PASSED: median={}, above_min={:.0}%, peer_conns={:.0}%",
median_conn,
fraction_above_min * 100.0,
peer_conn_fraction * 100.0
);
}
#[cfg(feature = "nightly_tests")]
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_nightly_connection_growth_checkpoints() {
use freenet::dev_tool::NodeLabel;
const SEED: u64 = 0x3511_6C8E_0002;
const NETWORK_NAME: &str = "nightly-growth-checkpoints";
const GATEWAYS: usize = 4;
const NODES: usize = 50;
const RING_MAX_HTL: usize = 10;
const RND_IF_HTL_ABOVE: usize = 5;
const MAX_CONN: usize = 20;
const MIN_CONN: usize = 10;
const CHECKPOINTS_SECS: [u64; 4] = [300, 900, 1800, 3600];
tracing::info!("=== Nightly: Connection Growth Checkpoints ===");
setup_deterministic_state(SEED);
let mut sim = SimNetwork::new(
NETWORK_NAME,
GATEWAYS,
NODES,
RING_MAX_HTL,
RND_IF_HTL_ABOVE,
MAX_CONN,
MIN_CONN,
SEED,
)
.await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
.await;
let mut checkpoint_medians: Vec<(u64, usize)> = Vec::new();
let mut elapsed_so_far = 0u64;
for &target_secs in &CHECKPOINTS_SECS {
let delta = target_secs - elapsed_so_far;
let_network_run(&mut sim, Duration::from_secs(delta)).await;
elapsed_so_far = target_secs;
let mut counts: Vec<usize> = (0..NODES)
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
counts.sort_unstable();
let median = if counts.is_empty() {
0
} else {
counts[counts.len() / 2]
};
tracing::info!(
"Checkpoint @{}m: median={}, counts={:?}",
target_secs / 60,
median,
counts
);
assert!(
median > 0 || elapsed_so_far <= 300,
"Network collapse at checkpoint @{}m: median=0. Counts: {:?}. Seed: 0x{:X}",
target_secs / 60,
counts,
SEED
);
checkpoint_medians.push((target_secs, median));
}
for window in checkpoint_medians.windows(2) {
let (prev_t, prev_median) = window[0];
let (curr_t, curr_median) = window[1];
assert!(
curr_median + 1 >= prev_median,
"Connection growth regressed > 1 between @{}m (median={}) and @{}m (median={}). \
Growth must be near-monotonic. Seed: 0x{:X}",
prev_t / 60,
prev_median,
curr_t / 60,
curr_median,
SEED
);
}
let (_, final_median) = checkpoint_medians.last().unwrap();
assert!(
*final_median >= MIN_CONN,
"Final median={} < min_connections={} after 60 virtual minutes. \
Checkpoints: {:?}. Seed: 0x{:X}",
final_median,
MIN_CONN,
checkpoint_medians,
SEED
);
tracing::info!("PASSED: checkpoints={:?}", checkpoint_medians);
}
#[cfg(feature = "nightly_tests")]
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_nightly_fault_recovery_speed() {
use freenet::dev_tool::NodeLabel;
use freenet::simulation::FaultConfig;
const SEED: u64 = 0x3511_FA17_0003;
const NETWORK_NAME: &str = "nightly-fault-recovery";
const GATEWAYS: usize = 4;
const NODES: usize = 50;
const RING_MAX_HTL: usize = 10;
const RND_IF_HTL_ABOVE: usize = 5;
const MAX_CONN: usize = 20;
const MIN_CONN: usize = 10;
tracing::info!("=== Nightly: Fault Recovery Speed ===");
setup_deterministic_state(SEED);
let mut sim = SimNetwork::new(
NETWORK_NAME,
GATEWAYS,
NODES,
RING_MAX_HTL,
RND_IF_HTL_ABOVE,
MAX_CONN,
MIN_CONN,
SEED,
)
.await;
sim.with_start_backoff(Duration::from_millis(50));
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
.await;
tracing::info!("Phase 1: Convergence — 30 virtual minutes, no faults");
let_network_run(&mut sim, Duration::from_secs(1800)).await;
let node_ids = || GATEWAYS..GATEWAYS + NODES;
let mut pre_fault_counts: Vec<usize> = node_ids()
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
pre_fault_counts.sort_unstable();
assert!(
!pre_fault_counts.is_empty(),
"Phase 1: no connection managers available. Seed: 0x{:X}",
SEED
);
let pre_fault_median = pre_fault_counts[pre_fault_counts.len() / 2];
tracing::info!(
"Phase 1 done: median={}, counts={:?}",
pre_fault_median,
pre_fault_counts
);
assert!(
pre_fault_median >= MIN_CONN,
"Network did not converge before fault injection: median={} < min_connections={}. \
Counts: {:?}. Seed: 0x{:X}",
pre_fault_median,
MIN_CONN,
pre_fault_counts,
SEED
);
tracing::info!("Phase 2: Fault injection — 20% message loss for 5 virtual minutes");
sim.with_fault_injection(FaultConfig::builder().message_loss_rate(0.20).build());
let_network_run(&mut sim, Duration::from_secs(300)).await;
let mut during_fault_counts: Vec<usize> = node_ids()
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
during_fault_counts.sort_unstable();
assert_eq!(
during_fault_counts.len(),
NODES,
"Phase 2: expected connection managers for all {} nodes, got {}. Seed: 0x{:X}",
NODES,
during_fault_counts.len(),
SEED
);
let during_fault_median = during_fault_counts[during_fault_counts.len() / 2];
tracing::info!(
"Phase 2 done: median={}, counts={:?}",
during_fault_median,
during_fault_counts
);
let during_fault_floor = MIN_CONN / 2;
assert!(
during_fault_median >= during_fault_floor,
"Connection collapse under load: during_fault_median={} < MIN_CONN/2={}. \
Counts: {:?}. Seed: 0x{:X}",
during_fault_median,
during_fault_floor,
during_fault_counts,
SEED
);
tracing::info!("Phase 3: Recovery — faults cleared, 25 virtual minutes");
sim.clear_fault_injection();
let_network_run(&mut sim, Duration::from_secs(1500)).await;
let mut post_recovery_counts: Vec<usize> = node_ids()
.filter_map(|i| {
let label = NodeLabel::node(NETWORK_NAME, i);
sim.connection_count(&label)
})
.collect();
post_recovery_counts.sort_unstable();
assert_eq!(
post_recovery_counts.len(),
NODES,
"Phase 3: expected connection managers for all {} nodes, got {}. Seed: 0x{:X}",
NODES,
post_recovery_counts.len(),
SEED
);
let post_recovery_median = post_recovery_counts[post_recovery_counts.len() / 2];
let isolated_count = post_recovery_counts.iter().filter(|&&c| c == 0).count();
let fraction_isolated = isolated_count as f64 / NODES as f64;
tracing::info!(
"Phase 3 done: median={}, isolated={}/{} ({:.0}%), counts={:?}",
post_recovery_median,
isolated_count,
NODES,
fraction_isolated * 100.0,
post_recovery_counts
);
let recovery_floor = pre_fault_median.saturating_sub(1).max(MIN_CONN);
assert!(
post_recovery_median >= recovery_floor,
"Incomplete recovery: post_recovery_median={} < floor={} \
(max(pre_fault_median - 1, MIN_CONN)). \
Pre-fault: {:?}, Post-recovery: {:?}. Seed: 0x{:X}",
post_recovery_median,
recovery_floor,
pre_fault_counts,
post_recovery_counts,
SEED
);
assert!(
fraction_isolated < 0.05,
"{:.0}% of nodes isolated after recovery (threshold: 5%). \
Counts: {:?}. Seed: 0x{:X}",
fraction_isolated * 100.0,
post_recovery_counts,
SEED
);
tracing::info!(
"PASSED: pre_fault_median={}, during_fault_median={}, post_recovery_median={}, \
isolated={:.0}%",
pre_fault_median,
during_fault_median,
post_recovery_median,
fraction_isolated * 100.0
);
}
#[test]
fn node_label_node_and_gateway_formats_are_stable() {
assert_eq!(
NodeLabel::node("net", 5).to_string(),
"net-node-5",
"NodeLabel::node string format must remain stable"
);
assert_eq!(
NodeLabel::gateway("net", 2).to_string(),
"net-gateway-2",
"NodeLabel::gateway string format must remain stable"
);
assert_ne!(
NodeLabel::node("net", 0),
NodeLabel::gateway("net", 0),
"gateway and regular-node labels must be distinct at the same numeric ID"
);
assert_ne!(NodeLabel::node("net", 5), NodeLabel::gateway("net", 5));
}
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn sim_network_regular_node_labels_start_at_gateway_count() {
const NETWORK_NAME: &str = "label-indexing-regression";
const GATEWAYS: usize = 2;
const NODES: usize = 3;
const SEED: u64 = 0xABCD_1234;
setup_deterministic_state(SEED);
let mut sim = SimNetwork::new(
NETWORK_NAME,
GATEWAYS,
NODES,
3,
2,
4,
2,
SEED,
)
.await;
let _handles = sim
.start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
.await;
for i in 0..GATEWAYS {
assert!(
sim.connection_count(&NodeLabel::node(NETWORK_NAME, i))
.is_none(),
"node-{i} must NOT be a live regular-node label (inside gateway range)"
);
}
for i in GATEWAYS..GATEWAYS + NODES {
assert!(
sim.connection_count(&NodeLabel::node(NETWORK_NAME, i))
.is_some(),
"node-{i} must be a live regular-node label"
);
}
assert!(
sim.connection_count(&NodeLabel::node(NETWORK_NAME, GATEWAYS + NODES))
.is_none(),
"node-{} must NOT be a live regular-node label (past end of range)",
GATEWAYS + NODES
);
}
#[test_log::test]
#[ignore = "tracking issue #4250 — default event_chain workload doesn't produce terminal GETs at CI scales; primary regression coverage lives in test_get_msg_response_hop_count_roundtrip (get.rs) and classify_response_found_preserves_hop_count (op_ctx_task.rs). Un-ignore once a deterministic GET-producing workload is wired through TestConfig"]
fn test_hop_count_populated_on_terminal_get_events() {
let result = TestConfig::small("hop-count-regression", 0xCAFE_0001)
.with_nodes(4)
.with_max_contracts(5)
.with_iterations(50)
.with_duration(Duration::from_secs(60))
.with_sleep(Duration::from_secs(2))
.run()
.assert_ok();
const RING_MAX_HTL: usize = 7;
let rt = create_runtime();
let (populated, max_hop) = rt.block_on(async {
let logs = result.logs_handle.lock().await;
let hop_counts: Vec<usize> = logs
.iter()
.filter(|m| m.kind.variant_name() == "Get")
.filter_map(|m| m.kind.hop_count())
.collect();
let max_hop = hop_counts.iter().copied().max().unwrap_or(0);
(hop_counts.len(), max_hop)
});
assert!(
populated > 0,
"expected at least one terminal GET event with populated hop_count; \
got 0. Indicates hop_count is no longer being threaded through the \
GetMsg::Response wire format (PR #4245)."
);
assert!(
max_hop <= RING_MAX_HTL,
"max observed hop_count {} exceeds ring_max_htl {}; suggests garbage \
value propagation rather than (max_htl - htl) accounting",
max_hop,
RING_MAX_HTL
);
}
#[test_log::test]
fn test_contract_migrates_to_close_cluster_resolving_get_dead_end() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0xDEAD_F00D_0001;
const NETWORK_NAME: &str = "get-placement-deadend";
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0xBE);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let wrap = |x: f64| x.rem_euclid(1.0);
let cluster: Vec<f64> = [-0.010, -0.006, -0.003, 0.003, 0.006, 0.010]
.iter()
.map(|o| wrap(key_loc + o))
.collect();
let host_loc = wrap(key_loc + 0.40); let requester_loc = wrap(key_loc + 0.70); let mut node_locations = cluster.clone();
node_locations.push(host_loc); node_locations.push(requester_loc); let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1, num_nodes, 10, 7, 8, 3, SEED,
&node_locations,
)
.await
});
sim.enable_placement_migration();
let host_label = NodeLabel::node(NETWORK_NAME, 7); let requester_label = NodeLabel::node(NETWORK_NAME, 8);
let locs = sim.get_peer_locations();
let ring_dist = |a: f64, b: f64| {
let d = (a - b).abs();
d.min(1.0 - d)
};
let host_dist = ring_dist(locs[7], key_loc);
let cluster_min = (1..=6)
.map(|i| ring_dist(locs[i], key_loc))
.fold(f64::INFINITY, f64::min);
assert!(
cluster_min < host_dist,
"scenario setup wrong: a cluster node must be closer to the key than the host \
(cluster_min={cluster_min}, host_dist={host_dist})"
);
let operations = vec![
ScheduledOperation::new(
host_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![10, 20, 30, 40],
},
),
ScheduledOperation::new(
requester_label.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(60),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed: {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&host_label, &contract_key),
"host should still host the seeded contract after the simulation"
);
let migrated: Vec<usize> = (1..=6usize)
.filter(|n| result.is_node_hosting(&NodeLabel::node(NETWORK_NAME, *n), &contract_key))
.collect();
let requester_has_state = result
.node_storages
.get(&requester_label)
.is_some_and(|s| s.get_stored_state(&contract_key).is_some());
assert!(
!migrated.is_empty(),
"placement migration FAILED: no close-cluster peer (node_no 1..=6) hosts the \
contract after the simulation. The contract never migrated from the far host \
toward the key, so a key-routed GET would still dead-end. \
host_hosting={}, requester_has_state={requester_has_state}",
result.is_node_hosting(&host_label, &contract_key),
);
assert!(
requester_has_state,
"requester GET should now succeed: with the contract migrated onto the close \
cluster, the greedy GET toward the key lands on a host instead of dead-ending \
(migrated cluster nodes: {migrated:?})"
);
tracing::info!(
migrated_cluster_nodes = ?migrated,
requester_has_state,
"placement migration converged onto the close cluster"
);
}
#[test_log::test]
fn test_serve_during_demandless_copy_served_locally_never_dark() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0x5E27_D021_0001;
const NETWORK_NAME: &str = "serve-during-never-dark";
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0x5D);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new(
NETWORK_NAME,
1, 3, 7, 3, 5, 2, SEED,
)
.await
});
let holder = NodeLabel::node(NETWORK_NAME, 1);
let operations = vec![
ScheduledOperation::new(
holder.clone(),
SimOperation::SeedDemandlessCopy {
contract: contract.clone(),
state: vec![7, 7, 7, 7],
},
),
ScheduledOperation::new(
holder.clone(),
SimOperation::Get {
contract_id,
return_contract_code: false,
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed: {:?}",
result.turmoil_result.err()
);
let connections = result.node_open_connections(&holder);
assert!(
connections >= 1,
"node-1 must be connected for the serve to be attributable to \
serve-DURING interest (open_connections={connections})"
);
assert!(
result.is_node_hosting(&holder, &contract_key),
"node-1 must hold the seeded demandless copy"
);
assert!(
!result.node_is_receiving_updates(&holder, &contract_key),
"the seeded copy must be DEMANDLESS (not subscribed) — otherwise the serve \
would be attributable to the subscription term, not serve-DURING"
);
let serves = result.node_local_get_serves(&holder);
let forwards = result.node_local_get_forwards(&holder);
assert_eq!(
forwards, 0,
"serve-DURING regression: a demandless-copy GET was ROUTED to the network \
(local_get_forwards={forwards}, local_get_serves={serves}) — the node went \
dark on a read it could answer locally"
);
assert!(
serves >= 1,
"serve-DURING: the demandless-copy GET must be answered from the local copy \
(local_get_serves={serves}, local_get_forwards={forwards})"
);
tracing::info!(
connections,
serves,
forwards,
"serve-DURING: demandless-copy GET served locally (never dark)"
);
}
#[test_log::test]
fn test_get_dead_ends_at_close_cluster_without_migration() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0xDEAD_F00D_0001;
const NETWORK_NAME: &str = "get-placement-deadend-control";
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0xBE);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let wrap = |x: f64| x.rem_euclid(1.0);
let cluster: Vec<f64> = [-0.010, -0.006, -0.003, 0.003, 0.006, 0.010]
.iter()
.map(|o| wrap(key_loc + o))
.collect();
let host_loc = wrap(key_loc + 0.40);
let requester_loc = wrap(key_loc + 0.70);
let mut node_locations = cluster.clone();
node_locations.push(host_loc);
node_locations.push(requester_loc);
let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1,
num_nodes,
10,
7,
8,
3,
SEED,
&node_locations,
)
.await
});
sim.disable_placement_migration();
let host_label = NodeLabel::node(NETWORK_NAME, 7);
let requester_label = NodeLabel::node(NETWORK_NAME, 8);
let operations = vec![
ScheduledOperation::new(
host_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![10, 20, 30, 40],
},
),
ScheduledOperation::new(
requester_label.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(60),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed: {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&host_label, &contract_key),
"host should hold the seeded contract"
);
let migrated: Vec<usize> = (1..=6usize)
.filter(|n| result.is_node_hosting(&NodeLabel::node(NETWORK_NAME, *n), &contract_key))
.collect();
assert!(
migrated.is_empty(),
"without migration, no close-cluster peer should host the contract, but these do: \
{migrated:?} (cascade leaked into a migration-disabled sim?)"
);
let requester_has_state = result
.node_storages
.get(&requester_label)
.is_some_and(|s| s.get_stored_state(&contract_key).is_some());
assert!(
!requester_has_state,
"without migration the requester GET must dead-end at the close non-hosting cluster \
and obtain NO state (the far host is off the greedy path toward the key)"
);
}
#[test_log::test]
fn test_terminal_advertisement_consult_closes_get_dead_end() {
use freenet::config::GlobalTestMetrics;
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0xC0FF_EEC0_0004;
const NETWORK_NAME: &str = "terminal-consult-deadend";
setup_deterministic_state(SEED);
GlobalTestMetrics::reset();
let contract = SimOperation::create_test_contract(0xC0);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let wrap = |x: f64| x.rem_euclid(1.0);
let cluster: Vec<f64> = [-0.010, -0.006, -0.003, 0.003, 0.006, 0.010]
.iter()
.map(|o| wrap(key_loc + o))
.collect();
let host_loc = wrap(key_loc + 0.035);
let requester_loc = wrap(key_loc - 0.60);
let mut node_locations = cluster.clone();
node_locations.push(host_loc); node_locations.push(requester_loc); let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1, num_nodes, 10, 7, 8, 3, SEED,
&node_locations,
)
.await
});
sim.disable_placement_migration();
sim.enable_seeded_host_advertisements();
let host_label = NodeLabel::node(NETWORK_NAME, 7);
let requester_label = NodeLabel::node(NETWORK_NAME, 8);
let locs = sim.get_peer_locations();
let ring_dist = |a: f64, b: f64| {
let d = (a - b).abs();
d.min(1.0 - d)
};
let host_dist = ring_dist(locs[7], key_loc);
let cluster_min = (1..=6)
.map(|i| ring_dist(locs[i], key_loc))
.fold(f64::INFINITY, f64::min);
assert!(
cluster_min < host_dist,
"scenario setup wrong: a cluster peer must be closer to the key than the host \
(cluster_min={cluster_min}, host_dist={host_dist})"
);
let operations = vec![
ScheduledOperation::new(
host_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![11, 22, 33, 44],
},
),
ScheduledOperation::new(
requester_label.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(60),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed: {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&host_label, &contract_key),
"host should still host the seeded contract after the simulation"
);
let attempts = GlobalTestMetrics::terminal_consult_attempts();
let hits = GlobalTestMetrics::terminal_consult_hits();
let resolved_found = GlobalTestMetrics::terminal_consult_resolved_found();
let cached_on_cluster: Vec<usize> = (1..=6usize)
.filter(|n| result.is_node_hosting(&NodeLabel::node(NETWORK_NAME, *n), &contract_key))
.collect();
tracing::info!(
attempts,
hits,
resolved_found,
still_not_found = GlobalTestMetrics::terminal_consult_still_not_found(),
?cached_on_cluster,
"terminal consult telemetry"
);
let requester_has_state = result
.node_storages
.get(&requester_label)
.is_some_and(|s| s.get_stored_state(&contract_key).is_some());
assert!(
resolved_found > 0,
"terminal consult should have resolved the GET to Found via an advertised \
off-path host (attempts={attempts}, hits={hits}, resolved_found={resolved_found}, \
requester_has_state={requester_has_state})"
);
assert!(
requester_has_state,
"requester GET should succeed via the terminal advertisement consult \
(resolved_found={resolved_found})"
);
}
#[test_log::test]
fn test_terminal_advertisement_consult_closes_subscribe_dead_end() {
use freenet::config::GlobalTestMetrics;
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0xC0FF_EEC0_0008;
const NETWORK_NAME: &str = "terminal-consult-subscribe-deadend";
setup_deterministic_state(SEED);
GlobalTestMetrics::reset();
let contract = SimOperation::create_test_contract(0xC0);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let wrap = |x: f64| x.rem_euclid(1.0);
let cluster: Vec<f64> = [-0.010, -0.006, -0.003, 0.003, 0.006, 0.010]
.iter()
.map(|o| wrap(key_loc + o))
.collect();
let host_loc = wrap(key_loc + 0.05);
let requester_loc = wrap(key_loc - 0.60);
let mut node_locations = cluster.clone();
node_locations.push(host_loc); node_locations.push(requester_loc); let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1,
num_nodes,
10,
7,
8,
3,
SEED,
&node_locations,
)
.await
});
sim.disable_placement_migration();
sim.enable_seeded_host_advertisements();
let host_label = NodeLabel::node(NETWORK_NAME, 7);
let requester_label = NodeLabel::node(NETWORK_NAME, 8);
let operations = vec![
ScheduledOperation::new(
host_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![11, 22, 33, 44],
},
),
ScheduledOperation::new(
requester_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![11, 22, 33, 44],
},
),
ScheduledOperation::new(
requester_label.clone(),
SimOperation::Subscribe { contract_id },
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(60),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed: {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&host_label, &contract_key),
"host should still host the seeded contract after the simulation"
);
let attempts = GlobalTestMetrics::terminal_consult_attempts();
let hits = GlobalTestMetrics::terminal_consult_hits();
let resolved_found = GlobalTestMetrics::terminal_consult_resolved_found();
tracing::info!(
attempts,
hits,
resolved_found,
still_not_found = GlobalTestMetrics::terminal_consult_still_not_found(),
"terminal consult telemetry (subscribe)"
);
assert!(
resolved_found > 0,
"terminal consult should have resolved the SUBSCRIBE to Subscribed via an \
advertised off-path host (attempts={attempts}, hits={hits}, \
resolved_found={resolved_found})"
);
}
#[test_log::test]
fn test_subscription_root_renewal_does_not_storm() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
for seed in [0x4440_0001u64, 0x4440_0002, 0x4440_0003] {
let network_name = format!("renewal-storm-{seed:x}");
let network_name: &'static str = Box::leak(network_name.into_boxed_str());
setup_deterministic_state(seed);
let contract = SimOperation::create_test_contract(0x44);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let wrap = |x: f64| x.rem_euclid(1.0);
let node_locations = vec![
key_loc, wrap(key_loc + 0.30),
wrap(key_loc + 0.45),
wrap(key_loc + 0.60),
];
let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
network_name,
1, num_nodes, 10, 7, 8, 3, seed,
&node_locations,
)
.await
});
sim.enable_placement_migration();
let root_label = NodeLabel::node(network_name, 1);
let root_addr = sim
.node_address(&root_label)
.unwrap_or_else(|| panic!("root node {root_label:?} has no address (seed {seed:x})"));
let locs = sim.get_peer_locations(); let ring_dist = |a: f64, b: f64| {
let d = (a - b).abs();
d.min(1.0 - d)
};
let root_dist = ring_dist(locs[1], key_loc);
let others_min = (0..locs.len())
.filter(|&i| i != 1)
.map(|i| ring_dist(locs[i], key_loc))
.fold(f64::INFINITY, f64::min);
assert!(
root_dist < 1e-3,
"scenario setup wrong (seed {seed:x}): the root is placed AT the key, \
so its distance must be ~0 (root_dist={root_dist})"
);
assert!(
root_dist < others_min,
"scenario setup wrong (seed {seed:x}): the root must be the closest peer to the key \
including the gateway (root_dist={root_dist}, others_min={others_min})"
);
let non_root_label = NodeLabel::node(network_name, 2);
let non_root_addr = sim.node_address(&non_root_label).unwrap_or_else(|| {
panic!("non-root node {non_root_label:?} has no address (seed {seed:x})")
});
let non_root_loc = locs[2];
let (contract2, contract2_id) = {
let mut chosen = None;
for byte in 0u8..=255 {
let c = SimOperation::create_test_contract(byte);
let cloc = Location::from(&c.key()).as_f64();
let node2_d = ring_dist(non_root_loc, cloc);
let someone_closer = (0..locs.len())
.filter(|&i| i != 2)
.any(|i| ring_dist(locs[i], cloc) < node2_d);
if someone_closer {
chosen = Some((c.clone(), *c.key().id()));
break;
}
}
chosen.unwrap_or_else(|| {
panic!("seed {seed:x}: could not find a contract node 2 is NOT the root for")
})
};
let operations = vec![
ScheduledOperation::new(
root_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![1, 2, 3, 4],
},
),
ScheduledOperation::new(root_label.clone(), SimOperation::Subscribe { contract_id }),
ScheduledOperation::new(
non_root_label.clone(),
SimOperation::SeedHostedContract {
contract: contract2.clone(),
state: vec![5, 6, 7, 8],
},
),
ScheduledOperation::new(
non_root_label.clone(),
SimOperation::Subscribe {
contract_id: contract2_id,
},
),
];
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(900),
Duration::from_secs(540),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed (seed {seed:x}): {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&root_label, &contract_key),
"root should still host the seeded contract after the simulation (seed {seed:x})"
);
let root_metrics = result.renewal_metrics_for(&root_addr);
let non_root_metrics = result.renewal_metrics_for(&non_root_addr);
let totals = result.aggregate_renewal_metrics();
tracing::info!(
seed = format!("{seed:x}"),
root_wire_attempts = root_metrics.wire_attempts,
root_terminus_satisfied = root_metrics.terminus_satisfied,
non_root_wire_attempts = non_root_metrics.wire_attempts,
non_root_terminus_satisfied = non_root_metrics.terminus_satisfied,
total_wire_attempts = totals.wire_attempts,
total_terminus_satisfied = totals.terminus_satisfied,
"renewal storm metrics"
);
assert!(
root_metrics.wire_attempts + root_metrics.terminus_satisfied > 0,
"renewal driver never ran on the body-holding root (seed {seed:x}); \
test exercised neither the storm nor the fix \
(wire_attempts={}, terminus_satisfied={})",
root_metrics.wire_attempts,
root_metrics.terminus_satisfied,
);
assert!(
root_metrics.terminus_satisfied > 0,
"body-holding root never took the root-satisfied renewal path (seed {seed:x}): \
terminus_satisfied=0, wire_attempts={}. Before the #4440 fix the root storms \
with doomed upstream renewals instead of satisfying them locally.",
root_metrics.wire_attempts,
);
assert_eq!(
root_metrics.wire_attempts, 0,
"body-holding root must NOT send wire renewal requests — that is the storm \
(seed {seed:x}): wire_attempts={}, terminus_satisfied={}. Each wire renewal \
from a root routes greedily toward the key, dead-ends at the root, fails, and \
retries next cycle.",
root_metrics.wire_attempts, root_metrics.terminus_satisfied,
);
assert!(
root_metrics.terminus_satisfied <= 4,
"root took the root-satisfied path {} times (seed {seed:x}) — far more than the \
~6-minute lease-expiry cadence allows over this run. The local-lease refresh \
is supposed to keep an in-use root OFF the every-30s-cycle renewal selection; \
a count this high means it is being re-selected every cycle (the batch-starvation \
regression).",
root_metrics.terminus_satisfied,
);
assert!(
non_root_metrics.wire_attempts > 0,
"non-root host made no wire renewal attempts (seed {seed:x}): wire_attempts=0, \
terminus_satisfied={}. A non-root host MUST keep renewing upstream; zero here \
means the root short-circuit is firing on non-roots too (silent global renewal \
stoppage).",
non_root_metrics.terminus_satisfied,
);
assert_eq!(
non_root_metrics.terminus_satisfied, 0,
"non-root host wrongly took the root-satisfied path (seed {seed:x}): \
terminus_satisfied={}, wire_attempts={}. Only a body-holding root may \
short-circuit; a non-root host must wire-renew.",
non_root_metrics.terminus_satisfied, non_root_metrics.wire_attempts,
);
}
}
#[test_log::test]
fn test_no_interest_subscription_root_lease_lapses() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0x4440_0009;
let network_name = "renewal-root-lease-lapse";
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0x4A);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let wrap = |x: f64| x.rem_euclid(1.0);
let node_locations = vec![
key_loc,
wrap(key_loc + 0.30),
wrap(key_loc + 0.45),
wrap(key_loc + 0.60),
];
let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
network_name,
1,
num_nodes,
10,
7,
8,
3,
SEED,
&node_locations,
)
.await
});
sim.enable_placement_migration();
let root_label = NodeLabel::node(network_name, 1);
let root_addr = sim
.node_address(&root_label)
.expect("root node has an address");
let operations = vec![ScheduledOperation::new(
root_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![1, 2, 3, 4],
},
)];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(900),
Duration::from_secs(600),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed: {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&root_label, &contract_key),
"root should still host the contract (hosting does not depend on the lease)"
);
let root_metrics = result.renewal_metrics_for(&root_addr);
assert_eq!(
root_metrics.wire_attempts, 0,
"body-holding root must not wire-renew (wire_attempts={}, terminus_satisfied={})",
root_metrics.wire_attempts, root_metrics.terminus_satisfied,
);
let root_snapshot = result
.topology_snapshots
.iter()
.find(|s| s.peer_addr == root_addr)
.expect("root topology snapshot present");
assert!(
!root_snapshot
.active_subscription_keys
.contains(&contract_id),
"no-interest body-holding root self-perpetuated its lease: the contract is still in \
active_subscription_keys after the 8-minute lease window. The root-satisfied path \
must NOT refresh the lease when the contract is not in use (#4440 blocker 2) — \
otherwise it is an unbounded is_subscribed-only retention exemption."
);
tracing::info!(
root_terminus_satisfied = root_metrics.terminus_satisfied,
active_subs = root_snapshot.active_subscription_keys.len(),
"no-interest root lease lapsed as expected"
);
}
#[test_log::test]
fn test_placement_migration_at_scale_renewal_load_stays_bounded() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const MAX_RECOVERY_ATTEMPTS_PER_INTERVAL: u64 = 10;
const NUM_CONTRACTS: usize = 20;
for seed in [0x4601_0001u64, 0x4601_0002, 0x4601_0003] {
let network_name = format!("migration-scale-{seed:x}");
let network_name: &'static str = Box::leak(network_name.into_boxed_str());
setup_deterministic_state(seed);
let loaded_loc = 0.5;
let node_locations = vec![
loaded_loc, 0.05, 0.15, 0.30, 0.42, 0.62, 0.78, 0.92,
];
let num_nodes = node_locations.len();
let rt = create_runtime();
let mut sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
network_name,
1, num_nodes, 10, 7, 16, 3, seed,
&node_locations,
)
.await
});
sim.enable_placement_migration();
let loaded_label = NodeLabel::node(network_name, 1);
let loaded_addr = sim
.node_address(&loaded_label)
.unwrap_or_else(|| panic!("loaded node has no address (seed {seed:x})"));
let contracts: Vec<_> = (0..NUM_CONTRACTS)
.map(|i| SimOperation::create_test_contract(i as u8))
.collect();
let contract_keys: Vec<_> = contracts.iter().map(|c| c.key()).collect();
let mut operations = Vec::with_capacity(NUM_CONTRACTS * 2);
for contract in &contracts {
operations.push(ScheduledOperation::new(
loaded_label.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: vec![1, 2, 3, 4],
},
));
}
for contract in &contracts {
operations.push(ScheduledOperation::new(
loaded_label.clone(),
SimOperation::Subscribe {
contract_id: *contract.key().id(),
},
));
}
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(900),
Duration::from_secs(540),
);
assert!(
result.turmoil_result.is_ok(),
"simulation failed (seed {seed:x}): {:?}",
result.turmoil_result.err()
);
let loaded_metrics = result.renewal_metrics_for(&loaded_addr);
let totals = result.aggregate_renewal_metrics();
tracing::info!(
seed = format!("{seed:x}"),
loaded_max_cycle_batch = loaded_metrics.max_cycle_batch,
loaded_wire_attempts = loaded_metrics.wire_attempts,
loaded_terminus = loaded_metrics.terminus_satisfied,
agg_max_cycle_batch = totals.max_cycle_batch,
agg_wire_attempts = totals.wire_attempts,
"migration-at-scale renewal load"
);
for (addr, m) in &result.renewal_metrics {
assert!(
m.max_cycle_batch <= MAX_RECOVERY_ATTEMPTS_PER_INTERVAL,
"node {addr} spawned {} renewal tasks in a single 30s cycle (seed {seed:x}) — \
exceeds the production cap MAX_RECOVERY_ATTEMPTS_PER_INTERVAL={}. The renewal \
rate cap in recover_orphaned_subscriptions has regressed (#4601).",
m.max_cycle_batch,
MAX_RECOVERY_ATTEMPTS_PER_INTERVAL,
);
}
assert_eq!(
loaded_metrics.max_cycle_batch, MAX_RECOVERY_ATTEMPTS_PER_INTERVAL,
"loaded node never reached the renewal cap (seed {seed:x}): max_cycle_batch={}, \
expected exactly MAX_RECOVERY_ATTEMPTS_PER_INTERVAL={}. With {} contracts entering \
the renewal window together the cap must clip a cycle to exactly the cap; a lower \
value means the high-demand burst did not form and (A) is vacuous.",
loaded_metrics.max_cycle_batch, MAX_RECOVERY_ATTEMPTS_PER_INTERVAL, NUM_CONTRACTS,
);
let mut migrated: Vec<(usize, usize)> = Vec::new();
for ci in 0..NUM_CONTRACTS {
for n in 2..=num_nodes {
if result.is_node_hosting(&NodeLabel::node(network_name, n), &contract_keys[ci]) {
migrated.push((ci, n));
}
}
}
assert!(
!migrated.is_empty(),
"placement migration was enabled but NO contract migrated off the loaded node to a \
closer neighbor (seed {seed:x}). Either the cascade is inert (gate regression) or \
the topology never let a neighbor become a migration target — the test is not \
exercising migration. (contract_idx, node_no) hosting pairs: {migrated:?}",
);
tracing::info!(
seed = format!("{seed:x}"),
migrated_pairs = migrated.len(),
"migration-at-scale: cap held, cap engaged, migration ran"
);
}
}
#[test]
fn test_hosting_clock_injection_and_measurement_end_to_end() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const NETWORK: &str = "hosting-clock-injection";
const SEED: u64 = 0x4642_A001_CAFE;
setup_deterministic_state(SEED);
let rt = create_runtime();
let gateway = NodeLabel::gateway(NETWORK, 0);
let getter = NodeLabel::node(NETWORK, 1);
let demanded = SimOperation::create_test_contract(11);
let demanded_key = demanded.key();
let demanded_id = *demanded.key().id();
let demanded_state = SimOperation::create_test_state(11);
let (sim, clock, clock_start) = rt.block_on(async {
let mut sim = SimNetwork::new(NETWORK, 1, 2, 7, 3, 10, 2, SEED).await;
let clock = sim.enable_hosting_time_control();
sim.with_hosting_budget(4096);
let clock_start = clock.current_time();
(sim, clock, clock_start)
});
let operations = vec![
ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: demanded.clone(),
state: demanded_state.clone(),
subscribe: true,
},
),
ScheduledOperation::new(
gateway.clone(),
SimOperation::AdvanceHostingClock {
duration: Duration::from_secs(30 * 60),
},
),
ScheduledOperation::new(
getter.clone(),
SimOperation::Get {
contract_id: demanded_id,
return_contract_code: true,
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(30),
);
assert!(
clock.current_time() >= clock_start + Duration::from_secs(30 * 60),
"AdvanceHostingClock should have advanced the shared hosting clock by >= 30 minutes"
);
assert!(
result.turmoil_result.is_ok(),
"controlled simulation should complete: {:?}",
result.turmoil_result.err()
);
for label in result.captured_node_labels() {
tracing::info!(
node = %label,
hosting = result.node_hosting_count(&label),
subscriptions = result.node_subscription_count(&label),
active_demand = ?result.node_active_demand_count(&label),
"hosting measurement snapshot"
);
}
assert!(
result.is_node_hosting(&gateway, &demanded_key),
"gateway should still host the demanded (subscribed) contract after a 30-minute \
hosting-clock jump; its client subscription must protect it from eviction"
);
assert!(
result.node_hosting_count(&gateway) >= 1,
"gateway should host at least the demanded contract"
);
}
#[test]
fn test_scripted_node_crash_mid_run() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const NETWORK: &str = "scripted-node-crash";
const SEED: u64 = 0x4642_F001_CAFE;
setup_deterministic_state(SEED);
let rt = create_runtime();
let gateway = NodeLabel::gateway(NETWORK, 0);
let node1 = NodeLabel::node(NETWORK, 1);
let node2 = NodeLabel::node(NETWORK, 2);
let contract = SimOperation::create_test_contract(21);
let contract_key = contract.key();
let contract_id = *contract.key().id();
let state = SimOperation::create_test_state(21);
let sim = rt.block_on(async { SimNetwork::new(NETWORK, 1, 3, 7, 3, 10, 2, SEED).await });
let operations = vec![
ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state.clone(),
subscribe: true,
},
),
ScheduledOperation::new(node1.clone(), SimOperation::Subscribe { contract_id }),
ScheduledOperation::new(node2.clone(), SimOperation::Subscribe { contract_id }),
ScheduledOperation::new(node1.clone(), SimOperation::CrashNode),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(180),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"controlled simulation with a scripted crash should complete: {:?}",
result.turmoil_result.err()
);
for label in result.captured_node_labels() {
tracing::info!(
node = %label,
hosting = result.node_hosting_count(&label),
subscriptions = result.node_subscription_count(&label),
upstream = ?result.node_upstream_count(&label, &contract_key),
"post-crash measurement snapshot"
);
}
assert!(
result.crash_packets_dropped() > 0,
"scripted CrashNode must actually drop packets to/from the crashed node \
(crash_packets_dropped == 0 means the crash was a silent no-op)"
);
let hosted_somewhere = result
.captured_node_labels()
.iter()
.any(|label| result.is_node_hosting(label, &contract_key));
assert!(
hosted_somewhere,
"contract should remain hosted on a surviving node after node1 was crashed"
);
}
#[test]
fn test_advance_hosting_clock_without_control_is_graceful_noop() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const NETWORK: &str = "advance-clock-no-control";
const SEED: u64 = 0x4642_A002_CAFE;
setup_deterministic_state(SEED);
let rt = create_runtime();
let gateway = NodeLabel::gateway(NETWORK, 0);
let contract = SimOperation::create_test_contract(31);
let state = SimOperation::create_test_state(31);
let sim = rt.block_on(async { SimNetwork::new(NETWORK, 1, 1, 7, 3, 10, 2, SEED).await });
let operations = vec![
ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state.clone(),
subscribe: true,
},
),
ScheduledOperation::new(
gateway.clone(),
SimOperation::AdvanceHostingClock {
duration: Duration::from_secs(30 * 60),
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(20),
);
assert!(
result.turmoil_result.is_ok(),
"simulation must complete gracefully even when AdvanceHostingClock is scheduled \
without a controllable clock enabled: {:?}",
result.turmoil_result.err()
);
assert!(
result.is_node_hosting(&gateway, &contract.key()),
"gateway should still host its PUT contract"
);
}
#[test]
fn test_injected_hosting_clock_drives_subscription_lease_lapse() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SUB_LEASE_ADVANCE: Duration = Duration::from_secs(60); const SUPER_LEASE_ADVANCE: Duration = Duration::from_secs(20 * 60);
let run = |network: &str, seed: u64, advance: Duration| -> (usize, u64) {
setup_deterministic_state(seed);
let rt = create_runtime();
let gateway = NodeLabel::gateway(network, 0);
let subscriber = NodeLabel::node(network, 1);
let contract = SimOperation::create_test_contract(51);
let contract_id = *contract.key().id();
let sim = rt.block_on(async {
let mut sim = SimNetwork::new(network, 1, 3, 7, 3, 10, 2, seed).await;
sim.enable_hosting_time_control();
sim
});
let ops = vec![
ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_test_state(51),
subscribe: false,
},
),
ScheduledOperation::new(subscriber.clone(), SimOperation::Subscribe { contract_id }),
ScheduledOperation::new(subscriber.clone(), SimOperation::CrashNode),
ScheduledOperation::new(
gateway.clone(),
SimOperation::AdvanceHostingClock { duration: advance },
),
];
let result = sim.run_controlled_simulation(
seed,
ops,
Duration::from_secs(300),
Duration::from_secs(120),
);
assert!(
result.turmoil_result.is_ok(),
"controlled simulation should complete: {:?}",
result.turmoil_result.err()
);
let subscribed_peers = result
.topology_snapshots
.iter()
.filter(|s| s.active_subscription_keys.contains(&contract_id))
.count();
(subscribed_peers, result.crash_packets_dropped())
};
const SEED: u64 = 0x4642_A003_CAFE;
let (control_count, control_dropped) = run("clock-lease-control", SEED, SUB_LEASE_ADVANCE);
let (test_count, test_dropped) = run("clock-lease-test", SEED, SUPER_LEASE_ADVANCE);
tracing::info!(
control_count,
test_count,
control_dropped,
test_dropped,
"injected-clock lease lapse: control (sub-lease advance) vs test (super-lease advance)"
);
assert!(
control_dropped > 0 && test_dropped > 0,
"scripted CrashNode must drop packets in both runs (control={control_dropped}, \
test={test_dropped}); 0 means the crash was a silent no-op and renewal would \
keep refreshing the lease"
);
assert!(
control_count >= 1,
"sub-lease advance ({}s < 8min): the subscription must still be held by at least \
one peer — the lease is younger than SUBSCRIPTION_LEASE_DURATION, so it must NOT \
lapse; got {control_count} subscribed peer(s)",
SUB_LEASE_ADVANCE.as_secs(),
);
assert_eq!(
test_count,
0,
"super-lease advance ({}s > 8min): every subscription lease must lapse once the \
injected clock crosses SUBSCRIPTION_LEASE_DURATION (nothing renews it after the \
crash); got {test_count} peer(s) still subscribed",
SUPER_LEASE_ADVANCE.as_secs(),
);
assert!(
test_count < control_count,
"the super-lease run must leave strictly fewer subscribed peers than the sub-lease \
run (control={control_count}, test={test_count}); equal counts mean the injected \
clock never reached the HostingManager (silent no-op — the regression this test \
guards against)"
);
}
#[test_log::test]
fn test_subscription_count_tracks_demand_not_cache() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEEDS: [u64; 3] = [0x4642_D101, 0x4642_D102, 0x4642_D103];
const DEMAND: usize = 2; const CACHE: usize = 8;
for seed in SEEDS {
setup_deterministic_state(seed);
let rt = create_runtime();
let network = format!("subcount-demand-{seed:x}");
let gateway = NodeLabel::gateway(&network, 0);
let hub = NodeLabel::node(&network, 1);
let demand_contracts: Vec<_> = (0..DEMAND)
.map(|i| SimOperation::create_test_contract(0x40 + i as u8))
.collect();
let cache_contracts: Vec<_> = (0..CACHE)
.map(|i| SimOperation::create_test_contract(0x60 + i as u8))
.collect();
let demand_ids: HashSet<_> = demand_contracts.iter().map(|c| *c.key().id()).collect();
let cache_ids: HashSet<_> = cache_contracts.iter().map(|c| *c.key().id()).collect();
let (sim, _clock) = rt.block_on(async {
let mut sim = SimNetwork::new(&network, 1, 2, 7, 3, 10, 2, seed).await;
let clock = sim.enable_hosting_time_control();
sim.with_hosting_budget(1024 * 1024);
(sim, clock)
});
let mut ops = Vec::new();
for c in demand_contracts.iter().chain(cache_contracts.iter()) {
ops.push(ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: c.clone(),
state: SimOperation::create_test_state(1),
subscribe: false,
},
));
}
for c in &demand_contracts {
ops.push(ScheduledOperation::new(
hub.clone(),
SimOperation::Subscribe {
contract_id: *c.key().id(),
},
));
}
for c in &cache_contracts {
ops.push(ScheduledOperation::new(
hub.clone(),
SimOperation::Get {
contract_id: *c.key().id(),
return_contract_code: true,
subscribe: false,
},
));
}
ops.push(ScheduledOperation::new(
hub.clone(),
SimOperation::AdvanceHostingClock {
duration: Duration::from_secs(20 * 60),
},
));
let result = sim.run_controlled_simulation(
seed,
ops,
Duration::from_secs(300),
Duration::from_secs(120),
);
assert!(
result.turmoil_result.is_ok(),
"seed={seed:x}: sim failed: {:?}",
result.turmoil_result.err()
);
let hosting = result.node_hosting_count(&hub);
let subs = result.node_subscription_count(&hub);
let demand = result.node_active_demand_count(&hub);
let metrics = result.aggregate_renewal_metrics();
let mut demand_leases = 0usize;
let mut cache_leases = 0usize;
for snap in &result.topology_snapshots {
for id in &snap.active_subscription_keys {
if demand_ids.contains(id) {
demand_leases += 1;
} else if cache_ids.contains(id) {
cache_leases += 1;
}
}
}
eprintln!(
"[proof1 seed={seed:x}] hub hosting_count={hosting} subscription_count={subs} \
active_demand_count={demand:?} | network leases: demand={demand_leases} \
cache={cache_leases} | max_cycle_batch={}",
metrics.max_cycle_batch
);
assert!(
hosting >= DEMAND + CACHE / 2,
"seed={seed:x}: hub hosting_count ({hosting}) too low — the GET cache-load \
did not land; test would be degenerate"
);
assert_eq!(
cache_leases, 0,
"seed={seed:x}: #3763 storm signature — {cache_leases} cache-only contract lease(s) \
survived network-wide after demand faded. Subscriptions must track active demand, \
NOT accumulated cache (hub hosting_count={hosting})."
);
assert!(
subs <= DEMAND + 1,
"seed={seed:x}: hub subscription_count ({subs}) exceeds the demand it created \
({DEMAND}) + 1 — leases must not track the {hosting}-contract cache."
);
assert!(
hosting > subs,
"seed={seed:x}: hub hosting_count ({hosting}) must exceed subscription_count \
({subs}) — the separation between cache and leases must be real."
);
assert!(
demand_leases >= 1,
"seed={seed:x}: no demand-backed lease survived — the run is degenerate \
(subscribes never took or all lapsed), so the cache==0 result is vacuous"
);
assert!(
metrics.max_cycle_batch <= 10,
"seed={seed:x}: max_cycle_batch ({}) exceeded the renewal cap of 10",
metrics.max_cycle_batch
);
}
}
struct MeshObservation {
subscribed_peers: Vec<SocketAddr>,
hosting_nodes: usize,
max_upstreams: usize,
total_snapshots: usize,
}
fn run_subscription_mesh(
seed: u64,
network: &str,
subscriber_ids: &[usize],
collapse: bool,
) -> MeshObservation {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
setup_deterministic_state(seed);
let rt = create_runtime();
let gateway = NodeLabel::gateway(network, 0);
let contract = SimOperation::create_test_contract(0xC0);
let contract_key = contract.key();
let contract_id = *contract.key().id();
let sim = rt.block_on(async {
let mut sim = SimNetwork::new(network, 1, 15, 10, 3, 5, 3, seed).await;
sim.enable_hosting_time_control();
sim
});
let mut ops = vec![ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: SimOperation::create_test_state(1),
subscribe: true,
},
)];
for id in subscriber_ids {
ops.push(ScheduledOperation::new(
NodeLabel::node(network, *id),
SimOperation::Subscribe { contract_id },
));
}
if collapse {
ops.push(ScheduledOperation::new(
gateway.clone(),
SimOperation::Disconnect,
));
for id in subscriber_ids {
ops.push(ScheduledOperation::new(
NodeLabel::node(network, *id),
SimOperation::Disconnect,
));
}
ops.push(ScheduledOperation::new(
gateway.clone(),
SimOperation::AdvanceHostingClock {
duration: Duration::from_secs(20 * 60),
},
));
}
let result = sim.run_controlled_simulation(
seed,
ops,
Duration::from_secs(360),
Duration::from_secs(120),
);
assert!(
result.turmoil_result.is_ok(),
"seed={seed:x} collapse={collapse}: sim failed: {:?}",
result.turmoil_result.err()
);
let subscribed_peers: Vec<SocketAddr> = result
.topology_snapshots
.iter()
.filter(|s| s.active_subscription_keys.contains(&contract_id))
.map(|s| s.peer_addr)
.collect();
let hosting_nodes = result
.captured_node_labels()
.iter()
.filter(|l| result.is_node_hosting(l, &contract_key))
.count();
let max_upstreams = result
.captured_node_labels()
.iter()
.map(|l| result.node_upstream_count(l, &contract_key).unwrap_or(0))
.max()
.unwrap_or(0);
MeshObservation {
subscribed_peers,
hosting_nodes,
max_upstreams,
total_snapshots: result.topology_snapshots.len(),
}
}
const MESH_SUBSCRIBERS: [usize; 8] = [1, 3, 5, 7, 9, 11, 13, 15];
#[test_log::test]
fn test_subscription_chain_collapses_on_client_leave() {
const SEEDS: [u64; 3] = [0x4642_D201, 0x4642_D202, 0x4642_D203];
const MIN_MESH: usize = 3;
for seed in SEEDS {
let form = run_subscription_mesh(
seed,
&format!("chain-form-{seed:x}"),
&MESH_SUBSCRIBERS,
false,
);
eprintln!(
"[proof2 seed={seed:x} FORM] mesh_size={} hosting_nodes={} \
max_upstreams={} total_snapshots={}",
form.subscribed_peers.len(),
form.hosting_nodes,
form.max_upstreams,
form.total_snapshots,
);
assert!(
form.subscribed_peers.len() >= MIN_MESH,
"seed={seed:x}: formation degenerate — only {} peer(s) in the subscription mesh \
(need >= {MIN_MESH}); the collapse below would be vacuous. \
Requested {} subscribers.",
form.subscribed_peers.len(),
MESH_SUBSCRIBERS.len(),
);
let coll = run_subscription_mesh(
seed,
&format!("chain-collapse-{seed:x}"),
&MESH_SUBSCRIBERS,
true,
);
eprintln!(
"[proof2 seed={seed:x} COLLAPSE] survivors={} hosting_nodes={} total_snapshots={} {:?}",
coll.subscribed_peers.len(),
coll.hosting_nodes,
coll.total_snapshots,
coll.subscribed_peers,
);
assert!(
coll.total_snapshots > 0,
"seed={seed:x}: no snapshots captured in collapse run — cannot judge collapse"
);
assert!(
coll.subscribed_peers.is_empty(),
"seed={seed:x}: chain did NOT collapse — {} peer(s) still hold the contract in \
active_subscriptions after all client interest ended + a 20-min clock jump: {:?}. \
Interest-gated renewal must let every un-demanded lease lapse.",
coll.subscribed_peers.len(),
coll.subscribed_peers,
);
}
}
#[derive(Debug, Clone)]
struct PieceEGateMetrics {
get_attempts: u64,
get_successes: u64,
get_not_found: u64,
get_failures: u64,
get_timeouts: u64,
network_successes: u64,
findability_rate: f64,
requesters_total: usize,
requesters_with_state: usize,
client_findability_rate: f64,
time_to_first_success_ms: Option<u64>,
hosting_nodes: usize,
total_holders: usize,
fresh_holders: usize,
stale_holders: usize,
requesters_fresh: usize,
requesters_stale: usize,
stale_serve_rate: Option<f64>,
subscribers_total: usize,
subscribers_fresh: usize,
}
#[allow(clippy::too_many_arguments)]
fn compute_piece_e_metrics(
result: &freenet::dev_tool::ControlledSimulationResult,
logs: &[freenet::tracing::NetLogMessage],
contract_key: &freenet_stdlib::prelude::ContractKey,
current_source: &freenet::dev_tool::NodeLabel,
get_requesters: &[freenet::dev_tool::NodeLabel],
subscribers: &[freenet::dev_tool::NodeLabel],
) -> PieceEGateMetrics {
let summary = freenet::tracing::summarize_get_outcomes_per_tx(logs);
let get_attempts = summary.total();
let findability_rate = if get_attempts == 0 {
0.0
} else {
summary.successes as f64 / get_attempts as f64
};
let time_to_first_success_ms = summary.success_elapsed_ms.first().copied();
let current_hash = result
.node_storages
.get(current_source)
.and_then(|s| s.get_stored_state(contract_key))
.map(|ws| freenet::tracing::state_hash_short(&ws));
let hash_of = |label: &freenet::dev_tool::NodeLabel| -> Option<String> {
result
.node_storages
.get(label)
.and_then(|s| s.get_stored_state(contract_key))
.map(|ws| freenet::tracing::state_hash_short(&ws))
};
let requesters_total = get_requesters.len();
let mut requesters_with_state = 0usize;
let mut requesters_fresh = 0usize;
let mut requesters_stale = 0usize;
for r in get_requesters {
if let Some(h) = hash_of(r) {
requesters_with_state += 1;
if current_hash.as_deref() == Some(h.as_str()) {
requesters_fresh += 1;
} else {
requesters_stale += 1;
}
}
}
let client_findability_rate = if requesters_total == 0 {
0.0
} else {
requesters_with_state as f64 / requesters_total as f64
};
let served = requesters_fresh + requesters_stale;
let stale_serve_rate = if served == 0 {
None
} else {
Some(requesters_stale as f64 / served as f64)
};
let hosting_nodes = result
.captured_node_labels()
.iter()
.filter(|l| result.is_node_hosting(l, contract_key))
.count();
let mut total_holders = 0usize;
let mut fresh_holders = 0usize;
let mut stale_holders = 0usize;
for (_label, storage) in result.node_storages.iter() {
let Some(state) = storage.get_stored_state(contract_key) else {
continue;
};
total_holders += 1;
if current_hash.as_deref() == Some(freenet::tracing::state_hash_short(&state).as_str()) {
fresh_holders += 1;
} else {
stale_holders += 1;
}
}
let subscribers_total = subscribers.len();
let subscribers_fresh = subscribers
.iter()
.filter(|l| hash_of(l).as_deref() == current_hash.as_deref())
.count();
PieceEGateMetrics {
get_attempts,
get_successes: summary.successes,
get_not_found: summary.not_found,
get_failures: summary.failures,
get_timeouts: summary.timeouts,
network_successes: summary.network_successes,
findability_rate,
requesters_total,
requesters_with_state,
client_findability_rate,
time_to_first_success_ms,
hosting_nodes,
total_holders,
fresh_holders,
stale_holders,
requesters_fresh,
requesters_stale,
stale_serve_rate,
subscribers_total,
subscribers_fresh,
}
}
fn nodes_by_distance_to_key(locs: &[f64], num_nodes: usize, key_loc: f64) -> Vec<usize> {
let ring_dist = |a: f64, b: f64| {
let d = (a - b).abs();
d.min(1.0 - d)
};
let mut ranked: Vec<(usize, f64)> = (1..=num_nodes)
.map(|i| (i, ring_dist(locs[i], key_loc)))
.collect();
ranked.sort_by(|a, b| a.1.total_cmp(&b.1));
ranked.into_iter().map(|(n, _)| n).collect()
}
fn run_findability_seed(seed: u64) -> (PieceEGateMetrics, bool, u64) {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const NUM_NODES: usize = 15;
setup_deterministic_state(seed);
let network = format!("piece-e-find-{seed:x}");
let contract = SimOperation::create_test_contract(0xA5);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let state_v1 = SimOperation::create_test_state(1);
let wrap = |x: f64| x.rem_euclid(1.0);
let node_locations: Vec<f64> = (0..NUM_NODES)
.map(|i| wrap(key_loc + i as f64 / NUM_NODES as f64))
.collect();
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
&network,
1,
NUM_NODES,
10, 7, 5, 2, seed,
&node_locations,
)
.await
});
let locs = sim.get_peer_locations();
let ranked = nodes_by_distance_to_key(&locs, NUM_NODES, key_loc);
let holder_no = ranked[14];
let churn_nos = [ranked[3], ranked[4]];
let requester_nos: Vec<usize> = [6usize, 7, 8, 9, 10, 11, 12, 13]
.iter()
.map(|&i| ranked[i])
.collect();
let holder = NodeLabel::node(&network, holder_no);
let get_requesters: Vec<NodeLabel> = requester_nos
.iter()
.map(|n| NodeLabel::node(&network, *n))
.collect();
let mut operations = Vec::new();
operations.push(ScheduledOperation::new(
holder.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state_v1.clone(),
subscribe: true,
},
));
for c in &churn_nos {
operations.push(ScheduledOperation::new(
NodeLabel::node(&network, *c),
SimOperation::CrashNode,
));
}
for r in &get_requesters {
operations.push(ScheduledOperation::new(
r.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let logs_handle = sim.event_logs_handle();
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(300),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"seed={seed:x}: findability sim failed: {:?}",
result.turmoil_result.err()
);
let logs = rt.block_on(async { logs_handle.lock().await.clone() });
let metrics =
compute_piece_e_metrics(&result, &logs, &contract_key, &holder, &get_requesters, &[]);
let holder_hosting = result.is_node_hosting(&holder, &contract_key);
let crash_dropped = result.crash_packets_dropped();
(metrics, holder_hosting, crash_dropped)
}
#[test_log::test]
fn test_piece_e_findability_sparse_ring_gate() {
const SEEDS: [u64; 6] = [
0x4642_E0A0_5A1D,
0x4642_E0A0_0002,
0x4642_E0A0_0003,
0x4642_E0A0_0005,
0x4642_E0A0_0007,
0x4642_E0A0_000B,
];
const MEAN_FLOOR: f64 = 0.60;
const PER_SEED_FLOOR: f64 = 0.50;
let mut rates = Vec::new();
let mut total_crash = 0u64;
tracing::info!(target: "piece_e_gate",
"===== PIECE-E FINDABILITY BASELINE (sparse 15+1 PUT-scattered, main, {} seeds) =====",
SEEDS.len());
for seed in SEEDS {
let (m, holder_hosting, crash_dropped) = run_findability_seed(seed);
assert!(m.get_attempts > 0, "seed={seed:x}: no GET attempts");
assert!(
holder_hosting,
"seed={seed:x}: PUT origin must still host the contract"
);
total_crash += crash_dropped;
rates.push(m.client_findability_rate);
tracing::info!(target: "piece_e_gate",
" seed=0x{seed:X}: client_findability={:.3} ({}/{}) wire={:.3} hosting_nodes={} ttf_ms={:?}",
m.client_findability_rate, m.requesters_with_state, m.requesters_total,
m.findability_rate, m.hosting_nodes, m.time_to_first_success_ms);
}
let n = rates.len() as f64;
let mean = rates.iter().sum::<f64>() / n;
let min = rates.iter().copied().fold(f64::INFINITY, f64::min);
let max = rates.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let above = rates.iter().filter(|&&r| r >= PER_SEED_FLOOR).count();
tracing::info!(target: "piece_e_gate",
" MEAN client_findability = {mean:.3} (min={min:.3} max={max:.3}); \
{above}/{} seeds >= per-seed floor {PER_SEED_FLOOR}; rates={rates:?}",
SEEDS.len());
tracing::info!(target: "piece_e_gate", "===== END PIECE-E FINDABILITY BASELINE =====");
assert!(
total_crash > 0,
"scripted CrashNode dropped no packets across any seed — churn had no effect"
);
assert!(
mean >= MEAN_FLOOR,
"FINDABILITY REGRESSION: mean client-findability {mean:.3} across {} seeds \
< floor {MEAN_FLOOR} (rates={rates:?})",
SEEDS.len()
);
assert!(
above >= SEEDS.len() - 1,
"FINDABILITY REGRESSION: only {above}/{} seeds reached the per-seed floor \
{PER_SEED_FLOOR} (rates={rates:?})",
SEEDS.len()
);
}
#[test_log::test]
fn test_piece_e_unsubscribed_stale_serve_gate() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0x4642_E0B0_57A1;
const NETWORK_NAME: &str = "piece-e-stale-serve";
const NUM_NODES: usize = 8;
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0xB7);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
freenet::dev_tool::register_crdt_contract(contract_id);
let state_v1 = SimOperation::create_crdt_state(1, 0x11);
let state_v2 = SimOperation::create_crdt_state(2, 0x22);
let wrap = |x: f64| x.rem_euclid(1.0);
let node_locations: Vec<f64> = (0..NUM_NODES)
.map(|i| wrap(key_loc + i as f64 / NUM_NODES as f64))
.collect();
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1,
NUM_NODES,
10,
7,
8,
3,
SEED,
&node_locations,
)
.await
});
let locs = sim.get_peer_locations();
let ranked = nodes_by_distance_to_key(&locs, NUM_NODES, key_loc);
let host_no = ranked[0]; let reachable_no = ranked[1]; let isolated_no = ranked[2]; let mesh_nos = [ranked[3], ranked[4]];
let host = NodeLabel::node(NETWORK_NAME, host_no);
let reachable = NodeLabel::node(NETWORK_NAME, reachable_no);
let isolated = NodeLabel::node(NETWORK_NAME, isolated_no);
let mesh: Vec<NodeLabel> = mesh_nos
.iter()
.map(|n| NodeLabel::node(NETWORK_NAME, *n))
.collect();
tracing::info!(
key_loc,
host = host_no,
reachable = reachable_no,
isolated = isolated_no,
mesh = ?mesh_nos,
"piece-E stale-serve gate: roles by ring distance to key"
);
let mut operations = Vec::new();
operations.push(ScheduledOperation::new(
host.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: state_v1.clone(),
},
));
for n in [&reachable, &isolated] {
operations.push(ScheduledOperation::new(
n.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
for m in &mesh {
operations.push(ScheduledOperation::new(
m.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: true,
},
));
}
operations.push(ScheduledOperation::new(
isolated.clone(),
SimOperation::CrashNode,
));
operations.push(ScheduledOperation::new(
host.clone(),
SimOperation::Update {
key: contract_key,
data: state_v2.clone(),
},
));
let logs_handle = sim.event_logs_handle();
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(300),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"stale-serve gate sim failed: {:?}",
result.turmoil_result.err()
);
let logs = rt.block_on(async { logs_handle.lock().await.clone() });
let metrics = compute_piece_e_metrics(&result, &logs, &contract_key, &host, &[], &mesh);
let v1_hash = freenet::tracing::state_hash_short(&freenet_stdlib::prelude::WrappedState::new(
state_v1.clone(),
));
let v2_hash = freenet::tracing::state_hash_short(&freenet_stdlib::prelude::WrappedState::new(
state_v2.clone(),
));
let hash_of = |label: &NodeLabel| -> Option<String> {
result
.node_storages
.get(label)
.and_then(|s| s.get_stored_state(&contract_key))
.map(|ws| freenet::tracing::state_hash_short(&ws))
};
let host_current = hash_of(&host);
let isolated_hash = hash_of(&isolated);
let reachable_hash = hash_of(&reachable);
let forced_stale_detected =
isolated_hash.is_some() && isolated_hash.as_deref() != host_current.as_deref();
tracing::info!(target: "piece_e_gate",
"===== PIECE-E INVARIANT-1 STRUCTURAL GUARD + STALE-DETECTOR CONTROL (seed=0x{SEED:X}, main) =====");
tracing::info!(target: "piece_e_gate", " v1={v1_hash} v2={v2_hash} host_current={host_current:?}");
tracing::info!(target: "piece_e_gate",
" FORCED-STALE positive control: isolated node hash={isolated_hash:?} flagged_stale={forced_stale_detected}");
tracing::info!(target: "piece_e_gate",
" reachable (non-isolated) cache hash={reachable_hash:?} (main refreshes reachable caches -> expect v2)");
tracing::info!(target: "piece_e_gate",
" durable stale copies (all nodes): {} of {} holders (fresh={})",
metrics.stale_holders, metrics.total_holders, metrics.fresh_holders);
tracing::info!(target: "piece_e_gate",
" mesh subscribers fresh after UPDATE (positive control): {}/{}",
metrics.subscribers_fresh, metrics.subscribers_total);
tracing::info!(target: "piece_e_gate", "===== END =====");
assert_ne!(
v1_hash, v2_hash,
"v1 and v2 must differ — the UPDATE must be a real state change"
);
assert_eq!(
host_current.as_deref(),
Some(v2_hash.as_str()),
"UPDATE must apply at HOST (host_current should equal v2)"
);
assert_eq!(
metrics.subscribers_fresh, metrics.subscribers_total,
"mesh subscribers must all be fresh after the UPDATE (invariant-1 positive control)"
);
assert!(
result.crash_packets_dropped() > 0,
"crash-isolation of the stale node had no effect (no packets dropped)"
);
assert!(
forced_stale_detected,
"FORCED-STALE positive control FAILED: the crash-isolated node cached v1 and never \
received the UPDATE, so it must be flagged stale (isolated_hash={isolated_hash:?}, \
current={host_current:?})"
);
freenet::dev_tool::clear_crdt_contracts();
}
#[test_log::test]
fn test_piece_b_anti_entropy_heals_demandless_copy() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0x4642_B0B0_57A1;
const NETWORK_NAME: &str = "piece-b-anti-entropy-heal";
const NUM_NODES: usize = 8;
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0xB7);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
freenet::dev_tool::register_crdt_contract(contract_id);
let state_v1 = SimOperation::create_crdt_state(1, 0x11);
let state_v2 = SimOperation::create_crdt_state(2, 0x22);
let wrap = |x: f64| x.rem_euclid(1.0);
let node_locations: Vec<f64> = (0..NUM_NODES)
.map(|i| wrap(key_loc + i as f64 / NUM_NODES as f64))
.collect();
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1,
NUM_NODES,
10,
7,
8,
3,
SEED,
&node_locations,
)
.await
});
let locs = sim.get_peer_locations();
let ranked = nodes_by_distance_to_key(&locs, NUM_NODES, key_loc);
let host_no = ranked[0]; let reachable_no = ranked[1]; let isolated_no = ranked[2]; let mesh_nos = [ranked[3], ranked[4]];
let host = NodeLabel::node(NETWORK_NAME, host_no);
let reachable = NodeLabel::node(NETWORK_NAME, reachable_no);
let isolated = NodeLabel::node(NETWORK_NAME, isolated_no);
let mesh: Vec<NodeLabel> = mesh_nos
.iter()
.map(|n| NodeLabel::node(NETWORK_NAME, *n))
.collect();
tracing::info!(
key_loc,
host = host_no,
reachable = reachable_no,
isolated = isolated_no,
mesh = ?mesh_nos,
"piece-B anti-entropy heal: roles by ring distance to key"
);
let mut operations = Vec::new();
operations.push(ScheduledOperation::new(
host.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: state_v1.clone(),
},
));
for n in [&reachable, &isolated] {
operations.push(ScheduledOperation::new(
n.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
for m in &mesh {
operations.push(ScheduledOperation::new(
m.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: true,
},
));
}
operations.push(ScheduledOperation::new(
isolated.clone(),
SimOperation::CrashNode,
));
operations.push(ScheduledOperation::new(
host.clone(),
SimOperation::Update {
key: contract_key,
data: state_v2.clone(),
},
));
operations.push(ScheduledOperation::new(
isolated.clone(),
SimOperation::RecoverNode,
));
let logs_handle = sim.event_logs_handle();
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(720),
Duration::from_secs(680),
);
assert!(
result.turmoil_result.is_ok(),
"anti-entropy heal sim failed: {:?}",
result.turmoil_result.err()
);
let logs = rt.block_on(async { logs_handle.lock().await.clone() });
let metrics = compute_piece_e_metrics(
&result,
&logs,
&contract_key,
&host,
&[reachable.clone(), isolated.clone()],
&mesh,
);
let v1_hash = freenet::tracing::state_hash_short(&freenet_stdlib::prelude::WrappedState::new(
state_v1.clone(),
));
let v2_hash = freenet::tracing::state_hash_short(&freenet_stdlib::prelude::WrappedState::new(
state_v2.clone(),
));
let hash_of = |label: &NodeLabel| -> Option<String> {
result
.node_storages
.get(label)
.and_then(|s| s.get_stored_state(&contract_key))
.map(|ws| freenet::tracing::state_hash_short(&ws))
};
let host_current = hash_of(&host);
let isolated_hash = hash_of(&isolated);
let reachable_hash = hash_of(&reachable);
let isolated_hosting = result.is_node_hosting(&isolated, &contract_key);
tracing::info!(target: "piece_b_heal",
"===== PIECE-B ANTI-ENTROPY HEAL (seed=0x{SEED:X}) =====");
tracing::info!(target: "piece_b_heal", " v1={v1_hash} v2={v2_hash} host_current={host_current:?}");
tracing::info!(target: "piece_b_heal",
" ISOLATED demandless copy: hash={isolated_hash:?} hosting={isolated_hosting} (recovered; must HEAL to v2)");
tracing::info!(target: "piece_b_heal",
" REACHABLE demandless copy: hash={reachable_hash:?} (control; healed via broadcast)");
tracing::info!(target: "piece_b_heal",
" holders: {} of {} fresh (stale={})",
metrics.fresh_holders, metrics.total_holders, metrics.stale_holders);
tracing::info!(target: "piece_b_heal",
" demandless requesters fresh after heal: {}/{} (stale={})",
metrics.requesters_fresh, metrics.requesters_total, metrics.requesters_stale);
tracing::info!(target: "piece_b_heal",
" crash packets dropped (isolation took effect): {}", result.crash_packets_dropped());
tracing::info!(target: "piece_b_heal", "===== END =====");
assert_ne!(
v1_hash, v2_hash,
"v1 and v2 must differ — the UPDATE must be a real state change"
);
assert_eq!(
host_current.as_deref(),
Some(v2_hash.as_str()),
"UPDATE must apply at HOST (host_current should equal v2)"
);
assert!(
result.crash_packets_dropped() > 0,
"crash-isolation had no effect (no packets dropped) — the copy did not miss the update"
);
assert!(
isolated_hosting,
"isolated node must be a hosting copy (demandless every-hop hosting), got hosting=false"
);
assert!(
!mesh.contains(&isolated),
"test wiring error: the isolated (demandless) node must not be in the subscribed mesh"
);
assert_eq!(
metrics.subscribers_fresh, metrics.subscribers_total,
"mesh subscribers must all be fresh after the UPDATE (propagation positive control)"
);
assert_eq!(
isolated_hash.as_deref(),
Some(v2_hash.as_str()),
"PIECE-B: the recovered demandless copy MUST heal to v2 via anti-entropy \
(isolated_hash={isolated_hash:?}, v2={v2_hash}, host_current={host_current:?})"
);
assert_eq!(
isolated_hash.as_deref(),
host_current.as_deref(),
"PIECE-B: the recovered demandless copy MUST converge with the source of truth"
);
freenet::dev_tool::clear_crdt_contracts();
}
#[test_log::test]
fn test_step9_source2_removal_subscriber_heals_via_anti_entropy() {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
const SEED: u64 = 0x4642_0009_5AFE; const NETWORK_NAME: &str = "step9-source2-removal-heal";
const NUM_NODES: usize = 8;
setup_deterministic_state(SEED);
let contract = SimOperation::create_test_contract(0x59);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
freenet::dev_tool::register_crdt_contract(contract_id);
let state_v1 = SimOperation::create_crdt_state(1, 0x11);
let state_v2 = SimOperation::create_crdt_state(2, 0x22);
let wrap = |x: f64| x.rem_euclid(1.0);
let node_locations: Vec<f64> = (0..NUM_NODES)
.map(|i| wrap(key_loc + i as f64 / NUM_NODES as f64))
.collect();
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
NETWORK_NAME,
1,
NUM_NODES,
10,
7,
8,
3,
SEED,
&node_locations,
)
.await
});
let locs = sim.get_peer_locations();
let ranked = nodes_by_distance_to_key(&locs, NUM_NODES, key_loc);
let host_no = ranked[0]; let mesh_control_nos = [ranked[1], ranked[2]]; let isolated_no = ranked[3];
let host = NodeLabel::node(NETWORK_NAME, host_no);
let mesh_control: Vec<NodeLabel> = mesh_control_nos
.iter()
.map(|n| NodeLabel::node(NETWORK_NAME, *n))
.collect();
let isolated = NodeLabel::node(NETWORK_NAME, isolated_no);
tracing::info!(
key_loc,
host = host_no,
mesh_control = ?mesh_control_nos,
isolated = isolated_no,
"step-9 Source-2-removal heal: roles by ring distance to key"
);
let mut operations = Vec::new();
operations.push(ScheduledOperation::new(
host.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: state_v1.clone(),
},
));
for m in &mesh_control {
operations.push(ScheduledOperation::new(
m.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: true,
},
));
}
operations.push(ScheduledOperation::new(
isolated.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: true,
},
));
operations.push(ScheduledOperation::new(
isolated.clone(),
SimOperation::CrashNode,
));
operations.push(ScheduledOperation::new(
host.clone(),
SimOperation::Update {
key: contract_key,
data: state_v2.clone(),
},
));
operations.push(ScheduledOperation::new(
isolated.clone(),
SimOperation::RecoverNode,
));
let logs_handle = sim.event_logs_handle();
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(720),
Duration::from_secs(680),
);
assert!(
result.turmoil_result.is_ok(),
"step-9 heal sim failed: {:?}",
result.turmoil_result.err()
);
let logs = rt.block_on(async { logs_handle.lock().await.clone() });
let metrics = compute_piece_e_metrics(&result, &logs, &contract_key, &host, &[], &mesh_control);
let v1_hash = freenet::tracing::state_hash_short(&freenet_stdlib::prelude::WrappedState::new(
state_v1.clone(),
));
let v2_hash = freenet::tracing::state_hash_short(&freenet_stdlib::prelude::WrappedState::new(
state_v2.clone(),
));
let hash_of = |label: &NodeLabel| -> Option<String> {
result
.node_storages
.get(label)
.and_then(|s| s.get_stored_state(&contract_key))
.map(|ws| freenet::tracing::state_hash_short(&ws))
};
let host_current = hash_of(&host);
let isolated_hash = hash_of(&isolated);
let isolated_hosting = result.is_node_hosting(&isolated, &contract_key);
tracing::info!(target: "step9_heal",
"===== STEP-9 SOURCE-2-REMOVAL HEAL (seed=0x{SEED:X}) =====");
tracing::info!(target: "step9_heal", " v1={v1_hash} v2={v2_hash} host_current={host_current:?}");
tracing::info!(target: "step9_heal",
" ISOLATED subscriber: hash={isolated_hash:?} hosting={isolated_hosting} (recovered; must HEAL to v2)");
tracing::info!(target: "step9_heal",
" mesh subscribers fresh (Source-1 live fan-out): {}/{}",
metrics.subscribers_fresh, metrics.subscribers_total);
tracing::info!(target: "step9_heal",
" crash packets dropped (isolation took effect): {}", result.crash_packets_dropped());
tracing::info!(target: "step9_heal", "===== END =====");
assert_ne!(
v1_hash, v2_hash,
"v1 and v2 must differ — the UPDATE must be a real state change"
);
assert_eq!(
host_current.as_deref(),
Some(v2_hash.as_str()),
"UPDATE must apply at HOST (host_current should equal v2)"
);
assert!(
metrics.subscribers_total >= 2,
"test wiring: expected >=2 reachable mesh subscribers, got {}",
metrics.subscribers_total
);
assert_eq!(
metrics.subscribers_fresh, metrics.subscribers_total,
"reachable mesh subscribers must all be fresh via Source-1 live fan-out \
({}/{}) — a stale subscriber means advertised-co-host fan-out failed to \
deliver the UPDATE without Source-2",
metrics.subscribers_fresh, metrics.subscribers_total
);
assert!(
result.crash_packets_dropped() > 0,
"crash-isolation had no effect (no packets dropped) — the subscriber did \
not miss the update, so the heal below would not exercise anti-entropy"
);
assert!(
isolated_hosting,
"isolated node must host the contract (it GET-subscribed), got hosting=false"
);
assert_eq!(
isolated_hash.as_deref(),
Some(v2_hash.as_str()),
"STEP-9: the recovered subscriber MUST heal to v2 via anti-entropy \
(isolated_hash={isolated_hash:?}, v2={v2_hash}, host_current={host_current:?})"
);
assert_eq!(
isolated_hash.as_deref(),
host_current.as_deref(),
"STEP-9: the recovered subscriber MUST converge with the source of truth"
);
freenet::dev_tool::clear_crdt_contracts();
}
#[test_log::test]
fn test_summary_first_put_holder_found_ships_delta() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x4642_5F01_CAFE;
const NETWORK_NAME: &str = "summary-first-put-delta";
setup_deterministic_state(SEED);
let rt = create_runtime();
let gateway = NodeLabel::gateway(NETWORK_NAME, 0);
let node1 = NodeLabel::node(NETWORK_NAME, 1);
let contract = SimOperation::create_test_contract(0x5F);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let state_v1 = SimOperation::create_crdt_state(1, 0x11);
let state_v2 = SimOperation::create_crdt_state(2, 0x22);
const CODE_PARAMS_BYTES: u64 = 32 + 16;
let full_state_bytes_if_shipped_whole = state_v2.len() as u64 + CODE_PARAMS_BYTES;
let mut sim =
rt.block_on(async { SimNetwork::new(NETWORK_NAME, 1, 1, 7, 3, 10, 2, SEED).await });
sim.enable_summary_first_put();
let operations = vec![
ScheduledOperation::new(
node1.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state_v1.clone(),
subscribe: true,
},
),
ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state_v2.clone(),
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"summary-first PUT delta sim failed: {:?}",
result.turmoil_result.err()
);
let new_contract_sends = GlobalTestMetrics::put_probe_new_contract_sends();
let delta_sends = GlobalTestMetrics::put_probe_existing_mesh_delta_sends();
let delta_bytes = GlobalTestMetrics::put_probe_existing_mesh_delta_bytes();
tracing::info!(
new_contract_sends,
delta_sends,
delta_bytes,
full_state_bytes_if_shipped_whole,
"summary-first PUT delta test: probe outcome counters"
);
assert_eq!(
new_contract_sends, 0,
"no-holder path must NOT fire: the gateway's PUT found a fresh \
holder (node1), so the summary-first probe must take the \
holder-found branch, never the no-holder fallback"
);
assert_eq!(
delta_sends, 1,
"holder-found path must fire exactly once (the gateway's PUT \
probing the node1 holder)"
);
assert!(
delta_bytes > 0,
"the delta must be non-empty — v2's data genuinely differs from v1"
);
assert!(
delta_bytes < full_state_bytes_if_shipped_whole,
"the existing-mesh delta ({delta_bytes} bytes) must ship strictly \
fewer bytes than a full-state PUT of the same payload would have \
({full_state_bytes_if_shipped_whole} bytes = state + code + params) \
— this is the byte-savings claim the falsifier counters exist to \
prove or disprove"
);
assert!(
result.is_node_hosting(&node1, &contract_key),
"node1 (the holder) should still host the contract after the \
reconcile"
);
let node1_final_state = result
.node_storages
.get(&node1)
.and_then(|s| s.get_stored_state(&contract_key))
.expect("node1 should have a stored state for the contract");
let node1_version = u64::from_le_bytes(
node1_final_state.as_ref()[0..8]
.try_into()
.expect("CRDT state must have an 8-byte version prefix"),
);
assert_eq!(
node1_version, 2,
"the holder's stored state must have merged the gateway's v2 \
contribution via the ProbeReconcile delta (PUT reported success \
end-to-end, not just at the originator's local copy)"
);
freenet::dev_tool::clear_crdt_contracts();
}
#[test_log::test]
fn test_summary_first_put_no_holder_ships_full_state() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
const SEED: u64 = 0x4642_5F02_CAFE;
const NETWORK_NAME: &str = "summary-first-put-new-contract";
setup_deterministic_state(SEED);
let rt = create_runtime();
let gateway = NodeLabel::gateway(NETWORK_NAME, 0);
let node1 = NodeLabel::node(NETWORK_NAME, 1);
let contract = SimOperation::create_test_contract(0x5A);
let contract_key = contract.key();
let state = SimOperation::create_test_state(0x5A);
let mut sim =
rt.block_on(async { SimNetwork::new(NETWORK_NAME, 1, 1, 7, 3, 10, 2, SEED).await });
sim.enable_summary_first_put();
let operations = vec![ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state.clone(),
subscribe: false,
},
)];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"summary-first PUT new-contract sim failed: {:?}",
result.turmoil_result.err()
);
let new_contract_sends = GlobalTestMetrics::put_probe_new_contract_sends();
let delta_sends = GlobalTestMetrics::put_probe_existing_mesh_delta_sends();
tracing::info!(
new_contract_sends,
delta_sends,
"summary-first PUT new-contract test: probe outcome counters"
);
assert_eq!(
delta_sends, 0,
"holder-found path must NOT fire: this is a genuinely new contract, \
no peer hosts it"
);
assert_eq!(
new_contract_sends, 1,
"no-holder path must fire exactly once (the gateway's PUT probing \
node1, which has never seen this contract)"
);
let hosting_labels: Vec<_> = result
.captured_node_labels()
.into_iter()
.filter(|label| result.is_node_hosting(label, &contract_key))
.collect();
tracing::info!(?hosting_labels, "nodes hosting the new contract");
assert!(
hosting_labels.contains(&gateway),
"the originator (the gateway) must host its own PUT"
);
assert!(
hosting_labels.contains(&node1),
"the no-holder full-state fallback must propagate the contract to \
node1 (the gateway's only peer and the sole possible hop) — hop-by-hop \
hosting, not just the originator's own local-store copy — got only \
{hosting_labels:?}"
);
assert!(
hosting_labels.len() >= 2,
"the no-holder full-state fallback must propagate the contract to \
at least one hop beyond the originator's own local-store copy \
(hop-by-hop hosting) — got only {hosting_labels:?}"
);
}
#[test_log::test]
fn test_summary_first_put_reverse_delta_converges_originator() {
use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};
const SEED: u64 = 0x4642_5F03_CAFE;
const NETWORK_NAME: &str = "summary-first-put-reverse-delta";
setup_deterministic_state(SEED);
let rt = create_runtime();
let gateway = NodeLabel::gateway(NETWORK_NAME, 0);
let node1 = NodeLabel::node(NETWORK_NAME, 1);
let contract = SimOperation::create_test_contract(0x5C);
let contract_id = *contract.key().id();
let contract_key = contract.key();
register_crdt_contract(contract_id);
let state_v2 = SimOperation::create_crdt_state(2, 0x22);
let state_v1 = SimOperation::create_crdt_state(1, 0x11);
let mut sim =
rt.block_on(async { SimNetwork::new(NETWORK_NAME, 1, 1, 7, 3, 10, 2, SEED).await });
sim.enable_summary_first_put();
let operations = vec![
ScheduledOperation::new(
node1.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state_v2.clone(),
subscribe: true,
},
),
ScheduledOperation::new(
gateway.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state_v1.clone(),
subscribe: false,
},
),
];
let result = sim.run_controlled_simulation(
SEED,
operations,
Duration::from_secs(120),
Duration::from_secs(30),
);
assert!(
result.turmoil_result.is_ok(),
"summary-first PUT reverse-delta sim failed: {:?}",
result.turmoil_result.err()
);
let new_contract_sends = GlobalTestMetrics::put_probe_new_contract_sends();
let delta_sends = GlobalTestMetrics::put_probe_existing_mesh_delta_sends();
tracing::info!(
new_contract_sends,
delta_sends,
"summary-first PUT reverse-delta test: probe outcome counters"
);
assert_eq!(
new_contract_sends, 0,
"no-holder path must NOT fire: the gateway's PUT found a fresh holder \
(node1)"
);
assert_eq!(
delta_sends, 1,
"holder-found path must fire exactly once (the gateway probing node1)"
);
let node1_final_state = result
.node_storages
.get(&node1)
.and_then(|s| s.get_stored_state(&contract_key))
.expect("node1 should have a stored state for the contract");
let node1_version = u64::from_le_bytes(
node1_final_state.as_ref()[0..8]
.try_into()
.expect("CRDT state must have an 8-byte version prefix"),
);
assert_eq!(
node1_version, 2,
"the holder must stay at v2 (the originator's forward delta was for \
the older v1)"
);
let gateway_final_state = result
.node_storages
.get(&gateway)
.and_then(|s| s.get_stored_state(&contract_key))
.expect("the gateway (originator) should have a stored state for the contract");
let gateway_version = u64::from_le_bytes(
gateway_final_state.as_ref()[0..8]
.try_into()
.expect("CRDT state must have an 8-byte version prefix"),
);
assert_eq!(
gateway_version, 2,
"the originator (gateway) must converge to the full merged state (v2) \
by applying the holder's reverse delta in the SAME round-trip — it \
PUT v1 and would remain at v1 without the reverse leg"
);
freenet::dev_tool::clear_crdt_contracts();
}
const NN_LATTICE_SEEDS: [u64; 6] = [
0x4642_E0A0_5A1D,
0x4642_E0A0_0002,
0x4642_E0A0_0003,
0x4642_E0A0_0005,
0x4642_E0A0_0007,
0x4642_E0A0_000B,
];
fn nn_ring_signed(from: f64, to: f64) -> f64 {
let diff = to - from;
if diff > 0.5 {
diff - 1.0
} else if diff < -0.5 {
diff + 1.0
} else {
diff
}
}
fn nn_ring_dist(a: f64, b: f64) -> f64 {
let d = (a - b).abs();
d.min(1.0 - d)
}
fn nn_rank_of_loc(all_locs: &[f64], key_loc: f64, loc: f64) -> usize {
let d = nn_ring_dist(loc, key_loc);
all_locs
.iter()
.filter(|&&p| nn_ring_dist(p, key_loc) < d - 1e-12)
.count()
}
fn nn_node_locations(key_loc: f64, num_nodes: usize) -> Vec<f64> {
let wrap = |x: f64| x.rem_euclid(1.0);
(0..num_nodes)
.map(|i| wrap(key_loc + i as f64 / num_nodes as f64))
.collect()
}
struct NodeEdges {
has_succ: bool,
has_pred: bool,
long_link_dists: Vec<f64>,
}
fn nn_classify_edges(own: f64, nbrs: &[f64]) -> NodeEdges {
let mut nearest_succ: Option<f64> = None;
let mut nearest_pred: Option<f64> = None;
for &nb in nbrs {
let sd = nn_ring_signed(own, nb);
if sd > 0.0 {
if nearest_succ.is_none_or(|c: f64| sd.abs() < c) {
nearest_succ = Some(sd.abs());
}
} else if sd < 0.0 && nearest_pred.is_none_or(|c: f64| sd.abs() < c) {
nearest_pred = Some(sd.abs());
}
}
let mut long_link_dists = Vec::new();
let mut succ_removed = false;
let mut pred_removed = false;
for &nb in nbrs {
let sd = nn_ring_signed(own, nb);
let d = sd.abs();
if sd > 0.0 && !succ_removed && Some(d) == nearest_succ {
succ_removed = true;
continue;
}
if sd < 0.0 && !pred_removed && Some(d) == nearest_pred {
pred_removed = true;
continue;
}
long_link_dists.push(nn_ring_dist(own, nb));
}
NodeEdges {
has_succ: nearest_succ.is_some(),
has_pred: nearest_pred.is_some(),
long_link_dists,
}
}
fn nn_exact_nearest(own_idx: usize, locs: &[f64]) -> (Option<f64>, Option<f64>) {
let own = locs[own_idx];
let mut succ: Option<f64> = None;
let mut pred: Option<f64> = None;
let mut succ_d = f64::INFINITY;
let mut pred_d = f64::INFINITY;
for (i, &l) in locs.iter().enumerate() {
if i == own_idx {
continue;
}
let sd = nn_ring_signed(own, l);
if sd > 0.0 && sd < succ_d {
succ_d = sd;
succ = Some(l);
} else if sd < 0.0 && (-sd) < pred_d {
pred_d = -sd;
pred = Some(l);
}
}
(succ, pred)
}
struct NnTopologyRun {
exact_both_coverage: f64,
exact_both_count: usize,
both_edge_coverage: f64,
peers_total: usize,
get_find_rate: f64,
requesters_with_state: usize,
requesters_total: usize,
mean_found_hops: Option<f64>,
found_hop_samples: usize,
long_link_dists: Vec<f64>,
conn_min: usize,
conn_mean: f64,
conn_max: usize,
disconnect_events: usize,
holder_hosting: bool,
}
struct NnLatticeTestGuard;
impl Drop for NnLatticeTestGuard {
fn drop(&mut self) {
freenet::dev_tool::set_scatter_disabled(false);
freenet::dev_tool::set_nn_lattice_enabled(true);
freenet::dev_tool::set_nn_lattice_force_active(false);
}
}
fn nn_run_topology(
seed: u64,
nn_enabled: bool,
gateways: usize,
num_nodes: usize,
total_secs: u64,
settle_secs: u64,
) -> NnTopologyRun {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
setup_deterministic_state(seed);
freenet::dev_tool::set_scatter_disabled(true);
freenet::dev_tool::set_nn_lattice_enabled(nn_enabled);
freenet::dev_tool::clear_op_traces();
let _nn_guard = NnLatticeTestGuard;
let arm = if nn_enabled { "fix" } else { "stock" };
let network = format!("nn-topo-{arm}-{seed:x}-g{gateways}-n{num_nodes}");
let contract = SimOperation::create_test_contract(0xA5);
let contract_id = *contract.key().id();
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let state_v1 = SimOperation::create_test_state(1);
let node_locations = nn_node_locations(key_loc, num_nodes);
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
&network,
gateways,
num_nodes,
10, 7, 5, 2, seed,
&node_locations,
)
.await
});
let locs = sim.get_peer_locations();
let ranked = nodes_by_distance_to_key(&locs, num_nodes, key_loc);
let holder = NodeLabel::node(&network, ranked[0]);
let n_req = 8.min(num_nodes.saturating_sub(1));
let requester_nos: Vec<usize> = ranked[num_nodes - n_req..].to_vec();
let get_requesters: Vec<NodeLabel> = requester_nos
.iter()
.map(|n| NodeLabel::node(&network, *n))
.collect();
let mut operations = vec![ScheduledOperation::new(
holder.clone(),
SimOperation::SeedHostedContract {
contract: contract.clone(),
state: state_v1.clone(),
},
)];
for r in &get_requesters {
operations.push(ScheduledOperation::new(
r.clone(),
SimOperation::Get {
contract_id,
return_contract_code: true,
subscribe: false,
},
));
}
let logs_handle = sim.event_logs_handle();
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(total_secs),
Duration::from_secs(settle_secs),
);
assert!(
result.turmoil_result.is_ok(),
"seed={seed:x} nn={nn_enabled}: topology sim failed: {:?}",
result.turmoil_result.err()
);
let logs = rt.block_on(async { logs_handle.lock().await.clone() });
let traces = freenet::dev_tool::take_op_traces();
let metrics =
compute_piece_e_metrics(&result, &logs, &contract_key, &holder, &get_requesters, &[]);
let mut both_edge_count = 0usize;
let mut exact_both_count = 0usize;
let mut long_link_dists = Vec::new();
let mut conn_counts = Vec::new();
let peers_total = num_nodes + gateways;
let held = |nbrs: &[f64], target: Option<f64>| -> bool {
match target {
Some(t) => nbrs.iter().any(|&nb| nn_ring_dist(nb, t) < 1e-9),
None => true, }
};
for idx in 0..peers_total {
let label = if idx < gateways {
NodeLabel::gateway(&network, idx)
} else {
NodeLabel::node(&network, idx - gateways + 1)
};
let own = locs[idx];
let nbrs = result.node_neighbor_locations(&label);
let edges = nn_classify_edges(own, &nbrs);
if edges.has_succ && edges.has_pred {
both_edge_count += 1;
}
let (exact_succ, exact_pred) = nn_exact_nearest(idx, &locs);
if held(&nbrs, exact_succ) && held(&nbrs, exact_pred) {
exact_both_count += 1;
}
if idx >= gateways {
long_link_dists.extend(edges.long_link_dists);
conn_counts.push(result.node_open_connections(&label));
}
}
let both_edge_coverage = both_edge_count as f64 / peers_total as f64;
let exact_both_coverage = exact_both_count as f64 / peers_total as f64;
let conn_min = conn_counts.iter().copied().min().unwrap_or(0);
let conn_max = conn_counts.iter().copied().max().unwrap_or(0);
let conn_mean = if conn_counts.is_empty() {
0.0
} else {
conn_counts.iter().sum::<usize>() as f64 / conn_counts.len() as f64
};
let found_hops: Vec<usize> = traces
.iter()
.filter(|t| {
matches!(t.kind, freenet::dev_tool::ProbeOpKind::Get)
&& matches!(t.stop_reason, freenet::dev_tool::ProbeStopReason::Found)
})
.map(|t| t.hop_count)
.collect();
let mean_found_hops = if found_hops.is_empty() {
None
} else {
Some(found_hops.iter().sum::<usize>() as f64 / found_hops.len() as f64)
};
let disconnect_events = logs
.iter()
.filter(|l| matches!(l.kind, freenet::tracing::EventKind::Disconnected { .. }))
.count();
NnTopologyRun {
exact_both_coverage,
exact_both_count,
both_edge_coverage,
peers_total,
get_find_rate: metrics.client_findability_rate,
requesters_with_state: metrics.requesters_with_state,
requesters_total: metrics.requesters_total,
mean_found_hops,
found_hop_samples: found_hops.len(),
long_link_dists,
conn_min,
conn_mean,
conn_max,
disconnect_events,
holder_hosting: result.is_node_hosting(&holder, &contract_key),
}
}
fn nn_run_put_reach(
seed: u64,
nn_enabled: bool,
num_nodes: usize,
) -> (Option<usize>, usize, usize) {
use freenet::dev_tool::{Location, NodeLabel, ScheduledOperation, SimNetwork, SimOperation};
setup_deterministic_state(seed);
freenet::dev_tool::set_scatter_disabled(true);
freenet::dev_tool::set_nn_lattice_enabled(nn_enabled);
freenet::dev_tool::clear_op_traces();
let _nn_guard = NnLatticeTestGuard;
let arm = if nn_enabled { "fix" } else { "stock" };
let network = format!("nn-put-{arm}-{seed:x}-n{num_nodes}");
let contract = SimOperation::create_test_contract(0xA5);
let contract_key = contract.key();
let key_loc = Location::from(&contract_key).as_f64();
let state_v1 = SimOperation::create_test_state(1);
let node_locations = nn_node_locations(key_loc, num_nodes);
let rt = create_runtime();
let sim = rt.block_on(async {
SimNetwork::new_with_node_locations(
&network,
1,
num_nodes,
10,
7,
5,
2,
seed,
&node_locations,
)
.await
});
let locs = sim.get_peer_locations();
let ranked = nodes_by_distance_to_key(&locs, num_nodes, key_loc);
let origin_no = ranked[num_nodes - 1]; let origin = NodeLabel::node(&network, origin_no);
let origin_rank = nn_rank_of_loc(&locs, key_loc, locs[origin_no]);
let operations = vec![ScheduledOperation::new(
origin.clone(),
SimOperation::Put {
contract: contract.clone(),
state: state_v1.clone(),
subscribe: false,
},
)];
let result = sim.run_controlled_simulation(
seed,
operations,
Duration::from_secs(300),
Duration::from_secs(90),
);
assert!(
result.turmoil_result.is_ok(),
"seed={seed:x} nn={nn_enabled}: PUT-reach sim failed: {:?}",
result.turmoil_result.err()
);
let mut total_holders = 0usize;
let mut landing_loc: Option<f64> = None;
for (label, storage) in result.node_storages.iter() {
if storage.get_stored_state(&contract_key).is_some() {
total_holders += 1;
for idx in 0..locs.len() {
let matches_label = if idx == 0 {
*label == NodeLabel::gateway(&network, 0)
} else {
*label == NodeLabel::node(&network, idx)
};
if matches_label {
landing_loc = Some(locs[idx]);
}
}
}
}
let landing_rank = landing_loc.map(|l| nn_rank_of_loc(&locs, key_loc, l));
(landing_rank, total_holders, origin_rank)
}
#[test_log::test]
#[ignore = "heavy validation sweep (12 sim runs, ~15 min); cross-sim-sensitive, run serially: \
cargo test -p freenet --features simulation_tests,testing --test simulation_integration \
-- --ignored --test-threads=1 --nocapture test_nn_lattice"]
fn test_nn_lattice_cycle_completeness_control_vs_fix() {
const GW: usize = 1;
const N: usize = 15;
tracing::info!(target: "nn_lattice",
"===== EXACT-NEAREST COVERAGE ({} peers = {GW} gw + {N} nodes) stock vs fix =====", GW + N);
let mut stock_cov = Vec::new();
let mut fix_cov = Vec::new();
for seed in NN_LATTICE_SEEDS {
let stock = nn_run_topology(seed, false, GW, N, 300, 90);
let fix = nn_run_topology(seed, true, GW, N, 300, 90);
tracing::info!(target: "nn_lattice",
"0x{seed:012X} | stock exact {}/{} ({:.2}) any-edge {:.2} | \
fix exact {}/{} ({:.2}) any-edge {:.2} | conns stock[{}-{:.1}-{}] fix[{}-{:.1}-{}]",
stock.exact_both_count, stock.peers_total, stock.exact_both_coverage,
stock.both_edge_coverage,
fix.exact_both_count, fix.peers_total, fix.exact_both_coverage,
fix.both_edge_coverage,
stock.conn_min, stock.conn_mean, stock.conn_max,
fix.conn_min, fix.conn_mean, fix.conn_max);
assert!(
stock.holder_hosting && fix.holder_hosting,
"seed=0x{seed:X}: seed holder must host in both arms"
);
stock_cov.push(stock.exact_both_coverage);
fix_cov.push(fix.exact_both_coverage);
}
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
let mean_stock = mean(&stock_cov);
let mean_fix = mean(&fix_cov);
tracing::info!(target: "nn_lattice",
"MEAN exact-nearest coverage: stock={mean_stock:.3} fix={mean_fix:.3} delta={:+.3}",
mean_fix - mean_stock);
assert!(
mean_fix >= mean_stock - 0.05,
"fix exact-nearest coverage ({mean_fix:.3}) collapsed vs stock ({mean_stock:.3})"
);
}
#[test_log::test]
#[ignore = "heavy validation sweep (24 sim runs, ~17 min); cross-sim-sensitive, run serially: \
cargo test -p freenet --features simulation_tests,testing --test simulation_integration \
-- --ignored --test-threads=1 --nocapture test_nn_lattice"]
fn test_nn_lattice_findability_and_topology_control_vs_fix() {
const GW: usize = 1;
const N: usize = 15;
tracing::info!(target: "nn_lattice",
"===== PUT/GET REACH + TOPOLOGY ({} peers) stock vs fix =====", GW + N);
let mut put0_stock = 0usize;
let mut put0_fix = 0usize;
let mut put_valid_stock = 0usize;
let mut put_valid_fix = 0usize;
for seed in NN_LATTICE_SEEDS {
let (rs, hs, or_s) = nn_run_put_reach(seed, false, N);
let (rf, hf, or_f) = nn_run_put_reach(seed, true, N);
if hs == 1 {
put_valid_stock += 1;
if rs == Some(0) {
put0_stock += 1;
}
}
if hf == 1 {
put_valid_fix += 1;
if rf == Some(0) {
put0_fix += 1;
}
}
tracing::info!(target: "nn_lattice",
"PUT 0x{seed:012X} | stock origin_rank={or_s} land={rs:?} (holders={hs}) | \
fix origin_rank={or_f} land={rf:?} (holders={hf})");
}
tracing::info!(target: "nn_lattice",
"PUT lands at rank 0: stock {put0_stock}/{put_valid_stock} | fix {put0_fix}/{put_valid_fix}");
if put0_fix < put0_stock {
tracing::warn!(target: "nn_lattice",
"PUT-reach report: fix rank-0 {put0_fix}/{put_valid_fix} < stock \
{put0_stock}/{put_valid_stock} (report-only at max=5; tightening may regress here)");
}
let bucket = |dists: &[f64]| -> [usize; 4] {
let mut b = [0usize; 4];
for &d in dists {
let i = if d < 1.0 / 64.0 {
0
} else if d < 1.0 / 16.0 {
1
} else if d < 1.0 / 4.0 {
2
} else {
3
};
b[i] += 1;
}
b
};
let mut get_stock = Vec::new();
let mut get_fix = Vec::new();
let mut churn_stock = 0usize;
let mut churn_fix = 0usize;
for seed in NN_LATTICE_SEEDS {
let stock = nn_run_topology(seed, false, GW, N, 300, 90);
let fix = nn_run_topology(seed, true, GW, N, 300, 90);
tracing::info!(target: "nn_lattice",
"GET 0x{seed:012X} | stock find {}/{} ({:.3}) hops={:?}(n={}) | \
fix find {}/{} ({:.3}) hops={:?}(n={}) | disc stock={} fix={} | \
longlink-buckets stock={:?} fix={:?}",
stock.requesters_with_state, stock.requesters_total, stock.get_find_rate,
stock.mean_found_hops, stock.found_hop_samples,
fix.requesters_with_state, fix.requesters_total, fix.get_find_rate,
fix.mean_found_hops, fix.found_hop_samples,
stock.disconnect_events, fix.disconnect_events,
bucket(&stock.long_link_dists), bucket(&fix.long_link_dists));
get_stock.push(stock.get_find_rate);
get_fix.push(fix.get_find_rate);
churn_stock += stock.disconnect_events;
churn_fix += fix.disconnect_events;
}
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
let mean_get_stock = mean(&get_stock);
let mean_get_fix = mean(&get_fix);
tracing::info!(target: "nn_lattice",
"MEAN GET find-rate: stock={mean_get_stock:.3} fix={mean_get_fix:.3} delta={:+.3} | \
total disconnect events stock={churn_stock} fix={churn_fix}",
mean_get_fix - mean_get_stock);
if mean_get_fix < mean_get_stock - 0.10 {
tracing::warn!(target: "nn_lattice",
"GET find-rate report: fix {mean_get_fix:.3} vs stock {mean_get_stock:.3} \
(report-only at max=5; tightening expected to regress here)");
}
if churn_fix > churn_stock * 3 + 50 {
tracing::warn!(target: "nn_lattice",
"churn report: fix disconnects={churn_fix} vs stock {churn_stock} \
(report-only at max=5; tightening churns more)");
}
}
#[test_log::test]
#[ignore = "heavy larger-ring validation (6 × 48-peer sim runs); harness-limited, run manually: \
cargo test -p freenet --features simulation_tests,testing --test simulation_integration \
-- --ignored --test-threads=1 --nocapture test_nn_lattice_larger_ring"]
fn test_nn_lattice_larger_ring_report() {
const GW: usize = 3;
const N: usize = 45;
let seeds = &NN_LATTICE_SEEDS[..3];
tracing::info!(target: "nn_lattice",
"===== LARGER RING ({} peers = {GW} gw + {N} nodes, longer settle) stock vs fix =====", GW + N);
let mut formed = true;
for &seed in seeds {
let stock = nn_run_topology(seed, false, GW, N, 480, 180);
let fix = nn_run_topology(seed, true, GW, N, 480, 180);
tracing::info!(target: "nn_lattice",
"0x{seed:012X} | stock exact {:.2} any {:.2} find {:.3} conns[{}-{:.1}-{}] | \
fix exact {:.2} any {:.2} find {:.3} conns[{}-{:.1}-{}] disc={}",
stock.exact_both_coverage, stock.both_edge_coverage, stock.get_find_rate,
stock.conn_min, stock.conn_mean, stock.conn_max,
fix.exact_both_coverage, fix.both_edge_coverage, fix.get_find_rate,
fix.conn_min, fix.conn_mean, fix.conn_max, fix.disconnect_events);
if fix.conn_mean < 2.0 || stock.conn_mean < 2.0 {
formed = false;
}
}
assert!(
formed,
"48-peer harness did not form a topology (mean connections < 2) — larger-ring \
numbers are not meaningful; treat as a harness limitation, not a result"
);
}