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