1pub mod aleph_bft;
2pub mod api;
3pub mod db;
4pub mod debug;
5pub mod engine;
6mod iroh_api;
7pub mod peer_availability;
8pub mod transaction;
9
10use std::collections::BTreeMap;
11use std::net::SocketAddr;
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::time::Duration;
15
16use anyhow::{Context as _, bail, ensure};
17use async_channel::Sender;
18use db::{ServerDbMigrationContext, get_global_database_migrations};
19use fedimint_api_client::api::DynGlobalApi;
20use fedimint_connectors::ConnectorRegistry;
21use fedimint_core::NumPeers;
22use fedimint_core::config::P2PMessage;
23use fedimint_core::core::{ModuleInstanceId, ModuleKind};
24use fedimint_core::db::{Database, apply_migrations_dbtx, verify_module_db_integrity_dbtx};
25use fedimint_core::envs::is_running_in_test_env;
26use fedimint_core::module::registry::ModuleRegistry;
27use fedimint_core::module::{ApiAuth, FEDIMINT_API_ALPN};
28use fedimint_core::net::DynP2PConnections;
29use fedimint_core::net::iroh::build_iroh_endpoint;
30use fedimint_core::session_outcome::ConsensusItem;
31use fedimint_core::task::{TaskGroup, sleep};
32use fedimint_core::util::SafeUrl;
33use fedimint_logging::{LOG_CONSENSUS, LOG_CORE};
34use fedimint_server_core::bitcoin_rpc::{DynServerBitcoinRpc, ServerBitcoinRpcMonitor};
35use fedimint_server_core::dashboard_ui::IDashboardApi;
36use fedimint_server_core::migration::apply_migrations_server_dbtx;
37use fedimint_server_core::{DynServerModule, ServerModuleInitRegistry};
38use jsonrpsee::RpcModule;
39use jsonrpsee::server::ServerHandle;
40use tokio::net::TcpListener;
41use tokio::sync::watch;
42use tracing::{debug, info, warn};
43
44use crate::config::{ServerConfig, ServerConfigLocal};
45use crate::connection_limits::ConnectionLimits;
46use crate::consensus::api::ConsensusApi;
47use crate::consensus::engine::ConsensusEngine;
48use crate::consensus::iroh_api::{IrohApiState, run_iroh_api, run_iroh_api_next};
49use crate::db::verify_server_db_integrity_dbtx;
50use crate::net::api::ApiSecrets;
51use crate::net::api::announcement::get_api_urls;
52use crate::net::api::guardian_metadata::{
53 prepare_guardian_metadata_service, reconcile_guardian_metadata, start_guardian_metadata_service,
54};
55use crate::net::iroh::{build_iroh_v1_endpoint, derive_iroh_v1_api_secret_key};
56use crate::net::p2p::P2PStatusReceivers;
57use crate::{DashboardUiRouter, IrohNextApiSettings, net, update_server_info_version_dbtx};
58
59const TRANSACTION_BUFFER: usize = 1000;
61
62struct IrohApiEndpoints {
63 legacy: Option<iroh::Endpoint>,
64 next: Option<iroh_next::Endpoint>,
65}
66
67fn eligible_iroh_next_api_settings(
68 has_legacy_iroh_api: bool,
69 iroh_next_api_settings: Option<&IrohNextApiSettings>,
70) -> Option<&IrohNextApiSettings> {
71 if iroh_next_api_settings.is_some() && !has_legacy_iroh_api {
72 warn!(
73 target: LOG_CONSENSUS,
74 "Not starting the transitional Iroh 1.0 API because this federation was configured \
75 without the legacy Iroh API"
76 );
77 None
78 } else {
79 iroh_next_api_settings
80 }
81}
82
83fn ensure_api_secrets_enforceable(
96 has_iroh_api: bool,
97 force_api_secrets: &ApiSecrets,
98) -> anyhow::Result<()> {
99 ensure!(
100 !has_iroh_api || force_api_secrets.is_empty(),
101 "This federation serves its API over Iroh, which cannot authenticate clients, so the \
102 configured API secrets would not be enforced. Either unset FM_FORCE_API_SECRETS or run a \
103 federation that does not use Iroh."
104 );
105
106 Ok(())
107}
108
109#[cfg(test)]
110#[test]
111fn api_secrets_are_only_accepted_without_the_iroh_api() {
112 use std::str::FromStr as _;
113
114 let secrets = ApiSecrets::from_str("secret").expect("valid api secret");
115
116 ensure_api_secrets_enforceable(false, &secrets).expect("websocket api enforces secrets");
117 ensure_api_secrets_enforceable(true, &ApiSecrets::none()).expect("no secrets to enforce");
118 ensure_api_secrets_enforceable(true, &secrets)
119 .expect_err("the iroh api cannot enforce secrets");
120}
121
122fn resolve_iroh_next_api_bind(
123 api_bind: SocketAddr,
124 iroh_next_api_settings: Option<&IrohNextApiSettings>,
125) -> anyhow::Result<Option<SocketAddr>> {
126 iroh_next_api_settings
127 .map(|settings| {
128 settings.bind_override().map_or_else(
129 || {
130 let mut bind = api_bind;
131 bind.set_port(
132 bind.port()
133 .checked_add(10)
134 .context("Default Iroh 1.0 API bind port would overflow")?,
135 );
136 anyhow::Ok(bind)
137 },
138 anyhow::Ok,
139 )
140 })
141 .transpose()
142}
143
144#[cfg(test)]
145#[test]
146fn ineligible_iroh_next_api_does_not_validate_unused_default_bind() {
147 let api_bind = "127.0.0.1:65535".parse().expect("valid socket address");
148 let settings = IrohNextApiSettings::new(None);
149 let settings = eligible_iroh_next_api_settings(false, Some(&settings));
150
151 assert!(
152 resolve_iroh_next_api_bind(api_bind, settings)
153 .expect("ineligible listener should be skipped")
154 .is_none()
155 );
156}
157
158async fn prepare_iroh_api_endpoints(
159 cfg: &ServerConfig,
160 api_bind: SocketAddr,
161 iroh_dns: Option<SafeUrl>,
162 iroh_relays: Vec<SafeUrl>,
163 iroh_next_api_settings: Option<&IrohNextApiSettings>,
164) -> anyhow::Result<IrohApiEndpoints> {
165 let legacy = if let Some(iroh_api_sk) = cfg.private.iroh_api_sk.clone() {
166 Some(
167 build_iroh_endpoint(
168 iroh_api_sk,
169 api_bind,
170 iroh_dns.clone(),
171 iroh_relays,
172 FEDIMINT_API_ALPN,
173 )
174 .await?,
175 )
176 } else {
177 None
178 };
179
180 let next_bind = resolve_iroh_next_api_bind(api_bind, iroh_next_api_settings)?;
181 let next = if let Some(bind) = next_bind {
182 let next_api_sk = derive_iroh_v1_api_secret_key(&cfg.private.broadcast_secret_key);
183 Some(build_iroh_v1_endpoint(next_api_sk, bind, iroh_dns, FEDIMINT_API_ALPN).await?)
184 } else {
185 None
186 };
187
188 Ok(IrohApiEndpoints { legacy, next })
189}
190
191fn spawn_iroh_api_tasks(
192 consensus_api: ConsensusApi,
193 iroh_api_limits: ConnectionLimits,
194 endpoints: IrohApiEndpoints,
195 task_group: &TaskGroup,
196) {
197 let iroh_api = IrohApiState::new(consensus_api, iroh_api_limits);
198
199 if let Some(endpoint) = endpoints.legacy {
200 task_group.spawn_cancellable(
201 "iroh-api",
202 run_iroh_api(iroh_api.clone(), endpoint, task_group.clone()),
203 );
204 }
205
206 if let Some(endpoint) = endpoints.next {
207 task_group.spawn_cancellable(
208 "iroh-next-api",
209 run_iroh_api_next(iroh_api, endpoint, task_group.clone()),
210 );
211 }
212}
213
214#[allow(clippy::too_many_arguments)]
215pub async fn run(
216 connectors: ConnectorRegistry,
217 auth_ui: Option<ApiAuth>,
218 auth_api: Option<ApiAuth>,
219 connections: DynP2PConnections<P2PMessage>,
220 p2p_status_receivers: P2PStatusReceivers,
221 api_bind: SocketAddr,
222 iroh_dns: Option<SafeUrl>,
223 iroh_relays: Vec<SafeUrl>,
224 cfg: ServerConfig,
225 db: Database,
226 module_init_registry: ServerModuleInitRegistry,
227 task_group: &TaskGroup,
228 force_api_secrets: ApiSecrets,
229 data_dir: PathBuf,
230 code_version_str: String,
231 code_version_hash: String,
232 dyn_server_bitcoin_rpc: DynServerBitcoinRpc,
233 ui_bind: SocketAddr,
234 dashboard_ui_router: DashboardUiRouter,
235 db_checkpoint_retention: u64,
236 session_timeout: Duration,
237 iroh_api_limits: ConnectionLimits,
238 iroh_next_api_settings: Option<&IrohNextApiSettings>,
239) -> anyhow::Result<()> {
240 cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
241
242 let iroh_next_api_settings =
243 eligible_iroh_next_api_settings(cfg.private.iroh_api_sk.is_some(), iroh_next_api_settings);
244
245 ensure_api_secrets_enforceable(
246 cfg.private.iroh_api_sk.is_some() || iroh_next_api_settings.is_some(),
247 &force_api_secrets,
248 )?;
249
250 let mut global_dbtx = db.begin_transaction().await;
251 apply_migrations_server_dbtx(
252 &mut global_dbtx.to_ref_nc(),
253 Arc::new(ServerDbMigrationContext),
254 "fedimint-server".to_string(),
255 get_global_database_migrations(),
256 )
257 .await?;
258
259 update_server_info_version_dbtx(&mut global_dbtx.to_ref_nc(), &code_version_str).await;
260
261 if is_running_in_test_env() {
262 verify_server_db_integrity_dbtx(&mut global_dbtx.to_ref_nc()).await;
263 }
264 global_dbtx.commit_tx_result().await?;
265
266 let mut modules = BTreeMap::new();
267
268 let global_api = DynGlobalApi::new(
270 connectors.clone(),
271 cfg.consensus
272 .api_endpoints()
273 .iter()
274 .map(|(&peer_id, url)| (peer_id, url.url.clone()))
275 .collect(),
276 None,
277 );
278
279 let bitcoin_rpc_connection = ServerBitcoinRpcMonitor::new(
280 dyn_server_bitcoin_rpc,
281 if is_running_in_test_env() {
282 Duration::from_millis(100)
283 } else {
284 Duration::from_mins(1)
285 },
286 task_group,
287 );
288
289 for (module_id, module_cfg) in &cfg.consensus.modules {
290 match module_init_registry.get(&module_cfg.kind) {
291 Some(module_init) => {
292 info!(target: LOG_CORE, "Initialise module {module_id}...");
293
294 let mut dbtx = db.begin_transaction().await;
295 apply_migrations_dbtx(
296 &mut dbtx.to_ref_nc(),
297 Arc::new(ServerDbMigrationContext) as Arc<_>,
298 module_init.module_kind().to_string(),
299 module_init.get_database_migrations(),
300 Some(*module_id),
301 None,
302 )
303 .await?;
304
305 if let Some(used_db_prefixes) = module_init.used_db_prefixes()
306 && is_running_in_test_env()
307 {
308 verify_module_db_integrity_dbtx(
309 &mut dbtx.to_ref_nc(),
310 *module_id,
311 module_init.module_kind(),
312 &used_db_prefixes,
313 )
314 .await;
315 }
316 dbtx.commit_tx_result().await?;
317
318 let module = module_init
319 .init(
320 NumPeers::from(cfg.consensus.api_endpoints().len()),
321 cfg.get_module_config(*module_id)?,
322 db.with_prefix_module_id(*module_id).0,
323 task_group,
324 cfg.local.identity,
325 global_api.with_module(*module_id),
326 bitcoin_rpc_connection.clone(),
327 )
328 .await?;
329
330 modules.insert(*module_id, (module_cfg.kind.clone(), module));
331 }
332 None => bail!("Detected configuration for unsupported module id: {module_id}"),
333 }
334 }
335
336 let module_registry = ModuleRegistry::from(modules);
337
338 let client_cfg = cfg.consensus.to_client_config(&module_init_registry)?;
339
340 let (submission_sender, submission_receiver) = async_channel::bounded(TRANSACTION_BUFFER);
341 let (shutdown_sender, shutdown_receiver) = watch::channel(None);
342 let (ord_latency_sender, ord_latency_receiver) = watch::channel(None);
343 let (accepted_item_sender, accepted_item_receiver) = watch::channel(());
344 let (completed_session_sender, completed_session_receiver) = watch::channel(());
345
346 let mut ci_status_senders = BTreeMap::new();
347 let mut ci_status_receivers = BTreeMap::new();
348
349 for peer in cfg.consensus.broadcast_public_keys.keys().copied() {
350 let (ci_sender, ci_receiver) = watch::channel(None);
351
352 ci_status_senders.insert(peer, ci_sender);
353 ci_status_receivers.insert(peer, ci_receiver);
354 }
355
356 let supported_api_versions =
357 ServerConfig::supported_api_versions_summary(&cfg.consensus.modules, &module_registry);
358 debug!(
359 target: LOG_CONSENSUS,
360 ?supported_api_versions,
361 "Supported API versions",
362 );
363
364 let consensus_api = ConsensusApi {
365 cfg: cfg.clone(),
366 db: db.clone(),
367 modules: module_registry.clone(),
368 client_cfg: client_cfg.clone(),
369 submission_sender: submission_sender.clone(),
370 shutdown_sender,
371 shutdown_receiver: shutdown_receiver.clone(),
372 supported_api_versions,
373 auth_ui,
374 auth_api,
375 p2p_status_receivers,
376 ci_status_receivers,
377 ord_latency_receiver,
378 accepted_item_receiver,
379 completed_session_receiver,
380 bitcoin_rpc_connection: bitcoin_rpc_connection.clone(),
381 force_api_secret: force_api_secrets.get_active(),
382 code_version_str,
383 code_version_hash,
384 task_group: task_group.clone(),
385 };
386
387 let guardian_metadata_api =
388 prepare_guardian_metadata_service(&db, &cfg, force_api_secrets.get_active()).await?;
389
390 let iroh_api_endpoints = prepare_iroh_api_endpoints(
391 &cfg,
392 api_bind,
393 iroh_dns,
394 iroh_relays,
395 iroh_next_api_settings,
396 )
397 .await?;
398
399 let guardian_metadata_updated =
400 reconcile_guardian_metadata(&db, &cfg, iroh_next_api_settings).await?;
401
402 info!(target: LOG_CONSENSUS, "Starting Consensus Api...");
403
404 let api_handler = start_consensus_api(
405 &cfg.local,
406 consensus_api.clone(),
407 force_api_secrets.clone(),
408 api_bind,
409 )
410 .await;
411
412 spawn_iroh_api_tasks(
413 consensus_api.clone(),
414 iroh_api_limits,
415 iroh_api_endpoints,
416 task_group,
417 );
418
419 start_guardian_metadata_service(
420 &db,
421 task_group,
422 &cfg,
423 guardian_metadata_api,
424 guardian_metadata_updated,
425 );
426
427 info!(target: LOG_CONSENSUS, "Starting Submission of Module CI proposals...");
428
429 for (module_id, kind, module) in module_registry.iter_modules() {
430 submit_module_ci_proposals(
431 task_group,
432 db.clone(),
433 module_id,
434 kind.clone(),
435 module.clone(),
436 submission_sender.clone(),
437 );
438 }
439
440 let ui_service = dashboard_ui_router(consensus_api.clone().into_dyn()).into_make_service();
441
442 let ui_listener = TcpListener::bind(ui_bind)
443 .await
444 .expect("Failed to bind dashboard UI");
445
446 task_group.spawn("dashboard-ui", move |handle| async move {
447 axum::serve(ui_listener, ui_service)
448 .with_graceful_shutdown(handle.make_shutdown_rx())
449 .await
450 .expect("Failed to serve dashboard UI");
451 });
452
453 info!(target: LOG_CONSENSUS, "Dashboard UI running at http://{ui_bind} 🚀");
454
455 loop {
456 match bitcoin_rpc_connection.status() {
457 Some(status) => {
458 if let Some(progress) = status.sync_progress {
459 if progress >= 0.999 {
460 break;
461 }
462
463 info!(
464 target: LOG_CONSENSUS,
465 "Waiting for bitcoin backend to sync... {:.1}%",
466 progress * 100.0
467 );
468 } else {
469 break;
470 }
471 }
472 None => {
473 info!(target: LOG_CONSENSUS, "Waiting to connect to bitcoin backend...");
474 }
475 }
476
477 sleep(Duration::from_secs(1)).await;
478 }
479
480 info!(target: LOG_CONSENSUS, "Starting Consensus Engine...");
481
482 let api_urls = get_api_urls(&db, &cfg.consensus).await;
483
484 let peer_activity = Arc::new(std::sync::Mutex::new(peer_availability::PeerActivity::new(
485 cfg.local.identity,
486 fedimint_core::NumPeersExt::to_num_peers(&cfg.consensus.broadcast_public_keys),
487 )));
488
489 task_group.spawn_cancellable(
490 "peer-availability",
491 peer_availability::persist_peer_availability(db.clone(), peer_activity.clone()),
492 );
493
494 ConsensusEngine {
497 db,
498 federation_api: DynGlobalApi::new(
499 connectors,
500 api_urls,
501 force_api_secrets.get_active().as_deref(),
502 ),
503 cfg: cfg.clone(),
504 connections,
505 ord_latency_sender,
506 accepted_item_sender,
507 completed_session_sender,
508 ci_status_senders,
509 submission_receiver,
510 shutdown_receiver,
511 modules: module_registry,
512 task_group: task_group.clone(),
513 data_dir,
514 db_checkpoint_retention,
515 session_timeout,
516 peer_activity,
517 }
518 .run()
519 .await?;
520
521 api_handler
522 .stop()
523 .expect("Consensus api should still be running");
524
525 api_handler.stopped().await;
526
527 Ok(())
528}
529
530async fn start_consensus_api(
531 cfg: &ServerConfigLocal,
532 api: ConsensusApi,
533 force_api_secrets: ApiSecrets,
534 api_bind: SocketAddr,
535) -> ServerHandle {
536 let mut rpc_module = RpcModule::new(api.clone());
537
538 net::api::attach_endpoints(&mut rpc_module, api::server_endpoints(), None);
539
540 for (id, _, module) in api.modules.iter_modules() {
541 net::api::attach_endpoints(&mut rpc_module, module.api_endpoints(), Some(id));
542 }
543
544 net::api::spawn(
545 "consensus",
546 api_bind,
547 rpc_module,
548 cfg.max_connections,
549 force_api_secrets,
550 )
551 .await
552}
553
554const CONSENSUS_PROPOSAL_TIMEOUT: Duration = Duration::from_secs(30);
555
556fn submit_module_ci_proposals(
557 task_group: &TaskGroup,
558 db: Database,
559 module_id: ModuleInstanceId,
560 kind: ModuleKind,
561 module: DynServerModule,
562 submission_sender: Sender<ConsensusItem>,
563) {
564 let mut interval = tokio::time::interval(if is_running_in_test_env() {
565 Duration::from_millis(100)
566 } else {
567 Duration::from_secs(1)
568 });
569
570 task_group.spawn(
571 format!("citem_proposals_{module_id}"),
572 move |task_handle| async move {
573 while !task_handle.is_shutting_down() {
574 let module_consensus_items = tokio::time::timeout(
575 CONSENSUS_PROPOSAL_TIMEOUT,
576 module.consensus_proposal(
577 &mut db
578 .begin_transaction_nc()
579 .await
580 .to_ref_with_prefix_module_id(module_id)
581 .0
582 .into_nc(),
583 module_id,
584 ),
585 )
586 .await;
587
588 match module_consensus_items {
589 Ok(items) => {
590 for item in items {
591 if submission_sender
592 .send(ConsensusItem::Module(item))
593 .await
594 .is_err()
595 {
596 warn!(
597 target: LOG_CONSENSUS,
598 module_id,
599 "Unable to submit module consensus item proposal via channel"
600 );
601 }
602 }
603 }
604 Err(..) => {
605 warn!(
606 target: LOG_CONSENSUS,
607 module_id,
608 %kind,
609 "Module failed to propose consensus items on time"
610 );
611 }
612 }
613
614 interval.tick().await;
615 }
616 },
617 );
618}