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