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