1mod metrics;
2
3use std::collections::BTreeMap;
4use std::net::SocketAddr;
5use std::path::PathBuf;
6use std::time::Duration;
7
8use anyhow::{Context, format_err};
9use clap::{Parser, Subcommand};
10use fedimint_core::config::{
11 EmptyGenParams, ModuleInitParams, ServerModuleConfigGenParamsRegistry,
12};
13use fedimint_core::core::ModuleKind;
14use fedimint_core::db::{Database, get_current_database_version};
15use fedimint_core::envs::{
16 BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
17};
18use fedimint_core::module::registry::ModuleRegistry;
19use fedimint_core::module::{ServerApiVersionsSummary, ServerDbVersionsSummary};
20use fedimint_core::task::TaskGroup;
21use fedimint_core::util::{
22 FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command, write_overwrite,
23};
24use fedimint_core::{crit, timing};
25use fedimint_ln_common::config::{
26 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
27};
28use fedimint_ln_server::LightningInit;
29use fedimint_logging::{LOG_CORE, LOG_SERVER, TracingSetup};
30use fedimint_meta_server::{MetaGenParams, MetaInit};
31use fedimint_mint_server::MintInit;
32use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
33use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD};
34use fedimint_server::config::{ConfigGenSettings, NetworkingStack, ServerConfig};
35use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
36use fedimint_server::envs::FM_FORCE_IROH_ENV;
37use fedimint_server::net::api::ApiSecrets;
38use fedimint_unknown_common::config::UnknownGenParams;
39use fedimint_unknown_server::UnknownInit;
40use fedimint_wallet_server::WalletInit;
41use fedimint_wallet_server::common::config::{
42 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
43};
44use futures::FutureExt;
45use tracing::{debug, error, info};
46
47use crate::default_esplora_server;
48use crate::envs::{
49 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRICS_API_ENV, FM_BIND_P2P_ENV,
50 FM_BITCOIN_NETWORK_ENV, FM_DATA_DIR_ENV, FM_DISABLE_META_MODULE_ENV, FM_EXTRA_DKG_META_ENV,
51 FM_FINALITY_DELAY_ENV, FM_FORCE_API_SECRETS_ENV, FM_P2P_URL_ENV, FM_PASSWORD_ENV,
52 FM_TOKIO_CONSOLE_BIND_ENV,
53};
54use crate::fedimintd::metrics::APP_START_TS;
55
56const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
58
59#[derive(Parser)]
60#[command(version)]
61struct ServerOpts {
62 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
64 data_dir: Option<PathBuf>,
65 #[arg(long, env = FM_PASSWORD_ENV)]
69 password: Option<String>,
70 #[arg(long, env = FM_TOKIO_CONSOLE_BIND_ENV)]
72 tokio_console_bind: Option<SocketAddr>,
73 #[arg(long, default_value = "false")]
75 with_telemetry: bool,
76
77 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "127.0.0.1:8173")]
79 bind_p2p: SocketAddr,
80 #[arg(long, env = FM_P2P_URL_ENV, default_value = "fedimint://127.0.0.1:8173")]
82 p2p_url: SafeUrl,
83 #[arg(long, env = FM_BIND_API_ENV, default_value = "127.0.0.1:8174")]
85 bind_api: SocketAddr,
86 #[arg(long, env = FM_API_URL_ENV, default_value = "ws://127.0.0.1:8174")]
88 api_url: SafeUrl,
89 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
91 network: bitcoin::network::Network,
92 #[arg(long, env = FM_FINALITY_DELAY_ENV, default_value = "10")]
94 finality_delay: u32,
95
96 #[arg(long, env = FM_BIND_METRICS_API_ENV)]
97 bind_metrics_api: Option<SocketAddr>,
98
99 #[arg(long, env = FM_EXTRA_DKG_META_ENV, value_parser = parse_map, default_value="")]
102 extra_dkg_meta: BTreeMap<String, String>,
103
104 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
119 force_api_secrets: ApiSecrets,
120
121 #[clap(subcommand)]
122 subcommand: Option<ServerSubcommand>,
123}
124
125#[derive(Subcommand)]
126enum ServerSubcommand {
127 #[clap(subcommand)]
129 Dev(DevSubcommand),
130}
131
132#[derive(Subcommand)]
133enum DevSubcommand {
134 ListApiVersions,
136 ListDbVersions,
138}
139
140fn parse_map(s: &str) -> anyhow::Result<BTreeMap<String, String>> {
148 let mut map = BTreeMap::new();
149
150 if s.is_empty() {
151 return Ok(map);
152 }
153
154 for pair in s.split(',') {
155 let parts: Vec<&str> = pair.split('=').collect();
156
157 if parts.len() != 2 {
158 return Err(format_err!(
159 "Invalid key-value pair in map: '{}'. Expected format: 'key=value'.",
160 pair
161 ));
162 }
163
164 let key = parts[0].trim();
165 let value = parts[1].trim();
166
167 if let Some(previous_value) = map.insert(key.to_string(), value.to_string()) {
168 return Err(format_err!(
169 "Duplicate key in map: '{key}' (found values '{previous_value}' and '{value}')",
170 ));
171 }
172 }
173
174 Ok(map)
175}
176
177pub struct Fedimintd {
208 server_gens: ServerModuleInitRegistry,
209 server_gen_params: ServerModuleConfigGenParamsRegistry,
210 code_version_hash: String,
211 code_version_str: String,
212 opts: ServerOpts,
213 bitcoind_rpc: BitcoinRpcConfig,
214}
215
216impl Fedimintd {
217 pub fn new(
232 code_version_hash: &str,
233 code_version_vendor_suffix: Option<&str>,
234 ) -> anyhow::Result<Fedimintd> {
235 assert_eq!(
236 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
237 code_version_hash.len(),
238 "version_hash must have an expected length"
239 );
240
241 handle_version_hash_command(code_version_hash);
242
243 let fedimint_version = env!("CARGO_PKG_VERSION");
244
245 APP_START_TS
246 .with_label_values(&[fedimint_version, code_version_hash])
247 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
248
249 let opts: ServerOpts = ServerOpts::parse();
250
251 TracingSetup::default()
252 .tokio_console_bind(opts.tokio_console_bind)
253 .with_jaeger(opts.with_telemetry)
254 .init()
255 .unwrap();
256
257 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
258
259 let bitcoind_rpc = BitcoinRpcConfig::get_defaults_from_env_vars()?;
260
261 Ok(Self {
262 opts,
263 bitcoind_rpc,
264 server_gens: ServerModuleInitRegistry::new(),
265 server_gen_params: ServerModuleConfigGenParamsRegistry::default(),
266 code_version_hash: code_version_hash.to_owned(),
267 code_version_str: code_version_vendor_suffix.map_or_else(
268 || fedimint_version.to_string(),
269 |suffix| format!("{fedimint_version}+{suffix}"),
270 ),
271 })
272 }
273
274 pub fn with_module_kind<T>(mut self, r#gen: T) -> Self
278 where
279 T: ServerModuleInit + 'static + Send + Sync,
280 {
281 self.server_gens.attach(r#gen);
282 self
283 }
284
285 pub fn version_hash(&self) -> &str {
288 &self.code_version_hash
289 }
290
291 pub fn with_module_instance<P>(mut self, kind: ModuleKind, params: P) -> Self
296 where
297 P: ModuleInitParams,
298 {
299 self.server_gen_params
300 .attach_config_gen_params(kind, params);
301 self
302 }
303
304 pub fn with_default_modules(self) -> anyhow::Result<Self> {
306 let network = self.opts.network;
307
308 let bitcoind_rpc = self.bitcoind_rpc.clone();
309 let finality_delay = self.opts.finality_delay;
310 let s = self
311 .with_module_kind(LightningInit)
312 .with_module_instance(
313 LightningInit::kind(),
314 LightningGenParams {
315 local: LightningGenParamsLocal {
316 bitcoin_rpc: bitcoind_rpc.clone(),
317 },
318 consensus: LightningGenParamsConsensus { network },
319 },
320 )
321 .with_module_kind(MintInit)
322 .with_module_instance(
323 MintInit::kind(),
324 MintGenParams {
325 local: EmptyGenParams::default(),
326 consensus: MintGenParamsConsensus::new(
327 2,
328 fedimint_mint_common::config::FeeConsensus::zero(),
331 ),
332 },
333 )
334 .with_module_kind(WalletInit)
335 .with_module_instance(
336 WalletInit::kind(),
337 WalletGenParams {
338 local: WalletGenParamsLocal {
339 bitcoin_rpc: bitcoind_rpc.clone(),
340 },
341 consensus: WalletGenParamsConsensus {
342 network,
343 finality_delay,
346 client_default_bitcoin_rpc: default_esplora_server(network),
347 fee_consensus:
348 fedimint_wallet_server::common::config::FeeConsensus::default(),
349 },
350 },
351 );
352
353 let s = if is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV) {
354 s.with_module_kind(fedimint_lnv2_server::LightningInit)
355 .with_module_instance(
356 fedimint_lnv2_server::LightningInit::kind(),
357 fedimint_lnv2_common::config::LightningGenParams {
358 local: fedimint_lnv2_common::config::LightningGenParamsLocal {
359 bitcoin_rpc: bitcoind_rpc.clone(),
360 },
361 consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
362 fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(1_000)?,
364 network,
365 },
366 },
367 )
368 } else {
369 s
370 };
371
372 let s = if is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
373 s
374 } else {
375 s.with_module_kind(MetaInit)
376 .with_module_instance(MetaInit::kind(), MetaGenParams::default())
377 };
378
379 let s = if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
380 s.with_module_kind(UnknownInit)
381 .with_module_instance(UnknownInit::kind(), UnknownGenParams::default())
382 } else {
383 s
384 };
385
386 Ok(s)
387 }
388
389 pub async fn run(self) -> ! {
391 if let Some(subcommand) = &self.opts.subcommand {
393 match subcommand {
394 ServerSubcommand::Dev(DevSubcommand::ListApiVersions) => {
395 let api_versions = self.get_server_api_versions();
396 let api_versions = serde_json::to_string_pretty(&api_versions)
397 .expect("API versions struct is serializable");
398 println!("{api_versions}");
399 std::process::exit(0);
400 }
401 ServerSubcommand::Dev(DevSubcommand::ListDbVersions) => {
402 let db_versions = self.get_server_db_versions();
403 let db_versions = serde_json::to_string_pretty(&db_versions)
404 .expect("API versions struct is serializable");
405 println!("{db_versions}");
406 std::process::exit(0);
407 }
408 }
409 }
410
411 let root_task_group = TaskGroup::new();
412 root_task_group.install_kill_handler();
413
414 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
415
416 let task_group = root_task_group.clone();
417 root_task_group.spawn_cancellable("main", async move {
418 match run(
419 self.opts,
420 &task_group,
421 self.server_gens,
422 self.server_gen_params,
423 self.code_version_str,
424 )
425 .await
426 {
427 Ok(()) => {}
428 Err(error) => {
429 crit!(target: LOG_SERVER, err = %error.fmt_compact_anyhow(), "Main task returned error, shutting down");
430 task_group.shutdown();
431 }
432 }
433 });
434
435 let shutdown_future = root_task_group
436 .make_handle()
437 .make_shutdown_rx()
438 .then(|()| async {
439 info!(target: LOG_CORE, "Shutdown called");
440 });
441
442 shutdown_future.await;
443 debug!(target: LOG_CORE, "Terminating main task");
444
445 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
446 error!(target: LOG_CORE, ?err, "Error while shutting down task group");
447 }
448
449 debug!(target: LOG_CORE, "Shutdown complete");
450
451 fedimint_logging::shutdown();
452
453 drop(timing_total_runtime);
454
455 std::process::exit(-1);
457 }
458
459 fn get_server_api_versions(&self) -> ServerApiVersionsSummary {
460 ServerApiVersionsSummary {
461 core: ServerConfig::supported_api_versions().api,
462 modules: self
463 .server_gens
464 .kinds()
465 .into_iter()
466 .map(|module_kind| {
467 self.server_gens
468 .get(&module_kind)
469 .expect("module is present")
470 })
471 .map(|module_init| {
472 (
473 module_init.module_kind(),
474 module_init.supported_api_versions().api,
475 )
476 })
477 .collect(),
478 }
479 }
480
481 fn get_server_db_versions(&self) -> ServerDbVersionsSummary {
482 ServerDbVersionsSummary {
483 modules: self
484 .server_gens
485 .kinds()
486 .into_iter()
487 .map(|module_kind| {
488 self.server_gens
489 .get(&module_kind)
490 .expect("module is present")
491 })
492 .map(|module_init| {
493 (
494 module_init.module_kind(),
495 get_current_database_version(&module_init.get_database_migrations()),
496 )
497 })
498 .collect(),
499 }
500 }
501}
502
503async fn run(
504 opts: ServerOpts,
505 task_group: &TaskGroup,
506 module_inits: ServerModuleInitRegistry,
507 module_inits_params: ServerModuleConfigGenParamsRegistry,
508 code_version_str: String,
509) -> anyhow::Result<()> {
510 if let Some(socket_addr) = opts.bind_metrics_api.as_ref() {
511 task_group.spawn_cancellable("metrics-server", {
512 let task_group = task_group.clone();
513 let socket_addr = *socket_addr;
514 async move { fedimint_metrics::run_api_server(socket_addr, task_group).await }
515 });
516 }
517
518 let data_dir = opts.data_dir.context("data-dir option is not present")?;
519
520 if let Some(password) = opts.password {
524 write_overwrite(data_dir.join(PLAINTEXT_PASSWORD), password)?;
525 };
526 let use_iroh = is_env_var_set(FM_FORCE_IROH_ENV);
527
528 let settings = ConfigGenSettings {
530 p2p_bind: opts.bind_p2p,
531 api_bind: opts.bind_api,
532 p2p_url: opts.p2p_url,
533 api_url: opts.api_url,
534 meta: opts.extra_dkg_meta.clone(),
535 modules: module_inits_params.clone(),
536 registry: module_inits.clone(),
537 networking: if use_iroh {
538 NetworkingStack::Iroh
539 } else {
540 NetworkingStack::default()
541 },
542 };
543
544 let db = Database::new(
545 fedimint_rocksdb::RocksDb::open(data_dir.join(DB_FILE))?,
546 ModuleRegistry::default(),
547 );
548
549 Box::pin(fedimint_server::run(
550 data_dir,
551 opts.force_api_secrets,
552 settings,
553 db,
554 code_version_str,
555 &module_inits,
556 task_group.clone(),
557 ))
558 .await
559}