fedimintd/
lib.rs

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
62/// Time we will wait before forcefully shutting down tasks
63const 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    /// Path to folder containing federation config files
77    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
78    data_dir: PathBuf,
79
80    /// The bitcoin network of the federation
81    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
82    bitcoin_network: Network,
83
84    /// Bitcoind RPC URL, e.g. <http://user:pass@127.0.0.1:8332>
85    #[arg(long, env = FM_BITCOIND_URL_ENV)]
86    bitcoind_url: Option<SafeUrl>,
87
88    /// Esplora HTTP base URL, e.g. <https://mempool.space/api>
89    #[arg(long, env = FM_ESPLORA_URL_ENV)]
90    esplora_url: Option<SafeUrl>,
91
92    /// Address we bind to for p2p consensus communication
93    ///
94    /// Should be `0.0.0.0:8173` most of the time, as p2p connectivity is public
95    /// and direct, and the port should be open it in the firewall.
96    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
97    bind_p2p: SocketAddr,
98
99    /// Address we bind to for the API
100    ///
101    /// Should be `0.0.0.0:8174` most of the time, as api connectivity is public
102    /// and direct, and the port should be open it in the firewall.
103    #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
104    bind_api: SocketAddr,
105
106    /// Address we bind to for exposing the Web UI
107    ///
108    /// Built-in web UI is exposed as an HTTP port, and typically should
109    /// have TLS terminated by Nginx/Traefik/etc. and forwarded to the locally
110    /// bind port.
111    #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
112    bind_ui: SocketAddr,
113
114    /// Our external address for communicating with our peers
115    ///
116    /// `fedimint://<fqdn>:8173` for TCP/TLS p2p connectivity (legacy/standard).
117    ///
118    /// Ignored when Iroh stack is used. (newer/experimental)
119    #[arg(long, env = FM_P2P_URL_ENV)]
120    p2p_url: Option<SafeUrl>,
121
122    /// Our API address for clients to connect to us
123    ///
124    /// Typically `wss://<fqdn>/ws/` for TCP/TLS connectivity (legacy/standard)
125    ///
126    /// Ignored when Iroh stack is used. (newer/experimental)
127    #[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    /// Optional URL of the Iroh DNS server
134    #[arg(long, env = "FM_IROH_DNS", requires = "enable_iroh")]
135    iroh_dns: Option<SafeUrl>,
136
137    /// Optional URLs of the Iroh relays to use for registering
138    #[arg(long, env = "FM_IROH_RELAY", requires = "enable_iroh")]
139    iroh_relays: Vec<SafeUrl>,
140
141    /// Number of checkpoints from the current session to retain on disk
142    #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
143    db_checkpoint_retention: u64,
144
145    /// Enable tokio console logging
146    #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
147    bind_tokio_console: Option<SocketAddr>,
148
149    /// Enable jaeger for tokio console logging
150    #[arg(long, default_value = "false")]
151    with_jaeger: bool,
152
153    /// Enable prometheus metrics
154    #[arg(long, env = FM_BIND_METRCIS_ENV)]
155    bind_metrics: Option<SocketAddr>,
156
157    /// Comma separated list of API secrets.
158    ///
159    /// Setting it will enforce API authentication and make the Federation
160    /// "private".
161    ///
162    /// The first secret in the list is the "active" one that the peer will use
163    /// itself to connect to other peers. Any further one is accepted by
164    /// this peer, e.g. for the purposes of smooth rotation of secret
165    /// between users.
166    ///
167    /// Note that the value provided here will override any other settings
168    /// that the user might want to set via UI at runtime, etc.
169    /// In the future, managing secrets might be possible via Admin UI
170    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
171    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
172    force_api_secrets: ApiSecrets,
173}
174
175/// Block the thread and run a Fedimintd server
176///
177/// # Arguments
178///
179/// * `modules_fn` - A function to initialize the modules.
180///
181/// * `code_version_hash` - The git hash of the code that the `fedimintd` binary
182///   is being built from. This is used mostly for information purposes
183///   (`fedimintd version-hash`). See `fedimint-build` crate for easy way to
184///   obtain it.
185///
186/// * `code_version_vendor_suffix` - An optional suffix that will be appended to
187///   the internal fedimint release version, to distinguish binaries built by
188///   different vendors, usually with a different set of modules. Currently DKG
189///   will enforce that the combined `code_version` is the same between all
190///   peers.
191pub 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    // Should we ever shut down without an error code?
316    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                // TODO: wait for clients to support the relative fees and set them to
352                // non-zero in 0.6
353                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                    // TODO: actually make the relative fee configurable
390                    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
411/// For real Bitcoin we want to have a responsible default, while for demos
412/// nobody wants to wait multiple minutes
413fn 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}