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}