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