Merge branch 'refactor-node' into refactor-node-next

Brings the handshake-leg deletion onto the XX line: the leg struct
and src/peer/connection.rs are gone, the Noise handles and crypto
methods now live on PeerMachine, and every leg accessor reads the
machine's own ConnectionState.

The incoming branch speaks Noise IK, this one speaks XX, so the crypto
methods could not be taken as they arrived. Git added the IK versions
to machine.rs with no conflict marker, and complete_handshake_msg3
-- which XX needs and IK has no counterpart for -- was absent
from the merged tree entirely. All four are re-expressed here: the
incoming structure (the Option<HandshakeCrypto> handle, the self.conn
reads, the borrow scoping that hoists carrier writes out of the leg
borrow) carrying this line's XX bodies. start_handshake is the one
that mattered most: its signature is identical on both lines, so it
compiled silently, and left alone it would have emitted an IK msg1
through the two-argument new_initiator and panicked every anonymous
dial on an expect for an identity XX does not have at that point. It
now uses the one-argument XX form and no expect.

handle_msg1 keeps this line's flow, not the incoming one's. The two
functions share a name but not a shape -- the admission gate moved
out of msg1 into msg3 when the line went to XX, and identity is
unknown until msg3. The machine is built above the crypto because
it now drives it, but it stays a local until the late insert, so a
rejected msg1 still leaves no registry trace and the drop-the-local
error arms are unchanged. Parking it at SentMsg2 after the index is
allocated needed a transition the birth constructor could not provide,
so that constructor now delegates to it.

Node::start_handshake is re-expressed the same way and for the same
reason: keeping the machine a local until all fallible setup has
succeeded preserves the existing error arms exactly, with no machine
disposal to add.

The two leg-to-carrier mirror blocks collapse. With one carrier
the writes they mirrored are self-assignment. Their content
is preserved: the completion touch comes from the relocated
complete_handshake, on the same clock, and the negotiated profile
from process_fmp_negotiation, which now takes the machine directly
and writes through set_conn_peer_profile.  Worth recording, because
the collapse was justified on a narrower claim that does not hold:
complete_handshake writes only the touch. It never wrote the profile
on either line. The two writes land on the same carrier at the same
point, so the collapse is neutral, but it is discharged jointly by
two methods rather than by complete_handshake alone.

Three inbound tests are dropped rather than carried. Each asserts
a property at msg1 that XX does not have there: an ACL decision, a
no-registry-trace guard on that decision, and an identity learn on the
carrier. Under XX msg1 is ephemeral-only, so none of the three has a
landing site at that step. A comment at each drop site records where
the property actually lives on this line. One of them was a deletion
here that the incoming branch had merely modified, so its removal
is this line's own prior decision rather than a new one. The same is
true of the craft_and_send_msg1 helper and of HandshakeSeed::inbound,
both left without callers once those tests went.

