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::module::ApiAuth;
40use fedimint_core::net::DynP2PConnections;
41use fedimint_core::session_outcome::ConsensusItem;
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::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().bind().await;
297
298 Box::pin(consensus::run(
299 connectors,
300 auth_ui,
301 auth_api,
302 connections,
303 p2p_status_receivers,
304 settings.api_bind,
305 settings.iroh_dns,
306 settings.iroh_relays,
307 cfg,
308 db,
309 module_init_registry.clone(),
310 &task_group,
311 force_api_secrets,
312 data_dir,
313 code_version_str,
314 code_version_hash,
315 bitcoin_rpc,
316 settings.ui_bind,
317 dashboard_ui_router,
318 db_checkpoint_retention,
319 session_timeout,
320 iroh_api_limits,
321 iroh_next_api_settings.as_ref(),
322 ))
323 .await?;
324
325 info!(target: LOG_CONSENSUS, "Shutting down tasks...");
326
327 task_group.shutdown();
328
329 Ok(())
330}
331
332async fn update_server_info_version_dbtx(
333 dbtx: &mut DatabaseTransaction<'_>,
334 code_version_str: &str,
335) {
336 let mut server_info = dbtx.get_value(&ServerInfoKey).await.unwrap_or(ServerInfo {
337 init_version: code_version_str.to_string(),
338 last_version: code_version_str.to_string(),
339 });
340 server_info.last_version = code_version_str.to_string();
341 dbtx.insert_entry(&ServerInfoKey, &server_info).await;
342}
343
344pub fn get_config(data_dir: &Path) -> anyhow::Result<Option<ServerConfig>> {
345 if !data_dir.join("consensus.json").exists() {
346 return Ok(None);
347 }
348
349 read_server_config(data_dir).map(Some)
350}
351
352fn validate_restored_tcp_config(cfg: &ServerConfig) -> anyhow::Result<()> {
363 let tls_key = cfg
364 .private
365 .tls_key
366 .as_ref()
367 .context("Restored TCP config is missing the TLS private key")?;
368 let tls_key_bytes = Vec::from_hex(tls_key).context("Parsing restored TLS private key")?;
369 rustls::pki_types::PrivateKeyDer::try_from(tls_key_bytes)
370 .map_err(|e| anyhow::format_err!("Parsing restored TLS private key DER: {e}"))?;
371
372 ensure!(
373 cfg.consensus.tls_certs.contains_key(&cfg.local.identity),
374 "Restored TCP config is missing our TLS certificate"
375 );
376 for (peer, cert) in &cfg.consensus.tls_certs {
377 Vec::from_hex(cert)
378 .with_context(|| format!("Parsing restored TLS certificate for peer {peer}"))?;
379 }
380
381 let tls_config = cfg.tls_config();
382 let mut root_cert_store = rustls::RootCertStore::empty();
383 for cert in tls_config.certificates.values() {
384 root_cert_store
385 .add(cert.clone())
386 .context("Adding restored TLS certificate to root store")?;
387 }
388 let verifier = rustls::server::WebPkiClientVerifier::builder(root_cert_store.into())
389 .build()
390 .context("Creating restored TLS client verifier")?;
391 let certificate = tls_config
392 .certificates
393 .get(&cfg.local.identity)
394 .context("Restored TCP config is missing our TLS certificate")?
395 .clone();
396 rustls::ServerConfig::builder()
397 .with_client_cert_verifier(verifier)
398 .with_single_cert(vec![certificate], tls_config.private_key.clone_key())
399 .context("Creating restored TLS server config")?;
400
401 Ok(())
402}
403
404fn restored_iroh_p2p_key(cfg: &ServerConfig) -> anyhow::Result<iroh::SecretKey> {
411 let iroh_p2p_sk = cfg
412 .private
413 .iroh_p2p_sk
414 .clone()
415 .context("Restored Iroh config is missing the Iroh p2p secret key")?;
416 let local_endpoints = cfg
417 .consensus
418 .iroh_endpoints
419 .get(&cfg.local.identity)
420 .context("Restored Iroh config is missing our Iroh endpoints")?;
421 ensure!(
422 iroh_p2p_sk.public() == local_endpoints.p2p_pk,
423 "Restored Iroh p2p secret key does not match our Iroh endpoint"
424 );
425
426 let iroh_api_sk = cfg
427 .private
428 .iroh_api_sk
429 .clone()
430 .context("Restored Iroh config is missing the Iroh api secret key")?;
431 ensure!(
432 iroh_api_sk.public() == local_endpoints.api_pk,
433 "Restored Iroh api secret key does not match our Iroh endpoint"
434 );
435
436 Ok(iroh_p2p_sk)
437}
438
439#[allow(clippy::too_many_arguments)]
445pub async fn run_config_gen(
446 data_dir: PathBuf,
447 settings: ConfigGenSettings,
448 db: Database,
449 task_group: &TaskGroup,
450 code_version_str: String,
451 code_version_hash: String,
452 api_secrets: ApiSecrets,
453 setup_ui_handler: SetupUiRouter,
454 module_init_registry: ServerModuleInitRegistry,
455 auth_ui: Option<ApiAuth>,
456 auth_api: Option<ApiAuth>,
457) -> anyhow::Result<(
458 ServerConfig,
459 DynP2PConnections<P2PMessage>,
460 P2PStatusReceivers,
461)> {
462 run_config_gen_with_iroh_p2p_relays(
463 data_dir,
464 settings,
465 db,
466 task_group,
467 code_version_str,
468 code_version_hash,
469 api_secrets,
470 setup_ui_handler,
471 module_init_registry,
472 auth_ui,
473 auth_api,
474 Vec::new(),
475 )
476 .await
477}
478
479#[allow(clippy::too_many_arguments)]
481pub async fn run_config_gen_with_iroh_p2p_relays(
482 data_dir: PathBuf,
483 settings: ConfigGenSettings,
484 db: Database,
485 task_group: &TaskGroup,
486 code_version_str: String,
487 code_version_hash: String,
488 api_secrets: ApiSecrets,
489 setup_ui_handler: SetupUiRouter,
490 module_init_registry: ServerModuleInitRegistry,
491 auth_ui: Option<ApiAuth>,
492 auth_api: Option<ApiAuth>,
493 iroh_p2p_relays: Vec<SafeUrl>,
494) -> anyhow::Result<(
495 ServerConfig,
496 DynP2PConnections<P2PMessage>,
497 P2PStatusReceivers,
498)> {
499 info!(target: LOG_CONSENSUS, "Starting config gen");
500
501 initialize_gauge_metrics(task_group, &db).await;
502
503 let (cgp_sender, mut cgp_receiver) = tokio::sync::mpsc::channel(1);
504
505 let setup_api = SetupApi::new(
506 settings.clone(),
507 db.clone(),
508 cgp_sender,
509 code_version_str.clone(),
510 code_version_hash,
511 auth_ui,
512 auth_api,
513 );
514
515 let mut rpc_module = RpcModule::new(setup_api.clone());
516
517 net::api::attach_endpoints(&mut rpc_module, config::setup::server_endpoints(), None);
518
519 let api_handler = net::api::spawn(
520 "setup",
521 settings.api_bind,
523 rpc_module,
524 10,
525 api_secrets.clone(),
526 )
527 .await;
528
529 let ui_task_group = TaskGroup::new();
530
531 let ui_service = setup_ui_handler(setup_api.clone().into_dyn()).into_make_service();
532
533 let ui_listener = TcpListener::bind(settings.ui_bind)
534 .await
535 .expect("Failed to bind setup UI");
536
537 ui_task_group.spawn("setup-ui", move |handle| async move {
538 axum::serve(ui_listener, ui_service)
539 .with_graceful_shutdown(handle.make_shutdown_rx())
540 .await
541 .expect("Failed to serve setup UI");
542 });
543
544 info!(target: LOG_CONSENSUS, "Setup UI running at http://{} 🚀", settings.ui_bind);
545
546 loop {
547 let config_gen_outcome = cgp_receiver
548 .recv()
549 .await
550 .expect("Config gen params receiver closed unexpectedly");
551
552 match config_gen_outcome {
553 ConfigGenOutcome::Generated(cg_params) => {
554 sleep(Duration::from_millis(100)).await;
558
559 api_handler
560 .stop()
561 .expect("Config api should still be running");
562
563 api_handler.stopped().await;
564
565 ui_task_group
566 .shutdown_join_all(None)
567 .await
568 .context("Failed to shutdown UI server after config gen")?;
569
570 let cg_params = *cg_params;
571 let connector = if cg_params.iroh_endpoints().is_empty() {
572 TlsTcpConnector::new(
573 cg_params.tls_config(),
574 settings.p2p_bind,
575 cg_params.p2p_urls(),
576 cg_params.identity,
577 )
578 .await
579 .into_dyn()
580 } else {
581 IrohConnector::new(
582 cg_params
583 .iroh_p2p_sk
584 .clone()
585 .expect("Iroh p2p secret key is required for iroh endpoints"),
586 settings.p2p_bind,
587 settings.iroh_dns,
588 iroh_p2p_relays,
589 cg_params
590 .iroh_endpoints()
591 .iter()
592 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
593 .collect(),
594 )
595 .await?
596 .into_dyn()
597 };
598
599 let (p2p_status_senders, p2p_status_receivers) =
600 p2p_status_channels(connector.peers());
601
602 let connections = ReconnectP2PConnections::new(
603 cg_params.identity,
604 connector,
605 task_group,
606 p2p_status_senders,
607 None,
608 )
609 .into_dyn();
610
611 let cfg = ServerConfig::distributed_gen(
612 &cg_params,
613 module_init_registry.clone(),
614 code_version_str.clone(),
615 connections.clone(),
616 p2p_status_receivers.clone(),
617 )
618 .await?;
619
620 assert_ne!(
621 cfg.consensus.iroh_endpoints.is_empty(),
622 cfg.consensus.api_endpoints.is_empty(),
623 );
624
625 write_server_config(
626 &cfg,
627 &data_dir,
628 &module_init_registry,
629 api_secrets.get_active(),
630 )?;
631
632 return Ok((cfg, connections, p2p_status_receivers));
633 }
634 ConfigGenOutcome::Restored(restored, restore_result_sender) => {
635 let result: anyhow::Result<_> = async {
639 let cfg = *restored;
640
641 module_init_registry.decoders_strict(
642 cfg.consensus
643 .modules
644 .iter()
645 .map(|(id, config)| (*id, &config.kind)),
646 )?;
647
648 cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
649
650 if cfg.consensus.iroh_endpoints.is_empty() {
651 validate_restored_tcp_config(&cfg)?;
652 }
653
654 let connector = if cfg.consensus.iroh_endpoints.is_empty() {
657 TlsTcpConnector::new(
658 cfg.tls_config(),
659 settings.p2p_bind,
660 cfg.local.p2p_endpoints.clone(),
661 cfg.local.identity,
662 )
663 .await
664 .into_dyn()
665 } else {
666 let iroh_p2p_sk = restored_iroh_p2p_key(&cfg)?;
667
668 IrohConnector::new(
669 iroh_p2p_sk,
670 settings.p2p_bind,
671 settings.iroh_dns.clone(),
672 iroh_p2p_relays.clone(),
673 cfg.consensus
674 .iroh_endpoints
675 .iter()
676 .map(|(peer, endpoints)| (*peer, endpoints.p2p_pk))
677 .collect(),
678 )
679 .await?
680 .into_dyn()
681 };
682
683 let (p2p_status_senders, p2p_status_receivers) =
684 p2p_status_channels(connector.peers());
685
686 write_server_config(
689 &cfg,
690 &data_dir,
691 &module_init_registry,
692 api_secrets.get_active(),
693 )?;
694
695 Ok((cfg, connector, p2p_status_senders, p2p_status_receivers))
696 }
697 .await;
698
699 let ack = result
700 .as_ref()
701 .map(|_| ())
702 .map_err(std::string::ToString::to_string);
703 let restore_failed = ack.is_err();
704 let _ = restore_result_sender.send(ack);
705
706 if restore_failed {
707 continue;
708 }
709
710 sleep(Duration::from_millis(100)).await;
713
714 api_handler
715 .stop()
716 .expect("Config api should still be running");
717
718 api_handler.stopped().await;
719
720 ui_task_group
721 .shutdown_join_all(None)
722 .await
723 .context("Failed to shutdown UI server after restored config install")?;
724
725 let (cfg, connector, p2p_status_senders, p2p_status_receivers) = result?;
726 let connections = ReconnectP2PConnections::new(
727 cfg.local.identity,
728 connector,
729 task_group,
730 p2p_status_senders,
731 None,
732 )
733 .into_dyn();
734
735 return Ok((cfg, connections, p2p_status_receivers));
736 }
737 }
738 }
739}