fedimintd/
fedimintd.rs

1mod metrics;
2
3use std::env;
4use std::net::SocketAddr;
5use std::path::PathBuf;
6use std::time::Duration;
7
8use anyhow::{Context, bail};
9use clap::{Parser, Subcommand};
10use fedimint_core::config::{
11    EmptyGenParams, ModuleInitParams, ServerModuleConfigGenParamsRegistry,
12};
13use fedimint_core::core::ModuleKind;
14use fedimint_core::db::{Database, get_current_database_version};
15use fedimint_core::envs::{
16    BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
17};
18use fedimint_core::module::registry::ModuleRegistry;
19use fedimint_core::module::{ServerApiVersionsSummary, ServerDbVersionsSummary};
20use fedimint_core::task::TaskGroup;
21use fedimint_core::util::{
22    FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command, write_overwrite,
23};
24use fedimint_core::{crit, timing};
25use fedimint_ln_common::config::{
26    LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
27};
28use fedimint_ln_server::LightningInit;
29use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
30use fedimint_meta_server::{MetaGenParams, MetaInit};
31use fedimint_mint_server::MintInit;
32use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
33use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
34use fedimint_server::config::{ConfigGenSettings, NetworkingStack, ServerConfig};
35use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
36use fedimint_server::envs::FM_FORCE_IROH_ENV;
37use fedimint_server::net::api::ApiSecrets;
38use fedimint_unknown_common::config::UnknownGenParams;
39use fedimint_unknown_server::UnknownInit;
40use fedimint_wallet_server::WalletInit;
41use fedimint_wallet_server::common::config::{
42    WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
43};
44use futures::FutureExt;
45use tracing::{debug, error, info};
46
47use crate::default_esplora_server;
48use crate::envs::{
49    FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_API_IROH_ENV, FM_BIND_API_WS_ENV,
50    FM_BIND_METRICS_API_ENV, FM_BIND_P2P_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV,
51    FM_DATA_DIR_ENV, FM_DISABLE_META_MODULE_ENV, FM_FORCE_API_SECRETS_ENV, FM_P2P_URL_ENV,
52    FM_PASSWORD_ENV, FM_TOKIO_CONSOLE_BIND_ENV,
53};
54use crate::fedimintd::metrics::APP_START_TS;
55
56/// Time we will wait before forcefully shutting down tasks
57const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
58
59#[derive(Parser)]
60#[command(version)]
61struct ServerOpts {
62    /// Path to folder containing federation config files
63    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
64    data_dir: Option<PathBuf>,
65    /// Password to encrypt sensitive config files
66    // TODO: should probably never send password to the server directly, rather send the hash via
67    // the API
68    #[arg(long, env = FM_PASSWORD_ENV)]
69    password: Option<String>,
70    /// Enable tokio console logging
71    #[arg(long, env = FM_TOKIO_CONSOLE_BIND_ENV)]
72    tokio_console_bind: Option<SocketAddr>,
73    /// Enable telemetry logging
74    #[arg(long, default_value = "false")]
75    with_telemetry: bool,
76
77    /// Address we bind to for p2p consensus communication
78    ///
79    /// Should be `0.0.0.0:8173` most of the time, as p2p connectivity is public
80    /// and direct, and the port should be open it in the firewall.
81    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
82    bind_p2p: SocketAddr,
83
84    /// Our external address for communicating with our peers
85    ///
86    /// `fedimint://<fqdn>:8173` for TCP/TLS p2p connectivity (legacy/standard).
87    ///
88    /// Ignored when Iroh stack is used. (newer/experimental)
89    #[arg(long, env = FM_P2P_URL_ENV)]
90    p2p_url: Option<SafeUrl>,
91
92    /// Address we bind to for the WebSocket API
93    ///
94    /// Typically `127.0.0.1:8174` as the API requests
95    /// are terminated by Nginx/Traefik/etc. and forwarded to the local port.
96    ///
97    /// NOTE: websocket and iroh APIs can use the same port, as
98    /// one is using TCP and the other UDP.
99    #[arg(long, env = FM_BIND_API_WS_ENV, default_value = "127.0.0.1:8174")]
100    bind_api_ws: SocketAddr,
101
102    /// Address we bind to for Iroh API endpoint
103    ///
104    /// Typically `0.0.0.0:8174`, and the port should be opened in
105    /// the firewall.
106    ///
107    /// NOTE: websocket and iroh APIs can share the same port, as
108    /// one is using TCP and the other UDP.
109    #[arg(long, env = FM_BIND_API_IROH_ENV, default_value = "0.0.0.0:8174" )]
110    bind_api_iroh: SocketAddr,
111
112    /// Our API address for clients to connect to us
113    ///
114    /// Typically `wss://<fqdn>/ws/` for TCP/TLS connectivity (legacy/standard)
115    ///
116    /// Ignored when Iroh stack is used. (newer/experimental)
117    #[arg(long, env = FM_API_URL_ENV)]
118    api_url: Option<SafeUrl>,
119
120    /// Address we bind to for exposing the Web UI
121    ///
122    /// Built-in web UI is exposed as an HTTP port, and typically should
123    /// have TLS terminated by Nginx/Traefik/etc. and forwarded to the locally
124    /// bind port.
125    #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
126    bind_ui: SocketAddr,
127
128    /// The bitcoin network that fedimint will be running on
129    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
130    network: bitcoin::network::Network,
131
132    #[arg(long, env = FM_BIND_METRICS_API_ENV)]
133    bind_metrics_api: Option<SocketAddr>,
134
135    /// Comma separated list of API secrets.
136    ///
137    /// Setting it will enforce API authentication and make the Federation
138    /// "private".
139    ///
140    /// The first secret in the list is the "active" one that the peer will use
141    /// itself to connect to other peers. Any further one is accepted by
142    /// this peer, e.g. for the purposes of smooth rotation of secret
143    /// between users.
144    ///
145    /// Note that the value provided here will override any other settings
146    /// that the user might want to set via UI at runtime, etc.
147    /// In the future, managing secrets might be possible via Admin UI
148    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
149    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
150    force_api_secrets: ApiSecrets,
151
152    #[clap(subcommand)]
153    subcommand: Option<ServerSubcommand>,
154}
155
156#[derive(Subcommand)]
157enum ServerSubcommand {
158    /// Development-related commands
159    #[clap(subcommand)]
160    Dev(DevSubcommand),
161}
162
163#[derive(Subcommand)]
164enum DevSubcommand {
165    /// List supported server API versions and exit
166    ListApiVersions,
167    /// List supported server database versions and exit
168    ListDbVersions,
169}
170
171/// `fedimintd` builder
172///
173/// Fedimint supports third party modules. Right now (and for forseable feature)
174/// modules needs to be combined with the rest of the code at compilation time.
175///
176/// To make this easier, [`Fedimintd`] builder is exposed, allowing
177/// building `fedimintd` with custom set of modules.
178///
179///
180/// Example:
181///
182/// ```
183/// use fedimint_ln_server::LightningInit;
184/// use fedimint_mint_server::MintInit;
185/// use fedimint_wallet_server::WalletInit;
186/// use fedimintd::Fedimintd;
187///
188/// // Note: not called `main` to avoid rustdoc executing it
189/// // #[tokio::main]
190/// async fn main_() -> anyhow::Result<()> {
191///     Fedimintd::new(env!("FEDIMINT_BUILD_CODE_VERSION"), Some("vendor-xyz-1"))?
192///         // use `.with_default_modules()` to avoid having
193///         // to import these manually
194///         .with_module_kind(WalletInit)
195///         .with_module_kind(MintInit)
196///         .with_module_kind(LightningInit)
197///         .run()
198///         .await
199/// }
200/// ```
201pub struct Fedimintd {
202    server_gens: ServerModuleInitRegistry,
203    server_gen_params: ServerModuleConfigGenParamsRegistry,
204    code_version_hash: String,
205    code_version_str: String,
206    opts: ServerOpts,
207    bitcoind_rpc: BitcoinRpcConfig,
208}
209
210impl Fedimintd {
211    /// Builds a new `fedimintd`
212    ///
213    /// # Arguments
214    ///
215    /// * `code_version_hash` - The git hash of the code that the `fedimintd`
216    ///   binary is being built from. This is used mostly for information
217    ///   purposes (`fedimintd version-hash`). See `fedimint-build` crate for
218    ///   easy way to obtain it.
219    ///
220    /// * `code_version_vendor_suffix` - An optional suffix that will be
221    ///   appended to the internal fedimint release version, to distinguish
222    ///   binaries built by different vendors, usually with a different set of
223    ///   modules. Currently DKG will enforce that the combined `code_version`
224    ///   is the same between all peers.
225    pub fn new(
226        code_version_hash: &str,
227        code_version_vendor_suffix: Option<&str>,
228    ) -> anyhow::Result<Fedimintd> {
229        assert_eq!(
230            env!("FEDIMINT_BUILD_CODE_VERSION").len(),
231            code_version_hash.len(),
232            "version_hash must have an expected length"
233        );
234
235        handle_version_hash_command(code_version_hash);
236
237        let fedimint_version = env!("CARGO_PKG_VERSION");
238
239        APP_START_TS
240            .with_label_values(&[fedimint_version, code_version_hash])
241            .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
242
243        // Note: someone might want to set both old and new version for backward compat.
244        // We do that in devimint.
245        if env::var(FM_BIND_API_ENV).is_ok() && env::var(FM_BIND_API_WS_ENV).is_err() {
246            bail!(
247                "{} environment variable was removed and replaced with two separate: {} and {}. Please unset it.",
248                FM_BIND_API_ENV,
249                FM_BIND_API_WS_ENV,
250                FM_BIND_API_IROH_ENV,
251            );
252        }
253        let opts: ServerOpts = ServerOpts::parse();
254
255        let mut tracing_builder = TracingSetup::default();
256
257        #[cfg(feature = "telemetry")]
258        {
259            tracing_builder
260                .tokio_console_bind(opts.tokio_console_bind)
261                .with_jaeger(opts.with_telemetry);
262        }
263
264        tracing_builder.init().unwrap();
265
266        info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
267
268        let bitcoind_rpc = BitcoinRpcConfig::get_defaults_from_env_vars()?;
269
270        Ok(Self {
271            opts,
272            bitcoind_rpc,
273            server_gens: ServerModuleInitRegistry::new(),
274            server_gen_params: ServerModuleConfigGenParamsRegistry::default(),
275            code_version_hash: code_version_hash.to_owned(),
276            code_version_str: code_version_vendor_suffix.map_or_else(
277                || fedimint_version.to_string(),
278                |suffix| format!("{fedimint_version}+{suffix}"),
279            ),
280        })
281    }
282
283    /// Attach a server module kind to the Fedimintd instance
284    ///
285    /// This makes `fedimintd` support additional module types (aka. kinds)
286    pub fn with_module_kind<T>(mut self, r#gen: T) -> Self
287    where
288        T: ServerModuleInit + 'static + Send + Sync,
289    {
290        self.server_gens.attach(r#gen);
291        self
292    }
293
294    /// Get the version hash this `fedimintd` will report for diagnostic
295    /// purposes
296    pub fn version_hash(&self) -> &str {
297        &self.code_version_hash
298    }
299
300    /// Attach additional module instance with parameters
301    ///
302    /// Note: The `kind` needs to be added with [`Self::with_module_kind`] if
303    /// it's not the default one.
304    pub fn with_module_instance<P>(mut self, kind: ModuleKind, params: P) -> Self
305    where
306        P: ModuleInitParams,
307    {
308        self.server_gen_params
309            .attach_config_gen_params(kind, params);
310        self
311    }
312
313    /// Attach default server modules to Fedimintd instance
314    pub fn with_default_modules(self) -> anyhow::Result<Self> {
315        let network = self.opts.network;
316
317        let bitcoind_rpc = self.bitcoind_rpc.clone();
318        let s = self
319            .with_module_kind(LightningInit)
320            .with_module_instance(
321                LightningInit::kind(),
322                LightningGenParams {
323                    local: LightningGenParamsLocal {
324                        bitcoin_rpc: bitcoind_rpc.clone(),
325                    },
326                    consensus: LightningGenParamsConsensus { network },
327                },
328            )
329            .with_module_kind(MintInit)
330            .with_module_instance(
331                MintInit::kind(),
332                MintGenParams {
333                    local: EmptyGenParams::default(),
334                    consensus: MintGenParamsConsensus::new(
335                        2,
336                        // TODO: wait for clients to support the relative fees and set them to
337                        // non-zero in 0.6
338                        fedimint_mint_common::config::FeeConsensus::zero(),
339                    ),
340                },
341            )
342            .with_module_kind(WalletInit)
343            .with_module_instance(
344                WalletInit::kind(),
345                WalletGenParams {
346                    local: WalletGenParamsLocal {
347                        bitcoin_rpc: bitcoind_rpc.clone(),
348                    },
349                    consensus: WalletGenParamsConsensus {
350                        network,
351                        finality_delay: 10,
352                        client_default_bitcoin_rpc: default_esplora_server(network),
353                        fee_consensus:
354                            fedimint_wallet_server::common::config::FeeConsensus::default(),
355                    },
356                },
357            );
358
359        let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
360            || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
361
362        let s = if enable_lnv2 {
363            s.with_module_kind(fedimint_lnv2_server::LightningInit)
364                .with_module_instance(
365                    fedimint_lnv2_server::LightningInit::kind(),
366                    fedimint_lnv2_common::config::LightningGenParams {
367                        local: fedimint_lnv2_common::config::LightningGenParamsLocal {
368                            bitcoin_rpc: bitcoind_rpc.clone(),
369                        },
370                        consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
371                            // TODO: actually make the relative fee configurable
372                            fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(100)?,
373                            network,
374                        },
375                    },
376                )
377        } else {
378            s
379        };
380
381        let s = if is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
382            s
383        } else {
384            s.with_module_kind(MetaInit)
385                .with_module_instance(MetaInit::kind(), MetaGenParams::default())
386        };
387
388        let s = if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
389            s.with_module_kind(UnknownInit)
390                .with_module_instance(UnknownInit::kind(), UnknownGenParams::default())
391        } else {
392            s
393        };
394
395        Ok(s)
396    }
397
398    /// Block thread and run a Fedimintd server
399    pub async fn run(self) -> ! {
400        // handle optional subcommand
401        if let Some(subcommand) = &self.opts.subcommand {
402            match subcommand {
403                ServerSubcommand::Dev(DevSubcommand::ListApiVersions) => {
404                    let api_versions = self.get_server_api_versions();
405                    let api_versions = serde_json::to_string_pretty(&api_versions)
406                        .expect("API versions struct is serializable");
407                    println!("{api_versions}");
408                    std::process::exit(0);
409                }
410                ServerSubcommand::Dev(DevSubcommand::ListDbVersions) => {
411                    let db_versions = self.get_server_db_versions();
412                    let db_versions = serde_json::to_string_pretty(&db_versions)
413                        .expect("API versions struct is serializable");
414                    println!("{db_versions}");
415                    std::process::exit(0);
416                }
417            }
418        }
419
420        let root_task_group = TaskGroup::new();
421        root_task_group.install_kill_handler();
422
423        let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
424
425        let task_group = root_task_group.clone();
426        root_task_group.spawn_cancellable("main", async move {
427            match run(
428                self.opts,
429                &task_group,
430                self.server_gens,
431                self.server_gen_params,
432                self.code_version_str,
433            )
434            .await
435            {
436                Ok(()) => {}
437                Err(error) => {
438                    crit!(target: LOG_SERVER, err = %error.fmt_compact_anyhow(), "Main task returned error, shutting down");
439                    task_group.shutdown();
440                }
441            }
442        });
443
444        let shutdown_future = root_task_group
445            .make_handle()
446            .make_shutdown_rx()
447            .then(|()| async {
448                info!(target: LOG_CORE, "Shutdown called");
449            });
450
451        shutdown_future.await;
452        debug!(target: LOG_CORE, "Terminating main task");
453
454        if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
455            error!(target: LOG_CORE, ?err, "Error while shutting down task group");
456        }
457
458        debug!(target: LOG_CORE, "Shutdown complete");
459
460        fedimint_logging::shutdown();
461
462        drop(timing_total_runtime);
463
464        // Should we ever shut down without an error code?
465        std::process::exit(-1);
466    }
467
468    fn get_server_api_versions(&self) -> ServerApiVersionsSummary {
469        ServerApiVersionsSummary {
470            core: ServerConfig::supported_api_versions().api,
471            modules: self
472                .server_gens
473                .kinds()
474                .into_iter()
475                .map(|module_kind| {
476                    self.server_gens
477                        .get(&module_kind)
478                        .expect("module is present")
479                })
480                .map(|module_init| {
481                    (
482                        module_init.module_kind(),
483                        module_init.supported_api_versions().api,
484                    )
485                })
486                .collect(),
487        }
488    }
489
490    fn get_server_db_versions(&self) -> ServerDbVersionsSummary {
491        ServerDbVersionsSummary {
492            modules: self
493                .server_gens
494                .kinds()
495                .into_iter()
496                .map(|module_kind| {
497                    self.server_gens
498                        .get(&module_kind)
499                        .expect("module is present")
500                })
501                .map(|module_init| {
502                    (
503                        module_init.module_kind(),
504                        get_current_database_version(&module_init.get_database_migrations()),
505                    )
506                })
507                .collect(),
508        }
509    }
510}
511
512async fn run(
513    opts: ServerOpts,
514    task_group: &TaskGroup,
515    module_inits: ServerModuleInitRegistry,
516    module_inits_params: ServerModuleConfigGenParamsRegistry,
517    code_version_str: String,
518) -> anyhow::Result<()> {
519    if let Some(socket_addr) = opts.bind_metrics_api.as_ref() {
520        task_group.spawn_cancellable("metrics-server", {
521            let task_group = task_group.clone();
522            let socket_addr = *socket_addr;
523            async move { fedimint_metrics::run_api_server(socket_addr, task_group).await }
524        });
525    }
526
527    let data_dir = opts.data_dir.context("data-dir option is not present")?;
528
529    // TODO: Fedimintd should use the config gen API
530    // on each run we want to pass the currently passed password, so we need to
531    // overwrite
532    if let Some(password) = opts.password {
533        write_overwrite(data_dir.join(PLAINTEXT_PASSWORD), password)?;
534    };
535    let use_iroh = is_env_var_set(FM_FORCE_IROH_ENV);
536
537    // TODO: meh, move, refactor
538    let settings = ConfigGenSettings {
539        p2p_bind: opts.bind_p2p,
540        bind_api_ws: opts.bind_api_ws,
541        bind_api_iroh: opts.bind_api_iroh,
542        ui_bind: opts.bind_ui,
543        p2p_url: opts.p2p_url,
544        api_url: opts.api_url,
545        modules: module_inits_params.clone(),
546        registry: module_inits.clone(),
547        networking: if use_iroh {
548            NetworkingStack::Iroh
549        } else {
550            NetworkingStack::default()
551        },
552    };
553
554    let db = Database::new(
555        fedimint_rocksdb::RocksDb::open(data_dir.join(DB_FILE)).await?,
556        ModuleRegistry::default(),
557    );
558
559    Box::pin(fedimint_server::run(
560        data_dir,
561        opts.force_api_secrets,
562        settings,
563        db,
564        code_version_str,
565        &module_inits,
566        task_group.clone(),
567        Some(Box::new(fedimint_server_ui::dashboard::start)),
568        Some(Box::new(fedimint_server_ui::setup::start)),
569    ))
570    .await
571}