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