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