mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-11 00:47:39 +00:00
Merge branch 'refactor-node' into refactor-node-next
Re-express the handshake-state carrier collapse onto the XX code: the leg's handshake_state field is deleted and the displayed state is derived from the peer machine's phase, with failure carried on the machine (a send_failed flag that preserves the handshake phase) rather than on the leg. The next projection maps the SentMsg2 responder phase to received_msg1 and the anonymous-dial Discovered phase to sent_msg1; the three initiator send-failure sites carry failure via send_failed. Telemetry strings, wire bytes, index allocation, and stale-connection reaping are byte-identical to next.
This commit is contained in:
+56
-89
@@ -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.
|
||||
@@ -114,11 +111,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()
|
||||
@@ -134,21 +126,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()
|
||||
@@ -281,20 +258,15 @@ 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(),
|
||||
});
|
||||
}
|
||||
|
||||
// XX initiator: no remote static needed upfront
|
||||
// XX initiator: no remote static needed upfront. The old `!= Initial`
|
||||
// phase guard is dropped — this method creates the Noise handshake
|
||||
// handle, so there is no `take().expect()` to protect, and its Err path
|
||||
// was unreachable in production (one call per fresh outbound leg).
|
||||
let mut hs = noise::HandshakeState::new_initiator(our_keypair);
|
||||
hs.set_local_epoch(epoch);
|
||||
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)
|
||||
@@ -324,13 +296,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);
|
||||
|
||||
@@ -346,10 +311,11 @@ impl PeerConnection {
|
||||
msg2.extend_from_slice(&encrypted);
|
||||
}
|
||||
|
||||
// XX: handshake NOT complete yet — need msg3.
|
||||
// Keep the handshake state for complete_handshake_msg3().
|
||||
// XX: handshake NOT complete yet — need msg3. Keep the Noise handshake
|
||||
// handle for complete_handshake_msg3(); the phase (formerly
|
||||
// `ReceivedMsg1`) now lives on the control machine, so no phase is set
|
||||
// here. The handle's presence is the leg-local `ReceivedMsg1` signal.
|
||||
self.noise_handshake = Some(hs);
|
||||
self.state.set_handshake_state(HandshakeState::ReceivedMsg1);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(msg2)
|
||||
@@ -370,10 +336,14 @@ impl PeerConnection {
|
||||
negotiation_payload: Option<&[u8]>,
|
||||
current_time_ms: u64,
|
||||
) -> Result<(Vec<u8>, Option<Vec<u8>>), 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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -422,7 +392,6 @@ impl PeerConnection {
|
||||
// Handshake complete for initiator
|
||||
let session = hs.into_session()?;
|
||||
self.noise_session = Some(session);
|
||||
self.state.set_handshake_state(HandshakeState::Complete);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok((msg3, received_negotiation))
|
||||
@@ -440,10 +409,15 @@ impl PeerConnection {
|
||||
message: &[u8],
|
||||
current_time_ms: u64,
|
||||
) -> Result<Option<Vec<u8>>, NoiseError> {
|
||||
if self.state.handshake_state() != HandshakeState::ReceivedMsg1 {
|
||||
// The responder leg is at `ReceivedMsg1` iff its Noise handshake handle
|
||||
// is present (set by `receive_handshake_init`, taken here on msg3). Gate
|
||||
// on the handle directly now that the phase enum is gone — byte-equivalent
|
||||
// to the old `!= ReceivedMsg1` guard for every reachable transition, and
|
||||
// it protects the `take().expect()` below.
|
||||
if self.noise_handshake.is_none() {
|
||||
return Err(NoiseError::WrongState {
|
||||
expected: "received_msg1 state".to_string(),
|
||||
got: self.state.handshake_state().to_string(),
|
||||
got: "no active handshake".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -480,10 +454,10 @@ impl PeerConnection {
|
||||
// Capture remote epoch from msg3
|
||||
self.state.set_remote_epoch(hs.remote_epoch());
|
||||
|
||||
// Handshake complete for responder
|
||||
// Handshake complete for responder. The completion signal is now the
|
||||
// Noise session's presence (the phase enum is gone); no phase is set.
|
||||
let session = hs.into_session()?;
|
||||
self.noise_session = Some(session);
|
||||
self.state.set_handshake_state(HandshakeState::Complete);
|
||||
self.state.touch(current_time_ms);
|
||||
|
||||
Ok(received_negotiation)
|
||||
@@ -494,24 +468,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<NoiseSession> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -533,7 +506,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())
|
||||
@@ -568,18 +540,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();
|
||||
@@ -587,7 +547,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);
|
||||
}
|
||||
@@ -598,7 +558,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);
|
||||
}
|
||||
@@ -625,16 +585,16 @@ 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 (XX: does NOT complete yet)
|
||||
let msg2 = responder_conn
|
||||
.receive_handshake_init(responder_keypair, responder_epoch, &msg1, None, 1200)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
responder_conn.handshake_state(),
|
||||
HandshakeState::ReceivedMsg1
|
||||
);
|
||||
// XX: the responder parks awaiting msg3 — it holds an in-flight
|
||||
// handshake, not yet a session (the deleted phase was `ReceivedMsg1`).
|
||||
assert!(!responder_conn.has_session());
|
||||
// Responder does NOT know initiator's identity yet (XX property)
|
||||
assert!(responder_conn.expected_identity().is_none());
|
||||
|
||||
@@ -642,16 +602,17 @@ mod tests {
|
||||
let (msg3, _neg) = initiator_conn
|
||||
.complete_handshake(&msg2, None, 1300)
|
||||
.unwrap();
|
||||
assert_eq!(initiator_conn.handshake_state(), HandshakeState::Complete);
|
||||
// The initiator completes at msg3 generation: it now holds a session.
|
||||
assert!(initiator_conn.has_session());
|
||||
|
||||
// Initiator learned responder's identity from msg2
|
||||
let discovered = initiator_conn.expected_identity().unwrap();
|
||||
assert_eq!(discovered.pubkey(), responder_identity.pubkey());
|
||||
assert_eq!(initiator_conn.remote_epoch(), Some(responder_epoch));
|
||||
|
||||
// Responder processes msg3 (completes handshake)
|
||||
// Responder processes msg3 (completes handshake): it now holds a session.
|
||||
let _neg = responder_conn.complete_handshake_msg3(&msg3, 1400).unwrap();
|
||||
assert_eq!(responder_conn.handshake_state(), HandshakeState::Complete);
|
||||
assert!(responder_conn.has_session());
|
||||
|
||||
// Responder learned initiator's identity from msg3
|
||||
let discovered = responder_conn.expected_identity().unwrap();
|
||||
@@ -686,13 +647,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], None, 1200).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+182
-4
@@ -162,6 +162,57 @@ pub(crate) enum HandshakePhase {
|
||||
SentMsg2,
|
||||
}
|
||||
|
||||
/// 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 for every (leg, machine) pairing that
|
||||
/// rests in the pending-connection view. On XX three arms are
|
||||
/// production-reachable in that view (verified against the pre-collapse leg
|
||||
/// display):
|
||||
/// - `Handshaking{SentMsg1}` → `"sent_msg1"` (outbound identified leg).
|
||||
/// - `Handshaking{SentMsg2}` → `"received_msg1"` (the inbound responder leg,
|
||||
/// which parks at `SentMsg2` after replying to msg1 while the deleted leg
|
||||
/// phase displayed `received_msg1` — a direction-aware synthesis with no leg
|
||||
/// twin).
|
||||
/// - `Discovered` → `"sent_msg1"` (GAP A: the anonymous-outbound leg rests with
|
||||
/// its machine parked at `Discovered` while its leg is already at `SentMsg1`;
|
||||
/// the anon path sends msg1 inline and dispatches no handshake-start event, so
|
||||
/// advancing the machine to `Handshaking{SentMsg1}` here would arm a retransmit
|
||||
/// timer the anon leg does not have — the overload is the neutral choice).
|
||||
///
|
||||
/// The `send_failed` → `"failed"` override is applied by the caller
|
||||
/// ([`displayed_handshake_state`](PeerMachine::displayed_handshake_state)). The
|
||||
/// remaining arms are kept total for a complete mapping and are not reachable in
|
||||
/// the view.
|
||||
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::SentMsg2,
|
||||
..
|
||||
} => "received_msg1",
|
||||
// GAP A: the anonymous-outbound leg rests in the view with its machine
|
||||
// parked at `Discovered` while the leg itself is at `SentMsg1`, so it
|
||||
// displayed `"sent_msg1"` before the collapse. The only in-view
|
||||
// `Discovered`-with-leg case is this anon leg (identified dials leave
|
||||
// `Discovered` atomically within their handler before any tick), so the
|
||||
// overload preserves display parity byte-for-byte.
|
||||
PeerState::Discovered => "sent_msg1",
|
||||
PeerState::Established { .. }
|
||||
| PeerState::Active { .. }
|
||||
| PeerState::Maintaining { .. }
|
||||
| PeerState::Closing { .. } => "complete",
|
||||
PeerState::Failed { .. } => "failed",
|
||||
PeerState::Connecting { .. } | PeerState::Closed { .. } => "initial",
|
||||
}
|
||||
}
|
||||
|
||||
/// Which maintenance sub-machine `Maintaining` is running.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum MaintainKind {
|
||||
@@ -461,6 +512,13 @@ pub(crate) struct PeerMachine {
|
||||
conn: ConnectionState,
|
||||
/// Remote startup epoch (establish-path-only; NOT in send-state).
|
||||
remote_epoch: Option<[u8; 8]>,
|
||||
/// 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,
|
||||
@@ -509,6 +567,7 @@ impl PeerMachine {
|
||||
None => ConnectionState::outbound_anonymous(link, now),
|
||||
},
|
||||
remote_epoch: None,
|
||||
send_failed: false,
|
||||
rekey_in_progress: false,
|
||||
rekey_our_index: None,
|
||||
rekey_msg1: None,
|
||||
@@ -537,6 +596,7 @@ impl PeerMachine {
|
||||
leg: None,
|
||||
conn: ConnectionState::inbound(link, now),
|
||||
remote_epoch: None,
|
||||
send_failed: false,
|
||||
rekey_in_progress: false,
|
||||
rekey_our_index: None,
|
||||
rekey_msg1: None,
|
||||
@@ -605,6 +665,7 @@ impl PeerMachine {
|
||||
leg: None,
|
||||
conn,
|
||||
remote_epoch,
|
||||
send_failed: false,
|
||||
rekey_in_progress: false,
|
||||
rekey_our_index: None,
|
||||
rekey_msg1: None,
|
||||
@@ -708,6 +769,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<NodeAddr> {
|
||||
self.node_addr
|
||||
@@ -849,8 +945,11 @@ impl PeerMachine {
|
||||
/// actions are emitted.
|
||||
fn on_handshake_send_failed(&mut self) -> Vec<PeerAction> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -2872,7 +2971,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");
|
||||
@@ -2881,11 +2981,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());
|
||||
@@ -2893,6 +2999,78 @@ 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::SentMsg2,
|
||||
}),
|
||||
"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"
|
||||
);
|
||||
|
||||
// GAP A: the anonymous-outbound leg rests at `Discovered` while its leg
|
||||
// is at `SentMsg1`, so the pending-connection view displayed "sent_msg1".
|
||||
assert_eq!(handshake_state_str(PeerState::Discovered), "sent_msg1");
|
||||
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() {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user