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