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::builder::BoolishValueParser;
21use clap::{ArgGroup, CommandFactory, FromArgMatches, Parser};
22use fedimint_core::db::Database;
23use fedimint_core::envs::{
24 FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
25 is_running_in_test_env,
26};
27use fedimint_core::module::registry::ModuleRegistry;
28use fedimint_core::module::{ApiAuth, CORE_CONSENSUS_VERSION};
29use fedimint_core::rustls::install_crypto_provider;
30use fedimint_core::task::TaskGroup;
31use fedimint_core::timing;
32use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
33use fedimint_ln_server::LightningInit;
34use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
35use fedimint_meta_server::MetaInit;
36use fedimint_mint_server::MintInit;
37use fedimint_rocksdb::RocksDb;
38use fedimint_server::config::ConfigGenSettings;
39use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
40use fedimint_server::core::ServerModuleInitRegistry;
41use fedimint_server::net::api::ApiSecrets;
42use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
43use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
44use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
45use fedimint_server_bitcoin_rpc::tracked::ServerBitcoinRpcTracked;
46use fedimint_server_core::ServerModuleInitRegistryExt;
47use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
48use fedimint_unknown_server::UnknownInit;
49use fedimint_wallet_server::WalletInit;
50use fedimintd_envs::{
51 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
52 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
53 FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
54 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
55 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
56 FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, FM_P2P_URL_ENV,
57 FM_PASSWORD_API_ENV, FM_PASSWORD_UI_ENV, FM_SESSION_TIMEOUT_SECS_ENV,
58};
59use futures::FutureExt as _;
60#[cfg(all(
61 not(feature = "jemalloc"),
62 not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
63))]
64use tracing::warn;
65use tracing::{debug, error, info};
66
67use crate::metrics::APP_START_TS;
68
69const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
71
72#[derive(Parser)]
73#[command(version)]
74#[command(
75 group(
76 ArgGroup::new("bitcoind_password_auth")
77 .args(["bitcoind_password", "bitcoind_url_password_file"])
78 .multiple(false)
79 ),
80 group(
81 ArgGroup::new("bitcoind_auth")
82 .args(["bitcoind_url"])
83 .requires("bitcoind_password_auth")
84 .requires_all(["bitcoind_username", "bitcoind_url"])
85 ),
86 group(
87 ArgGroup::new("bitcoin_rpc")
88 .required(true)
89 .multiple(true)
90 .args(["bitcoind_url", "esplora_url"])
91 )
92)]
93struct ServerOpts {
94 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
96 data_dir: PathBuf,
97
98 #[arg(long, env = FM_PASSWORD_UI_ENV)]
103 password_ui: Option<String>,
104
105 #[arg(long, env = FM_PASSWORD_API_ENV)]
110 password_api: Option<String>,
111
112 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
114 bitcoin_network: Network,
115
116 #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
118 bitcoind_username: Option<String>,
119
120 #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
122 bitcoind_password: Option<String>,
123
124 #[arg(long, env = FM_BITCOIND_URL_ENV)]
128 bitcoind_url: Option<SafeUrl>,
129
130 #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
138 bitcoind_url_password_file: Option<PathBuf>,
139
140 #[arg(long, env = FM_ESPLORA_URL_ENV)]
142 esplora_url: Option<SafeUrl>,
143
144 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
149 bind_p2p: SocketAddr,
150
151 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
156 bind_api: SocketAddr,
157
158 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
164 bind_ui: SocketAddr,
165
166 #[arg(long, env = FM_P2P_URL_ENV)]
172 p2p_url: Option<SafeUrl>,
173
174 #[arg(long, env = FM_API_URL_ENV)]
180 api_url: Option<SafeUrl>,
181
182 #[arg(long, env = FM_ENABLE_IROH_ENV, value_parser = BoolishValueParser::new())]
189 enable_iroh: Option<bool>,
190
191 #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
193 iroh_dns: Option<SafeUrl>,
194
195 #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh", value_delimiter = ',')]
197 iroh_relays: Vec<SafeUrl>,
198
199 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
201 db_checkpoint_retention: u64,
202
203 #[arg(long, env = FM_SESSION_TIMEOUT_SECS_ENV, default_value = "3600")]
207 session_timeout_secs: u64,
208
209 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
211 bind_tokio_console: Option<SocketAddr>,
212
213 #[arg(long, default_value = "false")]
215 with_jaeger: bool,
216
217 #[arg(long, env = FM_BIND_METRICS_ENV, default_value = "127.0.0.1:8176")]
219 bind_metrics: Option<SocketAddr>,
220
221 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
236 force_api_secrets: ApiSecrets,
237
238 #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
240 iroh_api_max_connections: usize,
241
242 #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
244 iroh_api_max_requests_per_connection: usize,
245}
246
247impl ServerOpts {
248 pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
249 let url = self
250 .bitcoind_url
251 .clone()
252 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
253 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
254 let password = tokio::fs::read_to_string(password_file)
255 .await
256 .context("Failed to read the password")?
257 .trim()
258 .to_owned();
259 Ok((url, password))
260 } else {
261 let password = self
262 .bitcoind_password
263 .clone()
264 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
265 Ok((url, password))
266 }
267 }
268}
269
270#[allow(clippy::too_many_lines)]
287pub async fn run(
288 module_init_registry: ServerModuleInitRegistry,
289 code_version_hash: &str,
290 code_version_vendor_suffix: Option<&str>,
291) -> anyhow::Result<Infallible> {
292 assert_eq!(
293 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
294 code_version_hash.len(),
295 "version_hash must have an expected length"
296 );
297
298 handle_version_hash_command(code_version_hash);
299
300 let fedimint_version = env!("CARGO_PKG_VERSION");
301
302 APP_START_TS
303 .with_label_values(&[fedimint_version, code_version_hash])
304 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
305
306 let server_opts = {
307 let mut module_env_help = String::from("\nModule environment variables:\n");
310 for (_kind, module_init) in module_init_registry.iter() {
311 for doc in module_init.get_documented_env_vars() {
312 let _ = writeln!(module_env_help, " {:40} {}", doc.name, doc.description);
313 }
314 }
315 let matches = ServerOpts::command()
316 .after_long_help(module_env_help)
317 .get_matches();
318 ServerOpts::from_arg_matches(&matches)
319 .expect("clap arg matches must be valid after parsing")
320 };
321
322 let mut tracing_builder = TracingSetup::default();
323
324 tracing_builder
325 .tokio_console_bind(server_opts.bind_tokio_console)
326 .with_jaeger(server_opts.with_jaeger);
327
328 tracing_builder.init().unwrap();
329
330 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
331
332 #[cfg(all(
333 not(feature = "jemalloc"),
334 not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
335 ))]
336 warn!(
337 target: LOG_SERVER,
338 "fedimintd was built without the `jemalloc` feature. rocksdb is prone to memory \
339 fragmentation with the default allocator; consider rebuilding with `--features jemalloc`."
340 );
341
342 debug!(
343 target: LOG_SERVER,
344 core_consensus = %CORE_CONSENSUS_VERSION,
345 "Supported core consensus version",
346 );
347 for (kind, module) in module_init_registry.iter() {
348 let supported = module.supported_api_versions();
349 debug!(
350 target: LOG_SERVER,
351 module = %kind,
352 supported = %supported,
353 "Supported module versions",
354 );
355 }
356 let code_version_str = code_version_vendor_suffix.map_or_else(
357 || fedimint_version.to_string(),
358 |suffix| format!("{fedimint_version}+{suffix}"),
359 );
360
361 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
362
363 let root_task_group = TaskGroup::new();
364
365 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
366 info!(
367 target: LOG_SERVER,
368 url = %format!("http://{}/metrics", bind_metrics),
369 "Initializing metrics server",
370 );
371 fedimint_metrics::spawn_api_server(*bind_metrics, root_task_group.clone()).await?;
372 }
373
374 let settings = ConfigGenSettings {
375 p2p_bind: server_opts.bind_p2p,
376 api_bind: server_opts.bind_api,
377 ui_bind: server_opts.bind_ui,
378 p2p_url: server_opts.p2p_url.clone(),
379 api_url: server_opts.api_url.clone(),
380 enable_iroh: server_opts.enable_iroh.unwrap_or(!is_running_in_test_env()),
381 iroh_dns: server_opts.iroh_dns.clone(),
382 iroh_relays: server_opts.iroh_relays.clone(),
383 network: server_opts.bitcoin_network,
384 available_modules: module_init_registry.kinds(),
385 default_modules: module_init_registry.default_modules(),
386 };
387
388 let db = Database::new(
389 RocksDb::build(server_opts.data_dir.join(DB_FILE))
390 .open()
391 .await
392 .unwrap(),
393 ModuleRegistry::default(),
394 );
395
396 let dyn_server_bitcoin_rpc = match (
397 server_opts.bitcoind_url.as_ref(),
398 server_opts.esplora_url.as_ref(),
399 ) {
400 (Some(_), None) => {
401 let bitcoind_username = server_opts
402 .bitcoind_username
403 .clone()
404 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
405 let (bitcoind_url, bitcoind_password) = server_opts
406 .get_bitcoind_url_and_password()
407 .await
408 .expect("Failed to get bitcoind url");
409 BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
410 .unwrap()
411 .into_dyn()
412 }
413 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
414 (Some(_), Some(esplora_url)) => {
415 let bitcoind_username = server_opts
416 .bitcoind_username
417 .clone()
418 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
419 let (bitcoind_url, bitcoind_password) = server_opts
420 .get_bitcoind_url_and_password()
421 .await
422 .expect("Failed to get bitcoind url");
423 BitcoindClientWithFallback::new(
424 bitcoind_username,
425 bitcoind_password,
426 &bitcoind_url,
427 esplora_url,
428 )
429 .unwrap()
430 .into_dyn()
431 }
432 _ => unreachable!("ArgGroup already enforced XOR relation"),
433 };
434 let dyn_server_bitcoin_rpc =
435 ServerBitcoinRpcTracked::new(dyn_server_bitcoin_rpc, "server").into_dyn();
436
437 root_task_group.install_kill_handler();
438
439 install_crypto_provider().await;
440
441 let password_file = std::fs::read_to_string(server_opts.data_dir.join(PLAINTEXT_PASSWORD))
448 .ok()
449 .map(|s| s.trim().to_owned());
450
451 let auth_ui = server_opts.password_ui.or(password_file).map(ApiAuth::new);
452 let auth_api = server_opts.password_api.map(ApiAuth::new);
453
454 let task_group = root_task_group.clone();
455 let code_version_hash = code_version_hash.to_string();
456 root_task_group.spawn_cancellable("main", async move {
457 fedimint_server::run(
458 server_opts.data_dir,
459 auth_ui,
460 auth_api,
461 server_opts.force_api_secrets,
462 settings,
463 db,
464 code_version_str,
465 code_version_hash,
466 module_init_registry,
467 task_group,
468 dyn_server_bitcoin_rpc,
469 Box::new(fedimint_server_ui::setup::router),
470 Box::new(fedimint_server_ui::dashboard::router),
471 server_opts.db_checkpoint_retention,
472 Duration::from_secs(server_opts.session_timeout_secs),
473 fedimint_server::ConnectionLimits::new(
474 server_opts.iroh_api_max_connections,
475 server_opts.iroh_api_max_requests_per_connection,
476 ),
477 )
478 .await
479 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
480 });
481
482 let shutdown_future = root_task_group
483 .make_handle()
484 .make_shutdown_rx()
485 .then(|()| async {
486 info!(target: LOG_CORE, "Shutdown called");
487 });
488
489 shutdown_future.await;
490
491 debug!(target: LOG_CORE, "Terminating main task");
492
493 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
494 error!(target: LOG_CORE, err = %err.fmt_compact_anyhow(), "Error while shutting down task group");
495 }
496
497 debug!(target: LOG_CORE, "Shutdown complete");
498
499 fedimint_logging::shutdown();
500
501 drop(timing_total_runtime);
502
503 std::process::exit(-1);
504}
505
506pub fn default_modules() -> ServerModuleInitRegistry {
507 let mut server_gens = ServerModuleInitRegistry::new();
508
509 server_gens.attach(MintInit);
510 server_gens.attach(fedimint_mintv2_server::MintInit);
511
512 server_gens.attach(WalletInit);
513 server_gens.attach(fedimint_walletv2_server::WalletInit);
514
515 server_gens.attach(LightningInit);
516 server_gens.attach(fedimint_lnv2_server::LightningInit);
517
518 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
519 server_gens.attach(MetaInit);
520 }
521
522 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
523 server_gens.attach(UnknownInit);
524 }
525
526 server_gens
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 fn parse_server_opts_with_enable_iroh_env(value: &str) -> ServerOpts {
534 let previous = std::env::var_os(FM_ENABLE_IROH_ENV);
535 unsafe {
538 std::env::set_var(FM_ENABLE_IROH_ENV, value);
539 }
540
541 let opts = ServerOpts::try_parse_from([
542 "fedimintd",
543 "--data-dir",
544 "/tmp/fedimintd-test",
545 "--bitcoind-url",
546 "http://127.0.0.1:18443",
547 "--bitcoind-username",
548 "user",
549 "--bitcoind-password",
550 "pass",
551 ]);
552
553 unsafe {
556 if let Some(previous) = previous {
557 std::env::set_var(FM_ENABLE_IROH_ENV, previous);
558 } else {
559 std::env::remove_var(FM_ENABLE_IROH_ENV);
560 }
561 }
562
563 opts.expect("server opts should parse")
564 }
565
566 #[test]
567 fn enable_iroh_env_accepts_numeric_booleans() {
568 assert_eq!(
569 parse_server_opts_with_enable_iroh_env("1").enable_iroh,
570 Some(true)
571 );
572 assert_eq!(
573 parse_server_opts_with_enable_iroh_env("0").enable_iroh,
574 Some(false)
575 );
576 }
577}