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::env;
12use std::net::SocketAddr;
13use std::path::PathBuf;
14use std::time::Duration;
15
16use anyhow::Context as _;
17use bitcoin::Network;
18use clap::{ArgGroup, Parser};
19use fedimint_core::config::{EmptyGenParams, ServerModuleConfigGenParamsRegistry};
20use fedimint_core::db::Database;
21use fedimint_core::envs::{
22 BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV,
23 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::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
29use fedimint_core::{default_esplora_server, timing};
30use fedimint_ln_common::config::{
31 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
32};
33use fedimint_ln_server::LightningInit;
34use fedimint_logging::{LOG_CORE, TracingSetup};
35use fedimint_meta_server::{MetaGenParams, MetaInit};
36use fedimint_mint_server::MintInit;
37use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
38use fedimint_rocksdb::RocksDb;
39use fedimint_server::config::ConfigGenSettings;
40use fedimint_server::config::io::DB_FILE;
41use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
42use fedimint_server::net::api::ApiSecrets;
43use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
44use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
45use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
46use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
47use fedimint_unknown_common::config::UnknownGenParams;
48use fedimint_unknown_server::UnknownInit;
49use fedimint_wallet_server::WalletInit;
50use fedimint_wallet_server::common::config::{
51 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
52};
53use fedimintd_envs::{
54 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRCIS_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, FM_P2P_URL_ENV,
60 FM_PORT_ESPLORA_ENV,
61};
62use futures::FutureExt as _;
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")]
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_BIND_TOKIO_CONSOLE_ENV)]
183 bind_tokio_console: Option<SocketAddr>,
184
185 #[arg(long, default_value = "false")]
187 with_jaeger: bool,
188
189 #[arg(long, env = FM_BIND_METRCIS_ENV)]
191 bind_metrics: Option<SocketAddr>,
192
193 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
208 force_api_secrets: ApiSecrets,
209
210 #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
212 iroh_api_max_connections: usize,
213
214 #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
216 iroh_api_max_requests_per_connection: usize,
217}
218
219impl ServerOpts {
220 pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
221 let url = self
222 .bitcoind_url
223 .clone()
224 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
225 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
226 let password = tokio::fs::read_to_string(password_file)
227 .await
228 .context("Failed to read the password")?
229 .trim()
230 .to_owned();
231 Ok((url, password))
232 } else {
233 let password = self
234 .bitcoind_password
235 .clone()
236 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
237 Ok((url, password))
238 }
239 }
240}
241
242#[allow(clippy::too_many_lines)]
259pub async fn run(
260 modules_fn: fn(
261 Network,
262 ) -> (
263 ServerModuleInitRegistry,
264 ServerModuleConfigGenParamsRegistry,
265 ),
266 code_version_hash: &str,
267 code_version_vendor_suffix: Option<&str>,
268) -> ! {
269 assert_eq!(
270 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
271 code_version_hash.len(),
272 "version_hash must have an expected length"
273 );
274
275 handle_version_hash_command(code_version_hash);
276
277 let fedimint_version = env!("CARGO_PKG_VERSION");
278
279 APP_START_TS
280 .with_label_values(&[fedimint_version, code_version_hash])
281 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
282
283 let server_opts = ServerOpts::parse();
284
285 let mut tracing_builder = TracingSetup::default();
286
287 tracing_builder
288 .tokio_console_bind(server_opts.bind_tokio_console)
289 .with_jaeger(server_opts.with_jaeger);
290
291 tracing_builder.init().unwrap();
292
293 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
294
295 let code_version_str = code_version_vendor_suffix.map_or_else(
296 || fedimint_version.to_string(),
297 |suffix| format!("{fedimint_version}+{suffix}"),
298 );
299
300 let (server_gens, server_gen_params) = modules_fn(server_opts.bitcoin_network);
301
302 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
303
304 let root_task_group = TaskGroup::new();
305
306 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
307 root_task_group.spawn_cancellable(
308 "metrics-server",
309 fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
310 );
311 }
312
313 let settings = ConfigGenSettings {
314 p2p_bind: server_opts.bind_p2p,
315 api_bind: server_opts.bind_api,
316 ui_bind: server_opts.bind_ui,
317 p2p_url: server_opts.p2p_url.clone(),
318 api_url: server_opts.api_url.clone(),
319 enable_iroh: server_opts.enable_iroh,
320 iroh_dns: server_opts.iroh_dns.clone(),
321 iroh_relays: server_opts.iroh_relays.clone(),
322 modules: server_gen_params.clone(),
323 registry: server_gens.clone(),
324 };
325
326 let db = Database::new(
327 RocksDb::open(server_opts.data_dir.join(DB_FILE))
328 .await
329 .unwrap(),
330 ModuleRegistry::default(),
331 );
332
333 let dyn_server_bitcoin_rpc = match (
334 server_opts.bitcoind_url.as_ref(),
335 server_opts.esplora_url.as_ref(),
336 ) {
337 (Some(_), None) => {
338 let bitcoind_username = server_opts
339 .bitcoind_username
340 .clone()
341 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
342 let (bitcoind_url, bitcoind_password) = server_opts
343 .get_bitcoind_url_and_password()
344 .await
345 .expect("Failed to get bitcoind url");
346 BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
347 .unwrap()
348 .into_dyn()
349 }
350 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
351 (Some(_), Some(esplora_url)) => {
352 let bitcoind_username = server_opts
353 .bitcoind_username
354 .clone()
355 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
356 let (bitcoind_url, bitcoind_password) = server_opts
357 .get_bitcoind_url_and_password()
358 .await
359 .expect("Failed to get bitcoind url");
360 BitcoindClientWithFallback::new(
361 bitcoind_username,
362 bitcoind_password,
363 &bitcoind_url,
364 esplora_url,
365 )
366 .unwrap()
367 .into_dyn()
368 }
369 _ => unreachable!("ArgGroup already enforced XOR relation"),
370 };
371
372 root_task_group.install_kill_handler();
373
374 install_crypto_provider().await;
375
376 let task_group = root_task_group.clone();
377 root_task_group.spawn_cancellable("main", async move {
378 fedimint_server::run(
379 server_opts.data_dir,
380 server_opts.force_api_secrets,
381 settings,
382 db,
383 code_version_str,
384 server_gens,
385 task_group,
386 dyn_server_bitcoin_rpc,
387 Box::new(fedimint_server_ui::setup::router),
388 Box::new(fedimint_server_ui::dashboard::router),
389 server_opts.db_checkpoint_retention,
390 fedimint_server::ConnectionLimits::new(
391 server_opts.iroh_api_max_connections,
392 server_opts.iroh_api_max_requests_per_connection,
393 ),
394 )
395 .await
396 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
397 });
398
399 let shutdown_future = root_task_group
400 .make_handle()
401 .make_shutdown_rx()
402 .then(|()| async {
403 info!(target: LOG_CORE, "Shutdown called");
404 });
405
406 shutdown_future.await;
407 debug!(target: LOG_CORE, "Terminating main task");
408
409 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
410 error!(target: LOG_CORE, err = %err.fmt_compact_anyhow(), "Error while shutting down task group");
411 }
412
413 debug!(target: LOG_CORE, "Shutdown complete");
414
415 fedimint_logging::shutdown();
416
417 drop(timing_total_runtime);
418
419 std::process::exit(-1);
421}
422
423pub fn default_modules(
424 network: Network,
425) -> (
426 ServerModuleInitRegistry,
427 ServerModuleConfigGenParamsRegistry,
428) {
429 let mut server_gens = ServerModuleInitRegistry::new();
430 let mut server_gen_params = ServerModuleConfigGenParamsRegistry::default();
431
432 let bitcoin_rpc_config = BitcoinRpcConfig {
433 kind: "bitcoind".to_string(),
434 url: "http://unused_dummy.xyz".parse().unwrap(),
435 };
436
437 server_gens.attach(LightningInit);
438 server_gen_params.attach_config_gen_params(
439 LightningInit::kind(),
440 LightningGenParams {
441 local: LightningGenParamsLocal {
442 bitcoin_rpc: bitcoin_rpc_config.clone(),
443 },
444 consensus: LightningGenParamsConsensus { network },
445 },
446 );
447
448 server_gens.attach(MintInit);
449 server_gen_params.attach_config_gen_params(
450 MintInit::kind(),
451 MintGenParams {
452 local: EmptyGenParams::default(),
453 consensus: MintGenParamsConsensus::new(2, None),
454 },
455 );
456
457 server_gens.attach(WalletInit);
458 server_gen_params.attach_config_gen_params(
459 WalletInit::kind(),
460 WalletGenParams {
461 local: WalletGenParamsLocal {
462 bitcoin_rpc: bitcoin_rpc_config.clone(),
463 },
464 consensus: WalletGenParamsConsensus {
465 network,
466 finality_delay: default_finality_delay(network),
467 client_default_bitcoin_rpc: default_esplora_server(
468 network,
469 std::env::var(FM_PORT_ESPLORA_ENV).ok(),
470 ),
471 fee_consensus: fedimint_wallet_server::common::config::FeeConsensus::default(),
472 },
473 },
474 );
475
476 let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
477 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
478
479 if enable_lnv2 {
480 server_gens.attach(fedimint_lnv2_server::LightningInit);
481 server_gen_params.attach_config_gen_params(
482 fedimint_lnv2_server::LightningInit::kind(),
483 fedimint_lnv2_common::config::LightningGenParams {
484 local: fedimint_lnv2_common::config::LightningGenParamsLocal {
485 bitcoin_rpc: bitcoin_rpc_config.clone(),
486 },
487 consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
488 fee_consensus: None,
489 network,
490 },
491 },
492 );
493 }
494
495 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
496 server_gens.attach(MetaInit);
497 server_gen_params.attach_config_gen_params(MetaInit::kind(), MetaGenParams::default());
498 }
499
500 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
501 server_gens.attach(UnknownInit);
502 server_gen_params
503 .attach_config_gen_params(UnknownInit::kind(), UnknownGenParams::default());
504 }
505
506 (server_gens, server_gen_params)
507}
508
509fn default_finality_delay(network: Network) -> u32 {
512 match network {
513 Network::Bitcoin | Network::Regtest => 10,
514 Network::Testnet | Network::Signet | Network::Testnet4 => 2,
515 _ => panic!("Unsupported network"),
516 }
517}