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::path::{Path, PathBuf};
28use std::time::Duration;
29
30use anyhow::{Context, ensure};
31use bitcoin::hashes::hex::FromHex as _;
32use config::ServerConfig;
33use config::io::read_server_config;
34pub use connection_limits::ConnectionLimits;
35use fedimint_connectors::ConnectorRegistry;
36use fedimint_core::config::P2PMessage;
37use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
38use fedimint_core::epoch::ConsensusItem;
39use fedimint_core::module::ApiAuth;
40use fedimint_core::net::peers::DynP2PConnections;
41use fedimint_core::task::{TaskGroup, sleep};
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 tokio_rustls::rustls;
54use tracing::info;
55
56use crate::config::ConfigGenSettings;
57use crate::config::io::write_server_config;
58use crate::config::setup::{ConfigGenOutcome, SetupApi};
59use crate::db::{ServerInfo, ServerInfoKey};
60use crate::fedimint_core::net::peers::IP2PConnections;
61use crate::metrics::initialize_gauge_metrics;
62use crate::net::api::announcement::start_api_announcement_service;
63use crate::net::api::guardian_metadata::start_guardian_metadata_service;
64use crate::net::api::pkarr_publish::start_pkarr_publish_service;
65use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
66use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
67
68pub mod metrics;
69
70pub mod consensus;
72
73pub mod net;
75
76pub mod config;
78
79pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
81
82pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
84
85#[allow(clippy::too_many_arguments)]
86pub async fn run(
87 data_dir: PathBuf,
88 auth_ui: Option<ApiAuth>,
89 auth_api: Option<ApiAuth>,
90 force_api_secrets: ApiSecrets,
91 settings: ConfigGenSettings,
92 db: Database,
93 code_version_str: String,
94 code_version_hash: 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 session_timeout: Duration,
102 iroh_api_limits: ConnectionLimits,
103) -> anyhow::Result<()> {
104 let (cfg, connections, p2p_status_receivers) = 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) = p2p_status_channels(connector.peers());
132
133 let connections = ReconnectP2PConnections::new(
134 cfg.local.identity,
135 connector,
136 &task_group,
137 p2p_status_senders,
138 )
139 .into_dyn();
140
141 (cfg, connections, p2p_status_receivers)
142 }
143 None => {
144 Box::pin(run_config_gen(
145 data_dir.clone(),
146 settings.clone(),
147 db.clone(),
148 &task_group,
149 code_version_str.clone(),
150 code_version_hash.clone(),
151 force_api_secrets.clone(),
152 setup_ui_router,
153 module_init_registry.clone(),
154 auth_ui.clone(),
155 auth_api.clone(),
156 ))
157 .await?
158 }
159 };
160
161 let decoders = module_init_registry.decoders_strict(
162 cfg.consensus
163 .modules
164 .iter()
165 .map(|(id, config)| (*id, &config.kind)),
166 )?;
167
168 let db = db.with_decoders(decoders);
169
170 initialize_gauge_metrics(&task_group, &db).await;
171
172 start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
173 start_guardian_metadata_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
174 start_pkarr_publish_service(&db, &task_group, &cfg).await?;
175
176 info!(target: LOG_CONSENSUS, "Starting consensus...");
177
178 let connectors = ConnectorRegistry::build_from_server_defaults()
179 .bind()
180 .await?;
181
182 Box::pin(consensus::run(
183 connectors,
184 auth_ui,
185 auth_api,
186 connections,
187 p2p_status_receivers,
188 settings.api_bind,
189 settings.iroh_dns,
190 settings.iroh_relays,
191 cfg,
192 db,
193 module_init_registry.clone(),
194 &task_group,
195 force_api_secrets,
196 data_dir,
197 code_version_str,
198 code_version_hash,
199 bitcoin_rpc,
200 settings.ui_bind,
201 dashboard_ui_router,
202 db_checkpoint_retention,
203 session_timeout,
204 iroh_api_limits,
205 ))
206 .await?;
207
208 info!(target: LOG_CONSENSUS, "Shutting down tasks...");
209
210 task_group.shutdown();
211
212 Ok(())
213}
214
215async fn update_server_info_version_dbtx(
216 dbtx: &mut DatabaseTransaction<'_>,
217 code_version_str: &str,
218) {
219 let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
220 init_version: code_version_str.to_string(),
221 last_version: code_version_str.to_string(),
222 });
223 server_info.last_version = code_version_str.to_string();
224 dbtx.insert_entry(&ServerInfoKey, &server_info).await;
225}
226
227pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
228 if !data_dir.join("consensus.json").exists() {
229 return Ok(None);
230 }
231
232 read_server_config(data_dir).map(Some)
233}
234
235fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
246 let tls_key = cfg
247 .private
248 .tls_key
249 .as_ref()
250 .context("Restored TCP config is missing the TLS private key")?;
251 let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
252 rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
253 .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
254
255 ensure!(
256 cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
257 "Restored TCP config is missing our TLS certificate"
258 );
259 for (peer, cert) in &cfg.consensus.tls_certs {
260 Vec::from_hex(cert)
261 .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
262 }
263
264 let tls_config = cfg.tls_config();
265 let mut root_cert_store = rustls::RootCertStore::empty();
266 for cert in tls_config.certificates.values() {
267 root_cert_store
268 .add(cert.clone())
269 .context("Adding restored TLS certificate to root store")?;
270 }
271 let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
272 .build()
273 .context("Creating restored TLS client verifier")?;
274 let certificate = tls_config
275 .certificates
276 .get(&cfg.local.identity)
277 .context("Restored TCP config is missing our TLS certificate")?
278 .clone();
279 rustls::ServerConfig::builder()
280 .with_client_cert_verifier(verifier)
281 .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
282 .context("Creating restored TLS server config")?;
283
284 Ok(())
285}
286
287fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
294 let iroh_p2p_sk = cfg
295 .private
296 .iroh_p2p_sk
297 .clone()
298 .context("Restored Iroh config is missing the Iroh p2p secret key")?;
299 let local_endpoints = cfg
300 .consensus
301 .iroh_endpoints
302 .get(&cfg.local.identity)
303 .context("Restored Iroh config is missing our Iroh endpoints")?;
304 ensure!(
305 iroh_p2p_sk.public() == local_endpoints.p2p_pk,
306 "Restored Iroh p2p secret key does not match our Iroh endpoint"
307 );
308
309 let iroh_api_sk = cfg
310 .private
311 .iroh_api_sk
312 .clone()
313 .context("Restored Iroh config is missing the Iroh api secret key")?;
314 ensure!(
315 iroh_api_sk.public() == local_endpoints.api_pk,
316 "Restored Iroh api secret key does not match our Iroh endpoint"
317 );
318
319 Ok(iroh_p2p_sk)
320}
321
322#[allow(clippy::too_many_arguments)]
323pub async fn run_config_gen(
324 data_dir: PathBuf,
325 settings: ConfigGenSettings,
326 db: Database,
327 task_group: &TaskGroup,
328 code_version_str: String,
329 code_version_hash: String,
330 api_secrets: ApiSecrets,
331 setup_ui_handler: SetupUiRouter,
332 module_init_registry: ServerModuleInitRegistry,
333 auth_ui: Option<ApiAuth>,
334 auth_api: Option<ApiAuth>,
335) -> anyhow::Result<(
336 ServerConfig,
337 DynP2PConnections<P2PMessage>,
338 P2PStatusReceivers,
339)> {
340 info!(target: LOG_CONSENSUS, "Starting config gen");
341
342 initialize_gauge_metrics(task_group, &db).await;
343
344 let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
345
346 let setup_api = SetupApi::new(
347 settings.clone(),
348 db.clone(),
349 cgp_sender,
350 code_version_str.clone(),
351 code_version_hash,
352 auth_ui,
353 auth_api,
354 );
355
356 let mut rpc_module = RpcModule::new(setup_api.clone());
357
358 net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
359
360 let api_handler = net::api::spawn(
361 "setup",
362 settings.api_bind,
364 rpc_module,
365 10,
366 api_secrets.clone(),
367 )
368 .await;
369
370 let ui_task_group = TaskGroup::new();
371
372 let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
373
374 let ui_listener = TcpListener::bind(settings.ui_bind)
375 .await
376 .expect("Failed to bind setup UI");
377
378 ui_task_group.spawn("setup-ui", move |handle| async move {
379 axum::serve(ui_listener, ui_service)
380 .with_graceful_shutdown(handle.make_shutdown_rx())
381 .await
382 .expect("Failed to serve setup UI");
383 });
384
385 info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
386
387 loop {
388 let config_gen_outcome = cgp_receiver
389 .recv()
390 .await
391 .expect("Config gen params receiver closed unexpectedly");
392
393 match config_gen_outcome {
394 ConfigGenOutcome::Generated(cg_params) => {
395 sleep(Duration::from_millis(100)).await;
399
400 api_handler
401 .stop()
402 .expect("Config api should still be running");
403
404 api_handler.stopped().await;
405
406 ui_task_group
407 .shutdown_join_all(None)
408 .await
409 .context("Failed to shutdown UI server after config gen")?;
410
411 let cg_params = *cg_params;
412 let connector = if cg_params.iroh_endpoints().is_empty() {
413 TlsTcpConnector::new(
414 cg_params.tls_config(),
415 settings.p2p_bind,
416 cg_params.p2p_urls(),
417 cg_params.identity,
418 )
419 .await
420 .into_dyn()
421 } else {
422 IrohConnector::new(
423 cg_params
424 .iroh_p2p_sk
425 .clone()
426 .expect("Iroh p2p secret key is required for iroh endpoints"),
427 settings.p2p_bind,
428 settings.iroh_dns,
429 settings.iroh_relays,
430 cg_params
431 .iroh_endpoints()
432 .iter()
433 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
434 .collect(),
435 )
436 .await?
437 .into_dyn()
438 };
439
440 let (p2p_status_senders, p2p_status_receivers) =
441 p2p_status_channels(connector.peers());
442
443 let connections = ReconnectP2PConnections::new(
444 cg_params.identity,
445 connector,
446 task_group,
447 p2p_status_senders,
448 )
449 .into_dyn();
450
451 let cfg = ServerConfig::distributed_gen(
452 &cg_params,
453 module_init_registry.clone(),
454 code_version_str.clone(),
455 connections.clone(),
456 p2p_status_receivers.clone(),
457 )
458 .await?;
459
460 assert_ne!(
461 cfg.consensus.iroh_endpoints.is_empty(),
462 cfg.consensus.api_endpoints.is_empty(),
463 );
464
465 write_server_config(
466 &cfg,
467 &data_dir,
468 &module_init_registry,
469 api_secrets.get_active(),
470 )?;
471
472 return Ok((cfg, connections, p2p_status_receivers));
473 }
474 ConfigGenOutcome::Restored(restored, restore_result_sender) => {
475 let result: anyhow::Result<_> = async {
479 let cfg = *restored;
480
481 module_init_registry.decoders_strict(
482 cfg.consensus
483 .modules
484 .iter()
485 .map(|(id, config)| (*id, &config.kind)),
486 )?;
487
488 cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
489
490 if cfg.consensus.iroh_endpoints.is_empty() {
491 validate_restored_tcp_config(&cfg)?;
492 }
493
494 let connector = if cfg.consensus.iroh_endpoints.is_empty() {
497 TlsTcpConnector::new(
498 cfg.tls_config(),
499 settings.p2p_bind,
500 cfg.local.p2p_endpoints.clone(),
501 cfg.local.identity,
502 )
503 .await
504 .into_dyn()
505 } else {
506 let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
507
508 IrohConnector::new(
509 iroh_p2p_sk,
510 settings.p2p_bind,
511 settings.iroh_dns.clone(),
512 settings.iroh_relays.clone(),
513 cfg.consensus
514 .iroh_endpoints
515 .iter()
516 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
517 .collect(),
518 )
519 .await?
520 .into_dyn()
521 };
522
523 let (p2p_status_senders, p2p_status_receivers) =
524 p2p_status_channels(connector.peers());
525
526 write_server_config(
529 &cfg,
530 &data_dir,
531 &module_init_registry,
532 api_secrets.get_active(),
533 )?;
534
535 Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
536 }
537 .await;
538
539 let ack = result
540 .as_ref()
541 .map(|_| ())
542 .map_err(std::string::ToString::to_string);
543 let restore_failed = ack.is_err();
544 let _ = restore_result_sender.send(ack);
545
546 if restore_failed {
547 continue;
548 }
549
550 sleep(Duration::from_millis(100)).await;
553
554 api_handler
555 .stop()
556 .expect("Config api should still be running");
557
558 api_handler.stopped().await;
559
560 ui_task_group
561 .shutdown_join_all(None)
562 .await
563 .context("Failed to shutdown UI server after restored config install")?;
564
565 let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
566 let connections = ReconnectP2PConnections::new(
567 cfg.local.identity,
568 connector,
569 task_group,
570 p2p_status_senders,
571 )
572 .into_dyn();
573
574 return Ok((cfg, connections, p2p_status_receivers));
575 }
576 }
577 }
578}