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