diff --git a/src/node/dataplane/dispatch.rs b/src/node/dataplane/dispatch.rs index 2655e81..76f1c60 100644 --- a/src/node/dataplane/dispatch.rs +++ b/src/node/dataplane/dispatch.rs @@ -187,6 +187,11 @@ impl Node { // Remove link and address mapping self.remove_link(&link_id); + // Finding A: drop this peer's inert machine, keyed by the link_id + // derived above (a peer's link_id is immutable, so the key never + // moved). Keeps peers <-> peer_machines in exact correspondence on + // teardown. NEUTRAL — nothing reads peer_machines yet. + self.peer_machines.remove(&link_id); if let Some(transport_id) = transport_id { self.cleanup_bootstrap_transport_if_unused(transport_id); } diff --git a/src/node/handlers/handshake.rs b/src/node/handlers/handshake.rs index ebffb1f..fbf1762 100644 --- a/src/node/handlers/handshake.rs +++ b/src/node/handlers/handshake.rs @@ -9,6 +9,7 @@ use crate::PeerIdentity; use crate::node::acl::PeerAclContext; use crate::node::reject::{HandshakeReject, RejectReason}; use crate::node::{Node, NodeError}; +use crate::peer::machine::PeerMachine; use crate::peer::{ActivePeer, PeerConnection}; use crate::proto::fmp::wire::{Msg1Header, Msg2Header, Msg3Header, build_msg2, build_msg3}; use crate::proto::fmp::{ @@ -1538,6 +1539,13 @@ impl Node { }; let loser_link_id = old_peer.link_id(); + // Finding A: the replaced (losing) peer was established and so + // carried a machine keyed by its OWN link_id (loser_link_id); + // drop it so no machine orphans when its ActivePeer is removed. + // The winning connection's machine is inserted below keyed by + // the winner link_id. NEUTRAL — nothing reads peer_machines yet. + self.peer_machines.remove(&loser_link_id); + // Clean up old peer's index from peers_by_index if let (Some(old_tid), Some(old_idx)) = (old_peer.transport_id(), old_peer.our_index()) @@ -1595,6 +1603,20 @@ impl Node { ); self.peers.insert(peer_node_addr, new_peer); + // Finding A: populate the inert per-peer machine so every + // established peer has exactly one machine, keyed by its + // link_id (the winner link here). Nothing drives it yet. + self.peer_machines.insert( + link_id, + PeerMachine::established( + link_id, + verified_identity, + our_index, + is_outbound, + remote_epoch, + current_time_ms, + ), + ); self.peers_by_index .insert((transport_id, our_index.as_u32()), peer_node_addr); self.peering @@ -1712,6 +1734,20 @@ impl Node { } self.peers.insert(peer_node_addr, new_peer); + // Finding A: populate the inert per-peer machine so every + // established peer has exactly one machine, keyed by its link_id. + // Nothing drives it yet. + self.peer_machines.insert( + link_id, + PeerMachine::established( + link_id, + verified_identity, + our_index, + is_outbound, + remote_epoch, + current_time_ms, + ), + ); self.peers_by_index .insert((transport_id, our_index.as_u32()), peer_node_addr); self.peering diff --git a/src/node/mod.rs b/src/node/mod.rs index 6d52dbb..7fe26af 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -2388,6 +2388,14 @@ impl Node { } /// Remove a peer. + /// + /// Raw `pub` accessor with ZERO callers in the crate (production or test) — + /// the production teardown path is [`remove_active_peer`](Self::remove_active_peer), + /// which drops the peer's `peer_machines` entry. This helper deliberately + /// does NOT touch `peer_machines`: it never runs against an established peer + /// in production, so it cannot orphan a machine (Finding A). If it is ever + /// wired to remove established peers, mirror the `remove_active_peer` cleanup + /// (`self.peer_machines.remove(&peer.link_id())`) here. pub fn remove_peer(&mut self, node_addr: &NodeAddr) -> Option { self.peers.remove(node_addr) } diff --git a/src/peer/machine.rs b/src/peer/machine.rs index 7aece04..da1f383 100644 --- a/src/peer/machine.rs +++ b/src/peer/machine.rs @@ -448,6 +448,53 @@ impl PeerMachine { } } + /// New machine for an ALREADY-established peer: the post-handshake state a + /// promoted peer occupies before any rekey. M3 inserts one of these into + /// `Node.peer_machines` at each `promote_connection` establishment site so + /// every established peer has exactly one machine keyed by its `LinkId` + /// (Finding A). The machine is **inert** — nothing drives it yet — and is + /// parked at [`PeerState::Established`] so a later reap sees + /// [`is_established_context`](Self::is_established_context) true and a later + /// rekey step finds it. `our_index` is the peer's msg1-allocated session + /// index; `remote_epoch` is the crystallized peer's startup epoch. + pub(crate) fn established( + link: LinkId, + identity: PeerIdentity, + our_index: SessionIndex, + is_outbound: bool, + remote_epoch: Option<[u8; 8]>, + now: u64, + ) -> Self { + let addr = *identity.node_addr(); + let mut conn = if is_outbound { + ConnectionState::outbound(link, identity, now) + } else { + ConnectionState::inbound(link, now) + }; + conn.set_our_index(our_index); + conn.set_remote_epoch(remote_epoch); + Self { + state: PeerState::Established { addr }, + link, + identity: Some(identity), + node_addr: Some(addr), + conn, + remote_epoch, + rekey_in_progress: false, + rekey_our_index: None, + rekey_msg1: None, + rekey_resend_count: 0, + last_peer_rekey_ms: 0, + rekey_msg3_pending: false, + session_established_at_ms: now, + authenticated_at_ms: now, + rekey_jitter_secs: 0, + last_heartbeat_sent_ms: 0, + our_index: Some(our_index), + draining_index: None, + } + } + /// Current lifecycle state. pub(crate) fn state(&self) -> PeerState { self.state @@ -1267,6 +1314,30 @@ mod tests { } } + // ---- Test 0: established constructor (Finding A populate) -------------- + #[test] + fn established_constructor_yields_established_context() { + let id = peer_identity(); + let addr = *id.node_addr(); + let idx = SessionIndex::new(0x4242); + let m = PeerMachine::established( + LinkId::new(7), + id, + idx, + /* is_outbound */ true, + None, + 1_234, + ); + + // Parked at Established with the crystallized address + index visible, + // so a later reap's `is_established_context` and a later rekey both + // find it. + assert_eq!(m.state(), PeerState::Established { addr }); + assert!(m.is_established_context()); + assert_eq!(m.addr(), Some(addr)); + assert_eq!(m.our_index(), Some(idx)); + } + // ---- Test 1: rekey initiator cutover ---------------------------------- #[test] fn rekey_initiator_cutover() {