Skip to main content

hyperswarm/
peer_info.rs

1//! Peer information and priority tracking
2
3use std::{
4    collections::HashSet,
5    net::SocketAddr,
6    time::{Duration, Instant},
7};
8
9use dht_rpc::IdBytes;
10
11/// Minimum connection time before resetting attempt counter (15 seconds)
12const MIN_CONNECTION_TIME: Duration = Duration::from_secs(15);
13
14/// Connection lifecycle state
15#[derive(Debug, Clone, Default)]
16pub enum ConnectionState {
17    /// Not doing anything
18    #[default]
19    Idle,
20    /// In the connection queue
21    Queued,
22    /// Waiting in retry timer
23    Waiting,
24    /// Currently attempting to connect
25    Connecting,
26    /// Active connection established
27    Connected { since: Instant },
28}
29
30impl ConnectionState {
31    pub fn is_idle(&self) -> bool {
32        matches!(self, Self::Idle)
33    }
34
35    pub fn is_queued(&self) -> bool {
36        matches!(self, Self::Queued)
37    }
38
39    pub fn is_waiting(&self) -> bool {
40        matches!(self, Self::Waiting)
41    }
42
43    pub fn is_connecting(&self) -> bool {
44        matches!(self, Self::Connecting)
45    }
46
47    pub fn is_connected(&self) -> bool {
48        matches!(self, Self::Connected { .. })
49    }
50
51    /// Check if peer is busy (queued, waiting, or connecting)
52    pub fn is_busy(&self) -> bool {
53        matches!(self, Self::Queued | Self::Waiting | Self::Connecting)
54    }
55}
56
57/// How we learned about this peer
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59pub enum PeerOrigin {
60    /// Found via DHT lookup (we initiate connection)
61    #[default]
62    Discovered,
63    /// Added explicitly via join_peer
64    Explicit,
65    /// They connected to us
66    Incoming,
67}
68
69impl PeerOrigin {
70    /// Whether we are the client (initiator) for this peer
71    pub fn is_client(&self) -> bool {
72        matches!(self, Self::Discovered | Self::Explicit)
73    }
74
75    /// Whether this is an explicit peer (added via join_peer)
76    pub fn is_explicit(&self) -> bool {
77        matches!(self, Self::Explicit)
78    }
79}
80
81/// Trust level based on connection history
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
83pub enum TrustLevel {
84    /// Never successfully connected
85    #[default]
86    Unknown,
87    /// Had at least one successful connection
88    Proven,
89    /// Blocked due to firewall or too many failures
90    Banned,
91}
92
93impl TrustLevel {
94    pub fn is_proven(&self) -> bool {
95        matches!(self, Self::Proven)
96    }
97
98    pub fn is_banned(&self) -> bool {
99        matches!(self, Self::Banned)
100    }
101}
102
103/// Priority levels for peer connection attempts
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
105#[repr(u8)]
106pub enum Priority {
107    /// Lowest priority - many failed attempts or stale peer
108    VeryLow = 0,
109    /// Low priority - several failed attempts
110    Low = 1,
111    /// Normal priority - default for new peers
112    #[default]
113    Normal = 2,
114    /// High priority - few failed attempts
115    High = 3,
116    /// Highest priority - proven peer with recent success
117    VeryHigh = 4,
118}
119
120/// Information about a discovered peer
121#[derive(Debug)]
122pub struct PeerInfo {
123    /// The peer's public key
124    pub public_key: IdBytes,
125
126    /// Known relay addresses for this peer
127    pub relay_addresses: Vec<SocketAddr>,
128
129    /// Whether to attempt reconnection on disconnect
130    pub reconnecting: bool,
131
132    /// Trust level based on connection history
133    pub trust: TrustLevel,
134
135    /// Number of failed connection attempts
136    pub attempts: u32,
137
138    /// Current priority level
139    pub priority: Priority,
140
141    /// Connection lifecycle state
142    pub state: ConnectionState,
143
144    /// How we learned about this peer
145    pub origin: PeerOrigin,
146
147    /// Topics this peer is associated with
148    pub topics: HashSet<IdBytes>,
149}
150
151impl PeerInfo {
152    /// Create a new PeerInfo for a peer
153    pub fn new(public_key: IdBytes) -> Self {
154        Self {
155            public_key,
156            relay_addresses: Vec::new(),
157            reconnecting: true,
158            trust: TrustLevel::Unknown,
159            attempts: 0,
160            priority: Priority::Normal,
161            state: ConnectionState::Idle,
162            origin: PeerOrigin::Discovered,
163            topics: HashSet::new(),
164        }
165    }
166
167    /// Called when a connection is successfully established
168    pub fn connected(&mut self) {
169        self.state = ConnectionState::Connected {
170            since: Instant::now(),
171        };
172        self.trust = TrustLevel::Proven;
173        self.update_priority();
174    }
175
176    /// Called when a connection is closed
177    pub fn disconnected(&mut self) {
178        // Only increment attempts if connection was short-lived
179        if let ConnectionState::Connected { since } = self.state {
180            if since.elapsed() < MIN_CONNECTION_TIME {
181                self.attempts = self.attempts.saturating_add(1);
182            } else {
183                // Long-lived connection - reset attempts
184                self.attempts = 0;
185            }
186        } else {
187            // Never connected - increment attempts
188            self.attempts = self.attempts.saturating_add(1);
189        }
190        self.state = ConnectionState::Idle;
191        self.update_priority();
192    }
193
194    /// Add relay addresses if they are new (merge with existing)
195    pub fn add_relay_addresses(&mut self, relay_addresses: Vec<SocketAddr>) -> &mut Self {
196        for addr in relay_addresses {
197            if !self.relay_addresses.contains(&addr) {
198                self.relay_addresses.push(addr);
199            }
200        }
201        self
202    }
203
204    /// Add a topic this peer is associated with
205    pub fn add_topic(&mut self, topic: IdBytes) -> &mut Self {
206        self.topics.insert(topic);
207        self
208    }
209
210    /// Remove a topic association
211    pub fn remove_topic(&mut self, topic: &IdBytes) {
212        self.topics.remove(topic);
213    }
214
215    /// Ban this peer
216    pub fn ban(&mut self) {
217        self.trust = TrustLevel::Banned;
218        self.priority = Priority::VeryLow;
219    }
220
221    /// Update the priority based on current state
222    pub fn update_priority(&mut self) -> bool {
223        if self.trust.is_banned() {
224            self.priority = Priority::VeryLow;
225            return false;
226        }
227
228        let old_priority = self.priority;
229        self.priority = self.calculate_priority();
230
231        // Return true if should be queued (priority changed or is high enough)
232        self.priority != Priority::VeryLow
233            && (self.priority != old_priority || !self.state.is_queued())
234    }
235
236    /// Calculate priority based on attempts and trust level
237    fn calculate_priority(&self) -> Priority {
238        match (self.trust.is_proven(), self.attempts) {
239            // Proven peers get higher priority
240            (true, 0) => Priority::VeryHigh,
241            (true, 1) => Priority::VeryHigh,
242            (true, 2) => Priority::High,
243            (true, 3) => Priority::Normal,
244            (true, _) => Priority::Low,
245
246            // Unproven peers start lower
247            (false, 0) => Priority::Normal,
248            (false, 1) => Priority::High, // Give one retry at high priority
249            (false, 2) => Priority::Normal,
250            (false, 3) => Priority::Low,
251            (false, _) => Priority::VeryLow,
252        }
253    }
254
255    /// Reset state for re-discovery
256    pub fn reset(&mut self) {
257        if !self.trust.is_proven() {
258            self.attempts = 0;
259        }
260        self.update_priority();
261    }
262
263    /// Check if this peer should be garbage collected
264    pub fn should_gc(&self) -> bool {
265        // Don't GC if busy, explicit, or has topics
266        if self.state.is_busy() || self.origin.is_explicit() || !self.topics.is_empty() {
267            return false;
268        }
269
270        // GC if banned or too many attempts
271        self.trust.is_banned() || self.attempts > 10
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_priority_ordering() {
281        assert!(Priority::VeryLow < Priority::Low);
282        assert!(Priority::Low < Priority::Normal);
283        assert!(Priority::Normal < Priority::High);
284        assert!(Priority::High < Priority::VeryHigh);
285    }
286
287    #[test]
288    fn test_new_peer_priority() {
289        let info = PeerInfo::new(IdBytes::random());
290        assert_eq!(info.priority, Priority::Normal);
291        assert_eq!(info.trust, TrustLevel::Unknown);
292        assert_eq!(info.attempts, 0);
293    }
294
295    #[test]
296    fn test_connected_sets_proven() {
297        let mut info = PeerInfo::new(IdBytes::random());
298        assert!(!info.trust.is_proven());
299
300        info.connected();
301        assert!(info.trust.is_proven());
302        assert_eq!(info.priority, Priority::VeryHigh);
303    }
304
305    #[test]
306    fn test_ban_sets_very_low() {
307        let mut info = PeerInfo::new(IdBytes::random());
308        info.ban();
309
310        assert!(info.trust.is_banned());
311        assert_eq!(info.priority, Priority::VeryLow);
312    }
313
314    #[test]
315    fn test_attempts_decrease_priority() {
316        let mut info = PeerInfo::new(IdBytes::random());
317
318        // Start at Normal
319        assert_eq!(info.calculate_priority(), Priority::Normal);
320
321        // First attempt - High (retry)
322        info.attempts = 1;
323        assert_eq!(info.calculate_priority(), Priority::High);
324
325        // Second attempt - Normal
326        info.attempts = 2;
327        assert_eq!(info.calculate_priority(), Priority::Normal);
328
329        // Third attempt - Low
330        info.attempts = 3;
331        assert_eq!(info.calculate_priority(), Priority::Low);
332
333        // Many attempts - VeryLow
334        info.attempts = 5;
335        assert_eq!(info.calculate_priority(), Priority::VeryLow);
336    }
337
338    #[test]
339    fn test_proven_peer_priority() {
340        let mut info = PeerInfo::new(IdBytes::random());
341        info.trust = TrustLevel::Proven;
342
343        // Proven with no attempts - VeryHigh
344        assert_eq!(info.calculate_priority(), Priority::VeryHigh);
345
346        // Proven with attempts still gets better treatment
347        info.attempts = 3;
348        assert_eq!(info.calculate_priority(), Priority::Normal);
349
350        info.attempts = 5;
351        assert_eq!(info.calculate_priority(), Priority::Low);
352    }
353
354    #[test]
355    fn test_should_gc() {
356        let mut info = PeerInfo::new(IdBytes::random());
357
358        // New peer with no topics should not be GC'd immediately
359        assert!(!info.should_gc());
360
361        // Banned peer should be GC'd
362        info.ban();
363        assert!(info.should_gc());
364
365        // Reset and add topic - should not GC
366        info.trust = TrustLevel::Unknown;
367        info.topics.insert(IdBytes::random());
368        assert!(!info.should_gc());
369
370        // Explicit peer should not be GC'd
371        info.topics.clear();
372        info.origin = PeerOrigin::Explicit;
373        assert!(!info.should_gc());
374
375        // Queued peer should not be GC'd
376        info.origin = PeerOrigin::Discovered;
377        info.state = ConnectionState::Queued;
378        assert!(!info.should_gc());
379    }
380}