Skip to main content

bee/debug/
peers.rs

1//! Peer/connectivity endpoints: peers list, blocklist, ping, connect,
2//! topology, reserve state. Mirrors bee-go's
3//! `pkg/debug/connectivity.go` plus the relevant pieces of `node.go`.
4
5use std::collections::BTreeMap;
6
7use reqwest::Method;
8use serde::{Deserialize, Deserializer};
9
10use crate::client::request;
11use crate::swarm::Error;
12
13use super::DebugApi;
14
15/// One connected peer entry. Mirrors bee-go `Peer`.
16#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
17pub struct Peer {
18    /// Peer overlay address (hex).
19    pub address: String,
20    /// True for full nodes, false for light nodes.
21    #[serde(default, rename = "fullNode")]
22    pub full_node: bool,
23}
24
25/// Node addresses payload — `GET /addresses`.
26#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct Addresses {
29    /// Overlay (DHT) address.
30    pub overlay: String,
31    /// Underlay multiaddrs.
32    pub underlay: Vec<String>,
33    /// Ethereum address.
34    pub ethereum: String,
35    /// Node libp2p public key (hex).
36    pub public_key: String,
37    /// PSS public key (hex).
38    pub pss_public_key: String,
39}
40
41/// Per-peer connection metrics. Mirrors bee-go `MetricSnapshotView`.
42#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct MetricSnapshotView {
45    /// Unix timestamp (seconds) of the most recent observation.
46    #[serde(default)]
47    pub last_seen_timestamp: i64,
48    /// Connection retries within the current session.
49    #[serde(default)]
50    pub session_connection_retry: u64,
51    /// Total time the peer has been connected over its lifetime, in
52    /// fractional seconds.
53    #[serde(default)]
54    pub connection_total_duration: f64,
55    /// Time the peer has been connected in the current session.
56    #[serde(default)]
57    pub session_connection_duration: f64,
58    /// `"inbound"` / `"outbound"` for the current session.
59    #[serde(default)]
60    pub session_connection_direction: String,
61    /// Exponentially-weighted moving average latency (nanoseconds, the
62    /// raw value Bee writes — divide by 1e6 for ms).
63    #[serde(default, rename = "latencyEWMA")]
64    pub latency_ewma: i64,
65    /// Per-peer reachability string (`"Public"`, `"Private"`,
66    /// `"Unknown"`, etc.).
67    #[serde(default)]
68    pub reachability: String,
69    /// Whether the peer is currently considered healthy by the
70    /// node-health subsystem.
71    #[serde(default)]
72    pub healthy: bool,
73}
74
75/// One peer entry inside a [`BinInfo`]. Mirrors bee-go `PeerInfo`.
76#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
77pub struct PeerInfo {
78    /// Peer overlay (hex).
79    pub address: String,
80    /// Per-peer connection metrics. Bee omits the field for some
81    /// disconnected peers.
82    #[serde(default)]
83    pub metrics: Option<MetricSnapshotView>,
84}
85
86/// Per-bin population summary. Mirrors bee-go `BinInfo`.
87///
88/// Bee marshals empty `connectedPeers` / `disconnectedPeers` slices as
89/// JSON `null` (Go default for nil slices). We accept `null` and `[]`
90/// interchangeably so the parse is robust across Bee builds.
91#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct BinInfo {
94    /// Total known peers in this bin (connected + disconnected).
95    #[serde(default, rename = "population")]
96    pub population: u64,
97    /// Currently connected peers in this bin.
98    #[serde(default, rename = "connected")]
99    pub connected: u64,
100    /// Connected peers, with metrics.
101    #[serde(default, deserialize_with = "deserialize_null_or_seq")]
102    pub connected_peers: Vec<PeerInfo>,
103    /// Known-but-disconnected peers.
104    #[serde(default, deserialize_with = "deserialize_null_or_seq")]
105    pub disconnected_peers: Vec<PeerInfo>,
106}
107
108/// Treat a JSON `null` value as an empty `Vec<T>`. Matches Go's
109/// `json.Marshal` behaviour for nil slices.
110fn deserialize_null_or_seq<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
111where
112    D: Deserializer<'de>,
113    T: Deserialize<'de>,
114{
115    Ok(Option::<Vec<T>>::deserialize(d)?.unwrap_or_default())
116}
117
118/// Topology snapshot — `GET /topology`.
119///
120/// Bee returns the per-bin breakdown as 32 flat keys
121/// (`bin_0`..`bin_31`); we collapse them into [`Topology::bins`] for
122/// indexable access. The light-node bin sits beside the regular bins
123/// at [`Topology::light_nodes`].
124#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct Topology {
127    /// Base address (overlay).
128    pub base_addr: String,
129    /// Population (peers known across all bins).
130    pub population: i64,
131    /// Currently connected peers.
132    pub connected: i64,
133    /// Snapshot timestamp (RFC 3339).
134    pub timestamp: String,
135    /// Lower watermark for the nearest neighbour bin.
136    pub nn_low_watermark: i64,
137    /// Kademlia depth.
138    pub depth: u8,
139    /// Aggregate peer reachability state (`"Public"`, `"Private"`,
140    /// `"Unknown"`, etc.). Empty on Bee versions that pre-date the
141    /// AutoNAT field.
142    #[serde(default)]
143    pub reachability: String,
144    /// Network availability (`"Available"` / `"Unavailable"` /
145    /// empty on older Bee versions).
146    #[serde(default)]
147    pub network_availability: String,
148    /// 32 per-bin entries indexed by bin number `0..=31`. See
149    /// `BinInfo` for the per-bin shape.
150    #[serde(default = "default_bins", deserialize_with = "deserialize_bins")]
151    pub bins: Vec<BinInfo>,
152    /// Aggregated info for connected light nodes. Sits outside the
153    /// regular bins because light nodes don't get a Kademlia bin.
154    #[serde(default)]
155    pub light_nodes: BinInfo,
156}
157
158/// Default for the `Topology::bins` field. Always 32 empty entries —
159/// kept invariant so consumers can index `bins[i]` without bounds
160/// checks. Used by serde when the response omits the `bins` key
161/// entirely (older Bee builds, dev/mock servers).
162fn default_bins() -> Vec<BinInfo> {
163    vec![BinInfo::default(); 32]
164}
165
166/// Decode the flat `{ "bin_0": …, …, "bin_31": … }` object into a
167/// 32-element [`Vec<BinInfo>`]. Missing keys default to an empty
168/// [`BinInfo`] so partial dev/mock servers don't blow up the parse.
169fn deserialize_bins<'de, D>(d: D) -> Result<Vec<BinInfo>, D::Error>
170where
171    D: Deserializer<'de>,
172{
173    // Allow the field value to be `null` for forward compatibility.
174    let map: Option<BTreeMap<String, BinInfo>> = Option::deserialize(d)?;
175    let mut map = map.unwrap_or_default();
176    let mut out = Vec::with_capacity(32);
177    for i in 0..32u8 {
178        let key = format!("bin_{i}");
179        out.push(map.remove(&key).unwrap_or_default());
180    }
181    Ok(out)
182}
183
184/// Reserve state snapshot — `GET /reservestate`.
185#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct ReserveState {
188    /// Network radius.
189    pub radius: u8,
190    /// Storage radius.
191    pub storage_radius: u8,
192    /// Batch commitment.
193    pub commitment: i64,
194}
195
196impl DebugApi {
197    /// `GET /peers` — list every peer this node is currently connected to.
198    pub async fn peers(&self) -> Result<Vec<Peer>, Error> {
199        let builder = request(&self.inner, Method::GET, "peers")?;
200        #[derive(Deserialize)]
201        struct Resp {
202            peers: Vec<Peer>,
203        }
204        let r: Resp = self.inner.send_json(builder).await?;
205        Ok(r.peers)
206    }
207
208    /// `GET /blocklist` — peers currently blocklisted by this node.
209    pub async fn blocklist(&self) -> Result<Vec<Peer>, Error> {
210        let builder = request(&self.inner, Method::GET, "blocklist")?;
211        #[derive(Deserialize)]
212        struct Resp {
213            peers: Vec<Peer>,
214        }
215        let r: Resp = self.inner.send_json(builder).await?;
216        Ok(r.peers)
217    }
218
219    /// `DELETE /peers/{address}` — disconnect and forget a peer.
220    pub async fn remove_peer(&self, address: &str) -> Result<(), Error> {
221        let path = format!("peers/{address}");
222        let builder = request(&self.inner, Method::DELETE, &path)?;
223        self.inner.send(builder).await?;
224        Ok(())
225    }
226
227    /// `POST /pingpong/{address}` — round-trip ping a peer. Returns the
228    /// reported RTT string (e.g. `"2.5ms"`).
229    pub async fn ping_peer(&self, address: &str) -> Result<String, Error> {
230        let path = format!("pingpong/{address}");
231        let builder = request(&self.inner, Method::POST, &path)?;
232        #[derive(Deserialize)]
233        struct Resp {
234            rtt: String,
235        }
236        let r: Resp = self.inner.send_json(builder).await?;
237        Ok(r.rtt)
238    }
239
240    /// `POST /connect/{multiaddr}` — manually dial a peer at the given
241    /// multiaddress (e.g. `"/dns/bee.example.com/tcp/1634/p2p/16Uiu…"`).
242    /// Returns the resulting overlay address. A leading `/` in
243    /// `multiaddr` is stripped.
244    pub async fn connect_peer(&self, multiaddr: &str) -> Result<String, Error> {
245        let trimmed = multiaddr.trim_start_matches('/');
246        let path = format!("connect/{trimmed}");
247        let builder = request(&self.inner, Method::POST, &path)?;
248        #[derive(Deserialize)]
249        struct Resp {
250            address: String,
251        }
252        let r: Resp = self.inner.send_json(builder).await?;
253        Ok(r.address)
254    }
255
256    /// `GET /addresses` — node overlay / underlay / ethereum / pubkeys.
257    pub async fn addresses(&self) -> Result<Addresses, Error> {
258        let builder = request(&self.inner, Method::GET, "addresses")?;
259        self.inner.send_json(builder).await
260    }
261
262    /// `GET /topology` — Kademlia topology snapshot.
263    pub async fn topology(&self) -> Result<Topology, Error> {
264        let builder = request(&self.inner, Method::GET, "topology")?;
265        self.inner.send_json(builder).await
266    }
267
268    /// `GET /reservestate` — current reserve radius/commitment.
269    pub async fn reserve_state(&self) -> Result<ReserveState, Error> {
270        let builder = request(&self.inner, Method::GET, "reservestate")?;
271        self.inner.send_json(builder).await
272    }
273
274    /// `GET /welcome-message` — P2P welcome banner.
275    pub async fn welcome_message(&self) -> Result<String, Error> {
276        let builder = request(&self.inner, Method::GET, "welcome-message")?;
277        #[derive(Deserialize)]
278        struct Resp {
279            #[serde(rename = "welcomeMessage")]
280            welcome_message: String,
281        }
282        let r: Resp = self.inner.send_json(builder).await?;
283        Ok(r.welcome_message)
284    }
285
286    /// `POST /welcome-message` — update the P2P welcome banner.
287    pub async fn set_welcome_message(&self, message: &str) -> Result<(), Error> {
288        #[derive(serde::Serialize)]
289        struct Body<'a> {
290            #[serde(rename = "welcomeMessage")]
291            welcome_message: &'a str,
292        }
293        let body = serde_json::to_vec(&Body {
294            welcome_message: message,
295        })?;
296        let builder = request(&self.inner, Method::POST, "welcome-message")?
297            .header("Content-Type", "application/json")
298            .body(bytes::Bytes::from(body));
299        self.inner.send(builder).await?;
300        Ok(())
301    }
302}