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