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