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