fedimintd/
fedimintd.rs

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
56/// Time we will wait before forcefully shutting down tasks
57const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
58
59#[derive(Parser)]
60#[command(version)]
61struct ServerOpts {
62    /// Path to folder containing federation config files
63    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
64    data_dir: Option<PathBuf>,
65    /// Password to encrypt sensitive config files
66    // TODO: should probably never send password to the server directly, rather send the hash via
67    // the API
68    #[arg(long, env = FM_PASSWORD_ENV)]
69    password: Option<String>,
70    /// Enable tokio console logging
71    #[arg(long, env = FM_TOKIO_CONSOLE_BIND_ENV)]
72    tokio_console_bind: Option<SocketAddr>,
73    /// Enable telemetry logging
74    #[arg(long, default_value = "false")]
75    with_telemetry: bool,
76
77    /// Address we bind to for federation communication
78    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "127.0.0.1:8173")]
79    bind_p2p: SocketAddr,
80    /// Our external address for communicating with our peers
81    #[arg(long, env = FM_P2P_URL_ENV, default_value = "fedimint://127.0.0.1:8173")]
82    p2p_url: SafeUrl,
83    /// Address we bind to for exposing the API
84    #[arg(long, env = FM_BIND_API_ENV, default_value = "127.0.0.1:8174")]
85    bind_api: SocketAddr,
86    /// Our API address for clients to connect to us
87    #[arg(long, env = FM_API_URL_ENV, default_value = "ws://127.0.0.1:8174")]
88    api_url: SafeUrl,
89    /// The bitcoin network that fedimint will be running on
90    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
91    network: bitcoin::network::Network,
92    /// The number of blocks the federation stays behind the blockchain tip
93    #[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    /// List of default meta values to use during config generation (format:
100    /// `key1=value1,key2=value,...`)
101    #[arg(long, env = FM_EXTRA_DKG_META_ENV, value_parser = parse_map, default_value="")]
102    extra_dkg_meta: BTreeMap<String, String>,
103
104    /// Comma separated list of API secrets.
105    ///
106    /// Setting it will enforce API authentication and make the Federation
107    /// "private".
108    ///
109    /// The first secret in the list is the "active" one that the peer will use
110    /// itself to connect to other peers. Any further one is accepted by
111    /// this peer, e.g. for the purposes of smooth rotation of secret
112    /// between users.
113    ///
114    /// Note that the value provided here will override any other settings
115    /// that the user might want to set via UI at runtime, etc.
116    /// In the future, managing secrets might be possible via Admin UI
117    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
118    #[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    /// Development-related commands
128    #[clap(subcommand)]
129    Dev(DevSubcommand),
130}
131
132#[derive(Subcommand)]
133enum DevSubcommand {
134    /// List supported server API versions and exit
135    ListApiVersions,
136    /// List supported server database versions and exit
137    ListDbVersions,
138}
139
140/// Parse a key-value map from a string.
141///
142/// The string should be a comma-separated list of key-value pairs, where each
143/// pair is separated by an equals sign. For example, `key1=value1,key2=value2`.
144/// The keys and values are trimmed of whitespace, so
145/// `key1 = value1, key2 = value2` would be parsed the same as the previous
146/// example.
147fn 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
177/// `fedimintd` builder
178///
179/// Fedimint supports third party modules. Right now (and for forseable feature)
180/// modules needs to be combined with the rest of the code at compilation time.
181///
182/// To make this easier, [`Fedimintd`] builder is exposed, allowing
183/// building `fedimintd` with custom set of modules.
184///
185///
186/// Example:
187///
188/// ```
189/// use fedimint_ln_server::LightningInit;
190/// use fedimint_mint_server::MintInit;
191/// use fedimint_wallet_server::WalletInit;
192/// use fedimintd::Fedimintd;
193///
194/// // Note: not called `main` to avoid rustdoc executing it
195/// // #[tokio::main]
196/// async fn main_() -> anyhow::Result<()> {
197///     Fedimintd::new(env!("FEDIMINT_BUILD_CODE_VERSION"), Some("vendor-xyz-1"))?
198///         // use `.with_default_modules()` to avoid having
199///         // to import these manually
200///         .with_module_kind(WalletInit)
201///         .with_module_kind(MintInit)
202///         .with_module_kind(LightningInit)
203///         .run()
204///         .await
205/// }
206/// ```
207pub 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    /// Builds a new `fedimintd`
218    ///
219    /// # Arguments
220    ///
221    /// * `code_version_hash` - The git hash of the code that the `fedimintd`
222    ///   binary is being built from. This is used mostly for information
223    ///   purposes (`fedimintd version-hash`). See `fedimint-build` crate for
224    ///   easy way to obtain it.
225    ///
226    /// * `code_version_vendor_suffix` - An optional suffix that will be
227    ///   appended to the internal fedimint release version, to distinguish
228    ///   binaries built by different vendors, usually with a different set of
229    ///   modules. Currently DKG will enforce that the combined `code_version`
230    ///   is the same between all peers.
231    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    /// Attach a server module kind to the Fedimintd instance
275    ///
276    /// This makes `fedimintd` support additional module types (aka. kinds)
277    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    /// Get the version hash this `fedimintd` will report for diagnostic
286    /// purposes
287    pub fn version_hash(&self) -> &str {
288        &self.code_version_hash
289    }
290
291    /// Attach additional module instance with parameters
292    ///
293    /// Note: The `kind` needs to be added with [`Self::with_module_kind`] if
294    /// it's not the default one.
295    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    /// Attach default server modules to Fedimintd instance
305    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                        // TODO: wait for clients to support the relative fees and set them to
329                        // non-zero in 0.6
330                        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                        // TODO this is not very elegant, but I'm planning to get rid of it in a
344                        // next commit anyway
345                        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                            // TODO: actually make the relative fee configurable
363                            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    /// Block thread and run a Fedimintd server
390    pub async fn run(self) -> ! {
391        // handle optional subcommand
392        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        // Should we ever shut down without an error code?
456        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    // TODO: Fedimintd should use the config gen API
521    // on each run we want to pass the currently passed password, so we need to
522    // overwrite
523    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    // TODO: meh, move, refactor
529    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}