Skip to main content

fedimint_server/net/p2p_connector/
iroh.rs

1mod endpoint;
2#[cfg(test)]
3mod tests;
4
5use std::collections::BTreeMap;
6use std::net::SocketAddr;
7
8use anyhow::{Context as _, ensure};
9use async_trait::async_trait;
10use fedimint_core::PeerId;
11use fedimint_core::encoding::{Decodable, Encodable};
12use fedimint_core::envs::{FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV, parse_kv_list_from_env};
13use fedimint_core::net::STANDARD_FEDIMINT_P2P_PORT;
14use fedimint_core::util::SafeUrl;
15use fedimint_logging::LOG_NET_IROH;
16use fedimint_server_core::dashboard_ui::ConnectionType;
17use iroh::{NodeAddr, NodeId, SecretKey};
18use iroh_next::{Endpoint, EndpointAddr, EndpointId, TransportAddr};
19use serde::Serialize;
20use serde::de::DeserializeOwned;
21use tracing::trace;
22
23use self::endpoint::build_iroh_endpoint;
24use super::IP2PConnector;
25use crate::net::p2p_connection::{DynP2PConnection, IP2PConnection as _};
26
27/// Parses the host and port from a url
28pub fn parse_p2p(url: &SafeUrl) -> anyhow::Result<String> {
29    ensure!(url.scheme() == "fedimint", "p2p url has invalid scheme");
30
31    let host = url.host_str().context("p2p url is missing host")?;
32
33    let port = url.port().unwrap_or(STANDARD_FEDIMINT_P2P_PORT);
34
35    Ok(format!("{host}:{port}"))
36}
37
38#[derive(Debug, Clone)]
39/// Iroh 1.0 guardian P2P connector with a stable Iroh 0.35 public boundary.
40///
41/// Persisted configuration and callers retain the 0.35 key and address types;
42/// this connector converts their unchanged Ed25519 identity bytes into the Iroh
43/// 1.0 runtime representation.
44pub struct IrohConnector {
45    /// Map of all peers' connection information we want to be connected to
46    pub(crate) endpoint_ids: BTreeMap<PeerId, EndpointId>,
47    /// The Iroh endpoint
48    pub(crate) endpoint: Endpoint,
49    /// List of overrides to use when attempting to connect to an endpoint
50    ///
51    /// This is useful for testing, or forcing non-default network connectivity.
52    pub(crate) connection_overrides: BTreeMap<EndpointId, EndpointAddr>,
53}
54
55pub(crate) const FEDIMINT_P2P_ALPN: &[u8] = b"FEDIMINT_P2P_ALPN";
56
57impl IrohConnector {
58    pub async fn new(
59        secret_key: SecretKey,
60        p2p_bind_addr: SocketAddr,
61        iroh_dns: Option<SafeUrl>,
62        iroh_relays: Vec<SafeUrl>,
63        node_ids: BTreeMap<PeerId, NodeId>,
64    ) -> anyhow::Result<Self> {
65        let mut s =
66            Self::new_no_overrides(secret_key, p2p_bind_addr, iroh_dns, iroh_relays, node_ids)
67                .await?;
68
69        // Overrides are `<endpoint-id>=<socket-addr>` pairs. Iroh 1.0 no longer
70        // ships the `NodeTicket` format, so build the `EndpointAddr` from its
71        // parts to keep the wire format version agnostic. Pre-0.12 guardians
72        // read the `NodeTicket`-format
73        // `FM_IROH_CONNECT_OVERRIDES` instead; devimint emits both side by side.
74        for (k, v) in
75            parse_kv_list_from_env::<EndpointId, SocketAddr>(FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV)?
76        {
77            s.connection_overrides
78                .insert(k, EndpointAddr::from_parts(k, [TransportAddr::Ip(v)]));
79        }
80
81        Ok(s)
82    }
83
84    pub async fn new_no_overrides(
85        secret_key: SecretKey,
86        bind_addr: SocketAddr,
87        iroh_dns: Option<SafeUrl>,
88        iroh_relays: Vec<SafeUrl>,
89        node_ids: BTreeMap<PeerId, NodeId>,
90    ) -> anyhow::Result<Self> {
91        let secret_key = secret_key_stable_to_next(&secret_key);
92        let endpoint_ids = node_ids
93            .into_iter()
94            .map(|(peer, node_id)| {
95                endpoint_id_stable_to_next(node_id)
96                    .with_context(|| format!("Converting Iroh endpoint ID for peer {peer}"))
97                    .map(|endpoint_id| (peer, endpoint_id))
98            })
99            .collect::<anyhow::Result<BTreeMap<_, _>>>()?;
100
101        let identity = *endpoint_ids
102            .iter()
103            .find(|entry| entry.1 == &secret_key.public())
104            .expect("Our public key is not part of the keyset")
105            .0;
106
107        let endpoint = build_iroh_endpoint(
108            secret_key,
109            bind_addr,
110            iroh_dns,
111            iroh_relays,
112            FEDIMINT_P2P_ALPN,
113        )
114        .await?;
115
116        Ok(Self {
117            endpoint_ids: endpoint_ids
118                .into_iter()
119                .filter(|entry| entry.0 != identity)
120                .collect(),
121            endpoint,
122            connection_overrides: BTreeMap::default(),
123        })
124    }
125
126    /// Add a stable-Iroh connection override.
127    ///
128    /// This compatibility wrapper keeps the connector's existing public
129    /// boundary while converting the address to its Iroh 1.0 runtime
130    /// representation.
131    pub fn with_connection_override(mut self, node: NodeId, addr: NodeAddr) -> Self {
132        let endpoint_id = endpoint_id_stable_to_next(node)
133            .expect("an Iroh 0.35 node ID must be a valid Iroh 1.0 endpoint ID");
134        let relay = addr
135            .relay_url
136            .map(|relay| TransportAddr::Relay(iroh_next::RelayUrl::from(url::Url::from(relay))));
137        let direct = addr.direct_addresses.into_iter().map(TransportAddr::Ip);
138        self.connection_overrides.insert(
139            endpoint_id,
140            EndpointAddr::from_parts(endpoint_id, relay.into_iter().chain(direct)),
141        );
142        self
143    }
144}
145
146fn secret_key_stable_to_next(secret_key: &SecretKey) -> iroh_next::SecretKey {
147    iroh_next::SecretKey::from_bytes(&secret_key.to_bytes())
148}
149
150fn endpoint_id_stable_to_next(node_id: NodeId) -> anyhow::Result<EndpointId> {
151    Ok(EndpointId::from_bytes(node_id.as_bytes())?)
152}
153
154#[async_trait]
155impl<M> IP2PConnector<M> for IrohConnector
156where
157    M: Encodable + Decodable + Serialize + DeserializeOwned + Send + 'static,
158{
159    fn peers(&self) -> Vec<PeerId> {
160        self.endpoint_ids.keys().copied().collect()
161    }
162
163    async fn connect(&self, peer: PeerId) -> anyhow::Result<DynP2PConnection<M>> {
164        let endpoint_id = *self
165            .endpoint_ids
166            .get(&peer)
167            .expect("No endpoint id found for peer");
168
169        let connection = match self.connection_overrides.get(&endpoint_id) {
170            Some(endpoint_addr) => {
171                trace!(target: LOG_NET_IROH, %endpoint_id, "Using a connectivity override for connection");
172                self.endpoint
173                    .connect(endpoint_addr.clone(), FEDIMINT_P2P_ALPN)
174                    .await?
175            }
176            None => {
177                self.endpoint
178                    .connect(endpoint_id, FEDIMINT_P2P_ALPN)
179                    .await?
180            }
181        };
182
183        Ok(connection.into_dyn())
184    }
185
186    async fn accept(&self) -> anyhow::Result<(PeerId, DynP2PConnection<M>)> {
187        let connection = self
188            .endpoint
189            .accept()
190            .await
191            .context("Listener closed unexpectedly")?
192            .accept()?
193            .await?;
194
195        let endpoint_id = connection.remote_id();
196
197        let auth_peer = self
198            .endpoint_ids
199            .iter()
200            .find(|entry| entry.1 == &endpoint_id)
201            .with_context(|| format!("Endpoint id {endpoint_id} is unknown"))?
202            .0;
203
204        Ok((*auth_peer, connection.into_dyn()))
205    }
206
207    fn connection_type(&self, _peer: PeerId) -> Option<ConnectionType> {
208        // Iroh 1.0 reports paths on live connections rather than the endpoint.
209        None
210    }
211}