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::task::TaskGroup;
29use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
30use fedimint_core::{default_esplora_server, timing};
31use fedimint_ln_common::config::{
32 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
33};
34use fedimint_ln_server::LightningInit;
35use fedimint_logging::{LOG_CORE, TracingSetup};
36use fedimint_meta_server::{MetaGenParams, MetaInit};
37use fedimint_mint_server::MintInit;
38use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
39use fedimint_rocksdb::RocksDb;
40use fedimint_server::config::ConfigGenSettings;
41use fedimint_server::config::io::DB_FILE;
42use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
43use fedimint_server::net::api::ApiSecrets;
44use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
45use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
46use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
47use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
48use fedimint_unknown_common::config::UnknownGenParams;
49use fedimint_unknown_server::UnknownInit;
50use fedimint_wallet_server::WalletInit;
51use fedimint_wallet_server::common::config::{
52 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
53};
54use futures::FutureExt as _;
55use tracing::{debug, error, info};
56
57use crate::envs::{
58 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRCIS_ENV, FM_BIND_P2P_ENV,
59 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_URL_ENV,
60 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
61 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
62 FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, FM_P2P_URL_ENV,
63 FM_PORT_ESPLORA_ENV,
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("bitcoin_rpc")
75 .required(true)
76 .multiple(true)
77 .args(["bitcoind_url", "esplora_url"])
78 )
79)]
80struct ServerOpts {
81 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
83 data_dir: PathBuf,
84
85 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
87 bitcoin_network: Network,
88
89 #[arg(long, env = FM_BITCOIND_URL_ENV)]
91 bitcoind_url: Option<SafeUrl>,
92
93 #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
101 bitcoind_url_password_file: Option<PathBuf>,
102
103 #[arg(long, env = FM_ESPLORA_URL_ENV)]
105 esplora_url: Option<SafeUrl>,
106
107 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
112 bind_p2p: SocketAddr,
113
114 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
119 bind_api: SocketAddr,
120
121 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
127 bind_ui: SocketAddr,
128
129 #[arg(long, env = FM_P2P_URL_ENV)]
135 p2p_url: Option<SafeUrl>,
136
137 #[arg(long, env = FM_API_URL_ENV)]
143 api_url: Option<SafeUrl>,
144
145 #[arg(long, env = FM_ENABLE_IROH_ENV)]
146 enable_iroh: bool,
147
148 #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
150 iroh_dns: Option<SafeUrl>,
151
152 #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh")]
154 iroh_relays: Vec<SafeUrl>,
155
156 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
158 db_checkpoint_retention: u64,
159
160 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
162 bind_tokio_console: Option<SocketAddr>,
163
164 #[arg(long, default_value = "false")]
166 with_jaeger: bool,
167
168 #[arg(long, env = FM_BIND_METRCIS_ENV)]
170 bind_metrics: Option<SocketAddr>,
171
172 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
187 force_api_secrets: ApiSecrets,
188
189 #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
191 iroh_api_max_connections: usize,
192
193 #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
195 iroh_api_max_requests_per_connection: usize,
196}
197
198impl ServerOpts {
199 pub async fn get_bitcoind_url(&self) -> anyhow::Result<SafeUrl> {
200 let mut url = self
201 .bitcoind_url
202 .clone()
203 .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
204 if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
205 let password = tokio::fs::read_to_string(password_file)
206 .await
207 .context("Failed to read the password")?
208 .trim()
209 .to_owned();
210 url.set_password(Some(&password))
211 .ok()
212 .ok_or_else(|| anyhow::anyhow!("Failed to set the password from the url"))?;
213 }
214
215 Ok(url)
216 }
217}
218
219#[allow(clippy::too_many_lines)]
236pub async fn run(
237 modules_fn: fn(
238 Network,
239 ) -> (
240 ServerModuleInitRegistry,
241 ServerModuleConfigGenParamsRegistry,
242 ),
243 code_version_hash: &str,
244 code_version_vendor_suffix: Option<&str>,
245) -> ! {
246 assert_eq!(
247 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
248 code_version_hash.len(),
249 "version_hash must have an expected length"
250 );
251
252 handle_version_hash_command(code_version_hash);
253
254 let fedimint_version = env!("CARGO_PKG_VERSION");
255
256 APP_START_TS
257 .with_label_values(&[fedimint_version, code_version_hash])
258 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
259
260 let server_opts = ServerOpts::parse();
261
262 let mut tracing_builder = TracingSetup::default();
263
264 tracing_builder
265 .tokio_console_bind(server_opts.bind_tokio_console)
266 .with_jaeger(server_opts.with_jaeger);
267
268 tracing_builder.init().unwrap();
269
270 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
271
272 let code_version_str = code_version_vendor_suffix.map_or_else(
273 || fedimint_version.to_string(),
274 |suffix| format!("{fedimint_version}+{suffix}"),
275 );
276
277 let (server_gens, server_gen_params) = modules_fn(server_opts.bitcoin_network);
278
279 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
280
281 let root_task_group = TaskGroup::new();
282
283 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
284 root_task_group.spawn_cancellable(
285 "metrics-server",
286 fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
287 );
288 }
289
290 let settings = ConfigGenSettings {
291 p2p_bind: server_opts.bind_p2p,
292 api_bind: server_opts.bind_api,
293 ui_bind: server_opts.bind_ui,
294 p2p_url: server_opts.p2p_url.clone(),
295 api_url: server_opts.api_url.clone(),
296 enable_iroh: server_opts.enable_iroh,
297 iroh_dns: server_opts.iroh_dns.clone(),
298 iroh_relays: server_opts.iroh_relays.clone(),
299 modules: server_gen_params.clone(),
300 registry: server_gens.clone(),
301 };
302
303 let db = Database::new(
304 RocksDb::open(server_opts.data_dir.join(DB_FILE))
305 .await
306 .unwrap(),
307 ModuleRegistry::default(),
308 );
309
310 let dyn_server_bitcoin_rpc = match (
311 server_opts.bitcoind_url.as_ref(),
312 server_opts.esplora_url.as_ref(),
313 ) {
314 (Some(_), None) => BitcoindClient::new(
315 &server_opts
316 .get_bitcoind_url()
317 .await
318 .expect("Failed to get bitcoind url"),
319 )
320 .unwrap()
321 .into_dyn(),
322 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
323 (Some(_), Some(esplora_url)) => BitcoindClientWithFallback::new(
324 &server_opts
325 .get_bitcoind_url()
326 .await
327 .expect("Failed to get bitcoind url"),
328 esplora_url,
329 )
330 .unwrap()
331 .into_dyn(),
332 _ => unreachable!("ArgGroup already enforced XOR relation"),
333 };
334
335 root_task_group.install_kill_handler();
336
337 let task_group = root_task_group.clone();
338 root_task_group.spawn_cancellable("main", async move {
339 fedimint_server::run(
340 server_opts.data_dir,
341 server_opts.force_api_secrets,
342 settings,
343 db,
344 code_version_str,
345 server_gens,
346 task_group,
347 dyn_server_bitcoin_rpc,
348 Box::new(fedimint_server_ui::setup::router),
349 Box::new(fedimint_server_ui::dashboard::router),
350 server_opts.db_checkpoint_retention,
351 fedimint_server::ConnectionLimits::new(
352 server_opts.iroh_api_max_connections,
353 server_opts.iroh_api_max_requests_per_connection,
354 ),
355 )
356 .await
357 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
358 });
359
360 let shutdown_future = root_task_group
361 .make_handle()
362 .make_shutdown_rx()
363 .then(|()| async {
364 info!(target: LOG_CORE, "Shutdown called");
365 });
366
367 shutdown_future.await;
368 debug!(target: LOG_CORE, "Terminating main task");
369
370 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
371 error!(target: LOG_CORE, ?err, "Error while shutting down task group");
372 }
373
374 debug!(target: LOG_CORE, "Shutdown complete");
375
376 fedimint_logging::shutdown();
377
378 drop(timing_total_runtime);
379
380 std::process::exit(-1);
382}
383
384pub fn default_modules(
385 network: Network,
386) -> (
387 ServerModuleInitRegistry,
388 ServerModuleConfigGenParamsRegistry,
389) {
390 let mut server_gens = ServerModuleInitRegistry::new();
391 let mut server_gen_params = ServerModuleConfigGenParamsRegistry::default();
392
393 let bitcoin_rpc_config = BitcoinRpcConfig {
394 kind: "bitcoind".to_string(),
395 url: "http://unused_dummy.xyz".parse().unwrap(),
396 };
397
398 server_gens.attach(LightningInit);
399 server_gen_params.attach_config_gen_params(
400 LightningInit::kind(),
401 LightningGenParams {
402 local: LightningGenParamsLocal {
403 bitcoin_rpc: bitcoin_rpc_config.clone(),
404 },
405 consensus: LightningGenParamsConsensus { network },
406 },
407 );
408
409 server_gens.attach(MintInit);
410 server_gen_params.attach_config_gen_params(
411 MintInit::kind(),
412 MintGenParams {
413 local: EmptyGenParams::default(),
414 consensus: MintGenParamsConsensus::new(
415 2,
416 fedimint_mint_common::config::FeeConsensus::zero(),
419 ),
420 },
421 );
422
423 server_gens.attach(WalletInit);
424 server_gen_params.attach_config_gen_params(
425 WalletInit::kind(),
426 WalletGenParams {
427 local: WalletGenParamsLocal {
428 bitcoin_rpc: bitcoin_rpc_config.clone(),
429 },
430 consensus: WalletGenParamsConsensus {
431 network,
432 finality_delay: default_finality_delay(network),
433 client_default_bitcoin_rpc: default_esplora_server(
434 network,
435 std::env::var(FM_PORT_ESPLORA_ENV).ok(),
436 ),
437 fee_consensus: fedimint_wallet_server::common::config::FeeConsensus::default(),
438 },
439 },
440 );
441
442 let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
443 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
444
445 if enable_lnv2 {
446 server_gens.attach(fedimint_lnv2_server::LightningInit);
447 server_gen_params.attach_config_gen_params(
448 fedimint_lnv2_server::LightningInit::kind(),
449 fedimint_lnv2_common::config::LightningGenParams {
450 local: fedimint_lnv2_common::config::LightningGenParamsLocal {
451 bitcoin_rpc: bitcoin_rpc_config.clone(),
452 },
453 consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
454 fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(100).unwrap(),
456 network,
457 },
458 },
459 );
460 }
461
462 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
463 server_gens.attach(MetaInit);
464 server_gen_params.attach_config_gen_params(MetaInit::kind(), MetaGenParams::default());
465 }
466
467 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
468 server_gens.attach(UnknownInit);
469 server_gen_params
470 .attach_config_gen_params(UnknownInit::kind(), UnknownGenParams::default());
471 }
472
473 (server_gens, server_gen_params)
474}
475
476fn default_finality_delay(network: Network) -> u32 {
479 match network {
480 Network::Bitcoin | Network::Regtest => 10,
481 Network::Testnet | Network::Signet | Network::Testnet4 => 2,
482 _ => panic!("Unsupported network"),
483 }
484}