1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::net::SocketAddr;
4use std::pin::Pin;
5use std::str::FromStr;
6use std::sync::Arc;
7use std::time::Duration;
8
9use anyhow::{Context, bail};
10use async_trait::async_trait;
11use fedimint_core::config::ALEPH_BFT_UNIT_BYTE_LIMIT;
12use fedimint_core::envs::{
13 FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV, FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV,
14 FM_IROH_N0_DISCOVERY_ENABLE_ENV, FM_IROH_PKARR_RESOLVER_ENABLE_ENV, is_env_var_set_opt,
15 parse_kv_list_from_env,
16};
17use fedimint_core::module::{
18 ApiError, ApiMethod, ApiRequestErased, FEDIMINT_API_ALPN, FEDIMINT_GATEWAY_ALPN,
19 IrohApiRequest, IrohGatewayRequest, IrohGatewayResponse,
20};
21use fedimint_core::net::iroh::{IROH_IDLE_TIMEOUT, IROH_KEEP_ALIVE_INTERVAL};
22
23const IROH_MAX_RESPONSE_BYTES: usize = ALEPH_BFT_UNIT_BYTE_LIMIT * 3600 * 4 * 2;
32
33const IROH_REQUEST_TIMEOUT_DEFAULT: Duration = Duration::from_secs(60);
40
41const IROH_REQUEST_TIMEOUT_LONG_POLL: Duration = Duration::from_secs(60 * 60);
48
49const IROH_REQUEST_TIMEOUT_ERROR_CODE: u32 = 1;
55const IROH_REQUEST_TIMEOUT_ERROR_REASON: &[u8] = b"request timeout";
56
57fn request_timeout_for_method(method: &ApiMethod) -> Duration {
67 let name = match method {
68 ApiMethod::Core(name) => name.as_str(),
69 ApiMethod::Module(_, name) => name.as_str(),
70 };
71 if name.starts_with("await_") || name.starts_with("wait_") {
72 IROH_REQUEST_TIMEOUT_LONG_POLL
73 } else {
74 IROH_REQUEST_TIMEOUT_DEFAULT
75 }
76}
77use fedimint_core::task::spawn;
78use fedimint_core::util::{FmtCompact as _, SafeUrl};
79use fedimint_core::{apply, async_trait_maybe_send};
80use fedimint_logging::LOG_NET_IROH;
81use futures::Future;
82use futures::stream::{FuturesUnordered, StreamExt};
83use iroh::discovery::pkarr::PkarrResolver;
84use iroh::endpoint::Connection;
85use iroh::{Endpoint, NodeAddr, NodeId, PublicKey};
86use reqwest::{Method, StatusCode};
87use serde_json::Value;
88use tokio::sync::watch;
89use tracing::{debug, trace, warn};
90
91use super::{DynGuaridianConnection, IGuardianConnection, ServerError, ServerResult};
92use crate::{Connectivity, DynGatewayConnection, IConnection, IGatewayConnection, IrohPeerInfo};
93
94#[derive(Clone)]
95pub(crate) struct IrohConnector {
96 stable: iroh::endpoint::Endpoint,
97 next: iroh_next::endpoint::Endpoint,
98
99 connection_overrides: BTreeMap<NodeId, NodeAddr>,
105
106 path_change: Arc<watch::Sender<u64>>,
111}
112
113impl fmt::Debug for IrohConnector {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.debug_struct("IrohEndpoint")
116 .field("stable-id", &self.stable.node_id())
117 .field("next-id", &self.next.id())
118 .finish_non_exhaustive()
119 }
120}
121
122impl IrohConnector {
123 pub async fn new(
124 iroh_dns: Option<SafeUrl>,
125 iroh_enable_dht: bool,
126 path_change: Arc<watch::Sender<u64>>,
127 ) -> anyhow::Result<Self> {
128 let mut s = Self::new_no_overrides(iroh_dns, iroh_enable_dht, path_change).await?;
129
130 for env_var in [
137 FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV,
138 FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV,
139 ] {
140 for (k, v) in parse_kv_list_from_env::<NodeId, SocketAddr>(env_var)? {
141 s = s.with_connection_override(k, NodeAddr::new(k).with_direct_addresses([v]));
142 }
143 }
144
145 Ok(s)
146 }
147
148 #[allow(clippy::too_many_lines)]
149 pub async fn new_no_overrides(
150 iroh_dns: Option<SafeUrl>,
151 iroh_enable_dht: bool,
152 path_change: Arc<watch::Sender<u64>>,
153 ) -> anyhow::Result<Self> {
154 let endpoint_stable = Box::pin({
155 let iroh_dns = iroh_dns.clone();
156 async {
157 let mut builder = Endpoint::builder();
158
159 if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
160 builder = builder.add_discovery(|_| Some(PkarrResolver::new(iroh_dns)));
161 }
162
163 let mut builder = builder.relay_mode(iroh::RelayMode::Disabled);
165
166 #[cfg(not(target_family = "wasm"))]
167 if iroh_enable_dht {
168 builder = builder.discovery_dht();
169 }
170
171 {
174 if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
175 builder = builder.add_discovery(move |_| Some(PkarrResolver::n0_dns()));
176 } else {
177 warn!(
178 target: LOG_NET_IROH,
179 "Iroh pkarr resolver is disabled"
180 );
181 }
182
183 if is_env_var_set_opt(FM_IROH_N0_DISCOVERY_ENABLE_ENV).unwrap_or(true) {
184 #[cfg(not(target_family = "wasm"))]
185 {
186 builder = builder.add_discovery(move |_| {
187 Some(iroh::discovery::dns::DnsDiscovery::n0_dns())
188 });
189 }
190 } else {
191 warn!(
192 target: LOG_NET_IROH,
193 "Iroh n0 discovery is disabled"
194 );
195 }
196 }
197
198 let endpoint = builder
199 .transport_config(quic_transport_config())
200 .bind()
201 .await?;
202 debug!(
203 target: LOG_NET_IROH,
204 node_id = %endpoint.node_id(),
205 node_id_pkarr = %z32::encode(endpoint.node_id().as_bytes()),
206 "Iroh api client endpoint (stable)"
207 );
208 Ok::<_, anyhow::Error>(endpoint)
209 }
210 });
211 let endpoint_next = Box::pin(async {
212 let mut builder = iroh_next::Endpoint::builder(iroh_next::endpoint::presets::Minimal);
213
214 if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
215 builder = builder
216 .address_lookup(iroh_next::address_lookup::PkarrResolver::builder(iroh_dns));
217 }
218
219 let mut builder = builder.relay_mode(iroh_next::RelayMode::Default);
224
225 #[cfg(not(target_family = "wasm"))]
226 if iroh_enable_dht {
227 builder = builder
228 .address_lookup(iroh_mainline_address_lookup::DhtAddressLookup::builder());
229 }
230
231 {
234 builder =
236 builder.address_lookup(iroh_next::address_lookup::PkarrResolver::n0_dns());
237 #[cfg(not(target_family = "wasm"))]
239 {
240 builder = builder
241 .address_lookup(iroh_next::address_lookup::DnsAddressLookup::n0_dns());
242 }
243 }
244
245 let endpoint = builder
246 .transport_config(quic_transport_config_next())
247 .bind()
248 .await?;
249 debug!(
250 target: LOG_NET_IROH,
251 node_id = %endpoint.id(),
252 node_id_pkarr = %z32::encode(endpoint.id().as_bytes()),
253 "Iroh api client endpoint (next)"
254 );
255 Ok(endpoint)
256 });
257
258 let (endpoint_stable, endpoint_next) = tokio::try_join!(endpoint_stable, endpoint_next)?;
259
260 Ok(Self {
261 stable: endpoint_stable,
262 next: endpoint_next,
263 connection_overrides: BTreeMap::new(),
264 path_change,
265 })
266 }
267
268 pub fn with_connection_override(mut self, node: NodeId, addr: NodeAddr) -> Self {
269 self.connection_overrides.insert(node, addr);
270 self
271 }
272
273 pub fn node_id_from_url(url: &SafeUrl) -> anyhow::Result<NodeId> {
274 if url.scheme() != "iroh" {
275 bail!(
276 "Unsupported scheme: {}, passed to iroh endpoint handler",
277 url.scheme()
278 );
279 }
280 let host = url.host_str().context("Missing host string in Iroh URL")?;
281
282 let node_id = PublicKey::from_str(host).context("Failed to parse node id")?;
283
284 Ok(node_id)
285 }
286}
287
288#[async_trait::async_trait]
289impl crate::Connector for IrohConnector {
290 async fn connect_guardian(
291 &self,
292 url: &SafeUrl,
293 api_secret: Option<&str>,
294 ) -> ServerResult<DynGuaridianConnection> {
295 if api_secret.is_some() {
296 return Err(ServerError::Connection(anyhow::format_err!(
300 "Iroh api secrets currently not supported"
301 )));
302 }
303 let node_id =
304 Self::node_id_from_url(url).map_err(|source| ServerError::InvalidPeerUrl {
305 source,
306 url: url.to_owned(),
307 })?;
308 let next_only = crate::is_iroh_next_endpoint_url(url).map_err(|source| {
309 ServerError::InvalidPeerUrl {
310 source,
311 url: url.to_owned(),
312 }
313 })?;
314 let mut futures = FuturesUnordered::<
315 Pin<
316 Box<
317 dyn Future<Output = (ServerResult<DynGuaridianConnection>, &'static str)>
318 + Send,
319 >,
320 >,
321 >::new();
322 let connection_override = self.connection_overrides.get(&node_id).cloned();
323
324 if next_only {
327 return self
328 .make_new_connection_next(&self.next, node_id, connection_override)
329 .await
330 .map(super::IGuardianConnection::into_dyn);
331 }
332
333 let self_clone = self.clone();
334 futures.push(Box::pin({
335 let connection_override = connection_override.clone();
336 async move {
337 (
338 self_clone
339 .make_new_connection_stable(node_id, connection_override)
340 .await
341 .map(super::IGuardianConnection::into_dyn),
342 "stable",
343 )
344 }
345 }));
346
347 let self_clone = self.clone();
348 let endpoint_next = self.next.clone();
349 futures.push(Box::pin(async move {
350 (
351 self_clone
352 .make_new_connection_next(&endpoint_next, node_id, connection_override)
353 .await
354 .map(super::IGuardianConnection::into_dyn),
355 "next",
356 )
357 }));
358
359 let mut prev_err = None;
362
363 while let Some((result, iroh_stack)) = futures.next().await {
365 match result {
366 Ok(connection) => return Ok(connection),
367 Err(err) => {
368 warn!(
369 target: LOG_NET_IROH,
370 err = %err.fmt_compact(),
371 %iroh_stack,
372 "Join error in iroh connection task"
373 );
374 prev_err = Some(err);
375 }
376 }
377 }
378
379 Err(prev_err.unwrap_or_else(|| {
380 ServerError::ServerError(anyhow::anyhow!("Both iroh connection attempts failed"))
381 }))
382 }
383
384 async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
385 let node_id = Self::node_id_from_url(url)?;
386 if let Some(node_addr) = self.connection_overrides.get(&node_id).cloned() {
387 let conn = self
388 .stable
389 .connect(node_addr.clone(), FEDIMINT_GATEWAY_ALPN)
390 .await?;
391
392 #[cfg(not(target_family = "wasm"))]
393 Self::spawn_connection_monitoring_stable(
394 &self.stable,
395 node_id,
396 self.path_change.clone(),
397 );
398
399 Ok(IGatewayConnection::into_dyn(conn))
400 } else {
401 let conn = self.stable.connect(node_id, FEDIMINT_GATEWAY_ALPN).await?;
402 Ok(IGatewayConnection::into_dyn(conn))
403 }
404 }
405
406 fn connectivity(&self, url: &SafeUrl) -> Connectivity {
407 let Ok(node_id) = Self::node_id_from_url(url) else {
408 return Connectivity::Unknown;
409 };
410 let Ok(watcher) = self.stable.conn_type(node_id) else {
411 return Connectivity::Unknown;
412 };
413 match watcher.get() {
414 Ok(iroh::endpoint::ConnectionType::Direct(_)) => Connectivity::Direct,
415 Ok(iroh::endpoint::ConnectionType::Relay(_)) => Connectivity::Relay,
416 Ok(iroh::endpoint::ConnectionType::Mixed(..)) => Connectivity::Mixed,
417 Ok(iroh::endpoint::ConnectionType::None) | Err(_) => Connectivity::Unknown,
418 }
419 }
420
421 async fn iroh_peer_info(
422 &self,
423 url: &SafeUrl,
424 path_timeout: Duration,
425 ) -> ServerResult<Option<IrohPeerInfo>> {
426 let node_id =
427 Self::node_id_from_url(url).map_err(|source| ServerError::InvalidPeerUrl {
428 source,
429 url: url.to_owned(),
430 })?;
431 let connection_override = self.connection_overrides.get(&node_id).cloned();
432 let _connection = self
433 .make_new_connection_stable(node_id, connection_override)
434 .await?;
435
436 let mut conn_type_watcher = self
437 .stable
438 .conn_type(node_id)
439 .map_err(ServerError::Connection)?;
440 let mut conn_type = conn_type_watcher
441 .get()
442 .unwrap_or(iroh::endpoint::ConnectionType::None);
443
444 if path_timeout > Duration::ZERO {
445 let timeout = fedimint_core::runtime::sleep(path_timeout);
446 tokio::pin!(timeout);
447
448 while !matches!(
449 conn_type,
450 iroh::endpoint::ConnectionType::Direct(_)
451 | iroh::endpoint::ConnectionType::Mixed(..)
452 ) {
453 tokio::select! {
454 () = &mut timeout => break,
455 updated = conn_type_watcher.updated() => {
456 match updated {
457 Ok(updated) => conn_type = updated,
458 Err(_) => break,
459 }
460 }
461 }
462 }
463 }
464
465 Ok(Some(self.iroh_peer_info_from_conn_type(node_id, conn_type)))
466 }
467}
468
469impl IrohConnector {
470 fn iroh_peer_info_from_conn_type(
471 &self,
472 node_id: NodeId,
473 conn_type: iroh::endpoint::ConnectionType,
474 ) -> IrohPeerInfo {
475 let remote_info = self.stable.remote_info(node_id);
476
477 let direct_addr = match &conn_type {
478 iroh::endpoint::ConnectionType::Direct(addr)
479 | iroh::endpoint::ConnectionType::Mixed(addr, _) => Some(*addr),
480 iroh::endpoint::ConnectionType::Relay(_) | iroh::endpoint::ConnectionType::None => None,
481 };
482
483 let mut known_direct_addrs = remote_info
484 .as_ref()
485 .map(|info| {
486 info.addrs
487 .iter()
488 .map(|addr_info| addr_info.addr)
489 .collect::<BTreeSet<_>>()
490 })
491 .unwrap_or_default();
492 if let Some(direct_addr) = direct_addr {
493 known_direct_addrs.insert(direct_addr);
494 }
495
496 let relay_url = match &conn_type {
497 iroh::endpoint::ConnectionType::Relay(relay_url)
498 | iroh::endpoint::ConnectionType::Mixed(_, relay_url) => Some(relay_url.to_string()),
499 iroh::endpoint::ConnectionType::Direct(_) | iroh::endpoint::ConnectionType::None => {
500 remote_info.and_then(|info| info.relay_url.map(|relay| relay.relay_url.to_string()))
501 }
502 };
503
504 IrohPeerInfo {
505 node_id: node_id.to_string(),
506 connectivity: connectivity_from_iroh_conn_type(&conn_type),
507 direct_addr,
508 known_direct_addrs: known_direct_addrs.into_iter().collect(),
509 relay_url,
510 }
511 }
512
513 #[cfg(not(target_family = "wasm"))]
514 fn spawn_connection_monitoring_stable(
515 endpoint: &Endpoint,
516 node_id: NodeId,
517 path_change: Arc<watch::Sender<u64>>,
518 ) {
519 if let Ok(mut conn_type_watcher) = endpoint.conn_type(node_id) {
520 #[allow(clippy::let_underscore_future)]
521 let _ = spawn("iroh connection (stable)", async move {
522 if let Ok(conn_type) = conn_type_watcher.get() {
523 debug!(target: LOG_NET_IROH, %node_id, type = %conn_type, "Connection type (initial)");
524 }
525 while let Ok(event) = conn_type_watcher.updated().await {
526 debug!(target: LOG_NET_IROH, %node_id, type = %event, "Connection type (changed)");
527 path_change.send_modify(|c| *c = c.wrapping_add(1));
528 }
529 });
530 }
531 }
532
533 #[cfg(not(target_family = "wasm"))]
534 fn spawn_connection_monitoring_next(
535 conn: &iroh_next::endpoint::Connection,
536 node_id: iroh_next::EndpointId,
537 path_change: Arc<watch::Sender<u64>>,
538 ) {
539 let conn = conn.clone();
540 #[allow(clippy::let_underscore_future)]
541 let _ = spawn("iroh connection (next)", async move {
542 let mut paths = conn.paths_stream();
543 if let Some(paths) = paths.next().await {
544 debug!(target: LOG_NET_IROH, %node_id, ?paths, "Connection paths (initial)");
545 }
546 while let Some(paths) = paths.next().await {
547 debug!(target: LOG_NET_IROH, %node_id, ?paths, "Connection paths changed");
548 path_change.send_modify(|c| *c = c.wrapping_add(1));
549 }
550 });
551 }
552
553 async fn make_new_connection_stable(
554 &self,
555 node_id: NodeId,
556 node_addr: Option<NodeAddr>,
557 ) -> ServerResult<Connection> {
558 trace!(target: LOG_NET_IROH, %node_id, "Creating new stable connection");
559 let conn = match node_addr.clone() {
560 Some(node_addr) => {
561 trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
562 let conn = self.stable
563 .connect(node_addr.clone(), FEDIMINT_API_ALPN)
564 .await;
565
566 #[cfg(not(target_family = "wasm"))]
567 if conn.is_ok() {
568 Self::spawn_connection_monitoring_stable(
569 &self.stable,
570 node_id,
571 self.path_change.clone(),
572 );
573 }
574 conn
575 }
576 None => self.stable.connect(node_id, FEDIMINT_API_ALPN).await,
577 }.map_err(ServerError::Connection)?;
578
579 Ok(conn)
580 }
581
582 async fn make_new_connection_next(
583 &self,
584 endpoint_next: &iroh_next::Endpoint,
585 node_id: NodeId,
586 node_addr: Option<NodeAddr>,
587 ) -> ServerResult<iroh_next::endpoint::Connection> {
588 let next_node_id =
589 iroh_next::EndpointId::from_bytes(node_id.as_bytes()).expect("Can't fail");
590
591 let endpoint_next = endpoint_next.clone();
592
593 trace!(target: LOG_NET_IROH, %node_id, "Creating new next connection");
594 let conn = match node_addr.clone() {
595 Some(node_addr) => {
596 trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
597 let node_addr = node_addr_stable_to_next(&node_addr);
598 let conn = endpoint_next
599 .connect(node_addr.clone(), FEDIMINT_API_ALPN)
600 .await;
601
602 #[cfg(not(target_family = "wasm"))]
603 if let Ok(conn) = &conn {
604 Self::spawn_connection_monitoring_next(
605 conn,
606 node_addr.id,
607 self.path_change.clone(),
608 );
609 }
610
611 conn
612 }
613 None => endpoint_next.connect(
614 next_node_id,
615 FEDIMINT_API_ALPN
616 ).await,
617 }
618 .map_err(Into::into)
619 .map_err(ServerError::Connection)?;
620
621 Ok(conn)
622 }
623}
624
625fn quic_transport_config() -> iroh::endpoint::TransportConfig {
628 let mut config = iroh::endpoint::TransportConfig::default();
629 config.max_idle_timeout(Some(
630 IROH_IDLE_TIMEOUT
631 .try_into()
632 .expect("idle timeout fits in IdleTimeout"),
633 ));
634 config.keep_alive_interval(Some(IROH_KEEP_ALIVE_INTERVAL));
635 config
636}
637
638fn quic_transport_config_next() -> iroh_next::endpoint::QuicTransportConfig {
641 iroh_next::endpoint::QuicTransportConfig::builder()
642 .max_idle_timeout(Some(
643 IROH_IDLE_TIMEOUT
644 .try_into()
645 .expect("idle timeout fits in IdleTimeout"),
646 ))
647 .keep_alive_interval(IROH_KEEP_ALIVE_INTERVAL)
648 .build()
649}
650
651fn connectivity_from_iroh_conn_type(conn_type: &iroh::endpoint::ConnectionType) -> Connectivity {
652 match conn_type {
653 iroh::endpoint::ConnectionType::Direct(_) => Connectivity::Direct,
654 iroh::endpoint::ConnectionType::Relay(_) => Connectivity::Relay,
655 iroh::endpoint::ConnectionType::Mixed(..) => Connectivity::Mixed,
656 iroh::endpoint::ConnectionType::None => Connectivity::Unknown,
657 }
658}
659
660fn node_addr_stable_to_next(stable: &iroh::NodeAddr) -> iroh_next::EndpointAddr {
661 let next_node_id =
662 iroh_next::EndpointId::from_bytes(stable.node_id.as_bytes()).expect("Can't fail");
663 let relay_addrs = stable.relay_url.iter().map(|u| {
664 iroh_next::TransportAddr::Relay(
665 iroh_next::RelayUrl::from_str(&u.to_string()).expect("Can't fail"),
666 )
667 });
668 let direct_addrs = stable
669 .direct_addresses
670 .iter()
671 .copied()
672 .map(iroh_next::TransportAddr::Ip);
673
674 iroh_next::EndpointAddr::from_parts(next_node_id, relay_addrs.chain(direct_addrs))
675}
676
677#[apply(async_trait_maybe_send!)]
678impl IConnection for Connection {
679 async fn await_disconnection(&self) {
680 self.closed().await;
681 }
682
683 fn is_connected(&self) -> bool {
684 self.close_reason().is_none()
685 }
686}
687
688#[async_trait]
689impl IGuardianConnection for Connection {
690 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value> {
691 let timeout = request_timeout_for_method(&method);
692 let method_str = method.to_string();
693 let json = serde_json::to_vec(&IrohApiRequest { method, request })
694 .expect("Serialization to vec can't fail");
695
696 let result = fedimint_core::runtime::timeout(timeout, async {
697 let (mut sink, mut stream) = self
698 .open_bi()
699 .await
700 .map_err(|e| ServerError::Transport(e.into()))?;
701
702 sink.write_all(&json)
703 .await
704 .map_err(|e| ServerError::Transport(e.into()))?;
705
706 sink.finish()
707 .map_err(|e| ServerError::Transport(e.into()))?;
708
709 stream
710 .read_to_end(IROH_MAX_RESPONSE_BYTES)
711 .await
712 .map_err(|e| ServerError::Transport(e.into()))
713 })
714 .await;
715
716 let response = match result {
717 Ok(Ok(bytes)) => bytes,
718 Ok(Err(err)) => return Err(err),
719 Err(_) => {
720 warn!(
727 target: LOG_NET_IROH,
728 method = %method_str,
729 timeout_secs = timeout.as_secs(),
730 "iroh request timed out, closing connection",
731 );
732 self.close(
733 iroh::endpoint::VarInt::from_u32(IROH_REQUEST_TIMEOUT_ERROR_CODE),
734 IROH_REQUEST_TIMEOUT_ERROR_REASON,
735 );
736 return Err(ServerError::Transport(anyhow::anyhow!(
737 "iroh request {method_str} timed out after {timeout:?}"
738 )));
739 }
740 };
741
742 let response = serde_json::from_slice::<Result<Value, ApiError>>(&response)
744 .map_err(|e| ServerError::InvalidResponse(e.into()))?;
745
746 response.map_err(|e| ServerError::InvalidResponse(anyhow::anyhow!("Api Error: {:?}", e)))
747 }
748}
749
750#[apply(async_trait_maybe_send!)]
751impl IConnection for iroh_next::endpoint::Connection {
752 async fn await_disconnection(&self) {
753 self.closed().await;
754 }
755
756 fn is_connected(&self) -> bool {
757 self.close_reason().is_none()
758 }
759}
760
761#[async_trait]
762impl IGuardianConnection for iroh_next::endpoint::Connection {
763 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value> {
764 let timeout = request_timeout_for_method(&method);
765 let method_str = method.to_string();
766 let json = serde_json::to_vec(&IrohApiRequest { method, request })
767 .expect("Serialization to vec can't fail");
768
769 let result = fedimint_core::runtime::timeout(timeout, async {
770 let (mut sink, mut stream) = self
771 .open_bi()
772 .await
773 .map_err(|e| ServerError::Transport(e.into()))?;
774
775 sink.write_all(&json)
776 .await
777 .map_err(|e| ServerError::Transport(e.into()))?;
778
779 sink.finish()
780 .map_err(|e| ServerError::Transport(e.into()))?;
781
782 stream
783 .read_to_end(IROH_MAX_RESPONSE_BYTES)
784 .await
785 .map_err(|e| ServerError::Transport(e.into()))
786 })
787 .await;
788
789 let response = match result {
790 Ok(Ok(bytes)) => bytes,
791 Ok(Err(err)) => return Err(err),
792 Err(_) => {
793 warn!(
794 target: LOG_NET_IROH,
795 method = %method_str,
796 timeout_secs = timeout.as_secs(),
797 "iroh request timed out, closing connection",
798 );
799 self.close(
800 iroh_next::endpoint::VarInt::from_u32(IROH_REQUEST_TIMEOUT_ERROR_CODE),
801 IROH_REQUEST_TIMEOUT_ERROR_REASON,
802 );
803 return Err(ServerError::Transport(anyhow::anyhow!(
804 "iroh request {method_str} timed out after {timeout:?}"
805 )));
806 }
807 };
808
809 let response = serde_json::from_slice::<Result<Value, ApiError>>(&response)
811 .map_err(|e| ServerError::InvalidResponse(e.into()))?;
812
813 response.map_err(|e| ServerError::InvalidResponse(anyhow::anyhow!("Api Error: {:?}", e)))
814 }
815}
816
817#[apply(async_trait_maybe_send!)]
818impl IGatewayConnection for Connection {
819 async fn request(
820 &self,
821 password: Option<String>,
822 _method: Method,
823 route: &str,
824 payload: Option<Value>,
825 ) -> ServerResult<Value> {
826 let iroh_request = IrohGatewayRequest {
827 route: route.to_string(),
828 params: payload,
829 password,
830 };
831 let json = serde_json::to_vec(&iroh_request).expect("serialization cant fail");
832
833 let (mut sink, mut stream) = self
834 .open_bi()
835 .await
836 .map_err(|e| ServerError::Transport(e.into()))?;
837
838 sink.write_all(&json)
839 .await
840 .map_err(|e| ServerError::Transport(e.into()))?;
841
842 sink.finish()
843 .map_err(|e| ServerError::Transport(e.into()))?;
844
845 let response = stream
846 .read_to_end(IROH_MAX_RESPONSE_BYTES)
847 .await
848 .map_err(|e| ServerError::Transport(e.into()))?;
849
850 let response = serde_json::from_slice::<IrohGatewayResponse>(&response)
851 .map_err(|e| ServerError::InvalidResponse(e.into()))?;
852 match StatusCode::from_u16(response.status).map_err(|e| {
853 ServerError::InvalidResponse(anyhow::anyhow!("Invalid status code: {}", e))
854 })? {
855 StatusCode::OK => Ok(response.body),
856 status => Err(ServerError::ServerError(anyhow::anyhow!(
857 "Server returned status code: {}",
858 status
859 ))),
860 }
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use std::str::FromStr as _;
867
868 use fedimint_core::PeerId;
869 use fedimint_core::config::FederationId;
870 use fedimint_core::invite_code::InviteCode;
871 use fedimint_core::module::ApiMethod;
872 use fedimint_core::util::SafeUrl;
873
874 use super::{
875 IROH_REQUEST_TIMEOUT_DEFAULT, IROH_REQUEST_TIMEOUT_LONG_POLL, request_timeout_for_method,
876 };
877 use crate::{iroh_next_endpoint_url, is_iroh_next_endpoint_url, preserve_iroh_next_marker};
878
879 const TEST_ENDPOINT_ID: &str =
880 "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c";
881
882 #[test]
883 fn advertised_iroh_next_url_selects_only_the_next_stack() {
884 let next_url = iroh_next_endpoint_url(TEST_ENDPOINT_ID).expect("valid endpoint ID");
885 assert!(is_iroh_next_endpoint_url(&next_url).expect("valid Iroh API URL path"));
886
887 let invite = InviteCode::new(next_url, PeerId::from(0), FederationId::dummy(), None);
888 let round_tripped =
889 InviteCode::from_str(&invite.to_string()).expect("invite code round-trips");
890 assert!(is_iroh_next_endpoint_url(&round_tripped.url()).expect("valid Iroh API URL path"));
891
892 let stable_url =
893 SafeUrl::parse(&format!("iroh://{TEST_ENDPOINT_ID}")).expect("valid Iroh URL");
894 assert!(!is_iroh_next_endpoint_url(&stable_url).expect("valid Iroh API URL path"));
895
896 let replacement = SafeUrl::parse(
897 "iroh://d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
898 )
899 .expect("valid replacement URL");
900 let replacement = preserve_iroh_next_marker(&round_tripped.url(), &replacement);
901 assert!(is_iroh_next_endpoint_url(&replacement).expect("valid Iroh API URL path"));
902 }
903
904 #[test]
905 fn unknown_iroh_api_version_path_is_rejected() {
906 let url = SafeUrl::parse(&format!("iroh://{TEST_ENDPOINT_ID}/v2")).expect("valid Iroh URL");
907 assert!(is_iroh_next_endpoint_url(&url).is_err());
908 }
909
910 const AWAIT_ENDPOINTS: &[&str] = &[
916 "await_output_outcome",
918 "await_outputs_outcomes",
919 "await_session_outcome",
920 "await_signed_session_outcome",
921 "await_transaction",
922 "await_account",
924 "await_block_height",
925 "await_offer",
926 "await_outgoing_contract_cancelled",
927 "await_preimage_decryption",
928 "await_incoming_contract",
930 "await_incoming_contracts",
931 "await_preimage",
932 ];
933
934 const PROMPT_ENDPOINTS: &[&str] = &[
937 "block_count",
938 "session_count",
939 "session_status",
940 "status",
941 "version",
942 "client_config",
943 "audit",
944 "account",
945 "offer",
946 "list_gateways",
947 "submit_transaction",
948 "consensus_block_count",
949 ];
950
951 #[test]
952 fn await_prefix_gets_long_poll_timeout() {
953 for name in AWAIT_ENDPOINTS {
954 assert_eq!(
955 request_timeout_for_method(&ApiMethod::Core((*name).to_owned())),
956 IROH_REQUEST_TIMEOUT_LONG_POLL,
957 "core endpoint {name} should map to the long-poll timeout"
958 );
959 assert_eq!(
960 request_timeout_for_method(&ApiMethod::Module(0, (*name).to_owned())),
961 IROH_REQUEST_TIMEOUT_LONG_POLL,
962 "module endpoint {name} should map to the long-poll timeout"
963 );
964 }
965 }
966
967 #[test]
968 fn wait_prefix_also_gets_long_poll_timeout() {
969 assert_eq!(
973 request_timeout_for_method(&ApiMethod::Core("wait_for_event".to_owned())),
974 IROH_REQUEST_TIMEOUT_LONG_POLL,
975 );
976 }
977
978 #[test]
979 fn prompt_endpoints_get_default_timeout() {
980 for name in PROMPT_ENDPOINTS {
981 assert_eq!(
982 request_timeout_for_method(&ApiMethod::Core((*name).to_owned())),
983 IROH_REQUEST_TIMEOUT_DEFAULT,
984 "endpoint {name} should map to the default timeout"
985 );
986 }
987 }
988
989 #[test]
990 fn endpoints_that_merely_contain_await_are_not_misclassified() {
991 assert_eq!(
995 request_timeout_for_method(&ApiMethod::Core("submit_await_thing".to_owned())),
996 IROH_REQUEST_TIMEOUT_DEFAULT,
997 );
998 }
999}