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