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 anyhow::Context as _;
18use bitcoin::Network;
19use clap::{ArgGroup, Parser};
20use envs::FM_BITCOIND_URL_PASSWORD_FILE_ENV;
21use fedimint_core::config::{EmptyGenParams, ServerModuleConfigGenParamsRegistry};
22use fedimint_core::db::Database;
23use fedimint_core::envs::{
24    BitcoinRpcConfig, FM_ENABLE_MODULE_LNV2_ENV, FM_IROH_DNS_ENV, FM_IROH_RELAY_ENV,
25    FM_USE_UNKNOWN_MODULE_ENV, is_env_var_set,
26};
27use fedimint_core::module::registry::ModuleRegistry;
28use fedimint_core::task::TaskGroup;
29use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl, handle_version_hash_command};
30use fedimint_core::{default_esplora_server, timing};
31use fedimint_ln_common::config::{
32    LightningGenParams, LightningGenParamsConsensus, LightningGenParamsLocal,
33};
34use fedimint_ln_server::LightningInit;
35use fedimint_logging::{LOG_CORE, TracingSetup};
36use fedimint_meta_server::{MetaGenParams, MetaInit};
37use fedimint_mint_server::MintInit;
38use fedimint_mint_server::common::config::{MintGenParams, MintGenParamsConsensus};
39use fedimint_rocksdb::RocksDb;
40use fedimint_server::config::ConfigGenSettings;
41use fedimint_server::config::io::DB_FILE;
42use fedimint_server::core::{ServerModuleInit, ServerModuleInitRegistry};
43use fedimint_server::net::api::ApiSecrets;
44use fedimint_server_bitcoin_rpc::BitcoindClientWithFallback;
45use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
46use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
47use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
48use fedimint_unknown_common::config::UnknownGenParams;
49use fedimint_unknown_server::UnknownInit;
50use fedimint_wallet_server::WalletInit;
51use fedimint_wallet_server::common::config::{
52    WalletGenParams, WalletGenParamsConsensus, WalletGenParamsLocal,
53};
54use futures::FutureExt as _;
55use tracing::{debug, error, info};
56
57use crate::envs::{
58    FM_API_URL_ENV, FM_BIND_API_ENV, FM_BIND_METRCIS_ENV, FM_BIND_P2P_ENV,
59    FM_BIND_TOKIO_CONSOLE_ENV, FM_BIND_UI_ENV, FM_BITCOIN_NETWORK_ENV, FM_BITCOIND_URL_ENV,
60    FM_DATA_DIR_ENV, FM_DB_CHECKPOINT_RETENTION_ENV, FM_DISABLE_META_MODULE_ENV,
61    FM_ENABLE_IROH_ENV, FM_ESPLORA_URL_ENV, FM_FORCE_API_SECRETS_ENV,
62    FM_IROH_API_MAX_CONNECTIONS_ENV, FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, FM_P2P_URL_ENV,
63    FM_PORT_ESPLORA_ENV,
64};
65use crate::metrics::APP_START_TS;
66
67/// Time we will wait before forcefully shutting down tasks
68const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
69
70#[derive(Parser)]
71#[command(version)]
72#[command(
73    group(
74        ArgGroup::new("bitcoin_rpc")
75            .required(true)
76            .multiple(true)
77            .args(["bitcoind_url", "esplora_url"])
78    )
79)]
80struct ServerOpts {
81    /// Path to folder containing federation config files
82    #[arg(long = "data-dir", env = FM_DATA_DIR_ENV)]
83    data_dir: PathBuf,
84
85    /// The bitcoin network of the federation
86    #[arg(long, env = FM_BITCOIN_NETWORK_ENV, default_value = "regtest")]
87    bitcoin_network: Network,
88
89    /// Bitcoind RPC URL, e.g. <http://user:pass@127.0.0.1:8332>
90    #[arg(long, env = FM_BITCOIND_URL_ENV)]
91    bitcoind_url: Option<SafeUrl>,
92
93    /// If set, the password part of `--bitcoind-url` will be set/replaced with
94    /// the content of this file.
95    ///
96    /// This is useful for setups that provide secret material via ramdisk
97    /// e.g. SOPS, age, etc.
98    ///
99    /// Note this is not meant to handle bitcoind's cookie file.
100    #[arg(long, env = FM_BITCOIND_URL_PASSWORD_FILE_ENV)]
101    bitcoind_url_password_file: Option<PathBuf>,
102
103    /// Esplora HTTP base URL, e.g. <https://mempool.space/api>
104    #[arg(long, env = FM_ESPLORA_URL_ENV)]
105    esplora_url: Option<SafeUrl>,
106
107    /// Address we bind to for p2p consensus communication
108    ///
109    /// Should be `0.0.0.0:8173` most of the time, as p2p connectivity is public
110    /// and direct, and the port should be open it in the firewall.
111    #[arg(long, env = FM_BIND_P2P_ENV, default_value = "0.0.0.0:8173")]
112    bind_p2p: SocketAddr,
113
114    /// Address we bind to for the API
115    ///
116    /// Should be `0.0.0.0:8174` most of the time, as api connectivity is public
117    /// and direct, and the port should be open it in the firewall.
118    #[arg(long, env = FM_BIND_API_ENV, default_value = "0.0.0.0:8174")]
119    bind_api: SocketAddr,
120
121    /// Address we bind to for exposing the Web UI
122    ///
123    /// Built-in web UI is exposed as an HTTP port, and typically should
124    /// have TLS terminated by Nginx/Traefik/etc. and forwarded to the locally
125    /// bind port.
126    #[arg(long, env = FM_BIND_UI_ENV, default_value = "127.0.0.1:8175")]
127    bind_ui: SocketAddr,
128
129    /// Our external address for communicating with our peers
130    ///
131    /// `fedimint://<fqdn>:8173` for TCP/TLS p2p connectivity (legacy/standard).
132    ///
133    /// Ignored when Iroh stack is used. (newer/experimental)
134    #[arg(long, env = FM_P2P_URL_ENV)]
135    p2p_url: Option<SafeUrl>,
136
137    /// Our API address for clients to connect to us
138    ///
139    /// Typically `wss://<fqdn>/ws/` for TCP/TLS connectivity (legacy/standard)
140    ///
141    /// Ignored when Iroh stack is used. (newer/experimental)
142    #[arg(long, env = FM_API_URL_ENV)]
143    api_url: Option<SafeUrl>,
144
145    #[arg(long, env = FM_ENABLE_IROH_ENV)]
146    enable_iroh: bool,
147
148    /// Optional URL of the Iroh DNS server
149    #[arg(long, env = FM_IROH_DNS_ENV, requires = "enable_iroh")]
150    iroh_dns: Option<SafeUrl>,
151
152    /// Optional URLs of the Iroh relays to use for registering
153    #[arg(long, env = FM_IROH_RELAY_ENV, requires = "enable_iroh")]
154    iroh_relays: Vec<SafeUrl>,
155
156    /// Number of checkpoints from the current session to retain on disk
157    #[arg(long, env = FM_DB_CHECKPOINT_RETENTION_ENV, default_value = "1")]
158    db_checkpoint_retention: u64,
159
160    /// Enable tokio console logging
161    #[arg(long, env = FM_BIND_TOKIO_CONSOLE_ENV)]
162    bind_tokio_console: Option<SocketAddr>,
163
164    /// Enable jaeger for tokio console logging
165    #[arg(long, default_value = "false")]
166    with_jaeger: bool,
167
168    /// Enable prometheus metrics
169    #[arg(long, env = FM_BIND_METRCIS_ENV)]
170    bind_metrics: Option<SocketAddr>,
171
172    /// Comma separated list of API secrets.
173    ///
174    /// Setting it will enforce API authentication and make the Federation
175    /// "private".
176    ///
177    /// The first secret in the list is the "active" one that the peer will use
178    /// itself to connect to other peers. Any further one is accepted by
179    /// this peer, e.g. for the purposes of smooth rotation of secret
180    /// between users.
181    ///
182    /// Note that the value provided here will override any other settings
183    /// that the user might want to set via UI at runtime, etc.
184    /// In the future, managing secrets might be possible via Admin UI
185    /// and defaults will be provided via `FM_DEFAULT_API_SECRETS`.
186    #[arg(long, env = FM_FORCE_API_SECRETS_ENV, default_value = "")]
187    force_api_secrets: ApiSecrets,
188
189    /// Maximum number of concurrent Iroh API connections
190    #[arg(long = "iroh-api-max-connections", env = FM_IROH_API_MAX_CONNECTIONS_ENV, default_value = "1000")]
191    iroh_api_max_connections: usize,
192
193    /// Maximum number of parallel requests per Iroh API connection
194    #[arg(long = "iroh-api-max-requests-per-connection", env = FM_IROH_API_MAX_REQUESTS_PER_CONNECTION_ENV, default_value = "50")]
195    iroh_api_max_requests_per_connection: usize,
196}
197
198impl ServerOpts {
199    pub async fn get_bitcoind_url(&self) -> anyhow::Result<SafeUrl> {
200        let mut url = self
201            .bitcoind_url
202            .clone()
203            .ok_or_else(|| anyhow::anyhow!("No bitcoind url set"))?;
204        if let Some(password_file) = self.bitcoind_url_password_file.as_ref() {
205            let password = tokio::fs::read_to_string(password_file)
206                .await
207                .context("Failed to read the password")?
208                .trim()
209                .to_owned();
210            url.set_password(Some(&password))
211                .ok()
212                .ok_or_else(|| anyhow::anyhow!("Failed to set the password from the url"))?;
213        }
214
215        Ok(url)
216    }
217}
218
219/// Block the thread and run a Fedimintd server
220///
221/// # Arguments
222///
223/// * `modules_fn` - A function to initialize the modules.
224///
225/// * `code_version_hash` - The git hash of the code that the `fedimintd` binary
226///   is being built from. This is used mostly for information purposes
227///   (`fedimintd version-hash`). See `fedimint-build` crate for easy way to
228///   obtain it.
229///
230/// * `code_version_vendor_suffix` - An optional suffix that will be appended to
231///   the internal fedimint release version, to distinguish binaries built by
232///   different vendors, usually with a different set of modules. Currently DKG
233///   will enforce that the combined `code_version` is the same between all
234///   peers.
235#[allow(clippy::too_many_lines)]
236pub async fn run(
237    modules_fn: fn(
238        Network,
239    ) -> (
240        ServerModuleInitRegistry,
241        ServerModuleConfigGenParamsRegistry,
242    ),
243    code_version_hash: &str,
244    code_version_vendor_suffix: Option<&str>,
245) -> ! {
246    assert_eq!(
247        env!("FEDIMINT_BUILD_CODE_VERSION").len(),
248        code_version_hash.len(),
249        "version_hash must have an expected length"
250    );
251
252    handle_version_hash_command(code_version_hash);
253
254    let fedimint_version = env!("CARGO_PKG_VERSION");
255
256    APP_START_TS
257        .with_label_values(&[fedimint_version, code_version_hash])
258        .set(fedimint_core::time::duration_since_epoch().as_secs() as i64);
259
260    let server_opts = ServerOpts::parse();
261
262    let mut tracing_builder = TracingSetup::default();
263
264    tracing_builder
265        .tokio_console_bind(server_opts.bind_tokio_console)
266        .with_jaeger(server_opts.with_jaeger);
267
268    tracing_builder.init().unwrap();
269
270    info!("Starting fedimintd (version: {fedimint_version} version_hash: {code_version_hash})");
271
272    let code_version_str = code_version_vendor_suffix.map_or_else(
273        || fedimint_version.to_string(),
274        |suffix| format!("{fedimint_version}+{suffix}"),
275    );
276
277    let (server_gens, server_gen_params) = modules_fn(server_opts.bitcoin_network);
278
279    let timing_total_runtime = timing::TimeReporter::new("total-runtime").info();
280
281    let root_task_group = TaskGroup::new();
282
283    if let Some(bind_metrics) = server_opts.bind_metrics.as_ref() {
284        root_task_group.spawn_cancellable(
285            "metrics-server",
286            fedimint_metrics::run_api_server(*bind_metrics, root_task_group.clone()),
287        );
288    }
289
290    let settings = ConfigGenSettings {
291        p2p_bind: server_opts.bind_p2p,
292        api_bind: server_opts.bind_api,
293        ui_bind: server_opts.bind_ui,
294        p2p_url: server_opts.p2p_url.clone(),
295        api_url: server_opts.api_url.clone(),
296        enable_iroh: server_opts.enable_iroh,
297        iroh_dns: server_opts.iroh_dns.clone(),
298        iroh_relays: server_opts.iroh_relays.clone(),
299        modules: server_gen_params.clone(),
300        registry: server_gens.clone(),
301    };
302
303    let db = Database::new(
304        RocksDb::open(server_opts.data_dir.join(DB_FILE))
305            .await
306            .unwrap(),
307        ModuleRegistry::default(),
308    );
309
310    let dyn_server_bitcoin_rpc = match (
311        server_opts.bitcoind_url.as_ref(),
312        server_opts.esplora_url.as_ref(),
313    ) {
314        (Some(_), None) => BitcoindClient::new(
315            &server_opts
316                .get_bitcoind_url()
317                .await
318                .expect("Failed to get bitcoind url"),
319        )
320        .unwrap()
321        .into_dyn(),
322        (None, Some(url)) => EsploraClient::new(url).unwrap().into_dyn(),
323        (Some(_), Some(esplora_url)) => BitcoindClientWithFallback::new(
324            &server_opts
325                .get_bitcoind_url()
326                .await
327                .expect("Failed to get bitcoind url"),
328            esplora_url,
329        )
330        .unwrap()
331        .into_dyn(),
332        _ => unreachable!("ArgGroup already enforced XOR relation"),
333    };
334
335    root_task_group.install_kill_handler();
336
337    let task_group = root_task_group.clone();
338    root_task_group.spawn_cancellable("main", async move {
339        fedimint_server::run(
340            server_opts.data_dir,
341            server_opts.force_api_secrets,
342            settings,
343            db,
344            code_version_str,
345            server_gens,
346            task_group,
347            dyn_server_bitcoin_rpc,
348            Box::new(fedimint_server_ui::setup::router),
349            Box::new(fedimint_server_ui::dashboard::router),
350            server_opts.db_checkpoint_retention,
351            fedimint_server::ConnectionLimits::new(
352                server_opts.iroh_api_max_connections,
353                server_opts.iroh_api_max_requests_per_connection,
354            ),
355        )
356        .await
357        .unwrap_or_else(|err| panic!("Main task returned error: {}", err.fmt_compact_anyhow()));
358    });
359
360    let shutdown_future = root_task_group
361        .make_handle()
362        .make_shutdown_rx()
363        .then(|()| async {
364            info!(target: LOG_CORE, "Shutdown called");
365        });
366
367    shutdown_future.await;
368    debug!(target: LOG_CORE, "Terminating main task");
369
370    if let Err(err) = root_task_group.join_all(Some(SHUTDOWN_TIMEOUT)).await {
371        error!(target: LOG_CORE, ?err, "Error while shutting down task group");
372    }
373
374    debug!(target: LOG_CORE, "Shutdown complete");
375
376    fedimint_logging::shutdown();
377
378    drop(timing_total_runtime);
379
380    // Should we ever shut down without an error code?
381    std::process::exit(-1);
382}
383
384pub fn default_modules(
385    network: Network,
386) -> (
387    ServerModuleInitRegistry,
388    ServerModuleConfigGenParamsRegistry,
389) {
390    let mut server_gens = ServerModuleInitRegistry::new();
391    let mut server_gen_params = ServerModuleConfigGenParamsRegistry::default();
392
393    let bitcoin_rpc_config = BitcoinRpcConfig {
394        kind: "bitcoind".to_string(),
395        url: "http://unused_dummy.xyz".parse().unwrap(),
396    };
397
398    server_gens.attach(LightningInit);
399    server_gen_params.attach_config_gen_params(
400        LightningInit::kind(),
401        LightningGenParams {
402            local: LightningGenParamsLocal {
403                bitcoin_rpc: bitcoin_rpc_config.clone(),
404            },
405            consensus: LightningGenParamsConsensus { network },
406        },
407    );
408
409    server_gens.attach(MintInit);
410    server_gen_params.attach_config_gen_params(
411        MintInit::kind(),
412        MintGenParams {
413            local: EmptyGenParams::default(),
414            consensus: MintGenParamsConsensus::new(
415                2,
416                // TODO: wait for clients to support the relative fees and set them to
417                // non-zero in 0.6
418                fedimint_mint_common::config::FeeConsensus::zero(),
419            ),
420        },
421    );
422
423    server_gens.attach(WalletInit);
424    server_gen_params.attach_config_gen_params(
425        WalletInit::kind(),
426        WalletGenParams {
427            local: WalletGenParamsLocal {
428                bitcoin_rpc: bitcoin_rpc_config.clone(),
429            },
430            consensus: WalletGenParamsConsensus {
431                network,
432                finality_delay: default_finality_delay(network),
433                client_default_bitcoin_rpc: default_esplora_server(
434                    network,
435                    std::env::var(FM_PORT_ESPLORA_ENV).ok(),
436                ),
437                fee_consensus: fedimint_wallet_server::common::config::FeeConsensus::default(),
438            },
439        },
440    );
441
442    let enable_lnv2 = std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
443        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV);
444
445    if enable_lnv2 {
446        server_gens.attach(fedimint_lnv2_server::LightningInit);
447        server_gen_params.attach_config_gen_params(
448            fedimint_lnv2_server::LightningInit::kind(),
449            fedimint_lnv2_common::config::LightningGenParams {
450                local: fedimint_lnv2_common::config::LightningGenParamsLocal {
451                    bitcoin_rpc: bitcoin_rpc_config.clone(),
452                },
453                consensus: fedimint_lnv2_common::config::LightningGenParamsConsensus {
454                    // TODO: actually make the relative fee configurable
455                    fee_consensus: fedimint_lnv2_common::config::FeeConsensus::new(100).unwrap(),
456                    network,
457                },
458            },
459        );
460    }
461
462    if !is_env_var_set(FM_DISABLE_META_MODULE_ENV) {
463        server_gens.attach(MetaInit);
464        server_gen_params.attach_config_gen_params(MetaInit::kind(), MetaGenParams::default());
465    }
466
467    if is_env_var_set(FM_USE_UNKNOWN_MODULE_ENV) {
468        server_gens.attach(UnknownInit);
469        server_gen_params
470            .attach_config_gen_params(UnknownInit::kind(), UnknownGenParams::default());
471    }
472
473    (server_gens, server_gen_params)
474}
475
476/// For real Bitcoin we want to have a responsible default, while for demos
477/// nobody wants to wait multiple minutes
478fn default_finality_delay(network: Network) -> u32 {
479    match network {
480        Network::Bitcoin | Network::Regtest => 10,
481        Network::Testnet | Network::Signet | Network::Testnet4 => 2,
482        _ => panic!("Unsupported network"),
483    }
484}