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_core::config::P2PMessage;
37use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
38use fedimint_core::epoch::ConsensusItem;
39use fedimint_core::net::peers::DynP2PConnections;
40use fedimint_core::task::{TaskGroup, sleep};
41use fedimint_core::util::write_new;
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 tracing::info;
54
55use crate::config::ConfigGenSettings;
56use crate::config::io::{
57    SALT_FILE, finalize_password_change, recover_interrupted_password_change, trim_password,
58    write_server_config,
59};
60use crate::config::setup::SetupApi;
61use crate::db::{ServerInfo, ServerInfoKey};
62use crate::fedimint_core::net::peers::IP2PConnections;
63use crate::metrics::initialize_gauge_metrics;
64use crate::net::api::announcement::start_api_announcement_service;
65use crate::net::p2p::{
66    P2PConnectionTypeReceivers, ReconnectP2PConnections, p2p_connection_type_channels,
67    p2p_status_channels,
68};
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, p2p_connection_type_receivers) =
104        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) =
132                    p2p_status_channels(connector.peers());
133                let (p2p_connection_type_senders, p2p_connection_type_receivers) =
134                    p2p_connection_type_channels(connector.peers());
135
136                let connections = ReconnectP2PConnections::new(
137                    cfg.local.identity,
138                    connector,
139                    &task_group,
140                    p2p_status_senders,
141                    p2p_connection_type_senders,
142                )
143                .into_dyn();
144
145                (
146                    cfg,
147                    connections,
148                    p2p_status_receivers,
149                    p2p_connection_type_receivers,
150                )
151            }
152            None => {
153                Box::pin(run_config_gen(
154                    data_dir.clone(),
155                    settings.clone(),
156                    db.clone(),
157                    &task_group,
158                    code_version_str.clone(),
159                    force_api_secrets.clone(),
160                    setup_ui_router,
161                ))
162                .await?
163            }
164        };
165
166    let decoders = module_init_registry.decoders_strict(
167        cfg.consensus
168            .modules
169            .iter()
170            .map(|(id, config)| (*id, &config.kind)),
171    )?;
172
173    let db = db.with_decoders(decoders);
174
175    initialize_gauge_metrics(&task_group, &db).await;
176
177    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
178
179    info!(target: LOG_CONSENSUS, "Starting consensus...");
180
181    Box::pin(consensus::run(
182        connections,
183        p2p_status_receivers,
184        p2p_connection_type_receivers,
185        settings.api_bind,
186        settings.iroh_dns,
187        settings.iroh_relays,
188        cfg,
189        db,
190        module_init_registry.clone(),
191        &task_group,
192        force_api_secrets,
193        data_dir,
194        code_version_str,
195        bitcoin_rpc,
196        settings.ui_bind,
197        dashboard_ui_router,
198        db_checkpoint_retention,
199        iroh_api_limits,
200    ))
201    .await?;
202
203    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
204
205    task_group.shutdown();
206
207    Ok(())
208}
209
210async fn update_server_info_version_dbtx(
211    dbtx: &mut DatabaseTransaction<'_>,
212    code_version_str: &str,
213) {
214    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
215        init_version: code_version_str.to_string(),
216        last_version: code_version_str.to_string(),
217    });
218    server_info.last_version = code_version_str.to_string();
219    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
220}
221
222pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
223    recover_interrupted_password_change(data_dir)?;
224
225    // Attempt get the config with local password, otherwise start config gen
226    let path = data_dir.join(PLAINTEXT_PASSWORD);
227    if let Ok(password_untrimmed) = fs::read_to_string(&path) {
228        let password = trim_password(&password_untrimmed);
229        let cfg = read_server_config(password, data_dir)?;
230        finalize_password_change(data_dir)?;
231        return Ok(Some(cfg));
232    }
233
234    Ok(None)
235}
236
237pub async fn run_config_gen(
238    data_dir: PathBuf,
239    settings: ConfigGenSettings,
240    db: Database,
241    task_group: &TaskGroup,
242    code_version_str: String,
243    api_secrets: ApiSecrets,
244    setup_ui_handler: SetupUiRouter,
245) -> anyhow::Result<(
246    ServerConfig,
247    DynP2PConnections<P2PMessage>,
248    P2PStatusReceivers,
249    P2PConnectionTypeReceivers,
250)> {
251    info!(target: LOG_CONSENSUS, "Starting config gen");
252
253    initialize_gauge_metrics(task_group, &db).await;
254
255    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
256
257    let setup_api = SetupApi::new(settings.clone(), db.clone(), cgp_sender);
258
259    let mut rpc_module = RpcModule::new(setup_api.clone());
260
261    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
262
263    let api_handler = net::api::spawn(
264        "setup",
265        // config gen always uses ws api
266        settings.api_bind,
267        rpc_module,
268        10,
269        api_secrets.clone(),
270    )
271    .await;
272
273    let ui_task_group = TaskGroup::new();
274
275    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
276
277    let ui_listener = TcpListener::bind(settings.ui_bind)
278        .await
279        .expect("Failed to bind setup UI");
280
281    ui_task_group.spawn("setup-ui", move |handle| async move {
282        axum::serve(ui_listener, ui_service)
283            .with_graceful_shutdown(handle.make_shutdown_rx())
284            .await
285            .expect("Failed to serve setup UI");
286    });
287
288    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
289
290    let cg_params = cgp_receiver
291        .recv()
292        .await
293        .expect("Config gen params receiver closed unexpectedly");
294
295    // prevent failure of start dkg command in CI
296    sleep(Duration::from_millis(10)).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    let (p2p_connection_type_senders, p2p_connection_type_receivers) =
336        p2p_connection_type_channels(connector.peers());
337
338    let connections = ReconnectP2PConnections::new(
339        cg_params.identity,
340        connector,
341        task_group,
342        p2p_status_senders,
343        p2p_connection_type_senders,
344    )
345    .into_dyn();
346
347    let cfg = ServerConfig::distributed_gen(
348        settings.modules,
349        &cg_params,
350        settings.registry.clone(),
351        code_version_str.clone(),
352        connections.clone(),
353        p2p_status_receivers.clone(),
354    )
355    .await?;
356
357    assert_ne!(
358        cfg.consensus.iroh_endpoints.is_empty(),
359        cfg.consensus.api_endpoints.is_empty(),
360    );
361
362    // TODO: Make writing password optional
363    write_new(data_dir.join(PLAINTEXT_PASSWORD), &cfg.private.api_auth.0)?;
364    write_new(data_dir.join(SALT_FILE), random_salt())?;
365    write_server_config(
366        &cfg,
367        &data_dir,
368        &cfg.private.api_auth.0,
369        &settings.registry,
370        api_secrets.get_active(),
371    )?;
372
373    Ok((
374        cfg,
375        connections,
376        p2p_status_receivers,
377        p2p_connection_type_receivers,
378    ))
379}