Skip to main content

dig_pex/
state.rs

1//! Per-link, per-direction PEX state (SPEC §5, §9).
2//!
3//! PEX on a link is two independent half-conversations (SPEC §5.1). [`LinkState`] holds both for one
4//! link, keyed in the engine by the transport `peer_id` (never a wire field — SPEC §5.4, §10):
5//!
6//! - the **sender** (outgoing) direction — whether we've sent our handshake + snapshot, the per-link
7//!   *told-state* ("what I've told you", SPEC §9.1: `peer_id → advertised-content fingerprint`), the
8//!   send-cadence bookkeeping, and our own (possibly backed-off) declared interval;
9//! - the **receiver** (incoming) direction — the [`RecvPhase`] state machine, the remote's declared
10//!   interval, the last data-message arrival time (for the SPEC §6.4 floor), the strike count + mute
11//!   flag, and the set of `peer_id`s this link has told us (for `dropped` attribution, SPEC §8.3).
12//!
13//! All of this dies with the link (SPEC §5.5): a fresh connection restarts from
14//! [`RecvPhase::AwaitingHandshake`] with an empty told-state.
15
16use std::collections::HashMap;
17
18use crate::caps::PEX_DEFAULT_INTERVAL;
19
20/// The receiver-side state machine for one direction (SPEC §5.3).
21///
22/// ```text
23///   AwaitingHandshake --handshake(ok)--> AwaitingSnapshot --snapshot--> Streaming
24/// ```
25///
26/// A data message before the handshake, a delta before the snapshot, or a second snapshot is a
27/// protocol violation (code `6`).
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum RecvPhase {
30    /// No valid handshake seen yet — the only acceptable inbound message is `pex_handshake`.
31    AwaitingHandshake,
32    /// Handshake accepted; awaiting the single `pex_snapshot` that opens the data stream.
33    AwaitingSnapshot,
34    /// Snapshot seen; `pex_delta`s flow (subject to the SPEC §6.4 arrival floor).
35    Streaming,
36}
37
38/// All PEX state for one link (both directions). Created on `link_up` / first inbound message and
39/// discarded on `link_down` (SPEC §5.5).
40#[derive(Debug, Clone)]
41pub struct LinkState {
42    // ---- sender (outgoing) direction ----
43    /// Whether we have sent our `pex_handshake` on this link.
44    pub handshake_sent: bool,
45    /// Whether we have sent our one `pex_snapshot` on this link.
46    pub snapshot_sent: bool,
47    /// Per-link told-state (SPEC §9.1): `peer_id → advertised-content fingerprint hash`
48    /// ([`PeerEntry::fingerprint_hash`](crate::entry::PeerEntry::fingerprint_hash), a `Copy`,
49    /// allocation-free `u64` rather than the display-oriented `String` form — #179 MED optimization).
50    /// Deltas are computed relative to this; an unchanged told entry is never re-advertised.
51    pub told: HashMap<String, u64>,
52    /// When we last sent a **data message** (snapshot or delta) on this link, in `now_ms`. `None`
53    /// until the snapshot goes out; the cadence spaces subsequent sends from here (SPEC §6.1).
54    pub last_data_send_ms: Option<u64>,
55    /// The additive jitter (ms) drawn for the *current* schedule interval (SPEC §6.3).
56    pub send_jitter_ms: u64,
57    /// Our own declared interval (seconds) for this link — starts at the configured value and MAY
58    /// double on receiving `pex_error` code `3` (SPEC §6.4), capped at `PEX_MAX_INTERVAL`.
59    pub self_interval_secs: u32,
60    /// When a `pex_error` code-3 back-off was last actually applied to `self_interval_secs`, in
61    /// `now_ms` (LOW #179 fix). Bounds how often an unauthenticated `pex_error` can move our send
62    /// cadence: a further code-3 is honored at most once per (pre-doubling) effective interval, so a
63    /// peer spamming code-3 cannot ratchet the interval to `PEX_MAX_INTERVAL` faster than a single
64    /// genuine violation could.
65    pub last_backoff_applied_ms: Option<u64>,
66
67    // ---- receiver (incoming) direction ----
68    /// The inbound state machine (SPEC §5.3).
69    pub phase: RecvPhase,
70    /// The remote's handshake-declared interval (seconds), once its handshake arrived. Used as the
71    /// arrival-floor basis (SPEC §6.4) and as a spacing floor for our own sends (SPEC §6.2).
72    pub remote_declared_secs: Option<u32>,
73    /// When the last inbound **data message** arrived, in `now_ms` — the SPEC §6.4 clock.
74    pub last_arrival_ms: Option<u64>,
75    /// Violation strikes counted on the incoming direction (SPEC §11.2).
76    pub strikes: u32,
77    /// Whether the incoming direction is muted (all further inbound PEX ignored) — SPEC §5.2, §11.2.
78    pub muted: bool,
79    /// `peer_id`s this link has told us (via snapshot / delta `added`) — so a later `dropped` can be
80    /// attributed to it and ignored for ids it never told us (SPEC §4.4, §8.3).
81    pub received: HashMap<String, u64>,
82}
83
84impl LinkState {
85    /// A fresh link: both directions at their start state, no told/received history.
86    #[must_use]
87    pub fn new(self_interval_secs: u32) -> Self {
88        LinkState {
89            handshake_sent: false,
90            snapshot_sent: false,
91            told: HashMap::new(),
92            last_data_send_ms: None,
93            send_jitter_ms: 0,
94            self_interval_secs,
95            last_backoff_applied_ms: None,
96            phase: RecvPhase::AwaitingHandshake,
97            remote_declared_secs: None,
98            last_arrival_ms: None,
99            strikes: 0,
100            muted: false,
101            received: HashMap::new(),
102        }
103    }
104}
105
106impl Default for LinkState {
107    fn default() -> Self {
108        LinkState::new(PEX_DEFAULT_INTERVAL)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn fresh_link_starts_awaiting_handshake() {
118        let l = LinkState::new(60);
119        assert_eq!(l.phase, RecvPhase::AwaitingHandshake);
120        assert!(!l.handshake_sent);
121        assert!(!l.snapshot_sent);
122        assert!(!l.muted);
123        assert_eq!(l.strikes, 0);
124        assert!(l.told.is_empty());
125        assert!(l.received.is_empty());
126        assert_eq!(l.self_interval_secs, 60);
127        assert_eq!(l.last_backoff_applied_ms, None);
128    }
129
130    #[test]
131    fn default_uses_default_interval() {
132        assert_eq!(
133            LinkState::default().self_interval_secs,
134            PEX_DEFAULT_INTERVAL
135        );
136    }
137}