node: seed control machines directly in tests, ahead of removing the leg

Tests built a free-standing PeerConnection, mutated it, and handed it to
Node::add_connection by value. None of those sites survives the removal of
PeerConnection, so converting them afterwards would mean one enormous commit
that cannot be reviewed honestly. Convert them now, while the struct still
exists and the conversion can be validated against a green tree.

Adds a cfg(test) Node::seed_handshake_machine plus a HandshakeSeed builder,
and rewrites make_completed_connection (now seed_completed_connection) and
the twenty inline builders onto it. add_connection keeps its body and loses
its test callers.

The builder's carrier seeding is a verbatim copy of add_connection's: the two
conditional writes for their_index and transport_id, then set_leg, through the
same entry().or_insert_with() so an existing leg-less machine keeps its
constructor-side fields. Nothing else reaches the carrier -- our_index,
source_addr, post-construction started_at and the stored handshake bytes stay
leg-only. Seeding more than that would let these tests observe a carrier
richer than production's and keep passing even if a later production
write-lift were missed.

The Noise exchange now runs on the already-seeded leg rather than before the
hand-over. That is neutral because the only read of expected_identity is
guarded by is_outbound, and no crypto method allocates a session index.

Also adds a compile-time check that PeerAction is Clone + Eq, which is what
keeps a runtime handle from being smuggled into an action payload.
This commit is contained in:
Johnathan Corgan
2026-07-18 21:24:03 +00:00
parent a90049d3a1
commit a382b17931
12 changed files with 436 additions and 206 deletions
+142
View File
@@ -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<PeerIdentity>,
started_at_ms: u64,
transport_id: Option<TransportId>,
source_addr: Option<TransportAddr>,
our_index: Option<crate::utils::index::SessionIndex>,
their_index: Option<crate::utils::index::SessionIndex>,
}
#[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
}
}
+14 -7
View File
@@ -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);
+1 -2
View File
@@ -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();
+1 -2
View File
@@ -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.
+62 -35
View File
@@ -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);
+80 -45
View File
@@ -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);
+36 -18
View File
@@ -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
}
+12 -25
View File
@@ -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
+1 -3
View File
@@ -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
+16 -8
View File
@@ -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
+52 -61
View File
@@ -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!(
+19
View File
@@ -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<T: Clone + Eq>() {}
#[test]
fn peer_action_is_plain_comparable_data() {
assert_clone_eq::<PeerAction>();
}
}