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::sync::Arc;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::time::Duration;
19
20use anyhow::Context as _;
21use bitcoin::Network;
22use clap::builder::BoolishValueParser;
23use clap::{ArgGroup, CommandFactory, FromArgMatches, Parser};
24use fedimint_core::db::Database;
25use fedimint_core::envs::{
26 FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
27 is_running_in_test_env,
28};
29use fedimint_core::module::registry::ModuleRegistry;
30use fedimint_core::module::{ApiAuth, CORE_CONSENSUS_VERSION};
31use fedimint_core::rustls::install_crypto_provider;
32use fedimint_core::task::TaskGroup;
33use fedimint_core::timing;
34use fedimint_core::util::{FmtCompact as _, SafeUrl, handle_version_hash_command};
35use fedimint_ln_server::LightningInit;
36use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
37use fedimint_meta_server::MetaInit;
38use fedimint_mint_server::MintInit;
39use fedimint_rocksdb::RocksDb;
40use fedimint_server::config::ConfigGenSettings;
41use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
42use fedimint_server::core::ServerModuleInitRegistry;
43use fedimint_server::net::api::ApiSecrets;
44use fedimint_server::{ConsensusStop, IrohNextApiSettings, ShutdownRequest};
45use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
46use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
47use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
48use fedimint_server_bitcoin_rpc::tracked::ServerBitcoinRpcTracked;
49use fedimint_server_core::ServerModuleInitRegistryExt;
50use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
51use fedimint_unknown_server::UnknownInit;
52use fedimint_wallet_server::WalletInit;
53use fedimintd_envs::{
54 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_API_NEXT_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
55 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
56 FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
57 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
58 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
59 FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV,
60 FM_IROH_NEXT_ENABLE_ENV, FM_IROH_P2P_RELAY_ENV, FM_P2P_MAX_CONNECTION_AGE_SECS_ENV,
61 FM_P2P_URL_ENV, FM_PASSWORD_API_ENV, FM_PASSWORD_UI_ENV, FM_SESSION_TIMEOUT_SECS_ENV,
62};
63use futures::FutureExt as _;
64#[cfg(all(
65 not(feature = "jemalloc"),
66 not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
67))]
68use tracing::warn;
69use tracing::{debug, error, info};
70
71use crate::metrics::APP_START_TS;
72
73const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
75
76pub const SESSION_BOUNDARY_EXIT_CODE: i32 = 3;
88
89#[derive(Parser)]
90#[command(version)]
91#[command(
92 group(
93 ArgGroup::new("bitcoind_password_auth")
94 .args(["bitcoind_password", "bitcoind_url_password_file"])
95 .multiple(false)
96 ),
97 group(
98 ArgGroup::new("bitcoind_auth")
99 .args(["bitcoind_url"])
100 .requires("bitcoind_password_auth")
101 .requires_all(["bitcoind_username", "bitcoind_url"])
102 ),
103 group(
104 ArgGroup::new("bitcoin_rpc")
105 .required(true)
106 .multiple(true)
107 .args(["bitcoind_url", "esplora_url"])
108 )
109)]
110struct ServerOpts {
111 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
113 data_dir: PathBuf,
114
115 #[arg(long, env = FM_PASSWORD_UI_ENV)]
120 password_ui: Option<String>,
121
122 #[arg(long, env = FM_PASSWORD_API_ENV)]
127 password_api: Option<String>,
128
129 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
131 bitcoin_network: Network,
132
133 #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
135 bitcoind_username: Option<String>,
136
137 #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
139 bitcoind_password: Option<String>,
140
141 #[arg(long, env = FM_BITCOIND_URL_ENV)]
149 bitcoind_url: Option<SafeUrl>,
150
151 #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
159 bitcoind_url_password_file: Option<PathBuf>,
160
161 #[arg(long, env = FM_ESPLORA_URL_ENV)]
172 esplora_url: Option<SafeUrl>,
173
174 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
179 bind_p2p: SocketAddr,
180
181 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
186 bind_api: SocketAddr,
187
188 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
194 bind_ui: SocketAddr,
195
196 #[arg(long, env = FM_P2P_URL_ENV)]
202 p2p_url: Option<SafeUrl>,
203
204 #[arg(long, env = FM_API_URL_ENV)]
210 api_url: Option<SafeUrl>,
211
212 #[arg(long, env = FM_ENABLE_IROH_ENV, value_parser = BoolishValueParser::new())]
219 enable_iroh: Option<bool>,
220
221 #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
223 iroh_dns: Option<SafeUrl>,
224
225 #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh", value_delimiter = ',')]
227 iroh_relays: Vec<SafeUrl>,
228
229 #[arg(long, env = FM_IROH_P2P_RELAY_ENV, value_delimiter = ',')]
231 iroh_p2p_relays: Vec<SafeUrl>,
232
233 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
235 db_checkpoint_retention: u64,
236
237 #[arg(long, env = FM_SESSION_TIMEOUT_SECS_ENV, default_value = "3600")]
241 session_timeout_secs: u64,
242
243 #[arg(long, env = FM_P2P_MAX_CONNECTION_AGE_SECS_ENV)]
247 p2p_max_connection_age_secs: Option<u64>,
248
249 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
251 bind_tokio_console: Option<SocketAddr>,
252
253 #[arg(long, default_value = "false")]
255 with_jaeger: bool,
256
257 #[arg(long, env = FM_BIND_METRICS_ENV, default_value = "127.0.0.1:8176")]
259 bind_metrics: Option<SocketAddr>,
260
261 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
276 force_api_secrets: ApiSecrets,
277
278 #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
280 iroh_api_max_connections: usize,
281
282 #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
284 iroh_api_max_requests_per_connection: usize,
285
286 #[arg(
293 long,
294 env = FM_IROH_NEXT_ENABLE_ENV,
295 default_value_t = true,
296 action = clap::ArgAction::Set,
297 num_args = 0..=1,
298 default_missing_value = "true",
299 )]
300 enable_iroh_next: bool,
301
302 #[arg(long, env = FM_BIND_API_NEXT_ENV)]
304 bind_api_next: Option<SocketAddr>,
305}
306
307impl ServerOpts {
308 pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
309 let url = self
310 .bitcoind_url
311 .clone()
312 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
313 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
314 let password = tokio::fs::read_to_string(password_file)
315 .await
316 .context("Failed to read the password")?
317 .trim()
318 .to_owned();
319 Ok((url, password))
320 } else {
321 let password = self
322 .bitcoind_password
323 .clone()
324 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
325 Ok((url, password))
326 }
327 }
328}
329
330#[allow(clippy::too_many_lines)]
349pub async fn run(
350 module_init_registry: ServerModuleInitRegistry,
351 code_version_hash: &str,
352 code_version_vendor_suffix: Option<&str>,
353) -> anyhow::Result<Infallible> {
354 assert_eq!(
355 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
356 code_version_hash.len(),
357 "version_hash must have an expected length"
358 );
359
360 handle_version_hash_command(code_version_hash);
361
362 let fedimint_version = env!("CARGO_PKG_VERSION");
363
364 APP_START_TS
365 .with_label_values(&[fedimint_version, code_version_hash])
366 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
367
368 let server_opts = {
369 let mut module_env_help = String::from("\nModule environment variables:\n");
372 for (_kind, module_init) in module_init_registry.iter() {
373 for doc in module_init.get_documented_env_vars() {
374 let _ = writeln!(module_env_help, " {:40} {}", doc.name, doc.description);
375 }
376 }
377 let matches = ServerOpts::command()
378 .after_long_help(module_env_help)
379 .get_matches();
380 ServerOpts::from_arg_matches(&matches)
381 .expect("clap arg matches must be valid after parsing")
382 };
383
384 let mut tracing_builder = TracingSetup::default();
385
386 tracing_builder
387 .tokio_console_bind(server_opts.bind_tokio_console)
388 .with_jaeger(server_opts.with_jaeger);
389
390 tracing_builder.init().map_err(anyhow::Error::from).unwrap();
391
392 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
393
394 #[cfg(all(
395 not(feature = "jemalloc"),
396 not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
397 ))]
398 warn!(
399 target: LOG_SERVER,
400 "fedimintd was built without the `jemalloc` feature. rocksdb is prone to memory \
401 fragmentation with the default allocator; consider rebuilding with `--features jemalloc`."
402 );
403
404 debug!(
405 target: LOG_SERVER,
406 core_consensus = %CORE_CONSENSUS_VERSION,
407 "Supported core consensus version",
408 );
409
410 let code_version_str = code_version_vendor_suffix.map_or_else(
411 || fedimint_version.to_string(),
412 |suffix| format!("{fedimint_version}+{suffix}"),
413 );
414 fedimint_core::version::DkgVersion::parse(&code_version_str)
415 .context("Invalid Fedimint version vendor string")?;
416
417 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
418
419 let root_task_group = TaskGroup::new();
420
421 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
422 info!(
423 target: LOG_SERVER,
424 url = %format!("http://{}/metrics", bind_metrics),
425 "Initializing metrics server",
426 );
427 fedimint_metrics::spawn_api_server(*bind_metrics, root_task_group.clone()).await?;
428 }
429
430 let enable_iroh = server_opts.enable_iroh.unwrap_or(!is_running_in_test_env());
431 let iroh_next_api_settings = if server_opts.enable_iroh_next {
432 Some(IrohNextApiSettings::new(server_opts.bind_api_next))
433 } else {
434 None
435 };
436
437 let settings = ConfigGenSettings {
438 p2p_bind: server_opts.bind_p2p,
439 api_bind: server_opts.bind_api,
440 ui_bind: server_opts.bind_ui,
441 p2p_url: server_opts.p2p_url.clone(),
442 api_url: server_opts.api_url.clone(),
443 enable_iroh,
444 iroh_dns: server_opts.iroh_dns.clone(),
445 iroh_relays: server_opts.iroh_relays.clone(),
446 network: server_opts.bitcoin_network,
447 available_modules: module_init_registry.kinds(),
448 default_modules: module_init_registry.default_modules(),
449 };
450
451 let db = Database::new(
452 RocksDb::build(server_opts.data_dir.join(DB_FILE))
453 .open()
454 .await
455 .map_err(anyhow::Error::from)
456 .unwrap(),
457 ModuleRegistry::default(),
458 );
459
460 let dyn_server_bitcoin_rpc = match (
461 server_opts.bitcoind_url.as_ref(),
462 server_opts.esplora_url.as_ref(),
463 ) {
464 (Some(_), None) => {
465 let bitcoind_username = server_opts
466 .bitcoind_username
467 .clone()
468 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
469 let (bitcoind_url, bitcoind_password) = server_opts
470 .get_bitcoind_url_and_password()
471 .await
472 .expect("Failed to get bitcoind url");
473 BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
474 .unwrap()
475 .into_dyn()
476 }
477 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
478 (Some(_), Some(esplora_url)) => {
479 let bitcoind_username = server_opts
480 .bitcoind_username
481 .clone()
482 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
483 let (bitcoind_url, bitcoind_password) = server_opts
484 .get_bitcoind_url_and_password()
485 .await
486 .expect("Failed to get bitcoind url");
487 BitcoindClientWithFallback::new(
488 bitcoind_username,
489 bitcoind_password,
490 &bitcoind_url,
491 esplora_url,
492 )
493 .await
494 .unwrap()
495 .into_dyn()
496 }
497 _ => unreachable!("ArgGroup already enforced at least one Bitcoin backend"),
498 };
499 let dyn_server_bitcoin_rpc =
500 ServerBitcoinRpcTracked::new(dyn_server_bitcoin_rpc, "server").into_dyn();
501
502 root_task_group.install_kill_handler();
503
504 install_crypto_provider().await;
505
506 let password_file = std::fs::read_to_string(server_opts.data_dir.join(PLAINTEXT_PASSWORD))
513 .ok()
514 .map(|s| s.trim().to_owned());
515
516 let auth_ui = server_opts.password_ui.or(password_file).map(ApiAuth::new);
517 let auth_api = server_opts.password_api.map(ApiAuth::new);
518
519 let task_group = root_task_group.clone();
520 let code_version_hash = code_version_hash.to_string();
521 let stopped_at_session_boundary = Arc::new(AtomicBool::new(false));
522 let main_stopped_at_session_boundary = stopped_at_session_boundary.clone();
523 root_task_group.spawn_cancellable("main", async move {
524 let stop = fedimint_server::run_with_iroh_p2p_relays_and_next_api(
525 server_opts.data_dir,
526 auth_ui,
527 auth_api,
528 server_opts.force_api_secrets,
529 settings,
530 db,
531 code_version_str,
532 code_version_hash,
533 module_init_registry,
534 task_group,
535 dyn_server_bitcoin_rpc,
536 Box::new(fedimint_server_ui::setup::router),
537 Box::new(fedimint_server_ui::dashboard::router),
538 server_opts.db_checkpoint_retention,
539 Duration::from_secs(server_opts.session_timeout_secs),
540 server_opts
541 .p2p_max_connection_age_secs
542 .map(Duration::from_secs),
543 fedimint_server::ConnectionLimits::new(
544 server_opts.iroh_api_max_connections,
545 server_opts.iroh_api_max_requests_per_connection,
546 ),
547 server_opts.iroh_p2p_relays,
548 iroh_next_api_settings,
549 )
550 .await
551 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact()));
552 if stop == ConsensusStop::ScheduledShutdown(ShutdownRequest::AfterCurrentSession) {
553 main_stopped_at_session_boundary.store(true, Ordering::SeqCst);
554 }
555 });
556
557 let shutdown_future = root_task_group
558 .make_handle()
559 .make_shutdown_rx()
560 .then(|()| async {
561 info!(target: LOG_CORE, "Shutdown called");
562 });
563
564 shutdown_future.await;
565
566 debug!(target: LOG_CORE, "Terminating main task");
567
568 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
569 error!(target: LOG_CORE, err = %err.fmt_compact(), "Error while shutting down task group");
570 }
571
572 debug!(target: LOG_CORE, "Shutdown complete");
573
574 fedimint_logging::shutdown();
575
576 drop(timing_total_runtime);
577
578 if stopped_at_session_boundary.load(Ordering::SeqCst) {
579 std::process::exit(SESSION_BOUNDARY_EXIT_CODE);
580 }
581 std::process::exit(-1);
582}
583
584pub fn default_modules() -> ServerModuleInitRegistry {
585 let mut server_gens = ServerModuleInitRegistry::new();
586
587 server_gens.attach(MintInit);
588 server_gens.attach(fedimint_mintv2_server::MintInit);
589
590 server_gens.attach(WalletInit);
591 server_gens.attach(fedimint_walletv2_server::WalletInit);
592
593 server_gens.attach(LightningInit);
594 server_gens.attach(fedimint_lnv2_server::LightningInit);
595
596 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
597 server_gens.attach(MetaInit);
598 }
599
600 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
601 server_gens.attach(UnknownInit);
602 }
603
604 server_gens
605}
606
607#[cfg(test)]
608mod tests;