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::{ArgGroup, CommandFactory, FromArgMatches, Parser};
21use fedimint_core::db::Database;
22use fedimint_core::envs::{
23 FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
24};
25use fedimint_core::module::CORE_CONSENSUS_VERSION;
26use fedimint_core::module::registry::ModuleRegistry;
27use fedimint_core::rustls::install_crypto_provider;
28use fedimint_core::task::TaskGroup;
29use fedimint_core::timing;
30use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
31use fedimint_ln_server::LightningInit;
32use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
33use fedimint_meta_server::MetaInit;
34use fedimint_mint_server::MintInit;
35use fedimint_rocksdb::RocksDb;
36use fedimint_server::config::ConfigGenSettings;
37use fedimint_server::config::io::DB_FILE;
38use fedimint_server::core::ServerModuleInitRegistry;
39use fedimint_server::net::api::ApiSecrets;
40use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
41use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
42use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
43use fedimint_server_bitcoin_rpc::tracked::ServerBitcoinRpcTracked;
44use fedimint_server_core::ServerModuleInitRegistryExt;
45use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
46use fedimint_unknown_server::UnknownInit;
47use fedimint_wallet_server::WalletInit;
48use fedimintd_envs::{
49 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
50 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
51 FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
52 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
53 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
54 FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, FM_P2P_URL_ENV,
55 FM_SESSION_TIMEOUT_SECS_ENV,
56};
57use futures::FutureExt as _;
58#[cfg(all(
59 not(feature = "jemalloc"),
60 not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
61))]
62use tracing::warn;
63use tracing::{debug, error, info};
64
65use crate::metrics::APP_START_TS;
66
67const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
69
70#[derive(Parser)]
71#[command(version)]
72#[command(
73 group(
74 ArgGroup::new("bitcoind_password_auth")
75 .args(["bitcoind_password", "bitcoind_url_password_file"])
76 .multiple(false)
77 ),
78 group(
79 ArgGroup::new("bitcoind_auth")
80 .args(["bitcoind_url"])
81 .requires("bitcoind_password_auth")
82 .requires_all(["bitcoind_username", "bitcoind_url"])
83 ),
84 group(
85 ArgGroup::new("bitcoin_rpc")
86 .required(true)
87 .multiple(true)
88 .args(["bitcoind_url", "esplora_url"])
89 )
90)]
91struct ServerOpts {
92 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
94 data_dir: PathBuf,
95
96 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
98 bitcoin_network: Network,
99
100 #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
102 bitcoind_username: Option<String>,
103
104 #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
106 bitcoind_password: Option<String>,
107
108 #[arg(long, env = FM_BITCOIND_URL_ENV)]
112 bitcoind_url: Option<SafeUrl>,
113
114 #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
122 bitcoind_url_password_file: Option<PathBuf>,
123
124 #[arg(long, env = FM_ESPLORA_URL_ENV)]
126 esplora_url: Option<SafeUrl>,
127
128 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
133 bind_p2p: SocketAddr,
134
135 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
140 bind_api: SocketAddr,
141
142 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
148 bind_ui: SocketAddr,
149
150 #[arg(long, env = FM_P2P_URL_ENV)]
156 p2p_url: Option<SafeUrl>,
157
158 #[arg(long, env = FM_API_URL_ENV)]
164 api_url: Option<SafeUrl>,
165
166 #[arg(long, env = FM_ENABLE_IROH_ENV)]
167 enable_iroh: bool,
168
169 #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
171 iroh_dns: Option<SafeUrl>,
172
173 #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh", value_delimiter = ',')]
175 iroh_relays: Vec<SafeUrl>,
176
177 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
179 db_checkpoint_retention: u64,
180
181 #[arg(long, env = FM_SESSION_TIMEOUT_SECS_ENV, default_value = "3600")]
185 session_timeout_secs: u64,
186
187 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
189 bind_tokio_console: Option<SocketAddr>,
190
191 #[arg(long, default_value = "false")]
193 with_jaeger: bool,
194
195 #[arg(long, env = FM_BIND_METRICS_ENV, default_value = "127.0.0.1:8176")]
197 bind_metrics: Option<SocketAddr>,
198
199 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
214 force_api_secrets: ApiSecrets,
215
216 #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
218 iroh_api_max_connections: usize,
219
220 #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
222 iroh_api_max_requests_per_connection: usize,
223}
224
225impl ServerOpts {
226 pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
227 let url = self
228 .bitcoind_url
229 .clone()
230 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
231 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
232 let password = tokio::fs::read_to_string(password_file)
233 .await
234 .context("Failed to read the password")?
235 .trim()
236 .to_owned();
237 Ok((url, password))
238 } else {
239 let password = self
240 .bitcoind_password
241 .clone()
242 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
243 Ok((url, password))
244 }
245 }
246}
247
248#[allow(clippy::too_many_lines)]
265pub async fn run(
266 module_init_registry: ServerModuleInitRegistry,
267 code_version_hash: &str,
268 code_version_vendor_suffix: Option<&str>,
269) -> anyhow::Result<Infallible> {
270 assert_eq!(
271 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
272 code_version_hash.len(),
273 "version_hash must have an expected length"
274 );
275
276 handle_version_hash_command(code_version_hash);
277
278 let fedimint_version = env!("CARGO_PKG_VERSION");
279
280 APP_START_TS
281 .with_label_values(&[fedimint_version, code_version_hash])
282 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
283
284 let server_opts = {
285 let mut module_env_help = String::from("\nModule environment variables:\n");
288 for (_kind, module_init) in module_init_registry.iter() {
289 for doc in module_init.get_documented_env_vars() {
290 let _ = writeln!(module_env_help, " {:40} {}", doc.name, doc.description);
291 }
292 }
293 let matches = ServerOpts::command()
294 .after_long_help(module_env_help)
295 .get_matches();
296 ServerOpts::from_arg_matches(&matches)
297 .expect("clap arg matches must be valid after parsing")
298 };
299
300 let mut tracing_builder = TracingSetup::default();
301
302 tracing_builder
303 .tokio_console_bind(server_opts.bind_tokio_console)
304 .with_jaeger(server_opts.with_jaeger);
305
306 tracing_builder.init().unwrap();
307
308 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
309
310 #[cfg(all(
311 not(feature = "jemalloc"),
312 not(any(target_env = "msvc", target_os = "ios", target_os = "android"))
313 ))]
314 warn!(
315 target: LOG_SERVER,
316 "fedimintd was built without the `jemalloc` feature. rocksdb is prone to memory \
317 fragmentation with the default allocator; consider rebuilding with `--features jemalloc`."
318 );
319
320 debug!(
321 target: LOG_SERVER,
322 core_consensus = %CORE_CONSENSUS_VERSION,
323 "Supported core consensus version",
324 );
325 for (kind, module) in module_init_registry.iter() {
326 let supported = module.supported_api_versions();
327 debug!(
328 target: LOG_SERVER,
329 module = %kind,
330 supported = %supported,
331 "Supported module versions",
332 );
333 }
334 let code_version_str = code_version_vendor_suffix.map_or_else(
335 || fedimint_version.to_string(),
336 |suffix| format!("{fedimint_version}+{suffix}"),
337 );
338
339 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
340
341 let root_task_group = TaskGroup::new();
342
343 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
344 info!(
345 target: LOG_SERVER,
346 url = %format!("http://{}/metrics", bind_metrics),
347 "Initializing metrics server",
348 );
349 fedimint_metrics::spawn_api_server(*bind_metrics, root_task_group.clone()).await?;
350 }
351
352 let settings = ConfigGenSettings {
353 p2p_bind: server_opts.bind_p2p,
354 api_bind: server_opts.bind_api,
355 ui_bind: server_opts.bind_ui,
356 p2p_url: server_opts.p2p_url.clone(),
357 api_url: server_opts.api_url.clone(),
358 enable_iroh: server_opts.enable_iroh,
359 iroh_dns: server_opts.iroh_dns.clone(),
360 iroh_relays: server_opts.iroh_relays.clone(),
361 network: server_opts.bitcoin_network,
362 available_modules: module_init_registry.kinds(),
363 default_modules: module_init_registry.default_modules(),
364 };
365
366 let db = Database::new(
367 RocksDb::build(server_opts.data_dir.join(DB_FILE))
368 .open()
369 .await
370 .unwrap(),
371 ModuleRegistry::default(),
372 );
373
374 let dyn_server_bitcoin_rpc = match (
375 server_opts.bitcoind_url.as_ref(),
376 server_opts.esplora_url.as_ref(),
377 ) {
378 (Some(_), None) => {
379 let bitcoind_username = server_opts
380 .bitcoind_username
381 .clone()
382 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
383 let (bitcoind_url, bitcoind_password) = server_opts
384 .get_bitcoind_url_and_password()
385 .await
386 .expect("Failed to get bitcoind url");
387 BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
388 .unwrap()
389 .into_dyn()
390 }
391 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
392 (Some(_), Some(esplora_url)) => {
393 let bitcoind_username = server_opts
394 .bitcoind_username
395 .clone()
396 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
397 let (bitcoind_url, bitcoind_password) = server_opts
398 .get_bitcoind_url_and_password()
399 .await
400 .expect("Failed to get bitcoind url");
401 BitcoindClientWithFallback::new(
402 bitcoind_username,
403 bitcoind_password,
404 &bitcoind_url,
405 esplora_url,
406 )
407 .unwrap()
408 .into_dyn()
409 }
410 _ => unreachable!("ArgGroup already enforced XOR relation"),
411 };
412 let dyn_server_bitcoin_rpc =
413 ServerBitcoinRpcTracked::new(dyn_server_bitcoin_rpc, "server").into_dyn();
414
415 root_task_group.install_kill_handler();
416
417 install_crypto_provider().await;
418
419 let task_group = root_task_group.clone();
420 let code_version_hash = code_version_hash.to_string();
421 root_task_group.spawn_cancellable("main", async move {
422 fedimint_server::run(
423 server_opts.data_dir,
424 server_opts.force_api_secrets,
425 settings,
426 db,
427 code_version_str,
428 code_version_hash,
429 module_init_registry,
430 task_group,
431 dyn_server_bitcoin_rpc,
432 Box::new(fedimint_server_ui::setup::router),
433 Box::new(fedimint_server_ui::dashboard::router),
434 server_opts.db_checkpoint_retention,
435 Duration::from_secs(server_opts.session_timeout_secs),
436 fedimint_server::ConnectionLimits::new(
437 server_opts.iroh_api_max_connections,
438 server_opts.iroh_api_max_requests_per_connection,
439 ),
440 )
441 .await
442 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
443 });
444
445 let shutdown_future = root_task_group
446 .make_handle()
447 .make_shutdown_rx()
448 .then(|()| async {
449 info!(target: LOG_CORE, "Shutdown called");
450 });
451
452 shutdown_future.await;
453
454 debug!(target: LOG_CORE, "Terminating main task");
455
456 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
457 error!(target: LOG_CORE, err = %err.fmt_compact_anyhow(), "Error while shutting down task group");
458 }
459
460 debug!(target: LOG_CORE, "Shutdown complete");
461
462 fedimint_logging::shutdown();
463
464 drop(timing_total_runtime);
465
466 std::process::exit(-1);
467}
468
469pub fn default_modules() -> ServerModuleInitRegistry {
470 let mut server_gens = ServerModuleInitRegistry::new();
471
472 server_gens.attach(MintInit);
473 server_gens.attach(fedimint_mintv2_server::MintInit);
474
475 server_gens.attach(WalletInit);
476 server_gens.attach(fedimint_walletv2_server::WalletInit);
477
478 server_gens.attach(LightningInit);
479 server_gens.attach(fedimint_lnv2_server::LightningInit);
480
481 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
482 server_gens.attach(MetaInit);
483 }
484
485 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
486 server_gens.attach(UnknownInit);
487 }
488
489 server_gens
490}