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