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::env;
12use std::net::SocketAddr;
13use std::path::PathBuf;
14use std::time::Duration;
15
16use anyhow::Context as _;
17use bitcoin::Network;
18use clap::{ArgGroup, Parser};
19use fedimint_core::config::{EmptyGenParams, ServerModuleConfigGenParamsRegistry};
20use fedimint_core::db::Database;
21use fedimint_core::envs::{
22    BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV,
23    FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
24};
25use fedimint_core::module::registry::ModuleRegistry;
26use fedimint_core::rustls::install_crypto_provider;
27use fedimint_core::task::TaskGroup;
28use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
29use fedimint_core::{default_esplora_server, timing};
30use fedimint_ln_common::config::{
31    LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
32};
33use fedimint_ln_server::LightningInit;
34use fedimint_logging::{LOG_CORE, TracingSetup};
35use fedimint_meta_server::{MetaGenParams, MetaInit};
36use fedimint_mint_server::MintInit;
37use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
38use fedimint_rocksdb::RocksDb;
39use fedimint_server::config::ConfigGenSettings;
40use fedimint_server::config::io::DB_FILE;
41use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
42use fedimint_server::net::api::ApiSecrets;
43use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
44use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
45use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
46use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
47use fedimint_unknown_common::config::UnknownGenParams;
48use fedimint_unknown_server::UnknownInit;
49use fedimint_wallet_server::WalletInit;
50use fedimint_wallet_server::common::config::{
51    WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
52};
53use fedimintd_envs::{
54    FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRCIS_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, FM_P2P_URL_ENV,
60    FM_PORT_ESPLORA_ENV,
61};
62use futures::FutureExt as _;
63use tracing::{debug, error, info};
64
65use crate::metrics::APP_START_TS;
66
67/// Time we will wait before forcefully shutting down tasks
68const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
69
70#[derive(Parser)]
71#[command(version)]
72#[command(
73    group(
74        ArgGroup::new("bitcoind_password_auth")
75           .args(["bitcoind_password", "bitcoind_url_password_file"])
76           .multiple(false)
77    ),
78    group(
79        ArgGroup::new("bitcoind_auth")
80            .args(["bitcoind_url"])
81            .requires("bitcoind_password_auth")
82            .requires_all(["bitcoind_username", "bitcoind_url"])
83    ),
84    group(
85        ArgGroup::new("bitcoin_rpc")
86            .required(true)
87            .multiple(true)
88            .args(["bitcoind_url", "esplora_url"])
89    )
90)]
91struct ServerOpts {
92    /// Path to folder containing federation config files
93    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
94    data_dir: PathBuf,
95
96    /// The bitcoin network of the federation
97    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
98    bitcoin_network: Network,
99
100    /// The username to use when connecting to bitcoind
101    #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
102    bitcoind_username: Option<String>,
103
104    /// The password to use when connecting to bitcoind
105    #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
106    bitcoind_password: Option<String>,
107
108    /// Bitcoind RPC URL, e.g. <http://127.0.0.1:8332>
109    /// This should not include authentication parameters, they should be
110    /// included in `FM_BITCOIND_USERNAME` and `FM_BITCOIND_PASSWORD`
111    #[arg(long, env = FM_BITCOIND_URL_ENV)]
112    bitcoind_url: Option<SafeUrl>,
113
114    /// If set, the password part of `--bitcoind-url` will be set/replaced with
115    /// the content of this file.
116    ///
117    /// This is useful for setups that provide secret material via ramdisk
118    /// e.g. SOPS, age, etc.
119    ///
120    /// Note this is not meant to handle bitcoind's cookie file.
121    #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
122    bitcoind_url_password_file: Option<PathBuf>,
123
124    /// Esplora HTTP base URL, e.g. <https://mempool.space/api>
125    #[arg(long, env = FM_ESPLORA_URL_ENV)]
126    esplora_url: Option<SafeUrl>,
127
128    /// Address we bind to for p2p consensus communication
129    ///
130    /// Should be `0.0.0.0:8173` most of the time, as p2p connectivity is public
131    /// and direct, and the port should be open it in the firewall.
132    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
133    bind_p2p: SocketAddr,
134
135    /// Address we bind to for the API
136    ///
137    /// Should be `0.0.0.0:8174` most of the time, as api connectivity is public
138    /// and direct, and the port should be open it in the firewall.
139    #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
140    bind_api: SocketAddr,
141
142    /// Address we bind to for exposing the Web UI
143    ///
144    /// Built-in web UI is exposed as an HTTP port, and typically should
145    /// have TLS terminated by Nginx/Traefik/etc. and forwarded to the locally
146    /// bind port.
147    #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
148    bind_ui: SocketAddr,
149
150    /// Our external address for communicating with our peers
151    ///
152    /// `fedimint://<fqdn>:8173` for TCP/TLS p2p connectivity (legacy/standard).
153    ///
154    /// Ignored when Iroh stack is used. (newer/experimental)
155    #[arg(long, env = FM_P2P_URL_ENV)]
156    p2p_url: Option<SafeUrl>,
157
158    /// Our API address for clients to connect to us
159    ///
160    /// Typically `wss://<fqdn>/ws/` for TCP/TLS connectivity (legacy/standard)
161    ///
162    /// Ignored when Iroh stack is used. (newer/experimental)
163    #[arg(long, env = FM_API_URL_ENV)]
164    api_url: Option<SafeUrl>,
165
166    #[arg(long, env = FM_ENABLE_IROH_ENV)]
167    enable_iroh: bool,
168
169    /// Optional URL of the Iroh DNS server
170    #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
171    iroh_dns: Option<SafeUrl>,
172
173    /// Optional URLs of the Iroh relays to use for registering
174    #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh")]
175    iroh_relays: Vec<SafeUrl>,
176
177    /// Number of checkpoints from the current session to retain on disk
178    #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
179    db_checkpoint_retention: u64,
180
181    /// Enable tokio console logging
182    #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
183    bind_tokio_console: Option<SocketAddr>,
184
185    /// Enable jaeger for tokio console logging
186    #[arg(long, default_value = "false")]
187    with_jaeger: bool,
188
189    /// Enable prometheus metrics
190    #[arg(long, env = FM_BIND_METRCIS_ENV)]
191    bind_metrics: Option<SocketAddr>,
192
193    /// Comma separated list of API secrets.
194    ///
195    /// Setting it will enforce API authentication and make the Federation
196    /// "private".
197    ///
198    /// The first secret in the list is the "active" one that the peer will use
199    /// itself to connect to other peers. Any further one is accepted by
200    /// this peer, e.g. for the purposes of smooth rotation of secret
201    /// between users.
202    ///
203    /// Note that the value provided here will override any other settings
204    /// that the user might want to set via UI at runtime, etc.
205    /// In the future, managing secrets might be possible via Admin UI
206    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
207    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
208    force_api_secrets: ApiSecrets,
209
210    /// Maximum number of concurrent Iroh API connections
211    #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
212    iroh_api_max_connections: usize,
213
214    /// Maximum number of parallel requests per Iroh API connection
215    #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
216    iroh_api_max_requests_per_connection: usize,
217}
218
219impl ServerOpts {
220    pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
221        let url = self
222            .bitcoind_url
223            .clone()
224            .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
225        if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
226            let password = tokio::fs::read_to_string(password_file)
227                .await
228                .context("Failed to read the password")?
229                .trim()
230                .to_owned();
231            Ok((url, password))
232        } else {
233            let password = self
234                .bitcoind_password
235                .clone()
236                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
237            Ok((url, password))
238        }
239    }
240}
241
242/// Block the thread and run a Fedimintd server
243///
244/// # Arguments
245///
246/// * `modules_fn` - A function to initialize the modules.
247///
248/// * `code_version_hash` - The git hash of the code that the `fedimintd` binary
249///   is being built from. This is used mostly for information purposes
250///   (`fedimintd version-hash`). See `fedimint-build` crate for easy way to
251///   obtain it.
252///
253/// * `code_version_vendor_suffix` - An optional suffix that will be appended to
254///   the internal fedimint release version, to distinguish binaries built by
255///   different vendors, usually with a different set of modules. Currently DKG
256///   will enforce that the combined `code_version` is the same between all
257///   peers.
258#[allow(clippy::too_many_lines)]
259pub async fn run(
260    modules_fn: fn(
261        Network,
262    ) -> (
263        ServerModuleInitRegistry,
264        ServerModuleConfigGenParamsRegistry,
265    ),
266    code_version_hash: &str,
267    code_version_vendor_suffix: Option<&str>,
268) -> ! {
269    assert_eq!(
270        env!("FEDIMINT_BUILD_CODE_VERSION").len(),
271        code_version_hash.len(),
272        "version_hash must have an expected length"
273    );
274
275    handle_version_hash_command(code_version_hash);
276
277    let fedimint_version = env!("CARGO_PKG_VERSION");
278
279    APP_START_TS
280        .with_label_values(&[fedimint_version, code_version_hash])
281        .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
282
283    let server_opts = ServerOpts::parse();
284
285    let mut tracing_builder = TracingSetup::default();
286
287    tracing_builder
288        .tokio_console_bind(server_opts.bind_tokio_console)
289        .with_jaeger(server_opts.with_jaeger);
290
291    tracing_builder.init().unwrap();
292
293    info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
294
295    let code_version_str = code_version_vendor_suffix.map_or_else(
296        || fedimint_version.to_string(),
297        |suffix| format!("{fedimint_version}+{suffix}"),
298    );
299
300    let (server_gens, server_gen_params) = modules_fn(server_opts.bitcoin_network);
301
302    let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
303
304    let root_task_group = TaskGroup::new();
305
306    if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
307        root_task_group.spawn_cancellable(
308            "metrics-server",
309            fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
310        );
311    }
312
313    let settings = ConfigGenSettings {
314        p2p_bind: server_opts.bind_p2p,
315        api_bind: server_opts.bind_api,
316        ui_bind: server_opts.bind_ui,
317        p2p_url: server_opts.p2p_url.clone(),
318        api_url: server_opts.api_url.clone(),
319        enable_iroh: server_opts.enable_iroh,
320        iroh_dns: server_opts.iroh_dns.clone(),
321        iroh_relays: server_opts.iroh_relays.clone(),
322        modules: server_gen_params.clone(),
323        registry: server_gens.clone(),
324    };
325
326    let db = Database::new(
327        RocksDb::open(server_opts.data_dir.join(DB_FILE))
328            .await
329            .unwrap(),
330        ModuleRegistry::default(),
331    );
332
333    let dyn_server_bitcoin_rpc = match (
334        server_opts.bitcoind_url.as_ref(),
335        server_opts.esplora_url.as_ref(),
336    ) {
337        (Some(_), None) => {
338            let bitcoind_username = server_opts
339                .bitcoind_username
340                .clone()
341                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
342            let (bitcoind_url, bitcoind_password) = server_opts
343                .get_bitcoind_url_and_password()
344                .await
345                .expect("Failed to get bitcoind url");
346            BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
347                .unwrap()
348                .into_dyn()
349        }
350        (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
351        (Some(_), Some(esplora_url)) => {
352            let bitcoind_username = server_opts
353                .bitcoind_username
354                .clone()
355                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
356            let (bitcoind_url, bitcoind_password) = server_opts
357                .get_bitcoind_url_and_password()
358                .await
359                .expect("Failed to get bitcoind url");
360            BitcoindClientWithFallback::new(
361                bitcoind_username,
362                bitcoind_password,
363                &bitcoind_url,
364                esplora_url,
365            )
366            .unwrap()
367            .into_dyn()
368        }
369        _ => unreachable!("ArgGroup already enforced XOR relation"),
370    };
371
372    root_task_group.install_kill_handler();
373
374    install_crypto_provider().await;
375
376    let task_group = root_task_group.clone();
377    root_task_group.spawn_cancellable("main", async move {
378        fedimint_server::run(
379            server_opts.data_dir,
380            server_opts.force_api_secrets,
381            settings,
382            db,
383            code_version_str,
384            server_gens,
385            task_group,
386            dyn_server_bitcoin_rpc,
387            Box::new(fedimint_server_ui::setup::router),
388            Box::new(fedimint_server_ui::dashboard::router),
389            server_opts.db_checkpoint_retention,
390            fedimint_server::ConnectionLimits::new(
391                server_opts.iroh_api_max_connections,
392                server_opts.iroh_api_max_requests_per_connection,
393            ),
394        )
395        .await
396        .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
397    });
398
399    let shutdown_future = root_task_group
400        .make_handle()
401        .make_shutdown_rx()
402        .then(|()| async {
403            info!(target: LOG_CORE, "Shutdown called");
404        });
405
406    shutdown_future.await;
407    debug!(target: LOG_CORE, "Terminating main task");
408
409    if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
410        error!(target: LOG_CORE, err = %err.fmt_compact_anyhow(), "Error while shutting down task group");
411    }
412
413    debug!(target: LOG_CORE, "Shutdown complete");
414
415    fedimint_logging::shutdown();
416
417    drop(timing_total_runtime);
418
419    // Should we ever shut down without an error code?
420    std::process::exit(-1);
421}
422
423pub fn default_modules(
424    network: Network,
425) -> (
426    ServerModuleInitRegistry,
427    ServerModuleConfigGenParamsRegistry,
428) {
429    let mut server_gens = ServerModuleInitRegistry::new();
430    let mut server_gen_params = ServerModuleConfigGenParamsRegistry::default();
431
432    let bitcoin_rpc_config = BitcoinRpcConfig {
433        kind: "bitcoind".to_string(),
434        url: "http://unused_dummy.xyz".parse().unwrap(),
435    };
436
437    server_gens.attach(LightningInit);
438    server_gen_params.attach_config_gen_params(
439        LightningInit::kind(),
440        LightningGenParams {
441            local: LightningGenParamsLocal {
442                bitcoin_rpc: bitcoin_rpc_config.clone(),
443            },
444            consensus: LightningGenParamsConsensus { network },
445        },
446    );
447
448    server_gens.attach(MintInit);
449    server_gen_params.attach_config_gen_params(
450        MintInit::kind(),
451        MintGenParams {
452            local: EmptyGenParams::default(),
453            consensus: MintGenParamsConsensus::new(2, None),
454        },
455    );
456
457    server_gens.attach(WalletInit);
458    server_gen_params.attach_config_gen_params(
459        WalletInit::kind(),
460        WalletGenParams {
461            local: WalletGenParamsLocal {
462                bitcoin_rpc: bitcoin_rpc_config.clone(),
463            },
464            consensus: WalletGenParamsConsensus {
465                network,
466                finality_delay: default_finality_delay(network),
467                client_default_bitcoin_rpc: default_esplora_server(
468                    network,
469                    std::env::var(FM_PORT_ESPLORA_ENV).ok(),
470                ),
471                fee_consensus: fedimint_wallet_server::common::config::FeeConsensus::default(),
472            },
473        },
474    );
475
476    let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
477        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
478
479    if enable_lnv2 {
480        server_gens.attach(fedimint_lnv2_server::LightningInit);
481        server_gen_params.attach_config_gen_params(
482            fedimint_lnv2_server::LightningInit::kind(),
483            fedimint_lnv2_common::config::LightningGenParams {
484                local: fedimint_lnv2_common::config::LightningGenParamsLocal {
485                    bitcoin_rpc: bitcoin_rpc_config.clone(),
486                },
487                consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
488                    fee_consensus: None,
489                    network,
490                },
491            },
492        );
493    }
494
495    if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
496        server_gens.attach(MetaInit);
497        server_gen_params.attach_config_gen_params(MetaInit::kind(), MetaGenParams::default());
498    }
499
500    if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
501        server_gens.attach(UnknownInit);
502        server_gen_params
503            .attach_config_gen_params(UnknownInit::kind(), UnknownGenParams::default());
504    }
505
506    (server_gens, server_gen_params)
507}
508
509/// For real Bitcoin we want to have a responsible default, while for demos
510/// nobody wants to wait multiple minutes
511fn default_finality_delay(network: Network) -> u32 {
512    match network {
513        Network::Bitcoin | Network::Regtest => 10,
514        Network::Testnet | Network::Signet | Network::Testnet4 => 2,
515        _ => panic!("Unsupported network"),
516    }
517}