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 NetConnect {}
164impl RpcMethod<1> for NetConnect {
165    const NAME: &'static str = "Filecoin.NetConnect";
166    const PARAM_NAMES: [&'static str; 1] = ["peerAddressInfo"];
167    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
168    const PERMISSION: Permission = Permission::Write;
169    const DESCRIPTION: &'static str = "Connects to a specified peer.";
170
171    type Params = (AddrInfo,);
172    type Ok = ();
173
174    async fn handle(
175        ctx: Ctx,
176        (AddrInfo { id, addrs },): Self::Params,
177        _: &http::Extensions,
178    ) -> Result<Self::Ok, ServerError> {
179        let (_, id) = multibase::decode(format!("{}{}", "z", id))?;
180        let peer_id = PeerId::from_bytes(&id)?;
181
182        let (tx, rx) = flume::bounded(1);
183        let req = NetworkMessage::JSONRPCRequest {
184            method: NetRPCMethods::Connect(tx, peer_id, addrs),
185        };
186
187        ctx.network_send().send_async(req).await?;
188        let success = rx.recv_async().await?;
189
190        if success {
191            Ok(())
192        } else {
193            Err(anyhow::anyhow!("Peer could not be dialed from any address provided").into())
194        }
195    }
196}
197
198pub enum NetDisconnect {}
199impl RpcMethod<1> for NetDisconnect {
200    const NAME: &'static str = "Filecoin.NetDisconnect";
201    const PARAM_NAMES: [&'static str; 1] = ["peerId"];
202    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
203    const PERMISSION: Permission = Permission::Write;
204    const DESCRIPTION: &'static str = "Disconnects from the specified peer.";
205
206    type Params = (String,);
207    type Ok = ();
208
209    async fn handle(
210        ctx: Ctx,
211        (peer_id,): Self::Params,
212        _: &http::Extensions,
213    ) -> Result<Self::Ok, ServerError> {
214        let peer_id = PeerId::from_str(&peer_id)?;
215
216        let (tx, rx) = flume::bounded(1);
217        let req = NetworkMessage::JSONRPCRequest {
218            method: NetRPCMethods::Disconnect(tx, peer_id),
219        };
220
221        ctx.network_send().send_async(req).await?;
222        rx.recv_async().await?;
223
224        Ok(())
225    }
226}
227
228pub enum NetAgentVersion {}
229impl RpcMethod<1> for NetAgentVersion {
230    const NAME: &'static str = "Filecoin.NetAgentVersion";
231    const PARAM_NAMES: [&'static str; 1] = ["peerId"];
232    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
233    const PERMISSION: Permission = Permission::Read;
234    const DESCRIPTION: &'static str = "Returns the agent version string.";
235
236    type Params = (String,);
237    type Ok = String;
238
239    async fn handle(
240        ctx: Ctx,
241        (peer_id,): Self::Params,
242        _: &http::Extensions,
243    ) -> Result<Self::Ok, ServerError> {
244        let peer_id = PeerId::from_str(&peer_id)?;
245        let (tx, rx) = flume::bounded(1);
246        ctx.network_send()
247            .send_async(NetworkMessage::JSONRPCRequest {
248                method: NetRPCMethods::AgentVersion(tx, peer_id),
249            })
250            .await?;
251        Ok(rx.recv_async().await?.context("item not found")?)
252    }
253}
254
255pub enum NetAutoNatStatus {}
256impl RpcMethod<0> for NetAutoNatStatus {
257    const NAME: &'static str = "Filecoin.NetAutoNatStatus";
258    const PARAM_NAMES: [&'static str; 0] = [];
259    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
260    const PERMISSION: Permission = Permission::Read;
261    const DESCRIPTION: &'static str =
262        "Returns the node's AutoNAT reachability status and observed public addresses.";
263
264    type Params = ();
265    type Ok = NatStatusResult;
266
267    async fn handle(
268        ctx: Ctx,
269        (): Self::Params,
270        _: &http::Extensions,
271    ) -> Result<Self::Ok, ServerError> {
272        let (tx, rx) = flume::bounded(1);
273        let req = NetworkMessage::JSONRPCRequest {
274            method: NetRPCMethods::AutoNATStatus(tx),
275        };
276        ctx.network_send().send_async(req).await?;
277        let nat_status = rx.recv_async().await?;
278        Ok(nat_status.into())
279    }
280}
281
282pub enum NetVersion {}
283impl RpcMethod<0> for NetVersion {
284    const NAME: &'static str = "Filecoin.NetVersion";
285    const PARAM_NAMES: [&'static str; 0] = [];
286    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
287    const PERMISSION: Permission = Permission::Read;
288    const NAME_ALIAS: Option<&'static str> = Some("net_version");
289    const DESCRIPTION: &'static str =
290        "Returns the current network ID (the EIP-155 chain ID) as a decimal string.";
291
292    type Params = ();
293    type Ok = Arc<str>;
294
295    async fn handle(
296        ctx: Ctx,
297        (): Self::Params,
298        _: &http::Extensions,
299    ) -> Result<Self::Ok, ServerError> {
300        // `eth_chain_id` is fixed for the process lifetime; cache the decimal form.
301        static CACHED: OnceLock<Arc<str>> = OnceLock::new();
302        Ok(CACHED
303            .get_or_init(|| Arc::<str>::from(ctx.chain_config().eth_chain_id.to_string()))
304            .clone())
305    }
306}
307
308pub enum NetProtectAdd {}
309impl RpcMethod<1> for NetProtectAdd {
310    const NAME: &'static str = "Filecoin.NetProtectAdd";
311    const PARAM_NAMES: [&'static str; 1] = ["peerIdList"];
312    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
313    const PERMISSION: Permission = Permission::Admin;
314    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.";
315
316    type Params = (Vec<String>,);
317    type Ok = ();
318
319    // This whitelists a peer in forest peer manager but has no impact on libp2p swarm
320    // due to the fact that `rust-libp2p` implementation is very different to that
321    // in go. However it would be nice to investigate connection limiting options in Rust.
322    // See: <https://github.com/ChainSafe/forest/issues/4355>.
323    async fn handle(
324        ctx: Ctx,
325        (peer_ids,): Self::Params,
326        _: &http::Extensions,
327    ) -> Result<Self::Ok, ServerError> {
328        let peer_ids = peer_ids
329            .iter()
330            .map(String::as_str)
331            .map(PeerId::from_str)
332            .try_collect()?;
333        let (tx, rx) = flume::bounded(1);
334        ctx.network_send()
335            .send_async(NetworkMessage::JSONRPCRequest {
336                method: NetRPCMethods::ProtectPeer(tx, peer_ids),
337            })
338            .await?;
339        rx.recv_async().await?;
340        Ok(())
341    }
342}
343
344pub enum NetProtectList {}
345impl RpcMethod<0> for NetProtectList {
346    const NAME: &'static str = "Filecoin.NetProtectList";
347    const PARAM_NAMES: [&'static str; 0] = [];
348    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
349    const PERMISSION: Permission = Permission::Read;
350    const DESCRIPTION: &'static str = "Returns the current list of protected peers.";
351
352    type Params = ();
353    type Ok = Vec<String>;
354    async fn handle(
355        ctx: Ctx,
356        (): Self::Params,
357        _: &http::Extensions,
358    ) -> Result<Self::Ok, ServerError> {
359        let (tx, rx) = flume::bounded(1);
360        ctx.network_send()
361            .send_async(NetworkMessage::JSONRPCRequest {
362                method: NetRPCMethods::ListProtectedPeers(tx),
363            })
364            .await?;
365        let peers = rx.recv_async().await?;
366        Ok(peers.into_iter().map(|p| p.to_string()).collect())
367    }
368}
369
370pub enum NetProtectRemove {}
371impl RpcMethod<1> for NetProtectRemove {
372    const NAME: &'static str = "Filecoin.NetProtectRemove";
373    const PARAM_NAMES: [&'static str; 1] = ["peerIdList"];
374    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
375    const PERMISSION: Permission = Permission::Admin;
376    const DESCRIPTION: &'static str = "Remove a peer from the protected list.";
377
378    type Params = (Vec<String>,);
379    type Ok = ();
380
381    // Similar to NetProtectAdd
382    async fn handle(
383        ctx: Ctx,
384        (peer_ids,): Self::Params,
385        _: &http::Extensions,
386    ) -> Result<Self::Ok, ServerError> {
387        let peer_ids = peer_ids
388            .iter()
389            .map(String::as_str)
390            .map(PeerId::from_str)
391            .try_collect()?;
392        let (tx, rx) = flume::bounded(1);
393        ctx.network_send()
394            .send_async(NetworkMessage::JSONRPCRequest {
395                method: NetRPCMethods::UnprotectPeer(tx, peer_ids),
396            })
397            .await?;
398        rx.recv_async().await?;
399        Ok(())
400    }
401}
402
403pub enum NetChainExchange {}
404impl RpcMethod<3> for NetChainExchange {
405    const NAME: &'static str = "Forest.NetChainExchange";
406    const PARAM_NAMES: [&'static str; 3] = ["startTipsetKey", "len", "options"];
407    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
408    const PERMISSION: Permission = Permission::Admin;
409    const DESCRIPTION: &'static str = "Internal API for debugging chain exchange.";
410
411    type Params = (ApiTipsetKey, u64, u64);
412    type Ok = String;
413
414    async fn handle(
415        ctx: Ctx,
416        (tsk, request_len, options): Self::Params,
417        _: &http::Extensions,
418    ) -> Result<Self::Ok, ServerError> {
419        let request_len =
420            NonZeroU64::new(request_len).context("request length must be greater than 0")?;
421        let tsk = tsk
422            .0
423            .unwrap_or_else(|| ctx.chain_store().heaviest_tipset().key().clone());
424        let timer = Instant::now();
425        let result: Vec<TipsetBundle> = ctx
426            .sync_network_context
427            .handle_chain_exchange_request(None, &tsk, request_len, options, |_| true)
428            .await?;
429        Ok(format!(
430            "fetched {} tipsets, took {}",
431            result.len(),
432            humantime::format_duration(timer.elapsed())
433        ))
434    }
435}