zebra_network/peer_set/stall_tracker.rs
1//! Tracks peers that consistently return failed `FindBlocks` or `FindHeaders`
2//! responses, so the peer set can disconnect them.
3//!
4//! Successful responses, including empty ones, reset the per-peer counter.
5//! Empty replies can be legitimate when a peer has no blocks beyond our locator;
6//! fair request routing prevents them from monopolizing discovery.
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 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 successful 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;