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