fedimintd.rs
1 mod metrics; 2 3 use std::collections::BTreeMap; 4 use std::net::SocketAddr; 5 use std::path::PathBuf; 6 use std::time::Duration; 7 8 use anyhow::{format_err, Context}; 9 use clap::{Parser, Subcommand}; 10 use fedimint_core::admin_client::ConfigGenParamsRequest; 11 use fedimint_core::config::{ 12 ModuleInitParams, ServerModuleConfigGenParamsRegistry, ServerModuleInitRegistry, 13 }; 14 use fedimint_core::core::ModuleKind; 15 use fedimint_core::db::Database; 16 use fedimint_core::envs::{is_env_var_set, BitcoinRpcConfig, FM_USE_UNKNOWN_MODULE_ENV}; 17 use fedimint_core::module::{ServerApiVersionsSummary, ServerDbVersionsSummary, ServerModuleInit}; 18 use fedimint_core::task::TaskGroup; 19 use fedimint_core::timing; 20 use fedimint_core::util::{handle_version_hash_command, write_overwrite, SafeUrl}; 21 use fedimint_ln_common::config::{ 22 LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal, 23 }; 24 use fedimint_ln_server::LightningInit; 25 use fedimint_logging::TracingSetup; 26 use fedimint_meta_server::{MetaGenParams, MetaInit}; 27 use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus}; 28 use fedimint_mint_server::MintInit; 29 use fedimint_server::config::api::ConfigGenSettings; 30 use fedimint_server::config::io::{DB_FILE, PLAINTEXT_PASSWORD}; 31 use fedimint_server::config::ServerConfig; 32 use fedimint_unknown_common::config::UnknownGenParams; 33 use fedimint_unknown_server::UnknownInit; 34 use fedimint_wallet_server::common::config::{ 35 WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal, 36 }; 37 use fedimint_wallet_server::WalletInit; 38 use futures::FutureExt; 39 use tracing::{debug, error, info}; 40 41 use crate::default_esplora_server; 42 use crate::envs::{ 43 FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRICS_API_ENV, FM_BIND_P2P_ENV, 44 FM_BITCOIN_NETWORK_ENV, FM_DATA_DIR_ENV, FM_DISABLE_META_MODULE_ENV, FM_EXTRA_DKG_META_ENV, 45 FM_FINALITY_DELAY_ENV, FM_P2P_URL_ENV, FM_PASSWORD_ENV, FM_TOKIO_CONSOLE_BIND_ENV, 46 }; 47 use crate::fedimintd::metrics::APP_START_TS; 48 49 /// Time we will wait before forcefully shutting down tasks 50 const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); 51 52 #[derive(Parser)] 53 #[command(version)] 54 pub struct ServerOpts { 55 /// Path to folder containing federation config files 56 #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)] 57 pub data_dir: Option<PathBuf>, 58 /// Password to encrypt sensitive config files 59 // TODO: should probably never send password to the server directly, rather send the hash via 60 // the API 61 #[arg(long, env = FM_PASSWORD_ENV)] 62 pub password: Option<String>, 63 /// Enable tokio console logging 64 #[arg(long, env = FM_TOKIO_CONSOLE_BIND_ENV)] 65 pub tokio_console_bind: Option<SocketAddr>, 66 /// Enable telemetry logging 67 #[arg(long, default_value = "false")] 68 pub with_telemetry: bool, 69 70 /// Address we bind to for federation communication 71 #[arg(long, env = FM_BIND_P2P_ENV, default_value = "127.0.0.1:8173")] 72 bind_p2p: SocketAddr, 73 /// Our external address for communicating with our peers 74 #[arg(long, env = FM_P2P_URL_ENV, default_value = "fedimint://127.0.0.1:8173")] 75 p2p_url: SafeUrl, 76 /// Address we bind to for exposing the API 77 #[arg(long, env = FM_BIND_API_ENV, default_value = "127.0.0.1:8174")] 78 bind_api: SocketAddr, 79 /// Our API address for clients to connect to us 80 #[arg(long, env = FM_API_URL_ENV, default_value = "ws://127.0.0.1:8174")] 81 api_url: SafeUrl, 82 /// The bitcoin network that fedimint will be running on 83 #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")] 84 network: bitcoin::network::constants::Network, 85 /// The number of blocks the federation stays behind the blockchain tip 86 #[arg(long, env = FM_FINALITY_DELAY_ENV, default_value = "10")] 87 finality_delay: u32, 88 89 #[arg(long, env = FM_BIND_METRICS_API_ENV)] 90 bind_metrics_api: Option<SocketAddr>, 91 92 /// List of default meta values to use during config generation (format: 93 /// `key1=value1,key2=value,...`) 94 #[arg(long, env = FM_EXTRA_DKG_META_ENV, value_parser = parse_map, default_value="")] 95 extra_dkg_meta: BTreeMap<String, String>, 96 97 #[clap(subcommand)] 98 subcommand: Option<ServerSubcommand>, 99 } 100 101 #[derive(Subcommand)] 102 enum ServerSubcommand { 103 /// Development-related commands 104 #[clap(subcommand)] 105 Dev(DevSubcommand), 106 } 107 108 #[derive(Subcommand)] 109 enum DevSubcommand { 110 /// List supported server API versions and exit 111 ListApiVersions, 112 /// List supported server database versions and exit 113 ListDbVersions, 114 } 115 116 fn parse_map(s: &str) -> anyhow::Result<BTreeMap<String, String>> { 117 let mut map = BTreeMap::new(); 118 119 if s.is_empty() { 120 return Ok(map); 121 } 122 123 for pair in s.split(',') { 124 let parts: Vec<&str> = pair.split('=').collect(); 125 if parts.len() == 2 { 126 map.insert(parts[0].to_string(), parts[1].to_string()); 127 } else { 128 return Err(format_err!("Invalid pair in map: {}", pair)); 129 } 130 } 131 Ok(map) 132 } 133 134 /// `fedimintd` builder 135 /// 136 /// Fedimint supports third party modules. Right now (and for forseable feature) 137 /// modules needs to be combined with rest of the code at the compilation time. 138 /// 139 /// To make this easier, [`Fedimintd`] builder is exposed, allowing 140 /// building `fedimintd` with custom set of modules. 141 /// 142 /// 143 /// Example: 144 /// 145 /// ``` 146 /// use fedimint_ln_server::LightningInit; 147 /// use fedimint_mint_server::MintInit; 148 /// use fedimint_wallet_server::WalletInit; 149 /// use fedimintd::Fedimintd; 150 /// 151 /// // Note: not called `main` to avoid rustdoc executing it 152 /// // #[tokio::main] 153 /// async fn main_() -> anyhow::Result<()> { 154 /// Fedimintd::new(env!("FEDIMINT_BUILD_CODE_VERSION"))? 155 /// // use `.with_default_modules()` to avoid having 156 /// // to import these manually 157 /// .with_module_kind(WalletInit) 158 /// .with_module_kind(MintInit) 159 /// .with_module_kind(LightningInit) 160 /// .run() 161 /// .await 162 /// } 163 /// ``` 164 pub struct Fedimintd { 165 server_gens: ServerModuleInitRegistry, 166 server_gen_params: ServerModuleConfigGenParamsRegistry, 167 version_hash: String, 168 opts: ServerOpts, 169 bitcoind_rpc: BitcoinRpcConfig, 170 } 171 172 impl Fedimintd { 173 /// Start a new custom `fedimintd` 174 /// 175 /// Like [`Self::new`] but with an ability to customize version strings. 176 pub fn new(version_hash: &str) -> anyhow::Result<Fedimintd> { 177 assert_eq!( 178 env!("FEDIMINT_BUILD_CODE_VERSION").len(), 179 version_hash.len(), 180 "version_hash must have an expected length" 181 ); 182 183 handle_version_hash_command(version_hash); 184 185 let version = env!("CARGO_PKG_VERSION"); 186 187 APP_START_TS 188 .with_label_values(&[version, version_hash]) 189 .set(fedimint_core::time::duration_since_epoch().as_secs() as i64); 190 191 let opts: ServerOpts = ServerOpts::parse(); 192 193 TracingSetup::default() 194 .tokio_console_bind(opts.tokio_console_bind) 195 .with_jaeger(opts.with_telemetry) 196 .init() 197 .unwrap(); 198 199 info!("Starting fedimintd (version: {version} version_hash: {version_hash})"); 200 201 let bitcoind_rpc = BitcoinRpcConfig::get_defaults_from_env_vars()?; 202 203 Ok(Self { 204 opts, 205 bitcoind_rpc, 206 server_gens: ServerModuleInitRegistry::new(), 207 server_gen_params: ServerModuleConfigGenParamsRegistry::default(), 208 version_hash: version_hash.to_owned(), 209 }) 210 } 211 212 /// Attach a server module kind to the Fedimintd instance 213 /// 214 /// This makes `fedimintd` support additional module types (aka. kinds) 215 pub fn with_module_kind<T>(mut self, gen: T) -> Self 216 where 217 T: ServerModuleInit + 'static + Send + Sync, 218 { 219 self.server_gens.attach(gen); 220 self 221 } 222 223 /// Get the version hash this `fedimintd` will report for diagnostic 224 /// purposes 225 pub fn version_hash(&self) -> &str { 226 &self.version_hash 227 } 228 229 /// Attach additional module instance with parameters 230 /// 231 /// Note: The `kind` needs to be added with [`Self::with_module_kind`] if 232 /// it's not the default one. 233 pub fn with_module_instance<P>(mut self, kind: ModuleKind, params: P) -> Self 234 where 235 P: ModuleInitParams, 236 { 237 self.server_gen_params 238 .attach_config_gen_params(kind, params); 239 self 240 } 241 242 /// Attach default server modules to Fedimintd instance 243 pub fn with_default_modules(self) -> Self { 244 let network = self.opts.network; 245 246 let bitcoind_rpc = self.bitcoind_rpc.clone(); 247 let finality_delay = self.opts.finality_delay; 248 let s = self 249 .with_module_kind(LightningInit) 250 .with_module_instance( 251 LightningInit::kind(), 252 LightningGenParams { 253 local: LightningGenParamsLocal { 254 bitcoin_rpc: bitcoind_rpc.clone(), 255 }, 256 consensus: LightningGenParamsConsensus { network }, 257 }, 258 ) 259 .with_module_kind(MintInit) 260 .with_module_instance( 261 MintInit::kind(), 262 MintGenParams { 263 local: Default::default(), 264 consensus: MintGenParamsConsensus::new( 265 2, 266 fedimint_mint_server::common::config::FeeConsensus::default(), 267 ), 268 }, 269 ) 270 .with_module_kind(WalletInit) 271 .with_module_instance( 272 WalletInit::kind(), 273 WalletGenParams { 274 local: WalletGenParamsLocal { 275 bitcoin_rpc: bitcoind_rpc.clone(), 276 }, 277 consensus: WalletGenParamsConsensus { 278 network, 279 // TODO this is not very elegant, but I'm planning to get rid of it in a 280 // next commit anyway 281 finality_delay, 282 client_default_bitcoin_rpc: default_esplora_server(network), 283 fee_consensus: Default::default(), 284 }, 285 }, 286 ); 287 288 let s = if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) { 289 s.with_module_kind(MetaInit) 290 .with_module_instance(MetaInit::kind(), MetaGenParams::default()) 291 } else { 292 s 293 }; 294 295 if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) { 296 s.with_module_kind(UnknownInit) 297 .with_module_instance(UnknownInit::kind(), UnknownGenParams::default()) 298 } else { 299 s 300 } 301 } 302 303 /// Block thread and run a Fedimintd server 304 pub async fn run(self) -> ! { 305 // handle optional subcommand 306 if let Some(subcommand) = &self.opts.subcommand { 307 match subcommand { 308 ServerSubcommand::Dev(DevSubcommand::ListApiVersions) => { 309 let api_versions = self.get_server_api_versions(); 310 let api_versions = serde_json::to_string_pretty(&api_versions) 311 .expect("API versions struct is serializable"); 312 println!("{api_versions}"); 313 std::process::exit(0); 314 } 315 ServerSubcommand::Dev(DevSubcommand::ListDbVersions) => { 316 let db_versions = self.get_server_db_versions(); 317 let db_versions = serde_json::to_string_pretty(&db_versions) 318 .expect("API versions struct is serializable"); 319 println!("{db_versions}"); 320 std::process::exit(0); 321 } 322 } 323 } 324 325 let root_task_group = TaskGroup::new(); 326 root_task_group.install_kill_handler(); 327 328 let timing_total_runtime = timing::TimeReporter::new("total-runtime").info(); 329 330 let task_group = root_task_group.clone(); 331 root_task_group.spawn_cancellable("main", async move { 332 match run( 333 self.opts, 334 &task_group, 335 self.server_gens, 336 self.server_gen_params, 337 self.version_hash, 338 ) 339 .await 340 { 341 Ok(()) => {} 342 Err(error) => { 343 error!(?error, "Main task returned error, shutting down"); 344 task_group.shutdown(); 345 } 346 } 347 }); 348 349 let shutdown_future = 350 root_task_group 351 .make_handle() 352 .make_shutdown_rx() 353 .await 354 .then(|_| async { 355 info!("Shutdown called"); 356 }); 357 358 shutdown_future.await; 359 debug!("Terminating main task"); 360 361 if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await { 362 error!(?err, "Error while shutting down task group"); 363 } 364 365 info!("Shutdown complete"); 366 367 fedimint_logging::shutdown(); 368 369 drop(timing_total_runtime); 370 371 // Should we ever shut down without an error code? 372 std::process::exit(-1); 373 } 374 375 fn get_server_api_versions(&self) -> ServerApiVersionsSummary { 376 ServerApiVersionsSummary { 377 core: ServerConfig::supported_api_versions().api, 378 modules: self 379 .server_gens 380 .kinds() 381 .into_iter() 382 .map(|module_kind| { 383 self.server_gens 384 .get(&module_kind) 385 .expect("module is present") 386 }) 387 .map(|module_init| { 388 ( 389 module_init.module_kind(), 390 module_init.supported_api_versions().api, 391 ) 392 }) 393 .collect(), 394 } 395 } 396 397 fn get_server_db_versions(&self) -> ServerDbVersionsSummary { 398 ServerDbVersionsSummary { 399 modules: self 400 .server_gens 401 .kinds() 402 .into_iter() 403 .map(|module_kind| { 404 self.server_gens 405 .get(&module_kind) 406 .expect("module is present") 407 }) 408 .map(|module_init| (module_init.module_kind(), module_init.database_version())) 409 .collect(), 410 } 411 } 412 } 413 414 async fn run( 415 opts: ServerOpts, 416 task_group: &TaskGroup, 417 module_inits: ServerModuleInitRegistry, 418 module_inits_params: ServerModuleConfigGenParamsRegistry, 419 version_hash: String, 420 ) -> anyhow::Result<()> { 421 if let Some(socket_addr) = opts.bind_metrics_api.as_ref() { 422 task_group.spawn_cancellable("metrics-server", { 423 let task_group = task_group.clone(); 424 let socket_addr = *socket_addr; 425 async move { fedimint_metrics::run_api_server(socket_addr, task_group).await } 426 }); 427 } 428 429 let data_dir = opts.data_dir.context("data-dir option is not present")?; 430 431 // TODO: Fedimintd should use the config gen API 432 // on each run we want to pass the currently passed password, so we need to 433 // overwrite 434 if let Some(password) = opts.password { 435 write_overwrite(data_dir.join(PLAINTEXT_PASSWORD), password)?; 436 }; 437 let default_params = ConfigGenParamsRequest { 438 meta: opts.extra_dkg_meta.clone(), 439 modules: module_inits_params.clone(), 440 }; 441 // TODO: meh, move, refactor 442 let settings = ConfigGenSettings { 443 download_token_limit: None, 444 p2p_bind: opts.bind_p2p, 445 api_bind: opts.bind_api, 446 p2p_url: opts.p2p_url, 447 api_url: opts.api_url, 448 default_params, 449 max_connections: fedimint_server::config::max_connections(), 450 registry: module_inits.clone(), 451 }; 452 453 let db = Database::new( 454 fedimint_rocksdb::RocksDb::open(data_dir.join(DB_FILE))?, 455 Default::default(), 456 ); 457 458 fedimint_server::run( 459 data_dir, 460 settings, 461 db, 462 version_hash, 463 &module_inits, 464 task_group.clone(), 465 ) 466 .await?; 467 468 Ok(()) 469 }