Skip to main content

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}