zebra_network/peer_set/stall_tracker.rs
1//! Tracks peers that consistently return empty or failed `FindBlocks` or
2//! `FindHeaders` responses, so the peer set can disconnect them.
3//!
4//! A peer returning a single empty response may just be syncing itself; a peer
5//! that does so repeatedly stalls the syncer by forcing retries to others. The
6//! counter is per-peer and resets on any useful (non-empty) response.
7//!
8//! Only applies to `FindBlocks` and `FindHeaders`. An empty response to
9//! `BlocksByHash`/`TransactionsById` is a legitimate "I don't have this
10//! inventory" answer, so those don't feed the tracker.
11
12use std::collections::HashMap;
13
14use crate::PeerSocketAddr;
15
16/// Consecutive empty or failed `FindBlocks`/`FindHeaders` responses tolerated
17/// before the peer set disconnects a peer.
18pub(super) const FIND_RESPONSE_STALL_THRESHOLD: usize = 3;
19
20#[derive(Default)]
21pub(super) struct FindResponseStallTracker {
22 counts: HashMap<PeerSocketAddr, usize>,
23}
24
25impl FindResponseStallTracker {
26 pub(super) fn new() -> Self {
27 Self::default()
28 }
29
30 /// Records a stall for `addr`. Returns `true` once the peer reaches
31 /// [`FIND_RESPONSE_STALL_THRESHOLD`] — the caller must then disconnect it.
32 /// On threshold the entry is removed, so a reconnected peer starts fresh.
33 pub(super) fn record_stall(&mut self, addr: PeerSocketAddr) -> bool {
34 let count = self.counts.entry(addr).or_default();
35 *count += 1;
36
37 if *count >= FIND_RESPONSE_STALL_THRESHOLD {
38 self.counts.remove(&addr);
39 true
40 } else {
41 false
42 }
43 }
44
45 /// Clears tracking for a peer that sent a useful response or disconnected.
46 pub(super) fn clear(&mut self, addr: PeerSocketAddr) {
47 self.counts.remove(&addr);
48 }
49}
50
51#[cfg(test)]
52mod tests;