Skip to main content

fedimint_server/net/p2p_connector/
iroh.rs

1use std::collections::BTreeMap;
2use std::net::SocketAddr;
3
4use anyhow::{Context as _, ensure};
5use async_trait::async_trait;
6use fedimint_core::PeerId;
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::envs::{FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV, parse_kv_list_from_env};
9use fedimint_core::net::STANDARD_FEDIMINT_P2P_PORT;
10use fedimint_core::net::iroh::build_iroh_endpoint;
11use fedimint_core::util::SafeUrl;
12use fedimint_logging::LOG_NET_IROH;
13use fedimint_server_core::dashboard_ui::ConnectionType;
14use iroh::{Endpoint, NodeAddr, NodeId, SecretKey};
15use serde::Serialize;
16use serde::de::DeserializeOwned;
17use tracing::trace;
18
19use super::IP2PConnector;
20use crate::net::p2p_connection::{DynP2PConnection, IP2PConnection as _};
21
22/// Parses the host and port from a url
23pub fn parse_p2p(url: &SafeUrl) -> anyhow::Result<String> {
24    ensure!(url.scheme() == "fedimint", "p2p url has invalid scheme");
25
26    let host = url.host_str().context("p2p url is missing host")?;
27
28    let port = url.port().unwrap_or(STANDARD_FEDIMINT_P2P_PORT);
29
30    Ok(format!("{host}:{port}"))
31}
32
33#[derive(Debug, Clone)]
34pub struct IrohConnector {
35    /// Map of all peers' connection information we want to be connected to
36    pub(crate) node_ids: BTreeMap<PeerId, NodeId>,
37    /// The Iroh endpoint
38    pub(crate) endpoint: Endpoint,
39    /// List of overrides to use when attempting to connect to given `NodeId`
40    ///
41    /// This is useful for testing, or forcing non-default network connectivity.
42    pub(crate) connection_overrides: BTreeMap<NodeId, NodeAddr>,
43}
44
45pub(crate) const FEDIMINT_P2P_ALPN: &[u8] = b"FEDIMINT_P2P_ALPN";
46
47impl IrohConnector {
48    pub async fn new(
49        secret_key: SecretKey,
50        p2p_bind_addr: SocketAddr,
51        iroh_dns: Option<SafeUrl>,
52        iroh_relays: Vec<SafeUrl>,
53        node_ids: BTreeMap<PeerId, NodeId>,
54    ) -> anyhow::Result<Self> {
55        let mut s =
56            Self::new_no_overrides(secret_key, p2p_bind_addr, iroh_dns, iroh_relays, node_ids)
57                .await?;
58
59        // Overrides are `<node-id>=<socket-addr>` pairs (the node id is the
60        // key); iroh 1.0 no longer ships the `NodeTicket` format, so build the
61        // `NodeAddr` from its parts to keep the wire format version agnostic.
62        // Pre-0.12 guardians read the `NodeTicket`-format
63        // `FM_IROH_CONNECT_OVERRIDES` instead; devimint emits both side by side.
64        for (k, v) in
65            parse_kv_list_from_env::<NodeId, SocketAddr>(FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV)?
66        {
67            s = s.with_connection_override(k, NodeAddr::new(k).with_direct_addresses([v]));
68        }
69
70        Ok(s)
71    }
72
73    pub async fn new_no_overrides(
74        secret_key: SecretKey,
75        bind_addr: SocketAddr,
76        iroh_dns: Option<SafeUrl>,
77        iroh_relays: Vec<SafeUrl>,
78        node_ids: BTreeMap<PeerId, NodeId>,
79    ) -> anyhow::Result<Self> {
80        let identity = *node_ids
81            .iter()
82            .find(|entry| entry.1 == &secret_key.public())
83            .expect("Our public key is not part of the keyset")
84            .0;
85
86        let endpoint = build_iroh_endpoint(
87            secret_key,
88            bind_addr,
89            iroh_dns,
90            iroh_relays,
91            FEDIMINT_P2P_ALPN,
92        )
93        .await?;
94
95        Ok(Self {
96            node_ids: node_ids
97                .into_iter()
98                .filter(|entry| entry.0 != identity)
99                .collect(),
100            endpoint,
101            connection_overrides: BTreeMap::default(),
102        })
103    }
104
105    pub fn with_connection_override(mut self, node: NodeId, addr: NodeAddr) -> Self {
106        self.connection_overrides.insert(node, addr);
107        self
108    }
109}
110
111#[async_trait]
112impl<M> IP2PConnector<M> for IrohConnector
113where
114    M: Encodable + Decodable + Serialize + DeserializeOwned + Send + 'static,
115{
116    fn peers(&self) -> Vec<PeerId> {
117        self.node_ids.keys().copied().collect()
118    }
119
120    async fn connect(&self, peer: PeerId) -> anyhow::Result<DynP2PConnection<M>> {
121        let node_id = *self.node_ids.get(&peer).expect("No node id found for peer");
122
123        let connection = match self.connection_overrides.get(&node_id) {
124            Some(node_addr) => {
125                trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
126                self.endpoint
127                    .connect(node_addr.clone(), FEDIMINT_P2P_ALPN)
128                    .await?
129            }
130            None => self.endpoint.connect(node_id, FEDIMINT_P2P_ALPN).await?,
131        };
132
133        Ok(connection.into_dyn())
134    }
135
136    async fn accept(&self) -> anyhow::Result<(PeerId, DynP2PConnection<M>)> {
137        let connection = self
138            .endpoint
139            .accept()
140            .await
141            .context("Listener closed unexpectedly")?
142            .accept()?
143            .await?;
144
145        let node_id = connection.remote_node_id()?;
146
147        let auth_peer = self
148            .node_ids
149            .iter()
150            .find(|entry| entry.1 == &node_id)
151            .with_context(|| format!("Node id {node_id} is unknown"))?
152            .0;
153
154        Ok((*auth_peer, connection.into_dyn()))
155    }
156
157    fn connection_type(&self, peer: PeerId) -> Option<ConnectionType> {
158        let node_id = *self.node_ids.get(&peer).expect("No node id found for peer");
159
160        match self.endpoint.conn_type(node_id).ok()?.get().ok()? {
161            iroh::endpoint::ConnectionType::None => None,
162            iroh::endpoint::ConnectionType::Direct(..) => Some(ConnectionType::Direct),
163            iroh::endpoint::ConnectionType::Relay(..) => Some(ConnectionType::Relay),
164            iroh::endpoint::ConnectionType::Mixed(..) => Some(ConnectionType::Mixed),
165        }
166    }
167}