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.auth == Some(self.cfg.private.api_auth.clone()),
685 request.auth.clone(),
686 ),
687 )
688 }
689}
690
691#[async_trait]
692impl HasApiContext<DynServerModule> for ConsensusApi {
693 async fn context(
694 &self,
695 request: &ApiRequestErased,
696 id: Option<ModuleInstanceId>,
697 ) -> (&DynServerModule, ApiEndpointContext) {
698 let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
699 (
700 self.modules.get_expect(id.expect("required module id")),
701 context,
702 )
703 }
704}
705
706#[async_trait]
707impl IDashboardApi for ConsensusApi {
708 async fn auth(&self) -> ApiAuth {
709 self.cfg.private.api_auth.clone()
710 }
711
712 async fn guardian_id(&self) -> PeerId {
713 self.cfg.local.identity
714 }
715
716 async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
717 self.cfg
718 .consensus
719 .api_endpoints()
720 .iter()
721 .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
722 .collect()
723 }
724
725 async fn federation_name(&self) -> String {
726 self.cfg
727 .consensus
728 .meta
729 .get(META_FEDERATION_NAME_KEY)
730 .cloned()
731 .expect("Federation name must be set")
732 }
733
734 async fn session_count(&self) -> u64 {
735 self.session_count().await
736 }
737
738 async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
739 self.session_status(session_idx).await
740 }
741
742 async fn consensus_ord_latency(&self) -> Option<Duration> {
743 *self.ord_latency_receiver.borrow()
744 }
745
746 async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
747 self.p2p_status_receivers
748 .iter()
749 .map(|(peer, receiver)| (*peer, receiver.borrow().clone()))
750 .collect()
751 }
752
753 async fn federation_invite_code(&self) -> String {
754 self.cfg
755 .get_invite_code(self.get_active_api_secret())
756 .to_string()
757 }
758
759 async fn federation_audit(&self) -> AuditSummary {
760 self.get_federation_audit()
761 .await
762 .expect("Failed to get federation audit")
763 }
764
765 async fn bitcoin_rpc_url(&self) -> SafeUrl {
766 self.bitcoin_rpc_connection.url()
767 }
768
769 async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
770 self.bitcoin_rpc_connection.status()
771 }
772
773 async fn download_guardian_config_backup(
774 &self,
775 password: &str,
776 guardian_auth: &GuardianAuthToken,
777 ) -> GuardianConfigBackup {
778 self.get_guardian_config_backup(password, guardian_auth)
779 }
780
781 fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
782 self.modules
783 .iter_modules()
784 .find_map(|(_, module_kind, module)| {
785 if *module_kind == kind {
786 Some(module)
787 } else {
788 None
789 }
790 })
791 }
792
793 async fn fedimintd_version(&self) -> String {
794 self.code_version_str.clone()
795 }
796
797 async fn change_password(
798 &self,
799 new_password: &str,
800 current_password: &str,
801 guardian_auth: &GuardianAuthToken,
802 ) -> Result<(), String> {
803 let auth = &self.auth().await.0;
804 if auth != current_password {
805 return Err("Current password is incorrect".into());
806 }
807 self.change_guardian_password(new_password, guardian_auth)
808 .map_err(|e| e.to_string())
809 }
810}
811
812pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
813 vec![
814 api_endpoint! {
815 VERSION_ENDPOINT,
816 ApiVersion::new(0, 0),
817 async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
818 Ok(fedimint.api_versions_summary().to_owned())
819 }
820 },
821 api_endpoint! {
822 SUBMIT_TRANSACTION_ENDPOINT,
823 ApiVersion::new(0, 0),
824 async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
825 let transaction = transaction
826 .try_into_inner(&fedimint.modules.decoder_registry())
827 .map_err(|e| ApiError::bad_request(e.to_string()))?;
828
829 Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
832 }
833 },
834 api_endpoint! {
835 AWAIT_TRANSACTION_ENDPOINT,
836 ApiVersion::new(0, 0),
837 async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
838 fedimint.await_transaction(tx_hash).await;
839
840 Ok(tx_hash)
841 }
842 },
843 api_endpoint! {
844 AWAIT_OUTPUT_OUTCOME_ENDPOINT,
845 ApiVersion::new(0, 0),
846 async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
847 let outcome = fedimint
848 .await_output_outcome(outpoint)
849 .await
850 .map_err(|e| ApiError::bad_request(e.to_string()))?;
851
852 Ok(outcome)
853 }
854 },
855 api_endpoint! {
856 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
857 ApiVersion::new(0, 8),
858 async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
859 let outcomes = fedimint
860 .await_outputs_outcomes(outpoint_range)
861 .await
862 .map_err(|e| ApiError::bad_request(e.to_string()))?;
863
864 Ok(outcomes)
865 }
866 },
867 api_endpoint! {
868 INVITE_CODE_ENDPOINT,
869 ApiVersion::new(0, 0),
870 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
871 Ok(fedimint.cfg.get_invite_code(fedimint.get_active_api_secret()).to_string())
872 }
873 },
874 api_endpoint! {
875 FEDERATION_ID_ENDPOINT,
876 ApiVersion::new(0, 2),
877 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
878 Ok(fedimint.cfg.calculate_federation_id().to_string())
879 }
880 },
881 api_endpoint! {
882 CLIENT_CONFIG_ENDPOINT,
883 ApiVersion::new(0, 0),
884 async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
885 Ok(fedimint.client_cfg.clone())
886 }
887 },
888 api_endpoint! {
890 CLIENT_CONFIG_JSON_ENDPOINT,
891 ApiVersion::new(0, 0),
892 async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
893 Ok(fedimint.client_cfg.to_json())
894 }
895 },
896 api_endpoint! {
897 SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
898 ApiVersion::new(0, 0),
899 async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
900 Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
901 }
902 },
903 api_endpoint! {
904 STATUS_ENDPOINT,
905 ApiVersion::new(0, 0),
906 async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
907 Ok(StatusResponse {
908 server: ServerStatusLegacy::ConsensusRunning,
909 federation: Some(fedimint.get_federation_status().await?)
910 })}
911 },
912 api_endpoint! {
913 SETUP_STATUS_ENDPOINT,
914 ApiVersion::new(0, 0),
915 async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
916 Ok(SetupStatus::ConsensusIsRunning)
917 }
918 },
919 api_endpoint! {
920 CONSENSUS_ORD_LATENCY_ENDPOINT,
921 ApiVersion::new(0, 0),
922 async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
923 Ok(*fedimint.ord_latency_receiver.borrow())
924 }
925 },
926 api_endpoint! {
927 P2P_CONNECTION_STATUS_ENDPOINT,
928 ApiVersion::new(0, 0),
929 async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
930 Ok(fedimint.p2p_status_receivers
931 .iter()
932 .map(|(peer, receiver)| (*peer, receiver.borrow().clone()))
933 .collect())
934 }
935 },
936 api_endpoint! {
937 SESSION_COUNT_ENDPOINT,
938 ApiVersion::new(0, 0),
939 async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
940 Ok(fedimint.session_count().await)
941 }
942 },
943 api_endpoint! {
944 AWAIT_SESSION_OUTCOME_ENDPOINT,
945 ApiVersion::new(0, 0),
946 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
947 Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
948 }
949 },
950 api_endpoint! {
951 AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
952 ApiVersion::new(0, 0),
953 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
954 Ok((&fedimint.await_signed_session_outcome(index).await).into())
955 }
956 },
957 api_endpoint! {
958 SESSION_STATUS_ENDPOINT,
959 ApiVersion::new(0, 1),
960 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
961 Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
962 }
963 },
964 api_endpoint! {
965 SESSION_STATUS_V2_ENDPOINT,
966 ApiVersion::new(0, 5),
967 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
968 Ok((&fedimint.session_status(index).await).into())
969 }
970 },
971 api_endpoint! {
972 SHUTDOWN_ENDPOINT,
973 ApiVersion::new(0, 3),
974 async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
975 check_auth(context)?;
976 fedimint.shutdown(index);
977 Ok(())
978 }
979 },
980 api_endpoint! {
981 AUDIT_ENDPOINT,
982 ApiVersion::new(0, 0),
983 async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
984 check_auth(context)?;
985 Ok(fedimint.get_federation_audit().await?)
986 }
987 },
988 api_endpoint! {
989 GUARDIAN_CONFIG_BACKUP_ENDPOINT,
990 ApiVersion::new(0, 2),
991 async |fedimint: &ConsensusApi, context, _v: ()| -> GuardianConfigBackup {
992 let auth = check_auth(context)?;
993 let password = context.request_auth().expect("Auth was checked before").0;
994 Ok(fedimint.get_guardian_config_backup(&password, &auth))
995 }
996 },
997 api_endpoint! {
998 BACKUP_ENDPOINT,
999 ApiVersion::new(0, 0),
1000 async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
1001 let db = context.db();
1002 let mut dbtx = db.begin_transaction().await;
1003 fedimint
1004 .handle_backup_request(&mut dbtx.to_ref_nc(), request).await?;
1005 dbtx.commit_tx_result().await?;
1006 Ok(())
1007
1008 }
1009 },
1010 api_endpoint! {
1011 RECOVER_ENDPOINT,
1012 ApiVersion::new(0, 0),
1013 async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
1014 let db = context.db();
1015 let mut dbtx = db.begin_transaction_nc().await;
1016 Ok(fedimint
1017 .handle_recover_request(&mut dbtx, id).await)
1018 }
1019 },
1020 api_endpoint! {
1021 AUTH_ENDPOINT,
1022 ApiVersion::new(0, 0),
1023 async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
1024 check_auth(context)?;
1025 Ok(())
1026 }
1027 },
1028 api_endpoint! {
1029 API_ANNOUNCEMENTS_ENDPOINT,
1030 ApiVersion::new(0, 3),
1031 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
1032 Ok(fedimint.api_announcements().await)
1033 }
1034 },
1035 api_endpoint! {
1036 SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
1037 ApiVersion::new(0, 3),
1038 async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
1039 fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
1040 }
1041 },
1042 api_endpoint! {
1043 SIGN_API_ANNOUNCEMENT_ENDPOINT,
1044 ApiVersion::new(0, 3),
1045 async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
1046 check_auth(context)?;
1047 Ok(fedimint.sign_api_announcement(new_url).await)
1048 }
1049 },
1050 api_endpoint! {
1051 GUARDIAN_METADATA_ENDPOINT,
1052 ApiVersion::new(0, 9),
1053 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
1054 Ok(fedimint.guardian_metadata_list().await)
1055 }
1056 },
1057 api_endpoint! {
1058 SUBMIT_GUARDIAN_METADATA_ENDPOINT,
1059 ApiVersion::new(0, 9),
1060 async |fedimint: &ConsensusApi, _context, submission: fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission| -> () {
1061 fedimint.submit_guardian_metadata(submission.peer_id, submission.signed_guardian_metadata).await
1062 }
1063 },
1064 api_endpoint! {
1065 SIGN_GUARDIAN_METADATA_ENDPOINT,
1066 ApiVersion::new(0, 9),
1067 async |fedimint: &ConsensusApi, context, metadata: fedimint_core::net::guardian_metadata::GuardianMetadata| -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
1068 check_auth(context)?;
1069 Ok(fedimint.sign_guardian_metadata(metadata).await)
1070 }
1071 },
1072 api_endpoint! {
1073 FEDIMINTD_VERSION_ENDPOINT,
1074 ApiVersion::new(0, 4),
1075 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
1076 Ok(fedimint.fedimintd_version())
1077 }
1078 },
1079 api_endpoint! {
1080 BACKUP_STATISTICS_ENDPOINT,
1081 ApiVersion::new(0, 5),
1082 async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
1083 check_auth(context)?;
1084 let db = context.db();
1085 let mut dbtx = db.begin_transaction_nc().await;
1086 Ok(backup_statistics_static(&mut dbtx).await)
1087 }
1088 },
1089 api_endpoint! {
1090 CHANGE_PASSWORD_ENDPOINT,
1091 ApiVersion::new(0, 6),
1092 async |fedimint: &ConsensusApi, context, new_password: String| -> () {
1093 let auth = check_auth(context)?;
1094 fedimint.change_guardian_password(&new_password, &auth)?;
1095 let task_group = fedimint.task_group.clone();
1096 fedimint_core::runtime::spawn("shutdown after password change", async move {
1097 info!(target: LOG_NET_API, "Will shutdown after password change");
1098 fedimint_core:: runtime::sleep(Duration::from_secs(1)).await;
1099 task_group.shutdown();
1100 });
1101 Ok(())
1102 }
1103 },
1104 api_endpoint! {
1105 CHAIN_ID_ENDPOINT,
1106 ApiVersion::new(0, 9),
1107 async |fedimint: &ConsensusApi, _context, _v: ()| -> ChainId {
1108 fedimint
1109 .bitcoin_rpc_connection
1110 .get_chain_id()
1111 .await
1112 .map_err(|e| ApiError::server_error(e.to_string()))
1113 }
1114 },
1115 ]
1116}
1117
1118pub(crate) async fn backup_statistics_static(
1119 dbtx: &mut DatabaseTransaction<'_>,
1120) -> BackupStatistics {
1121 const DAY_SECS: u64 = 24 * 60 * 60;
1122 const WEEK_SECS: u64 = 7 * DAY_SECS;
1123 const MONTH_SECS: u64 = 30 * DAY_SECS;
1124 const QUARTER_SECS: u64 = 3 * MONTH_SECS;
1125
1126 let mut backup_stats = BackupStatistics::default();
1127
1128 let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
1129 while let Some((_, backup)) = all_backups_stream.next().await {
1130 backup_stats.num_backups += 1;
1131 backup_stats.total_size += backup.data.len();
1132
1133 let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
1134 if age_secs < DAY_SECS {
1135 backup_stats.refreshed_1d += 1;
1136 }
1137 if age_secs < WEEK_SECS {
1138 backup_stats.refreshed_1w += 1;
1139 }
1140 if age_secs < MONTH_SECS {
1141 backup_stats.refreshed_1m += 1;
1142 }
1143 if age_secs < QUARTER_SECS {
1144 backup_stats.refreshed_3m += 1;
1145 }
1146 }
1147
1148 backup_stats
1149}