Skip to main content

fedimint_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::needless_lifetimes)]
12#![allow(clippy::ref_option)]
13#![allow(clippy::return_self_not_must_use)]
14#![allow(clippy::similar_names)]
15#![allow(clippy::too_many_lines)]
16#![allow(clippy::needless_pass_by_value)]
17#![allow(clippy::manual_let_else)]
18#![allow(clippy::match_wildcard_for_single_variants)]
19#![allow(clippy::trivially_copy_pass_by_ref)]
20
21//! Server side fedimint module traits
22
23extern crate fedimint_core;
24pub mod connection_limits;
25pub mod db;
26
27use std::net::SocketAddr;
28use std::path::{Path, PathBuf};
29use std::time::Duration;
30
31use anyhow::{Context, ensure};
32use bitcoin::hashes::hex::FromHex as _;
33use config::ServerConfig;
34use config::io::read_server_config;
35pub use connection_limits::ConnectionLimits;
36use fedimint_connectors::ConnectorRegistry;
37use fedimint_core::config::P2PMessage;
38use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
39use fedimint_core::epoch::ConsensusItem;
40use fedimint_core::module::ApiAuth;
41use fedimint_core::net::peers::DynP2PConnections;
42use fedimint_core::task::{TaskGroup, sleep};
43use fedimint_core::util::SafeUrl;
44use fedimint_logging::LOG_CONSENSUS;
45pub use fedimint_server_core as core;
46use fedimint_server_core::ServerModuleInitRegistry;
47use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
48use fedimint_server_core::dashboard_ui::DynDashboardApi;
49use fedimint_server_core::setup_ui::{DynSetupApi, ISetupApi};
50use jsonrpsee::RpcModule;
51use net::api::ApiSecrets;
52use net::p2p::P2PStatusReceivers;
53use net::p2p_connector::IrohConnector;
54use tokio::net::TcpListener;
55use tokio_rustls::rustls;
56use tracing::info;
57
58use crate::config::ConfigGenSettings;
59use crate::config::io::write_server_config;
60use crate::config::setup::{ConfigGenOutcome, SetupApi};
61use crate::db::{ServerInfo, ServerInfoKey};
62use crate::fedimint_core::net::peers::IP2PConnections;
63use crate::metrics::initialize_gauge_metrics;
64use crate::net::api::announcement::start_api_announcement_service;
65use crate::net::api::pkarr_publish::start_pkarr_publish_service;
66use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
67use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
68
69pub mod metrics;
70
71/// The actual implementation of consensus
72pub mod consensus;
73
74/// Networking for mint-to-mint and client-to-mint communiccation
75pub mod net;
76
77/// Fedimint toplevel config
78pub mod config;
79
80/// Requested settings for the transitional Iroh 1.0 API listener.
81#[derive(Debug, Clone)]
82pub struct IrohNextApiSettings {
83    /// Optional explicit socket address on which to bind the Iroh 1.0 API
84    /// endpoint.
85    bind: Option<SocketAddr>,
86}
87
88impl IrohNextApiSettings {
89    /// Request the transitional listener, optionally overriding its bind
90    /// address.
91    pub fn new(bind: Option<SocketAddr>) -> Self {
92        Self { bind }
93    }
94
95    pub(crate) fn bind_override(&self) -> Option<SocketAddr> {
96        self.bind
97    }
98}
99
100/// A function/closure type for handling dashboard UI
101pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
102
103/// A function/closure type for handling setup UI
104pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
105
106/// Run a server without configuring custom Iroh 1.0 relays for guardian P2P.
107///
108/// Use [`run_with_iroh_p2p_relays`] to supply version-specific P2P relays.
109#[allow(clippy::too_many_arguments)]
110pub async fn run(
111    data_dir: PathBuf,
112    auth_ui: Option<ApiAuth>,
113    auth_api: Option<ApiAuth>,
114    force_api_secrets: ApiSecrets,
115    settings: ConfigGenSettings,
116    db: Database,
117    code_version_str: String,
118    code_version_hash: String,
119    module_init_registry: ServerModuleInitRegistry,
120    task_group: TaskGroup,
121    bitcoin_rpc: DynServerBitcoinRpc,
122    setup_ui_router: SetupUiRouter,
123    dashboard_ui_router: DashboardUiRouter,
124    db_checkpoint_retention: u64,
125    session_timeout: Duration,
126    iroh_api_limits: ConnectionLimits,
127) -> anyhow::Result<()> {
128    run_with_iroh_p2p_relays(
129        data_dir,
130        auth_ui,
131        auth_api,
132        force_api_secrets,
133        settings,
134        db,
135        code_version_str,
136        code_version_hash,
137        module_init_registry,
138        task_group,
139        bitcoin_rpc,
140        setup_ui_router,
141        dashboard_ui_router,
142        db_checkpoint_retention,
143        session_timeout,
144        iroh_api_limits,
145        Vec::new(),
146    )
147    .await
148}
149
150/// Run a server with a separate Iroh 1.0 relay list for guardian P2P.
151#[allow(clippy::too_many_arguments)]
152pub async fn run_with_iroh_p2p_relays(
153    data_dir: PathBuf,
154    auth_ui: Option<ApiAuth>,
155    auth_api: Option<ApiAuth>,
156    force_api_secrets: ApiSecrets,
157    settings: ConfigGenSettings,
158    db: Database,
159    code_version_str: String,
160    code_version_hash: String,
161    module_init_registry: ServerModuleInitRegistry,
162    task_group: TaskGroup,
163    bitcoin_rpc: DynServerBitcoinRpc,
164    setup_ui_router: SetupUiRouter,
165    dashboard_ui_router: DashboardUiRouter,
166    db_checkpoint_retention: u64,
167    session_timeout: Duration,
168    iroh_api_limits: ConnectionLimits,
169    iroh_p2p_relays: Vec<SafeUrl>,
170) -> anyhow::Result<()> {
171    run_with_iroh_p2p_relays_and_next_api(
172        data_dir,
173        auth_ui,
174        auth_api,
175        force_api_secrets,
176        settings,
177        db,
178        code_version_str,
179        code_version_hash,
180        module_init_registry,
181        task_group,
182        bitcoin_rpc,
183        setup_ui_router,
184        dashboard_ui_router,
185        db_checkpoint_retention,
186        session_timeout,
187        iroh_api_limits,
188        iroh_p2p_relays,
189        None,
190    )
191    .await
192}
193
194/// Run a server with explicit guardian P2P relays and an optional Iroh 1.0 API.
195#[allow(clippy::too_many_arguments)]
196pub async fn run_with_iroh_p2p_relays_and_next_api(
197    data_dir: PathBuf,
198    auth_ui: Option<ApiAuth>,
199    auth_api: Option<ApiAuth>,
200    force_api_secrets: ApiSecrets,
201    settings: ConfigGenSettings,
202    db: Database,
203    code_version_str: String,
204    code_version_hash: String,
205    module_init_registry: ServerModuleInitRegistry,
206    task_group: TaskGroup,
207    bitcoin_rpc: DynServerBitcoinRpc,
208    setup_ui_router: SetupUiRouter,
209    dashboard_ui_router: DashboardUiRouter,
210    db_checkpoint_retention: u64,
211    session_timeout: Duration,
212    iroh_api_limits: ConnectionLimits,
213    iroh_p2p_relays: Vec<SafeUrl>,
214    iroh_next_api_settings: Option<IrohNextApiSettings>,
215) -> anyhow::Result<()> {
216    let (cfg, connections, p2p_status_receivers) = match get_config(&data_dir)? {
217        Some(cfg) => {
218            let connector = if cfg.consensus.iroh_endpoints.is_empty() {
219                TlsTcpConnector::new(
220                    cfg.tls_config(),
221                    settings.p2p_bind,
222                    cfg.local.p2p_endpoints.clone(),
223                    cfg.local.identity,
224                )
225                .await
226                .into_dyn()
227            } else {
228                IrohConnector::new(
229                    cfg.private.iroh_p2p_sk.clone().unwrap(),
230                    settings.p2p_bind,
231                    settings.iroh_dns.clone(),
232                    iroh_p2p_relays.clone(),
233                    cfg.consensus
234                        .iroh_endpoints
235                        .iter()
236                        .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
237                        .collect(),
238                )
239                .await?
240                .into_dyn()
241            };
242
243            let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
244
245            let connections = ReconnectP2PConnections::new(
246                cfg.local.identity,
247                connector,
248                &task_group,
249                p2p_status_senders,
250            )
251            .into_dyn();
252
253            (cfg, connections, p2p_status_receivers)
254        }
255        None => {
256            Box::pin(run_config_gen_with_iroh_p2p_relays(
257                data_dir.clone(),
258                settings.clone(),
259                db.clone(),
260                &task_group,
261                code_version_str.clone(),
262                code_version_hash.clone(),
263                force_api_secrets.clone(),
264                setup_ui_router,
265                module_init_registry.clone(),
266                auth_ui.clone(),
267                auth_api.clone(),
268                iroh_p2p_relays,
269            ))
270            .await?
271        }
272    };
273
274    let decoders = module_init_registry.decoders_strict(
275        cfg.consensus
276            .modules
277            .iter()
278            .map(|(id, config)| (*id, &config.kind)),
279    )?;
280
281    let db = db.with_decoders(decoders);
282
283    initialize_gauge_metrics(&task_group, &db).await;
284
285    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
286    start_pkarr_publish_service(&db, &task_group, &cfg).await?;
287
288    info!(target: LOG_CONSENSUS, "Starting consensus...");
289
290    let connectors = ConnectorRegistry::build_from_server_defaults()
291        .bind()
292        .await?;
293
294    Box::pin(consensus::run(
295        connectors,
296        auth_ui,
297        auth_api,
298        connections,
299        p2p_status_receivers,
300        settings.api_bind,
301        settings.iroh_dns,
302        settings.iroh_relays,
303        cfg,
304        db,
305        module_init_registry.clone(),
306        &task_group,
307        force_api_secrets,
308        data_dir,
309        code_version_str,
310        code_version_hash,
311        bitcoin_rpc,
312        settings.ui_bind,
313        dashboard_ui_router,
314        db_checkpoint_retention,
315        session_timeout,
316        iroh_api_limits,
317        iroh_next_api_settings.as_ref(),
318    ))
319    .await?;
320
321    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
322
323    task_group.shutdown();
324
325    Ok(())
326}
327
328async fn update_server_info_version_dbtx(
329    dbtx: &mut DatabaseTransaction<'_>,
330    code_version_str: &str,
331) {
332    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
333        init_version: code_version_str.to_string(),
334        last_version: code_version_str.to_string(),
335    });
336    server_info.last_version = code_version_str.to_string();
337    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
338}
339
340pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
341    if !data_dir.join("consensus.json").exists() {
342        return Ok(None);
343    }
344
345    read_server_config(data_dir).map(Some)
346}
347
348/// Validate restored TCP transport material before building `TlsTcpConnector`.
349///
350/// `ServerConfig::tls_config()` and `TlsTcpConnector::new()` contain invariant
351/// checks that are fine for generated configs but too panic-prone for uploaded
352/// restore data. This preflights the same key/certificate material and returns
353/// a normal restore error before any files are installed.
354///
355/// Keep this in sync with the panic points in `ServerConfig::tls_config()`:
356/// missing `private.tls_key`, malformed TLS private-key hex, malformed TLS
357/// certificate hex, and malformed TLS private-key DER.
358fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
359    let tls_key = cfg
360        .private
361        .tls_key
362        .as_ref()
363        .context("Restored TCP config is missing the TLS private key")?;
364    let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
365    rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
366        .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
367
368    ensure!(
369        cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
370        "Restored TCP config is missing our TLS certificate"
371    );
372    for (peer, cert) in &cfg.consensus.tls_certs {
373        Vec::from_hex(cert)
374            .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
375    }
376
377    let tls_config = cfg.tls_config();
378    let mut root_cert_store = rustls::RootCertStore::empty();
379    for cert in tls_config.certificates.values() {
380        root_cert_store
381            .add(cert.clone())
382            .context("Adding restored TLS certificate to root store")?;
383    }
384    let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
385        .build()
386        .context("Creating restored TLS client verifier")?;
387    let certificate = tls_config
388        .certificates
389        .get(&cfg.local.identity)
390        .context("Restored TCP config is missing our TLS certificate")?
391        .clone();
392    rustls::ServerConfig::builder()
393        .with_client_cert_verifier(verifier)
394        .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
395        .context("Creating restored TLS server config")?;
396
397    Ok(())
398}
399
400/// Validate restored Iroh transport keys and return the p2p key for connector
401/// setup.
402///
403/// Restore data must contain both API and p2p secret keys, and both must match
404/// this guardian's public keys in the restored consensus endpoints before the
405/// config is installed.
406fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
407    let iroh_p2p_sk = cfg
408        .private
409        .iroh_p2p_sk
410        .clone()
411        .context("Restored Iroh config is missing the Iroh p2p secret key")?;
412    let local_endpoints = cfg
413        .consensus
414        .iroh_endpoints
415        .get(&cfg.local.identity)
416        .context("Restored Iroh config is missing our Iroh endpoints")?;
417    ensure!(
418        iroh_p2p_sk.public() == local_endpoints.p2p_pk,
419        "Restored Iroh p2p secret key does not match our Iroh endpoint"
420    );
421
422    let iroh_api_sk = cfg
423        .private
424        .iroh_api_sk
425        .clone()
426        .context("Restored Iroh config is missing the Iroh api secret key")?;
427    ensure!(
428        iroh_api_sk.public() == local_endpoints.api_pk,
429        "Restored Iroh api secret key does not match our Iroh endpoint"
430    );
431
432    Ok(iroh_p2p_sk)
433}
434
435/// Run config generation without configuring custom Iroh 1.0 relays for
436/// guardian P2P.
437///
438/// Use [`run_config_gen_with_iroh_p2p_relays`] to supply version-specific P2P
439/// relays.
440#[allow(clippy::too_many_arguments)]
441pub async fn run_config_gen(
442    data_dir: PathBuf,
443    settings: ConfigGenSettings,
444    db: Database,
445    task_group: &TaskGroup,
446    code_version_str: String,
447    code_version_hash: String,
448    api_secrets: ApiSecrets,
449    setup_ui_handler: SetupUiRouter,
450    module_init_registry: ServerModuleInitRegistry,
451    auth_ui: Option<ApiAuth>,
452    auth_api: Option<ApiAuth>,
453) -> anyhow::Result<(
454    ServerConfig,
455    DynP2PConnections<P2PMessage>,
456    P2PStatusReceivers,
457)> {
458    run_config_gen_with_iroh_p2p_relays(
459        data_dir,
460        settings,
461        db,
462        task_group,
463        code_version_str,
464        code_version_hash,
465        api_secrets,
466        setup_ui_handler,
467        module_init_registry,
468        auth_ui,
469        auth_api,
470        Vec::new(),
471    )
472    .await
473}
474
475/// Run config generation with a separate Iroh 1.0 relay list for guardian P2P.
476#[allow(clippy::too_many_arguments)]
477pub async fn run_config_gen_with_iroh_p2p_relays(
478    data_dir: PathBuf,
479    settings: ConfigGenSettings,
480    db: Database,
481    task_group: &TaskGroup,
482    code_version_str: String,
483    code_version_hash: String,
484    api_secrets: ApiSecrets,
485    setup_ui_handler: SetupUiRouter,
486    module_init_registry: ServerModuleInitRegistry,
487    auth_ui: Option<ApiAuth>,
488    auth_api: Option<ApiAuth>,
489    iroh_p2p_relays: Vec<SafeUrl>,
490) -> anyhow::Result<(
491    ServerConfig,
492    DynP2PConnections<P2PMessage>,
493    P2PStatusReceivers,
494)> {
495    info!(target: LOG_CONSENSUS, "Starting config gen");
496
497    initialize_gauge_metrics(task_group, &db).await;
498
499    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
500
501    let setup_api = SetupApi::new(
502        settings.clone(),
503        db.clone(),
504        cgp_sender,
505        code_version_str.clone(),
506        code_version_hash,
507        auth_ui,
508        auth_api,
509    );
510
511    let mut rpc_module = RpcModule::new(setup_api.clone());
512
513    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
514
515    let api_handler = net::api::spawn(
516        "setup",
517        // config gen always uses ws api
518        settings.api_bind,
519        rpc_module,
520        10,
521        api_secrets.clone(),
522    )
523    .await;
524
525    let ui_task_group = TaskGroup::new();
526
527    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
528
529    let ui_listener = TcpListener::bind(settings.ui_bind)
530        .await
531        .expect("Failed to bind setup UI");
532
533    ui_task_group.spawn("setup-ui", move |handle| async move {
534        axum::serve(ui_listener, ui_service)
535            .with_graceful_shutdown(handle.make_shutdown_rx())
536            .await
537            .expect("Failed to serve setup UI");
538    });
539
540    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
541
542    loop {
543        let config_gen_outcome = cgp_receiver
544            .recv()
545            .await
546            .expect("Config gen params receiver closed unexpectedly");
547
548        match config_gen_outcome {
549            ConfigGenOutcome::Generated(cg_params) => {
550                // HACK: The `start-dkg` API call needs to have some time to finish
551                // before we shut down api handling. There's no easy and good way to do
552                // that other than just giving it some grace period.
553                sleep(Duration::from_millis(100)).await;
554
555                api_handler
556                    .stop()
557                    .expect("Config api should still be running");
558
559                api_handler.stopped().await;
560
561                ui_task_group
562                    .shutdown_join_all(None)
563                    .await
564                    .context("Failed to shutdown UI server after config gen")?;
565
566                let cg_params = *cg_params;
567                let connector = if cg_params.iroh_endpoints().is_empty() {
568                    TlsTcpConnector::new(
569                        cg_params.tls_config(),
570                        settings.p2p_bind,
571                        cg_params.p2p_urls(),
572                        cg_params.identity,
573                    )
574                    .await
575                    .into_dyn()
576                } else {
577                    IrohConnector::new(
578                        cg_params
579                            .iroh_p2p_sk
580                            .clone()
581                            .expect("Iroh p2p secret key is required for iroh endpoints"),
582                        settings.p2p_bind,
583                        settings.iroh_dns,
584                        iroh_p2p_relays,
585                        cg_params
586                            .iroh_endpoints()
587                            .iter()
588                            .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
589                            .collect(),
590                    )
591                    .await?
592                    .into_dyn()
593                };
594
595                let (p2p_status_senders, p2p_status_receivers) =
596                    p2p_status_channels(connector.peers());
597
598                let connections = ReconnectP2PConnections::new(
599                    cg_params.identity,
600                    connector,
601                    task_group,
602                    p2p_status_senders,
603                )
604                .into_dyn();
605
606                let cfg = ServerConfig::distributed_gen(
607                    &cg_params,
608                    module_init_registry.clone(),
609                    code_version_str.clone(),
610                    connections.clone(),
611                    p2p_status_receivers.clone(),
612                )
613                .await?;
614
615                assert_ne!(
616                    cfg.consensus.iroh_endpoints.is_empty(),
617                    cfg.consensus.api_endpoints.is_empty(),
618                );
619
620                write_server_config(
621                    &cfg,
622                    &data_dir,
623                    &module_init_registry,
624                    api_secrets.get_active(),
625                )?;
626
627                return Ok((cfg, connections, p2p_status_receivers));
628            }
629            ConfigGenOutcome::Restored(restored, restore_result_sender) => {
630                // Process restore outcomes while setup serving is still running. This lets the
631                // HTTP handler wait for a precise success/error acknowledgement and keeps setup
632                // retryable if validation or the config write fails.
633                let result: anyhow::Result<_> = async {
634                    let cfg = *restored;
635
636                    module_init_registry.decoders_strict(
637                        cfg.consensus
638                            .modules
639                            .iter()
640                            .map(|(id, config)| (*id, &config.kind)),
641                    )?;
642
643                    cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
644
645                    if cfg.consensus.iroh_endpoints.is_empty() {
646                        validate_restored_tcp_config(&cfg)?;
647                    }
648
649                    // Build the connector from the already validated restored config before
650                    // writing it to its final location.
651                    let connector = if cfg.consensus.iroh_endpoints.is_empty() {
652                        TlsTcpConnector::new(
653                            cfg.tls_config(),
654                            settings.p2p_bind,
655                            cfg.local.p2p_endpoints.clone(),
656                            cfg.local.identity,
657                        )
658                        .await
659                        .into_dyn()
660                    } else {
661                        let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
662
663                        IrohConnector::new(
664                            iroh_p2p_sk,
665                            settings.p2p_bind,
666                            settings.iroh_dns.clone(),
667                            iroh_p2p_relays.clone(),
668                            cfg.consensus
669                                .iroh_endpoints
670                                .iter()
671                                .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
672                                .collect(),
673                        )
674                        .await?
675                        .into_dyn()
676                    };
677
678                    let (p2p_status_senders, p2p_status_receivers) =
679                        p2p_status_channels(connector.peers());
680
681                    // Write the restored config directly into the data directory, exactly
682                    // like a freshly generated config.
683                    write_server_config(
684                        &cfg,
685                        &data_dir,
686                        &module_init_registry,
687                        api_secrets.get_active(),
688                    )?;
689
690                    Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
691                }
692                .await;
693
694                let ack = result
695                    .as_ref()
696                    .map(|_| ())
697                    .map_err(std::string::ToString::to_string);
698                let restore_failed = ack.is_err();
699                let _ = restore_result_sender.send(ack);
700
701                if restore_failed {
702                    continue;
703                }
704
705                // Give the restore API call a chance to return the acknowledged outcome before
706                // shutting down setup serving.
707                sleep(Duration::from_millis(100)).await;
708
709                api_handler
710                    .stop()
711                    .expect("Config api should still be running");
712
713                api_handler.stopped().await;
714
715                ui_task_group
716                    .shutdown_join_all(None)
717                    .await
718                    .context("Failed to shutdown UI server after restored config install")?;
719
720                let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
721                let connections = ReconnectP2PConnections::new(
722                    cfg.local.identity,
723                    connector,
724                    task_group,
725                    p2p_status_senders,
726                )
727                .into_dyn();
728
729                return Ok((cfg, connections, p2p_status_receivers));
730            }
731        }
732    }
733}