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::fs;
28use std::path::{Path, PathBuf};
29use std::time::Duration;
30
31use anyhow::Context;
32use config::ServerConfig;
33use config::io::{PLAINTEXT_PASSWORD, read_server_config};
34pub use connection_limits::ConnectionLimits;
35use fedimint_aead::random_salt;
36use fedimint_connectors::ConnectorRegistry;
37use fedimint_core::config::P2PMessage;
38use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
39use fedimint_core::epoch::ConsensusItem;
40use fedimint_core::net::peers::DynP2PConnections;
41use fedimint_core::task::{TaskGroup, sleep};
42use fedimint_core::util::write_new;
43use fedimint_logging::LOG_CONSENSUS;
44pub use fedimint_server_core as core;
45use fedimint_server_core::ServerModuleInitRegistry;
46use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
47use fedimint_server_core::dashboard_ui::DynDashboardApi;
48use fedimint_server_core::setup_ui::{DynSetupApi, ISetupApi};
49use jsonrpsee::RpcModule;
50use net::api::ApiSecrets;
51use net::p2p::P2PStatusReceivers;
52use net::p2p_connector::IrohConnector;
53use tokio::net::TcpListener;
54use tracing::info;
55
56use crate::config::ConfigGenSettings;
57use crate::config::io::{
58    SALT_FILE, finalize_password_change, recover_interrupted_password_change, trim_password,
59    write_server_config,
60};
61use crate::config::setup::SetupApi;
62use crate::db::{ServerInfo, ServerInfoKey};
63use crate::fedimint_core::net::peers::IP2PConnections;
64use crate::metrics::initialize_gauge_metrics;
65use crate::net::api::announcement::start_api_announcement_service;
66use crate::net::api::guardian_metadata::start_guardian_metadata_service;
67use crate::net::api::pkarr_publish::start_pkarr_publish_service;
68use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
69use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
70
71pub mod metrics;
72
73/// The actual implementation of consensus
74pub mod consensus;
75
76/// Networking for mint-to-mint and client-to-mint communiccation
77pub mod net;
78
79/// Fedimint toplevel config
80pub mod config;
81
82/// A function/closure type for handling dashboard UI
83pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
84
85/// A function/closure type for handling setup UI
86pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
87
88#[allow(clippy::too_many_arguments)]
89pub async fn run(
90    data_dir: PathBuf,
91    force_api_secrets: ApiSecrets,
92    settings: ConfigGenSettings,
93    db: Database,
94    code_version_str: 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    iroh_api_limits: ConnectionLimits,
102) -> anyhow::Result<()> {
103    let (cfg, connections, p2p_status_receivers) = match get_config(&data_dir)? {
104        Some(cfg) => {
105            let connector = if cfg.consensus.iroh_endpoints.is_empty() {
106                TlsTcpConnector::new(
107                    cfg.tls_config(),
108                    settings.p2p_bind,
109                    cfg.local.p2p_endpoints.clone(),
110                    cfg.local.identity,
111                )
112                .await
113                .into_dyn()
114            } else {
115                IrohConnector::new(
116                    cfg.private.iroh_p2p_sk.clone().unwrap(),
117                    settings.p2p_bind,
118                    settings.iroh_dns.clone(),
119                    settings.iroh_relays.clone(),
120                    cfg.consensus
121                        .iroh_endpoints
122                        .iter()
123                        .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
124                        .collect(),
125                )
126                .await?
127                .into_dyn()
128            };
129
130            let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
131
132            let connections = ReconnectP2PConnections::new(
133                cfg.local.identity,
134                connector,
135                &task_group,
136                p2p_status_senders,
137            )
138            .into_dyn();
139
140            (cfg, connections, p2p_status_receivers)
141        }
142        None => {
143            Box::pin(run_config_gen(
144                data_dir.clone(),
145                settings.clone(),
146                db.clone(),
147                &task_group,
148                code_version_str.clone(),
149                force_api_secrets.clone(),
150                setup_ui_router,
151                module_init_registry.clone(),
152            ))
153            .await?
154        }
155    };
156
157    let decoders = module_init_registry.decoders_strict(
158        cfg.consensus
159            .modules
160            .iter()
161            .map(|(id, config)| (*id, &config.kind)),
162    )?;
163
164    let db = db.with_decoders(decoders);
165
166    initialize_gauge_metrics(&task_group, &db).await;
167
168    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
169    start_guardian_metadata_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
170    start_pkarr_publish_service(&db, &task_group, &cfg).await?;
171
172    info!(target: LOG_CONSENSUS, "Starting consensus...");
173
174    let connectors = ConnectorRegistry::build_from_server_defaults()
175        .bind()
176        .await?;
177
178    Box::pin(consensus::run(
179        connectors,
180        connections,
181        p2p_status_receivers,
182        settings.api_bind,
183        settings.iroh_dns,
184        settings.iroh_relays,
185        cfg,
186        db,
187        module_init_registry.clone(),
188        &task_group,
189        force_api_secrets,
190        data_dir,
191        code_version_str,
192        bitcoin_rpc,
193        settings.ui_bind,
194        dashboard_ui_router,
195        db_checkpoint_retention,
196        iroh_api_limits,
197    ))
198    .await?;
199
200    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
201
202    task_group.shutdown();
203
204    Ok(())
205}
206
207async fn update_server_info_version_dbtx(
208    dbtx: &mut DatabaseTransaction<'_>,
209    code_version_str: &str,
210) {
211    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
212        init_version: code_version_str.to_string(),
213        last_version: code_version_str.to_string(),
214    });
215    server_info.last_version = code_version_str.to_string();
216    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
217}
218
219pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
220    recover_interrupted_password_change(data_dir)?;
221
222    // Attempt get the config with local password, otherwise start config gen
223    let path = data_dir.join(PLAINTEXT_PASSWORD);
224    if let Ok(password_untrimmed) = fs::read_to_string(&path) {
225        let password = trim_password(&password_untrimmed);
226        let cfg = read_server_config(password, data_dir)?;
227        finalize_password_change(data_dir)?;
228        return Ok(Some(cfg));
229    }
230
231    Ok(None)
232}
233
234#[allow(clippy::too_many_arguments)]
235pub async fn run_config_gen(
236    data_dir: PathBuf,
237    settings: ConfigGenSettings,
238    db: Database,
239    task_group: &TaskGroup,
240    code_version_str: String,
241    api_secrets: ApiSecrets,
242    setup_ui_handler: SetupUiRouter,
243    module_init_registry: ServerModuleInitRegistry,
244) -> anyhow::Result<(
245    ServerConfig,
246    DynP2PConnections<P2PMessage>,
247    P2PStatusReceivers,
248)> {
249    info!(target: LOG_CONSENSUS, "Starting config gen");
250
251    initialize_gauge_metrics(task_group, &db).await;
252
253    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
254
255    let setup_api = SetupApi::new(settings.clone(), db.clone(), cgp_sender);
256
257    let mut rpc_module = RpcModule::new(setup_api.clone());
258
259    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
260
261    let api_handler = net::api::spawn(
262        "setup",
263        // config gen always uses ws api
264        settings.api_bind,
265        rpc_module,
266        10,
267        api_secrets.clone(),
268    )
269    .await;
270
271    let ui_task_group = TaskGroup::new();
272
273    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
274
275    let ui_listener = TcpListener::bind(settings.ui_bind)
276        .await
277        .expect("Failed to bind setup UI");
278
279    ui_task_group.spawn("setup-ui", move |handle| async move {
280        axum::serve(ui_listener, ui_service)
281            .with_graceful_shutdown(handle.make_shutdown_rx())
282            .await
283            .expect("Failed to serve setup UI");
284    });
285
286    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
287
288    let cg_params = cgp_receiver
289        .recv()
290        .await
291        .expect("Config gen params receiver closed unexpectedly");
292
293    // HACK: The `start-dkg` API call needs to have some time to finish
294    // before we shut down api handling. There's no easy and good way to do
295    // that other than just giving it some grace period.
296    sleep(Duration::from_millis(100)).await;
297
298    api_handler
299        .stop()
300        .expect("Config api should still be running");
301
302    api_handler.stopped().await;
303
304    ui_task_group
305        .shutdown_join_all(None)
306        .await
307        .context("Failed to shutdown UI server after config gen")?;
308
309    let connector = if cg_params.iroh_endpoints().is_empty() {
310        TlsTcpConnector::new(
311            cg_params.tls_config(),
312            settings.p2p_bind,
313            cg_params.p2p_urls(),
314            cg_params.identity,
315        )
316        .await
317        .into_dyn()
318    } else {
319        IrohConnector::new(
320            cg_params.iroh_p2p_sk.clone().unwrap(),
321            settings.p2p_bind,
322            settings.iroh_dns,
323            settings.iroh_relays,
324            cg_params
325                .iroh_endpoints()
326                .iter()
327                .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
328                .collect(),
329        )
330        .await?
331        .into_dyn()
332    };
333
334    let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
335
336    let connections = ReconnectP2PConnections::new(
337        cg_params.identity,
338        connector,
339        task_group,
340        p2p_status_senders,
341    )
342    .into_dyn();
343
344    let cfg = ServerConfig::distributed_gen(
345        &cg_params,
346        module_init_registry.clone(),
347        code_version_str.clone(),
348        connections.clone(),
349        p2p_status_receivers.clone(),
350    )
351    .await?;
352
353    assert_ne!(
354        cfg.consensus.iroh_endpoints.is_empty(),
355        cfg.consensus.api_endpoints.is_empty(),
356    );
357
358    // TODO: Make writing password optional
359    write_new(data_dir.join(PLAINTEXT_PASSWORD), &cfg.private.api_auth.0)?;
360    write_new(data_dir.join(SALT_FILE), random_salt())?;
361    write_server_config(
362        &cfg,
363        &data_dir,
364        &cfg.private.api_auth.0,
365        &module_init_registry,
366        api_secrets.get_active(),
367    )?;
368
369    Ok((cfg, connections, p2p_status_receivers))
370}