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 bitcoin::Network;
18use clap::{ArgGroup, Parser};
19use fedimint_core::config::{EmptyGenParams, ServerModuleConfigGenParamsRegistry};
20use fedimint_core::db::Database;
21use fedimint_core::envs::{
22 BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
23};
24use fedimint_core::module::registry::ModuleRegistry;
25use fedimint_core::task::TaskGroup;
26use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
27use fedimint_core::{default_esplora_server, timing};
28use fedimint_ln_common::config::{
29 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
30};
31use fedimint_ln_server::LightningInit;
32use fedimint_logging::{LOG_CORE, TracingSetup};
33use fedimint_meta_server::{MetaGenParams, MetaInit};
34use fedimint_mint_server::MintInit;
35use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
36use fedimint_rocksdb::RocksDb;
37use fedimint_server::config::ConfigGenSettings;
38use fedimint_server::config::io::DB_FILE;
39use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
40use fedimint_server::net::api::ApiSecrets;
41use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
42use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
43use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
44use fedimint_unknown_common::config::UnknownGenParams;
45use fedimint_unknown_server::UnknownInit;
46use fedimint_wallet_server::WalletInit;
47use fedimint_wallet_server::common::config::{
48 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
49};
50use futures::FutureExt as _;
51use tracing::{debug, error, info};
52
53use crate::envs::{
54 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRCIS_ENV, FM_BIND_P2P_ENV,
55 FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_URL_ENV,
56 FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
57 FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV, FM_P2P_URL_ENV,
58 FM_PORT_ESPLORA_ENV,
59};
60use crate::metrics::APP_START_TS;
61
62const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
64
65#[derive(Parser)]
66#[command(version)]
67#[command(
68 group(
69 ArgGroup::new("bitcoin_rpc")
70 .required(true)
71 .multiple(false)
72 .args(["bitcoind_url", "esplora_url"])
73 )
74)]
75struct ServerOpts {
76 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
78 data_dir: PathBuf,
79
80 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
82 bitcoin_network: Network,
83
84 #[arg(long, env = FM_BITCOIND_URL_ENV)]
86 bitcoind_url: Option<SafeUrl>,
87
88 #[arg(long, env = FM_ESPLORA_URL_ENV)]
90 esplora_url: Option<SafeUrl>,
91
92 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
97 bind_p2p: SocketAddr,
98
99 #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
104 bind_api: SocketAddr,
105
106 #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
112 bind_ui: SocketAddr,
113
114 #[arg(long, env = FM_P2P_URL_ENV)]
120 p2p_url: Option<SafeUrl>,
121
122 #[arg(long, env = FM_API_URL_ENV)]
128 api_url: Option<SafeUrl>,
129
130 #[arg(long, env = FM_ENABLE_IROH_ENV)]
131 enable_iroh: bool,
132
133 #[arg(long, env = "FM_IROH_DNS", requires = "enable_iroh")]
135 iroh_dns: Option<SafeUrl>,
136
137 #[arg(long, env = "FM_IROH_RELAY", requires = "enable_iroh")]
139 iroh_relays: Vec<SafeUrl>,
140
141 #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
143 db_checkpoint_retention: u64,
144
145 #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
147 bind_tokio_console: Option<SocketAddr>,
148
149 #[arg(long, default_value = "false")]
151 with_jaeger: bool,
152
153 #[arg(long, env = FM_BIND_METRCIS_ENV)]
155 bind_metrics: Option<SocketAddr>,
156
157 #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
172 force_api_secrets: ApiSecrets,
173}
174
175pub async fn run(
192 modules_fn: fn(
193 Network,
194 ) -> (
195 ServerModuleInitRegistry,
196 ServerModuleConfigGenParamsRegistry,
197 ),
198 code_version_hash: &str,
199 code_version_vendor_suffix: Option<&str>,
200) -> ! {
201 assert_eq!(
202 env!("FEDIMINT_BUILD_CODE_VERSION").len(),
203 code_version_hash.len(),
204 "version_hash must have an expected length"
205 );
206
207 handle_version_hash_command(code_version_hash);
208
209 let fedimint_version = env!("CARGO_PKG_VERSION");
210
211 APP_START_TS
212 .with_label_values(&[fedimint_version, code_version_hash])
213 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
214
215 let server_opts = ServerOpts::parse();
216
217 let mut tracing_builder = TracingSetup::default();
218
219 tracing_builder
220 .tokio_console_bind(server_opts.bind_tokio_console)
221 .with_jaeger(server_opts.with_jaeger);
222
223 tracing_builder.init().unwrap();
224
225 info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
226
227 let code_version_str = code_version_vendor_suffix.map_or_else(
228 || fedimint_version.to_string(),
229 |suffix| format!("{fedimint_version}+{suffix}"),
230 );
231
232 let (server_gens, server_gen_params) = modules_fn(server_opts.bitcoin_network);
233
234 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
235
236 let root_task_group = TaskGroup::new();
237
238 if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
239 root_task_group.spawn_cancellable(
240 "metrics-server",
241 fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
242 );
243 }
244
245 let settings = ConfigGenSettings {
246 p2p_bind: server_opts.bind_p2p,
247 api_bind: server_opts.bind_api,
248 ui_bind: server_opts.bind_ui,
249 p2p_url: server_opts.p2p_url,
250 api_url: server_opts.api_url,
251 enable_iroh: server_opts.enable_iroh,
252 iroh_dns: server_opts.iroh_dns,
253 iroh_relays: server_opts.iroh_relays,
254 modules: server_gen_params.clone(),
255 registry: server_gens.clone(),
256 };
257
258 let db = Database::new(
259 RocksDb::open(server_opts.data_dir.join(DB_FILE))
260 .await
261 .unwrap(),
262 ModuleRegistry::default(),
263 );
264
265 let dyn_server_bitcoin_rpc = match (
266 server_opts.bitcoind_url.as_ref(),
267 server_opts.esplora_url.as_ref(),
268 ) {
269 (Some(url), None) => BitcoindClient::new(url).unwrap().into_dyn(),
270 (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
271 _ => unreachable!("ArgGroup already enforced XOR relation"),
272 };
273
274 root_task_group.install_kill_handler();
275
276 let task_group = root_task_group.clone();
277 root_task_group.spawn_cancellable("main", async move {
278 fedimint_server::run(
279 server_opts.data_dir,
280 server_opts.force_api_secrets,
281 settings,
282 db,
283 code_version_str,
284 server_gens,
285 task_group,
286 dyn_server_bitcoin_rpc,
287 Box::new(fedimint_server_ui::setup::router),
288 Box::new(fedimint_server_ui::dashboard::router),
289 server_opts.db_checkpoint_retention,
290 )
291 .await
292 .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
293 });
294
295 let shutdown_future = root_task_group
296 .make_handle()
297 .make_shutdown_rx()
298 .then(|()| async {
299 info!(target: LOG_CORE, "Shutdown called");
300 });
301
302 shutdown_future.await;
303 debug!(target: LOG_CORE, "Terminating main task");
304
305 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
306 error!(target: LOG_CORE, ?err, "Error while shutting down task group");
307 }
308
309 debug!(target: LOG_CORE, "Shutdown complete");
310
311 fedimint_logging::shutdown();
312
313 drop(timing_total_runtime);
314
315 std::process::exit(-1);
317}
318
319pub fn default_modules(
320 network: Network,
321) -> (
322 ServerModuleInitRegistry,
323 ServerModuleConfigGenParamsRegistry,
324) {
325 let mut server_gens = ServerModuleInitRegistry::new();
326 let mut server_gen_params = ServerModuleConfigGenParamsRegistry::default();
327
328 let bitcoin_rpc_config = BitcoinRpcConfig {
329 kind: "bitcoind".to_string(),
330 url: "http://unused_dummy.xyz".parse().unwrap(),
331 };
332
333 server_gens.attach(LightningInit);
334 server_gen_params.attach_config_gen_params(
335 LightningInit::kind(),
336 LightningGenParams {
337 local: LightningGenParamsLocal {
338 bitcoin_rpc: bitcoin_rpc_config.clone(),
339 },
340 consensus: LightningGenParamsConsensus { network },
341 },
342 );
343
344 server_gens.attach(MintInit);
345 server_gen_params.attach_config_gen_params(
346 MintInit::kind(),
347 MintGenParams {
348 local: EmptyGenParams::default(),
349 consensus: MintGenParamsConsensus::new(
350 2,
351 fedimint_mint_common::config::FeeConsensus::zero(),
354 ),
355 },
356 );
357
358 server_gens.attach(WalletInit);
359 server_gen_params.attach_config_gen_params(
360 WalletInit::kind(),
361 WalletGenParams {
362 local: WalletGenParamsLocal {
363 bitcoin_rpc: bitcoin_rpc_config.clone(),
364 },
365 consensus: WalletGenParamsConsensus {
366 network,
367 finality_delay: default_finality_delay(network),
368 client_default_bitcoin_rpc: default_esplora_server(
369 network,
370 std::env::var(FM_PORT_ESPLORA_ENV).ok(),
371 ),
372 fee_consensus: fedimint_wallet_server::common::config::FeeConsensus::default(),
373 },
374 },
375 );
376
377 let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
378 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
379
380 if enable_lnv2 {
381 server_gens.attach(fedimint_lnv2_server::LightningInit);
382 server_gen_params.attach_config_gen_params(
383 fedimint_lnv2_server::LightningInit::kind(),
384 fedimint_lnv2_common::config::LightningGenParams {
385 local: fedimint_lnv2_common::config::LightningGenParamsLocal {
386 bitcoin_rpc: bitcoin_rpc_config.clone(),
387 },
388 consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
389 fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(100).unwrap(),
391 network,
392 },
393 },
394 );
395 }
396
397 if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
398 server_gens.attach(MetaInit);
399 server_gen_params.attach_config_gen_params(MetaInit::kind(), MetaGenParams::default());
400 }
401
402 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
403 server_gens.attach(UnknownInit);
404 server_gen_params
405 .attach_config_gen_params(UnknownInit::kind(), UnknownGenParams::default());
406 }
407
408 (server_gens, server_gen_params)
409}
410
411fn default_finality_delay(network: Network) -> u32 {
414 match network {
415 Network::Bitcoin | Network::Regtest => 10,
416 Network::Testnet | Network::Signet | Network::Testnet4 => 2,
417 _ => panic!("Unsupported network"),
418 }
419}