1use std::cmp::Ordering;
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9use bitcoin::hashes::sha256;
10use fedimint_api_client::api::{
11 LegacyFederationStatus, LegacyP2PConnectionStatus, LegacyPeerStatus, StatusResponse,
12};
13use fedimint_core::admin_client::{GuardianConfigBackup, ServerStatusLegacy, SetupStatus};
14use fedimint_core::backup::{
15 BackupStatistics, ClientBackupKey, ClientBackupKeyPrefix, ClientBackupSnapshot,
16};
17use fedimint_core::config::{ClientConfig, JsonClientConfig, META_FEDERATION_NAME_KEY};
18use fedimint_core::core::backup::{BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, SignedBackupRequest};
19use fedimint_core::core::{DynOutputOutcome, ModuleInstanceId, ModuleKind};
20use fedimint_core::db::{
21 Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
22};
23#[allow(deprecated)]
24use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
25use fedimint_core::endpoint_constants::{
26 API_ANNOUNCEMENTS_ENDPOINT, AUDIT_ENDPOINT, AUTH_ENDPOINT, AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
27 AWAIT_SESSION_OUTCOME_ENDPOINT, AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
28 AWAIT_TRANSACTION_ENDPOINT, BACKUP_ENDPOINT, BACKUP_STATISTICS_ENDPOINT, CHAIN_ID_ENDPOINT,
29 CLIENT_CONFIG_ENDPOINT, CLIENT_CONFIG_JSON_ENDPOINT, CONSENSUS_ORD_LATENCY_ENDPOINT,
30 FEDERATION_ID_ENDPOINT, FEDIMINTD_VERSION_ENDPOINT, GUARDIAN_CONFIG_BACKUP_ENDPOINT,
31 GUARDIAN_METADATA_ENDPOINT, INVITE_CODE_ENDPOINT, P2P_CONNECTION_STATUS_ENDPOINT,
32 RECOVER_ENDPOINT, SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT, SESSION_COUNT_ENDPOINT,
33 SESSION_STATUS_ENDPOINT, SESSION_STATUS_V2_ENDPOINT, SETUP_STATUS_ENDPOINT, SHUTDOWN_ENDPOINT,
34 SIGN_API_ANNOUNCEMENT_ENDPOINT, SIGN_GUARDIAN_METADATA_ENDPOINT, STATUS_ENDPOINT,
35 SUBMIT_API_ANNOUNCEMENT_ENDPOINT, SUBMIT_GUARDIAN_METADATA_ENDPOINT,
36 SUBMIT_TRANSACTION_ENDPOINT, VERSION_ENDPOINT,
37};
38use fedimint_core::epoch::ConsensusItem;
39use fedimint_core::invite_code::InviteCode;
40use fedimint_core::module::audit::{Audit, AuditSummary};
41use fedimint_core::module::{
42 ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiResult, ApiVersion,
43 SerdeModuleEncoding, SerdeModuleEncodingBase64, SupportedApiVersionsSummary, api_endpoint,
44};
45use fedimint_core::net::api_announcement::{
46 ApiAnnouncement, SignedApiAnnouncement, SignedApiAnnouncementSubmission,
47};
48use fedimint_core::net::auth::{GuardianAuthToken, check_auth};
49use fedimint_core::secp256k1::{PublicKey, SECP256K1};
50use fedimint_core::session_outcome::{
51 SessionOutcome, SessionStatus, SessionStatusV2, SignedSessionOutcome,
52};
53use fedimint_core::task::TaskGroup;
54use fedimint_core::transaction::{
55 SerdeTransaction, Transaction, TransactionError, TransactionSubmissionOutcome,
56};
57use fedimint_core::util::{FmtCompact, SafeUrl};
58use fedimint_core::version::non_zero_version_hash;
59use fedimint_core::{ChainId, OutPoint, OutPointRange, PeerId, TransactionId, secp256k1};
60use fedimint_logging::LOG_NET_API;
61use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
62use fedimint_server_core::dashboard_ui::{
63 IDashboardApi, P2PConnectionStatus, ServerBitcoinRpcStatus,
64};
65use fedimint_server_core::{DynServerModule, ServerModuleRegistry, ServerModuleRegistryExt};
66use futures::StreamExt;
67use tokio::sync::watch::{self, Receiver, Sender};
68use tracing::{debug, info, warn};
69
70use crate::config::io::{CONSENSUS_CONFIG, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG};
71use crate::config::{ServerConfig, legacy_consensus_config_hash};
72use crate::consensus::db::{AcceptedItemPrefix, AcceptedTransactionKey, SignedSessionOutcomeKey};
73use crate::consensus::engine::get_finished_session_count_static;
74use crate::consensus::transaction::{TxProcessingMode, process_transaction_with_dbtx};
75use crate::metrics::{BACKUP_WRITE_SIZE_BYTES, STORED_BACKUPS_COUNT};
76use crate::net::api::HasApiContext;
77use crate::net::api::announcement::{ApiAnnouncementKey, ApiAnnouncementPrefix, get_api_urls};
78use crate::net::p2p::P2PStatusReceivers;
79
80#[derive(Clone)]
81pub struct ConsensusApi {
82 pub cfg: ServerConfig,
84 pub db: Database,
86 pub modules: ServerModuleRegistry,
88 pub client_cfg: ClientConfig,
90 pub force_api_secret: Option<String>,
91 pub submission_sender: async_channel::Sender<ConsensusItem>,
93 pub shutdown_receiver: Receiver<Option<u64>>,
94 pub shutdown_sender: Sender<Option<u64>>,
95 pub ord_latency_receiver: watch::Receiver<Option<Duration>>,
96 pub p2p_status_receivers: P2PStatusReceivers,
97 pub ci_status_receivers: BTreeMap<PeerId, Receiver<Option<u64>>>,
98 pub bitcoin_rpc_connection: ServerBitcoinRpcMonitor,
99 pub supported_api_versions: SupportedApiVersionsSummary,
100 pub auth_ui: Option<ApiAuth>,
101 pub auth_api: Option<ApiAuth>,
102 pub code_version_str: String,
103 pub code_version_hash: String,
104 pub task_group: TaskGroup,
105}
106
107impl ConsensusApi {
108 pub fn api_versions_summary(&self) -> &SupportedApiVersionsSummary {
109 &self.supported_api_versions
110 }
111
112 pub fn get_active_api_secret(&self) -> Option<String> {
113 self.force_api_secret.clone()
116 }
117
118 pub async fn submit_transaction(
121 &self,
122 transaction: Transaction,
123 ) -> Result<TransactionId, TransactionError> {
124 let txid = transaction.tx_hash();
125
126 debug!(target: LOG_NET_API, %txid, "Received a submitted transaction");
127
128 let mut dbtx = self.db.begin_transaction_nc().await;
130 if dbtx
132 .get_value(&AcceptedTransactionKey(txid))
133 .await
134 .is_some()
135 {
136 debug!(target: LOG_NET_API, %txid, "Transaction already accepted");
137 return Ok(txid);
138 }
139
140 dbtx.ignore_uncommitted();
142
143 process_transaction_with_dbtx(
144 self.modules.clone(),
145 &mut dbtx,
146 &transaction,
147 self.cfg.consensus.version,
148 TxProcessingMode::Submission,
149 )
150 .await
151 .inspect_err(|err| {
152 debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
153 })?;
154
155 let _ = self
156 .submission_sender
157 .send(ConsensusItem::Transaction(transaction.clone()))
158 .await
159 .inspect_err(|err| {
160 warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
161 });
162
163 Ok(txid)
164 }
165
166 pub async fn await_transaction(
167 &self,
168 txid: TransactionId,
169 ) -> (Vec<ModuleInstanceId>, DatabaseTransaction<'_, Committable>) {
170 debug!(target: LOG_NET_API, %txid, "Awaiting transaction acceptance");
171 self.db
172 .wait_key_check(&AcceptedTransactionKey(txid), std::convert::identity)
173 .await
174 }
175
176 pub async fn await_output_outcome(
177 &self,
178 outpoint: OutPoint,
179 ) -> Result<SerdeModuleEncoding<DynOutputOutcome>> {
180 debug!(target: LOG_NET_API, %outpoint, "Awaiting output outcome");
181 let (module_ids, mut dbtx) = self.await_transaction(outpoint.txid).await;
182
183 let module_id = module_ids
184 .into_iter()
185 .nth(outpoint.out_idx as usize)
186 .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
187
188 #[allow(deprecated)]
189 let outcome = self
190 .modules
191 .get_expect(module_id)
192 .output_status(
193 &mut dbtx.to_ref_with_prefix_module_id(module_id).0.into_nc(),
194 outpoint,
195 module_id,
196 )
197 .await
198 .context("No output outcome for outpoint")?;
199
200 Ok((&outcome).into())
201 }
202
203 pub async fn await_outputs_outcomes(
204 &self,
205 outpoint_range: OutPointRange,
206 ) -> Result<Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>>> {
207 let (module_ids, mut dbtx) = self.await_transaction(outpoint_range.txid()).await;
209
210 let mut outcomes = Vec::with_capacity(outpoint_range.count());
211
212 for outpoint in outpoint_range {
213 let module_id = module_ids
214 .get(outpoint.out_idx as usize)
215 .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
216
217 #[allow(deprecated)]
218 let outcome = self
219 .modules
220 .get_expect(*module_id)
221 .output_status(
222 &mut dbtx.to_ref_with_prefix_module_id(*module_id).0.into_nc(),
223 outpoint,
224 *module_id,
225 )
226 .await
227 .map(|outcome| (&outcome).into());
228
229 outcomes.push(outcome);
230 }
231
232 Ok(outcomes)
233 }
234
235 pub async fn session_count(&self) -> u64 {
236 get_finished_session_count_static(&mut self.db.begin_transaction_nc().await).await
237 }
238
239 pub async fn await_signed_session_outcome(&self, index: u64) -> SignedSessionOutcome {
240 self.db
241 .wait_key_check(&SignedSessionOutcomeKey(index), std::convert::identity)
242 .await
243 .0
244 }
245
246 pub async fn session_status(&self, session_index: u64) -> SessionStatusV2 {
247 let mut dbtx = self.db.begin_transaction_nc().await;
248
249 match session_index.cmp(&get_finished_session_count_static(&mut dbtx).await) {
250 Ordering::Greater => SessionStatusV2::Initial,
251 Ordering::Equal => SessionStatusV2::Pending(
252 dbtx.find_by_prefix(&AcceptedItemPrefix)
253 .await
254 .map(|entry| entry.1)
255 .collect()
256 .await,
257 ),
258 Ordering::Less => SessionStatusV2::Complete(
259 dbtx.get_value(&SignedSessionOutcomeKey(session_index))
260 .await
261 .expect("There are no gaps in session outcomes"),
262 ),
263 }
264 }
265
266 pub async fn get_federation_status(&self) -> ApiResult<LegacyFederationStatus> {
267 let session_count = self.session_count().await;
268 let scheduled_shutdown = self.shutdown_receiver.borrow().to_owned();
269
270 let status_by_peer = self
271 .p2p_status_receivers
272 .iter()
273 .map(|(peer, p2p_receiver)| {
274 let ci_receiver = self.ci_status_receivers.get(peer).unwrap();
275
276 let consensus_status = LegacyPeerStatus {
277 connection_status: match p2p_receiver.borrow().connected {
278 Some(..) => LegacyP2PConnectionStatus::Connected,
279 None => LegacyP2PConnectionStatus::Disconnected,
280 },
281 last_contribution: *ci_receiver.borrow(),
282 flagged: ci_receiver.borrow().unwrap_or(0) + 1 < session_count,
283 };
284
285 (*peer, consensus_status)
286 })
287 .collect::<BTreeMap<_, _>>();
288
289 let peers_flagged = status_by_peer
290 .values()
291 .filter(|status| status.flagged)
292 .count() as u64;
293
294 let peers_online = status_by_peer
295 .values()
296 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Connected)
297 .count() as u64;
298
299 let peers_offline = status_by_peer
300 .values()
301 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Disconnected)
302 .count() as u64;
303
304 Ok(LegacyFederationStatus {
305 session_count,
306 status_by_peer,
307 peers_online,
308 peers_offline,
309 peers_flagged,
310 scheduled_shutdown,
311 })
312 }
313
314 fn shutdown(&self, index: Option<u64>) {
315 self.shutdown_sender.send_replace(index);
316 }
317
318 async fn get_federation_audit(&self) -> ApiResult<AuditSummary> {
319 let mut dbtx = self.db.begin_transaction_nc().await;
320 dbtx.ignore_uncommitted();
324
325 let mut audit = Audit::default();
326 let mut module_instance_id_to_kind = BTreeMap::new();
327 for (module_instance_id, kind, module) in self.modules.iter_modules() {
328 module_instance_id_to_kind.insert(module_instance_id, kind.as_str().to_string());
329 module
330 .audit(
331 &mut dbtx.to_ref_with_prefix_module_id(module_instance_id).0,
332 &mut audit,
333 module_instance_id,
334 )
335 .await;
336 }
337 Ok(AuditSummary::from_audit(
338 &audit,
339 &module_instance_id_to_kind,
340 ))
341 }
342
343 fn get_guardian_config_backup(&self, _auth: &GuardianAuthToken) -> GuardianConfigBackup {
347 let mut tar_archive_builder = tar::Builder::new(Vec::new());
348
349 let mut append = |name: &Path, data: &[u8]| {
350 let mut header = tar::Header::new_gnu();
351 header.set_path(name).expect("Error setting path");
352 header.set_size(data.len() as u64);
353 header.set_mode(0o644);
354 header.set_cksum();
355 tar_archive_builder
356 .append(&header, data)
357 .expect("Error adding data to tar archive");
358 };
359
360 append(
361 &PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
362 &serde_json::to_vec(&self.cfg.local).expect("Error encoding local config"),
363 );
364
365 append(
366 &PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
367 &serde_json::to_vec(&self.cfg.consensus).expect("Error encoding consensus config"),
368 );
369
370 append(
371 &PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
372 &serde_json::to_vec(&self.cfg.private).expect("Error encoding private config"),
373 );
374
375 let tar_archive_bytes = tar_archive_builder
376 .into_inner()
377 .expect("Error building tar archive");
378
379 GuardianConfigBackup { tar_archive_bytes }
380 }
381
382 async fn handle_backup_request(
383 &self,
384 dbtx: &mut DatabaseTransaction<'_>,
385 request: SignedBackupRequest,
386 ) -> Result<(), ApiError> {
387 let request = request
388 .verify_valid(SECP256K1)
389 .map_err(|_| ApiError::bad_request("invalid request".into()))?;
390
391 if request.payload.len() > BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES {
392 return Err(ApiError::bad_request("snapshot too large".into()));
393 }
394 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request");
395 if let Some(prev) = dbtx.get_value(&ClientBackupKey(request.id)).await
396 && request.timestamp <= prev.timestamp
397 {
398 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request with old timestamp - ignoring");
399 return Err(ApiError::bad_request("timestamp too small".into()));
400 }
401
402 info!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Storing new client backup");
403 let overwritten = dbtx
404 .insert_entry(
405 &ClientBackupKey(request.id),
406 &ClientBackupSnapshot {
407 timestamp: request.timestamp,
408 data: request.payload.clone(),
409 },
410 )
411 .await
412 .is_some();
413 BACKUP_WRITE_SIZE_BYTES.observe(request.payload.len() as f64);
414 if !overwritten {
415 dbtx.on_commit(|| STORED_BACKUPS_COUNT.inc());
416 }
417
418 Ok(())
419 }
420
421 async fn handle_recover_request(
422 &self,
423 dbtx: &mut DatabaseTransaction<'_>,
424 id: PublicKey,
425 ) -> Option<ClientBackupSnapshot> {
426 dbtx.get_value(&ClientBackupKey(id)).await
427 }
428
429 async fn api_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
432 self.db
433 .begin_transaction_nc()
434 .await
435 .find_by_prefix(&ApiAnnouncementPrefix)
436 .await
437 .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
438 .collect()
439 .await
440 }
441
442 fn fedimintd_version(&self) -> String {
444 self.code_version_str.clone()
445 }
446
447 async fn submit_api_announcement(
450 &self,
451 peer_id: PeerId,
452 announcement: SignedApiAnnouncement,
453 ) -> Result<(), ApiError> {
454 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
455 return Err(ApiError::bad_request("Peer not in federation".into()));
456 };
457
458 if !announcement.verify(SECP256K1, peer_key) {
459 return Err(ApiError::bad_request("Invalid signature".into()));
460 }
461
462 self.db
464 .autocommit(
465 |dbtx, _| {
466 let announcement = announcement.clone();
467 Box::pin(async move {
468 if let Some(existing_announcement) =
469 dbtx.get_value(&ApiAnnouncementKey(peer_id)).await
470 {
471 if existing_announcement.api_announcement
476 == announcement.api_announcement
477 {
478 return Ok(());
479 }
480
481 if existing_announcement.api_announcement.nonce
484 >= announcement.api_announcement.nonce
485 {
486 return Err(ApiError::bad_request(
487 "Outdated or redundant announcement".into(),
488 ));
489 }
490 }
491
492 dbtx.insert_entry(&ApiAnnouncementKey(peer_id), &announcement)
493 .await;
494 Ok(())
495 })
496 },
497 None,
498 )
499 .await
500 .map_err(|e| match e {
501 fedimint_core::db::AutocommitError::ClosureError { error, .. } => error,
502 fedimint_core::db::AutocommitError::CommitFailed { last_error, .. } => {
503 ApiError::server_error(format!("Database commit failed: {last_error}"))
504 }
505 })
506 }
507
508 async fn sign_api_announcement(&self, new_url: SafeUrl) -> SignedApiAnnouncement {
509 self.db
510 .autocommit(
511 |dbtx, _| {
512 let new_url_inner = new_url.clone();
513 Box::pin(async move {
514 let new_nonce = dbtx
515 .get_value(&ApiAnnouncementKey(self.cfg.local.identity))
516 .await
517 .map_or(0, |a| a.api_announcement.nonce + 1);
518 let announcement = ApiAnnouncement {
519 api_url: new_url_inner,
520 nonce: new_nonce,
521 };
522 let ctx = secp256k1::Secp256k1::new();
523 let signed_announcement = announcement
524 .sign(&ctx, &self.cfg.private.broadcast_secret_key.keypair(&ctx));
525
526 dbtx.insert_entry(
527 &ApiAnnouncementKey(self.cfg.local.identity),
528 &signed_announcement,
529 )
530 .await;
531
532 Result::<_, ()>::Ok(signed_announcement)
533 })
534 },
535 None,
536 )
537 .await
538 .expect("Will not terminate on error")
539 }
540
541 async fn guardian_metadata_list(
542 &self,
543 ) -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
544 use crate::net::api::guardian_metadata::{GuardianMetadataKey, GuardianMetadataPrefix};
545
546 self.db
547 .begin_transaction_nc()
548 .await
549 .find_by_prefix(&GuardianMetadataPrefix)
550 .await
551 .map(|(key, metadata): (GuardianMetadataKey, _)| (key.0, metadata))
552 .collect()
553 .await
554 }
555
556 async fn submit_guardian_metadata(
557 &self,
558 peer_id: PeerId,
559 metadata: fedimint_core::net::guardian_metadata::SignedGuardianMetadata,
560 ) -> Result<(), ApiError> {
561 use crate::net::api::guardian_metadata::GuardianMetadataKey;
562
563 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
564 return Err(ApiError::bad_request("Peer not in federation".into()));
565 };
566
567 let now = fedimint_core::time::duration_since_epoch();
568 if let Err(e) = metadata.verify(SECP256K1, peer_key, now) {
569 return Err(ApiError::bad_request(format!(
570 "Invalid signature or timestamp: {e}"
571 )));
572 }
573
574 let mut dbtx = self.db.begin_transaction().await;
575
576 if let Some(existing_metadata) = dbtx.get_value(&GuardianMetadataKey(peer_id)).await {
577 if existing_metadata.bytes == metadata.bytes {
581 return Ok(());
582 }
583
584 if metadata.guardian_metadata().timestamp_secs
586 <= existing_metadata.guardian_metadata().timestamp_secs
587 {
588 return Err(ApiError::bad_request(
589 "New metadata timestamp is not newer than existing".into(),
590 ));
591 }
592 }
593
594 dbtx.insert_entry(&GuardianMetadataKey(peer_id), &metadata)
595 .await;
596 dbtx.commit_tx().await;
597
598 Ok(())
599 }
600
601 async fn sign_guardian_metadata(
602 &self,
603 new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
604 ) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
605 sign_guardian_metadata_preserving_iroh_endpoint(
606 &self.db,
607 self.cfg.local.identity,
608 &self.cfg.private.broadcast_secret_key,
609 new_metadata,
610 )
611 .await
612 }
613
614 async fn get_invite_code(&self, api_secret: Option<String>) -> InviteCode {
615 let identity = self.cfg.local.identity;
616 let mut api_urls = get_api_urls(&self.db, &self.cfg.consensus).await;
617
618 InviteCode::new(
619 api_urls
620 .remove(&identity)
621 .expect("API URL for our identity must be present"),
622 identity,
623 self.cfg.calculate_federation_id(),
624 api_secret,
625 )
626 }
627}
628
629async fn sign_guardian_metadata_preserving_iroh_endpoint(
630 db: &Database,
631 identity: PeerId,
632 broadcast_secret_key: &secp256k1::SecretKey,
633 new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
634) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
635 use crate::net::api::guardian_metadata::GuardianMetadataKey;
636
637 db.autocommit(
638 |dbtx, _| {
639 let mut new_metadata = new_metadata.clone();
640 Box::pin(async move {
641 new_metadata.iroh_next_endpoint = dbtx
642 .get_value(&GuardianMetadataKey(identity))
643 .await
644 .and_then(|metadata| metadata.guardian_metadata().iroh_next_endpoint.clone());
645
646 let ctx = secp256k1::Secp256k1::new();
647 let signed_metadata = new_metadata.sign(&ctx, &broadcast_secret_key.keypair(&ctx));
648
649 dbtx.insert_entry(&GuardianMetadataKey(identity), &signed_metadata)
650 .await;
651
652 Result::<_, ()>::Ok(signed_metadata)
653 })
654 },
655 None,
656 )
657 .await
658 .expect("Will not terminate on error")
659}
660
661#[async_trait]
662impl HasApiContext<ConsensusApi> for ConsensusApi {
663 async fn context(
664 &self,
665 request: &ApiRequestErased,
666 id: Option<ModuleInstanceId>,
667 ) -> (&ConsensusApi, ApiEndpointContext) {
668 let mut db = self.db.clone();
669 if let Some(id) = id {
670 db = self.db.with_prefix_module_id(id).0;
671 }
672 let has_auth = match (&self.auth_api, &request.auth) {
673 (Some(server_auth), Some(req_auth)) => server_auth.verify(req_auth.as_str()),
674 _ => false,
675 };
676
677 (
678 self,
679 ApiEndpointContext::new(db, has_auth, request.auth.clone()),
680 )
681 }
682}
683
684#[async_trait]
685impl HasApiContext<DynServerModule> for ConsensusApi {
686 async fn context(
687 &self,
688 request: &ApiRequestErased,
689 id: Option<ModuleInstanceId>,
690 ) -> (&DynServerModule, ApiEndpointContext) {
691 let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
692 (
693 self.modules.get_expect(id.expect("required module id")),
694 context,
695 )
696 }
697}
698
699#[async_trait]
700impl IDashboardApi for ConsensusApi {
701 fn auth_ui(&self) -> Option<ApiAuth> {
702 self.auth_ui.clone()
703 }
704
705 async fn guardian_id(&self) -> PeerId {
706 self.cfg.local.identity
707 }
708
709 async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
710 self.cfg
711 .consensus
712 .api_endpoints()
713 .iter()
714 .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
715 .collect()
716 }
717
718 async fn federation_name(&self) -> String {
719 self.cfg
720 .consensus
721 .meta
722 .get(META_FEDERATION_NAME_KEY)
723 .cloned()
724 .expect("Federation name must be set")
725 }
726
727 async fn session_count(&self) -> u64 {
728 self.session_count().await
729 }
730
731 async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
732 self.session_status(session_idx).await
733 }
734
735 async fn consensus_ord_latency(&self) -> Option<Duration> {
736 *self.ord_latency_receiver.borrow()
737 }
738
739 async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
740 self.p2p_status_receivers
741 .iter()
742 .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
743 .collect()
744 }
745
746 async fn federation_invite_code(&self) -> String {
747 self.get_invite_code(self.get_active_api_secret())
748 .await
749 .to_string()
750 }
751
752 async fn federation_audit(&self) -> AuditSummary {
753 self.get_federation_audit()
754 .await
755 .expect("Failed to get federation audit")
756 }
757
758 async fn bitcoin_rpc_url(&self) -> SafeUrl {
759 self.bitcoin_rpc_connection.url()
760 }
761
762 async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
763 self.bitcoin_rpc_connection.status()
764 }
765
766 async fn download_guardian_config_backup(
767 &self,
768 guardian_auth: &GuardianAuthToken,
769 ) -> GuardianConfigBackup {
770 self.get_guardian_config_backup(guardian_auth)
771 }
772
773 fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
774 self.modules
775 .iter_modules()
776 .find_map(|(_, module_kind, module)| {
777 if *module_kind == kind {
778 Some(module)
779 } else {
780 None
781 }
782 })
783 }
784
785 async fn fedimintd_version(&self) -> String {
786 self.code_version_str.clone()
787 }
788
789 async fn fedimintd_version_hash(&self) -> Option<String> {
790 non_zero_version_hash(&self.code_version_hash).map(str::to_owned)
791 }
792}
793
794pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
795 vec![
796 api_endpoint! {
797 VERSION_ENDPOINT,
798 ApiVersion::new(0, 0),
799 async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
800 Ok(fedimint.api_versions_summary().to_owned())
801 }
802 },
803 api_endpoint! {
804 SUBMIT_TRANSACTION_ENDPOINT,
805 ApiVersion::new(0, 0),
806 async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
807 let transaction = transaction
808 .try_into_inner(&fedimint.modules.decoder_registry())
809 .map_err(|e| ApiError::bad_request(e.to_string()))?;
810
811 Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
814 }
815 },
816 api_endpoint! {
817 AWAIT_TRANSACTION_ENDPOINT,
818 ApiVersion::new(0, 0),
819 async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
820 fedimint.await_transaction(tx_hash).await;
821
822 Ok(tx_hash)
823 }
824 },
825 api_endpoint! {
826 AWAIT_OUTPUT_OUTCOME_ENDPOINT,
827 ApiVersion::new(0, 0),
828 async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
829 let outcome = fedimint
830 .await_output_outcome(outpoint)
831 .await
832 .map_err(|e| ApiError::bad_request(e.to_string()))?;
833
834 Ok(outcome)
835 }
836 },
837 api_endpoint! {
838 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
839 ApiVersion::new(0, 8),
840 async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
841 let outcomes = fedimint
842 .await_outputs_outcomes(outpoint_range)
843 .await
844 .map_err(|e| ApiError::bad_request(e.to_string()))?;
845
846 Ok(outcomes)
847 }
848 },
849 api_endpoint! {
850 INVITE_CODE_ENDPOINT,
851 ApiVersion::new(0, 0),
852 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
853 Ok(fedimint.get_invite_code(fedimint.get_active_api_secret()).await.to_string())
854 }
855 },
856 api_endpoint! {
857 FEDERATION_ID_ENDPOINT,
858 ApiVersion::new(0, 2),
859 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
860 Ok(fedimint.cfg.calculate_federation_id().to_string())
861 }
862 },
863 api_endpoint! {
864 CLIENT_CONFIG_ENDPOINT,
865 ApiVersion::new(0, 0),
866 async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
867 Ok(fedimint.client_cfg.clone())
868 }
869 },
870 api_endpoint! {
872 CLIENT_CONFIG_JSON_ENDPOINT,
873 ApiVersion::new(0, 0),
874 async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
875 Ok(fedimint.client_cfg.to_json())
876 }
877 },
878 api_endpoint! {
879 SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
880 ApiVersion::new(0, 0),
881 async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
882 Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
883 }
884 },
885 api_endpoint! {
886 STATUS_ENDPOINT,
887 ApiVersion::new(0, 0),
888 async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
889 Ok(StatusResponse {
890 server: ServerStatusLegacy::ConsensusRunning,
891 federation: Some(fedimint.get_federation_status().await?)
892 })}
893 },
894 api_endpoint! {
895 SETUP_STATUS_ENDPOINT,
896 ApiVersion::new(0, 0),
897 async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
898 Ok(SetupStatus::ConsensusIsRunning)
899 }
900 },
901 api_endpoint! {
902 CONSENSUS_ORD_LATENCY_ENDPOINT,
903 ApiVersion::new(0, 0),
904 async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
905 Ok(*fedimint.ord_latency_receiver.borrow())
906 }
907 },
908 api_endpoint! {
909 P2P_CONNECTION_STATUS_ENDPOINT,
910 ApiVersion::new(0, 0),
911 async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
912 Ok(fedimint.p2p_status_receivers
913 .iter()
914 .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
915 .collect())
916 }
917 },
918 api_endpoint! {
919 SESSION_COUNT_ENDPOINT,
920 ApiVersion::new(0, 0),
921 async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
922 Ok(fedimint.session_count().await)
923 }
924 },
925 api_endpoint! {
926 AWAIT_SESSION_OUTCOME_ENDPOINT,
927 ApiVersion::new(0, 0),
928 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
929 Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
930 }
931 },
932 api_endpoint! {
933 AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
934 ApiVersion::new(0, 0),
935 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
936 Ok((&fedimint.await_signed_session_outcome(index).await).into())
937 }
938 },
939 api_endpoint! {
940 SESSION_STATUS_ENDPOINT,
941 ApiVersion::new(0, 1),
942 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
943 Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
944 }
945 },
946 api_endpoint! {
947 SESSION_STATUS_V2_ENDPOINT,
948 ApiVersion::new(0, 5),
949 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
950 Ok((&fedimint.session_status(index).await).into())
951 }
952 },
953 api_endpoint! {
954 SHUTDOWN_ENDPOINT,
955 ApiVersion::new(0, 3),
956 async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
957 check_auth(context)?;
958 fedimint.shutdown(index);
959 Ok(())
960 }
961 },
962 api_endpoint! {
963 AUDIT_ENDPOINT,
964 ApiVersion::new(0, 0),
965 async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
966 check_auth(context)?;
967 Ok(fedimint.get_federation_audit().await?)
968 }
969 },
970 api_endpoint! {
971 GUARDIAN_CONFIG_BACKUP_ENDPOINT,
972 ApiVersion::new(0, 2),
973 async |fedimint: &ConsensusApi, context, _v: ()| -> GuardianConfigBackup {
974 let auth = check_auth(context)?;
975 Ok(fedimint.get_guardian_config_backup(&auth))
976 }
977 },
978 api_endpoint! {
979 BACKUP_ENDPOINT,
980 ApiVersion::new(0, 0),
981 async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
982 let db = context.db();
983 let mut dbtx = db.begin_transaction().await;
984 fedimint
985 .handle_backup_request(&mut dbtx.to_ref_nc(), request).await?;
986 dbtx.commit_tx_result().await?;
987 Ok(())
988
989 }
990 },
991 api_endpoint! {
992 RECOVER_ENDPOINT,
993 ApiVersion::new(0, 0),
994 async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
995 let db = context.db();
996 let mut dbtx = db.begin_transaction_nc().await;
997 Ok(fedimint
998 .handle_recover_request(&mut dbtx, id).await)
999 }
1000 },
1001 api_endpoint! {
1002 AUTH_ENDPOINT,
1003 ApiVersion::new(0, 0),
1004 async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
1005 check_auth(context)?;
1006 Ok(())
1007 }
1008 },
1009 api_endpoint! {
1010 API_ANNOUNCEMENTS_ENDPOINT,
1011 ApiVersion::new(0, 3),
1012 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
1013 Ok(fedimint.api_announcements().await)
1014 }
1015 },
1016 api_endpoint! {
1017 SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
1018 ApiVersion::new(0, 3),
1019 async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
1020 fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
1021 }
1022 },
1023 api_endpoint! {
1024 SIGN_API_ANNOUNCEMENT_ENDPOINT,
1025 ApiVersion::new(0, 3),
1026 async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
1027 check_auth(context)?;
1028 Ok(fedimint.sign_api_announcement(new_url).await)
1029 }
1030 },
1031 api_endpoint! {
1032 GUARDIAN_METADATA_ENDPOINT,
1033 ApiVersion::new(0, 9),
1034 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
1035 Ok(fedimint.guardian_metadata_list().await)
1036 }
1037 },
1038 api_endpoint! {
1039 SUBMIT_GUARDIAN_METADATA_ENDPOINT,
1040 ApiVersion::new(0, 9),
1041 async |fedimint: &ConsensusApi, _context, submission: fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission| -> () {
1042 fedimint.submit_guardian_metadata(submission.peer_id, submission.signed_guardian_metadata).await
1043 }
1044 },
1045 api_endpoint! {
1046 SIGN_GUARDIAN_METADATA_ENDPOINT,
1047 ApiVersion::new(0, 9),
1048 async |fedimint: &ConsensusApi, context, metadata: fedimint_core::net::guardian_metadata::GuardianMetadata| -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
1049 check_auth(context)?;
1050 Ok(fedimint.sign_guardian_metadata(metadata).await)
1051 }
1052 },
1053 api_endpoint! {
1054 FEDIMINTD_VERSION_ENDPOINT,
1055 ApiVersion::new(0, 4),
1056 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
1057 Ok(fedimint.fedimintd_version())
1058 }
1059 },
1060 api_endpoint! {
1061 BACKUP_STATISTICS_ENDPOINT,
1062 ApiVersion::new(0, 5),
1063 async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
1064 check_auth(context)?;
1065 let db = context.db();
1066 let mut dbtx = db.begin_transaction_nc().await;
1067 Ok(backup_statistics_static(&mut dbtx).await)
1068 }
1069 },
1070 api_endpoint! {
1071 CHAIN_ID_ENDPOINT,
1072 ApiVersion::new(0, 9),
1073 async |fedimint: &ConsensusApi, _context, _v: ()| -> ChainId {
1074 fedimint
1075 .bitcoin_rpc_connection
1076 .get_chain_id()
1077 .await
1078 .map_err(|e| ApiError::server_error(e.to_string()))
1079 }
1080 },
1081 ]
1082}
1083
1084pub(crate) async fn backup_statistics_static(
1085 dbtx: &mut DatabaseTransaction<'_>,
1086) -> BackupStatistics {
1087 const DAY_SECS: u64 = 24 * 60 * 60;
1088 const WEEK_SECS: u64 = 7 * DAY_SECS;
1089 const MONTH_SECS: u64 = 30 * DAY_SECS;
1090 const QUARTER_SECS: u64 = 3 * MONTH_SECS;
1091
1092 let mut backup_stats = BackupStatistics::default();
1093
1094 let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
1095 while let Some((_, backup)) = all_backups_stream.next().await {
1096 backup_stats.num_backups += 1;
1097 backup_stats.total_size += backup.data.len();
1098
1099 let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
1100 if age_secs < DAY_SECS {
1101 backup_stats.refreshed_1d += 1;
1102 }
1103 if age_secs < WEEK_SECS {
1104 backup_stats.refreshed_1w += 1;
1105 }
1106 if age_secs < MONTH_SECS {
1107 backup_stats.refreshed_1m += 1;
1108 }
1109 if age_secs < QUARTER_SECS {
1110 backup_stats.refreshed_3m += 1;
1111 }
1112 }
1113
1114 backup_stats
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119 use fedimint_core::db::IRawDatabaseExt as _;
1120 use fedimint_core::db::mem_impl::MemDatabase;
1121 use fedimint_core::net::guardian_metadata::GuardianMetadata;
1122
1123 use super::*;
1124 use crate::net::api::guardian_metadata::GuardianMetadataKey;
1125
1126 #[tokio::test]
1127 async fn admin_metadata_update_preserves_persisted_iroh_endpoint() {
1128 let db: Database = MemDatabase::new().into_database();
1129 let identity = PeerId::from(0);
1130 let broadcast_secret_key =
1131 secp256k1::SecretKey::from_slice(&[42; 32]).expect("valid test key");
1132 let ctx = secp256k1::Secp256k1::new();
1133
1134 let existing = GuardianMetadata::new(
1135 vec!["wss://old.example".parse().expect("valid URL")],
1136 "old-pkarr".to_owned(),
1137 1,
1138 )
1139 .with_iroh_next_endpoint("persisted-iroh-id".to_owned())
1140 .sign(&ctx, &broadcast_secret_key.keypair(&ctx));
1141 let mut dbtx = db.begin_transaction().await;
1142 dbtx.insert_entry(&GuardianMetadataKey(identity), &existing)
1143 .await;
1144 dbtx.commit_tx().await;
1145
1146 let updated = GuardianMetadata::new(
1147 vec!["wss://new.example".parse().expect("valid URL")],
1148 "new-pkarr".to_owned(),
1149 2,
1150 );
1151 let signed = sign_guardian_metadata_preserving_iroh_endpoint(
1152 &db,
1153 identity,
1154 &broadcast_secret_key,
1155 updated,
1156 )
1157 .await;
1158
1159 assert_eq!(
1160 signed.guardian_metadata().iroh_next_endpoint.as_deref(),
1161 Some("persisted-iroh-id")
1162 );
1163 assert_eq!(
1164 signed.guardian_metadata().api_urls,
1165 vec!["wss://new.example".parse().expect("valid URL")]
1166 );
1167 assert_eq!(signed.guardian_metadata().pkarr_id_z32, "new-pkarr");
1168 assert_eq!(
1169 db.begin_transaction_nc()
1170 .await
1171 .get_value(&GuardianMetadataKey(identity))
1172 .await
1173 .expect("metadata was persisted")
1174 .tagged_hash(),
1175 signed.tagged_hash()
1176 );
1177 }
1178}