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