diff --git a/src/control/queries.rs b/src/control/queries.rs index b5bbe1c..4982ffb 100644 --- a/src/control/queries.rs +++ b/src/control/queries.rs @@ -1307,7 +1307,7 @@ pub fn show_connections(node: &Node) -> Value { let mut conn_json = json!({ "link_id": conn.link_id().as_u64(), "direction": format!("{}", conn.direction()), - "handshake_state": format!("{}", conn.handshake_state()), + "handshake_state": node.connection_handshake_state(conn.link_id()), "started_at_ms": conn.started_at(), "idle_ms": now.saturating_sub(conn.last_activity()), "resend_count": node.connection_resend_count(conn.link_id()), diff --git a/src/lib.rs b/src/lib.rs index 5938477..f2c94ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,7 +92,7 @@ pub use cache::{CacheEntry, CacheError, CacheStats, CoordCache}; pub use proto::fmp::{PromotionResult, cross_connection_winner}; // Re-export peer types -pub use peer::{ActivePeer, ConnectivityState, HandshakeState, PeerConnection, PeerError}; +pub use peer::{ActivePeer, ConnectivityState, PeerConnection, PeerError}; // Re-export node types pub use node::{Node, NodeError, NodeState, UpdatePeersOutcome}; diff --git a/src/node/handlers/handshake.rs b/src/node/handlers/handshake.rs index 80ccc2c..a9e8bba 100644 --- a/src/node/handlers/handshake.rs +++ b/src/node/handlers/handshake.rs @@ -997,7 +997,18 @@ impl Node { error = %e, "Handshake completion failed" ); + // Drop the leg's Noise handle (byte-identical point) and record + // the failure on the control machine as `send_failed` — the + // failure state's new home. The machine PHASE stays exactly + // where the old leg-carried failure left it (`SentMsg1`): the + // stale-connection sweep reclaims the leg unconditionally via + // the machine `is_failed()` at the next tick, before any + // projection or resend, so the phase in that window is + // byte-identical to the pre-collapse machine. conn.mark_failed(); + if let Some(machine) = self.peer_machines.get_mut(&link_id) { + machine.mark_send_failed(); + } self.stats_mut() .record_reject(RejectReason::Handshake(HandshakeReject::BadState)); return; diff --git a/src/node/handlers/timeout.rs b/src/node/handlers/timeout.rs index e7e85a1..d47eccb 100644 --- a/src/node/handlers/timeout.rs +++ b/src/node/handlers/timeout.rs @@ -19,15 +19,15 @@ impl LifecycleView for Node { // reap. self.peer_machines .iter() - .filter_map(|(link_id, machine)| machine.leg().map(|conn| (link_id, conn))) - .filter(|(link_id, conn)| { - conn.is_failed() + .filter_map(|(link_id, machine)| machine.leg().map(|conn| (link_id, machine, conn))) + .filter(|(link_id, machine, conn)| { + machine.is_failed() || (conn.is_timed_out(now_ms, timeout_ms) && !self.peer_timers.get(*link_id).is_some_and(|timers| { timers.contains_key(&TimerKind::HandshakeTimeout) })) }) - .map(|(link_id, conn)| ConnSnapshot { + .map(|(link_id, _machine, conn)| ConnSnapshot { link: *link_id, is_outbound: conn.is_outbound(), retry_addr: conn.expected_identity().map(|id| *id.node_addr()), @@ -70,10 +70,16 @@ impl Node { match action { ConnAction::ScheduleRetry { peer } => self.note_handshake_timeout(peer, now_ms), ConnAction::Teardown { link } => { - // Log before cleanup (needs live connection state). + // Log before cleanup (needs live connection state). The + // failure signal is now read from the control machine; the + // leg still carries direction/idle for the log fields. + let is_failed = self + .peer_machines + .get(&link) + .is_some_and(|machine| machine.is_failed()); if let Some(conn) = self.leg(&link) { let direction = conn.direction(); - if conn.is_failed() { + if is_failed { debug!( link_id = %link, direction = %direction, diff --git a/src/node/mod.rs b/src/node/mod.rs index ad1bf6d..458aca2 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1981,7 +1981,7 @@ impl Node { .map(|conn| snap::ConnectionRow { link_id: conn.link_id().as_u64(), direction: format!("{}", conn.direction()), - handshake_state: format!("{}", conn.handshake_state()), + handshake_state: self.connection_handshake_state(conn.link_id()).to_string(), started_at_ms: conn.started_at(), last_activity_ms: conn.last_activity(), resend_count: self.connection_resend_count(conn.link_id()), @@ -2309,6 +2309,17 @@ impl Node { .map_or(0, |machine| machine.resend_count()) } + /// Operator-visible handshake-state string for a pending handshake `link`, + /// derived from the per-peer control machine (the phase's home now that the + /// leg no longer carries it). Every leg surfaced by `connections()` is + /// embedded in a machine, so the lookup resolves; the `"initial"` default is + /// unreachable in that view and only guards a missing machine. + pub(crate) fn connection_handshake_state(&self, link: LinkId) -> &'static str { + self.peer_machines + .get(&link) + .map_or("initial", |machine| machine.displayed_handshake_state()) + } + pub(crate) fn cleanup_bootstrap_transport_if_unused(&mut self, transport_id: TransportId) { if !self .supervisor diff --git a/src/node/tests/handshake.rs b/src/node/tests/handshake.rs index 18db30f..e54a397 100644 --- a/src/node/tests/handshake.rs +++ b/src/node/tests/handshake.rs @@ -751,7 +751,6 @@ async fn test_failed_connection_cleanup() { conn.set_our_index(our_index); conn.set_transport_id(transport_id); conn.set_source_addr(remote_addr.clone()); - conn.mark_failed(); // Simulate send failure let link = Link::connectionless( link_id, @@ -767,6 +766,24 @@ async fn test_failed_connection_cleanup() { node.pending_outbound .insert((transport_id, our_index.as_u32()), link_id); + // Simulate a stored-handshake send failure through the control machine — + // the failure carrier the stale-connection sweep now reads (the leg no + // longer carries a failed phase of its own). + { + let machine = node + .peer_machines + .get_mut(&link_id) + .expect("machine seeded by add_connection"); + let alloc = &mut node.index_allocator; + let actions = machine.step( + crate::peer::machine::PeerEvent::HandshakeSendFailed, + now_ms, + alloc, + ); + assert!(actions.is_empty()); + assert!(machine.is_failed()); + } + assert_eq!(node.connection_count(), 1); // Failed connections should be cleaned up immediately regardless of age diff --git a/src/peer/connection.rs b/src/peer/connection.rs index 3db97b9..e8c4aee 100644 --- a/src/peer/connection.rs +++ b/src/peer/connection.rs @@ -1,8 +1,11 @@ //! Peer Connection (Handshake Phase) //! //! Represents an in-progress connection before authentication completes. -//! PeerConnection tracks the Noise IK handshake state and transitions to -//! ActivePeer upon successful authentication. +//! PeerConnection tracks the Noise IK handshake and transitions to +//! ActivePeer upon successful authentication. The handshake *phase* (initial / +//! sent_msg1 / complete / failed) is no longer tracked here — it lives on the +//! per-peer control machine; the leg's crypto methods gate on the presence of +//! their Noise handles (`noise_handshake` / `noise_session`) directly. use crate::PeerIdentity; use crate::noise::{self, NoiseError, NoiseSession}; @@ -12,12 +15,6 @@ use crate::utils::index::SessionIndex; use secp256k1::Keypair; use std::fmt; -// The pure handshake-phase bookkeeping (`ConnectionState`) and its -// `HandshakeState` phase enum now live in `proto::fmp::state`. Re-export -// `HandshakeState` here so its original public path -// (`crate::peer::HandshakeState`) is preserved for existing call sites. -pub use crate::proto::fmp::HandshakeState; - /// A connection in the handshake phase, before authentication completes. /// /// For outbound connections, we know the expected peer identity from config. @@ -101,11 +98,6 @@ impl PeerConnection { self.state.direction() } - /// Get the handshake state. - pub fn handshake_state(&self) -> HandshakeState { - self.state.handshake_state() - } - /// Get the expected/learned peer identity, if known. pub fn expected_identity(&self) -> Option<&PeerIdentity> { self.state.expected_identity() @@ -121,21 +113,6 @@ impl PeerConnection { self.state.is_inbound() } - /// Check if handshake is in progress. - pub fn is_in_progress(&self) -> bool { - self.state.is_in_progress() - } - - /// Check if handshake completed. - pub fn is_complete(&self) -> bool { - self.state.is_complete() - } - - /// Check if handshake failed. - pub fn is_failed(&self) -> bool { - self.state.is_failed() - } - /// When the connection started. pub fn started_at(&self) -> u64 { self.state.started_at() @@ -256,13 +233,6 @@ impl PeerConnection { }); } - if self.state.handshake_state() != HandshakeState::Initial { - return Err(NoiseError::WrongState { - expected: "initial state".to_string(), - got: self.state.handshake_state().to_string(), - }); - } - let remote_static = self .state .expected_identity() @@ -274,7 +244,6 @@ impl PeerConnection { let msg1 = hs.write_message_1()?; self.noise_handshake = Some(hs); - self.state.set_handshake_state(HandshakeState::SentMsg1); self.state.touch(current_time_ms); Ok(msg1) @@ -298,13 +267,6 @@ impl PeerConnection { }); } - if self.state.handshake_state() != HandshakeState::Initial { - return Err(NoiseError::WrongState { - expected: "initial state".to_string(), - got: self.state.handshake_state().to_string(), - }); - } - let mut hs = noise::HandshakeState::new_responder(our_keypair); hs.set_local_epoch(epoch); @@ -328,7 +290,6 @@ impl PeerConnection { // Handshake is complete for responder let session = hs.into_session()?; self.noise_session = Some(session); - self.state.set_handshake_state(HandshakeState::Complete); self.state.touch(current_time_ms); Ok(msg2) @@ -342,10 +303,14 @@ impl PeerConnection { message: &[u8], current_time_ms: u64, ) -> Result<(), NoiseError> { - if self.state.handshake_state() != HandshakeState::SentMsg1 { + // The leg is at `SentMsg1` iff its Noise handshake handle is present + // (set by `start_handshake`, taken here on completion). Gate on the + // handle directly now that the phase enum is gone — byte-equivalent to + // the old `!= SentMsg1` guard for every reachable transition. + if self.noise_handshake.is_none() { return Err(NoiseError::WrongState { expected: "sent_msg1 state".to_string(), - got: self.state.handshake_state().to_string(), + got: "no active handshake".to_string(), }); } @@ -361,7 +326,6 @@ impl PeerConnection { let session = hs.into_session()?; self.noise_session = Some(session); - self.state.set_handshake_state(HandshakeState::Complete); self.state.touch(current_time_ms); Ok(()) @@ -372,24 +336,23 @@ impl PeerConnection { /// Returns the NoiseSession for use in ActivePeer. Can only be called /// once after handshake completes. pub fn take_session(&mut self) -> Option { - if self.state.handshake_state() == HandshakeState::Complete { - self.noise_session.take() - } else { - None - } + // The session exists iff the handshake reached `Complete`, so taking it + // unconditionally is byte-equivalent to the old `== Complete` gate. + self.noise_session.take() } /// Check if we have a completed session ready to take. pub fn has_session(&self) -> bool { - self.state.handshake_state() == HandshakeState::Complete && self.noise_session.is_some() + self.noise_session.is_some() } // === State Transitions (for manual control if needed) === - /// Mark handshake as failed. Sets the pure lifecycle state and drops the - /// shell-owned crypto handshake handle. + /// Drop the shell-owned crypto handshake handle. The failure *state* now + /// lives on the control machine (`PeerMachine`); this only releases the + /// leg's Noise handle at the identical point it was released before, so a + /// subsequent `complete_handshake` on this leg still reports `WrongState`. pub fn mark_failed(&mut self) { - self.state.mark_failed(); self.noise_handshake = None; } @@ -411,7 +374,6 @@ impl fmt::Debug for PeerConnection { f.debug_struct("PeerConnection") .field("link_id", &self.state.link_id()) .field("direction", &self.state.direction()) - .field("handshake_state", &self.state.handshake_state()) .field("expected_identity", &self.state.expected_identity()) .field("has_noise_handshake", &self.noise_handshake.is_some()) .field("has_noise_session", &self.noise_session.is_some()) @@ -446,18 +408,6 @@ mod tests { epoch } - #[test] - fn test_handshake_state_properties() { - assert!(HandshakeState::Initial.is_in_progress()); - assert!(HandshakeState::SentMsg1.is_in_progress()); - assert!(HandshakeState::ReceivedMsg1.is_in_progress()); - assert!(!HandshakeState::Complete.is_in_progress()); - assert!(!HandshakeState::Failed.is_in_progress()); - - assert!(HandshakeState::Complete.is_complete()); - assert!(HandshakeState::Failed.is_failed()); - } - #[test] fn test_outbound_connection() { let identity = make_peer_identity(); @@ -465,7 +415,7 @@ mod tests { assert!(conn.is_outbound()); assert!(!conn.is_inbound()); - assert_eq!(conn.handshake_state(), HandshakeState::Initial); + assert!(!conn.has_session()); assert!(conn.expected_identity().is_some()); assert_eq!(conn.started_at(), 1000); } @@ -476,7 +426,7 @@ mod tests { assert!(conn.is_inbound()); assert!(!conn.is_outbound()); - assert_eq!(conn.handshake_state(), HandshakeState::Initial); + assert!(!conn.has_session()); assert!(conn.expected_identity().is_none()); assert_eq!(conn.started_at(), 2000); } @@ -503,13 +453,15 @@ mod tests { let msg1 = initiator_conn .start_handshake(initiator_keypair, initiator_epoch, 1100) .unwrap(); - assert_eq!(initiator_conn.handshake_state(), HandshakeState::SentMsg1); + // Post-msg1 the initiator holds an in-flight handshake, not yet a session. + assert!(!initiator_conn.has_session()); // Responder processes msg1 and sends msg2 let msg2 = responder_conn .receive_handshake_init(responder_keypair, responder_epoch, &msg1, 1200) .unwrap(); - assert_eq!(responder_conn.handshake_state(), HandshakeState::Complete); + // The IK responder completes in one step: it now holds a session. + assert!(responder_conn.has_session()); // Responder learned initiator's identity let discovered = responder_conn.expected_identity().unwrap(); @@ -520,7 +472,7 @@ mod tests { // Initiator completes handshake initiator_conn.complete_handshake(&msg2, 1300).unwrap(); - assert_eq!(initiator_conn.handshake_state(), HandshakeState::Complete); + assert!(initiator_conn.has_session()); // Initiator learned responder's epoch assert_eq!(initiator_conn.remote_epoch(), Some(responder_epoch)); @@ -553,13 +505,19 @@ mod tests { #[test] fn test_connection_failure() { + // `mark_failed` releases the leg's Noise handshake handle. The failure + // *state* now lives on the control machine, but the leg-local effect is + // still observable: a completion attempt afterward reports `WrongState` + // (the handle-presence gate) and no session is produced. let identity = make_peer_identity(); + let keypair = make_keypair(); let mut conn = PeerConnection::outbound(LinkId::new(1), identity, 1000); + conn.start_handshake(keypair, make_epoch(), 1100).unwrap(); conn.mark_failed(); - assert!(conn.is_failed()); - assert!(!conn.is_in_progress()); - assert!(!conn.is_complete()); + + assert!(!conn.has_session()); + assert!(conn.complete_handshake(&[0u8; 96], 1200).is_err()); } #[test] diff --git a/src/peer/machine.rs b/src/peer/machine.rs index b3bd776..3343d7a 100644 --- a/src/peer/machine.rs +++ b/src/peer/machine.rs @@ -124,7 +124,7 @@ pub(crate) enum PeerState { Closed { backoff_deadline_ms: u64 }, } -/// Handshake phase (mirrors `proto::fmp::HandshakeState`'s in-progress arms). +/// Handshake phase (the in-progress arms of the peer lifecycle). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum HandshakePhase { Initial, @@ -132,6 +132,37 @@ pub(crate) enum HandshakePhase { ReceivedMsg1, } +/// Map a lifecycle state to the operator-visible pending-connection handshake +/// string. Total over `PeerState`; byte-identical to the strings the deleted +/// leg `HandshakeState` `Display` produced. Only the `Handshaking{SentMsg1}` +/// arm (and, via the `send_failed` flag handled by the caller, `"failed"`) is +/// production-reachable in the IK pending-connection view — the rest are kept +/// total for a complete mapping and the next-branch inbound window. +fn handshake_state_str(state: PeerState) -> &'static str { + match state { + PeerState::Handshaking { + phase: HandshakePhase::Initial, + .. + } => "initial", + PeerState::Handshaking { + phase: HandshakePhase::SentMsg1, + .. + } => "sent_msg1", + PeerState::Handshaking { + phase: HandshakePhase::ReceivedMsg1, + .. + } => "received_msg1", + PeerState::Established { .. } + | PeerState::Active { .. } + | PeerState::Maintaining { .. } + | PeerState::Closing { .. } => "complete", + PeerState::Failed { .. } => "failed", + PeerState::Discovered | PeerState::Connecting { .. } | PeerState::Closed { .. } => { + "initial" + } + } +} + /// Which maintenance sub-machine `Maintaining` is running. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum MaintainKind { @@ -397,6 +428,13 @@ pub(crate) struct PeerMachine { /// payload stashed in Phase 1 (`InboundMsg1`) and emitted in Phase 2 /// (`on_authorized`), so a rejected/unauthorized msg1 allocates no index. pending_msg2_payload: Option>, + /// A stored-handshake send failure was observed on this leg. The failure is + /// carried as a flag (not a `PeerState::Failed` transition) so retransmit + /// eligibility (`is_handshaking_sent_msg1`) survives until the + /// stale-connection sweep reclaims the leg. It drives both `is_failed` + /// (reaping) and the displayed handshake state (`"failed"`), reproducing the + /// pre-collapse leg `is_failed`/display signal byte-for-byte. + send_failed: bool, // --- rekey negotiation sub-state (control tier; NOT the pending send slot) --- rekey_in_progress: bool, @@ -437,6 +475,7 @@ impl PeerMachine { conn: ConnectionState::outbound(link, identity, now), remote_epoch: None, pending_msg2_payload: None, + send_failed: false, rekey_in_progress: false, rekey_our_index: None, rekey_msg1: None, @@ -464,6 +503,7 @@ impl PeerMachine { conn: ConnectionState::inbound(link, now), remote_epoch: None, pending_msg2_payload: None, + send_failed: false, rekey_in_progress: false, rekey_our_index: None, rekey_msg1: None, @@ -543,6 +583,41 @@ impl PeerMachine { ) } + /// Whether this peer's handshake has failed. The sole failure carrier now + /// that the leg's phase enum is gone: a terminal `PeerState::Failed` + /// (hard crypto/transport/ACL failures) OR the `send_failed` flag (a stored + /// handshake-initiation send failure that deliberately keeps the machine at + /// `Handshaking{SentMsg1}`). The stale-connection sweep reads this to reclaim + /// the leg, exactly as it read the leg's `is_failed` before. + pub(crate) fn is_failed(&self) -> bool { + matches!(self.state, PeerState::Failed { .. }) || self.send_failed + } + + /// The operator-visible handshake-state string for the pending-connection + /// view, derived from the machine phase. Byte-identical to the strings the + /// leg's `HandshakeState` `Display` produced before the phase collapsed onto + /// the machine. A `send_failed` leg renders `"failed"` while its phase stays + /// `SentMsg1`, matching the pre-collapse leg display. + pub(crate) fn displayed_handshake_state(&self) -> &'static str { + if self.send_failed { + return "failed"; + } + handshake_state_str(self.state) + } + + /// Record a handshake failure that the shell observed on the leg (e.g. a + /// `complete_handshake` that rejected msg2), WITHOUT leaving the current + /// handshake phase. Mirrors the `HandshakeSendFailed` carve-out: the failure + /// is carried as `send_failed`, so the machine PHASE is unchanged (matching + /// the pre-collapse behavior, where the shell marked only the leg failed and + /// left the machine in place) while `is_failed`/display report the failure. + /// The stale-connection sweep reclaims the leg via + /// [`is_failed`](Self::is_failed) at the next tick, before any projection or + /// resend. + pub(crate) fn mark_send_failed(&mut self) { + self.send_failed = true; + } + /// The crystallized node address, if identity is known. fn addr(&self) -> Option { self.identity.map(|id| *id.node_addr()) @@ -685,8 +760,11 @@ impl PeerMachine { /// actions are emitted. fn on_handshake_send_failed(&mut self) -> Vec { if let Some(leg) = self.leg.as_mut() { + // Drop the leg's Noise handshake handle at the identical point as + // before; the failure *state* is recorded on the machine. leg.mark_failed(); } + self.send_failed = true; Vec::new() } @@ -2467,7 +2545,8 @@ mod tests { ); m.set_leg(PeerConnection::outbound(LinkId::new(1), peer, 100)); assert!(m.is_handshaking_sent_msg1()); - assert!(!m.leg().expect("leg embedded").is_failed()); + assert!(!m.is_failed()); + assert_eq!(m.displayed_handshake_state(), "sent_msg1"); let actions = m.step(PeerEvent::HandshakeSendFailed, 200, &mut alloc); assert_eq!(actions, Vec::new(), "HandshakeSendFailed emits no actions"); @@ -2476,11 +2555,17 @@ mod tests { "retransmit eligibility survives a send failure" ); assert!( - m.leg().expect("leg retained").is_failed(), - "the leg carries the failed mark the sweep reads" + m.is_failed(), + "the machine carries the failed mark the sweep reads" + ); + assert_eq!( + m.displayed_handshake_state(), + "failed", + "the send-failed leg still displays as failed" ); - // With no leg (e.g. after take_leg) the event is a defensive no-op. + // With no leg (e.g. after take_leg) the event stays a defensive no-op + // for the leg handle and keeps the state retransmit-eligible. let _ = m.take_leg(); let actions = m.step(PeerEvent::HandshakeSendFailed, 300, &mut alloc); assert_eq!(actions, Vec::new()); @@ -2488,6 +2573,76 @@ mod tests { assert!(m.leg().is_none()); } + // ---- Test 7f: handshake_state_str is a total, byte-identical mapping --- + // Pins the displayed-string derivation for every `PeerState` arm against the + // strings the deleted leg `HandshakeState` `Display` produced. + #[test] + fn handshake_state_str_total_mapping() { + let link = LinkId::new(1); + let addr = *peer_identity().node_addr(); + + assert_eq!( + handshake_state_str(PeerState::Handshaking { + link, + phase: HandshakePhase::Initial, + }), + "initial" + ); + assert_eq!( + handshake_state_str(PeerState::Handshaking { + link, + phase: HandshakePhase::SentMsg1, + }), + "sent_msg1" + ); + assert_eq!( + handshake_state_str(PeerState::Handshaking { + link, + phase: HandshakePhase::ReceivedMsg1, + }), + "received_msg1" + ); + + assert_eq!( + handshake_state_str(PeerState::Established { addr }), + "complete" + ); + assert_eq!(handshake_state_str(PeerState::Active { addr }), "complete"); + assert_eq!( + handshake_state_str(PeerState::Maintaining { + addr, + kind: MaintainKind::Mtu, + }), + "complete" + ); + assert_eq!( + handshake_state_str(PeerState::Closing { + addr, + reason: CloseReason::Requested, + }), + "complete" + ); + + assert_eq!( + handshake_state_str(PeerState::Failed { + reason: FailReason::HandshakeFailed, + }), + "failed" + ); + + assert_eq!(handshake_state_str(PeerState::Discovered), "initial"); + assert_eq!( + handshake_state_str(PeerState::Connecting { link }), + "initial" + ); + assert_eq!( + handshake_state_str(PeerState::Closed { + backoff_deadline_ms: 0, + }), + "initial" + ); + } + // ---- Test 8: liveness -> LinkDeadSuspected -> ReportLost -------------- #[test] fn liveness_to_link_dead() { diff --git a/src/peer/mod.rs b/src/peer/mod.rs index 65e2c33..690dfd0 100644 --- a/src/peer/mod.rs +++ b/src/peer/mod.rs @@ -11,7 +11,7 @@ mod connection; pub(crate) mod machine; pub use active::{ActivePeer, ConnectivityState}; -pub use connection::{HandshakeState, PeerConnection}; +pub use connection::PeerConnection; use crate::NodeAddr; use crate::transport::LinkId; diff --git a/src/proto/fmp/mod.rs b/src/proto/fmp/mod.rs index c719921..96d2e8a 100644 --- a/src/proto/fmp/mod.rs +++ b/src/proto/fmp/mod.rs @@ -18,8 +18,8 @@ //! - `limits.rs` — the pure connection-retry backoff math. //! - `state.rs` — [`ConnectionState`], the pure handshake-phase connection //! bookkeeping (owned by the shell `PeerConnection` beside its Noise crypto -//! handles) and its [`HandshakeState`] phase enum, plus [`Fmp`], the -//! (stateless) lifecycle anchor owned by `Node`. +//! handles), plus [`Fmp`], the (stateless) lifecycle anchor owned by `Node`. +//! The handshake phase itself lives on the per-peer control machine. //! - `wire.rs` — the FMP link-framing codec: handshake message types, //! disconnect reasons, and the orderly disconnect message. Also carries the //! FMP link wire framing relocated from `node/wire.rs` — the common prefix, @@ -40,7 +40,6 @@ pub(crate) use core::{ }; pub use core::{PromotionResult, cross_connection_winner}; pub(crate) use limits::backoff_ms; -pub use state::HandshakeState; pub(crate) use state::{ConnectionState, Fmp}; pub use wire::HandshakeMessageType; pub(crate) use wire::{Disconnect, DisconnectReason}; diff --git a/src/proto/fmp/state.rs b/src/proto/fmp/state.rs index 1bf71ff..817c640 100644 --- a/src/proto/fmp/state.rs +++ b/src/proto/fmp/state.rs @@ -1,19 +1,20 @@ //! Sans-IO FMP connection-lifecycle state. //! //! The pure, runtime-agnostic bookkeeping for an in-progress FMP peer -//! connection — link/direction identity, the handshake-phase enum, learned -//! peer identity and epoch, index/transport/address tracking, handshake-resend -//! scheduling, and link statistics — extracted out of the async node shell. +//! connection — link/direction identity, learned peer identity and epoch, +//! index/transport/address tracking, handshake-resend scheduling, and link +//! statistics — extracted out of the async node shell. The handshake phase +//! itself lives solely on the per-peer control machine +//! ([`PeerMachine`](crate::peer::machine::PeerMachine)), not here. //! //! [`ConnectionState`] owns every **pure** field of the handshake-phase //! connection. The Noise crypto handles (`noise::HandshakeState`, //! `NoiseSession`) stay shell-owned in //! [`PeerConnection`](crate::peer::PeerConnection), which holds a //! `ConnectionState` alongside them and drives the two halves side by side. The -//! shell's XX transition methods validate against the pure phase, drive the -//! Noise objects, then write learned results back through the pure setters here -//! (`set_handshake_state`, `set_expected_identity`, `set_remote_epoch`, -//! `touch`). +//! shell's XX transition methods drive the Noise objects, then write learned +//! results back through the pure setters here (`set_expected_identity`, +//! `set_remote_epoch`, `touch`). //! //! This state is `no_std`+`alloc`-clean with respect to transport: the //! identifier/address/statistics value types are the plain-data `transport` @@ -30,59 +31,6 @@ use crate::PeerIdentity; use crate::transport::{LinkDirection, LinkId, LinkStats, TransportAddr, TransportId}; use crate::utils::index::SessionIndex; -use core::fmt; - -/// Handshake protocol state machine. -/// -/// For Noise IK pattern: -/// - Initiator: Initial → SentMsg1 → Complete -/// - Responder: Initial → ReceivedMsg1 → Complete -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum HandshakeState { - /// Initial state, ready to start handshake. - Initial, - /// Initiator: Sent message 1, awaiting message 2. - SentMsg1, - /// Responder: Received message 1, ready to send message 2. - ReceivedMsg1, - /// Handshake completed successfully. - Complete, - /// Handshake failed. - Failed, -} - -impl HandshakeState { - /// Check if handshake is still in progress. - pub fn is_in_progress(&self) -> bool { - matches!( - self, - HandshakeState::Initial | HandshakeState::SentMsg1 | HandshakeState::ReceivedMsg1 - ) - } - - /// Check if handshake completed successfully. - pub fn is_complete(&self) -> bool { - matches!(self, HandshakeState::Complete) - } - - /// Check if handshake failed. - pub fn is_failed(&self) -> bool { - matches!(self, HandshakeState::Failed) - } -} - -impl fmt::Display for HandshakeState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - HandshakeState::Initial => "initial", - HandshakeState::SentMsg1 => "sent_msg1", - HandshakeState::ReceivedMsg1 => "received_msg1", - HandshakeState::Complete => "complete", - HandshakeState::Failed => "failed", - }; - write!(f, "{}", s) - } -} /// Pure, runtime-agnostic bookkeeping for a connection in the handshake phase. /// @@ -100,10 +48,6 @@ pub struct ConnectionState { /// Connection direction (we initiated or they initiated). direction: LinkDirection, - // === Handshake State === - /// Current handshake state. - handshake_state: HandshakeState, - /// Expected peer identity (known for outbound, learned for inbound). /// Updated after receiving their static key in the handshake. expected_identity: Option, @@ -166,7 +110,6 @@ impl ConnectionState { Self { link_id, direction: LinkDirection::Outbound, - handshake_state: HandshakeState::Initial, expected_identity: Some(expected_identity), started_at: current_time_ms, last_activity: current_time_ms, @@ -191,7 +134,6 @@ impl ConnectionState { Self { link_id, direction: LinkDirection::Inbound, - handshake_state: HandshakeState::Initial, expected_identity: None, started_at: current_time_ms, last_activity: current_time_ms, @@ -220,7 +162,6 @@ impl ConnectionState { Self { link_id, direction: LinkDirection::Inbound, - handshake_state: HandshakeState::Initial, expected_identity: None, started_at: current_time_ms, last_activity: current_time_ms, @@ -249,11 +190,6 @@ impl ConnectionState { self.direction } - /// Get the handshake state. - pub fn handshake_state(&self) -> HandshakeState { - self.handshake_state - } - /// Get the expected/learned peer identity, if known. pub fn expected_identity(&self) -> Option<&PeerIdentity> { self.expected_identity.as_ref() @@ -269,21 +205,6 @@ impl ConnectionState { self.direction == LinkDirection::Inbound } - /// Check if handshake is in progress. - pub fn is_in_progress(&self) -> bool { - self.handshake_state.is_in_progress() - } - - /// Check if handshake completed. - pub fn is_complete(&self) -> bool { - self.handshake_state.is_complete() - } - - /// Check if handshake failed. - pub fn is_failed(&self) -> bool { - self.handshake_state.is_failed() - } - /// When the connection started. pub fn started_at(&self) -> u64 { self.started_at @@ -377,20 +298,6 @@ impl ConnectionState { self.expected_identity = Some(identity); } - // === Handshake Phase Advance === - - /// Advance the pure handshake phase. Driven by the shell after it has - /// stepped the Noise objects. - pub fn set_handshake_state(&mut self, state: HandshakeState) { - self.handshake_state = state; - } - - /// Mark the pure handshake phase failed. The shell drops the crypto handle - /// separately. - pub fn mark_failed(&mut self) { - self.handshake_state = HandshakeState::Failed; - } - // === Handshake Resend === /// Store the wire-format msg1 bytes for resend and schedule the first resend. diff --git a/src/proto/fmp/tests/state.rs b/src/proto/fmp/tests/state.rs index 390ba71..abd8418 100644 --- a/src/proto/fmp/tests/state.rs +++ b/src/proto/fmp/tests/state.rs @@ -1,9 +1,9 @@ -//! Unit tests for the pure FMP connection state ([`ConnectionState`]) and its -//! [`HandshakeState`] phase enum. These exercise the extracted bookkeeping -//! directly, with no crypto involved; the crypto-driving transition behavior is -//! covered by the shell `peer::connection` suite. +//! Unit tests for the pure FMP connection state ([`ConnectionState`]). These +//! exercise the extracted bookkeeping directly, with no crypto involved; the +//! crypto-driving transition behavior is covered by the shell `peer::connection` +//! suite, and the handshake phase itself lives on the control machine. -use crate::proto::fmp::{ConnectionState, HandshakeState}; +use crate::proto::fmp::ConnectionState; use crate::transport::{LinkId, TransportAddr, TransportId}; use crate::utils::index::SessionIndex; use crate::{Identity, PeerIdentity}; @@ -12,21 +12,6 @@ fn make_peer_identity() -> PeerIdentity { PeerIdentity::from_pubkey(Identity::generate().pubkey()) } -#[test] -fn handshake_state_predicates() { - assert!(HandshakeState::Initial.is_in_progress()); - assert!(HandshakeState::SentMsg1.is_in_progress()); - assert!(HandshakeState::ReceivedMsg1.is_in_progress()); - assert!(!HandshakeState::Complete.is_in_progress()); - assert!(!HandshakeState::Failed.is_in_progress()); - - assert!(HandshakeState::Complete.is_complete()); - assert!(!HandshakeState::Initial.is_complete()); - - assert!(HandshakeState::Failed.is_failed()); - assert!(!HandshakeState::Complete.is_failed()); -} - #[test] fn outbound_initializes_pure_fields() { let identity = make_peer_identity(); @@ -34,8 +19,6 @@ fn outbound_initializes_pure_fields() { assert!(state.is_outbound()); assert!(!state.is_inbound()); - assert_eq!(state.handshake_state(), HandshakeState::Initial); - assert!(state.is_in_progress()); assert!(state.expected_identity().is_some()); assert_eq!(state.link_id(), LinkId::new(1)); assert_eq!(state.started_at(), 1000); @@ -53,7 +36,6 @@ fn inbound_initializes_pure_fields() { assert!(state.is_inbound()); assert!(!state.is_outbound()); - assert_eq!(state.handshake_state(), HandshakeState::Initial); assert!(state.expected_identity().is_none()); assert_eq!(state.started_at(), 2000); } @@ -111,27 +93,6 @@ fn identity_and_epoch_setters() { assert_eq!(state.remote_epoch(), Some([9u8; 8])); } -#[test] -fn handshake_state_advance_and_fail() { - let mut state = ConnectionState::outbound(LinkId::new(1), make_peer_identity(), 0); - assert!(state.is_in_progress()); - - state.set_handshake_state(HandshakeState::SentMsg1); - assert_eq!(state.handshake_state(), HandshakeState::SentMsg1); - assert!(state.is_in_progress()); - assert!(!state.is_complete()); - - state.set_handshake_state(HandshakeState::Complete); - assert!(state.is_complete()); - assert!(!state.is_in_progress()); - - state.mark_failed(); - assert!(state.is_failed()); - assert!(!state.is_in_progress()); - assert!(!state.is_complete()); - assert_eq!(state.handshake_state(), HandshakeState::Failed); -} - #[test] fn resend_bookkeeping() { let mut state = ConnectionState::outbound(LinkId::new(1), make_peer_identity(), 0);