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