Skip to main content

fedimint_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::needless_lifetimes)]
12#![allow(clippy::ref_option)]
13#![allow(clippy::return_self_not_must_use)]
14#![allow(clippy::similar_names)]
15#![allow(clippy::too_many_lines)]
16#![allow(clippy::needless_pass_by_value)]
17#![allow(clippy::manual_let_else)]
18#![allow(clippy::match_wildcard_for_single_variants)]
19#![allow(clippy::trivially_copy_pass_by_ref)]
20
21//! Server side fedimint module traits
22
23extern crate fedimint_core;
24pub mod connection_limits;
25pub mod db;
26
27use std::path::{Path, PathBuf};
28use std::time::Duration;
29
30use anyhow::{Context, ensure};
31use bitcoin::hashes::hex::FromHex as _;
32use config::ServerConfig;
33use config::io::read_server_config;
34pub use connection_limits::ConnectionLimits;
35use fedimint_connectors::ConnectorRegistry;
36use fedimint_core::config::P2PMessage;
37use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
38use fedimint_core::epoch::ConsensusItem;
39use fedimint_core::module::ApiAuth;
40use fedimint_core::net::peers::DynP2PConnections;
41use fedimint_core::task::{TaskGroup, sleep};
42use fedimint_logging::LOG_CONSENSUS;
43pub use fedimint_server_core as core;
44use fedimint_server_core::ServerModuleInitRegistry;
45use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
46use fedimint_server_core::dashboard_ui::DynDashboardApi;
47use fedimint_server_core::setup_ui::{DynSetupApi, ISetupApi};
48use jsonrpsee::RpcModule;
49use net::api::ApiSecrets;
50use net::p2p::P2PStatusReceivers;
51use net::p2p_connector::IrohConnector;
52use tokio::net::TcpListener;
53use tokio_rustls::rustls;
54use tracing::info;
55
56use crate::config::ConfigGenSettings;
57use crate::config::io::write_server_config;
58use crate::config::setup::{ConfigGenOutcome, SetupApi};
59use crate::db::{ServerInfo, ServerInfoKey};
60use crate::fedimint_core::net::peers::IP2PConnections;
61use crate::metrics::initialize_gauge_metrics;
62use crate::net::api::announcement::start_api_announcement_service;
63use crate::net::api::guardian_metadata::start_guardian_metadata_service;
64use crate::net::api::pkarr_publish::start_pkarr_publish_service;
65use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
66use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
67
68pub mod metrics;
69
70/// The actual implementation of consensus
71pub mod consensus;
72
73/// Networking for mint-to-mint and client-to-mint communiccation
74pub mod net;
75
76/// Fedimint toplevel config
77pub mod config;
78
79/// A function/closure type for handling dashboard UI
80pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
81
82/// A function/closure type for handling setup UI
83pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
84
85#[allow(clippy::too_many_arguments)]
86pub async fn run(
87    data_dir: PathBuf,
88    auth_ui: Option<ApiAuth>,
89    auth_api: Option<ApiAuth>,
90    force_api_secrets: ApiSecrets,
91    settings: ConfigGenSettings,
92    db: Database,
93    code_version_str: String,
94    code_version_hash: String,
95    module_init_registry: ServerModuleInitRegistry,
96    task_group: TaskGroup,
97    bitcoin_rpc: DynServerBitcoinRpc,
98    setup_ui_router: SetupUiRouter,
99    dashboard_ui_router: DashboardUiRouter,
100    db_checkpoint_retention: u64,
101    session_timeout: Duration,
102    iroh_api_limits: ConnectionLimits,
103) -> anyhow::Result<()> {
104    let (cfg, connections, p2p_status_receivers) = match get_config(&data_dir)? {
105        Some(cfg) => {
106            let connector = if cfg.consensus.iroh_endpoints.is_empty() {
107                TlsTcpConnector::new(
108                    cfg.tls_config(),
109                    settings.p2p_bind,
110                    cfg.local.p2p_endpoints.clone(),
111                    cfg.local.identity,
112                )
113                .await
114                .into_dyn()
115            } else {
116                IrohConnector::new(
117                    cfg.private.iroh_p2p_sk.clone().unwrap(),
118                    settings.p2p_bind,
119                    settings.iroh_dns.clone(),
120                    settings.iroh_relays.clone(),
121                    cfg.consensus
122                        .iroh_endpoints
123                        .iter()
124                        .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
125                        .collect(),
126                )
127                .await?
128                .into_dyn()
129            };
130
131            let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
132
133            let connections = ReconnectP2PConnections::new(
134                cfg.local.identity,
135                connector,
136                &task_group,
137                p2p_status_senders,
138            )
139            .into_dyn();
140
141            (cfg, connections, p2p_status_receivers)
142        }
143        None => {
144            Box::pin(run_config_gen(
145                data_dir.clone(),
146                settings.clone(),
147                db.clone(),
148                &task_group,
149                code_version_str.clone(),
150                code_version_hash.clone(),
151                force_api_secrets.clone(),
152                setup_ui_router,
153                module_init_registry.clone(),
154                auth_ui.clone(),
155                auth_api.clone(),
156            ))
157            .await?
158        }
159    };
160
161    let decoders = module_init_registry.decoders_strict(
162        cfg.consensus
163            .modules
164            .iter()
165            .map(|(id, config)| (*id, &config.kind)),
166    )?;
167
168    let db = db.with_decoders(decoders);
169
170    initialize_gauge_metrics(&task_group, &db).await;
171
172    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
173    start_guardian_metadata_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
174    start_pkarr_publish_service(&db, &task_group, &cfg).await?;
175
176    info!(target: LOG_CONSENSUS, "Starting consensus...");
177
178    let connectors = ConnectorRegistry::build_from_server_defaults()
179        .bind()
180        .await?;
181
182    Box::pin(consensus::run(
183        connectors,
184        auth_ui,
185        auth_api,
186        connections,
187        p2p_status_receivers,
188        settings.api_bind,
189        settings.iroh_dns,
190        settings.iroh_relays,
191        cfg,
192        db,
193        module_init_registry.clone(),
194        &task_group,
195        force_api_secrets,
196        data_dir,
197        code_version_str,
198        code_version_hash,
199        bitcoin_rpc,
200        settings.ui_bind,
201        dashboard_ui_router,
202        db_checkpoint_retention,
203        session_timeout,
204        iroh_api_limits,
205    ))
206    .await?;
207
208    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
209
210    task_group.shutdown();
211
212    Ok(())
213}
214
215async fn update_server_info_version_dbtx(
216    dbtx: &mut DatabaseTransaction<'_>,
217    code_version_str: &str,
218) {
219    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
220        init_version: code_version_str.to_string(),
221        last_version: code_version_str.to_string(),
222    });
223    server_info.last_version = code_version_str.to_string();
224    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
225}
226
227pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
228    if !data_dir.join("consensus.json").exists() {
229        return Ok(None);
230    }
231
232    read_server_config(data_dir).map(Some)
233}
234
235/// Validate restored TCP transport material before building `TlsTcpConnector`.
236///
237/// `ServerConfig::tls_config()` and `TlsTcpConnector::new()` contain invariant
238/// checks that are fine for generated configs but too panic-prone for uploaded
239/// restore data. This preflights the same key/certificate material and returns
240/// a normal restore error before any files are installed.
241///
242/// Keep this in sync with the panic points in `ServerConfig::tls_config()`:
243/// missing `private.tls_key`, malformed TLS private-key hex, malformed TLS
244/// certificate hex, and malformed TLS private-key DER.
245fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
246    let tls_key = cfg
247        .private
248        .tls_key
249        .as_ref()
250        .context("Restored TCP config is missing the TLS private key")?;
251    let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
252    rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
253        .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
254
255    ensure!(
256        cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
257        "Restored TCP config is missing our TLS certificate"
258    );
259    for (peer, cert) in &cfg.consensus.tls_certs {
260        Vec::from_hex(cert)
261            .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
262    }
263
264    let tls_config = cfg.tls_config();
265    let mut root_cert_store = rustls::RootCertStore::empty();
266    for cert in tls_config.certificates.values() {
267        root_cert_store
268            .add(cert.clone())
269            .context("Adding restored TLS certificate to root store")?;
270    }
271    let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
272        .build()
273        .context("Creating restored TLS client verifier")?;
274    let certificate = tls_config
275        .certificates
276        .get(&cfg.local.identity)
277        .context("Restored TCP config is missing our TLS certificate")?
278        .clone();
279    rustls::ServerConfig::builder()
280        .with_client_cert_verifier(verifier)
281        .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
282        .context("Creating restored TLS server config")?;
283
284    Ok(())
285}
286
287/// Validate restored Iroh transport keys and return the p2p key for connector
288/// setup.
289///
290/// Restore data must contain both API and p2p secret keys, and both must match
291/// this guardian's public keys in the restored consensus endpoints before the
292/// config is installed.
293fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
294    let iroh_p2p_sk = cfg
295        .private
296        .iroh_p2p_sk
297        .clone()
298        .context("Restored Iroh config is missing the Iroh p2p secret key")?;
299    let local_endpoints = cfg
300        .consensus
301        .iroh_endpoints
302        .get(&cfg.local.identity)
303        .context("Restored Iroh config is missing our Iroh endpoints")?;
304    ensure!(
305        iroh_p2p_sk.public() == local_endpoints.p2p_pk,
306        "Restored Iroh p2p secret key does not match our Iroh endpoint"
307    );
308
309    let iroh_api_sk = cfg
310        .private
311        .iroh_api_sk
312        .clone()
313        .context("Restored Iroh config is missing the Iroh api secret key")?;
314    ensure!(
315        iroh_api_sk.public() == local_endpoints.api_pk,
316        "Restored Iroh api secret key does not match our Iroh endpoint"
317    );
318
319    Ok(iroh_p2p_sk)
320}
321
322#[allow(clippy::too_many_arguments)]
323pub async fn run_config_gen(
324    data_dir: PathBuf,
325    settings: ConfigGenSettings,
326    db: Database,
327    task_group: &TaskGroup,
328    code_version_str: String,
329    code_version_hash: String,
330    api_secrets: ApiSecrets,
331    setup_ui_handler: SetupUiRouter,
332    module_init_registry: ServerModuleInitRegistry,
333    auth_ui: Option<ApiAuth>,
334    auth_api: Option<ApiAuth>,
335) -> anyhow::Result<(
336    ServerConfig,
337    DynP2PConnections<P2PMessage>,
338    P2PStatusReceivers,
339)> {
340    info!(target: LOG_CONSENSUS, "Starting config gen");
341
342    initialize_gauge_metrics(task_group, &db).await;
343
344    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
345
346    let setup_api = SetupApi::new(
347        settings.clone(),
348        db.clone(),
349        cgp_sender,
350        code_version_str.clone(),
351        code_version_hash,
352        auth_ui,
353        auth_api,
354    );
355
356    let mut rpc_module = RpcModule::new(setup_api.clone());
357
358    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
359
360    let api_handler = net::api::spawn(
361        "setup",
362        // config gen always uses ws api
363        settings.api_bind,
364        rpc_module,
365        10,
366        api_secrets.clone(),
367    )
368    .await;
369
370    let ui_task_group = TaskGroup::new();
371
372    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
373
374    let ui_listener = TcpListener::bind(settings.ui_bind)
375        .await
376        .expect("Failed to bind setup UI");
377
378    ui_task_group.spawn("setup-ui", move |handle| async move {
379        axum::serve(ui_listener, ui_service)
380            .with_graceful_shutdown(handle.make_shutdown_rx())
381            .await
382            .expect("Failed to serve setup UI");
383    });
384
385    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
386
387    loop {
388        let config_gen_outcome = cgp_receiver
389            .recv()
390            .await
391            .expect("Config gen params receiver closed unexpectedly");
392
393        match config_gen_outcome {
394            ConfigGenOutcome::Generated(cg_params) => {
395                // HACK: The `start-dkg` API call needs to have some time to finish
396                // before we shut down api handling. There's no easy and good way to do
397                // that other than just giving it some grace period.
398                sleep(Duration::from_millis(100)).await;
399
400                api_handler
401                    .stop()
402                    .expect("Config api should still be running");
403
404                api_handler.stopped().await;
405
406                ui_task_group
407                    .shutdown_join_all(None)
408                    .await
409                    .context("Failed to shutdown UI server after config gen")?;
410
411                let cg_params = *cg_params;
412                let connector = if cg_params.iroh_endpoints().is_empty() {
413                    TlsTcpConnector::new(
414                        cg_params.tls_config(),
415                        settings.p2p_bind,
416                        cg_params.p2p_urls(),
417                        cg_params.identity,
418                    )
419                    .await
420                    .into_dyn()
421                } else {
422                    IrohConnector::new(
423                        cg_params
424                            .iroh_p2p_sk
425                            .clone()
426                            .expect("Iroh p2p secret key is required for iroh endpoints"),
427                        settings.p2p_bind,
428                        settings.iroh_dns,
429                        settings.iroh_relays,
430                        cg_params
431                            .iroh_endpoints()
432                            .iter()
433                            .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
434                            .collect(),
435                    )
436                    .await?
437                    .into_dyn()
438                };
439
440                let (p2p_status_senders, p2p_status_receivers) =
441                    p2p_status_channels(connector.peers());
442
443                let connections = ReconnectP2PConnections::new(
444                    cg_params.identity,
445                    connector,
446                    task_group,
447                    p2p_status_senders,
448                )
449                .into_dyn();
450
451                let cfg = ServerConfig::distributed_gen(
452                    &cg_params,
453                    module_init_registry.clone(),
454                    code_version_str.clone(),
455                    connections.clone(),
456                    p2p_status_receivers.clone(),
457                )
458                .await?;
459
460                assert_ne!(
461                    cfg.consensus.iroh_endpoints.is_empty(),
462                    cfg.consensus.api_endpoints.is_empty(),
463                );
464
465                write_server_config(
466                    &cfg,
467                    &data_dir,
468                    &module_init_registry,
469                    api_secrets.get_active(),
470                )?;
471
472                return Ok((cfg, connections, p2p_status_receivers));
473            }
474            ConfigGenOutcome::Restored(restored, restore_result_sender) => {
475                // Process restore outcomes while setup serving is still running. This lets the
476                // HTTP handler wait for a precise success/error acknowledgement and keeps setup
477                // retryable if validation or the config write fails.
478                let result: anyhow::Result<_> = async {
479                    let cfg = *restored;
480
481                    module_init_registry.decoders_strict(
482                        cfg.consensus
483                            .modules
484                            .iter()
485                            .map(|(id, config)| (*id, &config.kind)),
486                    )?;
487
488                    cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
489
490                    if cfg.consensus.iroh_endpoints.is_empty() {
491                        validate_restored_tcp_config(&cfg)?;
492                    }
493
494                    // Build the connector from the already validated restored config before
495                    // writing it to its final location.
496                    let connector = if cfg.consensus.iroh_endpoints.is_empty() {
497                        TlsTcpConnector::new(
498                            cfg.tls_config(),
499                            settings.p2p_bind,
500                            cfg.local.p2p_endpoints.clone(),
501                            cfg.local.identity,
502                        )
503                        .await
504                        .into_dyn()
505                    } else {
506                        let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
507
508                        IrohConnector::new(
509                            iroh_p2p_sk,
510                            settings.p2p_bind,
511                            settings.iroh_dns.clone(),
512                            settings.iroh_relays.clone(),
513                            cfg.consensus
514                                .iroh_endpoints
515                                .iter()
516                                .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
517                                .collect(),
518                        )
519                        .await?
520                        .into_dyn()
521                    };
522
523                    let (p2p_status_senders, p2p_status_receivers) =
524                        p2p_status_channels(connector.peers());
525
526                    // Write the restored config directly into the data directory, exactly
527                    // like a freshly generated config.
528                    write_server_config(
529                        &cfg,
530                        &data_dir,
531                        &module_init_registry,
532                        api_secrets.get_active(),
533                    )?;
534
535                    Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
536                }
537                .await;
538
539                let ack = result
540                    .as_ref()
541                    .map(|_| ())
542                    .map_err(std::string::ToString::to_string);
543                let restore_failed = ack.is_err();
544                let _ = restore_result_sender.send(ack);
545
546                if restore_failed {
547                    continue;
548                }
549
550                // Give the restore API call a chance to return the acknowledged outcome before
551                // shutting down setup serving.
552                sleep(Duration::from_millis(100)).await;
553
554                api_handler
555                    .stop()
556                    .expect("Config api should still be running");
557
558                api_handler.stopped().await;
559
560                ui_task_group
561                    .shutdown_join_all(None)
562                    .await
563                    .context("Failed to shutdown UI server after restored config install")?;
564
565                let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
566                let connections = ReconnectP2PConnections::new(
567                    cfg.local.identity,
568                    connector,
569                    task_group,
570                    p2p_status_senders,
571                )
572                .into_dyn();
573
574                return Ok((cfg, connections, p2p_status_receivers));
575            }
576        }
577    }
578}