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::net::SocketAddr;
28use std::path::{Path, PathBuf};
29use std::time::Duration;
30
31use anyhow::{Context, ensure};
32use bitcoin::hashes::hex::FromHex as _;
33use config::ServerConfig;
34use config::io::read_server_config;
35pub use connection_limits::ConnectionLimits;
36use fedimint_connectors::ConnectorRegistry;
37use fedimint_core::config::P2PMessage;
38use fedimint_core::db::{Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped as _};
39use fedimint_core::epoch::ConsensusItem;
40use fedimint_core::module::ApiAuth;
41use fedimint_core::net::peers::DynP2PConnections;
42use fedimint_core::task::{TaskGroup, sleep};
43use fedimint_core::util::SafeUrl;
44use fedimint_logging::LOG_CONSENSUS;
45pub use fedimint_server_core as core;
46use fedimint_server_core::ServerModuleInitRegistry;
47use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
48use fedimint_server_core::dashboard_ui::DynDashboardApi;
49use fedimint_server_core::setup_ui::{DynSetupApi, ISetupApi};
50use jsonrpsee::RpcModule;
51use net::api::ApiSecrets;
52use net::p2p::P2PStatusReceivers;
53use net::p2p_connector::IrohConnector;
54use tokio::net::TcpListener;
55use tokio_rustls::rustls;
56use tracing::info;
57
58use crate::config::ConfigGenSettings;
59use crate::config::io::write_server_config;
60use crate::config::setup::{ConfigGenOutcome, 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::api::pkarr_publish::start_pkarr_publish_service;
66use crate::net::p2p::{ReconnectP2PConnections, p2p_status_channels};
67use crate::net::p2p_connector::{IP2PConnector, TlsTcpConnector};
68
69pub mod metrics;
70
71pub mod consensus;
73
74pub mod net;
76
77pub mod config;
79
80#[derive(Debug, Clone)]
82pub struct IrohNextApiSettings {
83 bind: Option<SocketAddr>,
86}
87
88impl IrohNextApiSettings {
89 pub fn new(bind: Option<SocketAddr>) -> Self {
92 Self { bind }
93 }
94
95 pub(crate) fn bind_override(&self) -> Option<SocketAddr> {
96 self.bind
97 }
98}
99
100pub type DashboardUiRouter = Box<dyn Fn(DynDashboardApi) -> axum::Router + Send>;
102
103pub type SetupUiRouter = Box<dyn Fn(DynSetupApi) -> axum::Router + Send>;
105
106#[allow(clippy::too_many_arguments)]
110pub async fn run(
111 data_dir: PathBuf,
112 auth_ui: Option<ApiAuth>,
113 auth_api: Option<ApiAuth>,
114 force_api_secrets: ApiSecrets,
115 settings: ConfigGenSettings,
116 db: Database,
117 code_version_str: String,
118 code_version_hash: String,
119 module_init_registry: ServerModuleInitRegistry,
120 task_group: TaskGroup,
121 bitcoin_rpc: DynServerBitcoinRpc,
122 setup_ui_router: SetupUiRouter,
123 dashboard_ui_router: DashboardUiRouter,
124 db_checkpoint_retention: u64,
125 session_timeout: Duration,
126 iroh_api_limits: ConnectionLimits,
127) -> anyhow::Result<()> {
128 run_with_iroh_p2p_relays(
129 data_dir,
130 auth_ui,
131 auth_api,
132 force_api_secrets,
133 settings,
134 db,
135 code_version_str,
136 code_version_hash,
137 module_init_registry,
138 task_group,
139 bitcoin_rpc,
140 setup_ui_router,
141 dashboard_ui_router,
142 db_checkpoint_retention,
143 session_timeout,
144 iroh_api_limits,
145 Vec::new(),
146 )
147 .await
148}
149
150#[allow(clippy::too_many_arguments)]
152pub async fn run_with_iroh_p2p_relays(
153 data_dir: PathBuf,
154 auth_ui: Option<ApiAuth>,
155 auth_api: Option<ApiAuth>,
156 force_api_secrets: ApiSecrets,
157 settings: ConfigGenSettings,
158 db: Database,
159 code_version_str: String,
160 code_version_hash: String,
161 module_init_registry: ServerModuleInitRegistry,
162 task_group: TaskGroup,
163 bitcoin_rpc: DynServerBitcoinRpc,
164 setup_ui_router: SetupUiRouter,
165 dashboard_ui_router: DashboardUiRouter,
166 db_checkpoint_retention: u64,
167 session_timeout: Duration,
168 iroh_api_limits: ConnectionLimits,
169 iroh_p2p_relays: Vec<SafeUrl>,
170) -> anyhow::Result<()> {
171 run_with_iroh_p2p_relays_and_next_api(
172 data_dir,
173 auth_ui,
174 auth_api,
175 force_api_secrets,
176 settings,
177 db,
178 code_version_str,
179 code_version_hash,
180 module_init_registry,
181 task_group,
182 bitcoin_rpc,
183 setup_ui_router,
184 dashboard_ui_router,
185 db_checkpoint_retention,
186 session_timeout,
187 iroh_api_limits,
188 iroh_p2p_relays,
189 None,
190 )
191 .await
192}
193
194#[allow(clippy::too_many_arguments)]
196pub async fn run_with_iroh_p2p_relays_and_next_api(
197 data_dir: PathBuf,
198 auth_ui: Option<ApiAuth>,
199 auth_api: Option<ApiAuth>,
200 force_api_secrets: ApiSecrets,
201 settings: ConfigGenSettings,
202 db: Database,
203 code_version_str: String,
204 code_version_hash: String,
205 module_init_registry: ServerModuleInitRegistry,
206 task_group: TaskGroup,
207 bitcoin_rpc: DynServerBitcoinRpc,
208 setup_ui_router: SetupUiRouter,
209 dashboard_ui_router: DashboardUiRouter,
210 db_checkpoint_retention: u64,
211 session_timeout: Duration,
212 iroh_api_limits: ConnectionLimits,
213 iroh_p2p_relays: Vec<SafeUrl>,
214 iroh_next_api_settings: Option<IrohNextApiSettings>,
215) -> anyhow::Result<()> {
216 let (cfg, connections, p2p_status_receivers) = match get_config(&data_dir)? {
217 Some(cfg) => {
218 let connector = if cfg.consensus.iroh_endpoints.is_empty() {
219 TlsTcpConnector::new(
220 cfg.tls_config(),
221 settings.p2p_bind,
222 cfg.local.p2p_endpoints.clone(),
223 cfg.local.identity,
224 )
225 .await
226 .into_dyn()
227 } else {
228 IrohConnector::new(
229 cfg.private.iroh_p2p_sk.clone().unwrap(),
230 settings.p2p_bind,
231 settings.iroh_dns.clone(),
232 iroh_p2p_relays.clone(),
233 cfg.consensus
234 .iroh_endpoints
235 .iter()
236 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
237 .collect(),
238 )
239 .await?
240 .into_dyn()
241 };
242
243 let (p2p_status_senders, p2p_status_receivers) = p2p_status_channels(connector.peers());
244
245 let connections = ReconnectP2PConnections::new(
246 cfg.local.identity,
247 connector,
248 &task_group,
249 p2p_status_senders,
250 )
251 .into_dyn();
252
253 (cfg, connections, p2p_status_receivers)
254 }
255 None => {
256 Box::pin(run_config_gen_with_iroh_p2p_relays(
257 data_dir.clone(),
258 settings.clone(),
259 db.clone(),
260 &task_group,
261 code_version_str.clone(),
262 code_version_hash.clone(),
263 force_api_secrets.clone(),
264 setup_ui_router,
265 module_init_registry.clone(),
266 auth_ui.clone(),
267 auth_api.clone(),
268 iroh_p2p_relays,
269 ))
270 .await?
271 }
272 };
273
274 let decoders = module_init_registry.decoders_strict(
275 cfg.consensus
276 .modules
277 .iter()
278 .map(|(id, config)| (*id, &config.kind)),
279 )?;
280
281 let db = db.with_decoders(decoders);
282
283 initialize_gauge_metrics(&task_group, &db).await;
284
285 start_api_announcement_service(&db, &task_group, &cfg, force_api_secrets.get_active()).await?;
286 start_pkarr_publish_service(&db, &task_group, &cfg).await?;
287
288 info!(target: LOG_CONSENSUS, "Starting consensus...");
289
290 let connectors = ConnectorRegistry::build_from_server_defaults()
291 .bind()
292 .await?;
293
294 Box::pin(consensus::run(
295 connectors,
296 auth_ui,
297 auth_api,
298 connections,
299 p2p_status_receivers,
300 settings.api_bind,
301 settings.iroh_dns,
302 settings.iroh_relays,
303 cfg,
304 db,
305 module_init_registry.clone(),
306 &task_group,
307 force_api_secrets,
308 data_dir,
309 code_version_str,
310 code_version_hash,
311 bitcoin_rpc,
312 settings.ui_bind,
313 dashboard_ui_router,
314 db_checkpoint_retention,
315 session_timeout,
316 iroh_api_limits,
317 iroh_next_api_settings.as_ref(),
318 ))
319 .await?;
320
321 info!(target: LOG_CONSENSUS, "Shutting down tasks...");
322
323 task_group.shutdown();
324
325 Ok(())
326}
327
328async fn update_server_info_version_dbtx(
329 dbtx: &mut DatabaseTransaction<'_>,
330 code_version_str: &str,
331) {
332 let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
333 init_version: code_version_str.to_string(),
334 last_version: code_version_str.to_string(),
335 });
336 server_info.last_version = code_version_str.to_string();
337 dbtx.insert_entry(&ServerInfoKey, &server_info).await;
338}
339
340pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
341 if !data_dir.join("consensus.json").exists() {
342 return Ok(None);
343 }
344
345 read_server_config(data_dir).map(Some)
346}
347
348fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
359 let tls_key = cfg
360 .private
361 .tls_key
362 .as_ref()
363 .context("Restored TCP config is missing the TLS private key")?;
364 let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
365 rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
366 .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
367
368 ensure!(
369 cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
370 "Restored TCP config is missing our TLS certificate"
371 );
372 for (peer, cert) in &cfg.consensus.tls_certs {
373 Vec::from_hex(cert)
374 .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
375 }
376
377 let tls_config = cfg.tls_config();
378 let mut root_cert_store = rustls::RootCertStore::empty();
379 for cert in tls_config.certificates.values() {
380 root_cert_store
381 .add(cert.clone())
382 .context("Adding restored TLS certificate to root store")?;
383 }
384 let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
385 .build()
386 .context("Creating restored TLS client verifier")?;
387 let certificate = tls_config
388 .certificates
389 .get(&cfg.local.identity)
390 .context("Restored TCP config is missing our TLS certificate")?
391 .clone();
392 rustls::ServerConfig::builder()
393 .with_client_cert_verifier(verifier)
394 .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
395 .context("Creating restored TLS server config")?;
396
397 Ok(())
398}
399
400fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
407 let iroh_p2p_sk = cfg
408 .private
409 .iroh_p2p_sk
410 .clone()
411 .context("Restored Iroh config is missing the Iroh p2p secret key")?;
412 let local_endpoints = cfg
413 .consensus
414 .iroh_endpoints
415 .get(&cfg.local.identity)
416 .context("Restored Iroh config is missing our Iroh endpoints")?;
417 ensure!(
418 iroh_p2p_sk.public() == local_endpoints.p2p_pk,
419 "Restored Iroh p2p secret key does not match our Iroh endpoint"
420 );
421
422 let iroh_api_sk = cfg
423 .private
424 .iroh_api_sk
425 .clone()
426 .context("Restored Iroh config is missing the Iroh api secret key")?;
427 ensure!(
428 iroh_api_sk.public() == local_endpoints.api_pk,
429 "Restored Iroh api secret key does not match our Iroh endpoint"
430 );
431
432 Ok(iroh_p2p_sk)
433}
434
435#[allow(clippy::too_many_arguments)]
441pub async fn run_config_gen(
442 data_dir: PathBuf,
443 settings: ConfigGenSettings,
444 db: Database,
445 task_group: &TaskGroup,
446 code_version_str: String,
447 code_version_hash: String,
448 api_secrets: ApiSecrets,
449 setup_ui_handler: SetupUiRouter,
450 module_init_registry: ServerModuleInitRegistry,
451 auth_ui: Option<ApiAuth>,
452 auth_api: Option<ApiAuth>,
453) -> anyhow::Result<(
454 ServerConfig,
455 DynP2PConnections<P2PMessage>,
456 P2PStatusReceivers,
457)> {
458 run_config_gen_with_iroh_p2p_relays(
459 data_dir,
460 settings,
461 db,
462 task_group,
463 code_version_str,
464 code_version_hash,
465 api_secrets,
466 setup_ui_handler,
467 module_init_registry,
468 auth_ui,
469 auth_api,
470 Vec::new(),
471 )
472 .await
473}
474
475#[allow(clippy::too_many_arguments)]
477pub async fn run_config_gen_with_iroh_p2p_relays(
478 data_dir: PathBuf,
479 settings: ConfigGenSettings,
480 db: Database,
481 task_group: &TaskGroup,
482 code_version_str: String,
483 code_version_hash: String,
484 api_secrets: ApiSecrets,
485 setup_ui_handler: SetupUiRouter,
486 module_init_registry: ServerModuleInitRegistry,
487 auth_ui: Option<ApiAuth>,
488 auth_api: Option<ApiAuth>,
489 iroh_p2p_relays: Vec<SafeUrl>,
490) -> anyhow::Result<(
491 ServerConfig,
492 DynP2PConnections<P2PMessage>,
493 P2PStatusReceivers,
494)> {
495 info!(target: LOG_CONSENSUS, "Starting config gen");
496
497 initialize_gauge_metrics(task_group, &db).await;
498
499 let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
500
501 let setup_api = SetupApi::new(
502 settings.clone(),
503 db.clone(),
504 cgp_sender,
505 code_version_str.clone(),
506 code_version_hash,
507 auth_ui,
508 auth_api,
509 );
510
511 let mut rpc_module = RpcModule::new(setup_api.clone());
512
513 net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
514
515 let api_handler = net::api::spawn(
516 "setup",
517 settings.api_bind,
519 rpc_module,
520 10,
521 api_secrets.clone(),
522 )
523 .await;
524
525 let ui_task_group = TaskGroup::new();
526
527 let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
528
529 let ui_listener = TcpListener::bind(settings.ui_bind)
530 .await
531 .expect("Failed to bind setup UI");
532
533 ui_task_group.spawn("setup-ui", move |handle| async move {
534 axum::serve(ui_listener, ui_service)
535 .with_graceful_shutdown(handle.make_shutdown_rx())
536 .await
537 .expect("Failed to serve setup UI");
538 });
539
540 info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
541
542 loop {
543 let config_gen_outcome = cgp_receiver
544 .recv()
545 .await
546 .expect("Config gen params receiver closed unexpectedly");
547
548 match config_gen_outcome {
549 ConfigGenOutcome::Generated(cg_params) => {
550 sleep(Duration::from_millis(100)).await;
554
555 api_handler
556 .stop()
557 .expect("Config api should still be running");
558
559 api_handler.stopped().await;
560
561 ui_task_group
562 .shutdown_join_all(None)
563 .await
564 .context("Failed to shutdown UI server after config gen")?;
565
566 let cg_params = *cg_params;
567 let connector = if cg_params.iroh_endpoints().is_empty() {
568 TlsTcpConnector::new(
569 cg_params.tls_config(),
570 settings.p2p_bind,
571 cg_params.p2p_urls(),
572 cg_params.identity,
573 )
574 .await
575 .into_dyn()
576 } else {
577 IrohConnector::new(
578 cg_params
579 .iroh_p2p_sk
580 .clone()
581 .expect("Iroh p2p secret key is required for iroh endpoints"),
582 settings.p2p_bind,
583 settings.iroh_dns,
584 iroh_p2p_relays,
585 cg_params
586 .iroh_endpoints()
587 .iter()
588 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
589 .collect(),
590 )
591 .await?
592 .into_dyn()
593 };
594
595 let (p2p_status_senders, p2p_status_receivers) =
596 p2p_status_channels(connector.peers());
597
598 let connections = ReconnectP2PConnections::new(
599 cg_params.identity,
600 connector,
601 task_group,
602 p2p_status_senders,
603 )
604 .into_dyn();
605
606 let cfg = ServerConfig::distributed_gen(
607 &cg_params,
608 module_init_registry.clone(),
609 code_version_str.clone(),
610 connections.clone(),
611 p2p_status_receivers.clone(),
612 )
613 .await?;
614
615 assert_ne!(
616 cfg.consensus.iroh_endpoints.is_empty(),
617 cfg.consensus.api_endpoints.is_empty(),
618 );
619
620 write_server_config(
621 &cfg,
622 &data_dir,
623 &module_init_registry,
624 api_secrets.get_active(),
625 )?;
626
627 return Ok((cfg, connections, p2p_status_receivers));
628 }
629 ConfigGenOutcome::Restored(restored, restore_result_sender) => {
630 let result: anyhow::Result<_> = async {
634 let cfg = *restored;
635
636 module_init_registry.decoders_strict(
637 cfg.consensus
638 .modules
639 .iter()
640 .map(|(id, config)| (*id, &config.kind)),
641 )?;
642
643 cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
644
645 if cfg.consensus.iroh_endpoints.is_empty() {
646 validate_restored_tcp_config(&cfg)?;
647 }
648
649 let connector = if cfg.consensus.iroh_endpoints.is_empty() {
652 TlsTcpConnector::new(
653 cfg.tls_config(),
654 settings.p2p_bind,
655 cfg.local.p2p_endpoints.clone(),
656 cfg.local.identity,
657 )
658 .await
659 .into_dyn()
660 } else {
661 let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
662
663 IrohConnector::new(
664 iroh_p2p_sk,
665 settings.p2p_bind,
666 settings.iroh_dns.clone(),
667 iroh_p2p_relays.clone(),
668 cfg.consensus
669 .iroh_endpoints
670 .iter()
671 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
672 .collect(),
673 )
674 .await?
675 .into_dyn()
676 };
677
678 let (p2p_status_senders, p2p_status_receivers) =
679 p2p_status_channels(connector.peers());
680
681 write_server_config(
684 &cfg,
685 &data_dir,
686 &module_init_registry,
687 api_secrets.get_active(),
688 )?;
689
690 Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
691 }
692 .await;
693
694 let ack = result
695 .as_ref()
696 .map(|_| ())
697 .map_err(std::string::ToString::to_string);
698 let restore_failed = ack.is_err();
699 let _ = restore_result_sender.send(ack);
700
701 if restore_failed {
702 continue;
703 }
704
705 sleep(Duration::from_millis(100)).await;
708
709 api_handler
710 .stop()
711 .expect("Config api should still be running");
712
713 api_handler.stopped().await;
714
715 ui_task_group
716 .shutdown_join_all(None)
717 .await
718 .context("Failed to shutdown UI server after restored config install")?;
719
720 let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
721 let connections = ReconnectP2PConnections::new(
722 cfg.local.identity,
723 connector,
724 task_group,
725 p2p_status_senders,
726 )
727 .into_dyn();
728
729 return Ok((cfg, connections, p2p_status_receivers));
730 }
731 }
732 }
733}