fedimint_server/net/p2p_connector/
iroh.rs1mod 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
27pub 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)]
39pub struct IrohConnector {
45 pub(crate) endpoint_ids: BTreeMap<PeerId, EndpointId>,
47 pub(crate) endpoint: Endpoint,
49 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 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 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 None
210 }
211}