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