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 ServerError::Connection(anyhow::format_err!(
299 "Iroh api secrets currently not supported"
300 ));
301 }
302 let node_id =
303 Self::node_id_from_url(url).map_err(|source| ServerError::InvalidPeerUrl {
304 source,
305 url: url.to_owned(),
306 })?;
307 let next_only = crate::is_iroh_next_endpoint_url(url).map_err(|source| {
308 ServerError::InvalidPeerUrl {
309 source,
310 url: url.to_owned(),
311 }
312 })?;
313 let mut futures = FuturesUnordered::<
314 Pin<
315 Box<
316 dyn Future<Output = (ServerResult<DynGuaridianConnection>, &'static str)>
317 + Send,
318 >,
319 >,
320 >::new();
321 let connection_override = self.connection_overrides.get(&node_id).cloned();
322
323 if next_only {
326 return self
327 .make_new_connection_next(&self.next, node_id, connection_override)
328 .await
329 .map(super::IGuardianConnection::into_dyn);
330 }
331
332 let self_clone = self.clone();
333 futures.push(Box::pin({
334 let connection_override = connection_override.clone();
335 async move {
336 (
337 self_clone
338 .make_new_connection_stable(node_id, connection_override)
339 .await
340 .map(super::IGuardianConnection::into_dyn),
341 "stable",
342 )
343 }
344 }));
345
346 let self_clone = self.clone();
347 let endpoint_next = self.next.clone();
348 futures.push(Box::pin(async move {
349 (
350 self_clone
351 .make_new_connection_next(&endpoint_next, node_id, connection_override)
352 .await
353 .map(super::IGuardianConnection::into_dyn),
354 "next",
355 )
356 }));
357
358 let mut prev_err = None;
361
362 while let Some((result, iroh_stack)) = futures.next().await {
364 match result {
365 Ok(connection) => return Ok(connection),
366 Err(err) => {
367 warn!(
368 target: LOG_NET_IROH,
369 err = %err.fmt_compact(),
370 %iroh_stack,
371 "Join error in iroh connection task"
372 );
373 prev_err = Some(err);
374 }
375 }
376 }
377
378 Err(prev_err.unwrap_or_else(|| {
379 ServerError::ServerError(anyhow::anyhow!("Both iroh connection attempts failed"))
380 }))
381 }
382
383 async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
384 let node_id = Self::node_id_from_url(url)?;
385 if let Some(node_addr) = self.connection_overrides.get(&node_id).cloned() {
386 let conn = self
387 .stable
388 .connect(node_addr.clone(), FEDIMINT_GATEWAY_ALPN)
389 .await?;
390
391 #[cfg(not(target_family = "wasm"))]
392 Self::spawn_connection_monitoring_stable(
393 &self.stable,
394 node_id,
395 self.path_change.clone(),
396 );
397
398 Ok(IGatewayConnection::into_dyn(conn))
399 } else {
400 let conn = self.stable.connect(node_id, FEDIMINT_GATEWAY_ALPN).await?;
401 Ok(IGatewayConnection::into_dyn(conn))
402 }
403 }
404
405 fn connectivity(&self, url: &SafeUrl) -> Connectivity {
406 let Ok(node_id) = Self::node_id_from_url(url) else {
407 return Connectivity::Unknown;
408 };
409 let Ok(watcher) = self.stable.conn_type(node_id) else {
410 return Connectivity::Unknown;
411 };
412 match watcher.get() {
413 Ok(iroh::endpoint::ConnectionType::Direct(_)) => Connectivity::Direct,
414 Ok(iroh::endpoint::ConnectionType::Relay(_)) => Connectivity::Relay,
415 Ok(iroh::endpoint::ConnectionType::Mixed(..)) => Connectivity::Mixed,
416 Ok(iroh::endpoint::ConnectionType::None) | Err(_) => Connectivity::Unknown,
417 }
418 }
419
420 async fn iroh_peer_info(
421 &self,
422 url: &SafeUrl,
423 path_timeout: Duration,
424 ) -> ServerResult<Option<IrohPeerInfo>> {
425 let node_id =
426 Self::node_id_from_url(url).map_err(|source| ServerError::InvalidPeerUrl {
427 source,
428 url: url.to_owned(),
429 })?;
430 let connection_override = self.connection_overrides.get(&node_id).cloned();
431 let _connection = self
432 .make_new_connection_stable(node_id, connection_override)
433 .await?;
434
435 let mut conn_type_watcher = self
436 .stable
437 .conn_type(node_id)
438 .map_err(ServerError::Connection)?;
439 let mut conn_type = conn_type_watcher
440 .get()
441 .unwrap_or(iroh::endpoint::ConnectionType::None);
442
443 if path_timeout > Duration::ZERO {
444 let timeout = fedimint_core::runtime::sleep(path_timeout);
445 tokio::pin!(timeout);
446
447 while !matches!(
448 conn_type,
449 iroh::endpoint::ConnectionType::Direct(_)
450 | iroh::endpoint::ConnectionType::Mixed(..)
451 ) {
452 tokio::select! {
453 () = &mut timeout => break,
454 updated = conn_type_watcher.updated() => {
455 match updated {
456 Ok(updated) => conn_type = updated,
457 Err(_) => break,
458 }
459 }
460 }
461 }
462 }
463
464 Ok(Some(self.iroh_peer_info_from_conn_type(node_id, conn_type)))
465 }
466}
467
468impl IrohConnector {
469 fn iroh_peer_info_from_conn_type(
470 &self,
471 node_id: NodeId,
472 conn_type: iroh::endpoint::ConnectionType,
473 ) -> IrohPeerInfo {
474 let remote_info = self.stable.remote_info(node_id);
475
476 let direct_addr = match &conn_type {
477 iroh::endpoint::ConnectionType::Direct(addr)
478 | iroh::endpoint::ConnectionType::Mixed(addr, _) => Some(*addr),
479 iroh::endpoint::ConnectionType::Relay(_) | iroh::endpoint::ConnectionType::None => None,
480 };
481
482 let mut known_direct_addrs = remote_info
483 .as_ref()
484 .map(|info| {
485 info.addrs
486 .iter()
487 .map(|addr_info| addr_info.addr)
488 .collect::<BTreeSet<_>>()
489 })
490 .unwrap_or_default();
491 if let Some(direct_addr) = direct_addr {
492 known_direct_addrs.insert(direct_addr);
493 }
494
495 let relay_url = match &conn_type {
496 iroh::endpoint::ConnectionType::Relay(relay_url)
497 | iroh::endpoint::ConnectionType::Mixed(_, relay_url) => Some(relay_url.to_string()),
498 iroh::endpoint::ConnectionType::Direct(_) | iroh::endpoint::ConnectionType::None => {
499 remote_info.and_then(|info| info.relay_url.map(|relay| relay.relay_url.to_string()))
500 }
501 };
502
503 IrohPeerInfo {
504 node_id: node_id.to_string(),
505 connectivity: connectivity_from_iroh_conn_type(&conn_type),
506 direct_addr,
507 known_direct_addrs: known_direct_addrs.into_iter().collect(),
508 relay_url,
509 }
510 }
511
512 #[cfg(not(target_family = "wasm"))]
513 fn spawn_connection_monitoring_stable(
514 endpoint: &Endpoint,
515 node_id: NodeId,
516 path_change: Arc<watch::Sender<u64>>,
517 ) {
518 if let Ok(mut conn_type_watcher) = endpoint.conn_type(node_id) {
519 #[allow(clippy::let_underscore_future)]
520 let _ = spawn("iroh connection (stable)", async move {
521 if let Ok(conn_type) = conn_type_watcher.get() {
522 debug!(target: LOG_NET_IROH, %node_id, type = %conn_type, "Connection type (initial)");
523 }
524 while let Ok(event) = conn_type_watcher.updated().await {
525 debug!(target: LOG_NET_IROH, %node_id, type = %event, "Connection type (changed)");
526 path_change.send_modify(|c| *c = c.wrapping_add(1));
527 }
528 });
529 }
530 }
531
532 #[cfg(not(target_family = "wasm"))]
533 fn spawn_connection_monitoring_next(
534 conn: &iroh_next::endpoint::Connection,
535 node_id: iroh_next::EndpointId,
536 path_change: Arc<watch::Sender<u64>>,
537 ) {
538 let conn = conn.clone();
539 #[allow(clippy::let_underscore_future)]
540 let _ = spawn("iroh connection (next)", async move {
541 let mut paths = conn.paths_stream();
542 if let Some(paths) = paths.next().await {
543 debug!(target: LOG_NET_IROH, %node_id, ?paths, "Connection paths (initial)");
544 }
545 while let Some(paths) = paths.next().await {
546 debug!(target: LOG_NET_IROH, %node_id, ?paths, "Connection paths changed");
547 path_change.send_modify(|c| *c = c.wrapping_add(1));
548 }
549 });
550 }
551
552 async fn make_new_connection_stable(
553 &self,
554 node_id: NodeId,
555 node_addr: Option<NodeAddr>,
556 ) -> ServerResult<Connection> {
557 trace!(target: LOG_NET_IROH, %node_id, "Creating new stable connection");
558 let conn = match node_addr.clone() {
559 Some(node_addr) => {
560 trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
561 let conn = self.stable
562 .connect(node_addr.clone(), FEDIMINT_API_ALPN)
563 .await;
564
565 #[cfg(not(target_family = "wasm"))]
566 if conn.is_ok() {
567 Self::spawn_connection_monitoring_stable(
568 &self.stable,
569 node_id,
570 self.path_change.clone(),
571 );
572 }
573 conn
574 }
575 None => self.stable.connect(node_id, FEDIMINT_API_ALPN).await,
576 }.map_err(ServerError::Connection)?;
577
578 Ok(conn)
579 }
580
581 async fn make_new_connection_next(
582 &self,
583 endpoint_next: &iroh_next::Endpoint,
584 node_id: NodeId,
585 node_addr: Option<NodeAddr>,
586 ) -> ServerResult<iroh_next::endpoint::Connection> {
587 let next_node_id =
588 iroh_next::EndpointId::from_bytes(node_id.as_bytes()).expect("Can't fail");
589
590 let endpoint_next = endpoint_next.clone();
591
592 trace!(target: LOG_NET_IROH, %node_id, "Creating new next connection");
593 let conn = match node_addr.clone() {
594 Some(node_addr) => {
595 trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
596 let node_addr = node_addr_stable_to_next(&node_addr);
597 let conn = endpoint_next
598 .connect(node_addr.clone(), FEDIMINT_API_ALPN)
599 .await;
600
601 #[cfg(not(target_family = "wasm"))]
602 if let Ok(conn) = &conn {
603 Self::spawn_connection_monitoring_next(
604 conn,
605 node_addr.id,
606 self.path_change.clone(),
607 );
608 }
609
610 conn
611 }
612 None => endpoint_next.connect(
613 next_node_id,
614 FEDIMINT_API_ALPN
615 ).await,
616 }
617 .map_err(Into::into)
618 .map_err(ServerError::Connection)?;
619
620 Ok(conn)
621 }
622}
623
624fn quic_transport_config() -> iroh::endpoint::TransportConfig {
627 let mut config = iroh::endpoint::TransportConfig::default();
628 config.max_idle_timeout(Some(
629 IROH_IDLE_TIMEOUT
630 .try_into()
631 .expect("idle timeout fits in IdleTimeout"),
632 ));
633 config.keep_alive_interval(Some(IROH_KEEP_ALIVE_INTERVAL));
634 config
635}
636
637fn quic_transport_config_next() -> iroh_next::endpoint::QuicTransportConfig {
640 iroh_next::endpoint::QuicTransportConfig::builder()
641 .max_idle_timeout(Some(
642 IROH_IDLE_TIMEOUT
643 .try_into()
644 .expect("idle timeout fits in IdleTimeout"),
645 ))
646 .keep_alive_interval(IROH_KEEP_ALIVE_INTERVAL)
647 .build()
648}
649
650fn connectivity_from_iroh_conn_type(conn_type: &iroh::endpoint::ConnectionType) -> Connectivity {
651 match conn_type {
652 iroh::endpoint::ConnectionType::Direct(_) => Connectivity::Direct,
653 iroh::endpoint::ConnectionType::Relay(_) => Connectivity::Relay,
654 iroh::endpoint::ConnectionType::Mixed(..) => Connectivity::Mixed,
655 iroh::endpoint::ConnectionType::None => Connectivity::Unknown,
656 }
657}
658
659fn node_addr_stable_to_next(stable: &iroh::NodeAddr) -> iroh_next::EndpointAddr {
660 let next_node_id =
661 iroh_next::EndpointId::from_bytes(stable.node_id.as_bytes()).expect("Can't fail");
662 let relay_addrs = stable.relay_url.iter().map(|u| {
663 iroh_next::TransportAddr::Relay(
664 iroh_next::RelayUrl::from_str(&u.to_string()).expect("Can't fail"),
665 )
666 });
667 let direct_addrs = stable
668 .direct_addresses
669 .iter()
670 .copied()
671 .map(iroh_next::TransportAddr::Ip);
672
673 iroh_next::EndpointAddr::from_parts(next_node_id, relay_addrs.chain(direct_addrs))
674}
675
676#[apply(async_trait_maybe_send!)]
677impl IConnection for Connection {
678 async fn await_disconnection(&self) {
679 self.closed().await;
680 }
681
682 fn is_connected(&self) -> bool {
683 self.close_reason().is_none()
684 }
685}
686
687#[async_trait]
688impl IGuardianConnection for Connection {
689 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value> {
690 let timeout = request_timeout_for_method(&method);
691 let method_str = method.to_string();
692 let json = serde_json::to_vec(&IrohApiRequest { method, request })
693 .expect("Serialization to vec can't fail");
694
695 let result = fedimint_core::runtime::timeout(timeout, async {
696 let (mut sink, mut stream) = self
697 .open_bi()
698 .await
699 .map_err(|e| ServerError::Transport(e.into()))?;
700
701 sink.write_all(&json)
702 .await
703 .map_err(|e| ServerError::Transport(e.into()))?;
704
705 sink.finish()
706 .map_err(|e| ServerError::Transport(e.into()))?;
707
708 stream
709 .read_to_end(IROH_MAX_RESPONSE_BYTES)
710 .await
711 .map_err(|e| ServerError::Transport(e.into()))
712 })
713 .await;
714
715 let response = match result {
716 Ok(Ok(bytes)) => bytes,
717 Ok(Err(err)) => return Err(err),
718 Err(_) => {
719 warn!(
726 target: LOG_NET_IROH,
727 method = %method_str,
728 timeout_secs = timeout.as_secs(),
729 "iroh request timed out, closing connection",
730 );
731 self.close(
732 iroh::endpoint::VarInt::from_u32(IROH_REQUEST_TIMEOUT_ERROR_CODE),
733 IROH_REQUEST_TIMEOUT_ERROR_REASON,
734 );
735 return Err(ServerError::Transport(anyhow::anyhow!(
736 "iroh request {method_str} timed out after {timeout:?}"
737 )));
738 }
739 };
740
741 let response = serde_json::from_slice::<Result<Value, ApiError>>(&response)
743 .map_err(|e| ServerError::InvalidResponse(e.into()))?;
744
745 response.map_err(|e| ServerError::InvalidResponse(anyhow::anyhow!("Api Error: {:?}", e)))
746 }
747}
748
749#[apply(async_trait_maybe_send!)]
750impl IConnection for iroh_next::endpoint::Connection {
751 async fn await_disconnection(&self) {
752 self.closed().await;
753 }
754
755 fn is_connected(&self) -> bool {
756 self.close_reason().is_none()
757 }
758}
759
760#[async_trait]
761impl IGuardianConnection for iroh_next::endpoint::Connection {
762 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value> {
763 let timeout = request_timeout_for_method(&method);
764 let method_str = method.to_string();
765 let json = serde_json::to_vec(&IrohApiRequest { method, request })
766 .expect("Serialization to vec can't fail");
767
768 let result = fedimint_core::runtime::timeout(timeout, async {
769 let (mut sink, mut stream) = self
770 .open_bi()
771 .await
772 .map_err(|e| ServerError::Transport(e.into()))?;
773
774 sink.write_all(&json)
775 .await
776 .map_err(|e| ServerError::Transport(e.into()))?;
777
778 sink.finish()
779 .map_err(|e| ServerError::Transport(e.into()))?;
780
781 stream
782 .read_to_end(IROH_MAX_RESPONSE_BYTES)
783 .await
784 .map_err(|e| ServerError::Transport(e.into()))
785 })
786 .await;
787
788 let response = match result {
789 Ok(Ok(bytes)) => bytes,
790 Ok(Err(err)) => return Err(err),
791 Err(_) => {
792 warn!(
793 target: LOG_NET_IROH,
794 method = %method_str,
795 timeout_secs = timeout.as_secs(),
796 "iroh request timed out, closing connection",
797 );
798 self.close(
799 iroh_next::endpoint::VarInt::from_u32(IROH_REQUEST_TIMEOUT_ERROR_CODE),
800 IROH_REQUEST_TIMEOUT_ERROR_REASON,
801 );
802 return Err(ServerError::Transport(anyhow::anyhow!(
803 "iroh request {method_str} timed out after {timeout:?}"
804 )));
805 }
806 };
807
808 let response = serde_json::from_slice::<Result<Value, ApiError>>(&response)
810 .map_err(|e| ServerError::InvalidResponse(e.into()))?;
811
812 response.map_err(|e| ServerError::InvalidResponse(anyhow::anyhow!("Api Error: {:?}", e)))
813 }
814}
815
816#[apply(async_trait_maybe_send!)]
817impl IGatewayConnection for Connection {
818 async fn request(
819 &self,
820 password: Option<String>,
821 _method: Method,
822 route: &str,
823 payload: Option<Value>,
824 ) -> ServerResult<Value> {
825 let iroh_request = IrohGatewayRequest {
826 route: route.to_string(),
827 params: payload,
828 password,
829 };
830 let json = serde_json::to_vec(&iroh_request).expect("serialization cant fail");
831
832 let (mut sink, mut stream) = self
833 .open_bi()
834 .await
835 .map_err(|e| ServerError::Transport(e.into()))?;
836
837 sink.write_all(&json)
838 .await
839 .map_err(|e| ServerError::Transport(e.into()))?;
840
841 sink.finish()
842 .map_err(|e| ServerError::Transport(e.into()))?;
843
844 let response = stream
845 .read_to_end(IROH_MAX_RESPONSE_BYTES)
846 .await
847 .map_err(|e| ServerError::Transport(e.into()))?;
848
849 let response = serde_json::from_slice::<IrohGatewayResponse>(&response)
850 .map_err(|e| ServerError::InvalidResponse(e.into()))?;
851 match StatusCode::from_u16(response.status).map_err(|e| {
852 ServerError::InvalidResponse(anyhow::anyhow!("Invalid status code: {}", e))
853 })? {
854 StatusCode::OK => Ok(response.body),
855 status => Err(ServerError::ServerError(anyhow::anyhow!(
856 "Server returned status code: {}",
857 status
858 ))),
859 }
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use std::str::FromStr as _;
866
867 use fedimint_core::PeerId;
868 use fedimint_core::config::FederationId;
869 use fedimint_core::invite_code::InviteCode;
870 use fedimint_core::module::ApiMethod;
871 use fedimint_core::util::SafeUrl;
872
873 use super::{
874 IROH_REQUEST_TIMEOUT_DEFAULT, IROH_REQUEST_TIMEOUT_LONG_POLL, request_timeout_for_method,
875 };
876 use crate::{iroh_next_endpoint_url, is_iroh_next_endpoint_url, preserve_iroh_next_marker};
877
878 const TEST_ENDPOINT_ID: &str =
879 "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c";
880
881 #[test]
882 fn advertised_iroh_next_url_selects_only_the_next_stack() {
883 let next_url = iroh_next_endpoint_url(TEST_ENDPOINT_ID).expect("valid endpoint ID");
884 assert!(is_iroh_next_endpoint_url(&next_url).expect("valid Iroh API URL path"));
885
886 let invite = InviteCode::new(next_url, PeerId::from(0), FederationId::dummy(), None);
887 let round_tripped =
888 InviteCode::from_str(&invite.to_string()).expect("invite code round-trips");
889 assert!(is_iroh_next_endpoint_url(&round_tripped.url()).expect("valid Iroh API URL path"));
890
891 let stable_url =
892 SafeUrl::parse(&format!("iroh://{TEST_ENDPOINT_ID}")).expect("valid Iroh URL");
893 assert!(!is_iroh_next_endpoint_url(&stable_url).expect("valid Iroh API URL path"));
894
895 let replacement = SafeUrl::parse(
896 "iroh://d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
897 )
898 .expect("valid replacement URL");
899 let replacement = preserve_iroh_next_marker(&round_tripped.url(), &replacement);
900 assert!(is_iroh_next_endpoint_url(&replacement).expect("valid Iroh API URL path"));
901 }
902
903 #[test]
904 fn unknown_iroh_api_version_path_is_rejected() {
905 let url = SafeUrl::parse(&format!("iroh://{TEST_ENDPOINT_ID}/v2")).expect("valid Iroh URL");
906 assert!(is_iroh_next_endpoint_url(&url).is_err());
907 }
908
909 const AWAIT_ENDPOINTS: &[&str] = &[
915 "await_output_outcome",
917 "await_outputs_outcomes",
918 "await_session_outcome",
919 "await_signed_session_outcome",
920 "await_transaction",
921 "await_account",
923 "await_block_height",
924 "await_offer",
925 "await_outgoing_contract_cancelled",
926 "await_preimage_decryption",
927 "await_incoming_contract",
929 "await_incoming_contracts",
930 "await_preimage",
931 ];
932
933 const PROMPT_ENDPOINTS: &[&str] = &[
936 "block_count",
937 "session_count",
938 "session_status",
939 "status",
940 "version",
941 "client_config",
942 "audit",
943 "account",
944 "offer",
945 "list_gateways",
946 "submit_transaction",
947 "consensus_block_count",
948 ];
949
950 #[test]
951 fn await_prefix_gets_long_poll_timeout() {
952 for name in AWAIT_ENDPOINTS {
953 assert_eq!(
954 request_timeout_for_method(&ApiMethod::Core((*name).to_owned())),
955 IROH_REQUEST_TIMEOUT_LONG_POLL,
956 "core endpoint {name} should map to the long-poll timeout"
957 );
958 assert_eq!(
959 request_timeout_for_method(&ApiMethod::Module(0, (*name).to_owned())),
960 IROH_REQUEST_TIMEOUT_LONG_POLL,
961 "module endpoint {name} should map to the long-poll timeout"
962 );
963 }
964 }
965
966 #[test]
967 fn wait_prefix_also_gets_long_poll_timeout() {
968 assert_eq!(
972 request_timeout_for_method(&ApiMethod::Core("wait_for_event".to_owned())),
973 IROH_REQUEST_TIMEOUT_LONG_POLL,
974 );
975 }
976
977 #[test]
978 fn prompt_endpoints_get_default_timeout() {
979 for name in PROMPT_ENDPOINTS {
980 assert_eq!(
981 request_timeout_for_method(&ApiMethod::Core((*name).to_owned())),
982 IROH_REQUEST_TIMEOUT_DEFAULT,
983 "endpoint {name} should map to the default timeout"
984 );
985 }
986 }
987
988 #[test]
989 fn endpoints_that_merely_contain_await_are_not_misclassified() {
990 assert_eq!(
994 request_timeout_for_method(&ApiMethod::Core("submit_await_thing".to_owned())),
995 IROH_REQUEST_TIMEOUT_DEFAULT,
996 );
997 }
998}