1pub mod aleph_bft;
2pub mod api;
3pub mod db;
4pub mod debug;
5pub mod engine;
6pub mod transaction;
7
8use std::collections::BTreeMap;
9use std::net::SocketAddr;
10use std::path::PathBuf;
11use std::sync::Arc;
12use std::time::Duration;
13
14use anyhow::bail;
15use async_channel::Sender;
16use db::{ServerDbMigrationContext, get_global_database_migrations};
17use fedimint_api_client::api::DynGlobalApi;
18use fedimint_connectors::ConnectorRegistry;
19use fedimint_core::NumPeers;
20use fedimint_core::config::P2PMessage;
21use fedimint_core::core::{ModuleInstanceId, ModuleKind};
22use fedimint_core::db::{Database, apply_migrations_dbtx, verify_module_db_integrity_dbtx};
23use fedimint_core::envs::is_running_in_test_env;
24use fedimint_core::epoch::ConsensusItem;
25use fedimint_core::module::registry::ModuleRegistry;
26use fedimint_core::module::{
27 ApiAuth, ApiEndpoint, ApiError, ApiMethod, FEDIMINT_API_ALPN, IrohApiRequest,
28};
29use fedimint_core::net::iroh::build_iroh_endpoint;
30use fedimint_core::net::peers::DynP2PConnections;
31use fedimint_core::task::{TaskGroup, sleep};
32use fedimint_core::util::{FmtCompactAnyhow as _, SafeUrl};
33use fedimint_logging::{LOG_CONSENSUS, LOG_CORE, LOG_NET_API};
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 futures::FutureExt;
39use iroh::Endpoint;
40use iroh::endpoint::{Incoming, RecvStream, SendStream, VarInt};
41use jsonrpsee::RpcModule;
42use jsonrpsee::server::ServerHandle;
43use serde_json::Value;
44use tokio::net::TcpListener;
45use tokio::sync::{Semaphore, watch};
46use tracing::{info, warn};
47
48use crate::config::{ServerConfig, ServerConfigLocal};
49use crate::connection_limits::ConnectionLimits;
50use crate::consensus::api::{ConsensusApi, server_endpoints};
51use crate::consensus::engine::ConsensusEngine;
52use crate::db::verify_server_db_integrity_dbtx;
53use crate::metrics::{
54 IROH_API_CONNECTION_DURATION_SECONDS, IROH_API_CONNECTION_IDLE_TIMEOUT_TOTAL,
55 IROH_API_CONNECTIONS_ACTIVE, IROH_API_REQUEST_DURATION_SECONDS, IROH_API_REQUEST_RESPONSE_CODE,
56};
57use crate::net::api::announcement::get_api_urls;
58use crate::net::api::{ApiSecrets, HasApiContext};
59use crate::net::p2p::P2PStatusReceivers;
60use crate::{DashboardUiRouter, net, update_server_info_version_dbtx};
61
62const TRANSACTION_BUFFER: usize = 1000;
64
65const IROH_API_CONNECTION_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
67
68const IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE: u32 = 0;
71
72const IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON: &[u8] = b"idle timeout";
74
75#[allow(clippy::too_many_arguments)]
76pub async fn run(
77 connectors: ConnectorRegistry,
78 auth_ui: Option<ApiAuth>,
79 auth_api: Option<ApiAuth>,
80 connections: DynP2PConnections<P2PMessage>,
81 p2p_status_receivers: P2PStatusReceivers,
82 api_bind: SocketAddr,
83 iroh_dns: Option<SafeUrl>,
84 iroh_relays: Vec<SafeUrl>,
85 cfg: ServerConfig,
86 db: Database,
87 module_init_registry: ServerModuleInitRegistry,
88 task_group: &TaskGroup,
89 force_api_secrets: ApiSecrets,
90 data_dir: PathBuf,
91 code_version_str: String,
92 code_version_hash: String,
93 dyn_server_bitcoin_rpc: DynServerBitcoinRpc,
94 ui_bind: SocketAddr,
95 dashboard_ui_router: DashboardUiRouter,
96 db_checkpoint_retention: u64,
97 session_timeout: Duration,
98 iroh_api_limits: ConnectionLimits,
99) -> anyhow::Result<()> {
100 cfg.validate_config(&cfg.local.identity, &module_init_registry)?;
101
102 let mut global_dbtx = db.begin_transaction().await;
103 apply_migrations_server_dbtx(
104 &mut global_dbtx.to_ref_nc(),
105 Arc::new(ServerDbMigrationContext),
106 "fedimint-server".to_string(),
107 get_global_database_migrations(),
108 )
109 .await?;
110
111 update_server_info_version_dbtx(&mut global_dbtx.to_ref_nc(), &code_version_str).await;
112
113 if is_running_in_test_env() {
114 verify_server_db_integrity_dbtx(&mut global_dbtx.to_ref_nc()).await;
115 }
116 global_dbtx.commit_tx_result().await?;
117
118 let mut modules = BTreeMap::new();
119
120 let global_api = DynGlobalApi::new(
122 connectors.clone(),
123 cfg.consensus
124 .api_endpoints()
125 .iter()
126 .map(|(&peer_id, url)| (peer_id, url.url.clone()))
127 .collect(),
128 None,
129 )?;
130
131 let bitcoin_rpc_connection = ServerBitcoinRpcMonitor::new(
132 dyn_server_bitcoin_rpc,
133 if is_running_in_test_env() {
134 Duration::from_millis(100)
135 } else {
136 Duration::from_mins(1)
137 },
138 task_group,
139 );
140
141 for (module_id, module_cfg) in &cfg.consensus.modules {
142 match module_init_registry.get(&module_cfg.kind) {
143 Some(module_init) => {
144 info!(target: LOG_CORE, "Initialise module {module_id}...");
145
146 let mut dbtx = db.begin_transaction().await;
147 apply_migrations_dbtx(
148 &mut dbtx.to_ref_nc(),
149 Arc::new(ServerDbMigrationContext) as Arc<_>,
150 module_init.module_kind().to_string(),
151 module_init.get_database_migrations(),
152 Some(*module_id),
153 None,
154 )
155 .await?;
156
157 if let Some(used_db_prefixes) = module_init.used_db_prefixes()
158 && is_running_in_test_env()
159 {
160 verify_module_db_integrity_dbtx(
161 &mut dbtx.to_ref_nc(),
162 *module_id,
163 module_init.module_kind(),
164 &used_db_prefixes,
165 )
166 .await;
167 }
168 dbtx.commit_tx_result().await?;
169
170 let module = module_init
171 .init(
172 NumPeers::from(cfg.consensus.api_endpoints().len()),
173 cfg.get_module_config(*module_id)?,
174 db.with_prefix_module_id(*module_id).0,
175 task_group,
176 cfg.local.identity,
177 global_api.with_module(*module_id),
178 bitcoin_rpc_connection.clone(),
179 )
180 .await?;
181
182 modules.insert(*module_id, (module_cfg.kind.clone(), module));
183 }
184 None => bail!("Detected configuration for unsupported module id: {module_id}"),
185 }
186 }
187
188 let module_registry = ModuleRegistry::from(modules);
189
190 let client_cfg = cfg.consensus.to_client_config(&module_init_registry)?;
191
192 let (submission_sender, submission_receiver) = async_channel::bounded(TRANSACTION_BUFFER);
193 let (shutdown_sender, shutdown_receiver) = watch::channel(None);
194 let (ord_latency_sender, ord_latency_receiver) = watch::channel(None);
195
196 let mut ci_status_senders = BTreeMap::new();
197 let mut ci_status_receivers = BTreeMap::new();
198
199 for peer in cfg.consensus.broadcast_public_keys.keys().copied() {
200 let (ci_sender, ci_receiver) = watch::channel(None);
201
202 ci_status_senders.insert(peer, ci_sender);
203 ci_status_receivers.insert(peer, ci_receiver);
204 }
205
206 let consensus_api = ConsensusApi {
207 cfg: cfg.clone(),
208 db: db.clone(),
209 modules: module_registry.clone(),
210 client_cfg: client_cfg.clone(),
211 submission_sender: submission_sender.clone(),
212 shutdown_sender,
213 shutdown_receiver: shutdown_receiver.clone(),
214 supported_api_versions: ServerConfig::supported_api_versions_summary(
215 &cfg.consensus.modules,
216 &module_init_registry,
217 ),
218 auth_ui,
219 auth_api,
220 p2p_status_receivers,
221 ci_status_receivers,
222 ord_latency_receiver,
223 bitcoin_rpc_connection: bitcoin_rpc_connection.clone(),
224 force_api_secret: force_api_secrets.get_active(),
225 code_version_str,
226 code_version_hash,
227 task_group: task_group.clone(),
228 };
229
230 info!(target: LOG_CONSENSUS, "Starting Consensus Api...");
231
232 let api_handler = start_consensus_api(
233 &cfg.local,
234 consensus_api.clone(),
235 force_api_secrets.clone(),
236 api_bind,
237 )
238 .await;
239
240 if let Some(iroh_api_sk) = cfg.private.iroh_api_sk.clone()
241 && let Err(e) = Box::pin(start_iroh_api(
242 iroh_api_sk,
243 api_bind,
244 iroh_dns,
245 iroh_relays,
246 consensus_api.clone(),
247 task_group,
248 iroh_api_limits,
249 ))
250 .await
251 {
252 api_handler.stop().expect("Just started");
254 api_handler.stopped().await;
255 return Err(e);
256 }
257
258 info!(target: LOG_CONSENSUS, "Starting Submission of Module CI proposals...");
259
260 for (module_id, kind, module) in module_registry.iter_modules() {
261 submit_module_ci_proposals(
262 task_group,
263 db.clone(),
264 module_id,
265 kind.clone(),
266 module.clone(),
267 submission_sender.clone(),
268 );
269 }
270
271 let ui_service = dashboard_ui_router(consensus_api.clone().into_dyn()).into_make_service();
272
273 let ui_listener = TcpListener::bind(ui_bind)
274 .await
275 .expect("Failed to bind dashboard UI");
276
277 task_group.spawn("dashboard-ui", move |handle| async move {
278 axum::serve(ui_listener, ui_service)
279 .with_graceful_shutdown(handle.make_shutdown_rx())
280 .await
281 .expect("Failed to serve dashboard UI");
282 });
283
284 info!(target: LOG_CONSENSUS, "Dashboard UI running at http://{ui_bind} 🚀");
285
286 loop {
287 match bitcoin_rpc_connection.status() {
288 Some(status) => {
289 if let Some(progress) = status.sync_progress {
290 if progress >= 0.999 {
291 break;
292 }
293
294 info!(target: LOG_CONSENSUS, "Waiting for bitcoin backend to sync... {progress:.1}%");
295 } else {
296 break;
297 }
298 }
299 None => {
300 info!(target: LOG_CONSENSUS, "Waiting to connect to bitcoin backend...");
301 }
302 }
303
304 sleep(Duration::from_secs(1)).await;
305 }
306
307 info!(target: LOG_CONSENSUS, "Starting Consensus Engine...");
308
309 let api_urls = get_api_urls(&db, &cfg.consensus).await;
310
311 ConsensusEngine {
314 db,
315 federation_api: DynGlobalApi::new(
316 connectors,
317 api_urls,
318 force_api_secrets.get_active().as_deref(),
319 )?,
320 cfg: cfg.clone(),
321 connections,
322 ord_latency_sender,
323 ci_status_senders,
324 submission_receiver,
325 shutdown_receiver,
326 modules: module_registry,
327 task_group: task_group.clone(),
328 data_dir,
329 db_checkpoint_retention,
330 session_timeout,
331 }
332 .run()
333 .await?;
334
335 api_handler
336 .stop()
337 .expect("Consensus api should still be running");
338
339 api_handler.stopped().await;
340
341 Ok(())
342}
343
344async fn start_consensus_api(
345 cfg: &ServerConfigLocal,
346 api: ConsensusApi,
347 force_api_secrets: ApiSecrets,
348 api_bind: SocketAddr,
349) -> ServerHandle {
350 let mut rpc_module = RpcModule::new(api.clone());
351
352 net::api::attach_endpoints(&mut rpc_module, api::server_endpoints(), None);
353
354 for (id, _, module) in api.modules.iter_modules() {
355 net::api::attach_endpoints(&mut rpc_module, module.api_endpoints(), Some(id));
356 }
357
358 net::api::spawn(
359 "consensus",
360 api_bind,
361 rpc_module,
362 cfg.max_connections,
363 force_api_secrets,
364 )
365 .await
366}
367
368const CONSENSUS_PROPOSAL_TIMEOUT: Duration = Duration::from_secs(30);
369
370fn submit_module_ci_proposals(
371 task_group: &TaskGroup,
372 db: Database,
373 module_id: ModuleInstanceId,
374 kind: ModuleKind,
375 module: DynServerModule,
376 submission_sender: Sender<ConsensusItem>,
377) {
378 let mut interval = tokio::time::interval(if is_running_in_test_env() {
379 Duration::from_millis(100)
380 } else {
381 Duration::from_secs(1)
382 });
383
384 task_group.spawn(
385 format!("citem_proposals_{module_id}"),
386 move |task_handle| async move {
387 while !task_handle.is_shutting_down() {
388 let module_consensus_items = tokio::time::timeout(
389 CONSENSUS_PROPOSAL_TIMEOUT,
390 module.consensus_proposal(
391 &mut db
392 .begin_transaction_nc()
393 .await
394 .to_ref_with_prefix_module_id(module_id)
395 .0
396 .into_nc(),
397 module_id,
398 ),
399 )
400 .await;
401
402 match module_consensus_items {
403 Ok(items) => {
404 for item in items {
405 if submission_sender
406 .send(ConsensusItem::Module(item))
407 .await
408 .is_err()
409 {
410 warn!(
411 target: LOG_CONSENSUS,
412 module_id,
413 "Unable to submit module consensus item proposal via channel"
414 );
415 }
416 }
417 }
418 Err(..) => {
419 warn!(
420 target: LOG_CONSENSUS,
421 module_id,
422 %kind,
423 "Module failed to propose consensus items on time"
424 );
425 }
426 }
427
428 interval.tick().await;
429 }
430 },
431 );
432}
433
434async fn start_iroh_api(
435 secret_key: iroh::SecretKey,
436 api_bind: SocketAddr,
437 iroh_dns: Option<SafeUrl>,
438 iroh_relays: Vec<SafeUrl>,
439 consensus_api: ConsensusApi,
440 task_group: &TaskGroup,
441 iroh_api_limits: ConnectionLimits,
442) -> anyhow::Result<()> {
443 let endpoint = build_iroh_endpoint(
444 secret_key,
445 api_bind,
446 iroh_dns,
447 iroh_relays,
448 FEDIMINT_API_ALPN,
449 )
450 .await?;
451 task_group.spawn_cancellable(
452 "iroh-api",
453 run_iroh_api(consensus_api, endpoint, task_group.clone(), iroh_api_limits),
454 );
455
456 Ok(())
457}
458
459async fn run_iroh_api(
460 consensus_api: ConsensusApi,
461 endpoint: Endpoint,
462 task_group: TaskGroup,
463 iroh_api_limits: ConnectionLimits,
464) {
465 let core_api = server_endpoints()
466 .into_iter()
467 .map(|endpoint| (endpoint.path.to_string(), endpoint))
468 .collect::<BTreeMap<String, ApiEndpoint<ConsensusApi>>>();
469
470 let module_api = consensus_api
471 .modules
472 .iter_modules()
473 .map(|(id, _, module)| {
474 let api_endpoints = module
475 .api_endpoints()
476 .into_iter()
477 .map(|endpoint| (endpoint.path.to_string(), endpoint))
478 .collect::<BTreeMap<String, ApiEndpoint<DynServerModule>>>();
479
480 (id, api_endpoints)
481 })
482 .collect::<BTreeMap<ModuleInstanceId, BTreeMap<String, ApiEndpoint<DynServerModule>>>>();
483
484 let consensus_api = Arc::new(consensus_api);
485 let core_api = Arc::new(core_api);
486 let module_api = Arc::new(module_api);
487 let parallel_connections_limit = Arc::new(Semaphore::new(iroh_api_limits.max_connections));
488
489 loop {
490 match endpoint.accept().await {
491 Some(incoming) => {
492 if parallel_connections_limit.available_permits() == 0 {
493 warn!(
494 target: LOG_NET_API,
495 limit = iroh_api_limits.max_connections,
496 "Iroh API connection limit reached, blocking new connections"
497 );
498 }
499 let permit = parallel_connections_limit
500 .clone()
501 .acquire_owned()
502 .await
503 .expect("semaphore should not be closed");
504 task_group.spawn_cancellable_silent(
505 "handle-iroh-connection",
506 handle_incoming(
507 consensus_api.clone(),
508 core_api.clone(),
509 module_api.clone(),
510 task_group.clone(),
511 incoming,
512 permit,
513 iroh_api_limits.max_requests_per_connection,
514 )
515 .then(|result| async {
516 if let Err(err) = result {
517 warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle iroh connection");
518 }
519 }),
520 );
521 }
522 None => return,
523 }
524 }
525}
526
527async fn handle_incoming(
528 consensus_api: Arc<ConsensusApi>,
529 core_api: Arc<BTreeMap<String, ApiEndpoint<ConsensusApi>>>,
530 module_api: Arc<BTreeMap<ModuleInstanceId, BTreeMap<String, ApiEndpoint<DynServerModule>>>>,
531 task_group: TaskGroup,
532 incoming: Incoming,
533 _connection_permit: tokio::sync::OwnedSemaphorePermit,
534 iroh_api_max_requests_per_connection: usize,
535) -> anyhow::Result<()> {
536 let connection = incoming.accept()?.await?;
537 let parallel_requests_limit = Arc::new(Semaphore::new(iroh_api_max_requests_per_connection));
538
539 IROH_API_CONNECTIONS_ACTIVE.inc();
540 let connection_timer = IROH_API_CONNECTION_DURATION_SECONDS.start_timer();
541 scopeguard::defer! {
542 IROH_API_CONNECTIONS_ACTIVE.dec();
543 connection_timer.observe_duration();
544 }
545
546 loop {
547 let accept_result = fedimint_core::runtime::timeout(
548 IROH_API_CONNECTION_IDLE_TIMEOUT,
549 connection.accept_bi(),
550 )
551 .await;
552
553 let (send_stream, recv_stream) = match accept_result {
554 Ok(streams) => streams?,
555 Err(_)
556 if parallel_requests_limit.available_permits()
557 < iroh_api_max_requests_per_connection =>
558 {
559 continue;
560 }
561 Err(_) => {
562 IROH_API_CONNECTION_IDLE_TIMEOUT_TOTAL.inc();
563 tracing::debug!(
564 target: LOG_NET_API,
565 idle_timeout_secs = IROH_API_CONNECTION_IDLE_TIMEOUT.as_secs(),
566 "Closing idle iroh API connection"
567 );
568 connection.close(
569 VarInt::from_u32(IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE),
570 IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON,
571 );
572 return Ok(());
573 }
574 };
575
576 if parallel_requests_limit.available_permits() == 0 {
577 warn!(
578 target: LOG_NET_API,
579 limit = iroh_api_max_requests_per_connection,
580 "Iroh API request limit reached for connection, blocking new requests"
581 );
582 }
583 let permit = parallel_requests_limit
584 .clone()
585 .acquire_owned()
586 .await
587 .expect("semaphore should not be closed");
588 task_group.spawn_cancellable_silent(
589 "handle-iroh-request",
590 handle_request(
591 consensus_api.clone(),
592 core_api.clone(),
593 module_api.clone(),
594 send_stream,
595 recv_stream,
596 permit,
597 )
598 .then(|result| async {
599 if let Err(err) = result {
600 warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle iroh request");
601 }
602 }),
603 );
604 }
605}
606
607async fn handle_request(
608 consensus_api: Arc<ConsensusApi>,
609 core_api: Arc<BTreeMap<String, ApiEndpoint<ConsensusApi>>>,
610 module_api: Arc<BTreeMap<ModuleInstanceId, BTreeMap<String, ApiEndpoint<DynServerModule>>>>,
611 mut send_stream: SendStream,
612 mut recv_stream: RecvStream,
613 _request_permit: tokio::sync::OwnedSemaphorePermit,
614) -> anyhow::Result<()> {
615 let request = recv_stream.read_to_end(100_000).await?;
616
617 let request = serde_json::from_slice::<IrohApiRequest>(&request)?;
618
619 let method = request.method.to_string();
620 let timer = IROH_API_REQUEST_DURATION_SECONDS
621 .with_label_values(&[&method])
622 .start_timer();
623
624 let response = await_response(consensus_api, core_api, module_api, request).await;
625
626 timer.observe_duration();
627
628 let response_code = response
629 .as_ref()
630 .map_or_else(|err| err.code.to_string(), |_| "0".to_string());
631 IROH_API_REQUEST_RESPONSE_CODE
632 .with_label_values(&[method.as_str(), response_code.as_str(), "default"])
633 .inc();
634
635 let response = serde_json::to_vec(&response)?;
636
637 send_stream.write_all(&response).await?;
638
639 send_stream.finish()?;
640
641 Ok(())
642}
643
644async fn await_response(
645 consensus_api: Arc<ConsensusApi>,
646 core_api: Arc<BTreeMap<String, ApiEndpoint<ConsensusApi>>>,
647 module_api: Arc<BTreeMap<ModuleInstanceId, BTreeMap<String, ApiEndpoint<DynServerModule>>>>,
648 request: IrohApiRequest,
649) -> Result<Value, ApiError> {
650 match request.method {
651 ApiMethod::Core(method) => {
652 let endpoint = core_api.get(&method).ok_or(ApiError::not_found(method))?;
653
654 let (state, context) = consensus_api.context(&request.request, None).await;
655
656 (endpoint.handler)(state, context, request.request).await
657 }
658 ApiMethod::Module(module_id, method) => {
659 let endpoint = module_api
660 .get(&module_id)
661 .ok_or(ApiError::not_found(module_id.to_string()))?
662 .get(&method)
663 .ok_or(ApiError::not_found(method))?;
664
665 let (state, context) = consensus_api
666 .context(&request.request, Some(module_id))
667 .await;
668
669 (endpoint.handler)(state, context, request.request).await
670 }
671 }
672}