Skip to main content

fedimintd/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_wrap)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::must_use_candidate)]
6#![allow(clippy::return_self_not_must_use)]
7#![allow(clippy::large_futures)]
8
9mod metrics;
10
11use std::convert::Infallible;
12use std::env;
13use std::fmt::Write as _;
14use std::net::SocketAddr;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::time::Duration;
19
20use anyhow::Context as _;
21use bitcoin::Network;
22use clap::builder::BoolishValueParser;
23use clap::{ArgGroup, CommandFactory, FromArgMatches, Parser};
24use fedimint_core::db::Database;
25use fedimint_core::envs::{
26    FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
27    is_running_in_test_env,
28};
29use fedimint_core::module::registry::ModuleRegistry;
30use fedimint_core::module::{ApiAuth, CORE_CONSENSUS_VERSION};
31use fedimint_core::rustls::install_crypto_provider;
32use fedimint_core::task::TaskGroup;
33use fedimint_core::timing;
34use fedimint_core::util::{FmtCompact as _, SafeUrl, handle_version_hash_command};
35use fedimint_ln_server::LightningInit;
36use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
37use fedimint_meta_server::MetaInit;
38use fedimint_mint_server::MintInit;
39use fedimint_rocksdb::RocksDb;
40use fedimint_server::config::ConfigGenSettings;
41use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
42use fedimint_server::core::ServerModuleInitRegistry;
43use fedimint_server::net::api::ApiSecrets;
44use fedimint_server::{ConsensusStop, IrohNextApiSettings, ShutdownRequest};
45use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
46use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
47use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
48use fedimint_server_bitcoin_rpc::tracked::ServerBitcoinRpcTracked;
49use fedimint_server_core::ServerModuleInitRegistryExt;
50use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
51use fedimint_unknown_server::UnknownInit;
52use fedimint_wallet_server::WalletInit;
53use fedimintd_envs::{
54    FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_API_NEXT_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
55    FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
56    FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
57    FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
58    FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
59    FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV,
60    FM_IROH_NEXT_ENABLE_ENV, FM_IROH_P2P_RELAY_ENV, FM_P2P_MAX_CONNECTION_AGE_SECS_ENV,
61    FM_P2P_URL_ENV, FM_PASSWORD_API_ENV, FM_PASSWORD_UI_ENV, FM_SESSION_TIMEOUT_SECS_ENV,
62};
63use futures::FutureExt as _;
64#[cfg(all(
65    not(feature = "jemalloc"),
66    not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
67))]
68use tracing::warn;
69use tracing::{debug, error, info};
70
71use crate::metrics::APP_START_TS;
72
73/// Time we will wait before forcefully shutting down tasks
74const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
75
76/// Exit code of a fedimintd whose consensus stopped because a shutdown
77/// requested through `shutdown_after_current_session` took effect at a
78/// session boundary: its last session completed and it started no later
79/// session, so it signed no units of an unfinished session.
80///
81/// The code depends only on why consensus stopped. Tasks that are still
82/// running when the task group's join times out end with the process; none
83/// of them can start a session. Every other stop, including a shutdown
84/// scheduled for a given session through `shutdown`, a session timeout, a
85/// failed main task, a panic of the main task, or a signal, exits with a
86/// different code or by signal.
87pub const SESSION_BOUNDARY_EXIT_CODE: i32 = 3;
88
89#[derive(Parser)]
90#[command(version)]
91#[command(
92    group(
93        ArgGroup::new("bitcoind_password_auth")
94           .args(["bitcoind_password", "bitcoind_url_password_file"])
95           .multiple(false)
96    ),
97    group(
98        ArgGroup::new("bitcoind_auth")
99            .args(["bitcoind_url"])
100            .requires("bitcoind_password_auth")
101            .requires_all(["bitcoind_username", "bitcoind_url"])
102    ),
103    group(
104        ArgGroup::new("bitcoin_rpc")
105            .required(true)
106            .multiple(true)
107            .args(["bitcoind_url", "esplora_url"])
108    )
109)]
110struct ServerOpts {
111    /// Path to folder containing federation config files
112    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
113    data_dir: PathBuf,
114
115    /// Password gating the guardian admin UI on bind-ui. Optional: falls
116    /// back to reading password.private from data-dir, and if neither is
117    /// present the UI is served without a login form. Only safe when
118    /// bind-ui stays on a trusted interface (the default 127.0.0.1).
119    #[arg(long, env = FM_PASSWORD_UI_ENV)]
120    password_ui: Option<String>,
121
122    /// Password gating admin RPCs on the public API (WebSocket on bind-api
123    /// and iroh). Optional and never falls back to disk: when unset, admin
124    /// RPCs return 401 unconditionally, since the public API is always
125    /// network-reachable and must be enabled explicitly.
126    #[arg(long, env = FM_PASSWORD_API_ENV)]
127    password_api: Option<String>,
128
129    /// The bitcoin network of the federation
130    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
131    bitcoin_network: Network,
132
133    /// The username to use when connecting to bitcoind
134    #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
135    bitcoind_username: Option<String>,
136
137    /// The password to use when connecting to bitcoind
138    #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
139    bitcoind_password: Option<String>,
140
141    /// Bitcoind RPC URL, e.g. <http://127.0.0.1:8332>
142    /// This should not include authentication parameters, they should be
143    /// included in `FM_BITCOIND_USERNAME` and `FM_BITCOIND_PASSWORD`
144    ///
145    /// With Esplora configured, reads remain bitcoind-first except block count
146    /// uses Esplora while bitcoind reports initial block download. Broadcast
147    /// remains bitcoind-first.
148    #[arg(long, env = FM_BITCOIND_URL_ENV)]
149    bitcoind_url: Option<SafeUrl>,
150
151    /// If set, the password part of `--bitcoind-url` will be set/replaced with
152    /// the content of this file.
153    ///
154    /// This is useful for setups that provide secret material via ramdisk
155    /// e.g. SOPS, age, etc.
156    ///
157    /// Note this is not meant to handle bitcoind's cookie file.
158    #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
159    bitcoind_url_password_file: Option<PathBuf>,
160
161    /// Trusted Esplora HTTP base URL, e.g. <https://mempool.space/api>
162    ///
163    /// With bitcoind configured, retries individual failed reads and supplies
164    /// block count while bitcoind reports initial block download. Hybrid
165    /// construction does not probe either backend or compare their chain
166    /// identities. Esplora can bootstrap while bitcoind is offline and must
167    /// be trusted to serve the intended chain; its chain selection is not
168    /// independently verified.
169    /// Transactions go to Esplora only after a bitcoind broadcast error.
170    /// See SECURITY.md for the trust model.
171    #[arg(long, env = FM_ESPLORA_URL_ENV)]
172    esplora_url: Option<SafeUrl>,
173
174    /// Address we bind to for p2p consensus communication
175    ///
176    /// Should be `0.0.0.0:8173` most of the time, as p2p connectivity is public
177    /// and direct, and the port should be open it in the firewall.
178    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
179    bind_p2p: SocketAddr,
180
181    /// Address we bind to for the API
182    ///
183    /// Should be `0.0.0.0:8174` most of the time, as api connectivity is public
184    /// and direct, and the port should be open it in the firewall.
185    #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
186    bind_api: SocketAddr,
187
188    /// Address we bind to for exposing the Web UI
189    ///
190    /// Built-in web UI is exposed as an HTTP port, and typically should
191    /// have TLS terminated by Nginx/Traefik/etc. and forwarded to the locally
192    /// bind port.
193    #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
194    bind_ui: SocketAddr,
195
196    /// Our external address for communicating with our peers
197    ///
198    /// `fedimint://<fqdn>:8173` for TCP/TLS p2p connectivity (legacy/standard).
199    ///
200    /// Ignored when Iroh stack is used. (newer/experimental)
201    #[arg(long, env = FM_P2P_URL_ENV)]
202    p2p_url: Option<SafeUrl>,
203
204    /// Our API address for clients to connect to us
205    ///
206    /// Typically `wss://<fqdn>/ws/` for TCP/TLS connectivity (legacy/standard)
207    ///
208    /// Ignored when Iroh stack is used. (newer/experimental)
209    #[arg(long, env = FM_API_URL_ENV)]
210    api_url: Option<SafeUrl>,
211
212    /// Whether to use the Iroh networking stack instead of the legacy
213    /// TCP/TLS/websocket stack. Defaults to Iroh in production and to the
214    /// legacy stack in test environments (`cargo test` / `devimint`). Only
215    /// consulted at DKG / config generation time; existing federations keep the
216    /// stack baked into their config. Pass `--enable-iroh true/false` (or
217    /// `FM_ENABLE_IROH=true/false`) to override.
218    #[arg(long, env = FM_ENABLE_IROH_ENV, value_parser = BoolishValueParser::new())]
219    enable_iroh: Option<bool>,
220
221    /// Optional URL of the Iroh DNS server
222    #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
223    iroh_dns: Option<SafeUrl>,
224
225    /// Optional URLs of the Iroh relays to use for registering
226    #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh", value_delimiter = ',')]
227    iroh_relays: Vec<SafeUrl>,
228
229    /// Optional Iroh 1.0 relay URLs to use for guardian P2P
230    #[arg(long, env = FM_IROH_P2P_RELAY_ENV, value_delimiter = ',')]
231    iroh_p2p_relays: Vec<SafeUrl>,
232
233    /// Number of checkpoints from the current session to retain on disk
234    #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
235    db_checkpoint_retention: u64,
236
237    /// Exit the process if a consensus session is not completed within this
238    /// many seconds, relying on the process supervisor to restart fedimintd.
239    /// Restarting guardians has been observed to resolve stuck sessions.
240    #[arg(long, env = FM_SESSION_TIMEOUT_SECS_ENV, default_value = "3600")]
241    session_timeout_secs: u64,
242
243    /// Drop a p2p connection to a peer once it exceeds this age in seconds,
244    /// relying on the reconnection logic to re-establish it. Unset by
245    /// default, in which case connections are never dropped due to their age.
246    #[arg(long, env = FM_P2P_MAX_CONNECTION_AGE_SECS_ENV)]
247    p2p_max_connection_age_secs: Option<u64>,
248
249    /// Enable tokio console logging
250    #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
251    bind_tokio_console: Option<SocketAddr>,
252
253    /// Enable jaeger for tokio console logging
254    #[arg(long, default_value = "false")]
255    with_jaeger: bool,
256
257    /// Enable prometheus metrics
258    #[arg(long, env = FM_BIND_METRICS_ENV, default_value = "127.0.0.1:8176")]
259    bind_metrics: Option<SocketAddr>,
260
261    /// Comma separated list of API secrets.
262    ///
263    /// Setting it will enforce API authentication and make the Federation
264    /// "private".
265    ///
266    /// The first secret in the list is the "active" one that the peer will use
267    /// itself to connect to other peers. Any further one is accepted by
268    /// this peer, e.g. for the purposes of smooth rotation of secret
269    /// between users.
270    ///
271    /// Note that the value provided here will override any other settings
272    /// that the user might want to set via UI at runtime, etc.
273    /// In the future, managing secrets might be possible via Admin UI
274    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
275    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
276    force_api_secrets: ApiSecrets,
277
278    /// Maximum number of concurrent Iroh API connections
279    #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
280    iroh_api_max_connections: usize,
281
282    /// Maximum number of parallel requests per Iroh API connection
283    #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
284    iroh_api_max_requests_per_connection: usize,
285
286    /// Enable the transitional Iroh 1.0 API endpoint alongside Iroh 0.35.
287    ///
288    /// For a federation configured with the legacy Iroh API, this is a runtime
289    /// setting independent of the DKG-only `--enable-iroh` option. It is
290    /// enabled by default. Once the endpoint is advertised, it must remain
291    /// enabled.
292    #[arg(
293        long,
294        env = FM_IROH_NEXT_ENABLE_ENV,
295        default_value_t = true,
296        action = clap::ArgAction::Set,
297        num_args = 0..=1,
298        default_missing_value = "true",
299    )]
300    enable_iroh_next: bool,
301
302    /// Bind address for the transitional Iroh 1.0 API endpoint
303    #[arg(long, env = FM_BIND_API_NEXT_ENV)]
304    bind_api_next: Option<SocketAddr>,
305}
306
307impl ServerOpts {
308    pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
309        let url = self
310            .bitcoind_url
311            .clone()
312            .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
313        if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
314            let password = tokio::fs::read_to_string(password_file)
315                .await
316                .context("Failed to read the password")?
317                .trim()
318                .to_owned();
319            Ok((url, password))
320        } else {
321            let password = self
322                .bitcoind_password
323                .clone()
324                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
325            Ok((url, password))
326        }
327    }
328}
329
330/// Block the thread and run a Fedimintd server
331///
332/// # Arguments
333///
334/// * `module_init_registry` - The registry of available modules.
335///
336/// * `code_version_hash` - The git hash of the code that the `fedimintd` binary
337///   is being built from. This is used mostly for information purposes
338///   (`fedimintd version-hash`). See `fedimint-build` crate for easy way to
339///   obtain it.
340///
341/// * `code_version_vendor_suffix` - An optional vendor string appended to the
342///   internal Fedimint release version, to distinguish binaries built by
343///   different vendors, usually with a different set of modules. It is encoded
344///   as `SemVer` build metadata and therefore must contain dot-separated,
345///   non-empty identifiers with only ASCII alphanumeric characters and hyphens.
346///   Setup/DKG compatibility and consensus config generation require the same
347///   exact optional vendor string as well as the same `major.minor` series.
348#[allow(clippy::too_many_lines)]
349pub async fn run(
350    module_init_registry: ServerModuleInitRegistry,
351    code_version_hash: &str,
352    code_version_vendor_suffix: Option<&str>,
353) -> anyhow::Result<Infallible> {
354    assert_eq!(
355        env!("FEDIMINT_BUILD_CODE_VERSION").len(),
356        code_version_hash.len(),
357        "version_hash must have an expected length"
358    );
359
360    handle_version_hash_command(code_version_hash);
361
362    let fedimint_version = env!("CARGO_PKG_VERSION");
363
364    APP_START_TS
365        .with_label_values(&[fedimint_version, code_version_hash])
366        .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
367
368    let server_opts = {
369        // Collect env vars from all registered modules and append them to the
370        // long-help text so operators can discover them via `fedimintd --help`.
371        let mut module_env_help = String::from("\nModule environment variables:\n");
372        for (_kind, module_init) in module_init_registry.iter() {
373            for doc in module_init.get_documented_env_vars() {
374                let _ = writeln!(module_env_help, "  {:40}  {}", doc.name, doc.description);
375            }
376        }
377        let matches = ServerOpts::command()
378            .after_long_help(module_env_help)
379            .get_matches();
380        ServerOpts::from_arg_matches(&matches)
381            .expect("clap arg matches must be valid after parsing")
382    };
383
384    let mut tracing_builder = TracingSetup::default();
385
386    tracing_builder
387        .tokio_console_bind(server_opts.bind_tokio_console)
388        .with_jaeger(server_opts.with_jaeger);
389
390    tracing_builder.init().map_err(anyhow::Error::from).unwrap();
391
392    info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
393
394    #[cfg(all(
395        not(feature = "jemalloc"),
396        not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
397    ))]
398    warn!(
399        target: LOG_SERVER,
400        "fedimintd was built without the `jemalloc` feature. rocksdb is prone to memory \
401         fragmentation with the default allocator; consider rebuilding with `--features jemalloc`."
402    );
403
404    debug!(
405        target: LOG_SERVER,
406        core_consensus = %CORE_CONSENSUS_VERSION,
407        "Supported core consensus version",
408    );
409
410    let code_version_str = code_version_vendor_suffix.map_or_else(
411        || fedimint_version.to_string(),
412        |suffix| format!("{fedimint_version}+{suffix}"),
413    );
414    fedimint_core::version::DkgVersion::parse(&code_version_str)
415        .context("Invalid Fedimint version vendor string")?;
416
417    let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
418
419    let root_task_group = TaskGroup::new();
420
421    if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
422        info!(
423            target: LOG_SERVER,
424            url = %format!("http://{}/metrics", bind_metrics),
425            "Initializing metrics server",
426        );
427        fedimint_metrics::spawn_api_server(*bind_metrics, root_task_group.clone()).await?;
428    }
429
430    let enable_iroh = server_opts.enable_iroh.unwrap_or(!is_running_in_test_env());
431    let iroh_next_api_settings = if server_opts.enable_iroh_next {
432        Some(IrohNextApiSettings::new(server_opts.bind_api_next))
433    } else {
434        None
435    };
436
437    let settings = ConfigGenSettings {
438        p2p_bind: server_opts.bind_p2p,
439        api_bind: server_opts.bind_api,
440        ui_bind: server_opts.bind_ui,
441        p2p_url: server_opts.p2p_url.clone(),
442        api_url: server_opts.api_url.clone(),
443        enable_iroh,
444        iroh_dns: server_opts.iroh_dns.clone(),
445        iroh_relays: server_opts.iroh_relays.clone(),
446        network: server_opts.bitcoin_network,
447        available_modules: module_init_registry.kinds(),
448        default_modules: module_init_registry.default_modules(),
449    };
450
451    let db = Database::new(
452        RocksDb::build(server_opts.data_dir.join(DB_FILE))
453            .open()
454            .await
455            .map_err(anyhow::Error::from)
456            .unwrap(),
457        ModuleRegistry::default(),
458    );
459
460    let dyn_server_bitcoin_rpc = match (
461        server_opts.bitcoind_url.as_ref(),
462        server_opts.esplora_url.as_ref(),
463    ) {
464        (Some(_), None) => {
465            let bitcoind_username = server_opts
466                .bitcoind_username
467                .clone()
468                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
469            let (bitcoind_url, bitcoind_password) = server_opts
470                .get_bitcoind_url_and_password()
471                .await
472                .expect("Failed to get bitcoind url");
473            BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
474                .unwrap()
475                .into_dyn()
476        }
477        (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
478        (Some(_), Some(esplora_url)) => {
479            let bitcoind_username = server_opts
480                .bitcoind_username
481                .clone()
482                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
483            let (bitcoind_url, bitcoind_password) = server_opts
484                .get_bitcoind_url_and_password()
485                .await
486                .expect("Failed to get bitcoind url");
487            BitcoindClientWithFallback::new(
488                bitcoind_username,
489                bitcoind_password,
490                &bitcoind_url,
491                esplora_url,
492            )
493            .await
494            .unwrap()
495            .into_dyn()
496        }
497        _ => unreachable!("ArgGroup already enforced at least one Bitcoin backend"),
498    };
499    let dyn_server_bitcoin_rpc =
500        ServerBitcoinRpcTracked::new(dyn_server_bitcoin_rpc, "server").into_dyn();
501
502    root_task_group.install_kill_handler();
503
504    install_crypto_provider().await;
505
506    // The UI password falls back to the legacy password.private file on disk
507    // if its env var is not set, since the UI defaults to a loopback bind.
508    // The API password never falls back: the public admin API is always
509    // network-reachable, so it must be enabled explicitly via its env var or
510    // else admin RPCs return 401. Absence of a password for a given plane opts
511    // that plane into passwordless mode (UI) or disabled admin RPCs (API).
512    let password_file = std::fs::read_to_string(server_opts.data_dir.join(PLAINTEXT_PASSWORD))
513        .ok()
514        .map(|s| s.trim().to_owned());
515
516    let auth_ui = server_opts.password_ui.or(password_file).map(ApiAuth::new);
517    let auth_api = server_opts.password_api.map(ApiAuth::new);
518
519    let task_group = root_task_group.clone();
520    let code_version_hash = code_version_hash.to_string();
521    let stopped_at_session_boundary = Arc::new(AtomicBool::new(false));
522    let main_stopped_at_session_boundary = stopped_at_session_boundary.clone();
523    root_task_group.spawn_cancellable("main", async move {
524        let stop = fedimint_server::run_with_iroh_p2p_relays_and_next_api(
525            server_opts.data_dir,
526            auth_ui,
527            auth_api,
528            server_opts.force_api_secrets,
529            settings,
530            db,
531            code_version_str,
532            code_version_hash,
533            module_init_registry,
534            task_group,
535            dyn_server_bitcoin_rpc,
536            Box::new(fedimint_server_ui::setup::router),
537            Box::new(fedimint_server_ui::dashboard::router),
538            server_opts.db_checkpoint_retention,
539            Duration::from_secs(server_opts.session_timeout_secs),
540            server_opts
541                .p2p_max_connection_age_secs
542                .map(Duration::from_secs),
543            fedimint_server::ConnectionLimits::new(
544                server_opts.iroh_api_max_connections,
545                server_opts.iroh_api_max_requests_per_connection,
546            ),
547            server_opts.iroh_p2p_relays,
548            iroh_next_api_settings,
549        )
550        .await
551        .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact()));
552        if stop == ConsensusStop::ScheduledShutdown(ShutdownRequest::AfterCurrentSession) {
553            main_stopped_at_session_boundary.store(true, Ordering::SeqCst);
554        }
555    });
556
557    let shutdown_future = root_task_group
558        .make_handle()
559        .make_shutdown_rx()
560        .then(|()| async {
561            info!(target: LOG_CORE, "Shutdown called");
562        });
563
564    shutdown_future.await;
565
566    debug!(target: LOG_CORE, "Terminating main task");
567
568    if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
569        error!(target: LOG_CORE, err = %err.fmt_compact(), "Error while shutting down task group");
570    }
571
572    debug!(target: LOG_CORE, "Shutdown complete");
573
574    fedimint_logging::shutdown();
575
576    drop(timing_total_runtime);
577
578    if stopped_at_session_boundary.load(Ordering::SeqCst) {
579        std::process::exit(SESSION_BOUNDARY_EXIT_CODE);
580    }
581    std::process::exit(-1);
582}
583
584pub fn default_modules() -> ServerModuleInitRegistry {
585    let mut server_gens = ServerModuleInitRegistry::new();
586
587    server_gens.attach(MintInit);
588    server_gens.attach(fedimint_mintv2_server::MintInit);
589
590    server_gens.attach(WalletInit);
591    server_gens.attach(fedimint_walletv2_server::WalletInit);
592
593    server_gens.attach(LightningInit);
594    server_gens.attach(fedimint_lnv2_server::LightningInit);
595
596    if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
597        server_gens.attach(MetaInit);
598    }
599
600    if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
601        server_gens.attach(UnknownInit);
602    }
603
604    server_gens
605}
606
607#[cfg(test)]
608mod tests;