freenet/node/testing_impl.rs
1//! Simulation network implementation for testing Freenet nodes.
2//!
3//! This module provides `SimNetwork`, a fully in-memory simulation framework for testing
4//! Freenet's peer-to-peer network behavior. It enables deterministic testing of complex
5//! distributed scenarios without real network I/O.
6//!
7//! # Key Components
8//!
9//! - [`SimNetwork`]: The main simulation controller that manages virtual nodes
10//! - [`NodeLabel`]: Identifies nodes in the simulation (gateways and regular nodes)
11//! - [`FaultConfig`]: Configures fault injection (message loss, partitions, latency)
12//! - [`VirtualTime`]: Controls deterministic time progression
13//!
14//! # Features
15//!
16//! - **Deterministic execution**: All randomness is seeded for reproducible tests
17//! - **Fault injection**: Simulate message loss, network partitions, and node crashes
18//! - **Virtual time**: Control time progression for testing time-dependent behavior
19//! - **Node lifecycle**: Crash and restart nodes while preserving identity
20//! - **Network-scoped state**: Each simulation is isolated from others
21//!
22//! # Example
23//!
24//! ```ignore
25//! use freenet::dev_tool::SimNetwork;
26//! use freenet::simulation::FaultConfig;
27//!
28//! // Create a 3-node network with 1 gateway
29//! let mut sim = SimNetwork::new(
30//! "my-test",
31//! 1, // gateways
32//! 2, // regular nodes
33//! 7, // ring_max_htl
34//! 3, // rnd_if_htl_above
35//! 10, // max_connections
36//! 2, // min_connections
37//! 42, // seed for determinism
38//! ).await;
39//!
40//! // Start the network
41//! let handles = sim.start_with_rand_gen::<rand::rngs::SmallRng>(42, 1, 1).await;
42//!
43//! // Inject 10% message loss
44//! sim.with_fault_injection(FaultConfig::builder()
45//! .message_loss_rate(0.1)
46//! .build());
47//!
48//! // Advance virtual time to trigger pending message deliveries
49//! sim.advance_time(Duration::from_millis(100));
50//! ```
51//!
52//! # Network Isolation
53//!
54//! Each `SimNetwork` instance is identified by its name and maintains isolated state.
55//! This allows running multiple independent simulations in parallel tests without
56//! interference. The fault injection state is automatically cleaned up when the
57//! simulation is dropped.
58//!
59//! # Thread Safety
60//!
61//! The simulation is thread-safe and can be used with `#[tokio::test]` multi-threaded
62//! test configurations. Internal state is protected by appropriate synchronization
63//! primitives.
64
65use freenet_stdlib::prelude::*;
66use futures::Future;
67use rand::prelude::IndexedRandom;
68use std::{
69 collections::{BTreeMap, HashMap, HashSet},
70 net::{Ipv6Addr, SocketAddr},
71 num::NonZeroUsize,
72 pin::Pin,
73 sync::Arc,
74 time::Duration,
75};
76use tokio::sync::{broadcast, mpsc, watch};
77use tracing::info;
78
79#[cfg(feature = "trace-ot")]
80use crate::tracing::CombinedRegister;
81use crate::{
82 client_events::test::{MemoryEventsGen, RandomEventGenerator},
83 config::{ConfigArgs, GlobalExecutor, GlobalRng},
84 dev_tool::TransportKeypair,
85 node::{InitPeerNode, NetEventRegister, NodeConfig},
86 ring::{ConnectionManager, Distance, Location, PeerKeyLocation},
87 simulation::{FaultConfig, VirtualTime},
88 tracing::TestEventListener,
89 transport::{
90 TransportPublicKey,
91 in_memory_socket::{register_network_time_source, unregister_network_time_source},
92 },
93};
94
95mod in_memory;
96mod network;
97pub mod turmoil_runner;
98
99pub use self::network::{NetworkPeer, PeerMessage, PeerStatus};
100pub use self::turmoil_runner::{TurmoilConfig, TurmoilResult, run_turmoil_simulation};
101
102pub(crate) type EventId = u32;
103
104/// A controlled operation to execute on a specific node during simulation.
105///
106/// `SimOperation` allows tests to specify exact operations instead of relying
107/// on random event generation. This enables precise testing of specific scenarios
108/// like subscription topology formation.
109///
110/// # Usage
111///
112/// ```ignore
113/// use freenet::dev_tool::{SimNetwork, SimOperation, NodeLabel};
114///
115/// let mut sim = SimNetwork::new("test", 1, 3, ...).await;
116///
117/// // Create a contract and have specific nodes subscribe
118/// let contract = SimOperation::create_test_contract(42);
119/// let operations = vec![
120/// (NodeLabel::gateway("test", 0), SimOperation::Put {
121/// contract: contract.clone(),
122/// state: vec![1, 2, 3],
123/// subscribe: true,
124/// }),
125/// (NodeLabel::node("test", 0), SimOperation::Subscribe {
126/// contract_id: *contract.key().id(),
127/// }),
128/// ];
129///
130/// let handles = sim.start_with_controlled_events(operations).await;
131/// ```
132#[derive(Clone, Debug)]
133pub enum SimOperation {
134 /// PUT a new contract with initial state.
135 Put {
136 /// The contract container (code + parameters)
137 contract: ContractContainer,
138 /// Initial state bytes
139 state: Vec<u8>,
140 /// Whether to subscribe to updates after PUT
141 subscribe: bool,
142 },
143 /// GET a contract's current state.
144 Get {
145 /// The contract instance ID to retrieve
146 contract_id: ContractInstanceId,
147 /// Whether to return the contract code
148 return_contract_code: bool,
149 /// Whether to subscribe to updates after GET
150 subscribe: bool,
151 },
152 /// SUBSCRIBE to a contract's updates.
153 Subscribe {
154 /// The contract instance ID to subscribe to
155 contract_id: ContractInstanceId,
156 },
157 /// UPDATE a contract's state.
158 Update {
159 /// The contract key to update
160 key: ContractKey,
161 /// New state data
162 data: Vec<u8>,
163 },
164 /// Seed a contract into a node's local store WITHOUT network propagation.
165 ///
166 /// Unlike `Put`, this does not trigger a network PUT operation — the
167 /// contract is only stored in the target node's `MockStateStorage`.
168 /// This is useful for setting up test scenarios where specific nodes
169 /// must have (or not have) a contract before subscribe/get operations.
170 ///
171 /// Example: seed only on gateway, then subscribe from other nodes.
172 /// The subscribe will go through the network because other nodes lack
173 /// the contract, and relay peers will forward (generating ForwardingAck).
174 SeedContract {
175 /// The contract container (code + parameters)
176 contract: ContractContainer,
177 /// Initial state bytes
178 state: Vec<u8>,
179 },
180 /// Seed a contract into a node's local store AND register the node as
181 /// genuinely HOSTING it (state + `host_contract` + active subscription),
182 /// without any network propagation.
183 ///
184 /// Unlike [`SeedContract`](Self::SeedContract) — which only writes the raw
185 /// state/params into `MockStateStorage` — this routes the contract through
186 /// the node's startup `append_contracts` path with `subscription = true`,
187 /// so `ring.is_hosting_contract` / `ring.hosting_contract_keys` report it.
188 /// That is what makes the node a valid migration source: the
189 /// placement-migration trigger only nudges contracts the node actually
190 /// hosts. Use this when a test needs a node to be the authoritative holder
191 /// of a contract that was never PUT through the network (e.g. reproducing
192 /// the GET-dead-end placement gap, #4404).
193 SeedHostedContract {
194 /// The contract container (code + parameters)
195 contract: ContractContainer,
196 /// Initial state bytes
197 state: Vec<u8>,
198 },
199 /// Seed a **demandless every-hop copy** into a node's local store: state
200 /// present, `is_hosting_contract` true, `has_local_interest` true, but NO
201 /// subscription (`is_receiving_updates` false) and NO prior local client
202 /// access.
203 ///
204 /// This is the state a production every-hop GET/PUT store leaves on a RELAY
205 /// hop (`get`/`put` `op_ctx_task` call `register_local_hosting` without
206 /// `ring.subscribe` and, on a non-originator hop, without
207 /// `mark_local_client_access`). Unlike
208 /// [`SeedHostedContract`](Self::SeedHostedContract) — which registers an
209 /// active subscription, so the pre-serve-DURING gate already served it — a
210 /// demandless copy is exactly what the pre-serve-DURING originator gate
211 /// (`is_hosting && has_local_client_access`) would REFUSE to serve, forcing
212 /// a whole GET through the network for a copy the node already held fresh.
213 /// Use this to exercise serve-DURING (#4642 R3 piece C): a connected node
214 /// holding a demandless copy must answer a GET from its local copy instead
215 /// of going dark on the network.
216 SeedDemandlessCopy {
217 /// The contract container (code + parameters)
218 contract: ContractContainer,
219 /// Initial state bytes
220 state: Vec<u8>,
221 },
222 /// Simulate a client disconnecting from this node.
223 ///
224 /// Emits `ClientRequest::Disconnect` through the same
225 /// `MemoryEventsGen` → `client_event_handling` path the real
226 /// WebSocket client takes, so the node runs
227 /// `remove_client_from_all_subscriptions`,
228 /// `should_unsubscribe_upstream`, and `send_unsubscribe_upstream`
229 /// — producing `UnsubscribeSent` / `UnsubscribeReceived` telemetry
230 /// that can be asserted on.
231 ///
232 /// This targets `ClientId::FIRST`, which is the client id used by
233 /// every other `SimOperation` in the same node, so subscriptions
234 /// issued earlier by this operation's node are the ones cleaned up.
235 Disconnect,
236 /// Advance the sim's controllable hosting clock by `duration`, in-order
237 /// with the surrounding events.
238 ///
239 /// Requires [`SimNetwork::enable_hosting_time_control`]; the clock is shared
240 /// by every node, so the `node` on the [`ScheduledOperation`] is only a
241 /// sequencing placeholder (advancement is global). Use this to jump past the
242 /// hosting-cache TTL gate deterministically so a subsequent access triggers
243 /// demand-driven eviction — without running minutes of virtual time
244 /// (#4642 piece A). Handled specially by `run_controlled_simulation`
245 /// (it is not a client request).
246 AdvanceHostingClock {
247 /// How far to advance the shared hosting clock.
248 duration: Duration,
249 },
250 /// Crash the target node mid-run by blocking all its inbound/outbound
251 /// messages via the per-network fault injector, in-order with the
252 /// surrounding events.
253 ///
254 /// This is a SILENT crash (message-blocking, like the churn driver's
255 /// soft-crash): the node's transport is not torn down, so peers detect the
256 /// loss on their own routing-health cadence rather than an immediate
257 /// transport-close. It lets a test remove a specific peer at a scripted
258 /// point (e.g. to observe re-routing / re-subscription). Handled specially
259 /// by `run_controlled_simulation` (it is not a client request).
260 CrashNode,
261 /// Recover a previously [`CrashNode`](Self::CrashNode)-crashed node mid-run,
262 /// un-blocking its inbound/outbound messages via the per-network fault
263 /// injector, in-order with the surrounding events.
264 ///
265 /// The inverse of [`CrashNode`](Self::CrashNode): the node was never torn
266 /// down (message-blocking crash), so recovering simply lets its traffic flow
267 /// again. Use it to reconnect a node after it has deliberately missed some
268 /// traffic (e.g. an update fan-out), so a test can observe it re-converge via
269 /// anti-entropy. Handled specially by `run_controlled_simulation` (it is not
270 /// a client request).
271 RecoverNode,
272}
273
274impl SimOperation {
275 /// Creates a deterministic test contract from a seed.
276 ///
277 /// The contract code and parameters are derived from the seed,
278 /// making it reproducible across test runs.
279 pub fn create_test_contract(seed: u8) -> ContractContainer {
280 let mut code_bytes = vec![0u8; 32];
281 let mut params_bytes = vec![0u8; 16];
282
283 // Fill with deterministic bytes based on seed
284 for (i, byte) in code_bytes.iter_mut().enumerate() {
285 *byte = seed.wrapping_add(i as u8);
286 }
287 for (i, byte) in params_bytes.iter_mut().enumerate() {
288 *byte = seed.wrapping_add(i as u8).wrapping_mul(2);
289 }
290
291 let code = ContractCode::from(code_bytes);
292 let params = Parameters::from(params_bytes);
293 ContractWasmAPIVersion::V1(WrappedContract::new(code.into(), params)).into()
294 }
295
296 /// Creates a large deterministic state for streaming tests.
297 ///
298 /// `size_bytes` controls total state size (use > streaming_threshold to trigger streaming).
299 /// The content is deterministic based on `seed` for reproducibility.
300 pub fn create_large_state(size_bytes: usize, seed: u8) -> Vec<u8> {
301 let mut state = Vec::with_capacity(size_bytes);
302 for i in 0..size_bytes {
303 state.push(seed.wrapping_add((i % 256) as u8).wrapping_mul(3));
304 }
305 state
306 }
307
308 /// Creates a deterministic test state from a seed.
309 pub fn create_test_state(seed: u8) -> Vec<u8> {
310 let mut state_bytes = vec![0u8; 64];
311 for (i, byte) in state_bytes.iter_mut().enumerate() {
312 *byte = seed.wrapping_add(i as u8).wrapping_mul(3);
313 }
314 state_bytes
315 }
316
317 /// Creates a CRDT-mode test state with version prefix.
318 ///
319 /// Format: [version: u64 LE][64 bytes of data]
320 ///
321 /// Use this with `register_crdt_contract()` to test version-aware delta handling.
322 /// The CRDT mode enables testing of PR #2763's summary caching fix by:
323 /// - Using version-aware summaries (version + hash)
324 /// - Computing version-specific deltas
325 /// - Failing delta application when versions don't match
326 pub fn create_crdt_state(version: u64, seed: u8) -> Vec<u8> {
327 // State must be > 80 bytes for delta to be "efficient"
328 // (efficiency check: summary_size * 2 < state_size, where summary = 40 bytes)
329 // Using 128 bytes of data for total state size of 136 bytes
330 let mut state_bytes = Vec::with_capacity(8 + 128);
331 // Add version prefix
332 state_bytes.extend_from_slice(&version.to_le_bytes());
333 // Add data (128 bytes to make delta efficient)
334 for i in 0..128u8 {
335 state_bytes.push(seed.wrapping_add(i).wrapping_mul(3));
336 }
337 state_bytes
338 }
339
340 /// Converts this operation to a ClientRequest.
341 #[cfg(any(test, feature = "testing"))]
342 pub(crate) fn into_client_request(self) -> freenet_stdlib::client_api::ClientRequest<'static> {
343 use freenet_stdlib::client_api::{ClientRequest, ContractRequest};
344
345 match self {
346 SimOperation::Put {
347 contract,
348 state,
349 subscribe,
350 } => ClientRequest::ContractOp(ContractRequest::Put {
351 contract,
352 state: WrappedState::new(state),
353 related_contracts: RelatedContracts::new(),
354 subscribe,
355 blocking_subscribe: false,
356 }),
357 SimOperation::Get {
358 contract_id,
359 return_contract_code,
360 subscribe,
361 } => ClientRequest::ContractOp(ContractRequest::Get {
362 key: contract_id,
363 return_contract_code,
364 subscribe,
365 blocking_subscribe: false,
366 }),
367 SimOperation::Subscribe { contract_id } => {
368 ClientRequest::ContractOp(ContractRequest::Subscribe {
369 key: contract_id,
370 summary: None,
371 })
372 }
373 SimOperation::Update { key, data } => {
374 ClientRequest::ContractOp(ContractRequest::Update {
375 key,
376 data: UpdateData::State(State::from(data)),
377 })
378 }
379 SimOperation::SeedContract { .. } => {
380 panic!(
381 "SeedContract is not a client request — it must be handled \
382 by run_controlled_simulation before event dispatch"
383 )
384 }
385 SimOperation::SeedHostedContract { .. } => {
386 panic!(
387 "SeedHostedContract is not a client request — it must be handled \
388 by run_controlled_simulation before event dispatch"
389 )
390 }
391 SimOperation::SeedDemandlessCopy { .. } => {
392 panic!(
393 "SeedDemandlessCopy is not a client request — it must be handled \
394 by run_controlled_simulation before event dispatch"
395 )
396 }
397 SimOperation::AdvanceHostingClock { .. } => {
398 panic!(
399 "AdvanceHostingClock is not a client request — it must be handled \
400 by run_controlled_simulation during event dispatch"
401 )
402 }
403 SimOperation::CrashNode => {
404 panic!(
405 "CrashNode is not a client request — it must be handled \
406 by run_controlled_simulation during event dispatch"
407 )
408 }
409 SimOperation::RecoverNode => {
410 panic!(
411 "RecoverNode is not a client request — it must be handled \
412 by run_controlled_simulation during event dispatch"
413 )
414 }
415 SimOperation::Disconnect => ClientRequest::Disconnect { cause: None },
416 }
417 }
418}
419
420/// A scheduled operation for controlled event simulation.
421#[derive(Clone, Debug)]
422pub struct ScheduledOperation {
423 /// Which node should execute this operation
424 pub node: NodeLabel,
425 /// The operation to execute
426 pub operation: SimOperation,
427}
428
429impl ScheduledOperation {
430 /// Create a new scheduled operation.
431 pub fn new(node: NodeLabel, operation: SimOperation) -> Self {
432 Self { node, operation }
433 }
434}
435
436/// Packet-delivery decision consulted by every `SimulationSocket` (via the
437/// global delivery callback installed by `run_controlled_simulation` and
438/// `run_simulation_direct`) so an injected node crash or partition actually
439/// drops packets instead of being a silent no-op.
440///
441/// Per-network: it looks up the fault injector for the socket's own network and,
442/// only if that network opted in (`enforce_fault_drops`), DROPS any packet whose
443/// source or destination is a crashed node (counted in
444/// `stats.messages_dropped_crash`) or that is blocked by an active partition
445/// (counted in `stats.messages_dropped_partition`). Every other packet (and
446/// every network that did not opt in) is delivered unchanged, so installing this
447/// callback cannot alter fault behavior for any path that did not opt in.
448/// See #4642 piece F (scripted crashes) and #4694 (direct-runner churn).
449#[cfg(any(test, feature = "testing"))]
450fn fault_injection_delivery_decision(
451 network_name: &str,
452 from: SocketAddr,
453 to: SocketAddr,
454) -> crate::transport::in_memory_socket::PacketDeliveryDecision {
455 use crate::transport::in_memory_socket::PacketDeliveryDecision;
456 if let Some(injector) = crate::node::network_bridge::get_fault_injector(network_name) {
457 let mut inj = injector.lock().unwrap();
458 if inj.enforce_fault_drops {
459 if inj.config.is_crashed(&from) || inj.config.is_crashed(&to) {
460 inj.stats.messages_dropped_crash += 1;
461 return PacketDeliveryDecision::Drop;
462 }
463 // Partitions are time-scoped (start/heal), so use this network's
464 // VirtualTime as the current time; without VirtualTime a partition
465 // cannot be evaluated deterministically, so treat it as inactive.
466 use crate::simulation::TimeSource;
467 if let Some(now) = inj.virtual_time.as_ref().map(|vt| vt.now_nanos()) {
468 if inj.config.is_partitioned(&from, &to, now) {
469 inj.stats.messages_dropped_partition += 1;
470 return PacketDeliveryDecision::Drop;
471 }
472 }
473 }
474 }
475 PacketDeliveryDecision::Deliver
476}
477
478/// Result of a controlled simulation, including topology snapshots.
479///
480/// This struct captures the simulation result along with topology snapshots
481/// taken at the end of the simulation (before cleanup). This allows tests
482/// to validate subscription topology after the simulation completes.
483pub struct ControlledSimulationResult {
484 /// The Turmoil simulation result
485 pub turmoil_result: turmoil::Result,
486 /// Topology snapshots captured at the end of simulation
487 pub topology_snapshots: Vec<crate::ring::topology_registry::TopologySnapshot>,
488 /// Shared storage handles for each node, keyed by NodeLabel.
489 /// These are clones of the Arc-backed storages passed into Turmoil,
490 /// so they reflect all state stored during the simulation.
491 pub node_storages: HashMap<NodeLabel, crate::wasm_runtime::MockStateStorage>,
492 /// Live `Arc<Ring>` for each node that started, keyed by NodeLabel.
493 /// Captured via the `shared_ring` slots so governance sim tests can
494 /// read `ring.contract_ban_list.is_banned(..)` after the simulation
495 /// (the `SimNetwork` itself is consumed by `run_controlled_simulation`).
496 /// Only present under `cfg(test)`/`testing`.
497 ///
498 /// `allow(dead_code)`: read only by the `cfg(test)` governance e2e
499 /// module, so non-test builds with the `testing` feature see it unused.
500 #[cfg(any(test, feature = "testing"))]
501 #[allow(dead_code)]
502 pub(crate) node_rings: HashMap<NodeLabel, Arc<crate::ring::Ring>>,
503 /// Per-peer subscription-renewal metrics captured at the end of the
504 /// simulation, keyed by socket address. Captured here (before the
505 /// `SimNetwork` is dropped, which clears the registry) so renewal-storm
506 /// tests can read `wire_attempts` / `terminus_satisfied` after the run
507 /// returns. See `crate::ring::topology_registry::RenewalMetrics` (#4440).
508 pub renewal_metrics:
509 HashMap<std::net::SocketAddr, crate::ring::topology_registry::RenewalMetrics>,
510 /// Number of packets DROPPED because their source or destination was a
511 /// crashed node (`SimOperation::CrashNode`), captured from the fault
512 /// injector's `stats.messages_dropped_crash` before the `SimNetwork` is
513 /// dropped. A discriminating signal that a scripted crash actually took
514 /// effect: it is `> 0` only if a crash was scheduled AND real traffic
515 /// to/from the crashed node was subsequently blocked. `0` when no crash was
516 /// scheduled. See #4642 piece F.
517 pub crash_packets_dropped: u64,
518 /// Per-peer count of WASM `summarize_state` invocations (summary-cache
519 /// SLOW-path misses) captured at the end of the run, keyed by socket
520 /// address. Captured here (before the `SimNetwork` is dropped, which clears
521 /// the registry) so the every-hop-placement summarize-storm falsifier can
522 /// assert this stays flat as the hosted set / neighbor overlap grows. A
523 /// cache hit does NOT increment, so a working cache keeps this proportional
524 /// to the state-change rate, not to hosted-set size. See
525 /// `crate::ring::topology_registry::record_summarize_wasm_call` (#4440, spec
526 /// step 8).
527 pub summarize_wasm_calls: HashMap<std::net::SocketAddr, u64>,
528}
529
530#[cfg(any(test, feature = "testing"))]
531impl ControlledSimulationResult {
532 /// Whether `label`'s node was hosting `key` in its live Ring at the end of
533 /// the simulation.
534 ///
535 /// Reads the per-node `Ring` captured via the `shared_ring` slots, so it
536 /// reflects `ring.is_hosting_contract` exactly (not a storage proxy). Used
537 /// by placement/migration tests to assert a contract has migrated onto a
538 /// given peer. Returns `false` if the node never started or never published
539 /// its Ring.
540 pub fn is_node_hosting(&self, label: &NodeLabel, key: &ContractKey) -> bool {
541 self.node_rings
542 .get(label)
543 .is_some_and(|ring| ring.is_hosting_contract(key))
544 }
545
546 /// The protocol version `label`'s node had recorded for the peer at `addr`
547 /// at the end of the run, or `None` if it never learned one (#5161).
548 ///
549 /// Reads the same `ConnectionManager` mirror the production emission gates
550 /// read, so it answers the question those gates ask rather than a proxy for
551 /// it. `None` when the node never published its Ring.
552 pub fn node_recorded_remote_version(
553 &self,
554 label: &NodeLabel,
555 addr: SocketAddr,
556 ) -> Option<(u8, u8, u16)> {
557 self.node_rings
558 .get(label)
559 .and_then(|ring| ring.connection_manager.remote_version(addr))
560 }
561
562 /// Whether `label`'s node would emit the hash-first summary encoding to the
563 /// peer at `addr` — i.e. whether that link passes the version gate (#4965).
564 ///
565 /// The end-to-end consequence of #5161 on a node->gateway link: before the
566 /// version-carrying ack this could never be true from the node's side, so
567 /// the node answered every anti-entropy exchange with full summary bytes.
568 /// `false` when the node never published its Ring.
569 pub fn node_supports_hash_first_summaries(&self, label: &NodeLabel, addr: SocketAddr) -> bool {
570 self.node_rings
571 .get(label)
572 .is_some_and(|ring| ring.connection_manager.supports_hash_first_summaries(addr))
573 }
574
575 /// Number of contracts `label`'s node held in its hosting cache at the end
576 /// of the run (its "cache size"). Returns 0 if the node never published its
577 /// Ring. See [`Ring::hosting_contracts_count`](crate::ring::Ring::hosting_contracts_count).
578 pub fn node_hosting_count(&self, label: &NodeLabel) -> usize {
579 self.node_rings
580 .get(label)
581 .map(|ring| ring.hosting_contracts_count())
582 .unwrap_or(0)
583 }
584
585 /// Number of active network subscription leases `label`'s node held at the
586 /// end of the run. Returns 0 if the node never published its Ring. See
587 /// [`Ring::active_subscription_count`](crate::ring::Ring::active_subscription_count).
588 pub fn node_subscription_count(&self, label: &NodeLabel) -> usize {
589 self.node_rings
590 .get(label)
591 .map(|ring| ring.active_subscription_count())
592 .unwrap_or(0)
593 }
594
595 /// Number of contracts `label`'s node had *real demand* for (local client
596 /// or downstream subscriber, EXCLUDING cache-only hosting) at the end of the
597 /// run. This is the denominator for the #3763 no-storm assertion — see
598 /// [`Ring::active_demand_count`](crate::ring::Ring::active_demand_count).
599 ///
600 /// Returns `None` when the count is UNMEASURABLE — the node never published
601 /// its Ring, or its `OpManager` was detached (startup/shutdown). A no-storm
602 /// assertion MUST distinguish `None` (unmeasurable) from `Some(0)`
603 /// ("attached, genuinely no demand"): treating a torn-down node's `None` as
604 /// `0` would let a real storm read as "no demand" and pass falsely. Callers
605 /// that only log can `{:?}` it; callers that assert should handle `None`
606 /// explicitly (e.g. skip the node, or fail if a measurement was required).
607 pub fn node_active_demand_count(&self, label: &NodeLabel) -> Option<usize> {
608 self.node_rings
609 .get(label)
610 .and_then(|ring| ring.active_demand_count())
611 }
612
613 /// Number of upstream peers `label`'s node recorded for `key` (its parents
614 /// in the subscription tree) at the end of the run. Used by re-subscribe
615 /// tests (#4642 piece F) to assert a KNOWN subscriber→upstream edge. See
616 /// [`Ring::upstream_interest_count`](crate::ring::Ring::upstream_interest_count).
617 ///
618 /// Returns `None` when unmeasurable (node never published its Ring, or its
619 /// `OpManager` was detached) — distinct from `Some(0)` ("attached, no
620 /// upstream edge recorded"). A piece-F re-root assertion must not read a
621 /// detached node's `None` as "edge gone".
622 pub fn node_upstream_count(&self, label: &NodeLabel, key: &ContractKey) -> Option<usize> {
623 self.node_rings
624 .get(label)
625 .and_then(|ring| ring.upstream_interest_count(key))
626 }
627
628 /// Labels of every node whose live Ring was captured (started and published
629 /// its Ring). Handy for iterating per-node assertions.
630 pub fn captured_node_labels(&self) -> Vec<NodeLabel> {
631 self.node_rings.keys().cloned().collect()
632 }
633
634 /// Number of open ring connections `label`'s node held at the end of the run.
635 /// Returns 0 if the node never published its Ring. Used by the serve-DURING
636 /// falsifier to confirm the serving node was CONNECTED (so a local serve is
637 /// attributable to serve-DURING interest, not the isolated-node fallback).
638 pub fn node_open_connections(&self, label: &NodeLabel) -> usize {
639 self.node_rings
640 .get(label)
641 .map(|ring| ring.open_connections())
642 .unwrap_or(0)
643 }
644
645 /// Ring locations (as `f64`) of `label`'s connected neighbors at the end of
646 /// the run, read from the captured live `Ring`'s connection manager. Empty
647 /// if the node never published its Ring. Used by the nearest-neighbor
648 /// findability experiment to premise-check that the guaranteed
649 /// successor/predecessor edges actually formed. Only present under
650 /// `cfg(test)`/`testing`.
651 #[cfg(any(test, feature = "testing"))]
652 pub fn node_neighbor_locations(&self, label: &NodeLabel) -> Vec<f64> {
653 self.node_rings
654 .get(label)
655 .map(|ring| {
656 ring.connection_manager
657 .get_connections_by_location()
658 .keys()
659 .map(|loc| loc.as_f64())
660 .collect()
661 })
662 .unwrap_or_default()
663 }
664
665 /// Whether `label`'s node was actively receiving updates for `key` (has a
666 /// live network/client subscription keeping the copy fresh) at the end of the
667 /// run. Returns `false` if the node never published its Ring. The serve-DURING
668 /// falsifier asserts this is `false` for a demandless copy, so the local serve
669 /// is attributable to `has_local_interest`, not an active subscription.
670 pub fn node_is_receiving_updates(&self, label: &NodeLabel, key: &ContractKey) -> bool {
671 self.node_rings
672 .get(label)
673 .is_some_and(|ring| ring.is_receiving_updates(key))
674 }
675
676 /// Number of client GETs `label`'s node answered from local hosted state (A3
677 /// serve-DURING hit counter). Returns 0 if the node never published its Ring.
678 /// See [`Ring::local_get_serves`](crate::ring::Ring::local_get_serves).
679 pub fn node_local_get_serves(&self, label: &NodeLabel) -> u64 {
680 self.node_rings
681 .get(label)
682 .map(|ring| ring.local_get_serves())
683 .unwrap_or(0)
684 }
685
686 /// Number of client GETs `label`'s node routed to the network (A3 forward/miss
687 /// counter). Returns 0 if the node never published its Ring. The serve-DURING
688 /// falsifier asserts this stays 0 for a demandless-copy GET (never went dark).
689 /// See [`Ring::local_get_forwards`](crate::ring::Ring::local_get_forwards).
690 pub fn node_local_get_forwards(&self, label: &NodeLabel) -> u64 {
691 self.node_rings
692 .get(label)
693 .map(|ring| ring.local_get_forwards())
694 .unwrap_or(0)
695 }
696
697 /// Subscription-renewal metrics for the peer at `addr` (captured before the
698 /// `SimNetwork` was dropped). Returns `RenewalMetrics::default()` (all zero)
699 /// if the peer recorded no renewal activity. See #4440.
700 pub fn renewal_metrics_for(
701 &self,
702 addr: &std::net::SocketAddr,
703 ) -> crate::ring::topology_registry::RenewalMetrics {
704 self.renewal_metrics.get(addr).copied().unwrap_or_default()
705 }
706
707 /// Sum of the renewal metrics across every peer in the simulation. See #4440.
708 pub fn aggregate_renewal_metrics(&self) -> crate::ring::topology_registry::RenewalMetrics {
709 self.renewal_metrics.values().fold(
710 crate::ring::topology_registry::RenewalMetrics::default(),
711 |mut acc, m| {
712 acc.wire_attempts += m.wire_attempts;
713 acc.terminus_satisfied += m.terminus_satisfied;
714 // Per-node peak → aggregate as the max across peers.
715 acc.max_cycle_batch = acc.max_cycle_batch.max(m.max_cycle_batch);
716 acc.max_reroot_batch = acc.max_reroot_batch.max(m.max_reroot_batch);
717 acc
718 },
719 )
720 }
721
722 /// Number of packets dropped because a `SimOperation::CrashNode` marked
723 /// their source or destination crashed. `> 0` proves a scripted crash
724 /// actually blocked real traffic (discriminating signal for piece-F crash
725 /// tests); `0` when no crash was scheduled. See #4642 piece F.
726 pub fn crash_packets_dropped(&self) -> u64 {
727 self.crash_packets_dropped
728 }
729
730 /// WASM `summarize_state` invocations (summary-cache misses) recorded for
731 /// the peer at `addr`. `0` if the peer ran none. See #4440 / spec step 8.
732 pub fn summarize_wasm_calls_for(&self, addr: &std::net::SocketAddr) -> u64 {
733 self.summarize_wasm_calls.get(addr).copied().unwrap_or(0)
734 }
735
736 /// Total WASM `summarize_state` invocations across every peer in the run.
737 ///
738 /// This is the every-hop summarize-storm falsifier's primary signal: it must
739 /// stay proportional to the contract STATE-CHANGE count, NOT to hosted-set
740 /// size or neighbor overlap. If it grows with the number of shared hosted
741 /// contracts × neighbors, the state-hash summary cache is not covering
742 /// every-hop load and the #4440 storm has re-armed.
743 pub fn total_summarize_wasm_calls(&self) -> u64 {
744 self.summarize_wasm_calls.values().copied().sum()
745 }
746
747 /// The single peer's peak WASM-summarize count — the worst per-node
748 /// summarize load any peer bore during the run.
749 pub fn max_summarize_wasm_calls(&self) -> u64 {
750 self.summarize_wasm_calls
751 .values()
752 .copied()
753 .max()
754 .unwrap_or(0)
755 }
756}
757
758#[derive(PartialEq, Eq, Hash, Clone, PartialOrd, Ord, Debug)]
759pub struct NodeLabel(Arc<str>);
760
761impl NodeLabel {
762 /// Creates a gateway label for a network.
763 pub fn gateway(network_name: &str, id: usize) -> Self {
764 Self(format!("{network_name}-gateway-{id}").into())
765 }
766
767 /// Creates a regular node label for a network.
768 pub fn node(network_name: &str, id: usize) -> Self {
769 Self(format!("{network_name}-node-{id}").into())
770 }
771
772 /// Returns true if this is a gateway label.
773 pub fn is_gateway(&self) -> bool {
774 self.0.contains("-gateway-")
775 }
776
777 /// Returns true if this is a regular node label.
778 pub fn is_node(&self) -> bool {
779 self.0.contains("-node-")
780 }
781
782 pub fn number(&self) -> usize {
783 // Label format is "{network_name}-{gateway|node}-{id}"
784 // The number is always the last part after the final '-'
785 self.0
786 .rsplit('-')
787 .next()
788 .expect("should have a number part")
789 .parse::<usize>()
790 .expect("last part should be a number")
791 }
792}
793
794impl std::fmt::Display for NodeLabel {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 write!(f, "{}", self.0)
797 }
798}
799
800impl std::ops::Deref for NodeLabel {
801 type Target = str;
802
803 fn deref(&self) -> &Self::Target {
804 self.0.deref()
805 }
806}
807
808impl<'a> From<&'a str> for NodeLabel {
809 fn from(value: &'a str) -> Self {
810 assert!(value.starts_with("gateway-") || value.starts_with("node-"));
811 let mut parts = value.split('-');
812 assert!(parts.next().is_some());
813 assert!(
814 parts
815 .next()
816 .map(|s| s.parse::<u16>())
817 .transpose()
818 .expect("should be an u16")
819 .is_some()
820 );
821 assert!(parts.next().is_none());
822 Self(value.to_string().into())
823 }
824}
825
826#[derive(Clone)]
827pub(crate) struct GatewayConfig {
828 #[allow(dead_code)]
829 label: NodeLabel,
830 peer_key_location: PeerKeyLocation,
831 location: Location,
832}
833
834/// Summary of a network event for deterministic comparison.
835///
836/// Excludes timestamps which vary between runs.
837#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
838pub struct EventSummary {
839 pub tx: crate::message::Transaction,
840 pub peer_addr: std::net::SocketAddr,
841 /// String representation of the event kind for sorting
842 pub event_kind_name: String,
843 /// Contract key if this event involves a contract operation
844 pub contract_key: Option<String>,
845 /// State hash if this event includes state (Put/Update success/broadcast)
846 pub state_hash: Option<String>,
847 /// Full debug representation of the event (for backwards compatibility)
848 pub event_detail: String,
849}
850
851/// A stream of events for simulation testing.
852///
853/// `EventChain` provides a `Stream` of events that drive user interactions in
854/// the simulated network.
855///
856/// # Ownership and Cleanup
857///
858/// There are two ways to obtain an `EventChain`:
859///
860/// 1. **`event_chain(&mut sim)`** - Borrows from `SimNetwork`. The `SimNetwork`
861/// retains ownership of labels and handles cleanup on drop. The `EventChain`
862/// is created with `clean_up_tmp_dirs = false`.
863///
864/// 2. **`into_event_chain(sim)`** - Consumes `SimNetwork`. The `EventChain` takes
865/// ownership of labels and handles cleanup on drop. The `EventChain` is created
866/// with `clean_up_tmp_dirs` set to whatever `SimNetwork` had.
867///
868/// This design allows tests to either retain access to `SimNetwork` for verification
869/// methods, or to release it for simpler cleanup.
870pub struct EventChain<S = watch::Sender<(EventId, TransportPublicKey)>> {
871 labels: Vec<(NodeLabel, TransportPublicKey)>,
872 user_ev_controller: S,
873 total_events: u32,
874 count: u32,
875 rng: rand::rngs::SmallRng,
876 /// Whether this EventChain is responsible for cleaning up temp directories.
877 /// Set to `true` when created via `into_event_chain()` (consuming SimNetwork),
878 /// `false` when created via `event_chain()` (borrowing from SimNetwork).
879 clean_up_tmp_dirs: bool,
880 choice: Option<TransportPublicKey>,
881}
882
883impl<S> EventChain<S> {
884 pub fn new(
885 labels: Vec<(NodeLabel, TransportPublicKey)>,
886 user_ev_controller: S,
887 total_events: u32,
888 clean_up_tmp_dirs: bool,
889 ) -> Self {
890 const SEED: u64 = 0xdeadbeef;
891 EventChain {
892 labels,
893 user_ev_controller,
894 total_events,
895 count: 0,
896 rng: rand::rngs::SmallRng::seed_from_u64(SEED),
897 clean_up_tmp_dirs,
898 choice: None,
899 }
900 }
901
902 fn increment_count(self: Pin<&mut Self>) {
903 // SAFETY: We only modify `count` (a non-address-sensitive `usize` field)
904 // through the pinned reference; the EventChain itself is not moved.
905 unsafe {
906 let this = self.get_unchecked_mut();
907 this.count += 1;
908 }
909 }
910
911 fn choose_peer(self: Pin<&mut Self>) -> TransportPublicKey {
912 // SAFETY: We access `choice`, `rng`, and `labels` by mutable reference
913 // without moving the EventChain out of its pinned location.
914 let this = unsafe { self.get_unchecked_mut() };
915 if let Some(id) = this.choice.take() {
916 return id;
917 }
918 let rng = &mut this.rng;
919 let labels = &mut this.labels;
920 let (_, id) = labels.choose(rng).expect("not empty");
921 id.clone()
922 }
923
924 fn set_choice(self: Pin<&mut Self>, id: TransportPublicKey) {
925 // SAFETY: We only write to the `choice` field without moving the
926 // pinned EventChain itself.
927 let this = unsafe { self.get_unchecked_mut() };
928 this.choice = Some(id);
929 }
930}
931
932trait EventSender {
933 fn send(
934 &self,
935 cx: &mut std::task::Context<'_>,
936 value: (EventId, TransportPublicKey),
937 ) -> std::task::Poll<Result<(), ()>>;
938}
939
940impl EventSender for mpsc::Sender<(EventId, TransportPublicKey)> {
941 fn send(
942 &self,
943 cx: &mut std::task::Context<'_>,
944 value: (EventId, TransportPublicKey),
945 ) -> std::task::Poll<Result<(), ()>> {
946 let f = self.send(value);
947 futures::pin_mut!(f);
948 f.poll(cx).map(|r| r.map_err(|_| ()))
949 }
950}
951
952impl EventSender for watch::Sender<(EventId, TransportPublicKey)> {
953 fn send(
954 &self,
955 _cx: &mut std::task::Context<'_>,
956 value: (EventId, TransportPublicKey),
957 ) -> std::task::Poll<Result<(), ()>> {
958 match self.send(value) {
959 Ok(_) => std::task::Poll::Ready(Ok(())),
960 Err(_) => std::task::Poll::Ready(Err(())),
961 }
962 }
963}
964
965impl EventSender for broadcast::Sender<(EventId, TransportPublicKey)> {
966 fn send(
967 &self,
968 _cx: &mut std::task::Context<'_>,
969 value: (EventId, TransportPublicKey),
970 ) -> std::task::Poll<Result<(), ()>> {
971 match self.send(value) {
972 Ok(_) => std::task::Poll::Ready(Ok(())),
973 Err(_) => std::task::Poll::Ready(Err(())),
974 }
975 }
976}
977
978impl<S: EventSender> futures::stream::Stream for EventChain<S> {
979 type Item = EventId;
980
981 fn poll_next(
982 mut self: std::pin::Pin<&mut Self>,
983 cx: &mut std::task::Context<'_>,
984 ) -> std::task::Poll<Option<Self::Item>> {
985 if self.count < self.total_events {
986 let id = self.as_mut().choose_peer();
987 match self
988 .user_ev_controller
989 .send(cx, (self.count, id.clone()))
990 .map_err(|_| {
991 tracing::error!("peer controller should be alive, finishing event chain")
992 }) {
993 std::task::Poll::Ready(_) => {}
994 std::task::Poll::Pending => {
995 self.as_mut().set_choice(id);
996 return std::task::Poll::Pending;
997 }
998 }
999 self.as_mut().increment_count();
1000 std::task::Poll::Ready(Some(self.count))
1001 } else {
1002 std::task::Poll::Ready(None)
1003 }
1004 }
1005}
1006
1007impl<S> Drop for EventChain<S> {
1008 fn drop(&mut self) {
1009 if self.clean_up_tmp_dirs {
1010 clean_up_tmp_dirs(self.labels.iter().map(|(l, _)| l));
1011 }
1012 }
1013}
1014
1015/// A controlled event chain that triggers events in a specific sequence.
1016///
1017/// Unlike `EventChain` which randomly selects peers, `ControlledEventChain`
1018/// triggers events in the exact order specified, allowing deterministic testing.
1019pub struct ControlledEventChain {
1020 user_ev_controller: watch::Sender<(EventId, TransportPublicKey)>,
1021 /// Sequence of (EventId, NodeLabel) to trigger, in order
1022 event_sequence: Vec<(EventId, NodeLabel)>,
1023 /// Map from NodeLabel to TransportPublicKey
1024 label_to_key: HashMap<NodeLabel, TransportPublicKey>,
1025 /// Current position in the event sequence
1026 current_index: usize,
1027}
1028
1029impl ControlledEventChain {
1030 /// Create a new controlled event chain.
1031 pub fn new(
1032 user_ev_controller: watch::Sender<(EventId, TransportPublicKey)>,
1033 event_sequence: Vec<(EventId, NodeLabel)>,
1034 label_to_key: HashMap<NodeLabel, TransportPublicKey>,
1035 ) -> Self {
1036 Self {
1037 user_ev_controller,
1038 event_sequence,
1039 label_to_key,
1040 current_index: 0,
1041 }
1042 }
1043
1044 /// Trigger the next event in the sequence.
1045 ///
1046 /// Returns the EventId that was triggered, or None if all events have been triggered.
1047 pub fn trigger_next(&mut self) -> Option<EventId> {
1048 if self.current_index >= self.event_sequence.len() {
1049 return None;
1050 }
1051
1052 let (event_id, label) = &self.event_sequence[self.current_index];
1053 let key = self
1054 .label_to_key
1055 .get(label)
1056 .expect("Label should exist in mapping");
1057
1058 match self.user_ev_controller.send((*event_id, key.clone())) {
1059 Ok(()) => {
1060 self.current_index += 1;
1061 Some(*event_id)
1062 }
1063 Err(e) => {
1064 tracing::error!("Failed to send event {}: {:?}", event_id, e);
1065 None
1066 }
1067 }
1068 }
1069
1070 /// Trigger all remaining events in sequence.
1071 ///
1072 /// Returns the number of events triggered.
1073 pub fn trigger_all(&mut self) -> usize {
1074 let mut count = 0;
1075 while self.trigger_next().is_some() {
1076 count += 1;
1077 }
1078 count
1079 }
1080
1081 /// Returns true if all events have been triggered.
1082 pub fn is_complete(&self) -> bool {
1083 self.current_index >= self.event_sequence.len()
1084 }
1085
1086 /// Returns the number of events remaining.
1087 pub fn remaining(&self) -> usize {
1088 self.event_sequence.len().saturating_sub(self.current_index)
1089 }
1090}
1091
1092impl futures::stream::Stream for ControlledEventChain {
1093 type Item = EventId;
1094
1095 fn poll_next(
1096 mut self: std::pin::Pin<&mut Self>,
1097 _cx: &mut std::task::Context<'_>,
1098 ) -> std::task::Poll<Option<Self::Item>> {
1099 match self.trigger_next() {
1100 Some(event_id) => std::task::Poll::Ready(Some(event_id)),
1101 None => std::task::Poll::Ready(None),
1102 }
1103 }
1104}
1105
1106#[cfg(feature = "trace-ot")]
1107type DefaultRegistry = CombinedRegister<2>;
1108
1109#[cfg(not(feature = "trace-ot"))]
1110type DefaultRegistry = TestEventListener;
1111
1112/// How a startup-seeded contract is registered on its owning node.
1113///
1114/// Threads the distinction between the two seed primitives from
1115/// `run_controlled_simulation` down into [`in_memory::append_contracts`], which
1116/// runs at node startup. Both store the state locally with no network
1117/// propagation; they differ ONLY in the hosting/demand bookkeeping they install,
1118/// which is exactly what the serve-DURING gate keys on.
1119///
1120/// The variants are only ever constructed by `run_controlled_simulation`
1121/// (`#[cfg(any(test, feature = "testing"))]`), but `testing_impl` itself compiles
1122/// in every build (e.g. `trace-ot`), where `append_contracts` still matches on
1123/// this enum. So in a non-testing build the variants are legitimately never
1124/// constructed — allow dead_code there, keeping the lint live under testing.
1125#[cfg_attr(not(any(test, feature = "testing")), allow(dead_code))]
1126#[derive(Clone, Copy, Debug)]
1127pub(super) enum SeedMode {
1128 /// Genuine host WITH an active subscription (`SeedHostedContract`):
1129 /// `host_contract` + `ring.subscribe`, so `is_receiving_updates` is true.
1130 Subscribed,
1131 /// Demandless every-hop copy (`SeedDemandlessCopy`): `host_contract` +
1132 /// `register_local_hosting` (interest `hosting = true`), with NO subscription
1133 /// and NO prior local client access. This is the exact state a production
1134 /// every-hop GET/PUT store leaves on a RELAY hop (`is_hosting_contract` and
1135 /// `has_local_interest` true, `is_receiving_updates` false), which the
1136 /// serve-DURING originator gate (#4642 R3 piece C) must serve locally.
1137 Demandless,
1138}
1139
1140pub(super) struct Builder<ER> {
1141 pub config: NodeConfig,
1142 contract_handler_name: String,
1143 event_register: ER,
1144 contracts: Vec<(ContractContainer, WrappedState, SeedMode)>,
1145 contract_subscribers: HashMap<ContractKey, Vec<PeerKeyLocation>>,
1146 /// Pre-formed ring connections injected at node startup, bypassing the
1147 /// organic CONNECT handshake. Each entry is a peer this node should already
1148 /// hold a direct ring connection to when its event loop starts. Drained by
1149 /// `run_node_with_{shared_storage,mock_wasm}` into
1150 /// `apply_preseeded_connections` (see that helper for the rationale).
1151 /// Populated by [`SimNetwork::preseed_direct_star`]; empty for every node by
1152 /// default, so this is a zero-cost no-op for all existing simulations.
1153 preseed_connections: Vec<PeerKeyLocation>,
1154 /// Seed for deterministic RNG in this node's transport layer
1155 pub rng_seed: u64,
1156 /// Network name for scoped fault injection
1157 pub network_name: String,
1158 /// Shared handle for capturing the live `ConnectionManager` after node start.
1159 pub shared_cm: Option<Arc<parking_lot::Mutex<Option<crate::ring::ConnectionManager>>>>,
1160 /// Shared handle for capturing the live `Arc<Ring>` after node start.
1161 /// Used by governance sim tests to observe per-node ban-list state
1162 /// (`ring.contract_ban_list.is_banned(..)`) without exposing the Ring
1163 /// on the public `RunningNode` surface. Mirrors `shared_cm`.
1164 pub shared_ring: Option<Arc<parking_lot::Mutex<Option<Arc<crate::ring::Ring>>>>>,
1165}
1166
1167impl<ER: NetEventRegister> Builder<ER> {
1168 /// Builds an in-memory node. Does nothing upon construction.
1169 pub fn build(
1170 builder: NodeConfig,
1171 event_register: ER,
1172 contract_handler_name: String,
1173 rng_seed: u64,
1174 network_name: String,
1175 ) -> Builder<ER> {
1176 Builder {
1177 config: builder.clone(),
1178 contract_handler_name,
1179 event_register,
1180 contracts: Vec::new(),
1181 contract_subscribers: HashMap::new(),
1182 preseed_connections: Vec::new(),
1183 rng_seed,
1184 network_name,
1185 shared_cm: None,
1186 shared_ring: None,
1187 }
1188 }
1189}
1190
1191/// Information about a running node, used for crash/restart operations.
1192#[derive(Debug)]
1193pub struct RunningNode {
1194 /// The label identifying this node
1195 pub label: NodeLabel,
1196 /// Socket address for fault injection
1197 pub addr: SocketAddr,
1198 /// Handle to abort the running task (AbortHandle can be cloned and used independently)
1199 pub abort_handle: tokio::task::AbortHandle,
1200}
1201
1202/// Configuration saved for node restart.
1203///
1204/// When a node is started, its configuration is saved here so it can be
1205/// restarted with the same identity (keypair), location, and data directory.
1206#[derive(Clone)]
1207pub struct RestartableNodeConfig {
1208 /// The node's configuration (contains keypair, location, data dir, etc.)
1209 pub config: NodeConfig,
1210 /// The node label (reserved for future use in restart scenarios)
1211 #[allow(dead_code)]
1212 pub label: NodeLabel,
1213 /// Whether this is a gateway node
1214 pub is_gateway: bool,
1215 /// Gateway addresses to connect to (for non-gateway nodes)
1216 #[allow(dead_code)]
1217 pub gateway_configs: Vec<GatewayConfig>,
1218 /// Seed for deterministic RNG in this node's transport layer
1219 pub rng_seed: u64,
1220 /// Shared in-memory storage for contract state (persists across restarts)
1221 pub shared_storage: crate::wasm_runtime::MockStateStorage,
1222}
1223
1224/// State for a node in the direct simulation runner, shared with the chaos driver.
1225#[cfg(any(test, feature = "testing"))]
1226struct DirectNodeState {
1227 label: NodeLabel,
1228 addr: SocketAddr,
1229 is_gateway: bool,
1230 permanently_dropped: bool,
1231}
1232
1233/// Configuration for deterministic node churn (crash/restart) during simulation.
1234///
1235/// When enabled, a chaos driver task periodically crashes and restarts nodes
1236/// to test network resilience under churn conditions.
1237#[derive(Debug, Clone)]
1238pub struct ChurnConfig {
1239 /// Probability (0.0–1.0) that a non-gateway node is crashed each tick.
1240 pub crash_probability: f64,
1241 /// How often the chaos driver evaluates crashes.
1242 pub tick_interval: Duration,
1243 /// How long a crashed node stays down before restarting.
1244 pub recovery_delay: Duration,
1245 /// Maximum number of nodes crashed simultaneously.
1246 /// Defaults to 25% of non-gateway node count.
1247 pub max_simultaneous_crashes: Option<usize>,
1248 /// Fraction (0.0–1.0) of crashes that are permanent (no restart).
1249 pub permanent_crash_rate: f64,
1250 /// Time to wait before the chaos driver starts crashing nodes.
1251 pub warmup_delay: Duration,
1252}
1253
1254impl Default for ChurnConfig {
1255 fn default() -> Self {
1256 Self {
1257 crash_probability: 0.1,
1258 tick_interval: Duration::from_secs(5),
1259 recovery_delay: Duration::from_secs(3),
1260 max_simultaneous_crashes: None,
1261 permanent_crash_rate: 0.05,
1262 warmup_delay: Duration::from_secs(5),
1263 }
1264 }
1265}
1266
1267/// For each target ring location, find a distinct IPv6-loopback port whose
1268/// `Location::from_address((::1, port))` is closest (in ring distance) to that
1269/// target. Used by [`SimNetwork::new_with_node_locations`] to place nodes at
1270/// chosen locations without decoupling a peer's advertised location from its
1271/// address-derived one.
1272///
1273/// Ports are unique across the returned vector (a port already chosen for an
1274/// earlier target is skipped for later targets), so the resulting peers occupy
1275/// distinct addresses. The achieved location for each target is the closest
1276/// available port's location — approximate (ports are discrete) but fully
1277/// deterministic. Read the achieved locations back with
1278/// [`SimNetwork::get_peer_locations`].
1279///
1280/// The search range excludes the `50000..=59999` band used by
1281/// `derive_deterministic_port`, so explicit ports never collide with the
1282/// default-path derived ports.
1283pub fn loopback_ports_for_locations(targets: &[f64]) -> Vec<u16> {
1284 use std::collections::HashSet;
1285 const SEARCH_RANGE: std::ops::RangeInclusive<u16> = 1024..=49999;
1286 let mut used: HashSet<u16> = HashSet::new();
1287 let mut out = Vec::with_capacity(targets.len());
1288 for &target in targets {
1289 let mut best_port = *SEARCH_RANGE.start();
1290 let mut best_dist = f64::INFINITY;
1291 for port in SEARCH_RANGE {
1292 if used.contains(&port) {
1293 continue;
1294 }
1295 let addr: SocketAddr = (Ipv6Addr::LOCALHOST, port).into();
1296 let loc = Location::from_address(&addr).as_f64();
1297 let raw = (loc - target).abs();
1298 let dist = raw.min(1.0 - raw); // ring distance
1299 if dist < best_dist {
1300 best_dist = dist;
1301 best_port = port;
1302 }
1303 }
1304 used.insert(best_port);
1305 out.push(best_port);
1306 }
1307 out
1308}
1309
1310/// A simulated in-memory network topology.
1311pub struct SimNetwork {
1312 name: String,
1313 clean_up_tmp_dirs: bool,
1314 labels: Vec<(NodeLabel, TransportPublicKey)>,
1315 pub(crate) event_listener: TestEventListener,
1316 user_ev_controller: Option<watch::Sender<(EventId, TransportPublicKey)>>,
1317 receiver_ch: watch::Receiver<(EventId, TransportPublicKey)>,
1318 number_of_gateways: usize,
1319 gateways: Vec<(Builder<DefaultRegistry>, GatewayConfig)>,
1320 number_of_nodes: usize,
1321 nodes: Vec<(Builder<DefaultRegistry>, NodeLabel)>,
1322 ring_max_htl: usize,
1323 rnd_if_htl_above: usize,
1324 max_connections: usize,
1325 min_connections: usize,
1326 start_backoff: Duration,
1327 /// Master seed for deterministic RNG - used to derive per-peer seeds
1328 seed: u64,
1329 /// VirtualTime for deterministic simulation - always enabled
1330 virtual_time: VirtualTime,
1331 /// Running nodes indexed by label for crash/restart operations
1332 running_nodes: HashMap<NodeLabel, RunningNode>,
1333 /// Map from label to socket address for quick lookup
1334 node_addresses: HashMap<NodeLabel, SocketAddr>,
1335 /// Saved configurations for node restart (preserved after crash)
1336 restartable_configs: HashMap<NodeLabel, RestartableNodeConfig>,
1337 /// All gateway configs (needed for restarting non-gateway nodes)
1338 all_gateway_configs: Vec<GatewayConfig>,
1339 /// Size threshold (bytes) above which streaming is used.
1340 /// Default: `None` → uses `usize::MAX` so tests don't stream unless explicitly opted in
1341 /// via `with_streaming_threshold()`. This preserves the pre-streaming-always-on behavior
1342 /// where simulation tests used inline messages for all payloads.
1343 pub streaming_threshold: Option<usize>,
1344 connection_managers: HashMap<NodeLabel, ConnectionManager>,
1345 /// When true, use `MockWasmRuntime` (production `ContractExecutor` code path)
1346 /// instead of `MockRuntime` (simplified hash-based merge).
1347 pub use_mock_wasm: bool,
1348 /// When true, the direct runner skips the long post-event convergence-polling
1349 /// loop and does only a brief fixed propagation settle instead.
1350 ///
1351 /// The direct runner normally polls for full state convergence for up to
1352 /// ~1800s of virtual time after the event phase (30 rounds × 60s, early-exit
1353 /// on convergence). That tail is advisory — the runner only logs a warning if
1354 /// convergence is never reached, it does not fail the simulation. For tests
1355 /// that analyze events produced *during* the event phase and never assert
1356 /// convergence (e.g. `test_interest_renewal`), running the full polling tail
1357 /// when the network happens not to converge adds up to 1800s of virtual time
1358 /// for no benefit, blowing the wall-clock budget and making the test time out
1359 /// in CI. The test crate sets this via `TestConfig::no_convergence_wait()` so
1360 /// such tests stay bounded regardless of whether the network converges.
1361 /// See #3792.
1362 pub skip_convergence_wait: bool,
1363 /// Optional churn (crash/restart) configuration for the chaos driver.
1364 churn_config: Option<ChurnConfig>,
1365 /// Optional pre-operation join-convergence barrier for
1366 /// [`run_controlled_simulation`](Self::run_controlled_simulation). When
1367 /// `Some((min_fraction, max_wait))`, the controlled-event client waits —
1368 /// before firing any scheduled operation — until at least `min_fraction`
1369 /// of the network's peers have completed their network join, or `max_wait`
1370 /// virtual time elapses, whichever comes first. Join is detected via the
1371 /// topology-snapshot registry: a peer only registers a snapshot once its
1372 /// own address/location is established (`peer_ready`), so the count of
1373 /// distinct snapshot peers is a direct join tally (see
1374 /// `ring::register_topology_snapshots_periodically`, which `continue`s
1375 /// while `get_own_addr()` is `None`).
1376 ///
1377 /// `None` (default) preserves the historical behavior of firing operations
1378 /// after a fixed 3s warmup, racing topology formation. This barrier exists
1379 /// because a cold-start GET-reliability measurement must run against a
1380 /// FORMED network: with only the fixed warmup, higher-index nodes' one-shot,
1381 /// no-retry GETs fire before those nodes finish joining and are rejected
1382 /// with `PeerNotJoined`, so the metric conflates join speed with GET
1383 /// reliability. See `test_get_reliability_diagnostic`. Opt in via
1384 /// [`wait_for_join_convergence_before_ops`](Self::wait_for_join_convergence_before_ops).
1385 wait_for_join_before_ops: Option<(f64, Duration)>,
1386 /// Optional override for the delay the controlled-event client waits after
1387 /// triggering each scheduled operation before triggering the next one.
1388 /// `None` (default) uses the historical fixed 3s settle. A test whose
1389 /// operations complete quickly against an already-formed network (e.g. a
1390 /// read-only GET sweep behind `wait_for_join_convergence_before_ops`) can
1391 /// shrink this to avoid spending ~3s of virtual time — and the wall-clock
1392 /// to simulate it — per operation for work that finishes in milliseconds.
1393 /// Only affects regular client-request operations; special in-client ops
1394 /// (clock advance, crash/recover) keep their own fixed settle. See
1395 /// [`with_controlled_op_interval`](Self::with_controlled_op_interval).
1396 controlled_op_interval: Option<Duration>,
1397 /// Optional governance-manager config override applied to every node
1398 /// this network builds. Lets governance sim tests compress the
1399 /// production minute-to-hour timescales and lower `min_samples` so the
1400 /// rate-limit → MAD → evict → ban chain fires within a paused-time sim.
1401 /// Pair with `use_mock_wasm = true` so the production cost-reporting
1402 /// path actually feeds the detector. See #4301.
1403 governance_config_override: Option<crate::contract::governance::GovernanceConfig>,
1404 /// Per-node placement-migration version-floor override (see
1405 /// [`SimNetwork::enable_placement_migration`]). Defaults to
1406 /// `Some(SIM_MIGRATION_DISABLED_FLOOR)` (an unreachable floor) so the
1407 /// `SubscribeHint` cascade is FAIL-CLOSED — OFF — in every sim regardless of
1408 /// build version, and only a test that explicitly calls
1409 /// `enable_placement_migration` gets the cascade. This keeps migration from
1410 /// perturbing unrelated simulations now that the crate version is past the
1411 /// real production floor (#4601). Production is untouched: `NodeConfig::new`
1412 /// sets this to `None`, which resolves to the real `SUBSCRIBE_HINT_MIN_VERSION`.
1413 subscribe_hint_floor_override: Option<(u8, u8, u16)>,
1414 /// Per-node summary-first PUT probe version-floor override (see
1415 /// [`SimNetwork::enable_summary_first_put`]). Mirrors
1416 /// `subscribe_hint_floor_override` exactly: defaults to
1417 /// `Some(SIM_MIGRATION_DISABLED_FLOOR)` (an unreachable floor) so the
1418 /// probe/dispatch cascade is FAIL-CLOSED — OFF — in every sim regardless
1419 /// of build version, and only a test that explicitly calls
1420 /// `enable_summary_first_put` gets summary-first PUT. Production is
1421 /// untouched: `NodeConfig::new` sets this to `None`, which resolves to
1422 /// the real `SUMMARY_FIRST_PUT_MIN_VERSION`.
1423 summary_first_put_floor_override: Option<(u8, u8, u16)>,
1424 /// Per-node hash-first summary version-floor override (#4965, see
1425 /// [`SimNetwork::enable_hash_first_summaries`]).
1426 ///
1427 /// **Defaults to `Some(SIM_MIGRATION_ENABLED_FLOOR)` — ON — which is the
1428 /// OPPOSITE of the two overrides above.** They gate behavioural cascades
1429 /// that pile load onto unrelated sims, so they fail closed. Hash-first
1430 /// only changes the ENCODING of the `Summaries` exchange that already
1431 /// runs in every sim, with identical convergence semantics, so ON by
1432 /// default costs unrelated sims nothing — and it is the only way the
1433 /// suite exercises the new wire path before the release that lifts the
1434 /// crate version over the production floor. Without it the first
1435 /// integration-level run of a Full-tier protocol change would be the
1436 /// release PR, where a red sim could not be attributed between the
1437 /// feature and the version bump.
1438 hash_first_summaries_floor_override: Option<(u8, u8, u16)>,
1439 /// Per-node version-carrying-ack version-floor override (#5161, see
1440 /// [`SimNetwork::enable_gateway_ack_version`]).
1441 ///
1442 /// **Defaults to `Some(SIM_MIGRATION_DISABLED_FLOOR)` — OFF**, like the two
1443 /// cascade gates and unlike `hash_first_summaries_floor_override`. The gate
1444 /// is encoding-only where it fires, but the version it teaches is the INPUT
1445 /// to every other `version_supports_*` gate, so enabling it network-wide
1446 /// makes node->gateway links newly eligible for those features. Measured,
1447 /// not assumed: defaulting it ON changed the outcome of unrelated sims.
1448 ///
1449 /// The cost of OFF, stated plainly: sims keep the pre-0.2.120 topology
1450 /// asymmetry (a regular node never learns its gateway's version) even after
1451 /// production stops having it. `enable_gateway_ack_version` is how a test
1452 /// that cares opts in.
1453 ack_version_floor_override: Option<(u8, u8, u16)>,
1454 /// Per-node override for the originator-target-list version floor (#5147).
1455 ///
1456 /// Defaults to `SIM_MIGRATION_DISABLED_FLOOR` — OFF — grouping with
1457 /// `subscribe_hint_floor_override` and `summary_first_put_floor_override`
1458 /// rather than with `hash_first_summaries_floor_override`. Hash-first is ON
1459 /// by default because it changes only the ENCODING of an exchange with
1460 /// identical semantics. This one changes fan-out BEHAVIOUR: it removes
1461 /// peers from a broadcast. Defaulting it ON would silently alter the
1462 /// delivery graph under every sim that asserts on convergence or delivery
1463 /// counts, and a failure could not be attributed between that sim's own
1464 /// subject and this suppression. Tests that want it call
1465 /// [`enable_broadcast_target_list`](Self::enable_broadcast_target_list).
1466 broadcast_target_list_floor_override: Option<(u8, u8, u16)>,
1467 /// Optional controllable hosting clock injected into every node's
1468 /// `HostingManager` (via `NodeConfig::hosting_time_source_override`). When
1469 /// set, hosting-cache TTL and subscription-lease eviction advance ONLY when
1470 /// the sim advances this clock — either directly (`hosting_clock()`) or, for
1471 /// mid-run control under the turmoil runner, via
1472 /// `SimOperation::AdvanceHostingClock`. Enables deterministic eviction sims
1473 /// (#4642 piece A). `None` → nodes use the default wall-clock time source.
1474 hosting_clock: Option<crate::util::time_source::SharedMockTimeSource>,
1475 /// Optional per-node hosting-cache byte budget (`max_hosting_storage`).
1476 /// When set, every node this network builds is given this budget instead of
1477 /// the capability-derived default, so a test can force cache pressure with a
1478 /// tiny budget and observe demand-driven eviction (#4642 piece A).
1479 hosting_budget_override: Option<u64>,
1480 /// Per-node capture slots for the live `Arc<Ring>`, populated by
1481 /// `run_controlled_simulation` so governance sim tests can read each
1482 /// node's `contract_ban_list` after the simulation completes. Keyed by
1483 /// label; the inner `Option` is filled once the node's run loop reaches
1484 /// the `shared_ring` write in `run_node_with_{shared_storage,mock_wasm}`.
1485 ///
1486 /// `allow(dead_code)`: populated unconditionally in
1487 /// `run_controlled_simulation`, but only *read* in the
1488 /// `cfg(any(test, feature = "testing"))` `node_rings` extraction, so a
1489 /// production build (where `testing_impl` is still compiled) sees it
1490 /// as write-only.
1491 #[allow(dead_code)]
1492 shared_rings: HashMap<NodeLabel, Arc<parking_lot::Mutex<Option<Arc<crate::ring::Ring>>>>>,
1493 /// Optional explicit loopback ports for regular nodes, indexed by
1494 /// regular-node order (i.e. `node_no - number_of_gateways`). When present,
1495 /// `config_nodes` uses these instead of `derive_deterministic_port`, which
1496 /// lets a test place nodes at chosen ring locations: a peer's location is
1497 /// `Location::from_address(&addr)`, a pure function of the loopback port, so
1498 /// choosing the port chooses the location *consistently* (the address-derived
1499 /// location and the advertised `own_location` stay equal, exactly as the
1500 /// default port path keeps them). Build via `new_with_node_locations`.
1501 node_port_override: Option<Vec<u16>>,
1502}
1503
1504impl SimNetwork {
1505 /// Default seed for deterministic simulation
1506 pub const DEFAULT_SEED: u64 = 0xDEADBEEF_CAFEBABE;
1507
1508 /// Placement-migration version floor that pins the `SubscribeHint` cascade
1509 /// OFF for a simulation: an unreachable value above any real or simulated
1510 /// crate version, so `version_supports_subscribe_hint` always returns false.
1511 /// This is the per-node default for every `SimNetwork` (see `new_inner`),
1512 /// making migration genuinely OPT-IN in sims; `enable_placement_migration`
1513 /// lowers it to [`Self::SIM_MIGRATION_ENABLED_FLOOR`]. See #4601.
1514 pub(crate) const SIM_MIGRATION_DISABLED_FLOOR: (u8, u8, u16) = (255, 255, 65535);
1515
1516 /// Placement-migration version floor that forces the `SubscribeHint` cascade
1517 /// ON for a simulation regardless of build version (every peer's version is
1518 /// `>= (0,0,0)`). Set by [`Self::enable_placement_migration`].
1519 pub(crate) const SIM_MIGRATION_ENABLED_FLOOR: (u8, u8, u16) = (0, 0, 0);
1520
1521 #[allow(clippy::too_many_arguments)]
1522 pub async fn new(
1523 name: &str,
1524 gateways: usize,
1525 nodes: usize,
1526 ring_max_htl: usize,
1527 rnd_if_htl_above: usize,
1528 max_connections: usize,
1529 min_connections: usize,
1530 seed: u64,
1531 ) -> Self {
1532 Self::new_inner(
1533 name,
1534 gateways,
1535 nodes,
1536 ring_max_htl,
1537 rnd_if_htl_above,
1538 max_connections,
1539 min_connections,
1540 seed,
1541 None,
1542 )
1543 .await
1544 }
1545
1546 /// Like [`new`](Self::new), but places each regular node at (approximately)
1547 /// a chosen ring location. `node_target_locations` is matched to regular
1548 /// nodes in order and must have exactly `nodes` entries.
1549 ///
1550 /// Locations are realised by choosing, for each target, a distinct loopback
1551 /// port whose `Location::from_address` is closest to the target (see
1552 /// [`loopback_ports_for_locations`]). The achieved locations are therefore
1553 /// approximate (discrete ports); read them back with [`get_peer_locations`]
1554 /// (gateways first, then regular nodes in order) and build the scenario
1555 /// around the actual values. Gateways keep their derived locations.
1556 #[allow(clippy::too_many_arguments)]
1557 pub async fn new_with_node_locations(
1558 name: &str,
1559 gateways: usize,
1560 nodes: usize,
1561 ring_max_htl: usize,
1562 rnd_if_htl_above: usize,
1563 max_connections: usize,
1564 min_connections: usize,
1565 seed: u64,
1566 node_target_locations: &[f64],
1567 ) -> Self {
1568 assert_eq!(
1569 node_target_locations.len(),
1570 nodes,
1571 "node_target_locations must have exactly `nodes` entries"
1572 );
1573 let ports = loopback_ports_for_locations(node_target_locations);
1574 Self::new_inner(
1575 name,
1576 gateways,
1577 nodes,
1578 ring_max_htl,
1579 rnd_if_htl_above,
1580 max_connections,
1581 min_connections,
1582 seed,
1583 Some(ports),
1584 )
1585 .await
1586 }
1587
1588 #[allow(clippy::too_many_arguments)]
1589 async fn new_inner(
1590 name: &str,
1591 gateways: usize,
1592 nodes: usize,
1593 ring_max_htl: usize,
1594 rnd_if_htl_above: usize,
1595 max_connections: usize,
1596 min_connections: usize,
1597 seed: u64,
1598 node_port_override: Option<Vec<u16>>,
1599 ) -> Self {
1600 assert!(nodes > 0);
1601
1602 // Seed GlobalRng for deterministic location generation
1603 // This ensures Location::random() calls in config_gateways/config_nodes are deterministic
1604 GlobalRng::set_seed(seed);
1605
1606 let (user_ev_controller, mut receiver_ch) =
1607 watch::channel((0, TransportKeypair::new().public().clone()));
1608 receiver_ch.borrow_and_update();
1609
1610 // VirtualTime is always enabled for deterministic simulation
1611 let virtual_time = VirtualTime::new();
1612
1613 // Register the VirtualTime for this network so SimulationSocket can use it
1614 register_network_time_source(name, virtual_time.clone());
1615
1616 let mut net = Self {
1617 name: name.into(),
1618 clean_up_tmp_dirs: true,
1619 event_listener: TestEventListener::new().await,
1620 labels: Vec::with_capacity(nodes + gateways),
1621 user_ev_controller: Some(user_ev_controller),
1622 receiver_ch,
1623 number_of_gateways: gateways,
1624 gateways: Vec::with_capacity(gateways),
1625 number_of_nodes: nodes,
1626 nodes: Vec::with_capacity(nodes),
1627 ring_max_htl,
1628 rnd_if_htl_above,
1629 max_connections,
1630 min_connections,
1631 start_backoff: Duration::from_millis(1),
1632 seed,
1633 virtual_time,
1634 running_nodes: HashMap::new(),
1635 node_addresses: HashMap::new(),
1636 restartable_configs: HashMap::new(),
1637 all_gateway_configs: Vec::new(),
1638 streaming_threshold: None,
1639 connection_managers: HashMap::new(),
1640 use_mock_wasm: false,
1641 skip_convergence_wait: false,
1642 churn_config: None,
1643 wait_for_join_before_ops: None,
1644 controlled_op_interval: None,
1645 governance_config_override: None,
1646 // Fail-closed by default: pin the placement-migration (`SubscribeHint`)
1647 // cascade OFF for every simulation unless a test explicitly opts in
1648 // via `enable_placement_migration`. The cascade gates on the build
1649 // version being `>= SUBSCRIBE_HINT_MIN_VERSION` (0.2.80); since this
1650 // crate is now past that floor, a `None` override (real floor) would
1651 // run migration in EVERY sim and pile directed-subscribe + renewal
1652 // load onto unrelated simulations — the #4601 regression that turned
1653 // the 500-node nightly red. Defaulting to an unreachable floor keeps
1654 // migration genuinely opt-in, as the docs have always claimed.
1655 subscribe_hint_floor_override: Some(Self::SIM_MIGRATION_DISABLED_FLOOR),
1656 // Fail-closed by default, same rationale as
1657 // `subscribe_hint_floor_override` above: summary-first PUT is
1658 // genuinely opt-in per sim via `enable_summary_first_put`.
1659 summary_first_put_floor_override: Some(Self::SIM_MIGRATION_DISABLED_FLOOR),
1660 // Fail-OPEN by default, deliberately unlike the two above (#4965).
1661 // See the field's rustdoc: hash-first is an encoding change, not a
1662 // load-bearing cascade, and defaulting it ON is what gives a
1663 // version-gated wire change simulation coverage BEFORE the release
1664 // that lifts the crate version past its floor.
1665 hash_first_summaries_floor_override: Some(Self::SIM_MIGRATION_ENABLED_FLOOR),
1666 // Fail-CLOSED by default, like the two cascade gates and unlike
1667 // hash-first. This gate is encoding-only where it fires, but what
1668 // it teaches — the remote's version — is the INPUT to every other
1669 // `version_supports_*` gate, so switching it on network-wide is a
1670 // cascade in effect. Opt in per sim via `enable_gateway_ack_version`.
1671 ack_version_floor_override: Some(Self::SIM_MIGRATION_DISABLED_FLOOR),
1672 // Fail-CLOSED by default, like the two behavioural gates above and
1673 // unlike hash-first (#5147). See the field's rustdoc.
1674 broadcast_target_list_floor_override: Some(Self::SIM_MIGRATION_DISABLED_FLOOR),
1675 hosting_clock: None,
1676 hosting_budget_override: None,
1677 shared_rings: HashMap::new(),
1678 node_port_override,
1679 };
1680 net.config_gateways(
1681 gateways
1682 .try_into()
1683 .expect("should have at least one gateway"),
1684 )
1685 .await;
1686 net.config_nodes(nodes).await;
1687
1688 // Auto-configure fault injection with VirtualTime enabled
1689 // Users can call with_fault_injection() to customize
1690 net.init_default_fault_injection();
1691
1692 net
1693 }
1694
1695 /// Initializes the default fault injection with VirtualTime but no faults.
1696 /// Users can call with_fault_injection() to add message loss, latency, etc.
1697 fn init_default_fault_injection(&mut self) {
1698 use crate::node::network_bridge::{FaultInjectorState, set_fault_injector};
1699 let fault_seed = self.seed.wrapping_add(0xFA01_7777);
1700 let state = FaultInjectorState::new(FaultConfig::default(), fault_seed)
1701 .with_virtual_time(self.virtual_time.clone());
1702 set_fault_injector(
1703 &self.name,
1704 Some(std::sync::Arc::new(std::sync::Mutex::new(state))),
1705 );
1706 }
1707
1708 /// Sets the streaming threshold for operations.
1709 ///
1710 /// Payloads larger than `threshold` bytes will use streaming instead of
1711 /// inline messages. This retroactively updates all already-built gateway
1712 /// and node configs.
1713 pub fn with_streaming_threshold(&mut self, threshold: usize) {
1714 self.streaming_threshold = Some(threshold);
1715
1716 // Retroactively update already-built node configs (since config_gateways/config_nodes
1717 // ran inside new() before this method could be called).
1718 for (builder, _) in &mut self.gateways {
1719 let old_config = &*builder.config.config;
1720 let mut new_network_api = old_config.network_api.clone();
1721 new_network_api.streaming_threshold = threshold;
1722 let mut new_config = old_config.clone();
1723 new_config.network_api = new_network_api;
1724 builder.config.config = Arc::new(new_config);
1725 }
1726 for (builder, _) in &mut self.nodes {
1727 let old_config = &*builder.config.config;
1728 let mut new_network_api = old_config.network_api.clone();
1729 new_network_api.streaming_threshold = threshold;
1730 let mut new_config = old_config.clone();
1731 new_config.network_api = new_network_api;
1732 builder.config.config = Arc::new(new_config);
1733 }
1734 }
1735
1736 /// Sets the readiness gating threshold for all nodes.
1737 ///
1738 /// Nodes must have at least `min` ring connections before they advertise
1739 /// readiness for non-CONNECT operations. Retroactively updates all
1740 /// already-built gateway and node configs.
1741 pub fn with_readiness_gating(&mut self, min: usize) {
1742 for (builder, _) in &mut self.gateways {
1743 builder.config.relay_ready_connections = Some(min);
1744 }
1745 for (builder, _) in &mut self.nodes {
1746 builder.config.relay_ready_connections = Some(min);
1747 }
1748 }
1749
1750 /// Derives a deterministic per-peer seed from the master seed and peer index.
1751 fn derive_peer_seed(&self, peer_index: usize) -> u64 {
1752 // Use a simple but effective mixing function
1753 let mut seed = self.seed;
1754 seed = seed.wrapping_add(peer_index as u64);
1755 seed ^= seed >> 33;
1756 seed = seed.wrapping_mul(0xff51afd7ed558ccd);
1757 seed ^= seed >> 33;
1758 seed = seed.wrapping_mul(0xc4ceb9fe1a85ec53);
1759 seed ^= seed >> 33;
1760 seed
1761 }
1762}
1763
1764impl SimNetwork {
1765 pub fn with_start_backoff(&mut self, value: Duration) {
1766 self.start_backoff = value;
1767 }
1768
1769 /// Enables the chaos driver which periodically crashes and restarts nodes.
1770 pub fn with_churn(&mut self, config: ChurnConfig) -> &mut Self {
1771 self.churn_config = Some(config);
1772 self
1773 }
1774
1775 /// Inject a controllable hosting clock into every node this network builds,
1776 /// so hosting-cache TTL and subscription-lease eviction advance only when
1777 /// the sim advances the clock. Returns a handle the test can advance
1778 /// directly (e.g. before/after the run); for mid-run advancement under the
1779 /// turmoil-based `run_controlled_simulation`, schedule
1780 /// [`SimOperation::AdvanceHostingClock`] instead (the runner advances THIS
1781 /// same clock in-order with the other events).
1782 ///
1783 /// Enables deterministic eviction/TTL simulations (#4642 piece A) that were
1784 /// previously impossible because `HostingManager` hardcoded the wall clock.
1785 /// Pair with [`with_hosting_budget`](Self::with_hosting_budget) to force
1786 /// cache pressure. Idempotent: repeated calls reuse the existing clock.
1787 pub fn enable_hosting_time_control(
1788 &mut self,
1789 ) -> crate::util::time_source::SharedMockTimeSource {
1790 use crate::util::time_source::{DynTimeSource, SharedMockTimeSource};
1791 let clock = self
1792 .hosting_clock
1793 .get_or_insert_with(SharedMockTimeSource::new)
1794 .clone();
1795 // `config_gateways` / `config_nodes` already ran during
1796 // `SimNetwork::new()` with the override still `None`, so patch the
1797 // already-built node/gateway configs here (mirrors
1798 // `with_governance_config`). Without this the builders keep the
1799 // production wall clock and the injection is silently a no-op.
1800 let dyn_clock: DynTimeSource = std::sync::Arc::new(clock.clone());
1801 for (builder, _) in self.gateways.iter_mut() {
1802 builder.config.hosting_time_source_override = Some(dyn_clock.clone());
1803 }
1804 for (builder, _) in self.nodes.iter_mut() {
1805 builder.config.hosting_time_source_override = Some(dyn_clock.clone());
1806 }
1807 clock
1808 }
1809
1810 /// The controllable hosting clock, if [`enable_hosting_time_control`] was
1811 /// called. Advancing it moves every node's hosting TTL/eviction clock
1812 /// forward deterministically.
1813 ///
1814 /// [`enable_hosting_time_control`]: Self::enable_hosting_time_control
1815 pub fn hosting_clock(&self) -> Option<crate::util::time_source::SharedMockTimeSource> {
1816 self.hosting_clock.clone()
1817 }
1818
1819 /// Set the per-node hosting-cache byte budget (`max_hosting_storage`) for
1820 /// every node this network builds. A tiny budget forces cache pressure so a
1821 /// test can observe demand-driven eviction of low-demand contracts
1822 /// (#4642 piece A). Pair with [`enable_hosting_time_control`] so aged
1823 /// entries actually cross the TTL gate.
1824 ///
1825 /// [`enable_hosting_time_control`]: Self::enable_hosting_time_control
1826 pub fn with_hosting_budget(&mut self, budget_bytes: u64) -> &mut Self {
1827 self.hosting_budget_override = Some(budget_bytes);
1828 // Patch already-built configs (see `with_governance_config`).
1829 for (builder, _) in self.gateways.iter_mut() {
1830 std::sync::Arc::make_mut(&mut builder.config.config).max_hosting_storage = budget_bytes;
1831 }
1832 for (builder, _) in self.nodes.iter_mut() {
1833 std::sync::Arc::make_mut(&mut builder.config.config).max_hosting_storage = budget_bytes;
1834 }
1835 self
1836 }
1837
1838 /// Inject a governance-manager config override into every node this
1839 /// network builds. Compresses the production minute-to-hour governance
1840 /// timescales and lowers `min_samples` so the rate-limit → MAD → evict
1841 /// → ban chain can be exercised within a paused-time sim. Pair with
1842 /// `use_mock_wasm = true` so the production cost-reporting path feeds
1843 /// the detector. Test-only; see #4301.
1844 ///
1845 /// `allow(dead_code)`: consumed only by the `cfg(test)` governance
1846 /// e2e module, so the lib-only build sees it as unused.
1847 #[allow(dead_code)]
1848 pub(crate) fn with_governance_config(
1849 &mut self,
1850 config: crate::contract::governance::GovernanceConfig,
1851 ) -> &mut Self {
1852 self.governance_config_override = Some(config.clone());
1853 // `config_gateways` / `config_nodes` already ran during
1854 // `SimNetwork::new()`, when `governance_config_override` was still
1855 // `None`, so the builders captured `None`. Patch the already-built
1856 // node/gateway configs here so the override actually reaches each
1857 // node's `Ring::new` → `GovernanceManager`. Without this, the
1858 // builders keep the production default (e.g. `min_samples = 30`)
1859 // and the override is silently a no-op.
1860 for (builder, _) in self.gateways.iter_mut() {
1861 builder.config.governance_config_override = Some(config.clone());
1862 }
1863 for (builder, _) in self.nodes.iter_mut() {
1864 builder.config.governance_config_override = Some(config.clone());
1865 }
1866 self
1867 }
1868
1869 /// Enable the placement-migration (`SubscribeHint`) cascade for this
1870 /// simulation by lowering the version floor to
1871 /// [`Self::SIM_MIGRATION_ENABLED_FLOOR`] (`(0,0,0)`) on every node.
1872 ///
1873 /// Migration is OFF by default in every `SimNetwork` (the per-node floor
1874 /// defaults to the unreachable [`Self::SIM_MIGRATION_DISABLED_FLOOR`], so the
1875 /// `SubscribeHint` gate never fires — see `new_inner` and #4601). This makes
1876 /// the cascade genuinely opt-in: only a test that specifically exercises
1877 /// migration calls this to force it ON regardless of build version; every
1878 /// other sim is left untouched and cannot be perturbed by migration load.
1879 /// Like `with_governance_config`, this patches the already-built node/gateway
1880 /// configs (config_* ran during construction with the disabled default).
1881 /// Make [`run_controlled_simulation`](Self::run_controlled_simulation) wait
1882 /// for the ring to form before firing any scheduled operation: the
1883 /// controlled-event client blocks until at least `min_fraction` of this
1884 /// network's peers (gateways + nodes) have joined — registered a topology
1885 /// snapshot, which a peer only does once its own address is established
1886 /// (`peer_ready`) — or `max_wait` virtual time elapses, whichever comes
1887 /// first.
1888 ///
1889 /// Without this, operations fire after a fixed 3s warmup and race topology
1890 /// formation; a node whose one-shot, no-retry GET fires before it finishes
1891 /// joining is rejected with `PeerNotJoined` and never dispatches, so a
1892 /// cold-start reliability metric ends up measuring join speed rather than
1893 /// GET reliability. Opt-in; the default is the historical fixed-warmup
1894 /// behavior. See the `wait_for_join_before_ops` field.
1895 #[allow(dead_code)]
1896 pub fn wait_for_join_convergence_before_ops(
1897 &mut self,
1898 min_fraction: f64,
1899 max_wait: Duration,
1900 ) -> &mut Self {
1901 // Clamp to [0, 1]: a fraction > 1.0 would make the target unreachable
1902 // and always burn the full `max_wait` before proceeding.
1903 self.wait_for_join_before_ops = Some((min_fraction.clamp(0.0, 1.0), max_wait));
1904 self
1905 }
1906
1907 /// Override the delay the controlled-event client waits after triggering
1908 /// each scheduled *regular* operation (GET/PUT/etc.) before triggering the
1909 /// next. Default is a fixed 3s settle. Lower it for a test whose operations
1910 /// finish quickly against a formed network to avoid burning ~3s of virtual
1911 /// time — and the wall-clock to simulate it — per operation. Does not affect
1912 /// special in-client ops (clock advance, crash/recover). See the
1913 /// `controlled_op_interval` field.
1914 #[allow(dead_code)]
1915 pub fn with_controlled_op_interval(&mut self, interval: Duration) -> &mut Self {
1916 self.controlled_op_interval = Some(interval);
1917 self
1918 }
1919
1920 #[allow(dead_code)]
1921 pub fn enable_placement_migration(&mut self) -> &mut Self {
1922 let floor = Some(Self::SIM_MIGRATION_ENABLED_FLOOR);
1923 self.subscribe_hint_floor_override = floor;
1924 for (builder, _) in self.gateways.iter_mut() {
1925 builder.config.subscribe_hint_floor_override = floor;
1926 }
1927 for (builder, _) in self.nodes.iter_mut() {
1928 builder.config.subscribe_hint_floor_override = floor;
1929 }
1930 self
1931 }
1932
1933 /// Opt this simulation into advertising startup-hosted contracts
1934 /// (`SeedHostedContract`) through the neighbor-hosting mesh, so the
1935 /// terminal advertisement consult (hosting redesign piece C, invariant 5)
1936 /// can find a seeded off-path host.
1937 ///
1938 /// OFF by default: a seeded host does NOT advertise (the harness's
1939 /// historical behavior), so a key-routed GET to a non-hosting region still
1940 /// dead-ends — which the migration dead-end controls rely on. A test that
1941 /// exercises the consult opts in here. Patches the already-built
1942 /// node/gateway configs, mirroring
1943 /// [`enable_placement_migration`](Self::enable_placement_migration).
1944 #[allow(dead_code)]
1945 pub fn enable_seeded_host_advertisements(&mut self) -> &mut Self {
1946 for (builder, _) in self.gateways.iter_mut() {
1947 builder.config.advertise_seeded_hosts = true;
1948 }
1949 for (builder, _) in self.nodes.iter_mut() {
1950 builder.config.advertise_seeded_hosts = true;
1951 }
1952 self
1953 }
1954
1955 /// Force the placement-migration (`SubscribeHint`) cascade OFF for this
1956 /// simulation by pinning the per-node version floor to the unreachable
1957 /// [`Self::SIM_MIGRATION_DISABLED_FLOOR`].
1958 ///
1959 /// The mirror of [`enable_placement_migration`](Self::enable_placement_migration).
1960 /// Since #4601 the cascade is already OFF by default in every sim (the
1961 /// per-node floor defaults to this same unreachable value), so this is now
1962 /// belt-and-suspenders: it makes a test's "migration must stay off" premise
1963 /// explicit at the call site. Calling it is harmless and recommended for
1964 /// tests whose correctness depends on migration NOT running. Like
1965 /// `enable_placement_migration`, this patches the already-built node/gateway
1966 /// configs.
1967 #[allow(dead_code)]
1968 pub fn disable_placement_migration(&mut self) -> &mut Self {
1969 // Unreachable floor: no real or simulated peer version reaches it, so
1970 // `version_supports_subscribe_hint` always returns false → cascade off.
1971 let floor = Some(Self::SIM_MIGRATION_DISABLED_FLOOR);
1972 self.subscribe_hint_floor_override = floor;
1973 for (builder, _) in self.gateways.iter_mut() {
1974 builder.config.subscribe_hint_floor_override = floor;
1975 }
1976 for (builder, _) in self.nodes.iter_mut() {
1977 builder.config.subscribe_hint_floor_override = floor;
1978 }
1979 self
1980 }
1981
1982 /// Enable summary-first PUT (#4642, step 3-bis) for this simulation by
1983 /// lowering the probe version floor to [`Self::SIM_MIGRATION_ENABLED_FLOOR`]
1984 /// (`(0,0,0)`) on every node.
1985 ///
1986 /// Summary-first PUT is OFF by default in every `SimNetwork` (the
1987 /// per-node floor defaults to the unreachable
1988 /// [`Self::SIM_MIGRATION_DISABLED_FLOOR`] — see `new_inner`), so this
1989 /// makes the probe/dispatch cascade genuinely opt-in: only a test that
1990 /// specifically exercises it calls this to force it ON regardless of
1991 /// build version; every other sim is left untouched. Mirrors
1992 /// [`enable_placement_migration`](Self::enable_placement_migration) —
1993 /// same sentinel floors, same patch-already-built-configs shape, distinct
1994 /// override field (`summary_first_put_floor_override`).
1995 #[allow(dead_code)]
1996 pub fn enable_summary_first_put(&mut self) -> &mut Self {
1997 let floor = Some(Self::SIM_MIGRATION_ENABLED_FLOOR);
1998 self.summary_first_put_floor_override = floor;
1999 for (builder, _) in self.gateways.iter_mut() {
2000 builder.config.summary_first_put_floor_override = floor;
2001 }
2002 for (builder, _) in self.nodes.iter_mut() {
2003 builder.config.summary_first_put_floor_override = floor;
2004 }
2005 self
2006 }
2007
2008 /// State explicitly that this simulation exercises the hash-first summary
2009 /// exchange (#4965) by pinning the per-node floor to the always-passing
2010 /// [`Self::SIM_MIGRATION_ENABLED_FLOOR`].
2011 ///
2012 /// This is ALREADY the default for every `SimNetwork` (see `new_inner`),
2013 /// so calling it changes nothing — it exists so a test whose premise
2014 /// depends on hash-first being on says so at the call site instead of
2015 /// relying on a default that a future edit could flip out from under it.
2016 /// Mirrors [`disable_summary_first_put`](Self::disable_summary_first_put)'s
2017 /// belt-and-suspenders rationale, in the opposite direction.
2018 #[allow(dead_code)]
2019 pub fn enable_hash_first_summaries(&mut self) -> &mut Self {
2020 let floor = Some(Self::SIM_MIGRATION_ENABLED_FLOOR);
2021 self.hash_first_summaries_floor_override = floor;
2022 for (builder, _) in self.gateways.iter_mut() {
2023 builder.config.hash_first_summaries_floor_override = floor;
2024 }
2025 for (builder, _) in self.nodes.iter_mut() {
2026 builder.config.hash_first_summaries_floor_override = floor;
2027 }
2028 self
2029 }
2030
2031 /// Opt this simulation into the version-carrying connection ack (#5161) by
2032 /// pinning the per-node floor to the always-passing
2033 /// [`Self::SIM_MIGRATION_ENABLED_FLOOR`], so a joiner learns the version of
2034 /// the gateway (or ack-racing peer) it connects to.
2035 ///
2036 /// Genuinely opt-in, unlike
2037 /// [`enable_hash_first_summaries`](Self::enable_hash_first_summaries) which
2038 /// only restates a default. The reasoning is the fail-closed criterion this
2039 /// codebase already applies: hash-first is opt-OUT because it changes only
2040 /// the ENCODING of an exchange every sim already runs. This gate looks like
2041 /// that from the inside — it changes only how one ack is encoded — but its
2042 /// EFFECT is a cascade, because the version it teaches is the input to
2043 /// every other `version_supports_*` gate. Turning it on network-wide makes
2044 /// node->gateway links newly eligible for summary-first PUT probes and
2045 /// hash-first digests in whatever sims have those enabled, which is exactly
2046 /// the "piles load onto unrelated simulations" shape that
2047 /// `subscribe_hint_floor_override` and `summary_first_put_floor_override`
2048 /// default OFF to avoid. Measured, not assumed: defaulting it ON changed
2049 /// the outcome of several unrelated sims.
2050 ///
2051 /// Note this leaves the sim suite pinned OFF even after the production
2052 /// floor is reached, so sims and production diverge on this from 0.2.120
2053 /// onwards. That is the deliberate trade — the alternative is every sim
2054 /// silently changing behaviour at a release — and the coverage it costs is
2055 /// bought back by `test_joiner_records_gateway_version_through_sim_handshake`
2056 /// plus the transport-level tests in `connection_handler`.
2057 #[allow(dead_code)]
2058 pub fn enable_gateway_ack_version(&mut self) -> &mut Self {
2059 let floor = Some(Self::SIM_MIGRATION_ENABLED_FLOOR);
2060 self.ack_version_floor_override = floor;
2061 for (builder, _) in self.gateways.iter_mut() {
2062 builder.config.ack_version_floor_override = floor;
2063 }
2064 for (builder, _) in self.nodes.iter_mut() {
2065 builder.config.ack_version_floor_override = floor;
2066 }
2067 self
2068 }
2069
2070 /// Turn the originator target list (#5147) ON for this simulation by
2071 /// pinning the per-node version floor to the always-passing
2072 /// [`Self::SIM_MIGRATION_ENABLED_FLOOR`].
2073 ///
2074 /// Unlike [`enable_hash_first_summaries`](Self::enable_hash_first_summaries),
2075 /// this is NOT the default and the call is load-bearing: without it every
2076 /// peer falls back to the legacy `BroadcastTo` and the sim measures
2077 /// today's unsuppressed fan-out. That is exactly what the control arm of a
2078 /// suppression measurement wants, so the two arms differ by this one call.
2079 #[allow(dead_code)]
2080 pub fn enable_broadcast_target_list(&mut self) -> &mut Self {
2081 let floor = Some(Self::SIM_MIGRATION_ENABLED_FLOOR);
2082 self.broadcast_target_list_floor_override = floor;
2083 for (builder, _) in self.gateways.iter_mut() {
2084 builder.config.broadcast_target_list_floor_override = floor;
2085 }
2086 for (builder, _) in self.nodes.iter_mut() {
2087 builder.config.broadcast_target_list_floor_override = floor;
2088 }
2089 self
2090 }
2091
2092 /// Force the originator target list OFF for this simulation.
2093 ///
2094 /// Already the default (see `new_inner`); exists so the CONTROL arm of a
2095 /// suppression measurement states its premise at the call site rather than
2096 /// depending on a default a future edit could flip.
2097 #[allow(dead_code)]
2098 pub fn disable_broadcast_target_list(&mut self) -> &mut Self {
2099 let floor = Some(Self::SIM_MIGRATION_DISABLED_FLOOR);
2100 self.broadcast_target_list_floor_override = floor;
2101 for (builder, _) in self.gateways.iter_mut() {
2102 builder.config.broadcast_target_list_floor_override = floor;
2103 }
2104 for (builder, _) in self.nodes.iter_mut() {
2105 builder.config.broadcast_target_list_floor_override = floor;
2106 }
2107 self
2108 }
2109
2110 /// Force the hash-first summary exchange OFF for this simulation by
2111 /// pinning the per-node floor to the unreachable
2112 /// [`Self::SIM_MIGRATION_DISABLED_FLOOR`], so every peer falls back to the
2113 /// full-bytes `InterestMessage::Summaries`.
2114 ///
2115 /// This is the PRE-0.2.116 fleet: it reproduces what an un-upgraded peer
2116 /// does.
2117 ///
2118 /// Note the override is per-node (`builder.config`), so a genuinely MIXED
2119 /// simulation — one peer at the floor, one below — is constructible by
2120 /// setting the two builders differently rather than using this helper,
2121 /// which sets every node uniformly. No such test exists yet; the mixed
2122 /// case that IS covered today is incidental rather than constructed: unless
2123 /// a sim calls [`enable_gateway_ack_version`](Self::enable_gateway_ack_version),
2124 /// a regular node never learns its gateway's version, so every gateway link
2125 /// already runs digests one way and full bytes the other.
2126 #[allow(dead_code)]
2127 pub fn disable_hash_first_summaries(&mut self) -> &mut Self {
2128 let floor = Some(Self::SIM_MIGRATION_DISABLED_FLOOR);
2129 self.hash_first_summaries_floor_override = floor;
2130 for (builder, _) in self.gateways.iter_mut() {
2131 builder.config.hash_first_summaries_floor_override = floor;
2132 }
2133 for (builder, _) in self.nodes.iter_mut() {
2134 builder.config.hash_first_summaries_floor_override = floor;
2135 }
2136 self
2137 }
2138
2139 /// Force summary-first PUT OFF for this simulation by pinning the
2140 /// per-node probe version floor to the unreachable
2141 /// [`Self::SIM_MIGRATION_DISABLED_FLOOR`].
2142 ///
2143 /// The mirror of [`enable_summary_first_put`](Self::enable_summary_first_put),
2144 /// following [`disable_placement_migration`](Self::disable_placement_migration)'s
2145 /// belt-and-suspenders rationale: the cascade is already OFF by default in
2146 /// every sim, so this just makes a test's "summary-first PUT must stay
2147 /// off" premise explicit at the call site.
2148 #[allow(dead_code)]
2149 pub fn disable_summary_first_put(&mut self) -> &mut Self {
2150 let floor = Some(Self::SIM_MIGRATION_DISABLED_FLOOR);
2151 self.summary_first_put_floor_override = floor;
2152 for (builder, _) in self.gateways.iter_mut() {
2153 builder.config.summary_first_put_floor_override = floor;
2154 }
2155 for (builder, _) in self.nodes.iter_mut() {
2156 builder.config.summary_first_put_floor_override = floor;
2157 }
2158 self
2159 }
2160
2161 /// Resolve a label to its full [`PeerKeyLocation`] (pub_key + address) from
2162 /// the already-built node/gateway configs. Returns `None` if the label is
2163 /// not found among this network's nodes. Must be called before the builders
2164 /// are drained into the run loop (i.e. before `run_controlled_simulation`).
2165 fn peer_key_location_for(&self, label: &NodeLabel) -> Option<PeerKeyLocation> {
2166 let builder = self
2167 .gateways
2168 .iter()
2169 .map(|(b, cfg)| (b, &cfg.label))
2170 .chain(self.nodes.iter().map(|(b, l)| (b, l)))
2171 .find(|(_, l)| *l == label)
2172 .map(|(b, _)| b)?;
2173 let addr = std::net::SocketAddr::new(
2174 builder.config.network_listener_ip,
2175 builder.config.network_listener_port,
2176 );
2177 Some(PeerKeyLocation::new(
2178 builder.config.key_pair.public().clone(),
2179 addr,
2180 ))
2181 }
2182
2183 /// Preseed a **direct star** topology: inject a pre-formed ring connection
2184 /// between `host` and every label in `subscribers`, in BOTH directions, so
2185 /// that when the simulation starts the host already holds direct connections
2186 /// to all subscribers (and each subscriber to the host) — bypassing the
2187 /// organic ring-routed CONNECT handshake entirely.
2188 ///
2189 /// ## Why this exists (#4233)
2190 ///
2191 /// A faithful 40-80-subscriber direct star (the production-incident
2192 /// fan-out topology) cannot be bootstrapped organically in this Turmoil
2193 /// harness: ring-routed CONNECT cannot form that many direct connections to
2194 /// a single 1-gateway hub within the virtual-time budget — subscribers hit
2195 /// "at terminus, no uphill peers available — rejecting" and the simulation
2196 /// exhausts its whole schedule in topology formation before any UPDATE
2197 /// fires (N=8/16 form; N>=24 never finish). This primitive is the
2198 /// connection-side analogue of the `SeedHostedContract` contract preseed: it
2199 /// writes the ring connection state directly so the star exists at t=0 and
2200 /// the test can reach the sustained-fan-out broadcast phase it actually
2201 /// targets.
2202 ///
2203 /// ## What it injects — and what it leaves to the organic path
2204 ///
2205 /// Only the ring `Connection`/`Location` state is injected (via
2206 /// [`Ring::add_connection`](crate::ring::Ring::add_connection) at node
2207 /// startup). The underlying transport connection is materialized lazily on
2208 /// first send, and each subscriber's interest/subscription is registered
2209 /// organically when it runs its real `Subscribe` op (which now routes
2210 /// directly to the host over the injected connection). So the genuine
2211 /// subscribe + interest + broadcast machinery is exercised unchanged; only
2212 /// the CONNECT handshake is replaced. See
2213 /// `apply_preseeded_connections` for the full mechanism.
2214 ///
2215 /// ## Caller responsibilities
2216 ///
2217 /// - The host's `max_connections` must be `>= subscribers.len()` (plus slack
2218 /// for its own gateway-mesh connections), or the [`ConnectionManager`] cap
2219 /// will reject the overflow connections at injection time (logged + skipped,
2220 /// surfacing as a connectivity shortfall in the test's assertions).
2221 /// - Call this AFTER `SimNetwork::new*` and BEFORE
2222 /// `run_controlled_simulation` (it mutates the builders in place; once they
2223 /// are drained into the run loop the preseed list is consumed at startup).
2224 ///
2225 /// Panics if `host` or any subscriber label is not found in this network.
2226 pub fn preseed_direct_star(&mut self, host: &NodeLabel, subscribers: &[NodeLabel]) {
2227 let host_pkl = self
2228 .peer_key_location_for(host)
2229 .unwrap_or_else(|| panic!("preseed_direct_star: host {host:?} not found in network"));
2230
2231 // Resolve every subscriber up front so we fail fast on a bad label and
2232 // do not partially mutate the builders.
2233 let sub_pkls: Vec<(NodeLabel, PeerKeyLocation)> = subscribers
2234 .iter()
2235 .map(|label| {
2236 let pkl = self.peer_key_location_for(label).unwrap_or_else(|| {
2237 panic!("preseed_direct_star: subscriber {label:?} not found in network")
2238 });
2239 (label.clone(), pkl)
2240 })
2241 .collect();
2242
2243 for (sub_label, sub_pkl) in sub_pkls {
2244 // host -> subscriber
2245 self.push_preseed_connection(host, sub_pkl);
2246 // subscriber -> host
2247 self.push_preseed_connection(&sub_label, host_pkl.clone());
2248 }
2249 }
2250
2251 /// Append a single pre-formed connection to `label`'s builder. Internal
2252 /// helper for [`preseed_direct_star`](Self::preseed_direct_star).
2253 fn push_preseed_connection(&mut self, label: &NodeLabel, peer: PeerKeyLocation) {
2254 let builder = self
2255 .gateways
2256 .iter_mut()
2257 .find(|(_, cfg)| cfg.label == *label)
2258 .map(|(b, _)| b)
2259 .or_else(|| {
2260 self.nodes
2261 .iter_mut()
2262 .find(|(_, l)| *l == *label)
2263 .map(|(b, _)| b)
2264 })
2265 .unwrap_or_else(|| {
2266 panic!("push_preseed_connection: label {label:?} not found in network")
2267 });
2268 builder.preseed_connections.push(peer);
2269 }
2270
2271 /// Returns the VirtualTime instance for this simulation.
2272 ///
2273 /// VirtualTime is always enabled. Use this to advance time and control
2274 /// message delivery timing.
2275 ///
2276 /// # Example
2277 /// ```ignore
2278 /// let mut sim = SimNetwork::new(...).await;
2279 ///
2280 /// // Advance virtual time by 100ms
2281 /// sim.virtual_time().advance(Duration::from_millis(100));
2282 ///
2283 /// // Deliver pending messages
2284 /// let delivered = sim.advance_virtual_time();
2285 /// ```
2286 pub fn virtual_time(&self) -> &VirtualTime {
2287 &self.virtual_time
2288 }
2289
2290 /// Configures fault injection for the network simulation.
2291 ///
2292 /// This enables deterministic fault injection using the simulation framework's
2293 /// `FaultConfig`. Faults include:
2294 /// - Message drops (via `message_loss_rate`) - deterministic with seeded RNG
2295 /// - Network partitions
2296 /// - Node crashes
2297 /// - Latency injection (via `latency_range`)
2298 ///
2299 /// VirtualTime is automatically used for deterministic latency injection.
2300 ///
2301 /// # Example
2302 /// ```ignore
2303 /// use freenet::simulation::FaultConfig;
2304 /// use std::time::Duration;
2305 ///
2306 /// let mut sim = SimNetwork::new(...).await;
2307 /// sim.with_fault_injection(FaultConfig::builder()
2308 /// .message_loss_rate(0.1)
2309 /// .latency_range(Duration::from_millis(10)..Duration::from_millis(50))
2310 /// .build());
2311 ///
2312 /// // Advance time and deliver pending messages
2313 /// sim.virtual_time().advance(Duration::from_millis(100));
2314 /// sim.advance_virtual_time();
2315 /// ```
2316 pub fn with_fault_injection(&mut self, config: FaultConfig) {
2317 use crate::node::network_bridge::{FaultInjectorState, set_fault_injector};
2318 // Use a derived seed for fault injection to maintain determinism
2319 let fault_seed = self.seed.wrapping_add(0xFA01_7777);
2320 // Always use VirtualTime for deterministic behavior
2321 let state = FaultInjectorState::new(config, fault_seed)
2322 .with_virtual_time(self.virtual_time.clone());
2323 set_fault_injector(
2324 &self.name,
2325 Some(std::sync::Arc::new(std::sync::Mutex::new(state))),
2326 );
2327 }
2328
2329 /// Clears any configured fault injection and resets to default (VirtualTime only).
2330 pub fn clear_fault_injection(&mut self) {
2331 self.init_default_fault_injection();
2332 }
2333
2334 /// Configures fault injection with a custom VirtualTime instance.
2335 ///
2336 /// This is useful if you want to use a shared VirtualTime across multiple
2337 /// simulations or have more control over time advancement.
2338 ///
2339 /// For most cases, use [`with_fault_injection`](Self::with_fault_injection) instead,
2340 /// which uses the built-in VirtualTime.
2341 #[deprecated(
2342 since = "0.1.0",
2343 note = "VirtualTime is now always enabled. Use with_fault_injection() and virtual_time() instead."
2344 )]
2345 pub fn with_fault_injection_virtual_time(
2346 &mut self,
2347 config: FaultConfig,
2348 virtual_time: VirtualTime,
2349 ) {
2350 use crate::node::network_bridge::{FaultInjectorState, set_fault_injector};
2351 let fault_seed = self.seed.wrapping_add(0xFA01_7777);
2352 let state = FaultInjectorState::new(config, fault_seed).with_virtual_time(virtual_time);
2353 set_fault_injector(
2354 &self.name,
2355 Some(std::sync::Arc::new(std::sync::Mutex::new(state))),
2356 );
2357 }
2358
2359 /// Advances virtual time and delivers pending messages.
2360 ///
2361 /// This should be called after advancing the VirtualTime instance to deliver
2362 /// messages whose deadlines have passed.
2363 ///
2364 /// Returns the number of messages delivered.
2365 pub fn advance_virtual_time(&mut self) -> usize {
2366 use crate::node::network_bridge::get_fault_injector;
2367 if let Some(injector) = get_fault_injector(&self.name) {
2368 let mut state = injector.lock().unwrap();
2369 state.advance_time()
2370 } else {
2371 0
2372 }
2373 }
2374
2375 /// Advances virtual time by the given duration and delivers pending messages.
2376 ///
2377 /// This is a convenience method combining `virtual_time().advance()` and
2378 /// `advance_virtual_time()`.
2379 ///
2380 /// Returns the number of messages delivered.
2381 pub fn advance_time(&mut self, duration: Duration) -> usize {
2382 self.virtual_time.advance(duration);
2383 self.advance_virtual_time()
2384 }
2385
2386 /// Returns the current network statistics from fault injection.
2387 ///
2388 /// These statistics track:
2389 /// - Messages sent, delivered, dropped
2390 /// - Drop reasons (loss rate, partition, crash)
2391 /// - Latency injection metrics
2392 pub fn get_network_stats(&self) -> Option<crate::node::network_bridge::NetworkStats> {
2393 use crate::node::network_bridge::get_fault_injector;
2394 get_fault_injector(&self.name).map(|injector| {
2395 let state = injector.lock().unwrap();
2396 state.stats().clone()
2397 })
2398 }
2399
2400 /// Resets the network statistics.
2401 pub fn reset_network_stats(&mut self) {
2402 use crate::node::network_bridge::get_fault_injector;
2403 if let Some(injector) = get_fault_injector(&self.name) {
2404 let mut state = injector.lock().unwrap();
2405 state.reset_stats();
2406 }
2407 }
2408
2409 // =========================================================================
2410 // Node Lifecycle Management (Crash/Restart)
2411 // =========================================================================
2412
2413 /// Crashes a node, stopping its execution and blocking all messages to/from it.
2414 ///
2415 /// The node's task is aborted and all messages to/from its address are dropped.
2416 /// In-flight operations will fail. Use [`restart_node`](Self::restart_node) to
2417 /// bring the node back (note: restart is not yet implemented).
2418 ///
2419 /// # Example
2420 /// ```ignore
2421 /// let mut sim = SimNetwork::new(...).await;
2422 /// let handles = sim.start_with_rand_gen::<SmallRng>(seed, 10, 5).await;
2423 ///
2424 /// // Crash node "node-0"
2425 /// sim.crash_node(&NodeLabel::node(0));
2426 ///
2427 /// // Messages to/from this node will be dropped
2428 /// // Other nodes should handle the failure gracefully
2429 /// ```
2430 pub fn crash_node(&mut self, label: &NodeLabel) -> bool {
2431 use crate::node::network_bridge::get_fault_injector;
2432
2433 // Get the node's address
2434 let addr = match self.node_addresses.get(label) {
2435 Some(addr) => *addr,
2436 None => {
2437 tracing::warn!(?label, "Cannot crash node: address not found");
2438 return false;
2439 }
2440 };
2441
2442 // Mark as crashed in fault injector
2443 if let Some(injector) = get_fault_injector(&self.name) {
2444 let mut state = injector.lock().unwrap();
2445 state.config.crash_node(addr);
2446 tracing::info!(?label, ?addr, "Node marked as crashed in fault injector");
2447 }
2448
2449 // Abort the running task if we have a handle
2450 if let Some(running) = self.running_nodes.remove(label) {
2451 running.abort_handle.abort();
2452 tracing::info!(?label, "Node task aborted");
2453 true
2454 } else {
2455 tracing::warn!(
2456 ?label,
2457 "Node not found in running_nodes (may not be started yet)"
2458 );
2459 false
2460 }
2461 }
2462
2463 /// Recovers a crashed node, allowing messages to flow again.
2464 ///
2465 /// This removes the node from the crashed set, but does NOT restart the node's
2466 /// execution. The node will not process messages, but other nodes can attempt
2467 /// to reach it (messages won't be dropped due to crash status).
2468 ///
2469 /// For full restart, see the roadmap in `docs/architecture/simulation-testing-design.md`.
2470 pub fn recover_node(&mut self, label: &NodeLabel) -> bool {
2471 use crate::node::network_bridge::get_fault_injector;
2472
2473 let addr = match self.node_addresses.get(label) {
2474 Some(addr) => addr,
2475 None => {
2476 tracing::warn!(?label, "Cannot recover node: address not found");
2477 return false;
2478 }
2479 };
2480
2481 if let Some(injector) = get_fault_injector(&self.name) {
2482 let mut state = injector.lock().unwrap();
2483 state.config.recover_node(addr);
2484 tracing::info!(
2485 ?label,
2486 ?addr,
2487 "Node recovered (no longer marked as crashed)"
2488 );
2489 true
2490 } else {
2491 false
2492 }
2493 }
2494
2495 /// Checks if a node is currently marked as crashed.
2496 pub fn is_node_crashed(&self, label: &NodeLabel) -> bool {
2497 use crate::node::network_bridge::get_fault_injector;
2498
2499 let addr = match self.node_addresses.get(label) {
2500 Some(addr) => addr,
2501 None => return false,
2502 };
2503
2504 if let Some(injector) = get_fault_injector(&self.name) {
2505 let state = injector.lock().unwrap();
2506 state.config.is_crashed(addr)
2507 } else {
2508 false
2509 }
2510 }
2511
2512 /// Returns the socket address for a node label, if known.
2513 pub fn node_address(&self, label: &NodeLabel) -> Option<SocketAddr> {
2514 self.node_addresses.get(label).copied()
2515 }
2516
2517 /// Returns all node labels and their addresses.
2518 pub fn all_node_addresses(&self) -> &HashMap<NodeLabel, SocketAddr> {
2519 &self.node_addresses
2520 }
2521
2522 /// Restarts a crashed node, preserving its identity.
2523 ///
2524 /// This method:
2525 /// 1. Retrieves the saved configuration (keypair, data directory, location)
2526 /// 2. Unregisters the old peer from the transport layer
2527 /// 3. Creates a new node with the same identity
2528 /// 4. Starts the node task
2529 ///
2530 /// # What's Preserved
2531 /// - **Keypair/identity**: Same public key and address
2532 /// - **Data directory path**: Same location for any disk-based storage
2533 /// - **Network location**: Same ring location
2534 /// - **Gateway configs**: Same gateway connections
2535 ///
2536 /// # What's NOT Preserved (Current Limitation)
2537 /// - **In-memory state**: Memory caches are lost on crash
2538 /// - **In-flight transactions**: Any pending operations are lost
2539 /// - **Contract state**: Currently stored on disk (SQLite), which may or may not
2540 /// survive the abrupt task abort depending on flush timing
2541 ///
2542 /// # Future Work
2543 /// For truly deterministic state persistence, we need to implement shared
2544 /// in-memory storage using `MockStateStorage` instead of SQLite. This would
2545 /// require making `Executor` generic over the storage type.
2546 ///
2547 /// # Arguments
2548 /// * `label` - The label of the node to restart
2549 /// * `seed` - Seed for random event generation
2550 /// * `max_contract_num` - Maximum number of contracts for event generation
2551 /// * `iterations` - Number of iterations for event generation
2552 ///
2553 /// # Returns
2554 /// * `Some(JoinHandle)` if restart was successful
2555 /// * `None` if the node config was not found or restart failed
2556 ///
2557 /// # Example
2558 /// ```ignore
2559 /// // Crash a node
2560 /// sim.crash_node(&label);
2561 /// assert!(sim.is_node_crashed(&label));
2562 ///
2563 /// // Restart it with same identity
2564 /// let handle = sim.restart_node::<SmallRng>(&label, 0x5678, 10, 5).await;
2565 /// assert!(handle.is_some());
2566 /// assert!(!sim.is_node_crashed(&label));
2567 /// ```
2568 pub async fn restart_node<R>(
2569 &mut self,
2570 label: &NodeLabel,
2571 seed: u64,
2572 max_contract_num: usize,
2573 iterations: usize,
2574 ) -> Option<tokio::task::JoinHandle<anyhow::Result<()>>>
2575 where
2576 R: crate::client_events::test::RandomEventGenerator + Send + 'static,
2577 {
2578 use crate::node::network_bridge::get_fault_injector;
2579 use crate::transport::in_memory_socket::unregister_socket;
2580
2581 // Get the saved restartable config
2582 let restart_config = match self.restartable_configs.get(label) {
2583 Some(config) => config.clone(),
2584 None => {
2585 tracing::warn!(?label, "Cannot restart node: config not found");
2586 return None;
2587 }
2588 };
2589
2590 // Get the node address
2591 let node_addr = match self.node_addresses.get(label) {
2592 Some(addr) => *addr,
2593 None => {
2594 tracing::warn!(?label, "Cannot restart node: address not found");
2595 return None;
2596 }
2597 };
2598
2599 tracing::info!(?label, ?node_addr, "Restarting node with persisted state");
2600
2601 // Unregister the old socket from transport (the new node will re-register)
2602 unregister_socket(&self.name, &node_addr);
2603
2604 // Clear crash status in fault injector
2605 if let Some(injector) = get_fault_injector(&self.name) {
2606 let mut state = injector.lock().unwrap();
2607 state.config.recover_node(&node_addr);
2608 }
2609
2610 // Create a new Builder with the saved configuration
2611 let event_listener = {
2612 #[cfg(feature = "trace-ot")]
2613 {
2614 use crate::tracing::OTEventRegister;
2615 CombinedRegister::new([
2616 self.event_listener.trait_clone(),
2617 Box::new(OTEventRegister::new()),
2618 ])
2619 }
2620 #[cfg(not(feature = "trace-ot"))]
2621 {
2622 self.event_listener.clone()
2623 }
2624 };
2625
2626 let builder = Builder::build(
2627 restart_config.config.clone(),
2628 event_listener,
2629 format!("{}-{}", self.name, label),
2630 restart_config.rng_seed,
2631 self.name.clone(),
2632 );
2633
2634 // Calculate total peers for event generation params
2635 let total_peer_num = self.labels.len();
2636
2637 // Create user events for the restarted node
2638 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
2639 self.receiver_ch.clone(),
2640 restart_config.config.key_pair.public().clone(),
2641 seed,
2642 );
2643 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
2644
2645 // Create the appropriate span
2646 let span = if restart_config.is_gateway {
2647 tracing::info_span!("in_mem_gateway_restart", %label)
2648 } else {
2649 tracing::info_span!("in_mem_node_restart", %label)
2650 };
2651
2652 // Start the node task with shared storage for state persistence
2653 let shared_storage = restart_config.shared_storage.clone();
2654 let node_task = async move {
2655 builder
2656 .run_node_with_shared_storage(user_events, span, shared_storage)
2657 .await
2658 };
2659 let handle = GlobalExecutor::spawn(node_task);
2660
2661 // Track the new running node
2662 self.running_nodes.insert(
2663 label.clone(),
2664 RunningNode {
2665 label: label.clone(),
2666 addr: node_addr,
2667 abort_handle: handle.abort_handle(),
2668 },
2669 );
2670
2671 tracing::info!(?label, "Node restarted successfully with persisted state");
2672 Some(handle)
2673 }
2674
2675 #[allow(dead_code)]
2676 pub(crate) fn connection_manager(&self, label: &NodeLabel) -> Option<&ConnectionManager> {
2677 self.connection_managers.get(label)
2678 }
2679
2680 pub fn has_connection_or_pending(&self, label: &NodeLabel, addr: SocketAddr) -> Option<bool> {
2681 self.connection_managers
2682 .get(label)
2683 .map(|cm| cm.has_connection_or_pending(addr))
2684 }
2685
2686 /// Injects a pending reservation backdated by `age`, simulating a failed ConnectOp.
2687 pub fn inject_stale_reservation(
2688 &self,
2689 label: &NodeLabel,
2690 addr: SocketAddr,
2691 location: Location,
2692 age: Duration,
2693 ) -> bool {
2694 if let Some(cm) = self.connection_managers.get(label) {
2695 let created = tokio::time::Instant::now() - age;
2696 cm.inject_reservation(addr, location, created);
2697 true
2698 } else {
2699 false
2700 }
2701 }
2702
2703 pub fn connection_count(&self, label: &NodeLabel) -> Option<usize> {
2704 self.connection_managers
2705 .get(label)
2706 .map(|cm| cm.connection_count())
2707 }
2708
2709 /// Returns the number of pending reservations (including stale ones) for a node.
2710 pub fn reserved_connections_count(&self, label: &NodeLabel) -> Option<usize> {
2711 self.connection_managers
2712 .get(label)
2713 .map(|cm| cm.get_reserved_connections())
2714 }
2715
2716 /// Runs cleanup on stale pending reservations for a node.
2717 /// Returns the number of stale entries removed.
2718 pub fn cleanup_stale_reservations(&self, label: &NodeLabel) -> Option<usize> {
2719 self.connection_managers
2720 .get(label)
2721 .map(|cm| cm.cleanup_stale_reservations())
2722 }
2723
2724 /// Tests whether the node's connection manager would accept a new connection
2725 /// from the given address and location.
2726 pub fn should_accept(
2727 &self,
2728 label: &NodeLabel,
2729 location: Location,
2730 addr: SocketAddr,
2731 ) -> Option<bool> {
2732 self.connection_managers
2733 .get(label)
2734 .map(|cm| cm.should_accept(location, addr))
2735 }
2736
2737 /// Checks if a node has a saved configuration for restart.
2738 pub fn can_restart(&self, label: &NodeLabel) -> bool {
2739 self.restartable_configs.contains_key(label)
2740 }
2741
2742 #[allow(unused)]
2743 pub fn debug(&mut self) {
2744 self.clean_up_tmp_dirs = false;
2745 }
2746
2747 /// Derives a deterministic port from seed and peer index for simulation.
2748 /// Uses ports in the dynamic range (49152-65535) to avoid conflicts.
2749 fn derive_deterministic_port(&self, peer_index: usize) -> u16 {
2750 const BASE_PORT: u16 = 50000;
2751 const PORT_RANGE: u16 = 10000;
2752 // Use peer seed to get a deterministic offset
2753 let peer_seed = self.derive_peer_seed(peer_index);
2754 BASE_PORT + ((peer_seed % PORT_RANGE as u64) as u16)
2755 }
2756
2757 async fn config_gateways(&mut self, num: NonZeroUsize) {
2758 info!("Building {} gateways", num);
2759 let mut configs = Vec::with_capacity(num.into());
2760 for node_no in 0..num.into() {
2761 let label = NodeLabel::gateway(&self.name, node_no);
2762 // Use deterministic port for simulation instead of querying system
2763 let port = self.derive_deterministic_port(node_no);
2764 let keypair = crate::transport::TransportKeypair::new();
2765 let addr: SocketAddr = (Ipv6Addr::LOCALHOST, port).into();
2766 let peer_key_location = PeerKeyLocation::new(keypair.public().clone(), addr);
2767 // Use location computed from address for consistency with PeerKeyLocation::location()
2768 let location = Location::from_address(&addr);
2769
2770 // Track address for crash/restart operations
2771 self.node_addresses.insert(label.clone(), addr);
2772
2773 let config_args = ConfigArgs {
2774 id: Some(format!("{label}")),
2775 mode: Some(OperationMode::Local),
2776 network_api: crate::config::NetworkArgs {
2777 // Default to usize::MAX so tests don't trigger streaming unless
2778 // explicitly opted in via with_streaming_threshold().
2779 streaming_threshold: Some(self.streaming_threshold.unwrap_or(usize::MAX)),
2780 ..Default::default()
2781 },
2782 ..Default::default()
2783 };
2784 // TODO: it may be unnecessary use config_args.build() for the simulation. Related with the TODO in Config line 238
2785 let mut config = NodeConfig::new(config_args.build().await.unwrap())
2786 .await
2787 .unwrap();
2788 config.governance_config_override = self.governance_config_override.clone();
2789 config.subscribe_hint_floor_override = self.subscribe_hint_floor_override;
2790 config.summary_first_put_floor_override = self.summary_first_put_floor_override;
2791 config.hash_first_summaries_floor_override = self.hash_first_summaries_floor_override;
2792 config.ack_version_floor_override = self.ack_version_floor_override;
2793 config.broadcast_target_list_floor_override = self.broadcast_target_list_floor_override;
2794 config.hosting_time_source_override = self
2795 .hosting_clock
2796 .clone()
2797 .map(|c| std::sync::Arc::new(c) as crate::util::time_source::DynTimeSource);
2798 if let Some(budget) = self.hosting_budget_override {
2799 std::sync::Arc::make_mut(&mut config.config).max_hosting_storage = budget;
2800 }
2801 config.key_pair = keypair;
2802 config.network_listener_ip = Ipv6Addr::LOCALHOST.into();
2803 config.network_listener_port = port;
2804 config.with_own_addr(addr);
2805 config
2806 .with_location(location)
2807 .max_hops_to_live(self.ring_max_htl)
2808 .max_number_of_connections(self.max_connections)
2809 .min_number_of_connections(self.min_connections)
2810 .is_gateway()
2811 .rnd_if_htl_above(self.rnd_if_htl_above);
2812 // Disable readiness gating in SimNetwork by default
2813 config.relay_ready_connections = Some(0);
2814 self.event_listener
2815 .add_node(label.clone(), config.key_pair.public().clone());
2816 configs.push((
2817 config,
2818 GatewayConfig {
2819 label,
2820 peer_key_location,
2821 location,
2822 },
2823 ));
2824 }
2825 // All gateways are passive - they don't initiate connections to other gateways.
2826 // This ensures deterministic startup order where only regular nodes initiate connections.
2827 for config in &mut configs {
2828 config.0.should_connect = false;
2829 }
2830
2831 let gateways: Vec<_> = configs.iter().map(|(_, gw)| gw.clone()).collect();
2832 // Store all gateway configs for use when restarting non-gateway nodes
2833 self.all_gateway_configs = gateways.clone();
2834
2835 // Note: Gateways don't need to know about each other - they are passive entry points.
2836 // Only regular nodes need to know about gateways. This simplifies the topology and
2837 // improves determinism by avoiding gateway-to-gateway connection attempts.
2838 for (this_node, this_config) in configs {
2839 let event_listener = {
2840 #[cfg(feature = "trace-ot")]
2841 {
2842 use crate::tracing::OTEventRegister;
2843 CombinedRegister::new([
2844 self.event_listener.trait_clone(),
2845 Box::new(OTEventRegister::new()),
2846 ])
2847 }
2848 #[cfg(not(feature = "trace-ot"))]
2849 {
2850 self.event_listener.clone()
2851 }
2852 };
2853 let peer_seed = self.derive_peer_seed(this_config.label.number());
2854 let gateway = Builder::build(
2855 this_node,
2856 event_listener,
2857 format!("{}-{label}", self.name, label = this_config.label),
2858 peer_seed,
2859 self.name.clone(),
2860 );
2861 self.gateways.push((gateway, this_config));
2862 }
2863 }
2864
2865 async fn config_nodes(&mut self, num: usize) {
2866 info!("Building {} regular nodes", num);
2867 let gateways: Vec<_> = self
2868 .gateways
2869 .iter()
2870 .map(|(_node, config)| config)
2871 .cloned()
2872 .collect();
2873
2874 for node_no in self.number_of_gateways..num + self.number_of_gateways {
2875 let label = NodeLabel::node(&self.name, node_no);
2876
2877 let config_args = ConfigArgs {
2878 id: Some(format!("{label}")),
2879 mode: Some(OperationMode::Local),
2880 network_api: crate::config::NetworkArgs {
2881 // Default to usize::MAX so tests don't trigger streaming unless
2882 // explicitly opted in via with_streaming_threshold().
2883 streaming_threshold: Some(self.streaming_threshold.unwrap_or(usize::MAX)),
2884 ..Default::default()
2885 },
2886 ..Default::default()
2887 };
2888 let mut config = NodeConfig::new(config_args.build().await.unwrap())
2889 .await
2890 .unwrap();
2891 config.governance_config_override = self.governance_config_override.clone();
2892 config.subscribe_hint_floor_override = self.subscribe_hint_floor_override;
2893 config.summary_first_put_floor_override = self.summary_first_put_floor_override;
2894 config.hash_first_summaries_floor_override = self.hash_first_summaries_floor_override;
2895 config.ack_version_floor_override = self.ack_version_floor_override;
2896 config.broadcast_target_list_floor_override = self.broadcast_target_list_floor_override;
2897 config.hosting_time_source_override = self
2898 .hosting_clock
2899 .clone()
2900 .map(|c| std::sync::Arc::new(c) as crate::util::time_source::DynTimeSource);
2901 if let Some(budget) = self.hosting_budget_override {
2902 std::sync::Arc::make_mut(&mut config.config).max_hosting_storage = budget;
2903 }
2904 for GatewayConfig {
2905 peer_key_location,
2906 location,
2907 ..
2908 } in &gateways
2909 {
2910 config.add_gateway(InitPeerNode::new(peer_key_location.clone(), *location));
2911 }
2912 // Use an explicit per-node port when the test requested specific
2913 // ring locations (see `new_with_node_locations`); otherwise the
2914 // deterministic derived port. Either way location is computed from
2915 // the address below, so it stays consistent with what other peers
2916 // compute from this peer's address.
2917 let port = match &self.node_port_override {
2918 Some(ports) => ports[node_no - self.number_of_gateways],
2919 None => self.derive_deterministic_port(node_no),
2920 };
2921 let addr: SocketAddr = (Ipv6Addr::LOCALHOST, port).into();
2922 // Use location computed from address for consistency with PeerKeyLocation::location().
2923 // This ensures the stored location matches what other peers compute when looking at our address.
2924 let location = Location::from_address(&addr);
2925 config.network_listener_port = port;
2926 config.network_listener_ip = Ipv6Addr::LOCALHOST.into();
2927 config.key_pair = crate::transport::TransportKeypair::new();
2928 config.with_own_addr(addr);
2929 config
2930 .with_location(location)
2931 .max_hops_to_live(self.ring_max_htl)
2932 .rnd_if_htl_above(self.rnd_if_htl_above)
2933 .max_number_of_connections(self.max_connections);
2934 // Disable readiness gating in SimNetwork by default
2935 config.relay_ready_connections = Some(0);
2936
2937 // Track address for crash/restart operations
2938 self.node_addresses.insert(label.clone(), addr);
2939
2940 self.event_listener
2941 .add_node(label.clone(), config.key_pair.public().clone());
2942
2943 let event_listener = {
2944 #[cfg(feature = "trace-ot")]
2945 {
2946 use crate::tracing::OTEventRegister;
2947 CombinedRegister::new([
2948 self.event_listener.trait_clone(),
2949 Box::new(OTEventRegister::new()),
2950 ])
2951 }
2952 #[cfg(not(feature = "trace-ot"))]
2953 {
2954 self.event_listener.clone()
2955 }
2956 };
2957 let peer_seed = self.derive_peer_seed(node_no);
2958 let node = Builder::build(
2959 config,
2960 event_listener,
2961 format!("{}-{label}", self.name),
2962 peer_seed,
2963 self.name.clone(),
2964 );
2965 self.nodes.push((node, label));
2966 }
2967 }
2968
2969 pub async fn start_with_rand_gen<R>(
2970 &mut self,
2971 seed: u64,
2972 max_contract_num: usize,
2973 iterations: usize,
2974 ) -> Vec<tokio::task::JoinHandle<anyhow::Result<()>>>
2975 where
2976 R: RandomEventGenerator + Send + 'static,
2977 {
2978 use crate::ring::topology_registry::set_current_network_name;
2979 use crate::transport::in_memory_socket::is_socket_registered;
2980
2981 // Set the current network name so Ring can register topology snapshots
2982 set_current_network_name(&self.name);
2983
2984 let total_peer_num = self.gateways.len() + self.nodes.len();
2985 let mut peers = vec![];
2986
2987 // Phase 1: Start all gateways first and collect their addresses
2988 let gateways: Vec<_> = self.gateways.drain(..).collect();
2989 let mut gateway_addrs = Vec::with_capacity(gateways.len());
2990
2991 for (mut node, config) in gateways {
2992 let label = config.label.clone();
2993 // Use the peer_key_location address from GatewayConfig - this is the address
2994 // that will be registered in the peer registry
2995 let gateway_addr = *config
2996 .peer_key_location
2997 .peer_addr
2998 .as_known()
2999 .expect("Gateway should have known address");
3000 gateway_addrs.push(gateway_addr);
3001
3002 tracing::debug!(peer = %label, addr = %gateway_addr, "starting gateway");
3003
3004 // Create shared in-memory storage for this node (persists across restarts)
3005 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
3006
3007 // Save restartable config BEFORE starting (NodeConfig gets consumed)
3008 self.restartable_configs.insert(
3009 label.clone(),
3010 RestartableNodeConfig {
3011 config: node.config.clone(),
3012 label: label.clone(),
3013 is_gateway: true,
3014 gateway_configs: self.all_gateway_configs.clone(),
3015 rng_seed: node.rng_seed,
3016 shared_storage: shared_storage.clone(),
3017 },
3018 );
3019
3020 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
3021 self.receiver_ch.clone(),
3022 node.config.key_pair.public().clone(),
3023 seed,
3024 );
3025 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
3026 let span = tracing::info_span!("in_mem_gateway", %label);
3027 self.labels
3028 .push((label.clone(), node.config.key_pair.public().clone()));
3029
3030 let shared_cm: Arc<parking_lot::Mutex<Option<ConnectionManager>>> =
3031 Arc::new(parking_lot::Mutex::new(None));
3032 node.shared_cm = Some(shared_cm.clone());
3033
3034 // Use shared in-memory storage for state persistence across restarts
3035 let node_task = async move {
3036 node.run_node_with_shared_storage(user_events, span, shared_storage)
3037 .await
3038 };
3039 let handle = GlobalExecutor::spawn(node_task);
3040
3041 // Track running node for crash/restart
3042 self.running_nodes.insert(
3043 label.clone(),
3044 RunningNode {
3045 label: label.clone(),
3046 addr: gateway_addr,
3047 abort_handle: handle.abort_handle(),
3048 },
3049 );
3050
3051 peers.push(handle);
3052
3053 let captured_cm = capture_shared_slot(&shared_cm, self.start_backoff, &label)
3054 .await
3055 .unwrap_or_else(|| {
3056 panic!(
3057 "SimNetwork: node {label} failed to publish ConnectionManager \
3058 within the capture budget. This means the spawned `run_node` \
3059 task panicked before reaching the `shared_cm` write at the top \
3060 of run_node_with_shared_storage, or the task was never scheduled \
3061 at all. Check preceding logs for the real failure."
3062 )
3063 });
3064 self.connection_managers.insert(label, captured_cm);
3065 }
3066
3067 // Phase 2: Wait for all gateways to be registered in the peer registry
3068 // This prevents the race condition where regular nodes try to connect
3069 // before gateways are ready to receive connections
3070 let registration_timeout = Duration::from_secs(10);
3071 let poll_interval = Duration::from_millis(10);
3072 // Use tokio::time::Instant instead of std::time::Instant to work correctly
3073 // with start_paused = true in tests. std::time::Instant uses wall-clock time
3074 // which doesn't advance when tokio's time is paused.
3075 let start = tokio::time::Instant::now();
3076
3077 'wait_loop: loop {
3078 let mut all_registered = true;
3079 for addr in &gateway_addrs {
3080 if !is_socket_registered(&self.name, addr) {
3081 all_registered = false;
3082 break;
3083 }
3084 }
3085
3086 if all_registered {
3087 tracing::debug!("All {} gateways registered", gateway_addrs.len());
3088 // Give gateways additional time to fully initialize their event loops
3089 // before regular nodes start connecting
3090 tokio::time::sleep(Duration::from_millis(100)).await;
3091 tracing::debug!("Starting regular nodes");
3092 break 'wait_loop;
3093 }
3094
3095 if start.elapsed() > registration_timeout {
3096 tracing::warn!(
3097 "Timeout waiting for gateway registration, some may not be ready. \
3098 Registered: {}/{}",
3099 gateway_addrs
3100 .iter()
3101 .filter(|a| is_socket_registered(&self.name, a))
3102 .count(),
3103 gateway_addrs.len()
3104 );
3105 break 'wait_loop;
3106 }
3107
3108 tokio::time::sleep(poll_interval).await;
3109 }
3110
3111 // Phase 3: Start all regular nodes
3112 let nodes: Vec<_> = self.nodes.drain(..).collect();
3113 for (mut node, label) in nodes {
3114 // Get node address from tracked addresses
3115 let node_addr = self
3116 .node_addresses
3117 .get(&label)
3118 .copied()
3119 .expect("Node address should be tracked");
3120
3121 tracing::debug!(peer = %label, addr = %node_addr, "starting regular node");
3122
3123 // Create shared in-memory storage for this node (persists across restarts)
3124 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
3125
3126 // Save restartable config BEFORE starting (NodeConfig gets consumed)
3127 self.restartable_configs.insert(
3128 label.clone(),
3129 RestartableNodeConfig {
3130 config: node.config.clone(),
3131 label: label.clone(),
3132 is_gateway: false,
3133 gateway_configs: self.all_gateway_configs.clone(),
3134 rng_seed: node.rng_seed,
3135 shared_storage: shared_storage.clone(),
3136 },
3137 );
3138
3139 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
3140 self.receiver_ch.clone(),
3141 node.config.key_pair.public().clone(),
3142 seed,
3143 );
3144 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
3145 let span = tracing::info_span!("in_mem_node", %label);
3146 self.labels
3147 .push((label.clone(), node.config.key_pair.public().clone()));
3148
3149 let shared_cm: Arc<parking_lot::Mutex<Option<ConnectionManager>>> =
3150 Arc::new(parking_lot::Mutex::new(None));
3151 node.shared_cm = Some(shared_cm.clone());
3152
3153 // Use shared in-memory storage for state persistence across restarts
3154 let node_task = async move {
3155 node.run_node_with_shared_storage(user_events, span, shared_storage)
3156 .await
3157 };
3158 let handle = GlobalExecutor::spawn(node_task);
3159
3160 // Track running node for crash/restart
3161 self.running_nodes.insert(
3162 label.clone(),
3163 RunningNode {
3164 label: label.clone(),
3165 addr: node_addr,
3166 abort_handle: handle.abort_handle(),
3167 },
3168 );
3169
3170 peers.push(handle);
3171
3172 let captured_cm = capture_shared_slot(&shared_cm, self.start_backoff, &label)
3173 .await
3174 .unwrap_or_else(|| {
3175 panic!(
3176 "SimNetwork: node {label} failed to publish ConnectionManager \
3177 within the capture budget. This means the spawned `run_node` \
3178 task panicked before reaching the `shared_cm` write at the top \
3179 of run_node_with_shared_storage, or the task was never scheduled \
3180 at all. Check preceding logs for the real failure."
3181 )
3182 });
3183 self.connection_managers.insert(label, captured_cm);
3184 }
3185
3186 self.labels.sort_by(|(a, _), (b, _)| a.cmp(b));
3187 peers
3188 }
3189
3190 /// Starts the network with controlled events instead of random event generation.
3191 ///
3192 /// This method allows tests to specify exact operations to execute on specific nodes,
3193 /// enabling precise testing of scenarios like subscription topology formation.
3194 ///
3195 /// # Arguments
3196 ///
3197 /// * `operations` - A list of `(NodeLabel, SimOperation)` pairs specifying which
3198 /// operations to execute on which nodes.
3199 ///
3200 /// # Returns
3201 ///
3202 /// A vector of join handles for the node tasks and the number of operations scheduled.
3203 ///
3204 /// # Example
3205 ///
3206 /// ```ignore
3207 /// let contract = SimOperation::create_test_contract(42);
3208 /// let operations = vec![
3209 /// ScheduledOperation::new(
3210 /// NodeLabel::gateway("test", 0),
3211 /// SimOperation::Put {
3212 /// contract: contract.clone(),
3213 /// state: vec![1, 2, 3],
3214 /// subscribe: true,
3215 /// }
3216 /// ),
3217 /// ScheduledOperation::new(
3218 /// NodeLabel::node("test", 0),
3219 /// SimOperation::Subscribe {
3220 /// contract_id: *contract.key().id(),
3221 /// }
3222 /// ),
3223 /// ];
3224 ///
3225 /// let (handles, num_ops) = sim.start_with_controlled_events(operations).await;
3226 /// ```
3227 #[cfg(any(test, feature = "testing"))]
3228 pub async fn start_with_controlled_events(
3229 &mut self,
3230 operations: Vec<ScheduledOperation>,
3231 ) -> (Vec<tokio::task::JoinHandle<anyhow::Result<()>>>, usize) {
3232 use crate::ring::topology_registry::set_current_network_name;
3233 use crate::transport::in_memory_socket::is_socket_registered;
3234
3235 // Set the current network name so Ring can register topology snapshots
3236 set_current_network_name(&self.name);
3237
3238 let num_operations = operations.len();
3239 let mut peers = vec![];
3240
3241 // Build a map of label -> list of (event_id, operation)
3242 let mut operations_by_node: HashMap<NodeLabel, Vec<(EventId, SimOperation)>> =
3243 HashMap::new();
3244 for (event_id, scheduled_op) in operations.into_iter().enumerate() {
3245 operations_by_node
3246 .entry(scheduled_op.node)
3247 .or_default()
3248 .push((event_id as EventId, scheduled_op.operation));
3249 }
3250
3251 // Phase 1: Start all gateways first and collect their addresses
3252 let gateways: Vec<_> = self.gateways.drain(..).collect();
3253 let mut gateway_addrs = Vec::with_capacity(gateways.len());
3254
3255 for (node, config) in gateways {
3256 let label = config.label.clone();
3257 let gateway_addr = *config
3258 .peer_key_location
3259 .peer_addr
3260 .as_known()
3261 .expect("Gateway should have known address");
3262 gateway_addrs.push(gateway_addr);
3263
3264 tracing::debug!(peer = %label, addr = %gateway_addr, "starting gateway with controlled events");
3265
3266 // Create shared in-memory storage for this node (persists across restarts)
3267 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
3268
3269 // Save restartable config BEFORE starting (NodeConfig gets consumed)
3270 self.restartable_configs.insert(
3271 label.clone(),
3272 RestartableNodeConfig {
3273 config: node.config.clone(),
3274 label: label.clone(),
3275 is_gateway: true,
3276 gateway_configs: self.all_gateway_configs.clone(),
3277 rng_seed: node.rng_seed,
3278 shared_storage: shared_storage.clone(),
3279 },
3280 );
3281
3282 // Create MemoryEventsGen without RNG (deterministic mode)
3283 let mut user_events = MemoryEventsGen::new(
3284 self.receiver_ch.clone(),
3285 node.config.key_pair.public().clone(),
3286 );
3287
3288 // Populate events for this node
3289 if let Some(node_ops) = operations_by_node.remove(&label) {
3290 let events: Vec<_> = node_ops
3291 .into_iter()
3292 .map(|(id, op)| (id, op.into_client_request()))
3293 .collect();
3294 user_events.generate_events(events);
3295 }
3296
3297 let span = tracing::info_span!("in_mem_gateway_controlled", %label);
3298 self.labels
3299 .push((label.clone(), node.config.key_pair.public().clone()));
3300
3301 // Use shared in-memory storage for state persistence across restarts
3302 let node_task = async move {
3303 node.run_node_with_shared_storage(user_events, span, shared_storage)
3304 .await
3305 };
3306 let handle = GlobalExecutor::spawn(node_task);
3307
3308 // Track running node for crash/restart
3309 self.running_nodes.insert(
3310 label.clone(),
3311 RunningNode {
3312 label: label.clone(),
3313 addr: gateway_addr,
3314 abort_handle: handle.abort_handle(),
3315 },
3316 );
3317
3318 peers.push(handle);
3319
3320 tokio::time::sleep(self.start_backoff).await;
3321 }
3322
3323 // Phase 2: Wait for all gateways to be registered
3324 let registration_timeout = Duration::from_secs(10);
3325 let poll_interval = Duration::from_millis(10);
3326 let start = tokio::time::Instant::now();
3327
3328 'wait_loop: loop {
3329 let mut all_registered = true;
3330 for addr in &gateway_addrs {
3331 if !is_socket_registered(&self.name, addr) {
3332 all_registered = false;
3333 break;
3334 }
3335 }
3336
3337 if all_registered {
3338 tracing::debug!(
3339 "All {} gateways registered in {:?}",
3340 gateway_addrs.len(),
3341 start.elapsed()
3342 );
3343 break 'wait_loop;
3344 }
3345
3346 if start.elapsed() > registration_timeout {
3347 tracing::warn!(
3348 "Timeout waiting for gateway registration. {} of {} registered.",
3349 gateway_addrs
3350 .iter()
3351 .filter(|a| is_socket_registered(&self.name, a))
3352 .count(),
3353 gateway_addrs.len()
3354 );
3355 break 'wait_loop;
3356 }
3357
3358 tokio::time::sleep(poll_interval).await;
3359 }
3360
3361 // Phase 3: Start regular nodes
3362 for (node, label) in std::mem::take(&mut self.nodes) {
3363 let node_addr = *self
3364 .node_addresses
3365 .get(&label)
3366 .expect("Node address should be tracked");
3367
3368 tracing::debug!(peer = %label, addr = %node_addr, "starting regular node with controlled events");
3369
3370 // Create shared in-memory storage for this node (persists across restarts)
3371 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
3372
3373 // Save restartable config BEFORE starting (NodeConfig gets consumed)
3374 self.restartable_configs.insert(
3375 label.clone(),
3376 RestartableNodeConfig {
3377 config: node.config.clone(),
3378 label: label.clone(),
3379 is_gateway: false,
3380 gateway_configs: self.all_gateway_configs.clone(),
3381 rng_seed: node.rng_seed,
3382 shared_storage: shared_storage.clone(),
3383 },
3384 );
3385
3386 // Create MemoryEventsGen without RNG (deterministic mode)
3387 let mut user_events = MemoryEventsGen::new(
3388 self.receiver_ch.clone(),
3389 node.config.key_pair.public().clone(),
3390 );
3391
3392 // Populate events for this node
3393 if let Some(node_ops) = operations_by_node.remove(&label) {
3394 let events: Vec<_> = node_ops
3395 .into_iter()
3396 .map(|(id, op)| (id, op.into_client_request()))
3397 .collect();
3398 user_events.generate_events(events);
3399 }
3400
3401 let span = tracing::info_span!("in_mem_node_controlled", %label);
3402 self.labels
3403 .push((label.clone(), node.config.key_pair.public().clone()));
3404
3405 // Use shared in-memory storage for state persistence across restarts
3406 let node_task = async move {
3407 node.run_node_with_shared_storage(user_events, span, shared_storage)
3408 .await
3409 };
3410 let handle = GlobalExecutor::spawn(node_task);
3411
3412 // Track running node for crash/restart
3413 self.running_nodes.insert(
3414 label.clone(),
3415 RunningNode {
3416 label: label.clone(),
3417 addr: node_addr,
3418 abort_handle: handle.abort_handle(),
3419 },
3420 );
3421
3422 peers.push(handle);
3423
3424 tokio::time::sleep(self.start_backoff).await;
3425 }
3426
3427 // Warn if there were operations for unknown nodes
3428 if !operations_by_node.is_empty() {
3429 let unknown_nodes: Vec<_> = operations_by_node.keys().collect();
3430 tracing::warn!(
3431 "Operations scheduled for unknown nodes: {:?}",
3432 unknown_nodes
3433 );
3434 }
3435
3436 self.labels.sort_by(|(a, _), (b, _)| a.cmp(b));
3437 (peers, num_operations)
3438 }
3439
3440 /// Creates an event chain for controlled events.
3441 ///
3442 /// Unlike the standard `event_chain()`, this allows specifying the exact
3443 /// sequence of events to trigger, enabling deterministic testing.
3444 ///
3445 /// # Arguments
3446 ///
3447 /// * `event_sequence` - Pairs of `(EventId, NodeLabel)` specifying which events
3448 /// to trigger on which nodes, in order.
3449 ///
3450 /// # Returns
3451 ///
3452 /// A ControlledEventChain that can be used as a Stream.
3453 pub fn controlled_event_chain(
3454 &mut self,
3455 event_sequence: Vec<(EventId, NodeLabel)>,
3456 ) -> ControlledEventChain {
3457 let user_ev_controller = self
3458 .user_ev_controller
3459 .take()
3460 .expect("controller should be set");
3461 let label_to_key: HashMap<_, _> = self.labels.iter().cloned().collect();
3462 ControlledEventChain::new(user_ev_controller, event_sequence, label_to_key)
3463 }
3464
3465 /// Returns the locations of all peers (gateways + nodes) without consuming them.
3466 pub fn get_peer_locations(&self) -> Vec<f64> {
3467 let mut locations = Vec::new();
3468 for (builder, _) in &self.gateways {
3469 if let Some(loc) = &builder.config.location {
3470 locations.push(loc.as_f64());
3471 }
3472 }
3473 for (builder, _) in &self.nodes {
3474 if let Some(loc) = &builder.config.location {
3475 locations.push(loc.as_f64());
3476 }
3477 }
3478 locations
3479 }
3480
3481 /// Builds peer nodes and returns the controller to trigger events.
3482 pub fn build_peers(&mut self) -> Vec<(NodeLabel, NodeConfig)> {
3483 let gw = self.gateways.drain(..).map(|(n, c)| (n, c.label));
3484 let mut peers = vec![];
3485 for (builder, label) in gw.chain(self.nodes.drain(..)).collect::<Vec<_>>() {
3486 let pub_key = builder.config.key_pair.public();
3487 self.labels.push((label.clone(), pub_key.clone()));
3488 peers.push((label, builder.config));
3489 }
3490 self.labels.sort_by(|(a, _), (b, _)| a.cmp(b));
3491 peers.sort_by(|(a, _), (b, _)| a.cmp(b));
3492 peers
3493 }
3494
3495 /// Returns the connectivity in the network per peer (that is all the connections
3496 /// this peers has registered).
3497 pub fn node_connectivity(
3498 &self,
3499 ) -> HashMap<NodeLabel, (TransportPublicKey, HashMap<NodeLabel, Distance>)> {
3500 let mut peers_connections = HashMap::with_capacity(self.labels.len());
3501 let key_to_label: HashMap<_, _> = self.labels.iter().map(|(k, v)| (v, k)).collect();
3502 for (label, key) in &self.labels {
3503 let conns = self
3504 .event_listener
3505 .connections(key)
3506 .map(|(k, d)| (key_to_label[k.pub_key()].clone(), d))
3507 .collect::<HashMap<_, _>>();
3508 peers_connections.insert(label.clone(), (key.clone(), conns));
3509 }
3510 peers_connections
3511 }
3512
3513 /// Returns the set of neighbor public keys for a given node label.
3514 ///
3515 /// Uses the event listener's `connections()` to get the current peer set,
3516 /// returning just the `TransportPublicKey`s (without distances).
3517 pub fn neighbor_peer_keys(&self, label: &NodeLabel) -> Option<HashSet<TransportPublicKey>> {
3518 let key = self
3519 .labels
3520 .iter()
3521 .find(|(l, _)| l == label)
3522 .map(|(_, k)| k)?;
3523 Some(
3524 self.event_listener
3525 .connections(key)
3526 .map(|(k, _)| k.pub_key().clone())
3527 .collect(),
3528 )
3529 }
3530
3531 /// Start an event chain for this simulation. Allows passing a different controller for the peers.
3532 ///
3533 /// This method borrows the SimNetwork, allowing you to call verification methods
3534 /// (like `check_convergence()`, `get_operation_summary()`) after events complete.
3535 ///
3536 /// If done make sure you set the proper receiving side for the controller. For example in the
3537 /// nodes built through the [`build_peers`](`Self::build_peers`) method.
3538 ///
3539 /// # Example
3540 /// ```ignore
3541 /// let mut stream = sim.event_chain(100, None);
3542 /// while stream.next().await.is_some() {
3543 /// tokio::time::sleep(Duration::from_millis(100)).await;
3544 /// }
3545 /// drop(stream); // Drop the stream before verification
3546 /// let result = sim.check_convergence().await; // sim is still usable!
3547 /// ```
3548 pub fn event_chain(
3549 &mut self,
3550 total_events: u32,
3551 controller: Option<watch::Sender<(EventId, TransportPublicKey)>>,
3552 ) -> EventChain {
3553 let user_ev_controller = controller.unwrap_or_else(|| {
3554 self.user_ev_controller
3555 .take()
3556 .expect("controller should be set")
3557 });
3558 // Clone labels - SimNetwork retains ownership for verification methods
3559 let labels = self.labels.clone();
3560 // EventChain no longer handles cleanup - SimNetwork's Drop does
3561 EventChain::new(labels, user_ev_controller, total_events, false)
3562 }
3563
3564 /// Consumes the SimNetwork and returns an event chain.
3565 ///
3566 /// Use this when you don't need to access SimNetwork after events complete.
3567 /// For post-event verification, use [`event_chain`](Self::event_chain) instead.
3568 #[deprecated(
3569 since = "0.1.0",
3570 note = "Use event_chain(&mut self) instead to retain access to SimNetwork for verification"
3571 )]
3572 pub fn into_event_chain(
3573 mut self,
3574 total_events: u32,
3575 controller: Option<watch::Sender<(EventId, TransportPublicKey)>>,
3576 ) -> EventChain {
3577 let user_ev_controller = controller.unwrap_or_else(|| {
3578 self.user_ev_controller
3579 .take()
3580 .expect("controller should be set")
3581 });
3582 let labels = std::mem::take(&mut self.labels);
3583 let debug_val = self.clean_up_tmp_dirs;
3584 self.clean_up_tmp_dirs = false; // EventChain handles cleanup
3585 EventChain::new(labels, user_ev_controller, total_events, debug_val)
3586 }
3587
3588 /// Checks that all peers in the network have acquired at least one connection to any
3589 /// other peers.
3590 ///
3591 /// This function advances VirtualTime to allow connection messages to be delivered.
3592 pub async fn check_connectivity(&mut self, time_out: Duration) -> anyhow::Result<()> {
3593 self.connectivity(time_out, 1.0).await
3594 }
3595
3596 /// Checks that a percentage (given as a float between 0 and 1) of the nodes has at least
3597 /// one connection to any other peers.
3598 ///
3599 /// This function advances VirtualTime to allow connection messages to be delivered.
3600 pub async fn check_partial_connectivity(
3601 &mut self,
3602 time_out: Duration,
3603 percent: f64,
3604 ) -> anyhow::Result<()> {
3605 self.connectivity(time_out, percent).await
3606 }
3607
3608 /// Internal connectivity check that advances VirtualTime.
3609 ///
3610 /// Issue #2725: The previous implementation used std::time::Instant (real wall-clock time)
3611 /// but never advanced VirtualTime. Since simulations use VirtualTime for deterministic
3612 /// message delivery, connection handshake messages were never delivered, causing all
3613 /// connectivity checks to fail with "found disconnected nodes".
3614 ///
3615 /// This implementation advances VirtualTime in small steps while checking connectivity,
3616 /// allowing connection messages to be delivered and processed.
3617 async fn connectivity(&mut self, time_out: Duration, percent: f64) -> anyhow::Result<()> {
3618 let num_nodes = self.number_of_nodes;
3619 let mut connected = HashSet::new();
3620 let elapsed = std::time::Instant::now();
3621
3622 // Time step for advancing VirtualTime - small enough for responsiveness,
3623 // large enough to batch message delivery efficiently
3624 let time_step = Duration::from_millis(100);
3625
3626 while elapsed.elapsed() < time_out && (connected.len() as f64 / num_nodes as f64) < percent
3627 {
3628 // Advance VirtualTime to trigger message delivery
3629 // This is critical for VirtualTime-based simulations where messages
3630 // are scheduled for delivery at VirtualTime + latency
3631 self.advance_time(time_step);
3632
3633 // Yield to tokio so tasks can process delivered messages
3634 tokio::task::yield_now().await;
3635
3636 // Small real-time sleep to allow task scheduling without spinning CPU
3637 tokio::time::sleep(Duration::from_millis(10)).await;
3638
3639 // Check which nodes have connected
3640 for node in self.number_of_gateways..num_nodes + self.number_of_gateways {
3641 if !connected.contains(&node)
3642 && self.is_connected(&NodeLabel::node(&self.name, node))
3643 {
3644 connected.insert(node);
3645 }
3646 }
3647 }
3648
3649 let expected =
3650 HashSet::from_iter(self.number_of_gateways..num_nodes + self.number_of_gateways);
3651 let mut missing: Vec<_> = expected
3652 .difference(&connected)
3653 .map(|n| format!("{}-node-{n}", self.name))
3654 .collect();
3655
3656 tracing::info!("Number of simulated nodes: {num_nodes}");
3657
3658 // Calculate the percentage of nodes that are connected
3659 let connected_percent = connected.len() as f64 / num_nodes as f64;
3660 // Fail if fewer nodes are connected than the required percentage (with tolerance)
3661 if connected_percent < (percent - 0.01/* 1% error tolerance */) {
3662 missing.sort();
3663 let show_max = missing.len().min(100);
3664 tracing::error!("Nodes without connection: {:?}(..)", &missing[..show_max],);
3665 tracing::error!(
3666 "Total nodes without connection: {:?}, ({:.1}% connected < {:.1}% required)",
3667 missing.len(),
3668 connected_percent * 100.0,
3669 percent * 100.0
3670 );
3671 anyhow::bail!("found disconnected nodes");
3672 }
3673
3674 tracing::info!(
3675 "Required time for connecting all peers: {} secs",
3676 elapsed.elapsed().as_secs()
3677 );
3678
3679 Ok(())
3680 }
3681
3682 pub fn is_connected(&self, peer: &NodeLabel) -> bool {
3683 let pos = self
3684 .labels
3685 .binary_search_by(|(label, _)| label.cmp(peer))
3686 .expect("peer not found");
3687 self.event_listener.is_connected(&self.labels[pos].1)
3688 }
3689
3690 /// Returns a copy of all network event logs captured during the simulation.
3691 ///
3692 /// These logs can be used to verify deterministic behavior by comparing
3693 /// event sequences across runs with the same seed.
3694 pub async fn get_event_logs(&self) -> Vec<crate::tracing::NetLogMessage> {
3695 self.event_listener.logs.lock().await.clone()
3696 }
3697
3698 /// Returns event logs filtered and sorted for deterministic comparison.
3699 ///
3700 /// Events are sorted by (transaction, peer_addr, event_kind_name).
3701 /// Timestamps are ignored since they may vary between runs.
3702 pub async fn get_deterministic_event_summary(&self) -> Vec<EventSummary> {
3703 let logs = self.event_listener.logs.lock().await;
3704 let mut summaries: Vec<EventSummary> = logs
3705 .iter()
3706 .map(|log| {
3707 let event_detail = format!("{:?}", log.kind);
3708 // Use the structured variant_name() method instead of parsing debug output
3709 let event_kind_name = log.kind.variant_name().to_string();
3710 // Extract contract key using the structured accessor
3711 let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
3712 // Extract state hash using the structured accessor
3713 let state_hash = log.kind.state_hash().map(String::from);
3714 EventSummary {
3715 tx: log.tx,
3716 peer_addr: log.peer_id.socket_addr(),
3717 event_kind_name,
3718 contract_key,
3719 state_hash,
3720 event_detail,
3721 }
3722 })
3723 .collect();
3724 // Sort for deterministic comparison
3725 summaries.sort();
3726 summaries
3727 }
3728
3729 /// Counts events by type for quick comparison.
3730 pub async fn get_event_counts(&self) -> std::collections::HashMap<String, usize> {
3731 let logs = self.event_listener.logs.lock().await;
3732 let mut counts = std::collections::HashMap::new();
3733 for log in logs.iter() {
3734 // Use the structured variant_name() method instead of parsing debug output
3735 let key = log.kind.variant_name();
3736 *counts.entry(key.to_string()).or_insert(0) += 1;
3737 }
3738 counts
3739 }
3740
3741 /// Returns a handle to the event logs that can be accessed after `run_simulation` consumes `self`.
3742 ///
3743 /// This is useful for tests that need to compare event logs between runs when using
3744 /// Turmoil's deterministic scheduler via `run_simulation()`.
3745 ///
3746 /// # Example
3747 /// ```ignore
3748 /// let sim = SimNetwork::new(...).await;
3749 /// let logs_handle = sim.event_logs_handle();
3750 ///
3751 /// sim.run_simulation::<SmallRng, _, _>(...)?;
3752 ///
3753 /// // Access logs after simulation completes
3754 /// let logs = logs_handle.lock().await;
3755 /// ```
3756 pub fn event_logs_handle(&self) -> Arc<tokio::sync::Mutex<Vec<crate::tracing::NetLogMessage>>> {
3757 self.event_listener.logs.clone()
3758 }
3759
3760 /// Recommended to calling after `check_connectivity` to ensure enough time
3761 /// elapsed for all peers to become connected.
3762 ///
3763 /// Checks that there is a good connectivity over the simulated network,
3764 /// meaning that:
3765 ///
3766 /// - at least 50% of the peers have more than the minimum connections
3767 /// - the average number of connections per peer is above the mean between max and min connections
3768 pub fn network_connectivity_quality(&self) -> anyhow::Result<()> {
3769 const HIGHER_THAN_MIN_THRESHOLD: f64 = 0.5;
3770 let num_nodes = self.number_of_nodes;
3771
3772 // Guard against division by zero
3773 if num_nodes == 0 {
3774 anyhow::bail!("cannot check connectivity quality with zero nodes");
3775 }
3776
3777 let min_connections_threshold = (num_nodes as f64 * HIGHER_THAN_MIN_THRESHOLD) as usize;
3778 let node_connectivity = self.node_connectivity();
3779
3780 let mut connections_per_peer: Vec<_> = node_connectivity
3781 .iter()
3782 .map(|(k, v)| (k, v.1.len()))
3783 .filter(|&(k, _)| !k.is_gateway())
3784 .map(|(_, v)| v)
3785 .collect();
3786
3787 // Guard against empty connections list
3788 if connections_per_peer.is_empty() {
3789 anyhow::bail!("no non-gateway nodes found for connectivity check");
3790 }
3791
3792 // ensure at least "most" normal nodes have more than one connection
3793 connections_per_peer.sort_unstable_by_key(|num_conn| *num_conn);
3794 if connections_per_peer
3795 .get(min_connections_threshold)
3796 .copied()
3797 .unwrap_or(0)
3798 < self.min_connections
3799 {
3800 tracing::error!(
3801 "Low connectivity; more than {:.0}% of the nodes don't have more than minimum connections",
3802 HIGHER_THAN_MIN_THRESHOLD * 100.0
3803 );
3804 anyhow::bail!("low connectivity");
3805 } else {
3806 let idx = connections_per_peer[min_connections_threshold..]
3807 .iter()
3808 .position(|num_conn| *num_conn < self.min_connections)
3809 .unwrap_or_else(|| connections_per_peer[min_connections_threshold..].len() - 1)
3810 + (min_connections_threshold - 1);
3811 let percentile = idx as f64 / connections_per_peer.len() as f64 * 100.0;
3812 tracing::info!("{percentile:.0}% nodes have higher than required minimum connections");
3813 }
3814
3815 // ensure the average number of connections per peer is above the mean between max and min connections
3816 let expected_avg_connections =
3817 ((self.max_connections - self.min_connections) / 2) + self.min_connections;
3818 let avg_connections: usize = connections_per_peer.iter().sum::<usize>() / num_nodes;
3819 if avg_connections < expected_avg_connections {
3820 tracing::warn!(
3821 "Average number of connections ({avg_connections}) is low (< {expected_avg_connections})"
3822 );
3823 }
3824 Ok(())
3825 }
3826
3827 /// Checks convergence of contract states across all peers.
3828 ///
3829 /// Returns a `ConvergenceResult` containing:
3830 /// - Which contracts have converged (all replicas have the same state hash)
3831 /// - Which contracts have not converged (different state hashes across replicas)
3832 /// - Per-contract details of state hashes per peer
3833 ///
3834 /// This is useful for testing eventual consistency properties.
3835 ///
3836 /// # Implementation Note
3837 ///
3838 /// This method iterates through logs in insertion order (chronological order)
3839 /// rather than sorting by transaction ID. This is critical because broadcast
3840 /// events are logged with the sender's original transaction ID, not a new
3841 /// local timestamp. Sorting by transaction ID would cause delayed broadcasts
3842 /// with older transaction IDs to appear before newer local updates in the
3843 /// sorted order, resulting in incorrect "latest state" detection.
3844 ///
3845 /// For example, if Peer A updates to state S3 (tx=T15) and then receives a
3846 /// delayed broadcast of state S1 (tx=T1 < T15), the actual state is S1 (the
3847 /// broadcast was applied), but tx-sorted order would show S3 as "latest".
3848 pub async fn check_convergence(&self) -> ConvergenceResult {
3849 // Get logs in insertion order (chronological order within the simulation).
3850 // DO NOT use get_deterministic_event_summary() here - it sorts by transaction ID,
3851 // which is incorrect for determining latest state because broadcasts use the
3852 // sender's original transaction ID rather than a local timestamp.
3853 let logs = self.event_listener.logs.lock().await;
3854
3855 // Group (contract_key -> peer_addr -> latest_state_hash)
3856 // Use BTreeMap for deterministic iteration order in DST
3857 let mut contract_states: BTreeMap<String, BTreeMap<SocketAddr, String>> = BTreeMap::new();
3858
3859 // Iterate in insertion order - the last event for each (contract, peer) pair
3860 // is the actual current state.
3861 //
3862 // IMPORTANT: Use stored_state_hash() instead of state_hash() to only consider
3863 // events that represent actual stored state (PutSuccess, UpdateSuccess, BroadcastApplied).
3864 // Using state_hash() would incorrectly include BroadcastReceived events which record
3865 // the incoming state hash BEFORE it's applied, not the actual stored state.
3866 for log in logs.iter() {
3867 let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
3868 let state_hash = log.kind.stored_state_hash().map(String::from);
3869
3870 if let (Some(contract_key), Some(state_hash)) = (contract_key, state_hash) {
3871 // Keep the latest state for each peer/contract pair
3872 contract_states
3873 .entry(contract_key)
3874 .or_default()
3875 .insert(log.peer_id.socket_addr(), state_hash);
3876 }
3877 }
3878
3879 let mut converged = Vec::new();
3880 let mut diverged = Vec::new();
3881
3882 for (contract_key, peer_states) in contract_states {
3883 if peer_states.len() < 2 {
3884 // Need at least 2 peers to check convergence
3885 continue;
3886 }
3887
3888 let unique_states: HashSet<&String> = peer_states.values().collect();
3889
3890 if unique_states.len() == 1 {
3891 let state = unique_states.into_iter().next().unwrap().clone();
3892 converged.push(ConvergedContract {
3893 contract_key,
3894 state_hash: state,
3895 replica_count: peer_states.len(),
3896 });
3897 } else {
3898 diverged.push(DivergedContract {
3899 contract_key,
3900 peer_states: peer_states.into_iter().collect(),
3901 });
3902 }
3903 }
3904
3905 ConvergenceResult {
3906 converged,
3907 diverged,
3908 }
3909 }
3910
3911 /// Waits for convergence of contract states, polling at regular intervals.
3912 ///
3913 /// # Arguments
3914 /// * `timeout` - Maximum time to wait for convergence
3915 /// * `poll_interval` - How often to check convergence
3916 /// * `min_contracts` - Minimum number of contracts that must be replicated (default: 1)
3917 ///
3918 /// # Returns
3919 /// * `Ok(ConvergenceResult)` - If convergence achieved (no diverged contracts)
3920 /// * `Err(ConvergenceResult)` - If timeout reached with diverged contracts
3921 ///
3922 /// # Example
3923 /// ```ignore
3924 /// let result = sim.await_convergence(
3925 /// Duration::from_secs(30),
3926 /// Duration::from_millis(500),
3927 /// 1,
3928 /// ).await;
3929 ///
3930 /// match result {
3931 /// Ok(r) => println!("{} contracts converged", r.converged.len()),
3932 /// Err(r) => panic!("{} contracts still diverged", r.diverged.len()),
3933 /// }
3934 /// ```
3935 pub async fn await_convergence(
3936 &self,
3937 timeout: Duration,
3938 poll_interval: Duration,
3939 min_contracts: usize,
3940 ) -> Result<ConvergenceResult, ConvergenceResult> {
3941 // Use tokio::time::Instant for deterministic behavior in simulation
3942 let start = tokio::time::Instant::now();
3943
3944 loop {
3945 let result = self.check_convergence().await;
3946
3947 // Check if we have enough contracts and all have converged
3948 let total_replicated = result.converged.len() + result.diverged.len();
3949 if total_replicated >= min_contracts && result.diverged.is_empty() {
3950 tracing::info!(
3951 "Convergence achieved: {} contracts converged in {:?}",
3952 result.converged.len(),
3953 start.elapsed()
3954 );
3955 return Ok(result);
3956 }
3957
3958 // Check timeout
3959 if start.elapsed() >= timeout {
3960 tracing::warn!(
3961 "Convergence timeout after {:?}: {} converged, {} diverged",
3962 timeout,
3963 result.converged.len(),
3964 result.diverged.len()
3965 );
3966 return Err(result);
3967 }
3968
3969 tokio::time::sleep(poll_interval).await;
3970 }
3971 }
3972
3973 /// Asserts that all replicated contracts have converged.
3974 ///
3975 /// This is a convenience method that combines `await_convergence` with
3976 /// an assertion. It panics if convergence is not achieved.
3977 ///
3978 /// # Panics
3979 /// Panics if convergence is not achieved within the timeout.
3980 pub async fn assert_convergence(&self, timeout: Duration, poll_interval: Duration) {
3981 match self.await_convergence(timeout, poll_interval, 1).await {
3982 Ok(result) => {
3983 tracing::info!(
3984 "Convergence assertion passed: {} contracts",
3985 result.converged.len()
3986 );
3987 }
3988 Err(result) => {
3989 let diverged_details: Vec<String> = result
3990 .diverged
3991 .iter()
3992 .map(|d| {
3993 format!(
3994 "Contract {}: {} different states across {} peers",
3995 d.contract_key,
3996 d.unique_state_count(),
3997 d.peer_states.len()
3998 )
3999 })
4000 .collect();
4001
4002 panic!(
4003 "Convergence assertion failed after {:?}: {} contracts diverged\n{}",
4004 timeout,
4005 result.diverged.len(),
4006 diverged_details.join("\n")
4007 );
4008 }
4009 }
4010 }
4011
4012 /// Returns the convergence rate as a ratio (0.0 to 1.0).
4013 ///
4014 /// Convergence rate = converged_contracts / total_replicated_contracts
4015 pub async fn convergence_rate(&self) -> f64 {
4016 let result = self.check_convergence().await;
4017 let total = result.converged.len() + result.diverged.len();
4018 if total == 0 {
4019 return 1.0; // No contracts replicated yet
4020 }
4021 result.converged.len() as f64 / total as f64
4022 }
4023
4024 /// Run the state verifier on all collected telemetry events.
4025 ///
4026 /// This linearizes the state transitions for every contract across all peers
4027 /// and detects anomalies such as:
4028 /// - Missing broadcasts (emitted but never received)
4029 /// - Unapplied broadcasts (received but never applied)
4030 /// - Unexpected state changes (merge produced an unknown hash)
4031 /// - Final divergence (peers disagree after all events) with root-cause analysis
4032 ///
4033 /// Unlike `check_convergence()`, which only checks the final state snapshot,
4034 /// this traces the full causal history to pinpoint WHERE divergence originated.
4035 ///
4036 /// # Example
4037 /// ```ignore
4038 /// let report = sim.verify_state().await;
4039 /// if !report.is_clean() {
4040 /// eprintln!("{}", report.display());
4041 /// for anomaly in &report.anomalies {
4042 /// eprintln!(" {}", anomaly);
4043 /// }
4044 /// }
4045 /// ```
4046 pub async fn verify_state(&self) -> crate::tracing::VerificationReport {
4047 let logs = self.event_listener.logs.lock().await;
4048 let verifier = crate::tracing::StateVerifier::from_events(logs.clone());
4049 verifier.verify()
4050 }
4051
4052 /// Assert that state verification passes with no anomalies.
4053 ///
4054 /// Panics with a detailed report if any anomalies are detected.
4055 pub async fn assert_state_verified(&self) {
4056 let report = self.verify_state().await;
4057 if !report.is_clean() {
4058 panic!(
4059 "State verification failed with {} anomalies:\n{}",
4060 report.anomalies.len(),
4061 report.display()
4062 );
4063 }
4064 }
4065
4066 /// Returns the number of unique contracts that have been subscribed to.
4067 ///
4068 /// Counts distinct contracts from SubscribeSuccess events. Use this to verify
4069 /// that subscribed contracts are actually getting replicated and converged.
4070 ///
4071 /// # Example
4072 /// ```ignore
4073 /// let subscribed_count = sim.count_subscribed_contracts().await;
4074 /// let converged = sim.check_convergence().await;
4075 /// assert_eq!(subscribed_count, converged.total_contracts(),
4076 /// "All subscribed contracts should be replicated and checked for convergence");
4077 /// ```
4078 pub async fn count_subscribed_contracts(&self) -> usize {
4079 use std::collections::HashSet;
4080 let logs = self.event_listener.logs.lock().await;
4081
4082 let mut subscribed_contracts: HashSet<String> = HashSet::new();
4083
4084 for log in logs.iter() {
4085 if let crate::tracing::EventKind::Subscribe(
4086 crate::tracing::SubscribeEvent::SubscribeSuccess { key, .. },
4087 ) = &log.kind
4088 {
4089 subscribed_contracts.insert(format!("{:?}", key));
4090 }
4091 }
4092
4093 subscribed_contracts.len()
4094 }
4095
4096 // =========================================================================
4097 // Gap 4: Direct State Query API (event-based)
4098 // =========================================================================
4099
4100 /// Returns the latest known state hash for each contract on each peer.
4101 ///
4102 /// This provides a view of contract state distribution across the network
4103 /// by examining event logs. Returns a map of contract_key -> (peer_addr -> state_hash).
4104 ///
4105 /// # Example
4106 /// ```ignore
4107 /// let states = sim.get_contract_state_hashes().await;
4108 /// for (contract, peer_states) in states {
4109 /// println!("Contract {}: {} replicas", contract, peer_states.len());
4110 /// for (peer, hash) in peer_states {
4111 /// println!(" {}: {}", peer, hash);
4112 /// }
4113 /// }
4114 /// ```
4115 pub async fn get_contract_state_hashes(
4116 &self,
4117 ) -> BTreeMap<String, BTreeMap<SocketAddr, String>> {
4118 let summary = self.get_deterministic_event_summary().await;
4119
4120 // Use BTreeMap for deterministic iteration order in DST
4121 let mut contract_states: BTreeMap<String, BTreeMap<SocketAddr, String>> = BTreeMap::new();
4122
4123 for event in &summary {
4124 if let (Some(contract_key), Some(state_hash)) = (&event.contract_key, &event.state_hash)
4125 {
4126 // Keep the latest state for each peer/contract pair
4127 contract_states
4128 .entry(contract_key.clone())
4129 .or_default()
4130 .insert(event.peer_addr, state_hash.clone());
4131 }
4132 }
4133
4134 contract_states
4135 }
4136
4137 /// Returns a list of contracts and the peers that have them.
4138 ///
4139 /// This is useful for checking contract distribution across the network.
4140 pub async fn get_contract_distribution(&self) -> Vec<ContractDistribution> {
4141 let states = self.get_contract_state_hashes().await;
4142 states
4143 .into_iter()
4144 .map(|(contract_key, peer_states)| ContractDistribution {
4145 contract_key,
4146 replica_count: peer_states.len(),
4147 peers: peer_states.keys().cloned().collect(),
4148 })
4149 .collect()
4150 }
4151
4152 // =========================================================================
4153 // Gap T4: Operation Completion Tracking
4154 // =========================================================================
4155
4156 /// Returns a summary of operation completion status.
4157 ///
4158 /// This tracks Put, Get, Subscribe, and Update operations from request to
4159 /// completion (success or failure).
4160 ///
4161 /// # Example
4162 /// ```ignore
4163 /// let summary = sim.get_operation_summary().await;
4164 /// println!("Put: {}/{} completed ({:.1}% success)",
4165 /// summary.put.completed(),
4166 /// summary.put.requested,
4167 /// summary.put.success_rate() * 100.0);
4168 /// ```
4169 pub async fn get_operation_summary(&self) -> OperationSummary {
4170 let logs = self.event_listener.logs.lock().await;
4171
4172 let mut summary = OperationSummary::default();
4173
4174 for log in logs.iter() {
4175 match &log.kind {
4176 // Put operations
4177 crate::tracing::EventKind::Put(put_event) => {
4178 use crate::tracing::PutEvent;
4179 match put_event {
4180 PutEvent::Request { .. } => summary.put.requested += 1,
4181 PutEvent::PutSuccess { .. } => summary.put.succeeded += 1,
4182 PutEvent::PutFailure { .. } => summary.put.failed += 1,
4183 PutEvent::ResponseSent { .. } => {} // Response tracking, doesn't affect summary
4184 PutEvent::BroadcastEmitted { .. } => summary.put.broadcasts_emitted += 1,
4185 PutEvent::BroadcastReceived { .. } => summary.put.broadcasts_received += 1,
4186 }
4187 }
4188 // Get operations
4189 crate::tracing::EventKind::Get(get_event) => {
4190 use crate::tracing::GetEvent;
4191 match get_event {
4192 GetEvent::Request { .. } => summary.get.requested += 1,
4193 GetEvent::GetSuccess { .. } => summary.get.succeeded += 1,
4194 GetEvent::GetFailure { .. } => summary.get.failed += 1,
4195 // ClientTerminal is emitted IN ADDITION to
4196 // GetSuccess / GetNotFound (once per client op from
4197 // the driver), so counting it here would double-count
4198 // the sim summary; the GetSuccess arm above already
4199 // tallies succeeded.
4200 GetEvent::GetNotFound { .. }
4201 | GetEvent::ResponseSent { .. }
4202 | GetEvent::ForwardingAckSent { .. }
4203 | GetEvent::ForwardingAckReceived { .. }
4204 | GetEvent::ClientTerminal { .. } => {}
4205 }
4206 }
4207 // Subscribe operations
4208 crate::tracing::EventKind::Subscribe(sub_event) => {
4209 use crate::tracing::SubscribeEvent;
4210 match sub_event {
4211 SubscribeEvent::Request { .. } => summary.subscribe.requested += 1,
4212 SubscribeEvent::SubscribeSuccess { .. } => summary.subscribe.succeeded += 1,
4213 SubscribeEvent::SubscribeNotFound { .. }
4214 | SubscribeEvent::SubscribeTimeout { .. } => summary.subscribe.failed += 1,
4215 SubscribeEvent::ResponseSent { .. }
4216 | SubscribeEvent::HostingStarted { .. }
4217 | SubscribeEvent::HostingStopped { .. }
4218 | SubscribeEvent::_Reserved6
4219 | SubscribeEvent::_Reserved7
4220 | SubscribeEvent::_Reserved8
4221 | SubscribeEvent::_Reserved9
4222 | SubscribeEvent::_Reserved10
4223 | SubscribeEvent::UnsubscribeSent { .. }
4224 | SubscribeEvent::UnsubscribeReceived { .. } => {}
4225 }
4226 }
4227 crate::tracing::EventKind::Update(update_event) => {
4228 use crate::tracing::UpdateEvent;
4229 match update_event {
4230 UpdateEvent::Request { .. } => summary.update.requested += 1,
4231 UpdateEvent::UpdateSuccess { .. } => summary.update.succeeded += 1,
4232 UpdateEvent::BroadcastReceived { .. } => {
4233 summary.update.broadcasts_received += 1
4234 }
4235 UpdateEvent::BroadcastEmitted { .. } => {
4236 summary.update.broadcasts_emitted += 1
4237 }
4238 UpdateEvent::UpdateFailure { .. }
4239 | UpdateEvent::BroadcastComplete { .. }
4240 | UpdateEvent::BroadcastApplied { .. }
4241 | UpdateEvent::BroadcastDeliverySummary { .. } => {}
4242 }
4243 }
4244 // Timeouts
4245 crate::tracing::EventKind::Timeout { .. } => {
4246 summary.timeouts += 1;
4247 }
4248 crate::tracing::EventKind::Connect(_)
4249 | crate::tracing::EventKind::Route(_)
4250 | crate::tracing::EventKind::Transfer(_)
4251 | crate::tracing::EventKind::Lifecycle(_)
4252 | crate::tracing::EventKind::Ignored
4253 | crate::tracing::EventKind::Disconnected { .. }
4254 | crate::tracing::EventKind::TransportSnapshot(_)
4255 | crate::tracing::EventKind::InterestSync(_)
4256 | crate::tracing::EventKind::RoutingDecision(_)
4257 | crate::tracing::EventKind::RouterSnapshot(_) => {}
4258 }
4259 }
4260
4261 summary
4262 }
4263
4264 /// Checks if all requested operations have completed (either succeeded or failed).
4265 ///
4266 /// Returns (completed, pending) counts.
4267 pub async fn operation_completion_status(&self) -> (usize, usize) {
4268 let summary = self.get_operation_summary().await;
4269 let completed = summary.total_completed();
4270 let requested = summary.total_requested();
4271 let pending = requested.saturating_sub(completed);
4272 (completed, pending)
4273 }
4274
4275 /// Returns the overall operation success rate (0.0 to 1.0).
4276 pub async fn operation_success_rate(&self) -> f64 {
4277 let summary = self.get_operation_summary().await;
4278 summary.overall_success_rate()
4279 }
4280
4281 /// Waits for all operations to complete, up to a timeout.
4282 ///
4283 /// # Returns
4284 /// * `Ok(OperationSummary)` - If all operations completed
4285 /// * `Err(OperationSummary)` - If timeout reached with pending operations
4286 pub async fn await_operation_completion(
4287 &self,
4288 timeout: Duration,
4289 poll_interval: Duration,
4290 ) -> Result<OperationSummary, OperationSummary> {
4291 // Use tokio::time::Instant for deterministic behavior in simulation
4292 let start = tokio::time::Instant::now();
4293
4294 loop {
4295 let summary = self.get_operation_summary().await;
4296 let (completed, pending) = (summary.total_completed(), summary.total_requested());
4297
4298 // All operations completed (or none requested)
4299 if pending == 0 || completed >= pending {
4300 return Ok(summary);
4301 }
4302
4303 if start.elapsed() >= timeout {
4304 tracing::warn!(
4305 "Operation completion timeout: {} completed, {} pending",
4306 completed,
4307 pending.saturating_sub(completed)
4308 );
4309 return Err(summary);
4310 }
4311
4312 tokio::time::sleep(poll_interval).await;
4313 }
4314 }
4315
4316 /// Waits for network quiescence - when no new network activity is detected.
4317 ///
4318 /// This is useful for determining when broadcasts have finished propagating.
4319 /// It works by monitoring the event log and waiting until no new entries are
4320 /// added for `quiescence_duration`.
4321 ///
4322 /// # Arguments
4323 /// * `timeout` - Maximum time to wait for quiescence
4324 /// * `quiescence_duration` - How long activity must be quiet to consider quiesced
4325 /// * `poll_interval` - How often to check for new activity
4326 ///
4327 /// # Returns
4328 /// * `Ok(usize)` - Total log entries when quiesced
4329 /// * `Err(usize)` - Log entries at timeout (still active)
4330 pub async fn await_network_quiescence(
4331 &self,
4332 timeout: Duration,
4333 quiescence_duration: Duration,
4334 poll_interval: Duration,
4335 ) -> Result<usize, usize> {
4336 let start = tokio::time::Instant::now();
4337 let mut last_log_count = 0usize;
4338 let mut quiet_since = tokio::time::Instant::now();
4339
4340 loop {
4341 let current_count = self.event_listener.logs.lock().await.len();
4342
4343 if current_count != last_log_count {
4344 // Activity detected, reset quiet timer
4345 last_log_count = current_count;
4346 quiet_since = tokio::time::Instant::now();
4347 } else if quiet_since.elapsed() >= quiescence_duration {
4348 // Been quiet long enough
4349 tracing::info!(
4350 "Network quiesced after {:?} with {} log entries",
4351 start.elapsed(),
4352 current_count
4353 );
4354 return Ok(current_count);
4355 }
4356
4357 if start.elapsed() >= timeout {
4358 tracing::warn!(
4359 "Network quiescence timeout after {:?}: {} log entries, still active",
4360 timeout,
4361 current_count
4362 );
4363 return Err(current_count);
4364 }
4365
4366 tokio::time::sleep(poll_interval).await;
4367 }
4368 }
4369
4370 /// Asserts that operation success rate meets the threshold.
4371 ///
4372 /// # Panics
4373 /// Panics if success rate is below the threshold.
4374 pub async fn assert_operation_success_rate(&self, min_rate: f64) {
4375 let summary = self.get_operation_summary().await;
4376 let rate = summary.overall_success_rate();
4377
4378 if rate < min_rate {
4379 panic!(
4380 "Operation success rate {:.1}% is below threshold {:.1}%\n\
4381 Put: {}/{} ({:.1}%), Get: {}/{} ({:.1}%), \
4382 Subscribe: {}/{} ({:.1}%), Update: {}/{} ({:.1}%)",
4383 rate * 100.0,
4384 min_rate * 100.0,
4385 summary.put.succeeded,
4386 summary.put.completed(),
4387 summary.put.success_rate() * 100.0,
4388 summary.get.succeeded,
4389 summary.get.completed(),
4390 summary.get.success_rate() * 100.0,
4391 summary.subscribe.succeeded,
4392 summary.subscribe.completed(),
4393 summary.subscribe.success_rate() * 100.0,
4394 summary.update.succeeded,
4395 summary.update.completed(),
4396 summary.update.success_rate() * 100.0,
4397 );
4398 }
4399 }
4400
4401 // =========================================================================
4402 // Turmoil-based Deterministic Simulation
4403 // =========================================================================
4404
4405 /// Runs the simulation with deterministic scheduling using Turmoil.
4406 ///
4407 /// This method sets up all nodes as Turmoil hosts and runs the simulation
4408 /// deterministically. All tokio async operations are scheduled by Turmoil's
4409 /// deterministic scheduler, making tests reproducible.
4410 ///
4411 /// # Arguments
4412 ///
4413 /// * `seed` - Seed for random event generation
4414 /// * `max_contract_num` - Maximum number of contracts in the simulation
4415 /// * `iterations` - Number of iterations/events to run
4416 /// * `simulation_duration` - Maximum duration for the simulation
4417 /// * `test_fn` - A closure containing the test logic to run inside Turmoil
4418 ///
4419 /// # Example
4420 ///
4421 /// ```ignore
4422 /// use freenet::dev_tool::SimNetwork;
4423 /// use std::time::Duration;
4424 ///
4425 /// let sim = SimNetwork::new("test", 1, 5, 7, 3, 10, 2, 42).await;
4426 ///
4427 /// sim.run_simulation::<rand::rngs::SmallRng, _, _>(
4428 /// 42,
4429 /// 10,
4430 /// 100,
4431 /// Duration::from_secs(60),
4432 /// || async {
4433 /// // Test assertions here
4434 /// tokio::time::sleep(Duration::from_secs(5)).await;
4435 /// Ok(())
4436 /// },
4437 /// )?;
4438 /// ```
4439 ///
4440 /// # Determinism
4441 ///
4442 /// Running with the same seed will produce the same execution order and
4443 /// results. This is essential for:
4444 /// - Reproducing bugs
4445 /// - Property-based testing
4446 /// - CI reliability
4447 pub fn run_simulation<R, F, Fut>(
4448 mut self,
4449 seed: u64,
4450 max_contract_num: usize,
4451 iterations: usize,
4452 simulation_duration: Duration,
4453 event_wait: Duration,
4454 test_fn: F,
4455 ) -> turmoil::Result
4456 where
4457 R: RandomEventGenerator + Send + 'static,
4458 F: FnOnce() -> Fut + Send + 'static,
4459 Fut: std::future::Future<Output = turmoil::Result> + 'static,
4460 {
4461 use crate::config::{GlobalRng, GlobalSimulationTime};
4462 use std::sync::Mutex;
4463
4464 // Set up deterministic RNG and time for reproducible simulation
4465 GlobalRng::set_seed(seed);
4466
4467 // Derive simulation epoch from seed for deterministic ULID generation
4468 // Base: 2020-01-01 00:00:00 UTC, Range: ~5 years (keeps dates sensible: 2020-2025)
4469 const BASE_EPOCH_MS: u64 = 1577836800000; // 2020-01-01 00:00:00 UTC
4470 const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; // ~5 years in ms
4471 let epoch_offset = seed % RANGE_MS;
4472 GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + epoch_offset);
4473
4474 // Build Turmoil simulation with seeded RNG for deterministic execution
4475 let mut sim = turmoil::Builder::new()
4476 .simulation_duration(simulation_duration)
4477 .rng_seed(seed)
4478 .build();
4479
4480 // Get total peer count for event generation
4481 let total_peer_num = self.gateways.len() + self.nodes.len();
4482
4483 // Register all gateways as Turmoil hosts
4484 // Turmoil's sim.host requires Fn (can be called multiple times),
4485 // so we wrap non-Clone values in Arc<Mutex<Option<T>>> and take them on first call
4486 let gateways: Vec<_> = self.gateways.drain(..).collect();
4487 for (node, config) in gateways {
4488 let label = config.label.clone();
4489 let host_name = label.to_string();
4490 let receiver_ch = self.receiver_ch.clone();
4491
4492 // Create shared in-memory storage for this node
4493 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
4494
4495 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
4496 receiver_ch,
4497 node.config.key_pair.public().clone(),
4498 seed,
4499 );
4500 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
4501
4502 let span = tracing::info_span!("turmoil_gateway", %label);
4503
4504 // Store label for later reference
4505 self.labels
4506 .push((label.clone(), node.config.key_pair.public().clone()));
4507
4508 // Wrap in Option so we can take() on first call
4509 let node = Arc::new(Mutex::new(Some(node)));
4510 let user_events = Arc::new(Mutex::new(Some(user_events)));
4511 let span = Arc::new(Mutex::new(Some(span)));
4512 let shared_storage = Arc::new(Mutex::new(Some(shared_storage)));
4513
4514 sim.host(host_name, move || {
4515 let node = node.clone();
4516 let user_events = user_events.clone();
4517 let span = span.clone();
4518 let shared_storage = shared_storage.clone();
4519
4520 async move {
4521 let node = node
4522 .lock()
4523 .unwrap()
4524 .take()
4525 .expect("Turmoil host should only be called once");
4526 let user_events = user_events
4527 .lock()
4528 .unwrap()
4529 .take()
4530 .expect("Turmoil host should only be called once");
4531 let span = span
4532 .lock()
4533 .unwrap()
4534 .take()
4535 .expect("Turmoil host should only be called once");
4536 let shared_storage = shared_storage
4537 .lock()
4538 .unwrap()
4539 .take()
4540 .expect("Turmoil host should only be called once");
4541
4542 node.run_node_with_shared_storage(user_events, span, shared_storage)
4543 .await
4544 .map_err(|e| {
4545 Box::new(std::io::Error::other(e.to_string()))
4546 as Box<dyn std::error::Error>
4547 })
4548 }
4549 });
4550 }
4551
4552 // Register all regular nodes as Turmoil hosts
4553 let nodes: Vec<_> = self.nodes.drain(..).collect();
4554 for (node, label) in nodes {
4555 let host_name = label.to_string();
4556 let receiver_ch = self.receiver_ch.clone();
4557
4558 // Create shared in-memory storage for this node
4559 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
4560
4561 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
4562 receiver_ch,
4563 node.config.key_pair.public().clone(),
4564 seed,
4565 );
4566 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
4567
4568 let span = tracing::info_span!("turmoil_node", %label);
4569
4570 // Store label for later reference
4571 self.labels
4572 .push((label.clone(), node.config.key_pair.public().clone()));
4573
4574 // Wrap in Option so we can take() on first call
4575 let node = Arc::new(Mutex::new(Some(node)));
4576 let user_events = Arc::new(Mutex::new(Some(user_events)));
4577 let span = Arc::new(Mutex::new(Some(span)));
4578 let shared_storage = Arc::new(Mutex::new(Some(shared_storage)));
4579
4580 sim.host(host_name, move || {
4581 let node = node.clone();
4582 let user_events = user_events.clone();
4583 let span = span.clone();
4584 let shared_storage = shared_storage.clone();
4585
4586 async move {
4587 let node = node
4588 .lock()
4589 .unwrap()
4590 .take()
4591 .expect("Turmoil host should only be called once");
4592 let user_events = user_events
4593 .lock()
4594 .unwrap()
4595 .take()
4596 .expect("Turmoil host should only be called once");
4597 let span = span
4598 .lock()
4599 .unwrap()
4600 .take()
4601 .expect("Turmoil host should only be called once");
4602 let shared_storage = shared_storage
4603 .lock()
4604 .unwrap()
4605 .take()
4606 .expect("Turmoil host should only be called once");
4607
4608 node.run_node_with_shared_storage(user_events, span, shared_storage)
4609 .await
4610 .map_err(|e| {
4611 Box::new(std::io::Error::other(e.to_string()))
4612 as Box<dyn std::error::Error>
4613 })
4614 }
4615 });
4616 }
4617
4618 // Take the event controller and labels for triggering events
4619 let user_ev_controller = self
4620 .user_ev_controller
4621 .take()
4622 .expect("user_ev_controller should be set");
4623 let labels: Vec<_> = self.labels.clone();
4624
4625 // Register the test function as a Turmoil client
4626 sim.client("test", async move {
4627 // Give nodes time to start and establish connections
4628 tokio::time::sleep(Duration::from_secs(2)).await;
4629
4630 // Use a seeded RNG for deterministic peer selection
4631 use rand::SeedableRng;
4632 use rand::prelude::*;
4633 let mut event_rng = <rand::rngs::SmallRng as SeedableRng>::seed_from_u64(seed);
4634
4635 // Trigger events by sending signals to peer event generators
4636 // Each signal tells one peer to generate its next random event
4637 // Use longer delays to ensure each event completes before the next starts
4638 for event_id in 0..iterations as u32 {
4639 // Pick a random peer to generate an event
4640 if let Some((_, peer_key)) = labels.choose(&mut event_rng) {
4641 if user_ev_controller
4642 .send((event_id, peer_key.clone()))
4643 .is_err()
4644 {
4645 tracing::warn!(event_id, "Failed to send event signal - receivers dropped");
4646 break;
4647 }
4648
4649 // Delay between events to allow processing and control virtual time pacing
4650 tokio::time::sleep(event_wait).await;
4651 }
4652 }
4653
4654 // Wait for events to fully propagate through the network
4655 tokio::time::sleep(Duration::from_secs(2)).await;
4656
4657 // Run the user's test function
4658 test_fn().await
4659 });
4660
4661 // Run the simulation
4662 sim.run()
4663 }
4664
4665 /// Run a deterministic simulation with controlled events using Turmoil.
4666 ///
4667 /// Unlike `run_simulation` which uses random events, this method takes
4668 /// a predefined sequence of operations that will be executed in order.
4669 /// This is useful for testing specific scenarios like subscription topology.
4670 ///
4671 /// The simulation runs under Turmoil's deterministic scheduler, so
4672 /// `tokio::time::sleep` and other time-dependent operations are controlled.
4673 ///
4674 /// # Arguments
4675 /// * `seed` - Random seed for deterministic simulation
4676 /// * `operations` - Sequence of operations to execute in order
4677 /// * `simulation_duration` - Maximum duration for the simulation
4678 /// * `post_operations_wait` - Time to wait after operations complete (for recovery, etc.)
4679 ///
4680 /// # Returns
4681 /// A `turmoil::Result` indicating success or failure.
4682 ///
4683 /// # Example
4684 /// ```ignore
4685 /// let operations = vec![
4686 /// ScheduledOperation::new(NodeLabel::gateway("test", 0), SimOperation::Put { ... }),
4687 /// ScheduledOperation::new(NodeLabel::node("test", 1), SimOperation::Subscribe { ... }),
4688 /// ];
4689 /// let result = sim.run_controlled_simulation(
4690 /// SEED,
4691 /// operations,
4692 /// Duration::from_secs(120),
4693 /// Duration::from_secs(60), // Wait for orphan recovery
4694 /// );
4695 /// // After simulation, check result.topology_snapshots
4696 /// ```
4697 #[cfg(any(test, feature = "testing"))]
4698 pub fn run_controlled_simulation(
4699 mut self,
4700 seed: u64,
4701 operations: Vec<ScheduledOperation>,
4702 simulation_duration: Duration,
4703 post_operations_wait: Duration,
4704 ) -> ControlledSimulationResult {
4705 use crate::config::{GlobalRng, GlobalSimulationTime};
4706 use crate::ring::topology_registry::{
4707 get_all_topology_snapshots, set_current_network_name,
4708 };
4709 use std::collections::HashMap;
4710 use std::sync::Mutex;
4711
4712 // Set up deterministic RNG and time for reproducible simulation
4713 GlobalRng::set_seed(seed);
4714
4715 // Derive simulation epoch from seed for deterministic ULID generation
4716 const BASE_EPOCH_MS: u64 = 1577836800000; // 2020-01-01 00:00:00 UTC
4717 const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; // ~5 years in ms
4718 let epoch_offset = seed % RANGE_MS;
4719 GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + epoch_offset);
4720
4721 // Set the current network name for topology registration
4722 set_current_network_name(&self.name);
4723
4724 // Save network name for topology retrieval after simulation
4725 let network_name = self.name.clone();
4726
4727 // Make `SimOperation::CrashNode` a REAL crash: install the global
4728 // packet-delivery callback that consults each network's fault injector
4729 // and DROPS every packet to/from a crashed node, then opt THIS network
4730 // in via `enforce_fault_drops`. The callback is per-network aware (keyed
4731 // on the network name it is handed) and inert for any network that did
4732 // not opt in, so the direct-runner churn driver's crash semantics stay
4733 // unchanged. `SimNetwork::Drop` clears the callback. Without this, a
4734 // "crashed" node kept exchanging packets and piece-F crash tests were
4735 // false-green (#4642 piece F).
4736 crate::transport::in_memory_socket::set_packet_delivery_callback(Some(
4737 std::sync::Arc::new(fault_injection_delivery_decision),
4738 ));
4739 if let Some(injector) = crate::node::network_bridge::get_fault_injector(&network_name) {
4740 injector.lock().unwrap().enforce_fault_drops = true;
4741 }
4742
4743 // Build Turmoil simulation with seeded RNG for deterministic execution
4744 let mut sim = turmoil::Builder::new()
4745 .simulation_duration(simulation_duration)
4746 .rng_seed(seed)
4747 .build();
4748
4749 // Separate SeedContract operations (pre-simulation storage seeding)
4750 // from event operations (dispatched during simulation).
4751 let mut seed_ops: Vec<(NodeLabel, ContractContainer, Vec<u8>)> = Vec::new();
4752 // SeedHostedContract operations: like seed_ops, but the contract is
4753 // injected into the owning node's `contracts` list (below, before the
4754 // nodes are drained into Turmoil host closures) so startup
4755 // `append_contracts` registers genuine hosting + subscription.
4756 let mut seed_hosted_ops: Vec<(NodeLabel, ContractContainer, Vec<u8>)> = Vec::new();
4757 // SeedDemandlessCopy operations: like seed_hosted_ops, but injected with
4758 // `SeedMode::Demandless` so `append_contracts` registers a demandless
4759 // every-hop copy (hosting + local interest, NO subscription) rather than
4760 // a subscribed host. See serve-DURING (#4642 R3 piece C).
4761 let mut seed_demandless_ops: Vec<(NodeLabel, ContractContainer, Vec<u8>)> = Vec::new();
4762 let mut event_ops: Vec<ScheduledOperation> = Vec::new();
4763 for scheduled_op in operations {
4764 match scheduled_op.operation {
4765 SimOperation::SeedContract {
4766 ref contract,
4767 ref state,
4768 } => {
4769 seed_ops.push((scheduled_op.node, contract.clone(), state.clone()));
4770 }
4771 SimOperation::SeedHostedContract {
4772 ref contract,
4773 ref state,
4774 } => {
4775 seed_hosted_ops.push((scheduled_op.node, contract.clone(), state.clone()));
4776 }
4777 SimOperation::SeedDemandlessCopy {
4778 ref contract,
4779 ref state,
4780 } => {
4781 seed_demandless_ops.push((scheduled_op.node, contract.clone(), state.clone()));
4782 }
4783 SimOperation::Put { .. }
4784 | SimOperation::Get { .. }
4785 | SimOperation::Subscribe { .. }
4786 | SimOperation::Update { .. }
4787 | SimOperation::Disconnect
4788 // AdvanceHostingClock / CrashNode also flow through the ordered
4789 // event stream (so they fire in-sequence), but are routed to
4790 // `special_ops` at numbering time rather than to a node's
4791 // client-request generator.
4792 | SimOperation::AdvanceHostingClock { .. }
4793 | SimOperation::CrashNode
4794 | SimOperation::RecoverNode => event_ops.push(scheduled_op),
4795 }
4796 }
4797
4798 // Inject SeedHostedContract contracts into the owning node's `contracts`
4799 // list BEFORE the gateway/node Vecs are drained into Turmoil host
4800 // closures. At startup `append_contracts` runs a local PutQuery (no
4801 // network propagation) and, for `subscription = true`, registers the
4802 // contract in the Ring hosting manager (`host_contract` + `subscribe`)
4803 // so the node genuinely HOSTS it and the placement-migration trigger
4804 // (`ring.hosting_contract_keys`) sees it.
4805 for (target_label, contract, state) in seed_hosted_ops {
4806 let wrapped = WrappedState::new(state);
4807 let injected = self
4808 .nodes
4809 .iter_mut()
4810 .find(|(_, label)| *label == target_label)
4811 .map(|(node, _)| &mut node.contracts)
4812 .or_else(|| {
4813 self.gateways
4814 .iter_mut()
4815 .find(|(_, cfg)| cfg.label == target_label)
4816 .map(|(node, _)| &mut node.contracts)
4817 })
4818 .map(|contracts| contracts.push((contract, wrapped, SeedMode::Subscribed)))
4819 .is_some();
4820 assert!(
4821 injected,
4822 "SeedHostedContract: node {target_label:?} not found among gateways/nodes"
4823 );
4824 }
4825
4826 // Inject SeedDemandlessCopy contracts with `SeedMode::Demandless`. Same
4827 // injection point as the hosted seed above, but startup
4828 // `append_contracts` installs a demandless every-hop copy (hosting +
4829 // local interest, no subscription, no local client access) — the state a
4830 // production every-hop store leaves on a relay hop, which serve-DURING
4831 // (#4642 R3 piece C) must serve locally.
4832 for (target_label, contract, state) in seed_demandless_ops {
4833 let wrapped = WrappedState::new(state);
4834 let injected = self
4835 .nodes
4836 .iter_mut()
4837 .find(|(_, label)| *label == target_label)
4838 .map(|(node, _)| &mut node.contracts)
4839 .or_else(|| {
4840 self.gateways
4841 .iter_mut()
4842 .find(|(_, cfg)| cfg.label == target_label)
4843 .map(|(node, _)| &mut node.contracts)
4844 })
4845 .map(|contracts| contracts.push((contract, wrapped, SeedMode::Demandless)))
4846 .is_some();
4847 assert!(
4848 injected,
4849 "SeedDemandlessCopy: node {target_label:?} not found among gateways/nodes"
4850 );
4851 }
4852
4853 // In-order special dispatch: an event that the controlled-event client
4854 // performs itself (advance the shared hosting clock, or crash a node via
4855 // the fault injector) instead of turning into a client request.
4856 enum SpecialDispatch {
4857 AdvanceHostingClock(Duration),
4858 CrashNode(SocketAddr),
4859 RecoverNode(SocketAddr),
4860 }
4861
4862 // Build a map of label -> list of (event_id, operation)
4863 let mut operations_by_node: HashMap<NodeLabel, Vec<(EventId, SimOperation)>> =
4864 HashMap::new();
4865 let mut operation_sequence: Vec<(EventId, NodeLabel)> = Vec::new();
4866 // Special ops (clock advance / crash) are dispatched in-order by the
4867 // controlled-event client itself rather than being turned into a client
4868 // request routed to a node. Keyed by their sequence event_id.
4869 let mut special_ops: HashMap<EventId, SpecialDispatch> = HashMap::new();
4870
4871 for (event_id, scheduled_op) in event_ops.into_iter().enumerate() {
4872 let event_id = event_id as EventId;
4873 operation_sequence.push((event_id, scheduled_op.node.clone()));
4874 match scheduled_op.operation {
4875 SimOperation::AdvanceHostingClock { duration } => {
4876 special_ops.insert(event_id, SpecialDispatch::AdvanceHostingClock(duration));
4877 }
4878 SimOperation::CrashNode => {
4879 let addr = *self
4880 .node_addresses
4881 .get(&scheduled_op.node)
4882 .unwrap_or_else(|| {
4883 panic!(
4884 "CrashNode: node {:?} has no known address",
4885 scheduled_op.node
4886 )
4887 });
4888 special_ops.insert(event_id, SpecialDispatch::CrashNode(addr));
4889 }
4890 SimOperation::RecoverNode => {
4891 let addr = *self
4892 .node_addresses
4893 .get(&scheduled_op.node)
4894 .unwrap_or_else(|| {
4895 panic!(
4896 "RecoverNode: node {:?} has no known address",
4897 scheduled_op.node
4898 )
4899 });
4900 special_ops.insert(event_id, SpecialDispatch::RecoverNode(addr));
4901 }
4902 op @ (SimOperation::Put { .. }
4903 | SimOperation::Get { .. }
4904 | SimOperation::Subscribe { .. }
4905 | SimOperation::Update { .. }
4906 | SimOperation::Disconnect) => {
4907 operations_by_node
4908 .entry(scheduled_op.node)
4909 .or_default()
4910 .push((event_id, op));
4911 }
4912 // Seed ops were split out before the numbering loop (see the
4913 // separation match above), so they never reach here.
4914 SimOperation::SeedContract { .. }
4915 | SimOperation::SeedHostedContract { .. }
4916 | SimOperation::SeedDemandlessCopy { .. } => {
4917 unreachable!(
4918 "SeedContract/SeedHostedContract/SeedDemandlessCopy are separated out \
4919 before numbering"
4920 )
4921 }
4922 }
4923 }
4924
4925 // Handle for the controllable hosting clock (if any), captured into the
4926 // controlled-event client so `AdvanceHostingClock` can advance it
4927 // in-order with the rest of the sequence.
4928 let hosting_clock = self.hosting_clock.clone();
4929
4930 // Collect storage handles for the result — cloning Arc-backed storage
4931 // gives tests access to the same data written during the simulation.
4932 let mut node_storages: HashMap<NodeLabel, crate::wasm_runtime::MockStateStorage> =
4933 HashMap::new();
4934 // Pre-seeded contract stores for nodes with SeedContract operations.
4935 // Wrapped in Arc<Mutex> so closures can look up their store at
4936 // startup (after seed_ops populates the map, before sim.run()).
4937 let contract_stores: Arc<
4938 Mutex<HashMap<NodeLabel, crate::wasm_runtime::InMemoryContractStore>>,
4939 > = Arc::new(Mutex::new(HashMap::new()));
4940
4941 let use_mock_wasm = self.use_mock_wasm;
4942
4943 // Register all gateways as Turmoil hosts
4944 let gateways: Vec<_> = self.gateways.drain(..).collect();
4945 for (mut node, config) in gateways {
4946 let label = config.label.clone();
4947 let host_name = label.to_string();
4948 let receiver_ch = self.receiver_ch.clone();
4949
4950 // Capture slot for this node's live Ring (governance ban-list
4951 // observation post-simulation). See `shared_rings`.
4952 let ring_slot: Arc<parking_lot::Mutex<Option<Arc<crate::ring::Ring>>>> =
4953 Arc::new(parking_lot::Mutex::new(None));
4954 node.shared_ring = Some(ring_slot.clone());
4955 self.shared_rings.insert(label.clone(), ring_slot);
4956
4957 // Create shared in-memory storage for this node
4958 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
4959 node_storages.insert(label.clone(), shared_storage.clone());
4960
4961 // Create MemoryEventsGen without RNG (deterministic mode)
4962 // Clone receiver_ch so each node gets its own subscription
4963 let mut user_events =
4964 MemoryEventsGen::new(receiver_ch.clone(), node.config.key_pair.public().clone());
4965
4966 // Populate events for this node
4967 if let Some(node_ops) = operations_by_node.remove(&label) {
4968 let events: Vec<_> = node_ops
4969 .into_iter()
4970 .map(|(id, op)| (id, op.into_client_request()))
4971 .collect();
4972 user_events.generate_events(events);
4973 }
4974
4975 let span = tracing::info_span!("turmoil_gateway_controlled", %label);
4976
4977 // Store label for later reference
4978 self.labels
4979 .push((label.clone(), node.config.key_pair.public().clone()));
4980
4981 // Wrap in Option so we can take() on first call
4982 let node = Arc::new(Mutex::new(Some(node)));
4983 let user_events = Arc::new(Mutex::new(Some(user_events)));
4984 let span = Arc::new(Mutex::new(Some(span)));
4985 let shared_storage = Arc::new(Mutex::new(Some(shared_storage)));
4986 let contract_stores = contract_stores.clone();
4987 let label_for_closure = label.clone();
4988
4989 sim.host(host_name, move || {
4990 let node = node.clone();
4991 let user_events = user_events.clone();
4992 let span = span.clone();
4993 let shared_storage = shared_storage.clone();
4994 let contract_stores = contract_stores.clone();
4995 let label = label_for_closure.clone();
4996
4997 async move {
4998 let node = node
4999 .lock()
5000 .unwrap()
5001 .take()
5002 .expect("Turmoil host should only be called once");
5003 let user_events = user_events
5004 .lock()
5005 .unwrap()
5006 .take()
5007 .expect("Turmoil host should only be called once");
5008 let span = span
5009 .lock()
5010 .unwrap()
5011 .take()
5012 .expect("Turmoil host should only be called once");
5013 let shared_storage = shared_storage
5014 .lock()
5015 .unwrap()
5016 .take()
5017 .expect("Turmoil host should only be called once");
5018 let cs = contract_stores.lock().unwrap().remove(&label);
5019
5020 if use_mock_wasm {
5021 node.run_node_with_mock_wasm(user_events, span, shared_storage, cs)
5022 .await
5023 .map_err(|e| {
5024 Box::new(std::io::Error::other(e.to_string()))
5025 as Box<dyn std::error::Error>
5026 })
5027 } else {
5028 node.run_node_with_shared_storage(user_events, span, shared_storage)
5029 .await
5030 .map_err(|e| {
5031 Box::new(std::io::Error::other(e.to_string()))
5032 as Box<dyn std::error::Error>
5033 })
5034 }
5035 }
5036 });
5037 }
5038
5039 // Register all regular nodes as Turmoil hosts
5040 let nodes: Vec<_> = self.nodes.drain(..).collect();
5041 for (mut node, label) in nodes {
5042 let host_name = label.to_string();
5043 let receiver_ch = self.receiver_ch.clone();
5044
5045 // Capture slot for this node's live Ring (governance ban-list
5046 // observation post-simulation). See `shared_rings`.
5047 let ring_slot: Arc<parking_lot::Mutex<Option<Arc<crate::ring::Ring>>>> =
5048 Arc::new(parking_lot::Mutex::new(None));
5049 node.shared_ring = Some(ring_slot.clone());
5050 self.shared_rings.insert(label.clone(), ring_slot);
5051
5052 // Create shared in-memory storage for this node
5053 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
5054 node_storages.insert(label.clone(), shared_storage.clone());
5055
5056 // Create MemoryEventsGen without RNG (deterministic mode)
5057 // Clone receiver_ch so each node gets its own subscription
5058 let mut user_events =
5059 MemoryEventsGen::new(receiver_ch.clone(), node.config.key_pair.public().clone());
5060
5061 // Populate events for this node
5062 if let Some(node_ops) = operations_by_node.remove(&label) {
5063 let events: Vec<_> = node_ops
5064 .into_iter()
5065 .map(|(id, op)| (id, op.into_client_request()))
5066 .collect();
5067 user_events.generate_events(events);
5068 }
5069
5070 let span = tracing::info_span!("turmoil_node_controlled", %label);
5071
5072 // Store label for later reference
5073 self.labels
5074 .push((label.clone(), node.config.key_pair.public().clone()));
5075
5076 // Wrap in Option so we can take() on first call
5077 let node = Arc::new(Mutex::new(Some(node)));
5078 let user_events = Arc::new(Mutex::new(Some(user_events)));
5079 let span = Arc::new(Mutex::new(Some(span)));
5080 let shared_storage = Arc::new(Mutex::new(Some(shared_storage)));
5081 let contract_stores = contract_stores.clone();
5082 let label_for_closure = label.clone();
5083
5084 sim.host(host_name, move || {
5085 let node = node.clone();
5086 let user_events = user_events.clone();
5087 let span = span.clone();
5088 let shared_storage = shared_storage.clone();
5089 let contract_stores = contract_stores.clone();
5090 let label = label_for_closure.clone();
5091
5092 async move {
5093 let node = node
5094 .lock()
5095 .unwrap()
5096 .take()
5097 .expect("Turmoil host should only be called once");
5098 let user_events = user_events
5099 .lock()
5100 .unwrap()
5101 .take()
5102 .expect("Turmoil host should only be called once");
5103 let span = span
5104 .lock()
5105 .unwrap()
5106 .take()
5107 .expect("Turmoil host should only be called once");
5108 let shared_storage = shared_storage
5109 .lock()
5110 .unwrap()
5111 .take()
5112 .expect("Turmoil host should only be called once");
5113 let cs = contract_stores.lock().unwrap().remove(&label);
5114
5115 if use_mock_wasm {
5116 node.run_node_with_mock_wasm(user_events, span, shared_storage, cs)
5117 .await
5118 .map_err(|e| {
5119 Box::new(std::io::Error::other(e.to_string()))
5120 as Box<dyn std::error::Error>
5121 })
5122 } else {
5123 node.run_node_with_shared_storage(user_events, span, shared_storage)
5124 .await
5125 .map_err(|e| {
5126 Box::new(std::io::Error::other(e.to_string()))
5127 as Box<dyn std::error::Error>
5128 })
5129 }
5130 }
5131 });
5132 }
5133
5134 // Apply SeedContract operations: pre-populate specific nodes'
5135 // contract stores and state storage. This bypasses network PUT
5136 // so the contract exists only on the seeded nodes.
5137 for (label, contract, state) in seed_ops {
5138 let storage = node_storages
5139 .get(&label)
5140 .unwrap_or_else(|| panic!("SeedContract: node {label:?} not found in storages"));
5141 let key = contract.key();
5142 storage.seed_state(key, WrappedState::new(state));
5143 storage.seed_params(key, contract.params().clone());
5144 // Also seed into the contract store (WASM code + params).
5145 // Without this, the WASM cache check in client_events.rs
5146 // rejects subscribes with "contract WASM not cached locally".
5147 let mut stores = contract_stores.lock().unwrap();
5148 let contract_store = stores.entry(label.clone()).or_default();
5149 contract_store
5150 .store_contract(contract.clone())
5151 .unwrap_or_else(|e| {
5152 panic!("SeedContract: failed to store contract for {label:?}: {e}")
5153 });
5154 tracing::debug!(
5155 node = %label,
5156 contract = %key,
5157 "SeedContract: pre-populated contract in node storage"
5158 );
5159 }
5160
5161 // Take the event controller and labels for triggering events
5162 let user_ev_controller = self
5163 .user_ev_controller
5164 .take()
5165 .expect("user_ev_controller should be set");
5166 let labels: Vec<_> = self.labels.clone();
5167
5168 // Build a map from NodeLabel to peer key for event triggering
5169 let label_to_key: HashMap<NodeLabel, _> = labels.into_iter().collect();
5170
5171 // Clone the network name for the client closure (the outer
5172 // `network_name` is still needed after `sim.run()` for topology capture).
5173 let network_name_for_client = network_name.clone();
5174
5175 // Optional pre-operation join-convergence barrier (see
5176 // `wait_for_join_convergence_before_ops`). Captured by value into the
5177 // client closure.
5178 let wait_for_join_before_ops = self.wait_for_join_before_ops;
5179 let total_peers_expected = self.number_of_nodes + self.number_of_gateways;
5180 // Delay between triggering successive regular operations (default 3s).
5181 let controlled_op_interval = self
5182 .controlled_op_interval
5183 .unwrap_or_else(|| Duration::from_secs(3));
5184 // A peer counts as "joined" for the barrier once it has >= 1 established
5185 // ring connection. This is a slightly STRONGER condition than the
5186 // `peer_ready` bar `ensure_peer_ready` checks before letting a peer
5187 // originate an operation (`peer_ready` is set at handshake completion,
5188 // just before the connection is added to the ring), so any peer the
5189 // barrier counts as joined is definitely `peer_ready` — the barrier can
5190 // only over-wait, never under-wait, and every scheduled GET dispatches
5191 // rather than being rejected with `PeerNotJoined`. We deliberately do
5192 // NOT wait for the stronger min_connections/full-ring criterion: a
5193 // single connection is all a node needs to originate a GET (routing
5194 // stays correct from a lightly-connected node via the gateway-fallback
5195 // path), and waiting for a fully-formed ring roughly doubles the
5196 // barrier's wall-clock for no change in what the metric measures.
5197 const PEER_READY_MIN_CONNECTIONS: usize = 1;
5198 let join_conn_threshold = PEER_READY_MIN_CONNECTIONS;
5199
5200 // Register the test client that triggers controlled events
5201 sim.client("test", async move {
5202 // Give nodes time to start and establish connections
5203 tokio::time::sleep(Duration::from_secs(3)).await;
5204
5205 // Optional join-convergence barrier: wait until enough peers have
5206 // finished joining before firing any scheduled operation, so the
5207 // operations run against a FORMED network instead of racing topology
5208 // formation. A peer registers a topology snapshot only after its own
5209 // address is established (`peer_ready`), so the distinct snapshot
5210 // count is a live join tally. Poll it, advancing virtual time via
5211 // short sleeps, until the target fraction joins or the cap elapses.
5212 // Default (`None`) skips this entirely, preserving historical timing.
5213 if let Some((min_fraction, max_wait)) = wait_for_join_before_ops {
5214 let target = ((total_peers_expected as f64) * min_fraction).ceil() as usize;
5215 let poll_interval = Duration::from_secs(5);
5216 let mut waited = Duration::ZERO;
5217 loop {
5218 // Count peers that have reached the join threshold. Snapshot
5219 // *presence* is gated only on the (early) bind address, so it
5220 // is NOT a join signal; the stamped `connection_count` is.
5221 let joined = get_all_topology_snapshots(&network_name_for_client)
5222 .iter()
5223 .filter(|s| s.connection_count >= join_conn_threshold)
5224 .count();
5225 if joined >= target {
5226 tracing::info!(
5227 joined,
5228 target,
5229 total = total_peers_expected,
5230 waited_secs = waited.as_secs(),
5231 "controlled sim: ring join-convergence reached — firing operations"
5232 );
5233 break;
5234 }
5235 if waited >= max_wait {
5236 tracing::warn!(
5237 joined,
5238 target,
5239 total = total_peers_expected,
5240 waited_secs = waited.as_secs(),
5241 "controlled sim: join-convergence cap reached before target — \
5242 firing operations against a partially-formed network"
5243 );
5244 break;
5245 }
5246 tokio::time::sleep(poll_interval).await;
5247 waited += poll_interval;
5248 }
5249 }
5250
5251 // Trigger events in the specified order
5252 for (event_id, node_label) in operation_sequence {
5253 // Special in-order ops the client performs itself.
5254 if let Some(special) = special_ops.get(&event_id) {
5255 match special {
5256 SpecialDispatch::AdvanceHostingClock(duration) => {
5257 match hosting_clock.as_ref() {
5258 Some(clock) => {
5259 clock.advance_time(*duration);
5260 tracing::info!(
5261 event_id,
5262 advance_ms = duration.as_millis() as u64,
5263 "Advanced controlled hosting clock"
5264 );
5265 }
5266 None => tracing::warn!(
5267 event_id,
5268 "AdvanceHostingClock scheduled but no controllable \
5269 hosting clock was enabled (call \
5270 enable_hosting_time_control)"
5271 ),
5272 }
5273 }
5274 SpecialDispatch::CrashNode(addr) => {
5275 if let Some(injector) = crate::node::network_bridge::get_fault_injector(
5276 &network_name_for_client,
5277 ) {
5278 injector.lock().unwrap().config.crash_node(*addr);
5279 tracing::info!(
5280 event_id,
5281 node = %node_label,
5282 ?addr,
5283 "Crashed node via fault injector (message-blocking)"
5284 );
5285 } else {
5286 tracing::warn!(
5287 event_id,
5288 node = %node_label,
5289 "CrashNode scheduled but no fault injector registered"
5290 );
5291 }
5292 }
5293 SpecialDispatch::RecoverNode(addr) => {
5294 if let Some(injector) = crate::node::network_bridge::get_fault_injector(
5295 &network_name_for_client,
5296 ) {
5297 injector.lock().unwrap().config.recover_node(addr);
5298 tracing::info!(
5299 event_id,
5300 node = %node_label,
5301 ?addr,
5302 "Recovered node via fault injector (messages flow again)"
5303 );
5304 } else {
5305 tracing::warn!(
5306 event_id,
5307 node = %node_label,
5308 "RecoverNode scheduled but no fault injector registered"
5309 );
5310 }
5311 }
5312 }
5313 // Let the effect settle before the next event.
5314 tokio::time::sleep(Duration::from_secs(3)).await;
5315 continue;
5316 }
5317
5318 if let Some(peer_key) = label_to_key.get(&node_label) {
5319 tracing::info!(
5320 event_id,
5321 node = %node_label,
5322 "Triggering controlled event"
5323 );
5324
5325 if user_ev_controller
5326 .send((event_id, peer_key.clone()))
5327 .is_err()
5328 {
5329 tracing::warn!(
5330 event_id,
5331 node = %node_label,
5332 "Failed to send event signal - receivers dropped"
5333 );
5334 break;
5335 }
5336
5337 // Wait for operation to complete before triggering next
5338 // (overridable via `with_controlled_op_interval`; default 3s).
5339 tokio::time::sleep(controlled_op_interval).await;
5340 } else {
5341 tracing::warn!(
5342 event_id,
5343 node = %node_label,
5344 "No peer key found for node label"
5345 );
5346 }
5347 }
5348
5349 // Wait for post-operation processing (orphan recovery, topology stabilization, etc.)
5350 tracing::info!(
5351 wait_secs = post_operations_wait.as_secs(),
5352 "Waiting for post-operation processing"
5353 );
5354 tokio::time::sleep(post_operations_wait).await;
5355
5356 Ok(())
5357 });
5358
5359 // Run the simulation
5360 let turmoil_result = sim.run();
5361
5362 // Capture topology snapshots BEFORE self is dropped (which clears them)
5363 let topology_snapshots = get_all_topology_snapshots(&network_name);
5364
5365 // Capture renewal metrics BEFORE self is dropped — `SimNetwork::Drop`
5366 // calls `clear_renewal_metrics(&self.name)`, so reading the registry
5367 // after this function returns would always see an empty map (#4440).
5368 let renewal_metrics =
5369 crate::ring::topology_registry::get_all_renewal_metrics(&network_name);
5370
5371 // Capture WASM-summarize counts BEFORE self drops (Drop clears the
5372 // registry), same lifetime constraint as the renewal metrics above. The
5373 // every-hop summarize-storm falsifier reads these after the run returns.
5374 let summarize_wasm_calls =
5375 crate::ring::topology_registry::get_all_summarize_wasm_calls(&network_name);
5376
5377 // Capture the crash-drop count BEFORE self drops (Drop clears the fault
5378 // injector via `set_fault_injector(None)`). `> 0` proves a scripted
5379 // `CrashNode` actually blocked traffic — the discriminating signal for
5380 // piece-F crash tests.
5381 let crash_packets_dropped = crate::node::network_bridge::get_fault_injector(&network_name)
5382 .map(|inj| inj.lock().unwrap().stats.messages_dropped_crash)
5383 .unwrap_or(0);
5384
5385 // Extract the live Rings captured during the run BEFORE self drops.
5386 let node_rings: HashMap<NodeLabel, Arc<crate::ring::Ring>> = self
5387 .shared_rings
5388 .iter()
5389 .filter_map(|(label, slot)| slot.lock().clone().map(|ring| (label.clone(), ring)))
5390 .collect();
5391
5392 ControlledSimulationResult {
5393 turmoil_result,
5394 topology_snapshots,
5395 node_storages,
5396 node_rings,
5397 renewal_metrics,
5398 crash_packets_dropped,
5399 summarize_wasm_calls,
5400 }
5401 }
5402
5403 /// Run an fdev-style test using Turmoil's deterministic executor.
5404 ///
5405 /// This method provides the same functionality as the old `start_with_rand_gen`
5406 /// approach but runs everything inside Turmoil for full determinism.
5407 ///
5408 /// Returns `Ok(())` on success, or an error if the test failed.
5409 #[cfg(any(test, feature = "testing"))]
5410 pub fn run_fdev_test<R>(
5411 mut self,
5412 seed: u64,
5413 max_contract_num: usize,
5414 iterations: usize,
5415 simulation_duration: Duration,
5416 event_wait: Duration,
5417 ) -> anyhow::Result<()>
5418 where
5419 R: RandomEventGenerator + Send + 'static,
5420 {
5421 use crate::config::{GlobalRng, GlobalSimulationTime};
5422 use crate::ring::topology_registry::set_current_network_name;
5423 use std::sync::Mutex;
5424
5425 // Set up deterministic RNG and time for reproducible simulation
5426 GlobalRng::set_seed(seed);
5427
5428 // Derive simulation epoch from seed for deterministic ULID generation
5429 const BASE_EPOCH_MS: u64 = 1577836800000; // 2020-01-01 00:00:00 UTC
5430 const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; // ~5 years in ms
5431 let epoch_offset = seed % RANGE_MS;
5432 GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + epoch_offset);
5433
5434 // Set the current network name for topology registration
5435 set_current_network_name(&self.name);
5436
5437 // Build Turmoil simulation with seeded RNG for deterministic execution
5438 let mut sim = turmoil::Builder::new()
5439 .simulation_duration(simulation_duration)
5440 .rng_seed(seed)
5441 .build();
5442
5443 // Get total peer count for event generation
5444 let total_peer_num = self.gateways.len() + self.nodes.len();
5445
5446 // Register all gateways as Turmoil hosts
5447 let gateways: Vec<_> = self.gateways.drain(..).collect();
5448 for (node, config) in gateways {
5449 let label = config.label.clone();
5450 let host_name = label.to_string();
5451 let receiver_ch = self.receiver_ch.clone();
5452
5453 // Create shared in-memory storage for this node
5454 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
5455
5456 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
5457 receiver_ch,
5458 node.config.key_pair.public().clone(),
5459 seed,
5460 );
5461 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
5462
5463 let span = tracing::info_span!("turmoil_gateway", %label);
5464
5465 // Store label for later reference
5466 self.labels
5467 .push((label.clone(), node.config.key_pair.public().clone()));
5468
5469 // Wrap in Option so we can take() on first call
5470 let node = Arc::new(Mutex::new(Some(node)));
5471 let user_events = Arc::new(Mutex::new(Some(user_events)));
5472 let span = Arc::new(Mutex::new(Some(span)));
5473 let shared_storage = Arc::new(Mutex::new(Some(shared_storage)));
5474
5475 sim.host(host_name, move || {
5476 let node = node.clone();
5477 let user_events = user_events.clone();
5478 let span = span.clone();
5479 let shared_storage = shared_storage.clone();
5480
5481 async move {
5482 let node = node
5483 .lock()
5484 .unwrap()
5485 .take()
5486 .expect("Turmoil host should only be called once");
5487 let user_events = user_events
5488 .lock()
5489 .unwrap()
5490 .take()
5491 .expect("Turmoil host should only be called once");
5492 let span = span
5493 .lock()
5494 .unwrap()
5495 .take()
5496 .expect("Turmoil host should only be called once");
5497 let shared_storage = shared_storage
5498 .lock()
5499 .unwrap()
5500 .take()
5501 .expect("Turmoil host should only be called once");
5502
5503 node.run_node_with_shared_storage(user_events, span, shared_storage)
5504 .await
5505 .map_err(|e| {
5506 Box::new(std::io::Error::other(e.to_string()))
5507 as Box<dyn std::error::Error>
5508 })
5509 }
5510 });
5511 }
5512
5513 // Register all regular nodes as Turmoil hosts
5514 let nodes: Vec<_> = self.nodes.drain(..).collect();
5515 for (node, label) in nodes {
5516 let host_name = label.to_string();
5517 let receiver_ch = self.receiver_ch.clone();
5518
5519 // Create shared in-memory storage for this node
5520 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
5521
5522 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
5523 receiver_ch,
5524 node.config.key_pair.public().clone(),
5525 seed,
5526 );
5527 user_events.rng_params(label.number(), total_peer_num, max_contract_num, iterations);
5528
5529 let span = tracing::info_span!("turmoil_node", %label);
5530
5531 // Store label for later reference
5532 self.labels
5533 .push((label.clone(), node.config.key_pair.public().clone()));
5534
5535 // Wrap in Option so we can take() on first call
5536 let node = Arc::new(Mutex::new(Some(node)));
5537 let user_events = Arc::new(Mutex::new(Some(user_events)));
5538 let span = Arc::new(Mutex::new(Some(span)));
5539 let shared_storage = Arc::new(Mutex::new(Some(shared_storage)));
5540
5541 sim.host(host_name, move || {
5542 let node = node.clone();
5543 let user_events = user_events.clone();
5544 let span = span.clone();
5545 let shared_storage = shared_storage.clone();
5546
5547 async move {
5548 let node = node
5549 .lock()
5550 .unwrap()
5551 .take()
5552 .expect("Turmoil host should only be called once");
5553 let user_events = user_events
5554 .lock()
5555 .unwrap()
5556 .take()
5557 .expect("Turmoil host should only be called once");
5558 let span = span
5559 .lock()
5560 .unwrap()
5561 .take()
5562 .expect("Turmoil host should only be called once");
5563 let shared_storage = shared_storage
5564 .lock()
5565 .unwrap()
5566 .take()
5567 .expect("Turmoil host should only be called once");
5568
5569 node.run_node_with_shared_storage(user_events, span, shared_storage)
5570 .await
5571 .map_err(|e| {
5572 Box::new(std::io::Error::other(e.to_string()))
5573 as Box<dyn std::error::Error>
5574 })
5575 }
5576 });
5577 }
5578
5579 // Take the event controller and labels for triggering events
5580 let user_ev_controller = self
5581 .user_ev_controller
5582 .take()
5583 .expect("user_ev_controller should be set");
5584 let labels: Vec<_> = self.labels.clone();
5585
5586 // Clone event logs handle for convergence checking
5587 let event_logs = self.event_listener.logs.clone();
5588
5589 // Register the test function as a Turmoil client
5590 sim.client("test", async move {
5591 // Give nodes time to start and establish connections
5592 tokio::time::sleep(Duration::from_secs(2)).await;
5593
5594 // Use a seeded RNG for deterministic peer selection
5595 use rand::SeedableRng;
5596 use rand::prelude::*;
5597 let mut event_rng = <rand::rngs::SmallRng as SeedableRng>::seed_from_u64(seed);
5598
5599 // Trigger events by sending signals to peer event generators
5600 for event_id in 0..iterations as u32 {
5601 // Pick a random peer to generate an event
5602 if let Some((_, peer_key)) = labels.choose(&mut event_rng) {
5603 if user_ev_controller
5604 .send((event_id, peer_key.clone()))
5605 .is_err()
5606 {
5607 tracing::warn!(event_id, "Failed to send event signal - receivers dropped");
5608 break;
5609 }
5610 }
5611
5612 // Wait between events (Turmoil handles this deterministically)
5613 tokio::time::sleep(event_wait).await;
5614 }
5615
5616 // Wait for events to fully propagate through the network
5617 tokio::time::sleep(Duration::from_secs(2)).await;
5618
5619 // Convergence checking happens here, inside Turmoil
5620 // Count subscribed contracts from event logs
5621 let subscribed_count = {
5622 use std::collections::HashSet;
5623 let logs = event_logs.lock().await;
5624 let mut subscribed_contracts: HashSet<String> = HashSet::new();
5625 for log in logs.iter() {
5626 if let crate::tracing::EventKind::Subscribe(
5627 crate::tracing::SubscribeEvent::SubscribeSuccess { key, .. },
5628 ) = &log.kind
5629 {
5630 subscribed_contracts.insert(format!("{:?}", key));
5631 }
5632 }
5633 subscribed_contracts.len()
5634 };
5635
5636 if subscribed_count > 0 {
5637 tracing::info!(
5638 "Found {} subscribed contracts, checking convergence...",
5639 subscribed_count
5640 );
5641
5642 // Simple convergence check within Turmoil
5643 // We'll just wait a bit and then check final state
5644 tokio::time::sleep(Duration::from_secs(10)).await;
5645
5646 // TODO: Add actual convergence checking here if needed
5647 // For now, we just let the test complete
5648 }
5649
5650 Ok(())
5651 });
5652
5653 // Run the simulation
5654 sim.run()
5655 .map_err(|e| anyhow::anyhow!("Turmoil simulation failed: {:?}", e))
5656 }
5657
5658 /// Run a simulation using a single `current_thread` tokio runtime with paused time.
5659 ///
5660 /// This avoids Turmoil's O(n²) link overhead and O(n³) per-tick cost, making it
5661 /// feasible to simulate hundreds of nodes. Determinism is achieved via:
5662 /// - `current_thread` runtime (single-threaded, no scheduling races)
5663 /// - `start_paused(true)` (tokio auto-advances time deterministically)
5664 /// - `GlobalRng::set_seed` (seeded randomness)
5665 /// - `deterministic_select!` (ordered select branches)
5666 /// - `SimulationSocket` with BTreeMap (deterministic address ordering)
5667 #[cfg(any(test, feature = "testing"))]
5668 pub fn run_simulation_direct<R>(
5669 mut self,
5670 seed: u64,
5671 max_contract_num: usize,
5672 iterations: usize,
5673 event_wait: Duration,
5674 ) -> anyhow::Result<()>
5675 where
5676 R: RandomEventGenerator + Send + 'static,
5677 {
5678 use crate::config::{GlobalRng, GlobalSimulationTime, SimulationTransportOpt};
5679 use crate::ring::topology_registry::set_current_network_name;
5680
5681 // Set up deterministic RNG and time for reproducible simulation
5682 GlobalRng::set_seed(seed);
5683
5684 const BASE_EPOCH_MS: u64 = 1577836800000; // 2020-01-01 00:00:00 UTC
5685 const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; // ~5 years in ms
5686 let epoch_offset = seed % RANGE_MS;
5687 GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + epoch_offset);
5688
5689 set_current_network_name(&self.name);
5690
5691 // Make direct-runner ChurnConfig crashes REAL packet drops (#4694).
5692 //
5693 // The chaos driver below marks nodes crashed in this network's fault
5694 // injector, but a crash only drops packets if the global packet-delivery
5695 // callback is installed AND this network opted in via
5696 // `enforce_fault_drops`. Neither happened on the direct runner, so churn
5697 // faults were inert: a "crashed" node kept exchanging packets and every
5698 // near-K churn / partition validation on this runner was false-green
5699 // (#4694; blocker for the demand-driven-hosting release-2 gate, #4642).
5700 //
5701 // Gated on `churn_config.is_some()` so non-churn direct-runner tests are
5702 // byte-for-byte unchanged (the callback is inert unless a node is
5703 // actually crashed/partitioned, but gating keeps the change surgical and
5704 // makes the wiring impossible to miss when churn IS configured).
5705 // `SimNetwork::Drop` clears the global callback.
5706 if self.churn_config.is_some() {
5707 crate::transport::in_memory_socket::set_packet_delivery_callback(Some(
5708 std::sync::Arc::new(fault_injection_delivery_decision),
5709 ));
5710 if let Some(injector) = crate::node::network_bridge::get_fault_injector(&self.name) {
5711 injector.lock().unwrap().enforce_fault_drops = true;
5712 }
5713 }
5714
5715 // Single-threaded runtime with paused time for deterministic execution
5716 let rt = tokio::runtime::Builder::new_current_thread()
5717 .enable_all()
5718 .start_paused(true)
5719 .build()?;
5720
5721 let total_peer_num = self.gateways.len() + self.nodes.len();
5722
5723 // Extend connection idle timeout for ALL simulations. In start_paused(true)
5724 // mode, virtual time jumps past 120s during spawn_blocking (WASM execution),
5725 // causing spurious connection drops even with keepalive enabled.
5726 crate::config::SimulationIdleTimeout::enable();
5727
5728 // Relax transport timers for ALL direct-runner simulations. Disables keepalive
5729 // and uses 5x slower ACK/resend/rate-update intervals. In start_paused(true)
5730 // mode, production-rate timer firings create excessive virtual time advances
5731 // that interfere with heartbeat and broadcast scheduling, causing
5732 // non-deterministic convergence failures even in small networks.
5733 SimulationTransportOpt::enable();
5734 let use_mock_wasm = self.use_mock_wasm;
5735 let skip_convergence_wait = self.skip_convergence_wait;
5736
5737 let result: anyhow::Result<()> = rt.block_on(async {
5738 // Time driver: bridges tokio's paused time → VirtualTime
5739 // When all tasks are idle, tokio auto-advances past the 1ms sleep,
5740 // which wakes this driver, which advances VirtualTime, which wakes
5741 // VirtualSleep futures in node tasks.
5742 let vt = self.virtual_time.clone();
5743 let time_driver = tokio::spawn(async move {
5744 let start = tokio::time::Instant::now();
5745 loop {
5746 tokio::time::sleep(Duration::from_millis(1)).await;
5747 vt.advance_to(start.elapsed().as_nanos() as u64);
5748 }
5749 });
5750
5751 // Shared state for chaos driver to crash/restart nodes
5752 let direct_nodes: Arc<tokio::sync::Mutex<HashMap<NodeLabel, DirectNodeState>>> =
5753 Arc::new(tokio::sync::Mutex::new(HashMap::new()));
5754
5755 // Spawn gateway nodes
5756 let mut node_handles = Vec::new();
5757
5758 let gateways: Vec<_> = self.gateways.drain(..).collect();
5759 for (node, config) in gateways {
5760 let label = config.label.clone();
5761 let receiver_ch = self.receiver_ch.clone();
5762 let addr = *self.node_addresses.get(&label).expect("gateway address");
5763
5764 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
5765
5766 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
5767 receiver_ch,
5768 node.config.key_pair.public().clone(),
5769 seed,
5770 );
5771 user_events.rng_params(
5772 label.number(),
5773 total_peer_num,
5774 max_contract_num,
5775 iterations,
5776 );
5777
5778 let span = tracing::info_span!("direct_gateway", %label);
5779
5780 self.labels
5781 .push((label.clone(), node.config.key_pair.public().clone()));
5782
5783 let handle = if use_mock_wasm {
5784 tokio::spawn(async move {
5785 node.run_node_with_mock_wasm(user_events, span, shared_storage, None)
5786 .await
5787 })
5788 } else {
5789 tokio::spawn(async move {
5790 node.run_node_with_shared_storage(user_events, span, shared_storage)
5791 .await
5792 })
5793 };
5794
5795 {
5796 let mut nodes_map = direct_nodes.lock().await;
5797 nodes_map.insert(
5798 label.clone(),
5799 DirectNodeState {
5800 label: label.clone(),
5801 addr,
5802 is_gateway: true,
5803 permanently_dropped: false,
5804 },
5805 );
5806 }
5807
5808 node_handles.push(handle);
5809 }
5810
5811 // Spawn regular nodes with staggered start
5812 let nodes: Vec<_> = self.nodes.drain(..).collect();
5813 for (i, (node, label)) in nodes.into_iter().enumerate() {
5814 let receiver_ch = self.receiver_ch.clone();
5815 let addr = *self.node_addresses.get(&label).expect("node address");
5816
5817 let shared_storage = crate::wasm_runtime::MockStateStorage::new();
5818
5819 let mut user_events = MemoryEventsGen::<R>::new_with_seed(
5820 receiver_ch,
5821 node.config.key_pair.public().clone(),
5822 seed,
5823 );
5824 user_events.rng_params(
5825 label.number(),
5826 total_peer_num,
5827 max_contract_num,
5828 iterations,
5829 );
5830
5831 let span = tracing::info_span!("direct_node", %label);
5832
5833 self.labels
5834 .push((label.clone(), node.config.key_pair.public().clone()));
5835
5836 let backoff = self.start_backoff * (i as u32 + 1);
5837 let handle = if use_mock_wasm {
5838 tokio::spawn(async move {
5839 tokio::time::sleep(backoff).await;
5840 node.run_node_with_mock_wasm(user_events, span, shared_storage, None)
5841 .await
5842 })
5843 } else {
5844 tokio::spawn(async move {
5845 tokio::time::sleep(backoff).await;
5846 node.run_node_with_shared_storage(user_events, span, shared_storage)
5847 .await
5848 })
5849 };
5850
5851 {
5852 let mut nodes_map = direct_nodes.lock().await;
5853 nodes_map.insert(
5854 label.clone(),
5855 DirectNodeState {
5856 label: label.clone(),
5857 addr,
5858 is_gateway: false,
5859 permanently_dropped: false,
5860 },
5861 );
5862 }
5863
5864 node_handles.push(handle);
5865 }
5866
5867 // Chaos driver: periodically crash/recover nodes via fault injection.
5868 //
5869 // Uses "soft crash" via the fault injector (blocks all messages to/from
5870 // a node) rather than aborting tasks. This avoids orphaned sub-tasks
5871 // that would stall tokio's time auto-advance in start_paused(true).
5872 // The node's event loop keeps running but is functionally dead since
5873 // all its messages are dropped.
5874 let chaos_driver = if let Some(churn_config) = self.churn_config.clone() {
5875 use crate::node::network_bridge::get_fault_injector;
5876 use rand::SeedableRng;
5877 use rand::prelude::*;
5878
5879 let chaos_nodes = direct_nodes.clone();
5880 let network_name = self.name.clone();
5881 let non_gateway_count = self.number_of_nodes;
5882 let max_crashes = churn_config
5883 .max_simultaneous_crashes
5884 .unwrap_or((non_gateway_count / 4).max(1));
5885
5886 Some(tokio::spawn(async move {
5887 let mut chaos_rng = <rand::rngs::SmallRng as SeedableRng>::seed_from_u64(
5888 seed.wrapping_add(0xC1_0055_DEAD),
5889 );
5890
5891 // Wait for warmup before starting churn
5892 tokio::time::sleep(churn_config.warmup_delay).await;
5893
5894 // Track which nodes are currently crashed (pending recovery)
5895 let mut pending_recovery: Vec<(NodeLabel, tokio::time::Instant)> = Vec::new();
5896 let mut total_crashes: usize = 0;
5897 let mut total_recoveries: usize = 0;
5898 let mut total_permanent: usize = 0;
5899
5900 loop {
5901 tokio::time::sleep(churn_config.tick_interval).await;
5902
5903 // Phase 1: Recover nodes whose recovery delay has elapsed
5904 let now = tokio::time::Instant::now();
5905 let mut recovered = Vec::new();
5906 pending_recovery.retain(|(label, crash_time)| {
5907 if now.duration_since(*crash_time) >= churn_config.recovery_delay {
5908 recovered.push(label.clone());
5909 false
5910 } else {
5911 true
5912 }
5913 });
5914
5915 for label in recovered {
5916 let nodes_map = chaos_nodes.lock().await;
5917 let state = match nodes_map.get(&label) {
5918 Some(s) if !s.permanently_dropped => s,
5919 _ => continue,
5920 };
5921 let addr = state.addr;
5922 drop(nodes_map);
5923
5924 // Unblock messages — node resumes normal operation
5925 if let Some(injector) = get_fault_injector(&network_name) {
5926 let mut inj = injector.lock().unwrap();
5927 inj.config.recover_node(&addr);
5928 }
5929
5930 total_recoveries += 1;
5931 tracing::info!(
5932 ?label,
5933 ?addr,
5934 total_recoveries,
5935 "Chaos driver: node recovered"
5936 );
5937 }
5938
5939 // Phase 2: Crash eligible nodes
5940 let mut currently_crashed = pending_recovery.len();
5941 if currently_crashed >= max_crashes {
5942 continue;
5943 }
5944
5945 let nodes_map = chaos_nodes.lock().await;
5946 let eligible: Vec<(NodeLabel, SocketAddr)> = nodes_map
5947 .values()
5948 .filter(|s| {
5949 !s.is_gateway
5950 && !s.permanently_dropped
5951 && !pending_recovery.iter().any(|(l, _)| l == &s.label)
5952 })
5953 .map(|s| (s.label.clone(), s.addr))
5954 .collect();
5955 drop(nodes_map);
5956
5957 for (label, addr) in eligible {
5958 if currently_crashed >= max_crashes {
5959 break;
5960 }
5961 if !chaos_rng.random_bool(churn_config.crash_probability) {
5962 continue;
5963 }
5964
5965 let is_permanent =
5966 chaos_rng.random_bool(churn_config.permanent_crash_rate);
5967
5968 // Block all messages to/from this node
5969 if let Some(injector) = get_fault_injector(&network_name) {
5970 let mut inj = injector.lock().unwrap();
5971 inj.config.crash_node(addr);
5972 }
5973
5974 total_crashes += 1;
5975
5976 if is_permanent {
5977 let mut nodes_map = chaos_nodes.lock().await;
5978 if let Some(state) = nodes_map.get_mut(&label) {
5979 state.permanently_dropped = true;
5980 }
5981 total_permanent += 1;
5982 tracing::info!(
5983 ?label,
5984 ?addr,
5985 total_permanent,
5986 "Chaos driver: node permanently crashed"
5987 );
5988 } else {
5989 pending_recovery.push((label.clone(), tokio::time::Instant::now()));
5990 currently_crashed += 1;
5991 tracing::info!(
5992 ?label,
5993 ?addr,
5994 total_crashes,
5995 "Chaos driver: node crashed (will recover)"
5996 );
5997 }
5998 }
5999 }
6000 }))
6001 } else {
6002 None
6003 };
6004
6005 // Event driver: identical logic to run_fdev_test's client
6006 let user_ev_controller = self
6007 .user_ev_controller
6008 .take()
6009 .expect("user_ev_controller should be set");
6010 let labels: Vec<_> = self.labels.clone();
6011 let event_logs = self.event_listener.logs.clone();
6012
6013 // Give nodes time to start and establish connections
6014 tokio::time::sleep(Duration::from_secs(2)).await;
6015
6016 // Use a seeded RNG for deterministic peer selection
6017 use rand::SeedableRng;
6018 use rand::prelude::*;
6019 let mut event_rng = <rand::rngs::SmallRng as SeedableRng>::seed_from_u64(seed);
6020
6021 for event_id in 0..iterations as u32 {
6022 if let Some((_, peer_key)) = labels.choose(&mut event_rng) {
6023 if user_ev_controller
6024 .send((event_id, peer_key.clone()))
6025 .is_err()
6026 {
6027 tracing::warn!(event_id, "Failed to send event signal - receivers dropped");
6028 break;
6029 }
6030 }
6031 tokio::time::sleep(event_wait).await;
6032 }
6033
6034 // Wait for convergence with interest-sync repair.
6035 //
6036 // Poll convergence periodically while advancing virtual time.
6037 // Each poll interval lets heartbeat timers fire and broadcast
6038 // processing complete. The heartbeat interval is 300s, so
6039 // 1800s covers six full cycles — enough for cascading repair
6040 // broadcasts to propagate even when multiple state versions
6041 // compete across multi-hop topologies.
6042 //
6043 // Early exit: if all contracts converge, we break immediately.
6044 //
6045 // When `skip_convergence_wait` is set (TestConfig::no_convergence_wait()),
6046 // we do only a brief fixed settle and skip the polling loop entirely. The
6047 // polling tail is advisory — it only logs on failure, never fails the
6048 // simulation — so a test that never asserts convergence gains nothing from
6049 // it but pays up to 1800s of virtual time when the network happens not to
6050 // converge, which is exactly how test_interest_renewal timed out in CI (#3792).
6051 tokio::time::sleep(Duration::from_secs(15)).await;
6052
6053 if skip_convergence_wait {
6054 tracing::info!(
6055 "Skipping convergence-polling tail (require_convergence = false); \
6056 brief settle only"
6057 );
6058 } else {
6059 let converged = 'convergence: {
6060 for round in 0..30u32 {
6061 let result = check_convergence_from_logs(&event_logs).await;
6062 let total = result.converged.len() + result.diverged.len();
6063 if total > 0 && result.diverged.is_empty() {
6064 tracing::info!(
6065 converged = result.converged.len(),
6066 round,
6067 "Convergence achieved during propagation"
6068 );
6069 break 'convergence true;
6070 }
6071 tracing::debug!(
6072 converged = result.converged.len(),
6073 diverged = result.diverged.len(),
6074 round,
6075 "Convergence not yet achieved, waiting..."
6076 );
6077 tokio::time::sleep(Duration::from_secs(60)).await;
6078 }
6079 false
6080 };
6081 if !converged {
6082 tracing::warn!(
6083 "Propagation timeout — convergence not achieved after 30 rounds (1800s)"
6084 );
6085 }
6086 }
6087
6088 // Shutdown: abort chaos driver, time driver, then check node tasks
6089 if let Some(chaos) = chaos_driver {
6090 chaos.abort();
6091 }
6092 time_driver.abort();
6093
6094 let mut first_error: Option<anyhow::Error> = None;
6095 for handle in node_handles {
6096 handle.abort();
6097 match handle.await {
6098 // Node was aborted (normal shutdown) — ignore
6099 Err(e) if e.is_cancelled() => {}
6100 // Node panicked
6101 Err(e) => {
6102 let msg = format!("Node task panicked: {e}");
6103 tracing::error!("{}", msg);
6104 if first_error.is_none() {
6105 first_error = Some(anyhow::anyhow!("{}", msg));
6106 }
6107 }
6108 // Node returned an error before we aborted it
6109 Ok(Err(e)) => {
6110 tracing::error!("Node task failed: {e}");
6111 if first_error.is_none() {
6112 first_error = Some(e);
6113 }
6114 }
6115 // Node completed successfully (unlikely — event loops run forever)
6116 Ok(Ok(())) => {}
6117 }
6118 }
6119
6120 if let Some(e) = first_error {
6121 return Err(e);
6122 }
6123
6124 info!("Direct simulation completed successfully");
6125 Ok(())
6126 });
6127
6128 result
6129 }
6130
6131 // =========================================================================
6132 // Subscription Topology Validation
6133 // =========================================================================
6134
6135 /// Get the network name for this simulation.
6136 pub fn network_name(&self) -> &str {
6137 &self.name
6138 }
6139
6140 /// Get all subscription topology snapshots registered for this network.
6141 ///
6142 /// Returns snapshots registered by nodes via the topology registry.
6143 /// Call this after nodes have been running to see their subscription state.
6144 pub fn get_topology_snapshots(&self) -> Vec<crate::ring::topology_registry::TopologySnapshot> {
6145 crate::ring::topology_registry::get_all_topology_snapshots(&self.name)
6146 }
6147
6148 /// Validate subscription topology for a specific contract.
6149 ///
6150 /// Checks for:
6151 /// - Bidirectional cycles that create isolated islands
6152 /// - Orphan hosters without recovery paths
6153 /// - Unreachable hosters
6154 /// - Proximity violations in upstream selection
6155 ///
6156 /// Returns a validation result with any issues found.
6157 pub fn validate_subscription_topology(
6158 &self,
6159 contract_id: &freenet_stdlib::prelude::ContractInstanceId,
6160 contract_location: f64,
6161 ) -> crate::ring::topology_registry::TopologyValidationResult {
6162 crate::ring::topology_registry::validate_topology(
6163 &self.name,
6164 contract_id,
6165 contract_location,
6166 )
6167 }
6168
6169 /// Clear all topology snapshots for this network.
6170 ///
6171 /// Call this at the start of a test or after topology validation
6172 /// to reset the state.
6173 pub fn clear_topology_snapshots(&self) {
6174 crate::ring::topology_registry::clear_topology_snapshots(&self.name);
6175 }
6176
6177 /// Assert that subscription topology is healthy for a contract.
6178 ///
6179 /// # Panics
6180 /// Panics if any topology issues are detected.
6181 pub fn assert_topology_healthy(
6182 &self,
6183 contract_id: &freenet_stdlib::prelude::ContractInstanceId,
6184 contract_location: f64,
6185 ) {
6186 let result = self.validate_subscription_topology(contract_id, contract_location);
6187
6188 if !result.is_healthy() {
6189 let mut issues = Vec::new();
6190
6191 if !result.bidirectional_cycles.is_empty() {
6192 issues.push(format!(
6193 "ISSUE #2720: {} bidirectional cycles found: {:?}",
6194 result.bidirectional_cycles.len(),
6195 result.bidirectional_cycles
6196 ));
6197 }
6198
6199 if !result.orphan_hosters.is_empty() {
6200 issues.push(format!(
6201 "ISSUE #2719: {} orphan hosters found: {:?}",
6202 result.orphan_hosters.len(),
6203 result.orphan_hosters
6204 ));
6205 }
6206
6207 if !result.unreachable_hosters.is_empty() {
6208 issues.push(format!(
6209 "ISSUE #2720: {} unreachable hosters found: {:?}",
6210 result.unreachable_hosters.len(),
6211 result.unreachable_hosters
6212 ));
6213 }
6214
6215 if !result.proximity_violations.is_empty() {
6216 issues.push(format!(
6217 "ISSUE #2721: {} proximity violations found",
6218 result.proximity_violations.len()
6219 ));
6220 }
6221
6222 panic!(
6223 "Subscription topology has {} issues:\n{}",
6224 result.issue_count,
6225 issues.join("\n")
6226 );
6227 }
6228 }
6229}
6230
6231/// Result of a convergence check.
6232#[derive(Debug, Clone)]
6233pub struct ConvergenceResult {
6234 /// Contracts where all replicas have the same state hash.
6235 pub converged: Vec<ConvergedContract>,
6236 /// Contracts where replicas have different state hashes.
6237 pub diverged: Vec<DivergedContract>,
6238}
6239
6240impl ConvergenceResult {
6241 /// Returns the total number of contracts checked.
6242 pub fn total_contracts(&self) -> usize {
6243 self.converged.len() + self.diverged.len()
6244 }
6245
6246 /// Returns the convergence rate as a ratio (0.0 to 1.0).
6247 pub fn rate(&self) -> f64 {
6248 if self.total_contracts() == 0 {
6249 return 1.0;
6250 }
6251 self.converged.len() as f64 / self.total_contracts() as f64
6252 }
6253
6254 /// Returns true if all replicated contracts have converged.
6255 pub fn is_converged(&self) -> bool {
6256 self.diverged.is_empty()
6257 }
6258}
6259
6260/// A contract that has converged (all replicas have the same state).
6261#[derive(Debug, Clone)]
6262pub struct ConvergedContract {
6263 /// The contract key
6264 pub contract_key: String,
6265 /// The state hash that all replicas have
6266 pub state_hash: String,
6267 /// Number of replicas that have this state
6268 pub replica_count: usize,
6269}
6270
6271/// A contract that has diverged (replicas have different states).
6272#[derive(Debug, Clone)]
6273pub struct DivergedContract {
6274 /// The contract key
6275 pub contract_key: String,
6276 /// Map of peer address to their state hash
6277 pub peer_states: Vec<(SocketAddr, String)>,
6278}
6279
6280impl DivergedContract {
6281 /// Returns the number of unique states across replicas.
6282 pub fn unique_state_count(&self) -> usize {
6283 let unique: HashSet<&String> = self.peer_states.iter().map(|(_, h)| h).collect();
6284 unique.len()
6285 }
6286}
6287
6288// =============================================================================
6289// Gap 4: Contract Distribution Types
6290// =============================================================================
6291
6292/// Information about how a contract is distributed across the network.
6293#[derive(Debug, Clone)]
6294pub struct ContractDistribution {
6295 /// The contract key
6296 pub contract_key: String,
6297 /// Number of replicas
6298 pub replica_count: usize,
6299 /// Peers that have this contract
6300 pub peers: Vec<SocketAddr>,
6301}
6302
6303// =============================================================================
6304// Gap T4: Operation Tracking Types
6305// =============================================================================
6306
6307/// Summary of operation completion status across the network.
6308#[derive(Debug, Clone, Default)]
6309pub struct OperationSummary {
6310 /// Put operation statistics
6311 pub put: PutOperationStats,
6312 /// Get operation statistics
6313 pub get: OperationStats,
6314 /// Subscribe operation statistics
6315 pub subscribe: OperationStats,
6316 /// Update operation statistics
6317 pub update: UpdateOperationStats,
6318 /// Total number of timed-out operations
6319 pub timeouts: usize,
6320}
6321
6322impl OperationSummary {
6323 /// Returns total number of operations requested.
6324 pub fn total_requested(&self) -> usize {
6325 self.put.requested + self.get.requested + self.subscribe.requested + self.update.requested
6326 }
6327
6328 /// Returns total number of operations completed (succeeded + failed).
6329 pub fn total_completed(&self) -> usize {
6330 self.put.completed()
6331 + self.get.completed()
6332 + self.subscribe.completed()
6333 + self.update.completed()
6334 }
6335
6336 /// Returns total number of successful operations.
6337 pub fn total_succeeded(&self) -> usize {
6338 self.put.succeeded + self.get.succeeded + self.subscribe.succeeded + self.update.succeeded
6339 }
6340
6341 /// Returns total number of failed operations.
6342 pub fn total_failed(&self) -> usize {
6343 self.put.failed + self.get.failed + self.subscribe.failed + self.update.failed
6344 }
6345
6346 /// Returns overall success rate (0.0 to 1.0).
6347 /// Includes timeouts as failed operations.
6348 pub fn overall_success_rate(&self) -> f64 {
6349 let completed = self.total_completed() + self.timeouts;
6350 if completed == 0 {
6351 return 1.0; // No operations completed yet
6352 }
6353 self.total_succeeded() as f64 / completed as f64
6354 }
6355
6356 /// Returns true if all requested operations have completed.
6357 pub fn all_completed(&self) -> bool {
6358 self.total_completed() >= self.total_requested()
6359 }
6360}
6361
6362/// Statistics for a basic operation type (Get, Subscribe).
6363#[derive(Debug, Clone, Default)]
6364pub struct OperationStats {
6365 /// Number of operations requested
6366 pub requested: usize,
6367 /// Number of operations that succeeded
6368 pub succeeded: usize,
6369 /// Number of operations that failed
6370 pub failed: usize,
6371}
6372
6373impl OperationStats {
6374 /// Returns total completed operations (succeeded + failed).
6375 pub fn completed(&self) -> usize {
6376 self.succeeded + self.failed
6377 }
6378
6379 /// Returns success rate (0.0 to 1.0).
6380 pub fn success_rate(&self) -> f64 {
6381 let completed = self.completed();
6382 if completed == 0 {
6383 return 1.0;
6384 }
6385 self.succeeded as f64 / completed as f64
6386 }
6387}
6388
6389/// Statistics for Put operations (includes broadcast tracking).
6390#[derive(Debug, Clone, Default)]
6391pub struct PutOperationStats {
6392 /// Number of Put operations requested
6393 pub requested: usize,
6394 /// Number of Put operations that succeeded
6395 pub succeeded: usize,
6396 /// Number of Put operations that failed
6397 pub failed: usize,
6398 /// Number of broadcasts emitted
6399 pub broadcasts_emitted: usize,
6400 /// Number of broadcasts received by peers
6401 pub broadcasts_received: usize,
6402}
6403
6404impl PutOperationStats {
6405 /// Returns total completed operations (succeeded + failed).
6406 pub fn completed(&self) -> usize {
6407 self.succeeded + self.failed
6408 }
6409
6410 /// Returns success rate (0.0 to 1.0).
6411 pub fn success_rate(&self) -> f64 {
6412 let completed = self.completed();
6413 if completed == 0 {
6414 return 1.0;
6415 }
6416 self.succeeded as f64 / completed as f64
6417 }
6418
6419 /// Returns broadcast propagation rate (received / emitted).
6420 pub fn broadcast_propagation_rate(&self) -> f64 {
6421 if self.broadcasts_emitted == 0 {
6422 return 1.0;
6423 }
6424 self.broadcasts_received as f64 / self.broadcasts_emitted as f64
6425 }
6426}
6427
6428/// Statistics for Update operations (includes broadcast tracking).
6429#[derive(Debug, Clone, Default)]
6430pub struct UpdateOperationStats {
6431 /// Number of Update operations requested
6432 pub requested: usize,
6433 /// Number of Update operations that succeeded
6434 pub succeeded: usize,
6435 /// Number of Update operations that failed
6436 pub failed: usize,
6437 /// Number of update broadcasts emitted
6438 pub broadcasts_emitted: usize,
6439 /// Number of update broadcasts received by subscribers
6440 pub broadcasts_received: usize,
6441}
6442
6443impl UpdateOperationStats {
6444 /// Returns total completed operations (succeeded + failed).
6445 pub fn completed(&self) -> usize {
6446 self.succeeded + self.failed
6447 }
6448
6449 /// Returns success rate (0.0 to 1.0).
6450 pub fn success_rate(&self) -> f64 {
6451 let completed = self.completed();
6452 if completed == 0 {
6453 return 1.0;
6454 }
6455 self.succeeded as f64 / completed as f64
6456 }
6457}
6458
6459#[cfg(any(debug_assertions, test))]
6460impl std::fmt::Debug for SimNetwork {
6461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6462 f.debug_struct("SimNetwork")
6463 .field("name", &self.name)
6464 .field("labels", &self.labels)
6465 .field("number_of_gateways", &self.number_of_gateways)
6466 .field("number_of_nodes", &self.number_of_nodes)
6467 .field("ring_max_htl", &self.ring_max_htl)
6468 .field("rnd_if_htl_above", &self.rnd_if_htl_above)
6469 .field("max_connections", &self.max_connections)
6470 .field("min_connections", &self.min_connections)
6471 .field("init_backoff", &self.start_backoff)
6472 .finish()
6473 }
6474}
6475
6476impl Drop for SimNetwork {
6477 fn drop(&mut self) {
6478 use crate::node::network_bridge::set_fault_injector;
6479 use crate::ring::topology_registry::{
6480 clear_current_network_name, clear_renewal_metrics, clear_summarize_metrics,
6481 clear_topology_snapshots,
6482 };
6483 use crate::transport::in_memory_socket::{
6484 clear_network_address_mappings, remove_network_socket_registry,
6485 set_packet_delivery_callback, set_queue_packet_callback,
6486 };
6487
6488 // Per-network cleanup (safe for parallel tests)
6489 set_fault_injector(&self.name, None);
6490 unregister_network_time_source(&self.name);
6491 clear_topology_snapshots(&self.name);
6492 clear_renewal_metrics(&self.name);
6493 clear_summarize_metrics(&self.name);
6494 remove_network_socket_registry(&self.name);
6495 clear_network_address_mappings(&self.name);
6496
6497 // Clear global callbacks to prevent stale references between
6498 // sequential simulation runs (e.g., determinism tests).
6499 set_packet_delivery_callback(None);
6500 set_queue_packet_callback(None);
6501
6502 // Thread-local cleanup
6503 clear_current_network_name();
6504
6505 if self.clean_up_tmp_dirs {
6506 clean_up_tmp_dirs(self.labels.iter().map(|(l, _)| l));
6507 }
6508 }
6509}
6510
6511/// Wait for a spawned node task to publish its `ConnectionManager` (or any
6512/// other value) to the shared slot, then take it.
6513///
6514/// Returns `None` only if the spawned task fails to publish within
6515/// `MAX_PUBLISH_POLLS` yield passes, which in practice means it either
6516/// panicked before reaching its publish point or is wedged in a loop before
6517/// its first `.await`. The caller (`SimNetwork::start_*`) treats that as a
6518/// hard failure and panics with the node label, because downstream sim
6519/// helpers like `sim.connection_count(label)` would otherwise silently
6520/// return `None` for that node and confuse every assertion that depends on
6521/// the full node set.
6522///
6523/// ## Rationale
6524///
6525/// The previous implementation slept for `start_backoff` (50ms in the
6526/// nightly fault-recovery test) and then called `take()` once. On a busy
6527/// runner the spawned node task had not yet been polled far enough to reach
6528/// the `shared_cm` write at the top of `run_node_with_shared_storage`, so
6529/// `take()` returned `None` and the CM was silently dropped. That was the
6530/// deterministic "46/50 connection managers" failure in
6531/// `test_nightly_fault_recovery_speed`.
6532///
6533/// ## Mechanism
6534///
6535/// 1. Honor the configured `start_backoff` first so staggered-startup timing
6536/// is preserved for tests that depend on it.
6537/// 2. Try `take()` once — this covers the fast path where the node task was
6538/// already polled during the backoff sleep.
6539/// 3. If the slot is still empty, yield to the scheduler via
6540/// `tokio::task::yield_now()` and re-check, bounded by an iteration
6541/// budget. The publish happens synchronously before any `.await` in
6542/// `run_node_with_shared_storage`, so once the spawned task is polled
6543/// once past the entry point the slot is populated; we only need enough
6544/// yields to give the scheduler a chance to choose it. The bound is an
6545/// iteration count rather than a wall-clock deadline so the helper
6546/// behaves identically under real and paused tokio runtimes (the direct
6547/// simulation runner uses `tokio::time::start_paused(true)`, which makes
6548/// any `tokio::time::Instant`-based deadline auto-advance virtual time
6549/// while real scheduling has not progressed). It also keeps us out of
6550/// the DST rule that bans `std::time::Instant::now()` in `crates/core/`.
6551///
6552/// Generic over the slot payload so unit tests can drive the helper without
6553/// constructing a full `ConnectionManager`.
6554async fn capture_shared_slot<T>(
6555 slot: &Arc<parking_lot::Mutex<Option<T>>>,
6556 start_backoff: Duration,
6557 label: &NodeLabel,
6558) -> Option<T> {
6559 tokio::time::sleep(start_backoff).await;
6560
6561 if let Some(value) = slot.lock().take() {
6562 return Some(value);
6563 }
6564
6565 // Publication race: the spawned task hasn't been polled to its publish
6566 // point yet. Yield-and-recheck with a generous but bounded budget.
6567 //
6568 // On a current_thread runtime each `yield_now().await` hands control to
6569 // the scheduler, which then polls any other ready task (including our
6570 // target spawned task) before polling us again. A single yield is
6571 // usually enough, but a busy sim with hundreds of spawned tasks queued
6572 // ahead of ours may need many more. 1024 is comfortably above every
6573 // observed case and still returns in microseconds on the panic path.
6574 const MAX_PUBLISH_POLLS: usize = 1024;
6575 for _ in 0..MAX_PUBLISH_POLLS {
6576 tokio::task::yield_now().await;
6577 if let Some(value) = slot.lock().take() {
6578 return Some(value);
6579 }
6580 }
6581
6582 tracing::warn!(
6583 %label,
6584 max_polls = MAX_PUBLISH_POLLS,
6585 "SimNetwork: node did not publish its shared slot within the \
6586 yield-poll budget — the spawned `run_node` task likely panicked \
6587 before reaching its publish point, or is wedged before its first \
6588 `.await`."
6589 );
6590 None
6591}
6592
6593#[cfg(test)]
6594mod capture_shared_slot_tests {
6595 use super::{NodeLabel, capture_shared_slot};
6596 use std::{sync::Arc, time::Duration};
6597
6598 fn label() -> NodeLabel {
6599 NodeLabel::gateway("test", 0)
6600 }
6601
6602 /// Fast path: the spawned task already published its value during the
6603 /// initial `start_backoff` sleep (the old code's `take()` would have seen
6604 /// it too). `capture_shared_slot` must return the published value
6605 /// unchanged.
6606 #[tokio::test(flavor = "current_thread")]
6607 async fn fast_path_returns_published_value() {
6608 let slot = Arc::new(parking_lot::Mutex::new(Some(42u32)));
6609 let result = capture_shared_slot(&slot, Duration::from_millis(1), &label()).await;
6610 assert_eq!(result, Some(42));
6611 assert!(slot.lock().is_none(), "slot must be drained by take()");
6612 }
6613
6614 /// The pre-fix race: the spawned task publishes *after* the initial
6615 /// `start_backoff` sleep, during the yield-poll phase. The old code's
6616 /// single `take()` would miss this and return `None`. The new helper's
6617 /// yield loop must observe the publication and return `Some`.
6618 #[tokio::test(flavor = "current_thread")]
6619 async fn publication_race_resolves_via_yield_loop() {
6620 let slot = Arc::new(parking_lot::Mutex::new(None::<u32>));
6621 let publisher_slot = Arc::clone(&slot);
6622
6623 // Publish after several yield_now() passes — simulates a spawned
6624 // node task that hadn't been polled to its publish point yet when
6625 // `capture_shared_slot` entered the yield loop.
6626 tokio::spawn(async move {
6627 for _ in 0..3 {
6628 tokio::task::yield_now().await;
6629 }
6630 *publisher_slot.lock() = Some(7);
6631 });
6632
6633 let result = capture_shared_slot(&slot, Duration::from_millis(1), &label()).await;
6634 assert_eq!(result, Some(7));
6635 }
6636
6637 /// Timeout path: nothing ever publishes. `capture_shared_slot` must
6638 /// exhaust its yield-poll budget and return `None` so the caller can
6639 /// panic loudly with the node label. With the iteration-bounded loop
6640 /// this completes in microseconds (no wall-clock wait), so unlike the
6641 /// earlier draft that used a 5-second `std::time::Instant` deadline
6642 /// the test runs on every `cargo test` pass without `#[ignore]`.
6643 #[tokio::test(flavor = "current_thread")]
6644 async fn timeout_returns_none_when_never_published() {
6645 let slot: Arc<parking_lot::Mutex<Option<u32>>> = Arc::new(parking_lot::Mutex::new(None));
6646 let result = capture_shared_slot(&slot, Duration::from_millis(1), &label()).await;
6647 assert_eq!(result, None);
6648 }
6649}
6650
6651fn clean_up_tmp_dirs<'a>(labels: impl Iterator<Item = &'a NodeLabel>) {
6652 for label in labels {
6653 let p = std::env::temp_dir().join(format!(
6654 "freenet-executor-{sim}-{label}",
6655 sim = "sim",
6656 label = label
6657 ));
6658 // Best-effort cleanup of temp dirs; failure is not critical
6659 let _removed = std::fs::remove_dir_all(p);
6660 }
6661}
6662
6663/// Check convergence from event logs.
6664///
6665/// This function can be used after `run_simulation` completes to check
6666/// if all contracts have converged to the same state across replicas.
6667///
6668/// # Arguments
6669/// * `logs` - Event logs obtained via `sim.event_logs_handle()` before calling `run_simulation`
6670///
6671/// # Example
6672/// ```ignore
6673/// let logs_handle = sim.event_logs_handle();
6674/// let result = sim.run_simulation::<...>(...);
6675/// let convergence = check_convergence_from_logs(&logs_handle).await;
6676/// ```
6677pub async fn check_convergence_from_logs(
6678 logs: &Arc<tokio::sync::Mutex<Vec<crate::tracing::NetLogMessage>>>,
6679) -> ConvergenceResult {
6680 let logs = logs.lock().await;
6681
6682 // Group (contract_key -> peer_addr -> latest_state_hash)
6683 // Use BTreeMap for deterministic iteration order
6684 let mut contract_states: BTreeMap<String, BTreeMap<SocketAddr, String>> = BTreeMap::new();
6685
6686 // Iterate in insertion order - the last event for each (contract, peer) pair
6687 // is the actual current state.
6688 for log in logs.iter() {
6689 let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
6690 let state_hash = log.kind.stored_state_hash().map(String::from);
6691
6692 if let (Some(contract_key), Some(state_hash)) = (contract_key, state_hash) {
6693 contract_states
6694 .entry(contract_key)
6695 .or_default()
6696 .insert(log.peer_id.socket_addr(), state_hash);
6697 }
6698 }
6699
6700 let mut converged = Vec::new();
6701 let mut diverged = Vec::new();
6702
6703 for (contract_key, peer_states) in contract_states {
6704 if peer_states.len() < 2 {
6705 continue;
6706 }
6707
6708 let unique_states: HashSet<&String> = peer_states.values().collect();
6709
6710 if unique_states.len() == 1 {
6711 let state = unique_states.into_iter().next().unwrap().clone();
6712 converged.push(ConvergedContract {
6713 contract_key,
6714 state_hash: state,
6715 replica_count: peer_states.len(),
6716 });
6717 } else {
6718 diverged.push(DivergedContract {
6719 contract_key,
6720 peer_states: peer_states.into_iter().collect(),
6721 });
6722 }
6723 }
6724
6725 ConvergenceResult {
6726 converged,
6727 diverged,
6728 }
6729}
6730
6731use crate::contract::OperationMode;
6732
6733#[cfg(test)]
6734mod tests {
6735 use super::*;
6736
6737 /// Unit test for the fault-injection delivery decision (#4694 / #4642 piece F).
6738 ///
6739 /// Directly exercises `fault_injection_delivery_decision` — the callback the
6740 /// direct/controlled runners install so injected crashes and partitions
6741 /// become real packet drops. Verifies:
6742 /// 1. Without `enforce_fault_drops`, every packet is delivered (the inert
6743 /// state that made fault injection false-green before #4694).
6744 /// 2. With `enforce_fault_drops`, packets to/from a crashed node are
6745 /// dropped (either direction) and counted in `messages_dropped_crash`.
6746 /// 3. With `enforce_fault_drops`, partitioned pairs are dropped and counted
6747 /// in `messages_dropped_partition`.
6748 /// 4. Unaffected pairs are still delivered.
6749 #[test]
6750 fn test_fault_injection_delivery_decision_drops_crash_and_partition() {
6751 use crate::node::network_bridge::{FaultInjectorState, set_fault_injector};
6752 use crate::simulation::{FaultConfig, Partition, VirtualTime};
6753 use crate::transport::in_memory_socket::PacketDeliveryDecision;
6754 use std::collections::HashSet;
6755 use std::sync::{Arc, Mutex};
6756
6757 const NETWORK: &str = "fault-decision-unit-test";
6758
6759 let crashed: SocketAddr = "127.0.0.1:20001".parse().unwrap();
6760 let live_a: SocketAddr = "127.0.0.1:20002".parse().unwrap();
6761 let part_x: SocketAddr = "127.0.0.1:20003".parse().unwrap();
6762 let part_y: SocketAddr = "127.0.0.1:20004".parse().unwrap();
6763
6764 let mut config = FaultConfig::default();
6765 config.crash_node(crashed);
6766 config.add_partition(
6767 Partition::new(HashSet::from([part_x]), HashSet::from([part_y])).permanent(0),
6768 );
6769
6770 let state = FaultInjectorState::new(config, 42).with_virtual_time(VirtualTime::new());
6771 let injector = Arc::new(Mutex::new(state));
6772 set_fault_injector(NETWORK, Some(injector.clone()));
6773
6774 let is_drop = |d: PacketDeliveryDecision| matches!(d, PacketDeliveryDecision::Drop);
6775
6776 // (1) Not opted in: faults are inert, everything is delivered.
6777 assert!(
6778 !is_drop(fault_injection_delivery_decision(NETWORK, crashed, live_a)),
6779 "crash must be inert without enforce_fault_drops (pre-#4694 behavior)"
6780 );
6781 assert!(
6782 !is_drop(fault_injection_delivery_decision(NETWORK, part_x, part_y)),
6783 "partition must be inert without enforce_fault_drops"
6784 );
6785
6786 injector.lock().unwrap().enforce_fault_drops = true;
6787
6788 // (2) Crashed node: dropped in both directions.
6789 assert!(
6790 is_drop(fault_injection_delivery_decision(NETWORK, crashed, live_a)),
6791 "packet FROM a crashed node must be dropped"
6792 );
6793 assert!(
6794 is_drop(fault_injection_delivery_decision(NETWORK, live_a, crashed)),
6795 "packet TO a crashed node must be dropped"
6796 );
6797
6798 // (3) Partitioned pair: dropped in both directions.
6799 assert!(
6800 is_drop(fault_injection_delivery_decision(NETWORK, part_x, part_y)),
6801 "packet across an active partition must be dropped (x->y)"
6802 );
6803 assert!(
6804 is_drop(fault_injection_delivery_decision(NETWORK, part_y, part_x)),
6805 "packet across an active partition must be dropped (y->x)"
6806 );
6807
6808 // (4) Unaffected pair: delivered.
6809 assert!(
6810 !is_drop(fault_injection_delivery_decision(NETWORK, live_a, part_x)),
6811 "healthy, non-partitioned pair must be delivered"
6812 );
6813
6814 let stats = injector.lock().unwrap().stats.clone();
6815 assert_eq!(stats.messages_dropped_crash, 2, "two crash drops expected");
6816 assert_eq!(
6817 stats.messages_dropped_partition, 2,
6818 "two partition drops expected"
6819 );
6820
6821 set_fault_injector(NETWORK, None);
6822 }
6823
6824 /// Test that peer locations are deterministic with the same seed.
6825 ///
6826 /// Regression test for issue #2759 - SimNetwork should produce identical
6827 /// peer locations across multiple runs with the same seed.
6828 #[tokio::test]
6829 async fn test_deterministic_peer_locations() {
6830 const SEED: u64 = 0xDEADBEEF_CAFEBABE;
6831
6832 // Create first network
6833 let sim1 = SimNetwork::new(
6834 "determinism-test-1",
6835 2, // 2 gateways
6836 3, // 3 regular nodes
6837 7, // ring_max_htl
6838 3, // rnd_if_htl_above
6839 10, // max_connections
6840 2, // min_connections
6841 SEED,
6842 )
6843 .await;
6844
6845 let locations1 = sim1.get_peer_locations();
6846
6847 // Create second network with same seed
6848 let sim2 = SimNetwork::new(
6849 "determinism-test-2",
6850 2, // same config
6851 3,
6852 7,
6853 3,
6854 10,
6855 2,
6856 SEED, // same seed
6857 )
6858 .await;
6859
6860 let locations2 = sim2.get_peer_locations();
6861
6862 // Verify locations are identical
6863 assert_eq!(
6864 locations1, locations2,
6865 "Peer locations should be identical with the same seed.\n\
6866 Run 1: {:?}\n\
6867 Run 2: {:?}",
6868 locations1, locations2
6869 );
6870 }
6871}