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