//! RELAY_SPEC §4.7 / §6.2 behavioural tests for the relay vertex //! and its mutations. These exercise the network layer directly, //! mirroring `tests/network_invariants.rs` for the parent §5.7 //! properties. //! //! Each test name maps to a §4.7 / §6.2 property; the assertions //! avoid mirroring the implementation and focus on what a third //! party reading the spec would expect to observe. use simulation::network::{DropReason, Network, NetworkNotification, RelayDropReason, SendOutcome}; use simulation::scenario::{HostKindRegistry, load_from_str}; use std::path::Path; const ALL_NOTIFICATIONS: &str = include_str!("../scenarios/parity/reference.toml"); fn registry() -> HostKindRegistry { let mut r = HostKindRegistry::with_swim(); r.register(Box::new(simulation::parity_host::ParityStubKindValidator)); r } // Build a minimal scenario from inline TOML so the tests are self- // contained — they don't reach for shipped scenario files. fn scenario_from(text: &str) -> simulation::scenario::Scenario { load_from_str(Path::new("(test)"), text, ®istry()) .expect("test scenario must validate") } /// Three host topology routed through one relay. Used by most relay /// tests. fn one_relay_three_hosts() -> simulation::scenario::Scenario { scenario_from( r#" name = "relay_three_hosts" seed = 1 duration_ns = 1_000_000_000 [default_tick] period_ns = 1_000_000 [default_link] latency_ns = 1_000_000 jitter_stddev_ns = 0 loss_prob_ppm = 0 reorder_prob_ppm = 0 bandwidth_bps = 1_000_000_000 cold_dial_penalty_ns = 0 cache_warm_after_ns = 1_000_000_000 cache_invalidate_after_idle_ns = 10_000_000_000 [[relays]] id = "R" ingress_capacity_bps = 1_000_000_000 egress_capacity_bps_per_link = 8_000_000 # 1 MB/s per outbound link queue_depth_bytes = 1_000_000 cold_start_penalty_ns = 0 [[peers]] id = "alpha" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["alpha", "bravo", "charlie"] } [[peers]] id = "bravo" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["alpha", "bravo", "charlie"] } [[peers]] id = "charlie" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["alpha", "bravo", "charlie"] } [[links]] from = "alpha" to = "bravo" via = "R" [[links]] from = "bravo" to = "alpha" via = "R" [[links]] from = "alpha" to = "charlie" via = "R" [[links]] from = "charlie" to = "alpha" via = "R" [[links]] from = "bravo" to = "charlie" via = "R" [[links]] from = "charlie" to = "bravo" via = "R" "#, ) } fn collect_notifications(net: &mut Network) -> Vec { net.take_pending_notifications() } // ──────────────────────────────────────────────────────────────────── // RELAY_SPEC §4.7 // ──────────────────────────────────────────────────────────────────── #[test] fn composition_is_transparent_to_hosts() { // §4.7 "Composition is transparent to hosts." A `send` over a // relayed route returns one `SendOutcome` shaped identically to a // direct send's. The arrival is a single integer; no extra // structure leaks. let scen = one_relay_three_hosts(); let mut net = Network::new(&scen); let outcome = net.send("alpha", "bravo", 1024, 0); match outcome { SendOutcome::Arrive { delivery_id: _, at_ns } => { assert!(at_ns > 0, "relayed send should produce a positive arrival"); } SendOutcome::Drop { reason } => panic!("unexpected drop: {reason:?}"), } } #[test] fn relayed_arrival_is_later_than_a_direct_send_of_the_same_size() { // RELAY_SPEC §11 phase R1: "A direct send through a relay arrives // later than the same send over a direct edge of equal policy by // the relay's ingress + egress serialization time." We confirm // the direction; the exact numeric is the calibration suite's job. let scen_direct = scenario_from( r#" name = "direct" seed = 1 duration_ns = 1_000_000_000 [default_tick] period_ns = 1_000_000 [default_link] latency_ns = 1_000_000 jitter_stddev_ns = 0 loss_prob_ppm = 0 reorder_prob_ppm = 0 bandwidth_bps = 1_000_000_000 cold_dial_penalty_ns = 0 cache_warm_after_ns = 1_000_000_000 cache_invalidate_after_idle_ns = 10_000_000_000 [[peers]] id = "a" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[peers]] id = "b" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[links]] from = "a" to = "b" [[links]] from = "b" to = "a" "#, ); let scen_relayed = one_relay_three_hosts(); let mut direct = Network::new(&scen_direct); let mut relayed = Network::new(&scen_relayed); let SendOutcome::Arrive { at_ns: direct_arrival, .. } = direct.send("a", "b", 8192, 0) else { panic!("direct send must arrive"); }; let SendOutcome::Arrive { at_ns: relayed_arrival, .. } = relayed.send("alpha", "bravo", 8192, 0) else { panic!("relayed send must arrive"); }; assert!( relayed_arrival > direct_arrival, "relayed arrival ({relayed_arrival}) should exceed direct arrival ({direct_arrival}) by the relay's serialization time" ); } #[test] fn head_of_line_is_observable_on_a_shared_egress() { // §4.7 "HOL is observable and bounded." Two messages from the // same sender to the same destination through the relay are // serialized on the shared egress: the second's arrival is at or // after the first's arrival plus the first's egress serialization. let scen = one_relay_three_hosts(); let mut net = Network::new(&scen); // 100 KB messages so the egress serialization time dominates. let SendOutcome::Arrive { at_ns: first, .. } = net.send("alpha", "bravo", 100_000, 0) else { panic!("first send must arrive"); }; let SendOutcome::Arrive { at_ns: second, .. } = net.send("alpha", "bravo", 100_000, 0) else { panic!("second send must arrive"); }; // egress_capacity_bps_per_link in the spec is *bytes* per second // (SIM_SPEC §5.2 / RELAY_SPEC §4.2). 8_000_000 ⇒ 8 MB/s. 100 KB // takes 100_000 / 8_000_000 = 12.5 ms = 12_500_000 ns. The gap // between two shared-egress sends must be ≥ that floor. let expected_min_gap = 12_500_000u64; assert!( second >= first + expected_min_gap, "second arrival should follow first by ≥ egress serialization (first={first}, second={second}, expected_min_gap={expected_min_gap})" ); } #[test] fn ingress_and_egress_are_independent_across_destinations() { // §4.7 "Ingress and egress are independent." A message destined // for peer X does not delay a message destined for peer Y on the // egress side. Two simultaneous sends to *different* // destinations share only the ingress; the egress queues are // independent. let scen = one_relay_three_hosts(); let mut net = Network::new(&scen); let SendOutcome::Arrive { at_ns: to_bravo, .. } = net.send("alpha", "bravo", 100_000, 0) else { panic!("first send must arrive"); }; let SendOutcome::Arrive { at_ns: to_charlie, .. } = net.send("alpha", "charlie", 100_000, 0) else { panic!("second send must arrive"); }; // Same source so the inbound leg serializes, and the ingress // serializes, but the egress queues are separate. The second's // egress should NOT have to wait for the first's egress // serialization. So the gap should be much smaller than the // shared-egress case in `head_of_line_is_observable_on_a_shared_egress`. let shared_egress_min_gap = 100_000_000u64; let gap = to_charlie - to_bravo; assert!( gap < shared_egress_min_gap, "different-destination gap ({gap}) should be smaller than the shared-egress gap ({shared_egress_min_gap})" ); } #[test] fn queue_overflow_drops_with_relay_queue_full() { // §4.7 "Queue overflow is exact." A send that would push // `enqueued_bytes` strictly above `queue_depth_bytes` drops with // `RelayQueueFull` and emits a `RelayDrop` record. let scen = scenario_from( r#" name = "tight_queue" seed = 1 duration_ns = 1_000_000_000 [default_tick] period_ns = 1_000_000 [default_link] latency_ns = 1_000_000 jitter_stddev_ns = 0 loss_prob_ppm = 0 reorder_prob_ppm = 0 bandwidth_bps = 1_000_000_000 cold_dial_penalty_ns = 0 cache_warm_after_ns = 1_000_000_000 cache_invalidate_after_idle_ns = 10_000_000_000 [[relays]] id = "R" ingress_capacity_bps = 1_000_000_000 egress_capacity_bps_per_link = 8_000 # 1 KB/s; egress can't drain queue_depth_bytes = 1024 # 1 KB total queue cold_start_penalty_ns = 0 [[peers]] id = "a" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[peers]] id = "b" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[links]] from = "a" to = "b" via = "R" [[links]] from = "b" to = "a" via = "R" "#, ); let mut net = Network::new(&scen); // Three 512 B messages back to back: first fits, second fills the // queue (1024 total), third overflows. let SendOutcome::Arrive { .. } = net.send("a", "b", 512, 0) else { panic!("first 512B should fit"); }; let SendOutcome::Arrive { .. } = net.send("a", "b", 512, 0) else { panic!("second 512B should fit (queue = 1024 total)"); }; let outcome = net.send("a", "b", 1, 0); assert!( matches!(outcome, SendOutcome::Drop { reason: DropReason::RelayQueueFull }), "third send must overflow with RelayQueueFull, got {outcome:?}" ); let notes = collect_notifications(&mut net); let drops: Vec<_> = notes .iter() .filter_map(|n| { if let NetworkNotification::RelayDrop { relay, reason: RelayDropReason::QueueFull, .. } = n { Some(relay.clone()) } else { None } }) .collect(); assert_eq!( drops, vec!["R".to_string()], "exactly one RelayDrop{{QueueFull}} notification expected" ); } #[test] fn cold_start_penalty_is_paid_exactly_once_per_boot() { // §4.7 "Cold-start penalty is paid once per boot." After a // RelayBoot, the first forwarded message includes the penalty; // the second does not. let scen = scenario_from( r#" name = "boot_penalty" seed = 1 duration_ns = 1_000_000_000 [default_tick] period_ns = 1_000_000 [default_link] latency_ns = 1_000_000 jitter_stddev_ns = 0 loss_prob_ppm = 0 reorder_prob_ppm = 0 bandwidth_bps = 1_000_000_000 cold_dial_penalty_ns = 0 cache_warm_after_ns = 1_000_000_000 cache_invalidate_after_idle_ns = 10_000_000_000 [[relays]] id = "R" ingress_capacity_bps = 1_000_000_000 egress_capacity_bps_per_link = 1_000_000_000 queue_depth_bytes = 100_000 cold_start_penalty_ns = 50_000_000 # 50ms [[peers]] id = "a" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[peers]] id = "b" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[links]] from = "a" to = "b" via = "R" [[links]] from = "b" to = "a" via = "R" [[mutations]] at_ns = 100_000_000 kind = "relay_kill" relay = "R" [[mutations]] at_ns = 200_000_000 kind = "relay_boot" relay = "R" "#, ); use simulation::scenario::{Mutation, MutationKind}; let mut net = Network::new(&scen); // Boot the relay at t=200ms (simulating engine dispatch). net.apply_mutation( &Mutation { at_ns: 200_000_000, kind: MutationKind::RelayBoot { relay: "R".into(), }, }, 200_000_000, ); let SendOutcome::Arrive { at_ns: first, .. } = net.send("a", "b", 1024, 300_000_000) else { panic!("first post-boot send must arrive"); }; let SendOutcome::Arrive { at_ns: second, .. } = net.send("a", "b", 1024, 400_000_000) else { panic!("second post-boot send must arrive"); }; // The first should include the 50ms penalty; the second should // not, so first - sent ≥ 50ms and second - sent < 50ms. let first_relative = first - 300_000_000; let second_relative = second - 400_000_000; assert!( first_relative >= 50_000_000, "first post-boot send should include the 50ms cold-start penalty (relative={first_relative}ns)" ); assert!( second_relative < 50_000_000, "second post-boot send should NOT include the 50ms cold-start penalty (relative={second_relative}ns)" ); } #[test] fn relay_kill_drops_subsequent_sends_with_relay_down() { // §4.7 "Mutation invalidation is exact." After a RelayKill, sends // through the relay drop with RelayDown. let scen = scenario_from( r#" name = "kill_then_send" seed = 1 duration_ns = 1_000_000_000 [default_tick] period_ns = 1_000_000 [default_link] latency_ns = 1_000_000 jitter_stddev_ns = 0 loss_prob_ppm = 0 reorder_prob_ppm = 0 bandwidth_bps = 1_000_000_000 cold_dial_penalty_ns = 0 cache_warm_after_ns = 1_000_000_000 cache_invalidate_after_idle_ns = 10_000_000_000 [[relays]] id = "R" ingress_capacity_bps = 1_000_000_000 egress_capacity_bps_per_link = 1_000_000_000 queue_depth_bytes = 100_000 cold_start_penalty_ns = 0 [[peers]] id = "a" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[peers]] id = "b" kind = "parity_stub" initial_state = "ready" kind_config = { peers = ["a", "b"] } [[links]] from = "a" to = "b" via = "R" [[links]] from = "b" to = "a" via = "R" "#, ); use simulation::scenario::{Mutation, MutationKind}; let mut net = Network::new(&scen); net.apply_mutation( &Mutation { at_ns: 50_000_000, kind: MutationKind::RelayKill { relay: "R".into(), }, }, 50_000_000, ); let outcome = net.send("a", "b", 100, 60_000_000); assert!( matches!(outcome, SendOutcome::Drop { reason: DropReason::RelayDown }), "post-kill send must drop with RelayDown, got {outcome:?}" ); } #[test] fn relay_capacity_change_affects_only_future_sends() { // §4.7 "Mutation invalidation is exact." A RelayCapacityChange // invalidates no deliveries; it affects only sends that begin // after its time. We assert that pre-change in-flight count is // unchanged across the mutation. let scen = one_relay_three_hosts(); use simulation::scenario::{Mutation, MutationKind}; let mut net = Network::new(&scen); // Queue up a delivery, then apply the policy change. let _ = net.send("alpha", "bravo", 1024, 0); let before = net.relay_in_flight_count("R"); let invalidated = net.apply_mutation( &Mutation { at_ns: 100_000, kind: MutationKind::RelayCapacityChange { relay: "R".into(), ingress_capacity_bps: Some(1), egress_capacity_bps_per_link: Some(1), queue_depth_bytes: Some(1), }, }, 100_000, ); let after = net.relay_in_flight_count("R"); assert!(invalidated.is_empty(), "RelayCapacityChange invalidates no deliveries"); assert_eq!( before, after, "in-flight relay state must not change across RelayCapacityChange" ); } #[test] fn relay_kill_invalidates_in_flight_through_that_relay() { // §4.7 "Mutation invalidation is exact." A RelayKill returns // exactly the deliveries in-flight through the relay at the // mutation's virtual time and no others. let scen = one_relay_three_hosts(); use simulation::scenario::{Mutation, MutationKind}; let mut net = Network::new(&scen); let SendOutcome::Arrive { delivery_id, .. } = net.send("alpha", "bravo", 1024, 0) else { panic!("send must arrive"); }; let invalidated = net.apply_mutation( &Mutation { at_ns: 1, kind: MutationKind::RelayKill { relay: "R".into(), }, }, 1, ); assert_eq!(invalidated.len(), 1, "exactly one in-flight delivery"); assert_eq!(invalidated[0].delivery_id, delivery_id); // Relay's in-flight bookkeeping is drained. assert_eq!(net.relay_in_flight_count("R"), 0); } #[test] fn determinism_relayed_send_sequences_are_identical_across_runs() { // §4.7 "Determinism." Same topology, same seed, same query // sequence ⇒ identical composed SendOutcome sequence. let scen = one_relay_three_hosts(); let run = |s: &simulation::scenario::Scenario| -> Vec { let mut net = Network::new(s); let mut arrivals = Vec::new(); for (i, (from, to)) in [ ("alpha", "bravo"), ("bravo", "charlie"), ("charlie", "alpha"), ("alpha", "charlie"), ] .iter() .enumerate() { let outcome = net.send(from, to, 4096, (i as u64) * 1_000); if let SendOutcome::Arrive { at_ns, .. } = outcome { arrivals.push(at_ns); } else { panic!("unexpected drop in determinism test"); } } arrivals }; let a = run(&scen); let b = run(&scen); assert_eq!(a, b, "two runs of the same scenario must produce identical arrivals"); } // Smoke check: this constant is referenced to suppress the // otherwise-unused `include_str!` import. It's a sanity guard that // the parity scenario lives where we expect. #[test] fn parity_scenario_text_includes_default_link_header() { assert!(ALL_NOTIFICATIONS.contains("default_link")); }