node: populate the per-peer machine map at establishment

Insert a per-peer machine into peer_machines at each promote site and
remove it on teardown, so every established peer has exactly one machine
entry keyed by its link id. Nothing drives the machine yet — the entry
is inert — but this correspondence is what the rekey and liveness-reap
folds depend on.

Inserts pair with the two promote_connection arms (normal promote and
cross-connection-won); removes pair with remove_active_peer and the
cross-connection loser teardown. A new PeerMachine::established
constructor parks the machine in the post-handshake Established state.
Behavior is unchanged: nothing reads the map on a live path.
This commit is contained in:
Johnathan Corgan
2026-07-13 20:33:29 +00:00
parent 42de582ef0
commit 5498b2a6a3
4 changed files with 120 additions and 0 deletions
+5
View File
@@ -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);
}
+36
View File
@@ -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
+8
View File
@@ -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<ActivePeer> {
self.peers.remove(node_addr)
}
+71
View File
@@ -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() {