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
21extern 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
73pub mod consensus;
75
76pub mod net;
78
79pub mod config;
81
82pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
84
85pub 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 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 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 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 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}