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, LOG_CORE};
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, warn};
53
54use crate::config::ConfigGenSettings;
55use crate::config::io::{SALT_FILE, write_server_config};
56use crate::config::setup::SetupApi;
57use crate::db::{ServerInfo, ServerInfoKey};
58use crate::fedimint_core::net::peers::IP2PConnections;
59use crate::metrics::initialize_gauge_metrics;
60use crate::net::api::announcement::start_api_announcement_service;
61use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
62use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
63
64pub mod metrics;
65
66/// The actual implementation of consensus
67pub mod consensus;
68
69/// Networking for mint-to-mint and client-to-mint communiccation
70pub mod net;
71
72/// Fedimint toplevel config
73pub mod config;
74
75/// A function/closure type for handling dashboard UI
76pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
77
78/// A function/closure type for handling setup UI
79pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
80
81#[allow(clippy::too_many_arguments)]
82pub async fn run(
83    data_dir: PathBuf,
84    force_api_secrets: ApiSecrets,
85    settings: ConfigGenSettings,
86    db: Database,
87    code_version_str: String,
88    module_init_registry: ServerModuleInitRegistry,
89    task_group: TaskGroup,
90    bitcoin_rpc: DynServerBitcoinRpc,
91    setup_ui_router: SetupUiRouter,
92    dashboard_ui_router: DashboardUiRouter,
93    db_checkpoint_retention: u64,
94    iroh_api_limits: ConnectionLimits,
95) -> anyhow::Result<()> {
96    let (cfg, connections, p2p_status_receivers) = match get_config(&data_dir)? {
97        Some(cfg) => {
98            let connector = if cfg.consensus.iroh_endpoints.is_empty() {
99                TlsTcpConnector::new(
100                    cfg.tls_config(),
101                    settings.p2p_bind,
102                    cfg.local.p2p_endpoints.clone(),
103                    cfg.local.identity,
104                )
105                .await
106                .into_dyn()
107            } else {
108                IrohConnector::new(
109                    cfg.private.iroh_p2p_sk.clone().unwrap(),
110                    settings.p2p_bind,
111                    settings.iroh_dns.clone(),
112                    settings.iroh_relays.clone(),
113                    cfg.consensus
114                        .iroh_endpoints
115                        .iter()
116                        .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
117                        .collect(),
118                )
119                .await?
120                .into_dyn()
121            };
122
123            let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
124
125            let connections = ReconnectP2PConnections::new(
126                cfg.local.identity,
127                connector,
128                &task_group,
129                p2p_status_senders,
130            )
131            .into_dyn();
132
133            (cfg, connections, p2p_status_receivers)
134        }
135        None => {
136            Box::pin(run_config_gen(
137                data_dir.clone(),
138                settings.clone(),
139                db.clone(),
140                &task_group,
141                code_version_str.clone(),
142                force_api_secrets.clone(),
143                setup_ui_router,
144            ))
145            .await?
146        }
147    };
148
149    let decoders = module_init_registry.decoders_strict(
150        cfg.consensus
151            .modules
152            .iter()
153            .map(|(id, config)| (*id, &config.kind)),
154    )?;
155
156    let db = db.with_decoders(decoders);
157
158    initialize_gauge_metrics(&task_group, &db).await;
159
160    start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
161
162    info!(target: LOG_CONSENSUS, "Starting consensus...");
163
164    Box::pin(consensus::run(
165        connections,
166        p2p_status_receivers,
167        settings.api_bind,
168        settings.iroh_dns,
169        settings.iroh_relays,
170        cfg,
171        db,
172        module_init_registry.clone(),
173        &task_group,
174        force_api_secrets,
175        data_dir,
176        code_version_str,
177        bitcoin_rpc,
178        settings.ui_bind,
179        dashboard_ui_router,
180        db_checkpoint_retention,
181        iroh_api_limits,
182    ))
183    .await?;
184
185    info!(target: LOG_CONSENSUS, "Shutting down tasks...");
186
187    task_group.shutdown();
188
189    Ok(())
190}
191
192async fn update_server_info_version_dbtx(
193    dbtx: &mut DatabaseTransaction<'_>,
194    code_version_str: &str,
195) {
196    let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
197        init_version: code_version_str.to_string(),
198        last_version: code_version_str.to_string(),
199    });
200    server_info.last_version = code_version_str.to_string();
201    dbtx.insert_entry(&ServerInfoKey, &server_info).await;
202}
203
204pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
205    // Attempt get the config with local password, otherwise start config gen
206    let path = data_dir.join(PLAINTEXT_PASSWORD);
207    if let Ok(password_untrimmed) = fs::read_to_string(&path) {
208        // We definitely don't want leading/trailing newlines, and user
209        // editing the file manually will probably get a free newline added
210        // by the text editor.
211        let password = password_untrimmed.trim_matches('\n');
212        // In the future we also don't want to support any leading/trailing newlines
213        let password_fully_trimmed = password.trim();
214        if password_fully_trimmed != password {
215            warn!(
216                target: LOG_CORE,
217                path = %path.display(),
218                "Password in the password file contains leading/trailing whitespaces. This will an error in the future."
219            );
220        }
221        return Ok(Some(read_server_config(password, data_dir)?));
222    }
223
224    Ok(None)
225}
226
227pub async fn run_config_gen(
228    data_dir: PathBuf,
229    settings: ConfigGenSettings,
230    db: Database,
231    task_group: &TaskGroup,
232    code_version_str: String,
233    api_secrets: ApiSecrets,
234    setup_ui_handler: SetupUiRouter,
235) -> anyhow::Result<(
236    ServerConfig,
237    DynP2PConnections<P2PMessage>,
238    P2PStatusReceivers,
239)> {
240    info!(target: LOG_CONSENSUS, "Starting config gen");
241
242    initialize_gauge_metrics(task_group, &db).await;
243
244    let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
245
246    let setup_api = SetupApi::new(settings.clone(), db.clone(), cgp_sender);
247
248    let mut rpc_module = RpcModule::new(setup_api.clone());
249
250    net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
251
252    let api_handler = net::api::spawn(
253        "setup",
254        // config gen always uses ws api
255        settings.api_bind,
256        rpc_module,
257        10,
258        api_secrets.clone(),
259    )
260    .await;
261
262    let ui_task_group = TaskGroup::new();
263
264    let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
265
266    let ui_listener = TcpListener::bind(settings.ui_bind)
267        .await
268        .expect("Failed to bind setup UI");
269
270    ui_task_group.spawn("setup-ui", move |handle| async move {
271        axum::serve(ui_listener, ui_service)
272            .with_graceful_shutdown(handle.make_shutdown_rx())
273            .await
274            .expect("Failed to serve setup UI");
275    });
276
277    info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
278
279    let cg_params = cgp_receiver
280        .recv()
281        .await
282        .expect("Config gen params receiver closed unexpectedly");
283
284    api_handler
285        .stop()
286        .expect("Config api should still be running");
287
288    api_handler.stopped().await;
289
290    ui_task_group
291        .shutdown_join_all(None)
292        .await
293        .context("Failed to shutdown UI server after config gen")?;
294
295    let connector = if cg_params.iroh_endpoints().is_empty() {
296        TlsTcpConnector::new(
297            cg_params.tls_config(),
298            settings.p2p_bind,
299            cg_params.p2p_urls(),
300            cg_params.identity,
301        )
302        .await
303        .into_dyn()
304    } else {
305        IrohConnector::new(
306            cg_params.iroh_p2p_sk.clone().unwrap(),
307            settings.p2p_bind,
308            settings.iroh_dns,
309            settings.iroh_relays,
310            cg_params
311                .iroh_endpoints()
312                .iter()
313                .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
314                .collect(),
315        )
316        .await?
317        .into_dyn()
318    };
319
320    let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
321
322    let connections = ReconnectP2PConnections::new(
323        cg_params.identity,
324        connector,
325        task_group,
326        p2p_status_senders,
327    )
328    .into_dyn();
329
330    let cfg = ServerConfig::distributed_gen(
331        settings.modules,
332        &cg_params,
333        settings.registry.clone(),
334        code_version_str.clone(),
335        connections.clone(),
336        p2p_status_receivers.clone(),
337    )
338    .await?;
339
340    assert_ne!(
341        cfg.consensus.iroh_endpoints.is_empty(),
342        cfg.consensus.api_endpoints.is_empty(),
343    );
344
345    // TODO: Make writing password optional
346    write_new(data_dir.join(PLAINTEXT_PASSWORD), &cfg.private.api_auth.0)?;
347    write_new(data_dir.join(SALT_FILE), random_salt())?;
348    write_server_config(
349        &cfg,
350        &data_dir,
351        &cfg.private.api_auth.0,
352        &settings.registry,
353        api_secrets.get_active(),
354    )?;
355
356    Ok((cfg, connections, p2p_status_receivers))
357}