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};
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 resolve_iroh_next_api_bind(
83 api_bind: SocketAddr,
84 iroh_next_api_settings: Option<&IrohNextApiSettings>,
85) -> anyhow::Result<Option<SocketAddr>> {
86 iroh_next_api_settings
87 .map(|settings| {
88 settings.bind_override().map_or_else(
89 || {
90 let mut bind = api_bind;
91 bind.set_port(
92 bind.port()
93 .checked_add(10)
94 .context("Default Iroh 1.0 API bind port would overflow")?,
95 );
96 anyhow::Ok(bind)
97 },
98 anyhow::Ok,
99 )
100 })
101 .transpose()
102}
103
104#[cfg(test)]
105#[test]
106fn ineligible_iroh_next_api_does_not_validate_unused_default_bind() {
107 let api_bind = "127.0.0.1:65535".parse().expect("valid socket address");
108 let settings = IrohNextApiSettings::new(None);
109 let settings = eligible_iroh_next_api_settings(false, Some(&settings));
110
111 assert!(
112 resolve_iroh_next_api_bind(api_bind, settings)
113 .expect("ineligible listener should be skipped")
114 .is_none()
115 );
116}
117
118async fn prepare_iroh_api_endpoints(
119 cfg: &ServerConfig,
120 api_bind: SocketAddr,
121 iroh_dns: Option<SafeUrl>,
122 iroh_relays: Vec<SafeUrl>,
123 iroh_next_api_settings: Option<&IrohNextApiSettings>,
124) -> anyhow::Result<IrohApiEndpoints> {
125 let legacy = if let Some(iroh_api_sk) = cfg.private.iroh_api_sk.clone() {
126 Some(
127 build_iroh_endpoint(
128 iroh_api_sk,
129 api_bind,
130 iroh_dns.clone(),
131 iroh_relays,
132 FEDIMINT_API_ALPN,
133 )
134 .await?,
135 )
136 } else {
137 None
138 };
139
140 let next_bind = resolve_iroh_next_api_bind(api_bind, iroh_next_api_settings)?;
141 let next = if let Some(bind) = next_bind {
142 let next_api_sk = derive_iroh_v1_api_secret_key(&cfg.private.broadcast_secret_key);
143 Some(build_iroh_v1_endpoint(next_api_sk, bind, iroh_dns, FEDIMINT_API_ALPN).await?)
144 } else {
145 None
146 };
147
148 Ok(IrohApiEndpoints { legacy, next })
149}
150
151fn spawn_iroh_api_tasks(
152 consensus_api: ConsensusApi,
153 iroh_api_limits: ConnectionLimits,
154 endpoints: IrohApiEndpoints,
155 task_group: &TaskGroup,
156) {
157 let iroh_api = IrohApiState::new(consensus_api, iroh_api_limits);
158
159 if let Some(endpoint) = endpoints.legacy {
160 task_group.spawn_cancellable(
161 "iroh-api",
162 run_iroh_api(iroh_api.clone(), endpoint, task_group.clone()),
163 );
164 }
165
166 if let Some(endpoint) = endpoints.next {
167 task_group.spawn_cancellable(
168 "iroh-next-api",
169 run_iroh_api_next(iroh_api, endpoint, task_group.clone()),
170 );
171 }
172}
173
174#[allow(clippy::too_many_arguments)]
175pub async fn run(
176 connectors: ConnectorRegistry,
177 auth_ui: Option<ApiAuth>,
178 auth_api: Option<ApiAuth>,
179 connections: DynP2PConnections<P2PMessage>,
180 p2p_status_receivers: P2PStatusReceivers,
181 api_bind: SocketAddr,
182 iroh_dns: Option<SafeUrl>,
183 iroh_relays: Vec<SafeUrl>,
184 cfg: ServerConfig,
185 db: Database,
186 module_init_registry: ServerModuleInitRegistry,
187 task_group: &TaskGroup,
188 force_api_secrets: ApiSecrets,
189 data_dir: PathBuf,
190 code_version_str: String,
191 code_version_hash: String,
192 dyn_server_bitcoin_rpc: DynServerBitcoinRpc,
193 ui_bind: SocketAddr,
194 dashboard_ui_router: DashboardUiRouter,
195 db_checkpoint_retention: u64,
196 session_timeout: Duration,
197 iroh_api_limits: ConnectionLimits,
198 iroh_next_api_settings: Option<&IrohNextApiSettings>,
199) -> anyhow::Result<()> {
200 cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
201
202 let iroh_next_api_settings =
203 eligible_iroh_next_api_settings(cfg.private.iroh_api_sk.is_some(), iroh_next_api_settings);
204
205 let mut global_dbtx = db.begin_transaction().await;
206 apply_migrations_server_dbtx(
207 &mut global_dbtx.to_ref_nc(),
208 Arc::new(ServerDbMigrationContext),
209 "fedimint-server".to_string(),
210 get_global_database_migrations(),
211 )
212 .await?;
213
214 update_server_info_version_dbtx(&mut global_dbtx.to_ref_nc(), &code_version_str).await;
215
216 if is_running_in_test_env() {
217 verify_server_db_integrity_dbtx(&mut global_dbtx.to_ref_nc()).await;
218 }
219 global_dbtx.commit_tx_result().await?;
220
221 let mut modules = BTreeMap::new();
222
223 let global_api = DynGlobalApi::new(
225 connectors.clone(),
226 cfg.consensus
227 .api_endpoints()
228 .iter()
229 .map(|(&peer_id, url)| (peer_id, url.url.clone()))
230 .collect(),
231 None,
232 )?;
233
234 let bitcoin_rpc_connection = ServerBitcoinRpcMonitor::new(
235 dyn_server_bitcoin_rpc,
236 if is_running_in_test_env() {
237 Duration::from_millis(100)
238 } else {
239 Duration::from_mins(1)
240 },
241 task_group,
242 );
243
244 for (module_id, module_cfg) in &cfg.consensus.modules {
245 match module_init_registry.get(&module_cfg.kind) {
246 Some(module_init) => {
247 info!(target: LOG_CORE, "Initialise module {module_id}...");
248
249 let mut dbtx = db.begin_transaction().await;
250 apply_migrations_dbtx(
251 &mut dbtx.to_ref_nc(),
252 Arc::new(ServerDbMigrationContext) as Arc<_>,
253 module_init.module_kind().to_string(),
254 module_init.get_database_migrations(),
255 Some(*module_id),
256 None,
257 )
258 .await?;
259
260 if let Some(used_db_prefixes) = module_init.used_db_prefixes()
261 && is_running_in_test_env()
262 {
263 verify_module_db_integrity_dbtx(
264 &mut dbtx.to_ref_nc(),
265 *module_id,
266 module_init.module_kind(),
267 &used_db_prefixes,
268 )
269 .await;
270 }
271 dbtx.commit_tx_result().await?;
272
273 let module = module_init
274 .init(
275 NumPeers::from(cfg.consensus.api_endpoints().len()),
276 cfg.get_module_config(*module_id)?,
277 db.with_prefix_module_id(*module_id).0,
278 task_group,
279 cfg.local.identity,
280 global_api.with_module(*module_id),
281 bitcoin_rpc_connection.clone(),
282 )
283 .await?;
284
285 modules.insert(*module_id, (module_cfg.kind.clone(), module));
286 }
287 None => bail!("Detected configuration for unsupported module id: {module_id}"),
288 }
289 }
290
291 let module_registry = ModuleRegistry::from(modules);
292
293 let client_cfg = cfg.consensus.to_client_config(&module_init_registry)?;
294
295 let (submission_sender, submission_receiver) = async_channel::bounded(TRANSACTION_BUFFER);
296 let (shutdown_sender, shutdown_receiver) = watch::channel(None);
297 let (ord_latency_sender, ord_latency_receiver) = watch::channel(None);
298
299 let mut ci_status_senders = BTreeMap::new();
300 let mut ci_status_receivers = BTreeMap::new();
301
302 for peer in cfg.consensus.broadcast_public_keys.keys().copied() {
303 let (ci_sender, ci_receiver) = watch::channel(None);
304
305 ci_status_senders.insert(peer, ci_sender);
306 ci_status_receivers.insert(peer, ci_receiver);
307 }
308
309 let supported_api_versions =
310 ServerConfig::supported_api_versions_summary(&cfg.consensus.modules, &module_registry);
311 debug!(
312 target: LOG_CONSENSUS,
313 ?supported_api_versions,
314 "Supported API versions",
315 );
316
317 let consensus_api = ConsensusApi {
318 cfg: cfg.clone(),
319 db: db.clone(),
320 modules: module_registry.clone(),
321 client_cfg: client_cfg.clone(),
322 submission_sender: submission_sender.clone(),
323 shutdown_sender,
324 shutdown_receiver: shutdown_receiver.clone(),
325 supported_api_versions,
326 auth_ui,
327 auth_api,
328 p2p_status_receivers,
329 ci_status_receivers,
330 ord_latency_receiver,
331 bitcoin_rpc_connection: bitcoin_rpc_connection.clone(),
332 force_api_secret: force_api_secrets.get_active(),
333 code_version_str,
334 code_version_hash,
335 task_group: task_group.clone(),
336 };
337
338 let guardian_metadata_api =
339 prepare_guardian_metadata_service(&db, &cfg, force_api_secrets.get_active()).await?;
340
341 let iroh_api_endpoints = prepare_iroh_api_endpoints(
342 &cfg,
343 api_bind,
344 iroh_dns,
345 iroh_relays,
346 iroh_next_api_settings,
347 )
348 .await?;
349
350 let guardian_metadata_updated =
351 reconcile_guardian_metadata(&db, &cfg, iroh_next_api_settings).await?;
352
353 info!(target: LOG_CONSENSUS, "Starting Consensus Api...");
354
355 let api_handler = start_consensus_api(
356 &cfg.local,
357 consensus_api.clone(),
358 force_api_secrets.clone(),
359 api_bind,
360 )
361 .await;
362
363 spawn_iroh_api_tasks(
364 consensus_api.clone(),
365 iroh_api_limits,
366 iroh_api_endpoints,
367 task_group,
368 );
369
370 start_guardian_metadata_service(
371 &db,
372 task_group,
373 &cfg,
374 guardian_metadata_api,
375 guardian_metadata_updated,
376 );
377
378 info!(target: LOG_CONSENSUS, "Starting Submission of Module CI proposals...");
379
380 for (module_id, kind, module) in module_registry.iter_modules() {
381 submit_module_ci_proposals(
382 task_group,
383 db.clone(),
384 module_id,
385 kind.clone(),
386 module.clone(),
387 submission_sender.clone(),
388 );
389 }
390
391 let ui_service = dashboard_ui_router(consensus_api.clone().into_dyn()).into_make_service();
392
393 let ui_listener = TcpListener::bind(ui_bind)
394 .await
395 .expect("Failed to bind dashboard UI");
396
397 task_group.spawn("dashboard-ui", move |handle| async move {
398 axum::serve(ui_listener, ui_service)
399 .with_graceful_shutdown(handle.make_shutdown_rx())
400 .await
401 .expect("Failed to serve dashboard UI");
402 });
403
404 info!(target: LOG_CONSENSUS, "Dashboard UI running at http://{ui_bind} 🚀");
405
406 loop {
407 match bitcoin_rpc_connection.status() {
408 Some(status) => {
409 if let Some(progress) = status.sync_progress {
410 if progress >= 0.999 {
411 break;
412 }
413
414 info!(target: LOG_CONSENSUS, "Waiting for bitcoin backend to sync... {progress:.1}%");
415 } else {
416 break;
417 }
418 }
419 None => {
420 info!(target: LOG_CONSENSUS, "Waiting to connect to bitcoin backend...");
421 }
422 }
423
424 sleep(Duration::from_secs(1)).await;
425 }
426
427 info!(target: LOG_CONSENSUS, "Starting Consensus Engine...");
428
429 let api_urls = get_api_urls(&db, &cfg.consensus).await;
430
431 ConsensusEngine {
434 db,
435 federation_api: DynGlobalApi::new(
436 connectors,
437 api_urls,
438 force_api_secrets.get_active().as_deref(),
439 )?,
440 cfg: cfg.clone(),
441 connections,
442 ord_latency_sender,
443 ci_status_senders,
444 submission_receiver,
445 shutdown_receiver,
446 modules: module_registry,
447 task_group: task_group.clone(),
448 data_dir,
449 db_checkpoint_retention,
450 session_timeout,
451 }
452 .run()
453 .await?;
454
455 api_handler
456 .stop()
457 .expect("Consensus api should still be running");
458
459 api_handler.stopped().await;
460
461 Ok(())
462}
463
464async fn start_consensus_api(
465 cfg: &ServerConfigLocal,
466 api: ConsensusApi,
467 force_api_secrets: ApiSecrets,
468 api_bind: SocketAddr,
469) -> ServerHandle {
470 let mut rpc_module = RpcModule::new(api.clone());
471
472 net::api::attach_endpoints(&mut rpc_module, api::server_endpoints(), None);
473
474 for (id, _, module) in api.modules.iter_modules() {
475 net::api::attach_endpoints(&mut rpc_module, module.api_endpoints(), Some(id));
476 }
477
478 net::api::spawn(
479 "consensus",
480 api_bind,
481 rpc_module,
482 cfg.max_connections,
483 force_api_secrets,
484 )
485 .await
486}
487
488const CONSENSUS_PROPOSAL_TIMEOUT: Duration = Duration::from_secs(30);
489
490fn submit_module_ci_proposals(
491 task_group: &TaskGroup,
492 db: Database,
493 module_id: ModuleInstanceId,
494 kind: ModuleKind,
495 module: DynServerModule,
496 submission_sender: Sender<ConsensusItem>,
497) {
498 let mut interval = tokio::time::interval(if is_running_in_test_env() {
499 Duration::from_millis(100)
500 } else {
501 Duration::from_secs(1)
502 });
503
504 task_group.spawn(
505 format!("citem_proposals_{module_id}"),
506 move |task_handle| async move {
507 while !task_handle.is_shutting_down() {
508 let module_consensus_items = tokio::time::timeout(
509 CONSENSUS_PROPOSAL_TIMEOUT,
510 module.consensus_proposal(
511 &mut db
512 .begin_transaction_nc()
513 .await
514 .to_ref_with_prefix_module_id(module_id)
515 .0
516 .into_nc(),
517 module_id,
518 ),
519 )
520 .await;
521
522 match module_consensus_items {
523 Ok(items) => {
524 for item in items {
525 if submission_sender
526 .send(ConsensusItem::Module(item))
527 .await
528 .is_err()
529 {
530 warn!(
531 target: LOG_CONSENSUS,
532 module_id,
533 "Unable to submit module consensus item proposal via channel"
534 );
535 }
536 }
537 }
538 Err(..) => {
539 warn!(
540 target: LOG_CONSENSUS,
541 module_id,
542 %kind,
543 "Module failed to propose consensus items on time"
544 );
545 }
546 }
547
548 interval.tick().await;
549 }
550 },
551 );
552}