/ fedimintd / src / fedimintd.rs
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  }