dig_peer_selector/pool_event.rs
1//! [`PoolEvent`] / [`PoolRemovalReason`] — the topology/churn feed, byte-compatible with
2//! `dig_gossip::PoolEvent` (SPEC.md §5.4, §7.2).
3//!
4//! # Why this shape is mirrored, not imported
5//!
6//! SPEC §5.4/§11 originally required `on_pool_event` to consume `dig_gossip::PoolEvent` **verbatim**.
7//! In practice `dig-gossip` pulls the entire chia-protocol / consensus / TLS stack into what is a
8//! pure decision layer, and its published git tip does not currently compile as a dependency (it
9//! lags the upstream `chia-*` crate versions it references). Taking that dependency would both bloat
10//! and *break* this crate's build — contradicting the SPEC's own dependency-minimalism principle
11//! (§1, §11) and the `dig-dht` / `dig-pex` precedent, which deliberately avoid `dig-gossip` for
12//! exactly this reason.
13//!
14//! This crate therefore mirrors the churn-event shape locally — **byte-identical** field names and
15//! variants, over the SAME re-used [`dig_nat::PeerId`] — exactly as `dig-pex` mirrors the L7 address
16//! shape "rather than pulling those crates in." The host adapter (`dig-node`) converts a
17//! `dig_gossip::PoolEvent` into this type with a trivial 1:1 field map. Because the shapes are
18//! identical, the contract is preserved; the SPEC records this deviation (§5.4, §7.2, §11, SEL-01).
19//!
20//! When `dig-gossip` is published to crates.io with a compiling tip, this module MAY be replaced by a
21//! direct re-export without changing any field name or variant — the shapes are the same.
22
23use std::net::SocketAddr;
24
25use dig_nat::PeerId;
26
27/// A churn event as the connected pool gains or loses a peer — byte-compatible with
28/// `dig_gossip::PoolEvent`.
29///
30/// The host subscribes to `dig-gossip`'s pool events and forwards each one to the selector via
31/// [`crate::PeerSelector::on_pool_event`], which keeps the registry live (SPEC §2.3):
32/// - `PeerAdded` **upserts** a registry entry (provenance [`crate::Provenance::Gossip`]), preserving
33/// any existing learned quality (a reconnecting peer keeps its history — SPEC §2.3);
34/// - `PeerRemoved` marks the peer **disconnected** but **retains** its entry + learned quality so a
35/// later reconnect resumes from history (subject to eviction — SPEC §2.5); a `Banned` reason makes
36/// the peer ineligible for selection until re-added (SPEC §9.4).
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum PoolEvent {
39 /// A peer was added to the connected pool (dialed successfully, or accepted inbound and adopted).
40 PeerAdded {
41 /// The verified peer identity now in the pool.
42 peer_id: PeerId,
43 /// The remote endpoint the connection runs over (peer, or relay for a relayed link).
44 addr: SocketAddr,
45 },
46 /// A peer left the connected pool (disconnected, evicted dead/stale, or banned).
47 PeerRemoved {
48 /// The peer identity that is no longer connected.
49 peer_id: PeerId,
50 /// Why it left.
51 reason: PoolRemovalReason,
52 },
53}
54
55/// Why a peer was removed from the pool — byte-compatible with `dig_gossip::PoolRemovalReason`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum PoolRemovalReason {
58 /// A normal disconnect (peer closed, or we called `disconnect`).
59 Disconnected,
60 /// Evicted because keepalive found it dead / unresponsive.
61 Dead,
62 /// Removed because the peer was banned for misbehaviour — ineligible until re-added (SPEC §9.4).
63 Banned,
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 fn pid(b: u8) -> PeerId {
71 PeerId::from_bytes([b; 32])
72 }
73
74 #[test]
75 fn pool_event_variants_construct_and_compare() {
76 let addr: SocketAddr = "203.0.113.7:9444".parse().unwrap();
77 let a = PoolEvent::PeerAdded {
78 peer_id: pid(1),
79 addr,
80 };
81 let b = PoolEvent::PeerRemoved {
82 peer_id: pid(1),
83 reason: PoolRemovalReason::Banned,
84 };
85 assert_ne!(a, b);
86 assert_eq!(
87 a.clone(),
88 PoolEvent::PeerAdded {
89 peer_id: pid(1),
90 addr
91 }
92 );
93 }
94
95 #[test]
96 fn removal_reasons_are_distinct() {
97 assert_ne!(PoolRemovalReason::Disconnected, PoolRemovalReason::Banned);
98 assert_ne!(PoolRemovalReason::Dead, PoolRemovalReason::Banned);
99 }
100}