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