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::time::Duration;
17
18use anyhow::Context as _;
19use bitcoin::Network;
20use clap::{ArgGroup, CommandFactory, FromArgMatches, Parser};
21use fedimint_core::db::Database;
22use fedimint_core::envs::{
23    FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
24};
25use fedimint_core::module::CORE_CONSENSUS_VERSION;
26use fedimint_core::module::registry::ModuleRegistry;
27use fedimint_core::rustls::install_crypto_provider;
28use fedimint_core::task::TaskGroup;
29use fedimint_core::timing;
30use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
31use fedimint_ln_server::LightningInit;
32use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
33use fedimint_meta_server::MetaInit;
34use fedimint_mint_server::MintInit;
35use fedimint_rocksdb::RocksDb;
36use fedimint_server::config::ConfigGenSettings;
37use fedimint_server::config::io::DB_FILE;
38use fedimint_server::core::ServerModuleInitRegistry;
39use fedimint_server::net::api::ApiSecrets;
40use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
41use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
42use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
43use fedimint_server_bitcoin_rpc::tracked::ServerBitcoinRpcTracked;
44use fedimint_server_core::ServerModuleInitRegistryExt;
45use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
46use fedimint_unknown_server::UnknownInit;
47use fedimint_wallet_server::WalletInit;
48use fedimintd_envs::{
49    FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
50    FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
51    FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
52    FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
53    FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
54    FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, FM_P2P_URL_ENV,
55    FM_SESSION_TIMEOUT_SECS_ENV,
56};
57use futures::FutureExt as _;
58#[cfg(all(
59    not(feature = "jemalloc"),
60    not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
61))]
62use tracing::warn;
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", value_delimiter = ',')]
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    /// Exit the process if a consensus session is not completed within this
182    /// many seconds, relying on the process supervisor to restart fedimintd.
183    /// Restarting guardians has been observed to resolve stuck sessions.
184    #[arg(long, env = FM_SESSION_TIMEOUT_SECS_ENV, default_value = "3600")]
185    session_timeout_secs: u64,
186
187    /// Enable tokio console logging
188    #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
189    bind_tokio_console: Option<SocketAddr>,
190
191    /// Enable jaeger for tokio console logging
192    #[arg(long, default_value = "false")]
193    with_jaeger: bool,
194
195    /// Enable prometheus metrics
196    #[arg(long, env = FM_BIND_METRICS_ENV, default_value = "127.0.0.1:8176")]
197    bind_metrics: Option<SocketAddr>,
198
199    /// Comma separated list of API secrets.
200    ///
201    /// Setting it will enforce API authentication and make the Federation
202    /// "private".
203    ///
204    /// The first secret in the list is the "active" one that the peer will use
205    /// itself to connect to other peers. Any further one is accepted by
206    /// this peer, e.g. for the purposes of smooth rotation of secret
207    /// between users.
208    ///
209    /// Note that the value provided here will override any other settings
210    /// that the user might want to set via UI at runtime, etc.
211    /// In the future, managing secrets might be possible via Admin UI
212    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
213    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
214    force_api_secrets: ApiSecrets,
215
216    /// Maximum number of concurrent Iroh API connections
217    #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
218    iroh_api_max_connections: usize,
219
220    /// Maximum number of parallel requests per Iroh API connection
221    #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
222    iroh_api_max_requests_per_connection: usize,
223}
224
225impl ServerOpts {
226    pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
227        let url = self
228            .bitcoind_url
229            .clone()
230            .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
231        if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
232            let password = tokio::fs::read_to_string(password_file)
233                .await
234                .context("Failed to read the password")?
235                .trim()
236                .to_owned();
237            Ok((url, password))
238        } else {
239            let password = self
240                .bitcoind_password
241                .clone()
242                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
243            Ok((url, password))
244        }
245    }
246}
247
248/// Block the thread and run a Fedimintd server
249///
250/// # Arguments
251///
252/// * `module_init_registry` - The registry of available modules.
253///
254/// * `code_version_hash` - The git hash of the code that the `fedimintd` binary
255///   is being built from. This is used mostly for information purposes
256///   (`fedimintd version-hash`). See `fedimint-build` crate for easy way to
257///   obtain it.
258///
259/// * `code_version_vendor_suffix` - An optional suffix that will be appended to
260///   the internal fedimint release version, to distinguish binaries built by
261///   different vendors, usually with a different set of modules. Currently DKG
262///   will enforce that the combined `code_version` is the same between all
263///   peers.
264#[allow(clippy::too_many_lines)]
265pub async fn run(
266    module_init_registry: ServerModuleInitRegistry,
267    code_version_hash: &str,
268    code_version_vendor_suffix: Option<&str>,
269) -> anyhow::Result<Infallible> {
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 = {
285        // Collect env vars from all registered modules and append them to the
286        // long-help text so operators can discover them via `fedimintd --help`.
287        let mut module_env_help = String::from("\nModule environment variables:\n");
288        for (_kind, module_init) in module_init_registry.iter() {
289            for doc in module_init.get_documented_env_vars() {
290                let _ = writeln!(module_env_help, "  {:40}  {}", doc.name, doc.description);
291            }
292        }
293        let matches = ServerOpts::command()
294            .after_long_help(module_env_help)
295            .get_matches();
296        ServerOpts::from_arg_matches(&matches)
297            .expect("clap arg matches must be valid after parsing")
298    };
299
300    let mut tracing_builder = TracingSetup::default();
301
302    tracing_builder
303        .tokio_console_bind(server_opts.bind_tokio_console)
304        .with_jaeger(server_opts.with_jaeger);
305
306    tracing_builder.init().unwrap();
307
308    info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
309
310    #[cfg(all(
311        not(feature = "jemalloc"),
312        not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
313    ))]
314    warn!(
315        target: LOG_SERVER,
316        "fedimintd was built without the `jemalloc` feature. rocksdb is prone to memory \
317         fragmentation with the default allocator; consider rebuilding with `--features jemalloc`."
318    );
319
320    debug!(
321        target: LOG_SERVER,
322        core_consensus = %CORE_CONSENSUS_VERSION,
323        "Supported core consensus version",
324    );
325    for (kind, module) in module_init_registry.iter() {
326        let supported = module.supported_api_versions();
327        debug!(
328            target: LOG_SERVER,
329            module = %kind,
330            supported = %supported,
331            "Supported module versions",
332        );
333    }
334    let code_version_str = code_version_vendor_suffix.map_or_else(
335        || fedimint_version.to_string(),
336        |suffix| format!("{fedimint_version}+{suffix}"),
337    );
338
339    let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
340
341    let root_task_group = TaskGroup::new();
342
343    if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
344        info!(
345            target: LOG_SERVER,
346            url = %format!("http://{}/metrics", bind_metrics),
347            "Initializing metrics server",
348        );
349        fedimint_metrics::spawn_api_server(*bind_metrics, root_task_group.clone()).await?;
350    }
351
352    let settings = ConfigGenSettings {
353        p2p_bind: server_opts.bind_p2p,
354        api_bind: server_opts.bind_api,
355        ui_bind: server_opts.bind_ui,
356        p2p_url: server_opts.p2p_url.clone(),
357        api_url: server_opts.api_url.clone(),
358        enable_iroh: server_opts.enable_iroh,
359        iroh_dns: server_opts.iroh_dns.clone(),
360        iroh_relays: server_opts.iroh_relays.clone(),
361        network: server_opts.bitcoin_network,
362        available_modules: module_init_registry.kinds(),
363        default_modules: module_init_registry.default_modules(),
364    };
365
366    let db = Database::new(
367        RocksDb::build(server_opts.data_dir.join(DB_FILE))
368            .open()
369            .await
370            .unwrap(),
371        ModuleRegistry::default(),
372    );
373
374    let dyn_server_bitcoin_rpc = match (
375        server_opts.bitcoind_url.as_ref(),
376        server_opts.esplora_url.as_ref(),
377    ) {
378        (Some(_), None) => {
379            let bitcoind_username = server_opts
380                .bitcoind_username
381                .clone()
382                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
383            let (bitcoind_url, bitcoind_password) = server_opts
384                .get_bitcoind_url_and_password()
385                .await
386                .expect("Failed to get bitcoind url");
387            BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
388                .unwrap()
389                .into_dyn()
390        }
391        (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
392        (Some(_), Some(esplora_url)) => {
393            let bitcoind_username = server_opts
394                .bitcoind_username
395                .clone()
396                .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
397            let (bitcoind_url, bitcoind_password) = server_opts
398                .get_bitcoind_url_and_password()
399                .await
400                .expect("Failed to get bitcoind url");
401            BitcoindClientWithFallback::new(
402                bitcoind_username,
403                bitcoind_password,
404                &bitcoind_url,
405                esplora_url,
406            )
407            .unwrap()
408            .into_dyn()
409        }
410        _ => unreachable!("ArgGroup already enforced XOR relation"),
411    };
412    let dyn_server_bitcoin_rpc =
413        ServerBitcoinRpcTracked::new(dyn_server_bitcoin_rpc, "server").into_dyn();
414
415    root_task_group.install_kill_handler();
416
417    install_crypto_provider().await;
418
419    let task_group = root_task_group.clone();
420    let code_version_hash = code_version_hash.to_string();
421    root_task_group.spawn_cancellable("main", async move {
422        fedimint_server::run(
423            server_opts.data_dir,
424            server_opts.force_api_secrets,
425            settings,
426            db,
427            code_version_str,
428            code_version_hash,
429            module_init_registry,
430            task_group,
431            dyn_server_bitcoin_rpc,
432            Box::new(fedimint_server_ui::setup::router),
433            Box::new(fedimint_server_ui::dashboard::router),
434            server_opts.db_checkpoint_retention,
435            Duration::from_secs(server_opts.session_timeout_secs),
436            fedimint_server::ConnectionLimits::new(
437                server_opts.iroh_api_max_connections,
438                server_opts.iroh_api_max_requests_per_connection,
439            ),
440        )
441        .await
442        .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
443    });
444
445    let shutdown_future = root_task_group
446        .make_handle()
447        .make_shutdown_rx()
448        .then(|()| async {
449            info!(target: LOG_CORE, "Shutdown called");
450        });
451
452    shutdown_future.await;
453
454    debug!(target: LOG_CORE, "Terminating main task");
455
456    if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
457        error!(target: LOG_CORE, err = %err.fmt_compact_anyhow(), "Error while shutting down task group");
458    }
459
460    debug!(target: LOG_CORE, "Shutdown complete");
461
462    fedimint_logging::shutdown();
463
464    drop(timing_total_runtime);
465
466    std::process::exit(-1);
467}
468
469pub fn default_modules() -> ServerModuleInitRegistry {
470    let mut server_gens = ServerModuleInitRegistry::new();
471
472    server_gens.attach(MintInit);
473    server_gens.attach(fedimint_mintv2_server::MintInit);
474
475    server_gens.attach(WalletInit);
476    server_gens.attach(fedimint_walletv2_server::WalletInit);
477
478    server_gens.attach(LightningInit);
479    server_gens.attach(fedimint_lnv2_server::LightningInit);
480
481    if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
482        server_gens.attach(MetaInit);
483    }
484
485    if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
486        server_gens.attach(UnknownInit);
487    }
488
489    server_gens
490}