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