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::db::Database;
20use fedimint_core::envs::{
21 FM_ENABLE_MODULE_LNV2_ENV, FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV, FM_USE_UNKNOWN_MODULE_ENV,
22 is_env_var_set, is_env_var_set_opt,
23};
24use fedimint_core::module::registry::ModuleRegistry;
25use fedimint_core::rustls::install_crypto_provider;
26use fedimint_core::task::TaskGroup;
27use fedimint_core::timing;
28use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
29use fedimint_ln_server::LightningInit;
30use fedimint_logging::{LOG_CORE, TracingSetup};
31use fedimint_meta_server::MetaInit;
32use fedimint_mint_server::MintInit;
33use fedimint_rocksdb::RocksDb;
34use fedimint_server::config::ConfigGenSettings;
35use fedimint_server::config::io::DB_FILE;
36use fedimint_server::core::ServerModuleInitRegistry;
37use fedimint_server::net::api::ApiSecrets;
38use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
39use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
40use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
41use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
42use fedimint_unknown_server::UnknownInit;
43use fedimint_wallet_server::WalletInit;
44use fedimintd_envs::{
45 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRICS_ENV, FM_BIND_P2P_ENV,
46 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_PASSWORD_ENV,
47 FM_BITCOIND_URL_ENV, FM_BITCOIND_URL_PASSWORD_FILE_ENV, FM_BITCOIND_USERNAME_ENV,
48 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
49 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
50 FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, FM_P2P_URL_ENV,
51};
52use futures::FutureExt as _;
53use tracing::{debug, error, info};
54
55use crate::metrics::APP_START_TS;
56
57const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
59
60#[derive(Parser)]
61#[command(version)]
62#[command(
63 group(
64 ArgGroup::new("bitcoind_password_auth")
65 .args(["bitcoind_password", "bitcoind_url_password_file"])
66 .multiple(false)
67 ),
68 group(
69 ArgGroup::new("bitcoind_auth")
70 .args(["bitcoind_url"])
71 .requires("bitcoind_password_auth")
72 .requires_all(["bitcoind_username", "bitcoind_url"])
73 ),
74 group(
75 ArgGroup::new("bitcoin_rpc")
76 .required(true)
77 .multiple(true)
78 .args(["bitcoind_url", "esplora_url"])
79 )
80)]
81struct ServerOpts {
82 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
84 data_dir: PathBuf,
85
86 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
88 bitcoin_network: Network,
89
90 #[arg(long, env = FM_BITCOIND_USERNAME_ENV)]
92 bitcoind_username: Option<String>,
93
94 #[arg(long, env = FM_BITCOIND_PASSWORD_ENV)]
96 bitcoind_password: Option<String>,
97
98 #[arg(long, env = FM_BITCOIND_URL_ENV)]
102 bitcoind_url: Option<SafeUrl>,
103
104 #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
112 bitcoind_url_password_file: Option<PathBuf>,
113
114 #[arg(long, env = FM_ESPLORA_URL_ENV)]
116 esplora_url: Option<SafeUrl>,
117
118 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
123 bind_p2p: SocketAddr,
124
125 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
130 bind_api: SocketAddr,
131
132 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
138 bind_ui: SocketAddr,
139
140 #[arg(long, env = FM_P2P_URL_ENV)]
146 p2p_url: Option<SafeUrl>,
147
148 #[arg(long, env = FM_API_URL_ENV)]
154 api_url: Option<SafeUrl>,
155
156 #[arg(long, env = FM_ENABLE_IROH_ENV)]
157 enable_iroh: bool,
158
159 #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
161 iroh_dns: Option<SafeUrl>,
162
163 #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh")]
165 iroh_relays: Vec<SafeUrl>,
166
167 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
169 db_checkpoint_retention: u64,
170
171 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
173 bind_tokio_console: Option<SocketAddr>,
174
175 #[arg(long, default_value = "false")]
177 with_jaeger: bool,
178
179 #[arg(long, env = FM_BIND_METRICS_ENV)]
181 bind_metrics: Option<SocketAddr>,
182
183 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
198 force_api_secrets: ApiSecrets,
199
200 #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
202 iroh_api_max_connections: usize,
203
204 #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
206 iroh_api_max_requests_per_connection: usize,
207}
208
209impl ServerOpts {
210 pub async fn get_bitcoind_url_and_password(&self) -> anyhow::Result<(SafeUrl, String)> {
211 let url = self
212 .bitcoind_url
213 .clone()
214 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
215 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
216 let password = tokio::fs::read_to_string(password_file)
217 .await
218 .context("Failed to read the password")?
219 .trim()
220 .to_owned();
221 Ok((url, password))
222 } else {
223 let password = self
224 .bitcoind_password
225 .clone()
226 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
227 Ok((url, password))
228 }
229 }
230}
231
232#[allow(clippy::too_many_lines)]
249pub async fn run(
250 module_init_registry: ServerModuleInitRegistry,
251 code_version_hash: &str,
252 code_version_vendor_suffix: Option<&str>,
253) -> ! {
254 assert_eq!(
255 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
256 code_version_hash.len(),
257 "version_hash must have an expected length"
258 );
259
260 handle_version_hash_command(code_version_hash);
261
262 let fedimint_version = env!("CARGO_PKG_VERSION");
263
264 APP_START_TS
265 .with_label_values(&[fedimint_version, code_version_hash])
266 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
267
268 let server_opts = ServerOpts::parse();
269
270 let mut tracing_builder = TracingSetup::default();
271
272 tracing_builder
273 .tokio_console_bind(server_opts.bind_tokio_console)
274 .with_jaeger(server_opts.with_jaeger);
275
276 tracing_builder.init().unwrap();
277
278 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
279
280 let code_version_str = code_version_vendor_suffix.map_or_else(
281 || fedimint_version.to_string(),
282 |suffix| format!("{fedimint_version}+{suffix}"),
283 );
284
285 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
286
287 let root_task_group = TaskGroup::new();
288
289 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
290 root_task_group.spawn_cancellable(
291 "metrics-server",
292 fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
293 );
294 }
295
296 let settings = ConfigGenSettings {
297 p2p_bind: server_opts.bind_p2p,
298 api_bind: server_opts.bind_api,
299 ui_bind: server_opts.bind_ui,
300 p2p_url: server_opts.p2p_url.clone(),
301 api_url: server_opts.api_url.clone(),
302 enable_iroh: server_opts.enable_iroh,
303 iroh_dns: server_opts.iroh_dns.clone(),
304 iroh_relays: server_opts.iroh_relays.clone(),
305 network: server_opts.bitcoin_network,
306 };
307
308 let db = Database::new(
309 RocksDb::open(server_opts.data_dir.join(DB_FILE))
310 .await
311 .unwrap(),
312 ModuleRegistry::default(),
313 );
314
315 let dyn_server_bitcoin_rpc = match (
316 server_opts.bitcoind_url.as_ref(),
317 server_opts.esplora_url.as_ref(),
318 ) {
319 (Some(_), None) => {
320 let bitcoind_username = server_opts
321 .bitcoind_username
322 .clone()
323 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
324 let (bitcoind_url, bitcoind_password) = server_opts
325 .get_bitcoind_url_and_password()
326 .await
327 .expect("Failed to get bitcoind url");
328 BitcoindClient::new(bitcoind_username, bitcoind_password, &bitcoind_url)
329 .unwrap()
330 .into_dyn()
331 }
332 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
333 (Some(_), Some(esplora_url)) => {
334 let bitcoind_username = server_opts
335 .bitcoind_username
336 .clone()
337 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
338 let (bitcoind_url, bitcoind_password) = server_opts
339 .get_bitcoind_url_and_password()
340 .await
341 .expect("Failed to get bitcoind url");
342 BitcoindClientWithFallback::new(
343 bitcoind_username,
344 bitcoind_password,
345 &bitcoind_url,
346 esplora_url,
347 )
348 .unwrap()
349 .into_dyn()
350 }
351 _ => unreachable!("ArgGroup already enforced XOR relation"),
352 };
353
354 root_task_group.install_kill_handler();
355
356 install_crypto_provider().await;
357
358 let task_group = root_task_group.clone();
359 root_task_group.spawn_cancellable("main", async move {
360 fedimint_server::run(
361 server_opts.data_dir,
362 server_opts.force_api_secrets,
363 settings,
364 db,
365 code_version_str,
366 module_init_registry,
367 task_group,
368 dyn_server_bitcoin_rpc,
369 Box::new(fedimint_server_ui::setup::router),
370 Box::new(fedimint_server_ui::dashboard::router),
371 server_opts.db_checkpoint_retention,
372 fedimint_server::ConnectionLimits::new(
373 server_opts.iroh_api_max_connections,
374 server_opts.iroh_api_max_requests_per_connection,
375 ),
376 )
377 .await
378 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
379 });
380
381 let shutdown_future = root_task_group
382 .make_handle()
383 .make_shutdown_rx()
384 .then(|()| async {
385 info!(target: LOG_CORE, "Shutdown called");
386 });
387
388 shutdown_future.await;
389
390 debug!(target: LOG_CORE, "Terminating main task");
391
392 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
393 error!(target: LOG_CORE, err = %err.fmt_compact_anyhow(), "Error while shutting down task group");
394 }
395
396 debug!(target: LOG_CORE, "Shutdown complete");
397
398 fedimint_logging::shutdown();
399
400 drop(timing_total_runtime);
401
402 std::process::exit(-1);
404}
405
406pub fn default_modules() -> ServerModuleInitRegistry {
407 let mut server_gens = ServerModuleInitRegistry::new();
408
409 server_gens.attach(LightningInit);
410 server_gens.attach(MintInit);
411 server_gens.attach(WalletInit);
412
413 if is_env_var_set_opt(FM_ENABLE_MODULE_LNV2_ENV).unwrap_or(true) {
414 server_gens.attach(fedimint_lnv2_server::LightningInit);
415 }
416
417 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
418 server_gens.attach(MetaInit);
419 }
420
421 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
422 server_gens.attach(UnknownInit);
423 }
424
425 server_gens
426}