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