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