Also carried: peer_actions.rs had no conflict and no incoming change,
but three sites reached deleted Node accessors and are repointed at
the machine; the EstablishView snapshot is not resurrected, as this
line has never had it; and the connection-state doc keeps this line's
XX wording with the registry-independence invariant grafted in.
This commit is contained in:
Johnathan Corgan
2026-07-19 02:42:24 +00:00
25 changed files with 1867 additions and 1369 deletions
+103 -92
View File
@@ -15,8 +15,7 @@ use crate::node::acl::PeerAclContext;
use crate::node::dataplane::PeerActionCtx;
use crate::nostr::{BootstrapEvent, NostrRendezvous};
use crate::nostr::{BootstrapHandoffResult, EstablishedTraversal};
use crate::peer::PeerConnection;
use crate::peer::machine::{PeerEvent, PeerMachine};
use crate::peer::machine::{HandshakeCrypto, PeerEvent, PeerMachine};
use crate::proto::fmp::wire::build_msg1;
use crate::proto::fmp::{Disconnect, DisconnectReason};
use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel};
@@ -372,27 +371,28 @@ impl Node {
}
fn is_connecting_to_peer(&self, peer_node_addr: &NodeAddr) -> bool {
self.connections().any(|conn| {
conn.expected_identity()
self.connections().any(|(_, machine)| {
machine
.conn_expected_identity()
.map(|id| id.node_addr() == peer_node_addr)
.unwrap_or(false)
})
}
fn is_connecting_to_peer_on_path(
pub(in crate::node) fn is_connecting_to_peer_on_path(
&self,
peer_node_addr: &NodeAddr,
transport_id: TransportId,
remote_addr: &TransportAddr,
) -> bool {
self.peer_machines.values().any(|machine| {
machine.leg().is_some_and(|conn| {
conn.expected_identity()
machine.leg().is_some()
&& machine
.conn_expected_identity()
.map(|id| id.node_addr() == peer_node_addr)
.unwrap_or(false)
&& machine.conn_transport_id() == Some(transport_id)
&& conn.source_addr() == Some(remote_addr)
})
&& machine.conn_transport_id() == Some(transport_id)
&& machine.conn_source_addr() == Some(remote_addr)
}) || self.peering.pending_connects.iter().any(|pending| {
pending
.peer_identity
@@ -607,9 +607,22 @@ impl Node {
) -> Result<(), NodeError> {
let peer_node_addr = *peer_identity.node_addr();
// Create connection in handshake phase (outbound knows expected identity)
let current_time_ms = Self::now_ms();
let mut connection = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
// The control machine drives the handshake, so it takes the crypto
// carrier before the crypto runs. The machine was born at dial and
// persisted in `initiate_connection`, so every live caller already has
// one; recover with a fresh one if a direct caller ever skips the dial.
debug_assert!(
self.peer_machines.contains_key(&link_id),
"outbound msg1 prepared for link {link_id} with no dial-time machine"
);
self.peer_machines
.entry(link_id)
.or_insert_with(|| {
PeerMachine::new_outbound(link_id, Some(peer_identity), current_time_ms)
})
.set_leg(HandshakeCrypto::new());
// Allocate a session index for this handshake
let our_index = match self.index_allocator.allocate() {
@@ -626,23 +639,30 @@ impl Node {
// Start the Noise handshake and get message 1
let our_keypair = self.identity().keypair();
let noise_msg1 =
match connection.start_handshake(our_keypair, self.startup_epoch(), current_time_ms) {
Ok(msg) => msg,
Err(e) => {
// Clean up the index, link, and dial-time machine
let _ = self.index_allocator.free(our_index);
self.links.remove(&link_id);
self.addr_to_link
.remove(&(transport_id, remote_addr.clone()));
self.remove_peer_machine(link_id);
return Err(NodeError::HandshakeFailed(e.to_string()));
}
};
let startup_epoch = self.startup_epoch();
let noise_msg1 = match self
.peer_machines
.get_mut(&link_id)
.expect("dial-time machine carries the connection")
.start_handshake(our_keypair, startup_epoch, current_time_ms)
{
Ok(msg) => msg,
Err(e) => {
// Clean up the index, link, and dial-time machine
let _ = self.index_allocator.free(our_index);
self.links.remove(&link_id);
self.addr_to_link
.remove(&(transport_id, remote_addr.clone()));
self.remove_peer_machine(link_id);
return Err(NodeError::HandshakeFailed(e.to_string()));
}
};
// Set index and transport info on the connection
connection.set_our_index(our_index);
connection.set_source_addr(remote_addr.clone());
self.peer_machines
.get_mut(&link_id)
.expect("dial-time machine carries the connection")
.set_conn_source_addr(remote_addr.clone());
// Build wire format msg1: [0x01][sender_idx:4 LE][noise_msg1:82]
let wire_msg1 = build_msg1(our_index, &noise_msg1);
@@ -665,20 +685,13 @@ impl Node {
self.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
// The dial-born machine (persisted in `initiate_connection`) carries
// the prepared connection from here. Every live caller dialed first,
// so the machine exists; recover with a fresh one if a direct caller
// ever skips the dial.
debug_assert!(
self.peer_machines.contains_key(&link_id),
"outbound msg1 prepared for link {link_id} with no dial-time machine"
);
let machine = self.peer_machines.entry(link_id).or_insert_with(|| {
PeerMachine::new_outbound(link_id, Some(peer_identity), current_time_ms)
});
let machine = self
.peer_machines
.get_mut(&link_id)
.expect("dial-time machine carries the connection");
// The dial-born machine carrier was stamped at dial; re-stamp it with the
// leg's msg1-prep clock so the surviving `started_at`/`last_activity`
// carry the leg's provenance. The two clocks differ when a connect
// msg1-prep clock so the surviving `started_at`/`last_activity` carry
// the preparation's provenance. The two clocks differ when a connect
// round-trip separates dial from msg1 preparation.
machine.set_conn_started_at(current_time_ms);
machine.touch_conn(current_time_ms);
@@ -686,14 +699,12 @@ impl Node {
// projects it to the promotion hand-off); holds even if a direct caller
// reached here without the dial-time `on_dial` write.
machine.set_conn_transport_id(transport_id);
// Record our session index on the surviving carrier the same index just
// written on the leg above — so the carrier is the single index home on
// the outbound path (the inbound path writes it at authorize).
// Record our session index on the surviving carrier, the single index
// home on the outbound path (the inbound path writes it at authorize).
machine.set_conn_our_index(our_index);
// Store the msg1 wire on the surviving carrier (the leg no longer holds
// the resend source); the retransmit driver reads it from here.
// Store the msg1 wire on the surviving carrier (the connection does not
// hold the resend source); the retransmit driver reads it from here.
machine.set_conn_handshake_msg1(wire_msg1, first_resend_at_ms);
machine.set_leg(connection);
Ok(())
}
@@ -712,14 +723,17 @@ impl Node {
remote_addr: TransportAddr,
peer_identity: Option<PeerIdentity>,
) -> Result<(), NodeError> {
// Create connection in handshake phase. Anonymous discovery
// Create the control machine in handshake phase. Anonymous discovery
// (no peer_identity) leaves identity to be learned from XX msg2.
//
// The machine drives the Noise crypto, so it is built before the leaf
// runs — but it stays a LOCAL until all fallible setup has succeeded.
// The index-alloc and Noise Err returns below therefore still just drop
// it, leaving no registry trace and needing no disposal, exactly as the
// pre-collapse local connection did.
let current_time_ms = Self::now_ms();
let mut connection = if let Some(identity) = peer_identity {
PeerConnection::outbound(link_id, identity, current_time_ms)
} else {
PeerConnection::outbound_anonymous(link_id, current_time_ms)
};
let mut machine = PeerMachine::new_outbound(link_id, peer_identity, current_time_ms);
machine.set_leg(HandshakeCrypto::new());
// Allocate a session index for this handshake
let our_index = match self.index_allocator.allocate() {
@@ -735,7 +749,7 @@ impl Node {
// Start the Noise handshake and get message 1
let our_keypair = self.identity().keypair();
let noise_msg1 =
match connection.start_handshake(our_keypair, self.startup_epoch(), current_time_ms) {
match machine.start_handshake(our_keypair, self.startup_epoch(), current_time_ms) {
Ok(msg) => msg,
Err(e) => {
// Clean up the index and link
@@ -746,15 +760,15 @@ impl Node {
}
};
// Set index and transport info on the connection
connection.set_our_index(our_index);
connection.set_transport_id(transport_id);
connection.set_source_addr(remote_addr.clone());
// Set index and transport info on the surviving carrier
machine.set_conn_our_index(our_index);
machine.set_conn_transport_id(transport_id);
machine.set_conn_source_addr(remote_addr.clone());
// Build wire format msg1: [0x01][sender_idx:4 LE][noise_msg1:82]
let wire_msg1 = build_msg1(our_index, &noise_msg1);
if let Some(id) = connection.expected_identity() {
if let Some(id) = machine.conn_expected_identity() {
debug!(
peer = %self.peer_display_name(id.node_addr()),
transport_id = %transport_id,
@@ -773,37 +787,28 @@ impl Node {
);
}
// Store msg1 for resend and schedule first resend
// Store msg1 for resend and schedule first resend. The carrier holds
// the resend wire; the retransmit driver reads it from there.
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
let first_resend_at_ms = current_time_ms + resend_interval;
connection.set_handshake_msg1(wire_msg1.clone(), first_resend_at_ms);
machine.set_conn_handshake_msg1(wire_msg1.clone(), first_resend_at_ms);
// Track in pending_outbound for msg2 dispatch
self.pending_outbound
.insert((transport_id, our_index.as_u32()), link_id);
// The leg's persistent control machine is born carrying its pending
// connection, after all fallible setup (the index-alloc and Noise Err
// returns above predate it and need no disposal). Both production
// callers dial anonymously (identified dials persist their machine at
// dial and go through `prepare_outbound_msg1`), so the machine starts
// identity-less; `handle_msg2` crystallizes the identity the XX
// handshake learns. Inserted before the send below so no suspension
// point observes a leg in flight without a machine. The send-failure
// arm retains the failed connection, and the machine keeps carrying
// it — the stale-connection reaper disposes both together. No timers
// are armed and no event is dispatched: this path sends msg1 inline,
// so the machine parks at `Discovered` until msg2.
let mut machine = PeerMachine::new_outbound(link_id, peer_identity, current_time_ms);
// Seed the surviving carrier with the leg's msg1-prep provenance so the
// started_at/last_activity timestamps, the transport ID, our index, and
// the msg1 resend wire all live on the carrier — the promotion hand-off
// and the retransmit driver read them there, not the leg.
machine.set_conn_started_at(current_time_ms);
machine.touch_conn(current_time_ms);
machine.set_conn_transport_id(transport_id);
machine.set_conn_our_index(our_index);
machine.set_conn_handshake_msg1(wire_msg1.clone(), first_resend_at_ms);
machine.set_leg(connection);
// The persistent control machine enters the registry here, after all
// fallible setup. Both production callers dial anonymously (identified
// dials persist their machine at dial and go through
// `prepare_outbound_msg1`), so the machine starts identity-less;
// `handle_msg2` crystallizes the identity the XX handshake learns.
// Inserted before the send below so no suspension point observes a
// handshake in flight without a machine. The send-failure arm retains
// the failed handshake and the machine keeps carrying it — the
// stale-connection reaper disposes both together. No timers are armed
// and no event is dispatched: this path sends msg1 inline, so the
// machine parks at `Discovered` until msg2. The carrier was born on
// this same `current_time_ms`, so its `started_at`/`last_activity`
// already carry the msg1-prep provenance.
self.peer_machines.insert(link_id, machine);
// Send the wire format handshake message
@@ -866,7 +871,11 @@ impl Node {
Some(w) => w.to_vec(),
None => return,
};
let our_index = self.leg(&link_id).and_then(|c| c.our_index());
let our_index = self
.peer_machines
.get(&link_id)
.filter(|machine| machine.leg().is_some())
.and_then(|machine| machine.our_index());
// Send the wire format handshake message
if let Some(transport) = self.transports.get(&transport_id) {
@@ -1130,12 +1139,13 @@ impl Node {
let now_ms = Self::now_ms();
let stale: Vec<LinkId> = self
.connections()
.filter(|conn| {
conn.expected_identity()
.filter(|(_, machine)| {
machine
.conn_expected_identity()
.map(|id| id.node_addr() == &peer_addr)
.unwrap_or(false)
})
.map(|conn| conn.link_id())
.map(|(_, machine)| machine.link_id())
.collect();
for link_id in stale {
self.cleanup_stale_connection(link_id, now_ms);
@@ -2959,11 +2969,11 @@ impl Node {
let connected: HashSet<NodeAddr> = self.peers.keys().copied().collect();
let connecting: HashSet<NodeAddr> = self
.connections()
.filter_map(|conn| conn.expected_identity().map(|id| *id.node_addr()))
.filter_map(|(_, machine)| machine.conn_expected_identity().map(|id| *id.node_addr()))
.collect();
let mut in_flight_by_peer: HashMap<NodeAddr, usize> = HashMap::new();
for conn in self.connections() {
if let Some(id) = conn.expected_identity() {
for (_, machine) in self.connections() {
if let Some(id) = machine.conn_expected_identity() {
*in_flight_by_peer.entry(*id.node_addr()).or_default() += 1;
}
}
@@ -3035,8 +3045,9 @@ impl Node {
let in_flight_for_peer = self
.connections()
.filter(|conn| {
conn.expected_identity()
.filter(|(_, machine)| {
machine
.conn_expected_identity()
.map(|identity| identity.node_addr() == peer_node_addr)
.unwrap_or(false)
})