Skip to main content

forest/rpc/methods/
net.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod types;
5pub use types::*;
6
7use std::num::NonZeroU64;
8use std::str::FromStr;
9use std::sync::OnceLock;
10use std::time::Instant;
11
12use crate::libp2p::chain_exchange::TipsetBundle;
13use crate::libp2p::{NetRPCMethods, NetworkMessage, PeerId};
14use crate::prelude::*;
15use crate::rpc::types::ApiTipsetKey;
16use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod, ServerError};
17use anyhow::Result;
18use cid::multibase;
19use enumflags2::BitFlags;
20
21pub enum NetAddrsListen {}
22impl RpcMethod<0> for NetAddrsListen {
23    const NAME: &'static str = "Filecoin.NetAddrsListen";
24    const PARAM_NAMES: [&'static str; 0] = [];
25    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
26    const PERMISSION: Permission = Permission::Read;
27    const DESCRIPTION: &'static str = "Returns a list of listening addresses and the peer ID.";
28
29    type Params = ();
30    type Ok = AddrInfo;
31
32    async fn handle(
33        ctx: Ctx,
34        (): Self::Params,
35        _: &http::Extensions,
36    ) -> Result<Self::Ok, ServerError> {
37        let (tx, rx) = flume::bounded(1);
38        let req = NetworkMessage::JSONRPCRequest {
39            method: NetRPCMethods::AddrsListen(tx),
40        };
41
42        ctx.network_send().send_async(req).await?;
43        let (id, addrs) = rx.recv_async().await?;
44
45        Ok(AddrInfo::new(id, addrs))
46    }
47}
48
49pub enum NetPeers {}
50impl RpcMethod<0> for NetPeers {
51    const NAME: &'static str = "Filecoin.NetPeers";
52    const PARAM_NAMES: [&'static str; 0] = [];
53    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
54    const PERMISSION: Permission = Permission::Read;
55    const DESCRIPTION: &'static str = "Returns a list of currently connected peers.";
56
57    type Params = ();
58    type Ok = Vec<AddrInfo>;
59
60    async fn handle(
61        ctx: Ctx,
62        (): Self::Params,
63        _: &http::Extensions,
64    ) -> Result<Self::Ok, ServerError> {
65        let (tx, rx) = flume::bounded(1);
66        let req = NetworkMessage::JSONRPCRequest {
67            method: NetRPCMethods::Peers(tx),
68        };
69
70        ctx.network_send().send_async(req).await?;
71        let peer_addresses = rx.recv_async().await?;
72
73        let connections = peer_addresses
74            .into_iter()
75            .map(|(id, addrs)| AddrInfo::new(id, addrs))
76            .collect();
77
78        Ok(connections)
79    }
80}
81
82pub enum NetFindPeer {}
83impl RpcMethod<1> for NetFindPeer {
84    const NAME: &'static str = "Filecoin.NetFindPeer";
85    const PARAM_NAMES: [&'static str; 1] = ["peerId"];
86    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
87    const PERMISSION: Permission = Permission::Read;
88    const DESCRIPTION: &'static str =
89        "Returns the known addresses of the peer with the given peer ID.";
90
91    type Params = (String,);
92    type Ok = AddrInfo;
93
94    async fn handle(
95        ctx: Ctx,
96        (peer_id,): Self::Params,
97        _: &http::Extensions,
98    ) -> Result<Self::Ok, ServerError> {
99        let peer_id = PeerId::from_str(&peer_id)?;
100        let (tx, rx) = flume::bounded(1);
101        ctx.network_send()
102            .send_async(NetworkMessage::JSONRPCRequest {
103                method: NetRPCMethods::Peer(tx, peer_id),
104            })
105            .await?;
106        let addrs = rx
107            .recv_async()
108            .await?
109            .with_context(|| format!("peer {peer_id} not found"))?;
110        Ok(AddrInfo::new(peer_id, addrs))
111    }
112}
113
114pub enum NetListening {}
115impl RpcMethod<0> for NetListening {
116    const NAME: &'static str = "Filecoin.NetListening";
117    const PARAM_NAMES: [&'static str; 0] = [];
118    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
119    const PERMISSION: Permission = Permission::Read;
120    const NAME_ALIAS: Option<&'static str> = Some("net_listening");
121    const DESCRIPTION: &'static str =
122        "Returns whether the node is listening for network connections (always `true`).";
123
124    type Params = ();
125    type Ok = bool;
126
127    async fn handle(
128        _: Ctx,
129        (): Self::Params,
130        _: &http::Extensions,
131    ) -> Result<Self::Ok, ServerError> {
132        Ok(true)
133    }
134}
135
136pub enum NetInfo {}
137impl RpcMethod<0> for NetInfo {
138    const NAME: &'static str = "Forest.NetInfo";
139    const PARAM_NAMES: [&'static str; 0] = [];
140    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
141    const PERMISSION: Permission = Permission::Read;
142    const DESCRIPTION: &'static str =
143        "Returns peer and connection statistics for the node's libp2p network.";
144
145    type Params = ();
146    type Ok = NetInfoResult;
147
148    async fn handle(
149        ctx: Ctx,
150        (): Self::Params,
151        _: &http::Extensions,
152    ) -> Result<Self::Ok, ServerError> {
153        let (tx, rx) = flume::bounded(1);
154        let req = NetworkMessage::JSONRPCRequest {
155            method: NetRPCMethods::Info(tx),
156        };
157
158        ctx.network_send().send_async(req).await?;
159        Ok(rx.recv_async().await?)
160    }
161}
162
163pub enum NetBandwidthStats {}
164impl RpcMethod<0> for NetBandwidthStats {
165    const NAME: &'static str = "Filecoin.NetBandwidthStats";
166    const PARAM_NAMES: [&'static str; 0] = [];
167    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
168    const PERMISSION: Permission = Permission::Read;
169    const DESCRIPTION: &'static str = "Returns statistics about the node's total bandwidth. Currently a stub that always returns zeros.";
170
171    type Params = ();
172    type Ok = BandwidthStats;
173
174    // Forest feeds libp2p bandwidth into a Prometheus registry that exposes no
175    // queryable totals, so report zeros. See <https://github.com/ChainSafe/forest/issues/7370>.
176    async fn handle(
177        _: Ctx,
178        (): Self::Params,
179        _: &http::Extensions,
180    ) -> Result<Self::Ok, ServerError> {
181        Ok(BandwidthStats::default())
182    }
183}
184
185pub enum NetConnect {}
186impl RpcMethod<1> for NetConnect {
187    const NAME: &'static str = "Filecoin.NetConnect";
188    const PARAM_NAMES: [&'static str; 1] = ["peerAddressInfo"];
189    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
190    const PERMISSION: Permission = Permission::Write;
191    const DESCRIPTION: &'static str = "Connects to a specified peer.";
192
193    type Params = (AddrInfo,);
194    type Ok = ();
195
196    async fn handle(
197        ctx: Ctx,
198        (AddrInfo { id, addrs },): Self::Params,
199        _: &http::Extensions,
200    ) -> Result<Self::Ok, ServerError> {
201        let (_, id) = multibase::decode(format!("{}{}", "z", id))?;
202        let peer_id = PeerId::from_bytes(&id)?;
203
204        let (tx, rx) = flume::bounded(1);
205        let req = NetworkMessage::JSONRPCRequest {
206            method: NetRPCMethods::Connect(tx, peer_id, addrs),
207        };
208
209        ctx.network_send().send_async(req).await?;
210        let success = rx.recv_async().await?;
211
212        if success {
213            Ok(())
214        } else {
215            Err(anyhow::anyhow!("Peer could not be dialed from any address provided").into())
216        }
217    }
218}
219
220pub enum NetDisconnect {}
221impl RpcMethod<1> for NetDisconnect {
222    const NAME: &'static str = "Filecoin.NetDisconnect";
223    const PARAM_NAMES: [&'static str; 1] = ["peerId"];
224    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
225    const PERMISSION: Permission = Permission::Write;
226    const DESCRIPTION: &'static str = "Disconnects from the specified peer.";
227
228    type Params = (String,);
229    type Ok = ();
230
231    async fn handle(
232        ctx: Ctx,
233        (peer_id,): Self::Params,
234        _: &http::Extensions,
235    ) -> Result<Self::Ok, ServerError> {
236        let peer_id = PeerId::from_str(&peer_id)?;
237
238        let (tx, rx) = flume::bounded(1);
239        let req = NetworkMessage::JSONRPCRequest {
240            method: NetRPCMethods::Disconnect(tx, peer_id),
241        };
242
243        ctx.network_send().send_async(req).await?;
244        rx.recv_async().await?;
245
246        Ok(())
247    }
248}
249
250pub enum NetAgentVersion {}
251impl RpcMethod<1> for NetAgentVersion {
252    const NAME: &'static str = "Filecoin.NetAgentVersion";
253    const PARAM_NAMES: [&'static str; 1] = ["peerId"];
254    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
255    const PERMISSION: Permission = Permission::Read;
256    const DESCRIPTION: &'static str = "Returns the agent version string.";
257
258    type Params = (String,);
259    type Ok = String;
260
261    async fn handle(
262        ctx: Ctx,
263        (peer_id,): Self::Params,
264        _: &http::Extensions,
265    ) -> Result<Self::Ok, ServerError> {
266        let peer_id = PeerId::from_str(&peer_id)?;
267        let (tx, rx) = flume::bounded(1);
268        ctx.network_send()
269            .send_async(NetworkMessage::JSONRPCRequest {
270                method: NetRPCMethods::AgentVersion(tx, peer_id),
271            })
272            .await?;
273        Ok(rx.recv_async().await?.context("item not found")?)
274    }
275}
276
277pub enum NetAutoNatStatus {}
278impl RpcMethod<0> for NetAutoNatStatus {
279    const NAME: &'static str = "Filecoin.NetAutoNatStatus";
280    const PARAM_NAMES: [&'static str; 0] = [];
281    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
282    const PERMISSION: Permission = Permission::Read;
283    const DESCRIPTION: &'static str =
284        "Returns the node's AutoNAT reachability status and observed public addresses.";
285
286    type Params = ();
287    type Ok = NatStatusResult;
288
289    async fn handle(
290        ctx: Ctx,
291        (): Self::Params,
292        _: &http::Extensions,
293    ) -> Result<Self::Ok, ServerError> {
294        let (tx, rx) = flume::bounded(1);
295        let req = NetworkMessage::JSONRPCRequest {
296            method: NetRPCMethods::AutoNATStatus(tx),
297        };
298        ctx.network_send().send_async(req).await?;
299        let nat_status = rx.recv_async().await?;
300        Ok(nat_status.into())
301    }
302}
303
304pub enum NetVersion {}
305impl RpcMethod<0> for NetVersion {
306    const NAME: &'static str = "Filecoin.NetVersion";
307    const PARAM_NAMES: [&'static str; 0] = [];
308    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
309    const PERMISSION: Permission = Permission::Read;
310    const NAME_ALIAS: Option<&'static str> = Some("net_version");
311    const DESCRIPTION: &'static str =
312        "Returns the current network ID (the EIP-155 chain ID) as a decimal string.";
313
314    type Params = ();
315    type Ok = Arc<str>;
316
317    async fn handle(
318        ctx: Ctx,
319        (): Self::Params,
320        _: &http::Extensions,
321    ) -> Result<Self::Ok, ServerError> {
322        // `eth_chain_id` is fixed for the process lifetime; cache the decimal form.
323        static CACHED: OnceLock<Arc<str>> = OnceLock::new();
324        Ok(CACHED
325            .get_or_init(|| Arc::<str>::from(ctx.chain_config().eth_chain_id.to_string()))
326            .clone())
327    }
328}
329
330pub enum NetProtectAdd {}
331impl RpcMethod<1> for NetProtectAdd {
332    const NAME: &'static str = "Filecoin.NetProtectAdd";
333    const PARAM_NAMES: [&'static str; 1] = ["peerIdList"];
334    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
335    const PERMISSION: Permission = Permission::Admin;
336    const DESCRIPTION: &'static str = "Protects a peer from having its connection(s) pruned in the event the libp2p host reaches its maximum number of peers.";
337
338    type Params = (Vec<String>,);
339    type Ok = ();
340
341    // This whitelists a peer in forest peer manager but has no impact on libp2p swarm
342    // due to the fact that `rust-libp2p` implementation is very different to that
343    // in go. However it would be nice to investigate connection limiting options in Rust.
344    // See: <https://github.com/ChainSafe/forest/issues/4355>.
345    async fn handle(
346        ctx: Ctx,
347        (peer_ids,): Self::Params,
348        _: &http::Extensions,
349    ) -> Result<Self::Ok, ServerError> {
350        let peer_ids = peer_ids
351            .iter()
352            .map(String::as_str)
353            .map(PeerId::from_str)
354            .try_collect()?;
355        let (tx, rx) = flume::bounded(1);
356        ctx.network_send()
357            .send_async(NetworkMessage::JSONRPCRequest {
358                method: NetRPCMethods::ProtectPeer(tx, peer_ids),
359            })
360            .await?;
361        rx.recv_async().await?;
362        Ok(())
363    }
364}
365
366pub enum NetProtectList {}
367impl RpcMethod<0> for NetProtectList {
368    const NAME: &'static str = "Filecoin.NetProtectList";
369    const PARAM_NAMES: [&'static str; 0] = [];
370    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
371    const PERMISSION: Permission = Permission::Read;
372    const DESCRIPTION: &'static str = "Returns the current list of protected peers.";
373
374    type Params = ();
375    type Ok = Vec<String>;
376    async fn handle(
377        ctx: Ctx,
378        (): Self::Params,
379        _: &http::Extensions,
380    ) -> Result<Self::Ok, ServerError> {
381        let (tx, rx) = flume::bounded(1);
382        ctx.network_send()
383            .send_async(NetworkMessage::JSONRPCRequest {
384                method: NetRPCMethods::ListProtectedPeers(tx),
385            })
386            .await?;
387        let peers = rx.recv_async().await?;
388        Ok(peers.into_iter().map(|p| p.to_string()).collect())
389    }
390}
391
392pub enum NetProtectRemove {}
393impl RpcMethod<1> for NetProtectRemove {
394    const NAME: &'static str = "Filecoin.NetProtectRemove";
395    const PARAM_NAMES: [&'static str; 1] = ["peerIdList"];
396    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
397    const PERMISSION: Permission = Permission::Admin;
398    const DESCRIPTION: &'static str = "Remove a peer from the protected list.";
399
400    type Params = (Vec<String>,);
401    type Ok = ();
402
403    // Similar to NetProtectAdd
404    async fn handle(
405        ctx: Ctx,
406        (peer_ids,): Self::Params,
407        _: &http::Extensions,
408    ) -> Result<Self::Ok, ServerError> {
409        let peer_ids = peer_ids
410            .iter()
411            .map(String::as_str)
412            .map(PeerId::from_str)
413            .try_collect()?;
414        let (tx, rx) = flume::bounded(1);
415        ctx.network_send()
416            .send_async(NetworkMessage::JSONRPCRequest {
417                method: NetRPCMethods::UnprotectPeer(tx, peer_ids),
418            })
419            .await?;
420        rx.recv_async().await?;
421        Ok(())
422    }
423}
424
425pub enum NetChainExchange {}
426impl RpcMethod<3> for NetChainExchange {
427    const NAME: &'static str = "Forest.NetChainExchange";
428    const PARAM_NAMES: [&'static str; 3] = ["startTipsetKey", "len", "options"];
429    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
430    const PERMISSION: Permission = Permission::Admin;
431    const DESCRIPTION: &'static str = "Internal API for debugging chain exchange.";
432
433    type Params = (ApiTipsetKey, u64, u64);
434    type Ok = String;
435
436    async fn handle(
437        ctx: Ctx,
438        (tsk, request_len, options): Self::Params,
439        _: &http::Extensions,
440    ) -> Result<Self::Ok, ServerError> {
441        let request_len =
442            NonZeroU64::new(request_len).context("request length must be greater than 0")?;
443        let tsk = tsk
444            .0
445            .unwrap_or_else(|| ctx.chain_store().heaviest_tipset().key().clone());
446        let timer = Instant::now();
447        let result: Vec<TipsetBundle> = ctx
448            .sync_network_context
449            .handle_chain_exchange_request(None, &tsk, request_len, options, |_| true)
450            .await?;
451        Ok(format!(
452            "fetched {} tipsets, took {}",
453            result.len(),
454            humantime::format_duration(timer.elapsed())
455        ))
456    }
457}