Skip to main content

fips_core/peer/
active.rs

1//! Active Peer (Authenticated Phase)
2//!
3//! Represents a fully authenticated peer after successful Noise handshake.
4//! ActivePeer holds tree state, Bloom filter, and routing information.
5
6use crate::bloom::BloomFilter;
7use crate::mmp::MmpConfig;
8use crate::node::REKEY_JITTER_SECS;
9use crate::noise::{HandshakeState as NoiseHandshakeState, NoiseError, NoiseSession};
10use crate::transport::{LinkId, LinkStats, TransportAddr, TransportId};
11use crate::tree::{ParentDeclaration, TreeCoordinate};
12use crate::utils::index::SessionIndex;
13use crate::{FipsAddress, NodeAddr, PeerIdentity};
14use rand::RngExt;
15use secp256k1::XOnlyPublicKey;
16use std::fmt;
17use std::time::{Duration, Instant};
18
19fn draw_rekey_jitter() -> i64 {
20    rand::rng().random_range(-REKEY_JITTER_SECS..=REKEY_JITTER_SECS)
21}
22
23/// Connectivity state for an active peer.
24///
25/// This is simpler than the full PeerState since authentication is complete.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum ConnectivityState {
28    /// Peer is fully connected and responsive.
29    Connected,
30    /// Peer hasn't been heard from recently (potential timeout).
31    Stale,
32    /// Connection lost, attempting to reconnect.
33    Reconnecting,
34    /// Peer has been explicitly disconnected.
35    Disconnected,
36}
37
38impl ConnectivityState {
39    /// Check if the peer is usable for sending traffic.
40    pub fn can_send(&self) -> bool {
41        matches!(
42            self,
43            ConnectivityState::Connected | ConnectivityState::Stale
44        )
45    }
46
47    /// Check if this is a terminal state requiring cleanup.
48    pub fn is_terminal(&self) -> bool {
49        matches!(self, ConnectivityState::Disconnected)
50    }
51
52    /// Check if peer is fully healthy.
53    pub fn is_healthy(&self) -> bool {
54        matches!(self, ConnectivityState::Connected)
55    }
56}
57
58impl fmt::Display for ConnectivityState {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        let s = match self {
61            ConnectivityState::Connected => "connected",
62            ConnectivityState::Stale => "stale",
63            ConnectivityState::Reconnecting => "reconnecting",
64            ConnectivityState::Disconnected => "disconnected",
65        };
66        write!(f, "{}", s)
67    }
68}
69
70/// A fully authenticated remote FIPS node.
71///
72/// Created only after successful Noise KK handshake. The identity is
73/// cryptographically verified at this point.
74///
75/// Note: ActivePeer intentionally does not implement Clone because it
76/// contains NoiseSession, which cannot be safely cloned (cloning would
77/// risk nonce reuse, a catastrophic security failure).
78#[derive(Debug)]
79pub struct ActivePeer {
80    // === Identity (Verified) ===
81    /// Cryptographic identity (verified via handshake).
82    identity: PeerIdentity,
83
84    // === Connection ===
85    /// Link used to reach this peer.
86    link_id: LinkId,
87    /// Current connectivity state.
88    connectivity: ConnectivityState,
89
90    // === Session (Wire Protocol) ===
91    /// Noise session for encryption/decryption (None if legacy peer).
92    noise_session: Option<NoiseSession>,
93    /// Our session index (they include this when sending TO us).
94    our_index: Option<SessionIndex>,
95    /// Their session index (we include this when sending TO them).
96    their_index: Option<SessionIndex>,
97    /// Transport ID for this peer's link.
98    transport_id: Option<TransportId>,
99    /// Current transport address (for roaming support).
100    current_addr: Option<TransportAddr>,
101
102    // === Spanning Tree ===
103    /// Their latest parent declaration.
104    declaration: Option<ParentDeclaration>,
105    /// Their path to root.
106    ancestry: Option<TreeCoordinate>,
107
108    // === Tree Announce Rate Limiting ===
109    /// Minimum interval between TreeAnnounce messages (milliseconds).
110    tree_announce_min_interval_ms: u64,
111    /// Last time we sent a TreeAnnounce to this peer (Unix milliseconds).
112    last_tree_announce_sent_ms: u64,
113    /// Whether a tree announce is pending (deferred due to rate limit).
114    pending_tree_announce: bool,
115
116    // === Bloom Filter ===
117    /// What's reachable through them (inbound filter).
118    inbound_filter: Option<BloomFilter>,
119    /// Their filter's sequence number.
120    filter_sequence: u64,
121    /// When we received their last filter (Unix milliseconds).
122    filter_received_at: u64,
123    /// Whether we owe them a filter update.
124    pending_filter_update: bool,
125
126    // === Timing ===
127    /// Session start time for computing session-relative timestamps.
128    /// Used as the epoch for the 4-byte inner header timestamp field.
129    session_start: Instant,
130    /// Local current-session generation for dataplane owner state.
131    session_generation: u64,
132
133    // === Statistics ===
134    /// Link statistics.
135    link_stats: LinkStats,
136    /// When this peer was authenticated (Unix milliseconds).
137    authenticated_at: u64,
138    /// When this peer was last seen (any activity, Unix milliseconds).
139    last_seen: u64,
140
141    // === Epoch (Restart Detection) ===
142    /// Remote peer's startup epoch (from handshake). Used to detect restarts.
143    remote_epoch: Option<[u8; 8]>,
144
145    // === MMP ===
146    /// Role used when seeding PM2's adjacent-link MMP owner state.
147    fmp_mmp_is_initiator: bool,
148
149    // === Heartbeat ===
150    /// When we last sent a heartbeat to this peer.
151    last_heartbeat_sent: Option<Instant>,
152
153    // === Handshake Resend ===
154    /// Wire-format msg2 for resend on duplicate msg1 (responder only).
155    /// Cleared after the handshake timeout window.
156    handshake_msg2: Option<Vec<u8>>,
157
158    // === Replay Detection Suppression ===
159    /// Number of replay detections suppressed since last session reset.
160    replay_suppressed_count: u32,
161    /// Consecutive decryption failures (reset on any successful decrypt).
162    consecutive_decrypt_failures: u32,
163
164    // === Rekey (Key Rotation) ===
165    /// When the current Noise session was established (for rekey timer).
166    session_established_at: Instant,
167    /// Per-session symmetric jitter applied to the rekey timer trigger.
168    rekey_jitter_secs: i64,
169    /// Current K-bit epoch value (alternates each rekey).
170    current_k_bit: bool,
171    /// Previous session kept alive during drain window after cutover.
172    previous_session: Option<NoiseSession>,
173    /// Previous session's our_index (for registry cleanup on drain expiry).
174    previous_our_index: Option<SessionIndex>,
175    /// When the drain window started (None = no drain in progress).
176    drain_started: Option<Instant>,
177    /// Pending new session from completed rekey (before K-bit cutover).
178    pending_new_session: Option<NoiseSession>,
179    /// Pending new session's our_index.
180    pending_our_index: Option<SessionIndex>,
181    /// Pending new session's their_index.
182    pending_their_index: Option<SessionIndex>,
183    /// True when this node initiated the pending FMP rekey.
184    pending_rekey_initiator: bool,
185    /// When the pending FMP rekey completed locally.
186    pending_rekey_completed_at: Option<Instant>,
187    /// Whether a rekey is currently in progress (handshake sent, not yet complete).
188    rekey_in_progress: bool,
189    /// When we last received a rekey msg1 from this peer (dampening).
190    last_peer_rekey: Option<Instant>,
191    /// In-progress rekey: Noise handshake state (initiator only).
192    rekey_handshake: Option<NoiseHandshakeState>,
193    /// In-progress rekey: our new session index.
194    rekey_our_index: Option<SessionIndex>,
195    /// In-progress rekey: wire-format msg1 for resend.
196    rekey_msg1: Option<Vec<u8>>,
197    /// In-progress rekey: next resend timestamp (Unix ms).
198    rekey_msg1_next_resend: u64,
199    /// In-progress rekey: number of msg1 retransmissions performed so far.
200    rekey_msg1_resend_count: u32,
201}
202
203impl ActivePeer {
204    /// Create a new active peer from verified identity.
205    ///
206    /// Called after successful authentication handshake.
207    /// For peers with Noise sessions, use `with_session` instead.
208    pub fn new(identity: PeerIdentity, link_id: LinkId, authenticated_at: u64) -> Self {
209        let now = Instant::now();
210        Self {
211            identity,
212            link_id,
213            connectivity: ConnectivityState::Connected,
214            noise_session: None,
215            our_index: None,
216            their_index: None,
217            transport_id: None,
218            current_addr: None,
219            declaration: None,
220            ancestry: None,
221            tree_announce_min_interval_ms: 500,
222            last_tree_announce_sent_ms: 0,
223            pending_tree_announce: false,
224            inbound_filter: None,
225            filter_sequence: 0,
226            filter_received_at: 0,
227            pending_filter_update: true, // Send filter on new connection
228            session_start: now,
229            session_generation: authenticated_at.max(1),
230            link_stats: LinkStats::new(),
231            authenticated_at,
232            last_seen: authenticated_at,
233            remote_epoch: None,
234            fmp_mmp_is_initiator: false,
235            last_heartbeat_sent: None,
236            handshake_msg2: None,
237            replay_suppressed_count: 0,
238            consecutive_decrypt_failures: 0,
239            session_established_at: now,
240            rekey_jitter_secs: draw_rekey_jitter(),
241            current_k_bit: false,
242            previous_session: None,
243            previous_our_index: None,
244            drain_started: None,
245            pending_new_session: None,
246            pending_our_index: None,
247            pending_their_index: None,
248            pending_rekey_initiator: false,
249            pending_rekey_completed_at: None,
250            rekey_in_progress: false,
251            last_peer_rekey: None,
252            rekey_handshake: None,
253            rekey_our_index: None,
254            rekey_msg1: None,
255            rekey_msg1_next_resend: 0,
256            rekey_msg1_resend_count: 0,
257        }
258    }
259
260    /// Create from verified identity with existing link stats.
261    ///
262    /// Used when promoting from PeerConnection, preserving handshake stats.
263    /// For peers with Noise sessions, use `with_session` instead.
264    pub fn with_stats(
265        identity: PeerIdentity,
266        link_id: LinkId,
267        authenticated_at: u64,
268        link_stats: LinkStats,
269    ) -> Self {
270        let mut peer = Self::new(identity, link_id, authenticated_at);
271        peer.link_stats = link_stats;
272        peer
273    }
274
275    /// Create from verified identity with Noise session and index tracking.
276    ///
277    /// This is the primary constructor for the wire protocol path.
278    /// The NoiseSession provides encryption/decryption and replay protection.
279    #[allow(clippy::too_many_arguments)]
280    pub fn with_session(
281        identity: PeerIdentity,
282        link_id: LinkId,
283        authenticated_at: u64,
284        noise_session: NoiseSession,
285        our_index: SessionIndex,
286        their_index: SessionIndex,
287        transport_id: TransportId,
288        current_addr: TransportAddr,
289        link_stats: LinkStats,
290        is_initiator: bool,
291        _mmp_config: &MmpConfig,
292        remote_epoch: Option<[u8; 8]>,
293    ) -> Self {
294        let now = Instant::now();
295        Self {
296            identity,
297            link_id,
298            connectivity: ConnectivityState::Connected,
299            noise_session: Some(noise_session),
300            our_index: Some(our_index),
301            their_index: Some(their_index),
302            transport_id: Some(transport_id),
303            current_addr: Some(current_addr),
304            declaration: None,
305            ancestry: None,
306            tree_announce_min_interval_ms: 500,
307            last_tree_announce_sent_ms: 0,
308            pending_tree_announce: false,
309            inbound_filter: None,
310            filter_sequence: 0,
311            filter_received_at: 0,
312            pending_filter_update: true,
313            session_start: now,
314            session_generation: authenticated_at.max(1),
315            link_stats,
316            authenticated_at,
317            last_seen: authenticated_at,
318            remote_epoch,
319            fmp_mmp_is_initiator: is_initiator,
320            last_heartbeat_sent: None,
321            handshake_msg2: None,
322            replay_suppressed_count: 0,
323            consecutive_decrypt_failures: 0,
324            session_established_at: now,
325            rekey_jitter_secs: draw_rekey_jitter(),
326            current_k_bit: false,
327            previous_session: None,
328            previous_our_index: None,
329            drain_started: None,
330            pending_new_session: None,
331            pending_our_index: None,
332            pending_their_index: None,
333            pending_rekey_initiator: false,
334            pending_rekey_completed_at: None,
335            rekey_in_progress: false,
336            last_peer_rekey: None,
337            rekey_handshake: None,
338            rekey_our_index: None,
339            rekey_msg1: None,
340            rekey_msg1_next_resend: 0,
341            rekey_msg1_resend_count: 0,
342        }
343    }
344
345    // === Identity Accessors ===
346
347    /// Get the peer's verified identity.
348    pub fn identity(&self) -> &PeerIdentity {
349        &self.identity
350    }
351
352    /// Get the peer's NodeAddr.
353    pub fn node_addr(&self) -> &NodeAddr {
354        self.identity.node_addr()
355    }
356
357    /// Get the peer's FIPS address.
358    pub fn address(&self) -> &FipsAddress {
359        self.identity.address()
360    }
361
362    /// Get the peer's public key.
363    pub fn pubkey(&self) -> XOnlyPublicKey {
364        self.identity.pubkey()
365    }
366
367    /// Get the peer's npub string.
368    pub fn npub(&self) -> String {
369        self.identity.npub()
370    }
371
372    // === Connection Accessors ===
373
374    /// Get the link ID.
375    pub fn link_id(&self) -> LinkId {
376        self.link_id
377    }
378
379    /// Get the connectivity state.
380    pub fn connectivity(&self) -> ConnectivityState {
381        self.connectivity
382    }
383
384    /// Check if peer can receive traffic.
385    pub fn can_send(&self) -> bool {
386        self.connectivity.can_send()
387    }
388
389    /// Check if peer is fully healthy.
390    pub fn is_healthy(&self) -> bool {
391        self.connectivity.is_healthy()
392    }
393
394    /// Check if peer is disconnected.
395    pub fn is_disconnected(&self) -> bool {
396        self.connectivity.is_terminal()
397    }
398
399    // === Session Accessors ===
400
401    /// Check if this peer has a Noise session.
402    pub fn has_session(&self) -> bool {
403        self.noise_session.is_some()
404    }
405
406    /// Get the Noise session, if present.
407    pub fn noise_session(&self) -> Option<&NoiseSession> {
408        self.noise_session.as_ref()
409    }
410
411    /// Get mutable access to the Noise session.
412    pub fn noise_session_mut(&mut self) -> Option<&mut NoiseSession> {
413        self.noise_session.as_mut()
414    }
415
416    /// Get our session index (they use this to send TO us).
417    pub fn our_index(&self) -> Option<SessionIndex> {
418        self.our_index
419    }
420
421    /// Get their session index (we use this to send TO them).
422    pub fn their_index(&self) -> Option<SessionIndex> {
423        self.their_index
424    }
425
426    /// Update their session index (used during cross-connection resolution
427    /// when the losing node keeps its inbound session but needs the peer's
428    /// outbound index).
429    pub fn set_their_index(&mut self, index: SessionIndex) {
430        self.their_index = Some(index);
431    }
432
433    /// Replace the Noise session and indices during cross-connection resolution.
434    ///
435    /// When both nodes simultaneously initiate, each promotes its inbound
436    /// handshake first. When the peer's msg2 arrives, we learn the correct
437    /// session — the outbound handshake that pairs with the peer's inbound.
438    /// This replaces the entire session so both nodes use matching keys.
439    ///
440    /// Returns the old our_index so the caller can update session-index dispatch.
441    /// Also resets the replay suppression counter since the session changed.
442    pub fn replace_session(
443        &mut self,
444        new_session: NoiseSession,
445        new_our_index: SessionIndex,
446        new_their_index: SessionIndex,
447    ) -> Option<SessionIndex> {
448        self.reset_replay_suppressed();
449        let old_our_index = self.our_index;
450        self.noise_session = Some(new_session);
451        self.our_index = Some(new_our_index);
452        self.their_index = Some(new_their_index);
453        old_our_index
454    }
455
456    /// Get the transport ID for this peer.
457    pub fn transport_id(&self) -> Option<TransportId> {
458        self.transport_id
459    }
460
461    /// Get the current transport address.
462    pub fn current_addr(&self) -> Option<&TransportAddr> {
463        self.current_addr.as_ref()
464    }
465
466    /// Update the current address (for roaming support).
467    ///
468    /// Called when we receive a valid authenticated packet from a new address.
469    /// Short-circuits when neither the transport_id nor the TransportAddr
470    /// bytes changed — at multi-Gbps the same peer's source 4-tuple is
471    /// stable per session and the overwhelming majority of inbound
472    /// packets hit this fast path. Saves both the redundant
473    /// `Option::take` + Vec drop on the cached side and the caller's
474    /// `.clone()` allocation on the input side: the caller can pass
475    /// `&TransportAddr` and we only `.to_owned()` when storing.
476    ///
477    /// Returns `true` iff the stored `(transport_id, current_addr)` pair
478    /// actually changed.
479    pub fn set_current_addr(&mut self, transport_id: TransportId, addr: &TransportAddr) -> bool {
480        if self.transport_id == Some(transport_id) && self.current_addr.as_ref() == Some(addr) {
481            return false;
482        }
483        self.transport_id = Some(transport_id);
484        self.current_addr = Some(addr.clone());
485        true
486    }
487
488    // === Handshake Resend ===
489
490    /// Store wire-format msg2 for resend on duplicate msg1.
491    pub fn set_handshake_msg2(&mut self, msg2: Vec<u8>) {
492        self.handshake_msg2 = Some(msg2);
493    }
494
495    /// Get stored msg2 bytes for resend.
496    pub fn handshake_msg2(&self) -> Option<&[u8]> {
497        self.handshake_msg2.as_deref()
498    }
499
500    /// Clear stored msg2 (no longer needed after handshake window).
501    pub fn clear_handshake_msg2(&mut self) {
502        self.handshake_msg2 = None;
503    }
504
505    // === Replay Detection Suppression ===
506
507    /// Increment replay suppression counter. Returns the new count.
508    pub fn increment_replay_suppressed(&mut self) -> u32 {
509        self.replay_suppressed_count += 1;
510        self.replay_suppressed_count
511    }
512
513    /// Reset replay suppression counter, returning previous count.
514    pub fn reset_replay_suppressed(&mut self) -> u32 {
515        let count = self.replay_suppressed_count;
516        self.replay_suppressed_count = 0;
517        count
518    }
519
520    /// Current replay suppression count.
521    pub fn replay_suppressed_count(&self) -> u32 {
522        self.replay_suppressed_count
523    }
524
525    // === Decryption Failure Tracking ===
526
527    /// Increment consecutive decryption failure counter, returning new count.
528    pub fn increment_decrypt_failures(&mut self) -> u32 {
529        self.consecutive_decrypt_failures += 1;
530        self.consecutive_decrypt_failures
531    }
532
533    /// Reset consecutive decryption failure counter.
534    pub fn reset_decrypt_failures(&mut self) {
535        self.consecutive_decrypt_failures = 0;
536    }
537
538    /// Current consecutive decryption failure count.
539    pub fn consecutive_decrypt_failures(&self) -> u32 {
540        self.consecutive_decrypt_failures
541    }
542
543    // === Epoch Accessors ===
544
545    /// Get the remote peer's startup epoch (from handshake).
546    pub fn remote_epoch(&self) -> Option<[u8; 8]> {
547        self.remote_epoch
548    }
549
550    /// Update the remote peer's startup epoch after a successful in-place
551    /// rekey. Initial handshakes set this through `with_session`, but recovery
552    /// rekeys also exchange epochs and must keep restart detection current.
553    pub(crate) fn set_remote_epoch(&mut self, remote_epoch: Option<[u8; 8]>) {
554        self.remote_epoch = remote_epoch;
555    }
556
557    // === Tree Accessors ===
558
559    /// Get the peer's tree coordinates, if known.
560    pub fn coords(&self) -> Option<&TreeCoordinate> {
561        self.ancestry.as_ref()
562    }
563
564    /// Get the peer's parent declaration, if known.
565    pub fn declaration(&self) -> Option<&ParentDeclaration> {
566        self.declaration.as_ref()
567    }
568
569    /// Check if this peer has a known tree position.
570    pub fn has_tree_position(&self) -> bool {
571        self.declaration.is_some() && self.ancestry.is_some()
572    }
573
574    // === Filter Accessors ===
575
576    /// Get the peer's inbound filter, if known.
577    pub fn inbound_filter(&self) -> Option<&BloomFilter> {
578        self.inbound_filter.as_ref()
579    }
580
581    /// Get the filter sequence number.
582    pub fn filter_sequence(&self) -> u64 {
583        self.filter_sequence
584    }
585
586    /// Check if this peer's filter is stale.
587    pub fn filter_is_stale(&self, current_time_ms: u64, stale_threshold_ms: u64) -> bool {
588        if self.filter_received_at == 0 {
589            return true;
590        }
591        current_time_ms.saturating_sub(self.filter_received_at) > stale_threshold_ms
592    }
593
594    /// Check if a destination might be reachable through this peer.
595    pub fn may_reach(&self, node_addr: &NodeAddr) -> bool {
596        match &self.inbound_filter {
597            Some(filter) => filter.contains(node_addr),
598            None => false,
599        }
600    }
601
602    /// Check if we need to send this peer a filter update.
603    pub fn needs_filter_update(&self) -> bool {
604        self.pending_filter_update
605    }
606
607    // === Statistics Accessors ===
608
609    /// Get link statistics.
610    pub fn link_stats(&self) -> &LinkStats {
611        &self.link_stats
612    }
613
614    /// Get mutable link statistics.
615    pub fn link_stats_mut(&mut self) -> &mut LinkStats {
616        &mut self.link_stats
617    }
618
619    /// Whether this node initiated the adjacent FMP session.
620    pub fn fmp_mmp_is_initiator(&self) -> bool {
621        self.fmp_mmp_is_initiator
622    }
623
624    /// When this peer was authenticated.
625    pub fn authenticated_at(&self) -> u64 {
626        self.authenticated_at
627    }
628
629    /// When this peer was last seen.
630    pub fn last_seen(&self) -> u64 {
631        self.last_seen
632    }
633
634    /// Time since last activity.
635    pub fn idle_time(&self, current_time_ms: u64) -> u64 {
636        current_time_ms.saturating_sub(self.last_seen)
637    }
638
639    /// Connection duration since authentication.
640    pub fn connection_duration(&self, current_time_ms: u64) -> u64 {
641        current_time_ms.saturating_sub(self.authenticated_at)
642    }
643
644    /// Session-relative elapsed time in milliseconds (for inner header timestamp).
645    ///
646    /// Returns milliseconds since session establishment, truncated to u32.
647    /// Wraps at ~49.7 days which is acceptable for session-relative timing.
648    pub fn session_elapsed_ms(&self) -> u32 {
649        self.session_start.elapsed().as_millis() as u32
650    }
651
652    /// Local dataplane generation for the current Noise session.
653    pub fn session_generation(&self) -> u64 {
654        self.session_generation
655    }
656
657    /// When this peer's session started (for link-dead fallback timing).
658    pub fn session_start(&self) -> Instant {
659        self.session_start
660    }
661
662    // === Heartbeat ===
663
664    /// When we last sent a heartbeat to this peer.
665    pub fn last_heartbeat_sent(&self) -> Option<Instant> {
666        self.last_heartbeat_sent
667    }
668
669    /// Record that we sent a heartbeat.
670    pub fn mark_heartbeat_sent(&mut self, now: Instant) {
671        self.last_heartbeat_sent = Some(now);
672    }
673
674    // === State Updates ===
675
676    /// Update last seen timestamp.
677    pub fn touch(&mut self, current_time_ms: u64) {
678        self.last_seen = current_time_ms;
679        // Stale links are still sendable, so authenticated traffic refreshes
680        // them. Reconnecting links were declared link-dead and need a fresh
681        // handshake/reprobe before they can carry traffic again.
682        if self.connectivity == ConnectivityState::Stale {
683            self.connectivity = ConnectivityState::Connected;
684        }
685    }
686
687    /// Mark peer as stale (no recent traffic).
688    pub fn mark_stale(&mut self) {
689        if self.connectivity == ConnectivityState::Connected {
690            self.connectivity = ConnectivityState::Stale;
691        }
692    }
693
694    /// Mark peer as reconnecting.
695    pub fn mark_reconnecting(&mut self) {
696        self.connectivity = ConnectivityState::Reconnecting;
697    }
698
699    /// Mark peer as disconnected.
700    pub fn mark_disconnected(&mut self) {
701        self.connectivity = ConnectivityState::Disconnected;
702    }
703
704    /// Mark peer as connected (e.g., after successful reconnect).
705    pub fn mark_connected(&mut self, current_time_ms: u64) {
706        self.connectivity = ConnectivityState::Connected;
707        self.last_seen = current_time_ms;
708    }
709
710    /// Update the link ID (e.g., on reconnect).
711    pub fn set_link_id(&mut self, link_id: LinkId) {
712        self.link_id = link_id;
713    }
714
715    // === Tree Updates ===
716
717    /// Update peer's tree position without refreshing path liveness.
718    ///
719    /// The TreeAnnounce itself may arrive over a fallback/transit FMP path.
720    /// Authenticated receive bookkeeping owns `last_seen`; topology metadata
721    /// must not make a stale direct path look alive again.
722    pub fn update_tree_position(
723        &mut self,
724        declaration: ParentDeclaration,
725        ancestry: TreeCoordinate,
726        _current_time_ms: u64,
727    ) {
728        self.declaration = Some(declaration);
729        self.ancestry = Some(ancestry);
730    }
731
732    /// Clear peer's tree position.
733    pub fn clear_tree_position(&mut self) {
734        self.declaration = None;
735        self.ancestry = None;
736    }
737
738    // === Tree Announce Rate Limiting ===
739
740    /// Set the minimum interval between TreeAnnounce messages (milliseconds).
741    pub fn set_tree_announce_min_interval_ms(&mut self, ms: u64) {
742        self.tree_announce_min_interval_ms = ms;
743    }
744
745    /// Get the last tree announce send timestamp (for carrying across reconnection).
746    pub fn last_tree_announce_sent_ms(&self) -> u64 {
747        self.last_tree_announce_sent_ms
748    }
749
750    /// Set the last tree announce send timestamp (to preserve rate limit across reconnection).
751    pub fn set_last_tree_announce_sent_ms(&mut self, ms: u64) {
752        self.last_tree_announce_sent_ms = ms;
753    }
754
755    /// Check if we can send a TreeAnnounce now (rate limiting).
756    pub fn can_send_tree_announce(&self, now_ms: u64) -> bool {
757        now_ms.saturating_sub(self.last_tree_announce_sent_ms) >= self.tree_announce_min_interval_ms
758    }
759
760    /// Record that we sent a TreeAnnounce to this peer.
761    pub fn record_tree_announce_sent(&mut self, now_ms: u64) {
762        self.last_tree_announce_sent_ms = now_ms;
763        self.pending_tree_announce = false;
764    }
765
766    /// Mark that a tree announce is pending (deferred due to rate limit).
767    pub fn mark_tree_announce_pending(&mut self) {
768        self.pending_tree_announce = true;
769    }
770
771    /// Check if a deferred tree announce is waiting to be sent.
772    pub fn has_pending_tree_announce(&self) -> bool {
773        self.pending_tree_announce
774    }
775
776    // === Filter Updates ===
777
778    /// Update peer's inbound filter.
779    pub fn update_filter(&mut self, filter: BloomFilter, sequence: u64, current_time_ms: u64) {
780        self.inbound_filter = Some(filter);
781        self.filter_sequence = sequence;
782        self.filter_received_at = current_time_ms;
783        self.last_seen = current_time_ms;
784    }
785
786    /// Clear peer's inbound filter.
787    pub fn clear_filter(&mut self) {
788        self.inbound_filter = None;
789        self.filter_sequence = 0;
790        self.filter_received_at = 0;
791    }
792
793    /// Mark that we need to send this peer a filter update.
794    pub fn mark_filter_update_needed(&mut self) {
795        self.pending_filter_update = true;
796    }
797
798    /// Clear the pending filter update flag.
799    pub fn clear_filter_update_needed(&mut self) {
800        self.pending_filter_update = false;
801    }
802}
803
804mod rekey;
805
806#[cfg(test)]
807mod tests;