diff --git a/src/node/mod.rs b/src/node/mod.rs index ea038c9..9799b02 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -2463,6 +2463,77 @@ impl Node { Ok(()) } + /// Test-support: seed a control machine for `seed.link_id` the way + /// [`Node::add_connection`] does, without the caller having to build a + /// free-standing leg first. + /// + /// The carrier seeding below is a verbatim copy of `add_connection`'s: the + /// two conditional writes (`their_index`, `transport_id`) and `set_leg`, + /// built through the same `entry(..).or_insert_with(..)` so an existing + /// leg-less machine keeps its constructor-side fields. Nothing else is + /// written to the carrier — `our_index`, `source_addr`, post-construction + /// `started_at`, and the stored handshake bytes stay leg-only, exactly as + /// they do for a test that goes through `add_connection` today. + /// + /// The duplication is deliberate: keeping the carrier writes visible here + /// is what lets each later step of the leg dissolution revise them at a + /// single reviewable site. The two bodies must stay in sync until + /// `add_connection` itself is removed; any drift surfaces as a test + /// failure while both paths still exist. + #[cfg(test)] + pub(crate) fn seed_handshake_machine(&mut self, seed: HandshakeSeed) -> Result<(), NodeError> { + let link_id = seed.link_id; + + let mut connection = match seed.expected_identity { + Some(identity) => PeerConnection::outbound(link_id, identity, seed.started_at_ms), + None => PeerConnection::inbound(link_id, seed.started_at_ms), + }; + if let Some(id) = seed.transport_id { + connection.set_transport_id(id); + } + if let Some(addr) = seed.source_addr { + connection.set_source_addr(addr); + } + if let Some(index) = seed.our_index { + connection.set_our_index(index); + } + if let Some(index) = seed.their_index { + connection.set_their_index(index); + } + + if self + .peer_machines + .get(&link_id) + .is_some_and(|machine| machine.leg().is_some()) + { + return Err(NodeError::ConnectionAlreadyExists(link_id)); + } + + if self.max_connections() > 0 && self.connection_count() >= self.max_connections() { + return Err(NodeError::MaxConnectionsExceeded { + max: self.max_connections(), + }); + } + + let machine = self.peer_machines.entry(link_id).or_insert_with(|| { + let now = connection.started_at(); + match connection.expected_identity() { + Some(identity) if connection.is_outbound() => { + PeerMachine::new_outbound(link_id, *identity, now) + } + _ => PeerMachine::new_inbound(link_id, now), + } + }); + if let Some(their) = connection.their_index() { + machine.set_conn_their_index(their); + } + if let Some(tid) = connection.transport_id() { + machine.set_conn_transport_id(tid); + } + machine.set_leg(connection); + Ok(()) + } + /// Get a connection by LinkId. pub fn get_connection(&self, link_id: &LinkId) -> Option<&PeerConnection> { self.leg(link_id) @@ -3183,3 +3254,74 @@ impl fmt::Debug for Node { .finish() } } + +/// Test-support seed spec for [`Node::seed_handshake_machine`]. +/// +/// Mirrors the leg constructors plus the setters tests apply to a connection +/// *before* handing it to `add_connection`. Only fields that exist on the leg +/// at add time belong here; crypto is run afterwards through +/// `get_connection_mut`, which `add_connection` never reads. +#[cfg(test)] +#[derive(Debug, Clone)] +pub(crate) struct HandshakeSeed { + link_id: LinkId, + expected_identity: Option, + started_at_ms: u64, + transport_id: Option, + source_addr: Option, + our_index: Option, + their_index: Option, +} + +#[cfg(test)] +impl HandshakeSeed { + /// Outbound leg: we know who we are dialing. + pub(crate) fn outbound( + link_id: LinkId, + expected_identity: PeerIdentity, + started_at_ms: u64, + ) -> Self { + Self { + link_id, + expected_identity: Some(expected_identity), + started_at_ms, + transport_id: None, + source_addr: None, + our_index: None, + their_index: None, + } + } + + /// Inbound leg: identity is unknown until msg1 decrypts. + pub(crate) fn inbound(link_id: LinkId, started_at_ms: u64) -> Self { + Self { + link_id, + expected_identity: None, + started_at_ms, + transport_id: None, + source_addr: None, + our_index: None, + their_index: None, + } + } + + pub(crate) fn with_transport_id(mut self, transport_id: TransportId) -> Self { + self.transport_id = Some(transport_id); + self + } + + pub(crate) fn with_source_addr(mut self, source_addr: TransportAddr) -> Self { + self.source_addr = Some(source_addr); + self + } + + pub(crate) fn with_our_index(mut self, index: crate::utils::index::SessionIndex) -> Self { + self.our_index = Some(index); + self + } + + pub(crate) fn with_their_index(mut self, index: crate::utils::index::SessionIndex) -> Self { + self.their_index = Some(index); + self + } +} diff --git a/src/node/tests/acl.rs b/src/node/tests/acl.rs index b28c77a..1ce5dbc 100644 --- a/src/node/tests/acl.rs +++ b/src/node/tests/acl.rs @@ -84,14 +84,22 @@ async fn test_outbound_msg2_denied_after_acl_reload() { let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full()); let link_id_a = node_a.allocate_link_id(); - let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000); let our_index_a = node_a.index_allocator.allocate().unwrap(); - let noise_msg1 = conn_a - .start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000) + node_a + .seed_handshake_machine( + HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000) + .with_our_index(our_index_a) + .with_transport_id(transport_id) + .with_source_addr(remote_addr.clone()), + ) + .unwrap(); + let keypair_a = node_a.identity().keypair(); + let epoch_a = node_a.startup_epoch(); + let noise_msg1 = node_a + .get_connection_mut(&link_id_a) + .unwrap() + .start_handshake(keypair_a, epoch_a, 1000) .unwrap(); - conn_a.set_our_index(our_index_a); - conn_a.set_transport_id(transport_id); - conn_a.set_source_addr(remote_addr.clone()); let link_a = Link::connectionless( link_id_a, @@ -104,7 +112,6 @@ async fn test_outbound_msg2_denied_after_acl_reload() { node_a .addr_to_link .insert((transport_id, remote_addr.clone()), link_id_a); - node_a.add_connection(conn_a).unwrap(); node_a .pending_outbound .insert((transport_id, our_index_a.as_u32()), link_id_a); diff --git a/src/node/tests/bootstrap.rs b/src/node/tests/bootstrap.rs index 69d3b20..459181b 100644 --- a/src/node/tests/bootstrap.rs +++ b/src/node/tests/bootstrap.rs @@ -127,9 +127,8 @@ async fn test_adopted_traversal_skips_already_connected_peer() { let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1_000); + let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1_000); let peer_node_addr = *peer_identity.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, peer_identity, 2_000) .unwrap(); diff --git a/src/node/tests/decrypt_failure.rs b/src/node/tests/decrypt_failure.rs index 8509339..b590be3 100644 --- a/src/node/tests/decrypt_failure.rs +++ b/src/node/tests/decrypt_failure.rs @@ -28,10 +28,9 @@ fn test_decrypt_failure_threshold_removes_peer() { // Build a fully-promoted active peer with our_index/transport_id set // so peers_by_index is populated by promote_connection. - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1_000); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1_000); let node_addr = *identity.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, identity, 2_000).unwrap(); // Sanity: peer is registered and indexed. diff --git a/src/node/tests/establish_chartests.rs b/src/node/tests/establish_chartests.rs index 3acc53e..995e887 100644 --- a/src/node/tests/establish_chartests.rs +++ b/src/node/tests/establish_chartests.rs @@ -192,8 +192,6 @@ async fn chartest_msg1_duplicate_pending_resends_stored_msg2() { // A pending inbound connection with a stored msg2, keyed in addr_to_link, // NOT promoted to an active peer. let link_id = node.allocate_link_id(); - let conn = - PeerConnection::inbound_with_transport(link_id, transport_id, peer_addr.clone(), 1000); let stored_msg2 = vec![0xC1, 0xC2, 0xC3, 0xC4, 0xC5]; let link = Link::connectionless( link_id, @@ -205,7 +203,12 @@ async fn chartest_msg1_duplicate_pending_resends_stored_msg2() { node.links.insert(link_id, link); node.addr_to_link .insert((transport_id, peer_addr.clone()), link_id); - node.add_connection(conn).unwrap(); + node.seed_handshake_machine( + HandshakeSeed::inbound(link_id, 1000) + .with_transport_id(transport_id) + .with_source_addr(peer_addr.clone()), + ) + .unwrap(); // The stored msg2 lives on the control machine's carrier (the resend source // for a duplicate msg1 while pending), mirroring the inbound establish path. node.peer_machines @@ -327,15 +330,21 @@ async fn chartest_msg1_inbound_promote_defers_pending_outbound_to_same_identity( // different source address. let out_link = node.allocate_link_id(); let out_addr = TransportAddr::from_string("10.0.0.9:2121"); - let mut out_conn = PeerConnection::outbound(out_link, sender_pid, 1000); - let our_keypair = node.identity().keypair(); - let _ = out_conn - .start_handshake(our_keypair, node.startup_epoch(), 1000) - .unwrap(); let out_index = node.index_allocator.allocate().unwrap(); - out_conn.set_our_index(out_index); - out_conn.set_transport_id(transport_id); - out_conn.set_source_addr(out_addr.clone()); + node.seed_handshake_machine( + HandshakeSeed::outbound(out_link, sender_pid, 1000) + .with_our_index(out_index) + .with_transport_id(transport_id) + .with_source_addr(out_addr.clone()), + ) + .unwrap(); + let our_keypair = node.identity().keypair(); + let startup_epoch = node.startup_epoch(); + let _ = node + .get_connection_mut(&out_link) + .unwrap() + .start_handshake(our_keypair, startup_epoch, 1000) + .unwrap(); let out_l = Link::connectionless( out_link, transport_id, @@ -346,7 +355,6 @@ async fn chartest_msg1_inbound_promote_defers_pending_outbound_to_same_identity( node.links.insert(out_link, out_l); node.addr_to_link .insert((transport_id, out_addr.clone()), out_link); - node.add_connection(out_conn).unwrap(); node.pending_outbound .insert((transport_id, out_index.as_u32()), out_link); assert_eq!(node.peer_count(), 0); @@ -402,16 +410,21 @@ async fn chartest_msg1_at_cap_with_pending_outbound_bypasses_early_gate() { // `has_pending_outbound_to_peer`, which turns off the early silent-drop. let out_link = node.allocate_link_id(); let out_addr = TransportAddr::from_string("10.0.0.9:2121"); - let mut out_conn = PeerConnection::outbound(out_link, sender_pid, 1000); - let our_keypair = node.identity().keypair(); - let _ = out_conn - .start_handshake(our_keypair, node.startup_epoch(), 1000) - .unwrap(); let out_index = node.index_allocator.allocate().unwrap(); - out_conn.set_our_index(out_index); - out_conn.set_transport_id(transport_id); - out_conn.set_source_addr(out_addr.clone()); - node.add_connection(out_conn).unwrap(); + node.seed_handshake_machine( + HandshakeSeed::outbound(out_link, sender_pid, 1000) + .with_our_index(out_index) + .with_transport_id(transport_id) + .with_source_addr(out_addr.clone()), + ) + .unwrap(); + let our_keypair = node.identity().keypair(); + let startup_epoch = node.startup_epoch(); + let _ = node + .get_connection_mut(&out_link) + .unwrap() + .start_handshake(our_keypair, startup_epoch, 1000) + .unwrap(); node.pending_outbound .insert((transport_id, out_index.as_u32()), out_link); @@ -499,14 +512,22 @@ async fn chartest_cross_connection_tiebreak_winner_and_loser() { // A initiates to B. let link_a_out = node_a.allocate_link_id(); - let mut conn_a = PeerConnection::outbound(link_a_out, peer_b_identity, 1000); let out_index_a = node_a.index_allocator.allocate().unwrap(); - let noise_msg1_a = conn_a - .start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000) + node_a + .seed_handshake_machine( + HandshakeSeed::outbound(link_a_out, peer_b_identity, 1000) + .with_our_index(out_index_a) + .with_transport_id(transport_id_a) + .with_source_addr(remote_addr_b.clone()), + ) + .unwrap(); + let keypair_a = node_a.identity().keypair(); + let epoch_a = node_a.startup_epoch(); + let noise_msg1_a = node_a + .get_connection_mut(&link_a_out) + .unwrap() + .start_handshake(keypair_a, epoch_a, 1000) .unwrap(); - conn_a.set_our_index(out_index_a); - conn_a.set_transport_id(transport_id_a); - conn_a.set_source_addr(remote_addr_b.clone()); let wire_msg1_a = build_msg1(out_index_a, &noise_msg1_a); node_a.links.insert( link_a_out, @@ -521,21 +542,28 @@ async fn chartest_cross_connection_tiebreak_winner_and_loser() { node_a .addr_to_link .insert((transport_id_a, remote_addr_b.clone()), link_a_out); - node_a.add_connection(conn_a).unwrap(); node_a .pending_outbound .insert((transport_id_a, out_index_a.as_u32()), link_a_out); // B initiates to A. let link_b_out = node_b.allocate_link_id(); - let mut conn_b = PeerConnection::outbound(link_b_out, peer_a_identity, 1000); let out_index_b = node_b.index_allocator.allocate().unwrap(); - let noise_msg1_b = conn_b - .start_handshake(node_b.identity().keypair(), node_b.startup_epoch(), 1000) + node_b + .seed_handshake_machine( + HandshakeSeed::outbound(link_b_out, peer_a_identity, 1000) + .with_our_index(out_index_b) + .with_transport_id(transport_id_b) + .with_source_addr(remote_addr_a.clone()), + ) + .unwrap(); + let keypair_b = node_b.identity().keypair(); + let epoch_b = node_b.startup_epoch(); + let noise_msg1_b = node_b + .get_connection_mut(&link_b_out) + .unwrap() + .start_handshake(keypair_b, epoch_b, 1000) .unwrap(); - conn_b.set_our_index(out_index_b); - conn_b.set_transport_id(transport_id_b); - conn_b.set_source_addr(remote_addr_a.clone()); let wire_msg1_b = build_msg1(out_index_b, &noise_msg1_b); node_b.links.insert( link_b_out, @@ -550,7 +578,6 @@ async fn chartest_cross_connection_tiebreak_winner_and_loser() { node_b .addr_to_link .insert((transport_id_b, remote_addr_a.clone()), link_b_out); - node_b.add_connection(conn_b).unwrap(); node_b .pending_outbound .insert((transport_id_b, out_index_b.as_u32()), link_b_out); diff --git a/src/node/tests/handshake.rs b/src/node/tests/handshake.rs index e5438bc..da5e087 100644 --- a/src/node/tests/handshake.rs +++ b/src/node/tests/handshake.rs @@ -53,19 +53,27 @@ async fn test_two_node_handshake_udp() { let peer_b_node_addr = *peer_b_identity.node_addr(); let link_id_a = node_a.allocate_link_id(); - let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000); // Allocate session index for A's outbound let our_index_a = node_a.index_allocator.allocate().unwrap(); + node_a + .seed_handshake_machine( + HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000) + .with_our_index(our_index_a) + .with_transport_id(transport_id_a) + .with_source_addr(remote_addr_b.clone()), + ) + .unwrap(); + // Start handshake (generates Noise IK msg1) let our_keypair_a = node_a.identity().keypair(); - let noise_msg1 = conn_a - .start_handshake(our_keypair_a, node_a.startup_epoch(), 1000) + let startup_epoch_a = node_a.startup_epoch(); + let noise_msg1 = node_a + .get_connection_mut(&link_id_a) + .unwrap() + .start_handshake(our_keypair_a, startup_epoch_a, 1000) .unwrap(); - conn_a.set_our_index(our_index_a); - conn_a.set_transport_id(transport_id_a); - conn_a.set_source_addr(remote_addr_b.clone()); // Build wire msg1 and track in node state let wire_msg1 = build_msg1(our_index_a, &noise_msg1); @@ -78,7 +86,6 @@ async fn test_two_node_handshake_udp() { Duration::from_millis(100), ); node_a.links.insert(link_id_a, link_a); - node_a.add_connection(conn_a).unwrap(); node_a .pending_outbound .insert((transport_id_a, our_index_a.as_u32()), link_id_a); @@ -294,16 +301,23 @@ async fn test_run_rx_loop_handshake() { let peer_b_node_addr = *peer_b_identity.node_addr(); let link_id_a = node_a.allocate_link_id(); - let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000); let our_index_a = node_a.index_allocator.allocate().unwrap(); - let our_keypair_a = node_a.identity().keypair(); - let noise_msg1 = conn_a - .start_handshake(our_keypair_a, node_a.startup_epoch(), 1000) + node_a + .seed_handshake_machine( + HandshakeSeed::outbound(link_id_a, peer_b_identity, 1000) + .with_our_index(our_index_a) + .with_transport_id(transport_id_a) + .with_source_addr(remote_addr_b.clone()), + ) + .unwrap(); + let our_keypair_a = node_a.identity().keypair(); + let startup_epoch_a = node_a.startup_epoch(); + let noise_msg1 = node_a + .get_connection_mut(&link_id_a) + .unwrap() + .start_handshake(our_keypair_a, startup_epoch_a, 1000) .unwrap(); - conn_a.set_our_index(our_index_a); - conn_a.set_transport_id(transport_id_a); - conn_a.set_source_addr(remote_addr_b.clone()); let wire_msg1 = build_msg1(our_index_a, &noise_msg1); @@ -315,7 +329,6 @@ async fn test_run_rx_loop_handshake() { Duration::from_millis(100), ); node_a.links.insert(link_id_a, link_a); - node_a.add_connection(conn_a).unwrap(); node_a .pending_outbound .insert((transport_id_a, our_index_a.as_u32()), link_id_a); @@ -483,15 +496,22 @@ async fn test_cross_connection_both_initiate() { // Node A initiates to Node B let link_id_a_out = node_a.allocate_link_id(); - let mut conn_a = PeerConnection::outbound(link_id_a_out, peer_b_identity, 1000); let our_index_a = node_a.index_allocator.allocate().unwrap(); - let our_keypair_a = node_a.identity().keypair(); - let noise_msg1_a = conn_a - .start_handshake(our_keypair_a, node_a.startup_epoch(), 1000) + node_a + .seed_handshake_machine( + HandshakeSeed::outbound(link_id_a_out, peer_b_identity, 1000) + .with_our_index(our_index_a) + .with_transport_id(transport_id_a) + .with_source_addr(remote_addr_b.clone()), + ) + .unwrap(); + let our_keypair_a = node_a.identity().keypair(); + let startup_epoch_a = node_a.startup_epoch(); + let noise_msg1_a = node_a + .get_connection_mut(&link_id_a_out) + .unwrap() + .start_handshake(our_keypair_a, startup_epoch_a, 1000) .unwrap(); - conn_a.set_our_index(our_index_a); - conn_a.set_transport_id(transport_id_a); - conn_a.set_source_addr(remote_addr_b.clone()); let wire_msg1_a = build_msg1(our_index_a, &noise_msg1_a); @@ -506,22 +526,28 @@ async fn test_cross_connection_both_initiate() { node_a .addr_to_link .insert((transport_id_a, remote_addr_b.clone()), link_id_a_out); - node_a.add_connection(conn_a).unwrap(); node_a .pending_outbound .insert((transport_id_a, our_index_a.as_u32()), link_id_a_out); // Node B initiates to Node A let link_id_b_out = node_b.allocate_link_id(); - let mut conn_b = PeerConnection::outbound(link_id_b_out, peer_a_identity, 1000); let our_index_b = node_b.index_allocator.allocate().unwrap(); - let our_keypair_b = node_b.identity().keypair(); - let noise_msg1_b = conn_b - .start_handshake(our_keypair_b, node_b.startup_epoch(), 1000) + node_b + .seed_handshake_machine( + HandshakeSeed::outbound(link_id_b_out, peer_a_identity, 1000) + .with_our_index(our_index_b) + .with_transport_id(transport_id_b) + .with_source_addr(remote_addr_a.clone()), + ) + .unwrap(); + let our_keypair_b = node_b.identity().keypair(); + let startup_epoch_b = node_b.startup_epoch(); + let noise_msg1_b = node_b + .get_connection_mut(&link_id_b_out) + .unwrap() + .start_handshake(our_keypair_b, startup_epoch_b, 1000) .unwrap(); - conn_b.set_our_index(our_index_b); - conn_b.set_transport_id(transport_id_b); - conn_b.set_source_addr(remote_addr_a.clone()); let wire_msg1_b = build_msg1(our_index_b, &noise_msg1_b); @@ -536,7 +562,6 @@ async fn test_cross_connection_both_initiate() { node_b .addr_to_link .insert((transport_id_b, remote_addr_a.clone()), link_id_b_out); - node_b.add_connection(conn_b).unwrap(); node_b .pending_outbound .insert((transport_id_b, our_index_b.as_u32()), link_id_b_out); @@ -662,17 +687,23 @@ async fn test_stale_connection_cleanup() { // Create outbound connection with a timestamp far in the past let past_time_ms = 1000; // A very early timestamp let link_id = node.allocate_link_id(); - let mut conn = PeerConnection::outbound(link_id, peer_identity, past_time_ms); // Allocate session index and set transport info let our_index = node.index_allocator.allocate().unwrap(); + node.seed_handshake_machine( + HandshakeSeed::outbound(link_id, peer_identity, past_time_ms) + .with_our_index(our_index) + .with_transport_id(transport_id) + .with_source_addr(remote_addr.clone()), + ) + .unwrap(); let our_keypair = node.identity().keypair(); - let _noise_msg1 = conn - .start_handshake(our_keypair, node.startup_epoch(), past_time_ms) + let startup_epoch = node.startup_epoch(); + let _noise_msg1 = node + .get_connection_mut(&link_id) + .unwrap() + .start_handshake(our_keypair, startup_epoch, past_time_ms) .unwrap(); - conn.set_our_index(our_index); - conn.set_transport_id(transport_id); - conn.set_source_addr(remote_addr.clone()); // Set up all the state that initiate_peer_connection would create let link = Link::connectionless( @@ -685,7 +716,6 @@ async fn test_stale_connection_cleanup() { node.links.insert(link_id, link); node.addr_to_link .insert((transport_id, remote_addr.clone()), link_id); - node.add_connection(conn).unwrap(); node.pending_outbound .insert((transport_id, our_index.as_u32()), link_id); @@ -741,16 +771,22 @@ async fn test_failed_connection_cleanup() { .map(|d| d.as_millis() as u64) .unwrap_or(0); let link_id = node.allocate_link_id(); - let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms); let our_index = node.index_allocator.allocate().unwrap(); + node.seed_handshake_machine( + HandshakeSeed::outbound(link_id, peer_identity, now_ms) + .with_our_index(our_index) + .with_transport_id(transport_id) + .with_source_addr(remote_addr.clone()), + ) + .unwrap(); let our_keypair = node.identity().keypair(); - let _noise_msg1 = conn - .start_handshake(our_keypair, node.startup_epoch(), now_ms) + let startup_epoch = node.startup_epoch(); + let _noise_msg1 = node + .get_connection_mut(&link_id) + .unwrap() + .start_handshake(our_keypair, startup_epoch, now_ms) .unwrap(); - conn.set_our_index(our_index); - conn.set_transport_id(transport_id); - conn.set_source_addr(remote_addr.clone()); let link = Link::connectionless( link_id, @@ -762,7 +798,6 @@ async fn test_failed_connection_cleanup() { node.links.insert(link_id, link); node.addr_to_link .insert((transport_id, remote_addr.clone()), link_id); - node.add_connection(conn).unwrap(); node.pending_outbound .insert((transport_id, our_index.as_u32()), link_id); diff --git a/src/node/tests/mod.rs b/src/node/tests/mod.rs index 8754f50..57d2500 100644 --- a/src/node/tests/mod.rs +++ b/src/node/tests/mod.rs @@ -84,27 +84,49 @@ pub(super) fn make_peer_identity() -> PeerIdentity { PeerIdentity::from_pubkey(identity.pubkey()) } -/// Create a PeerConnection with a completed Noise IK handshake. +/// Seed a control machine whose leg carries a completed Noise IK handshake. /// -/// Returns (connection, peer_identity) where the connection is outbound, -/// in Complete state, with session, indices, and transport info set. -pub(super) fn make_completed_connection( +/// Returns the peer identity. The leg is outbound, in Complete state, with +/// session, indices, and transport info set, and is installed on the node +/// through [`Node::seed_handshake_machine`] — the test-surface twin of +/// `Node::add_connection`. +/// +/// The Noise exchange runs on the already-seeded leg, where it used to run +/// before the leg was handed over. That reordering is neutral, but not +/// because the handshake leaves the seeded fields alone — +/// `receive_handshake_init` does write `expected_identity`. It is neutral +/// because the only read of `expected_identity` is guarded by `is_outbound`: +/// an inbound leg takes the `new_inbound` arm whether or not the identity has +/// been learned, and an outbound leg never runs that method. The remaining +/// reads (`link_id`, `started_at`, `is_outbound`, `their_index`, +/// `transport_id`) are genuinely untouched by the handshake. +pub(super) fn seed_completed_connection( node: &mut Node, link_id: LinkId, transport_id: TransportId, current_time_ms: u64, -) -> (PeerConnection, PeerIdentity) { +) -> PeerIdentity { let peer_identity_full = Identity::generate(); // Must use from_pubkey_full to preserve parity for ECDH let peer_identity = PeerIdentity::from_pubkey_full(peer_identity_full.pubkey_full()); - // Create outbound connection - let mut conn = PeerConnection::outbound(link_id, peer_identity, current_time_ms); + let our_index = node.index_allocator.allocate().unwrap(); + node.seed_handshake_machine( + HandshakeSeed::outbound(link_id, peer_identity, current_time_ms) + .with_our_index(our_index) + .with_their_index(SessionIndex::new(42)) + .with_transport_id(transport_id) + .with_source_addr(TransportAddr::from_string("127.0.0.1:5000")), + ) + .unwrap(); // Run initiator side of handshake let our_keypair = node.identity().keypair(); - let msg1 = conn - .start_handshake(our_keypair, node.startup_epoch(), current_time_ms) + let startup_epoch = node.startup_epoch(); + let msg1 = node + .get_connection_mut(&link_id) + .unwrap() + .start_handshake(our_keypair, startup_epoch, current_time_ms) .unwrap(); // Run responder side to generate msg2 @@ -117,14 +139,10 @@ pub(super) fn make_completed_connection( .unwrap(); // Complete initiator handshake - conn.complete_handshake(&msg2, current_time_ms).unwrap(); + node.get_connection_mut(&link_id) + .unwrap() + .complete_handshake(&msg2, current_time_ms) + .unwrap(); - // Set indices and transport info - let our_index = node.index_allocator.allocate().unwrap(); - conn.set_our_index(our_index); - conn.set_their_index(SessionIndex::new(42)); - conn.set_transport_id(transport_id); - conn.set_source_addr(TransportAddr::from_string("127.0.0.1:5000")); - - (conn, peer_identity) + peer_identity } diff --git a/src/node/tests/routing.rs b/src/node/tests/routing.rs index 5123581..cd6b1c2 100644 --- a/src/node/tests/routing.rs +++ b/src/node/tests/routing.rs @@ -29,9 +29,8 @@ fn test_routing_direct_peer() { let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); let peer_addr = *identity.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, identity, 2000).unwrap(); let result = node.find_next_hop(&peer_addr); @@ -58,15 +57,13 @@ fn test_routing_bloom_filter_hit() { // Create two peers let link_id1 = LinkId::new(1); - let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000); + let id1 = seed_completed_connection(&mut node, link_id1, transport_id, 1000); let peer1_addr = *id1.node_addr(); - node.add_connection(conn1).unwrap(); node.promote_connection(link_id1, id1, 2000).unwrap(); let link_id2 = LinkId::new(2); - let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000); + let id2 = seed_completed_connection(&mut node, link_id2, transport_id, 1000); let peer2_addr = *id2.node_addr(); - node.add_connection(conn2).unwrap(); node.promote_connection(link_id2, id2, 2000).unwrap(); // Set up tree: we are root, both peers are our children @@ -115,10 +112,9 @@ fn test_routing_bloom_filter_multiple_hits_tiebreak() { let mut peer_addrs = Vec::new(); for i in 1..=3 { let link_id = LinkId::new(i); - let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let id = seed_completed_connection(&mut node, link_id, transport_id, 1000); let addr = *id.node_addr(); peer_addrs.push(addr); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, id, 2000).unwrap(); } @@ -166,9 +162,8 @@ fn test_routing_tree_fallback() { // Create a peer let link_id = LinkId::new(1); - let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let id = seed_completed_connection(&mut node, link_id, transport_id, 1000); let peer_addr = *id.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, id, 2000).unwrap(); // Set up tree state through the public API. @@ -220,9 +215,8 @@ fn test_routing_bloom_hit_not_closer_falls_through_to_tree() { // tree_peer: child of self, on the path to dest (greedy tree pick). let tree_link = LinkId::new(1); - let (tree_conn, tree_id) = make_completed_connection(&mut node, tree_link, transport_id, 1000); + let tree_id = seed_completed_connection(&mut node, tree_link, transport_id, 1000); let tree_peer_addr = *tree_id.node_addr(); - node.add_connection(tree_conn).unwrap(); node.promote_connection(tree_link, tree_id, 2000).unwrap(); // bloom_peer: also a child of self, but with a stale/false-positive @@ -230,10 +224,8 @@ fn test_routing_bloom_hit_not_closer_falls_through_to_tree() { // ours, so the self-distance check in select_best_candidate excludes // it — leaving zero viable bloom candidates. let bloom_link = LinkId::new(2); - let (bloom_conn, bloom_id) = - make_completed_connection(&mut node, bloom_link, transport_id, 1000); + let bloom_id = seed_completed_connection(&mut node, bloom_link, transport_id, 1000); let bloom_peer_addr = *bloom_id.node_addr(); - node.add_connection(bloom_conn).unwrap(); node.promote_connection(bloom_link, bloom_id, 2000).unwrap(); // Tree topology (we are root): @@ -300,8 +292,7 @@ fn test_routing_tree_no_coords_in_cache() { // Create a peer let link_id = LinkId::new(1); - let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000); - node.add_connection(conn).unwrap(); + let id = seed_completed_connection(&mut node, link_id, transport_id, 1000); node.promote_connection(link_id, id, 2000).unwrap(); // Destination not in bloom filters and not in coord cache @@ -319,9 +310,8 @@ fn test_routing_refreshes_coord_cache_ttl() { // Create a peer let link_id = LinkId::new(1); - let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let id = seed_completed_connection(&mut node, link_id, transport_id, 1000); let peer_addr = *id.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, id, 2000).unwrap(); // Set up tree coordinates @@ -364,15 +354,13 @@ fn test_routing_bloom_hit_without_coords_returns_none() { // Create two peers let link_id1 = LinkId::new(1); - let (conn1, id1) = make_completed_connection(&mut node, link_id1, transport_id, 1000); + let id1 = seed_completed_connection(&mut node, link_id1, transport_id, 1000); let peer1_addr = *id1.node_addr(); - node.add_connection(conn1).unwrap(); node.promote_connection(link_id1, id1, 2000).unwrap(); let link_id2 = LinkId::new(2); - let (conn2, id2) = make_completed_connection(&mut node, link_id2, transport_id, 1000); + let id2 = seed_completed_connection(&mut node, link_id2, transport_id, 1000); let peer2_addr = *id2.node_addr(); - node.add_connection(conn2).unwrap(); node.promote_connection(link_id2, id2, 2000).unwrap(); let dest = make_node_addr(99); @@ -404,9 +392,8 @@ fn test_routing_discovery_coord_cache() { // Create a peer let link_id = LinkId::new(1); - let (conn, id) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let id = seed_completed_connection(&mut node, link_id, transport_id, 1000); let peer_addr = *id.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, id, 2000).unwrap(); // Set up tree: we are root, peer is our child diff --git a/src/node/tests/session.rs b/src/node/tests/session.rs index 8d50d1f..4ad7c65 100644 --- a/src/node/tests/session.rs +++ b/src/node/tests/session.rs @@ -1008,9 +1008,7 @@ fn test_identity_cache_populated_on_promote() { let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); - - node.add_connection(conn).unwrap(); + let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); // Promote let result = node diff --git a/src/node/tests/spanning_tree.rs b/src/node/tests/spanning_tree.rs index 12b323e..cb6e8eb 100644 --- a/src/node/tests/spanning_tree.rs +++ b/src/node/tests/spanning_tree.rs @@ -116,16 +116,25 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz let transport_id = initiator.transport_id; let link_id = initiator.node.allocate_link_id(); - let mut conn = PeerConnection::outbound(link_id, peer_identity, 1000); let our_index = initiator.node.index_allocator.allocate().unwrap(); - let our_keypair = initiator.node.identity().keypair(); - let noise_msg1 = conn - .start_handshake(our_keypair, initiator.node.startup_epoch(), 1000) + initiator + .node + .seed_handshake_machine( + HandshakeSeed::outbound(link_id, peer_identity, 1000) + .with_our_index(our_index) + .with_transport_id(transport_id) + .with_source_addr(responder_addr.clone()), + ) + .unwrap(); + let our_keypair = initiator.node.identity().keypair(); + let startup_epoch = initiator.node.startup_epoch(); + let noise_msg1 = initiator + .node + .get_connection_mut(&link_id) + .unwrap() + .start_handshake(our_keypair, startup_epoch, 1000) .unwrap(); - conn.set_our_index(our_index); - conn.set_transport_id(transport_id); - conn.set_source_addr(responder_addr.clone()); let wire_msg1 = build_msg1(our_index, &noise_msg1); @@ -141,7 +150,6 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz .node .addr_to_link .insert((transport_id, responder_addr.clone()), link_id); - initiator.node.add_connection(conn).unwrap(); initiator .node .pending_outbound diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index 13731c3..3185ba8 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -337,9 +337,9 @@ fn test_node_connection_management() { let identity = make_peer_identity(); let link_id = LinkId::new(1); - let conn = PeerConnection::outbound(link_id, identity, 1000); + node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000)) + .unwrap(); - node.add_connection(conn).unwrap(); assert_eq!(node.connection_count(), 1); assert!(node.get_connection(&link_id).is_some()); @@ -354,11 +354,10 @@ fn test_node_connection_duplicate() { let identity = make_peer_identity(); let link_id = LinkId::new(1); - let conn1 = PeerConnection::outbound(link_id, identity, 1000); - let conn2 = PeerConnection::outbound(link_id, identity, 2000); + node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000)) + .unwrap(); - node.add_connection(conn1).unwrap(); - let result = node.add_connection(conn2); + let result = node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 2000)); assert!(matches!(result, Err(NodeError::ConnectionAlreadyExists(_)))); } @@ -370,9 +369,8 @@ fn test_peer_maps_coherent_after_add_connection() { let identity = make_peer_identity(); let link_id = LinkId::new(1); - let conn = PeerConnection::outbound(link_id, identity, 1000); - - node.add_connection(conn).unwrap(); + node.seed_handshake_machine(HandshakeSeed::outbound(link_id, identity, 1000)) + .unwrap(); assert!( node.peer_machines.contains_key(&link_id), @@ -388,9 +386,8 @@ fn test_peer_maps_coherent_through_establish() { let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); - node.add_connection(conn).unwrap(); node.debug_assert_peer_maps_coherent(); let result = node.promote_connection(link_id, identity, 2000).unwrap(); @@ -427,10 +424,9 @@ fn test_node_promote_connection() { let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); let node_addr = *identity.node_addr(); - node.add_connection(conn).unwrap(); assert_eq!(node.connection_count(), 1); assert_eq!(node.peer_count(), 0); @@ -467,10 +463,9 @@ fn test_node_cross_connection_resolution() { // First connection and promotion (becomes active peer) let link_id1 = LinkId::new(1); - let (conn1, identity) = make_completed_connection(&mut node, link_id1, transport_id, 1000); + let identity = seed_completed_connection(&mut node, link_id1, transport_id, 1000); let node_addr = *identity.node_addr(); - node.add_connection(conn1).unwrap(); node.promote_connection(link_id1, identity, 1500).unwrap(); assert_eq!(node.peer_count(), 1); @@ -500,8 +495,7 @@ fn test_node_peer_limit() { // Add two peers via promotion for i in 0..2 { let link_id = LinkId::new(i as u64 + 1); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); - node.add_connection(conn).unwrap(); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); node.promote_connection(link_id, identity, 2000).unwrap(); } @@ -509,8 +503,7 @@ fn test_node_peer_limit() { // Third should fail let link_id = LinkId::new(3); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 3000); - node.add_connection(conn).unwrap(); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 3000); let result = node.promote_connection(link_id, identity, 4000); assert!(matches!(result, Err(NodeError::MaxPeersExceeded { .. }))); @@ -558,22 +551,19 @@ fn test_node_sendable_peers() { // Add a healthy peer let link_id1 = LinkId::new(1); - let (conn1, identity1) = make_completed_connection(&mut node, link_id1, transport_id, 1000); + let identity1 = seed_completed_connection(&mut node, link_id1, transport_id, 1000); let node_addr1 = *identity1.node_addr(); - node.add_connection(conn1).unwrap(); node.promote_connection(link_id1, identity1, 2000).unwrap(); // Add another peer and mark it stale (still sendable) let link_id2 = LinkId::new(2); - let (conn2, identity2) = make_completed_connection(&mut node, link_id2, transport_id, 1000); - node.add_connection(conn2).unwrap(); + let identity2 = seed_completed_connection(&mut node, link_id2, transport_id, 1000); node.promote_connection(link_id2, identity2, 2000).unwrap(); // Add a third peer and mark it disconnected (not sendable) let link_id3 = LinkId::new(3); - let (conn3, identity3) = make_completed_connection(&mut node, link_id3, transport_id, 1000); + let identity3 = seed_completed_connection(&mut node, link_id3, transport_id, 1000); let node_addr3 = *identity3.node_addr(); - node.add_connection(conn3).unwrap(); node.promote_connection(link_id3, identity3, 2000).unwrap(); node.get_peer_mut(&node_addr3).unwrap().mark_disconnected(); @@ -708,20 +698,24 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() { // This simulates A having sent msg1 to B before B was running. let pending_link_id = LinkId::new(1); let pending_time_ms = 1000; - let mut pending_conn = - PeerConnection::outbound(pending_link_id, peer_b_identity, pending_time_ms); + let pending_index = node.index_allocator.allocate().unwrap(); + let pending_addr = TransportAddr::from_string("10.0.0.2:2121"); + node.seed_handshake_machine( + HandshakeSeed::outbound(pending_link_id, peer_b_identity, pending_time_ms) + .with_our_index(pending_index) + .with_transport_id(transport_id) + .with_source_addr(pending_addr.clone()), + ) + .unwrap(); let our_keypair = node.identity().keypair(); - let _msg1 = pending_conn - .start_handshake(our_keypair, node.startup_epoch(), pending_time_ms) + let startup_epoch = node.startup_epoch(); + let _msg1 = node + .get_connection_mut(&pending_link_id) + .unwrap() + .start_handshake(our_keypair, startup_epoch, pending_time_ms) .unwrap(); - let pending_index = node.index_allocator.allocate().unwrap(); - pending_conn.set_our_index(pending_index); - pending_conn.set_transport_id(transport_id); - let pending_addr = TransportAddr::from_string("10.0.0.2:2121"); - pending_conn.set_source_addr(pending_addr.clone()); - let pending_link = Link::connectionless( pending_link_id, transport_id, @@ -732,7 +726,6 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() { node.links.insert(pending_link_id, pending_link); node.addr_to_link .insert((transport_id, pending_addr.clone()), pending_link_id); - node.add_connection(pending_conn).unwrap(); node.pending_outbound .insert((transport_id, pending_index.as_u32()), pending_link_id); @@ -747,12 +740,22 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() { let completing_link_id = LinkId::new(2); let completing_time_ms = 2000; - let mut completing_conn = - PeerConnection::outbound(completing_link_id, peer_b_identity, completing_time_ms); + let completing_index = node.index_allocator.allocate().unwrap(); + node.seed_handshake_machine( + HandshakeSeed::outbound(completing_link_id, peer_b_identity, completing_time_ms) + .with_our_index(completing_index) + .with_their_index(SessionIndex::new(99)) + .with_transport_id(transport_id) + .with_source_addr(TransportAddr::from_string("10.0.0.2:4001")), + ) + .unwrap(); let our_keypair = node.identity().keypair(); - let msg1 = completing_conn - .start_handshake(our_keypair, node.startup_epoch(), completing_time_ms) + let startup_epoch = node.startup_epoch(); + let msg1 = node + .get_connection_mut(&completing_link_id) + .unwrap() + .start_handshake(our_keypair, startup_epoch, completing_time_ms) .unwrap(); // B responds @@ -764,18 +767,11 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() { .receive_handshake_init(peer_keypair, resp_epoch, &msg1, completing_time_ms) .unwrap(); - completing_conn + node.get_connection_mut(&completing_link_id) + .unwrap() .complete_handshake(&msg2, completing_time_ms) .unwrap(); - let completing_index = node.index_allocator.allocate().unwrap(); - completing_conn.set_our_index(completing_index); - completing_conn.set_their_index(SessionIndex::new(99)); - completing_conn.set_transport_id(transport_id); - completing_conn.set_source_addr(TransportAddr::from_string("10.0.0.2:4001")); - - node.add_connection(completing_conn).unwrap(); - // Now 2 connections, 1 link (pending has link, completing doesn't yet need one for this test) assert_eq!(node.connection_count(), 2); assert_eq!(node.index_allocator.count(), 2); @@ -1039,9 +1035,8 @@ fn test_schedule_retry_skips_connected_peer() { // Promote a peer so it's in the peers map let link_id = LinkId::new(1); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); let node_addr = *identity.node_addr(); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, identity, 2000).unwrap(); assert_eq!(node.peer_count(), 1); @@ -1058,10 +1053,9 @@ async fn test_try_peer_addresses_skips_connected_peer() { let mut node = make_node(); let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); let peer_config = crate::config::PeerConfig::new(peer_identity.npub(), "udp", "127.0.0.1:9"); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, peer_identity, 2000) .unwrap(); let link_count = node.link_count(); @@ -1088,8 +1082,8 @@ async fn test_try_peer_addresses_skips_connecting_peer() { let mut node = make_node(); let peer_identity = make_peer_identity(); let peer_config = crate::config::PeerConfig::new(peer_identity.npub(), "udp", "127.0.0.1:9"); - let pending = PeerConnection::outbound(LinkId::new(1), peer_identity, 1000); - node.add_connection(pending).unwrap(); + node.seed_handshake_machine(HandshakeSeed::outbound(LinkId::new(1), peer_identity, 1000)) + .unwrap(); node.try_peer_addresses(&peer_config, peer_identity, true) .await @@ -1270,8 +1264,7 @@ async fn test_nostr_traversal_failure_skips_connected_peer() { let mut node = make_node(); let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); - node.add_connection(conn).unwrap(); + let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); node.promote_connection(link_id, peer_identity, 2000) .unwrap(); @@ -1304,8 +1297,7 @@ async fn test_nostr_traversal_established_skips_connected_peer() { let mut node = make_node(); let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, peer_identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); - node.add_connection(conn).unwrap(); + let peer_identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); node.promote_connection(link_id, peer_identity, 2000) .unwrap(); let link_count = node.link_count(); @@ -1525,7 +1517,7 @@ fn test_promote_clears_retry_pending() { let transport_id = TransportId::new(1); let link_id = LinkId::new(1); - let (conn, identity) = make_completed_connection(&mut node, link_id, transport_id, 1000); + let identity = seed_completed_connection(&mut node, link_id, transport_id, 1000); let node_addr = *identity.node_addr(); // Simulate a retry entry existing for this peer @@ -1535,7 +1527,6 @@ fn test_promote_clears_retry_pending() { ); assert_eq!(node.peering.reconciler.retry_pending.len(), 1); - node.add_connection(conn).unwrap(); node.promote_connection(link_id, identity, 2000).unwrap(); assert!( diff --git a/src/peer/machine.rs b/src/peer/machine.rs index 1834917..91941ea 100644 --- a/src/peer/machine.rs +++ b/src/peer/machine.rs @@ -2971,3 +2971,22 @@ mod tests { assert_eq!(alloc.count(), 0); } } + +/// T-SANSIO: the action vocabulary must stay plain, comparable data. +/// +/// `PeerAction` is the sans-IO boundary between the pure reducer and its +/// driver. Requiring `Clone + Eq` is a compile-time statement that no variant +/// may carry a runtime handle (a socket, a `JoinHandle`, a channel sender), +/// since none of those are `Clone + Eq`. If a future variant smuggles one in, +/// this bound stops compiling. +#[cfg(test)] +mod action_contract { + use super::PeerAction; + + fn assert_clone_eq() {} + + #[test] + fn peer_action_is_plain_comparable_data() { + assert_clone_eq::(); + } +}