1use std::cmp::Ordering;
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5use std::time::{Duration, SystemTime};
6
7use anyhow::{Context, Result, ensure};
8use async_trait::async_trait;
9use bitcoin::hashes::sha256;
10use fedimint_api_client::api::{
11 LegacyFederationStatus, LegacyP2PConnectionStatus, LegacyPeerStatus, StatusResponse,
12};
13use fedimint_core::admin_client::{GuardianConfigBackup, ServerStatusLegacy, SetupStatus};
14use fedimint_core::backup::{
15 BackupStatistics, ClientBackupKey, ClientBackupKeyPrefix, ClientBackupSnapshot,
16};
17use fedimint_core::config::{ClientConfig, JsonClientConfig, META_FEDERATION_NAME_KEY};
18use fedimint_core::core::backup::{BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES, SignedBackupRequest};
19use fedimint_core::core::{DynOutputOutcome, ModuleInstanceId, ModuleKind};
20use fedimint_core::db::{
21 Committable, Database, DatabaseTransaction, IDatabaseTransactionOpsCoreTyped,
22};
23#[allow(deprecated)]
24use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
25use fedimint_core::endpoint_constants::{
26 API_ANNOUNCEMENTS_ENDPOINT, AUDIT_ENDPOINT, AUTH_ENDPOINT, AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
27 AWAIT_SESSION_OUTCOME_ENDPOINT, AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
28 AWAIT_TRANSACTION_ENDPOINT, BACKUP_ENDPOINT, BACKUP_STATISTICS_ENDPOINT, CHAIN_ID_ENDPOINT,
29 CLIENT_CONFIG_ENDPOINT, CLIENT_CONFIG_JSON_ENDPOINT, CONSENSUS_ORD_LATENCY_ENDPOINT,
30 FEDERATION_ID_ENDPOINT, FEDIMINTD_VERSION_ENDPOINT, GUARDIAN_CONFIG_BACKUP_ENDPOINT,
31 GUARDIAN_METADATA_ENDPOINT, INVITE_CODE_ENDPOINT, P2P_CONNECTION_STATUS_ENDPOINT,
32 RECOVER_ENDPOINT, SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT, SESSION_COUNT_ENDPOINT,
33 SESSION_STATUS_ENDPOINT, SESSION_STATUS_V2_ENDPOINT, SETUP_STATUS_ENDPOINT, SHUTDOWN_ENDPOINT,
34 SIGN_API_ANNOUNCEMENT_ENDPOINT, SIGN_GUARDIAN_METADATA_ENDPOINT, STATUS_ENDPOINT,
35 SUBMIT_API_ANNOUNCEMENT_ENDPOINT, SUBMIT_GUARDIAN_METADATA_ENDPOINT,
36 SUBMIT_TRANSACTION_ENDPOINT, VERSION_ENDPOINT,
37};
38use fedimint_core::invite_code::InviteCode;
39use fedimint_core::module::audit::{Audit, AuditSummary};
40use fedimint_core::module::{
41 ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiResult, ApiVersion,
42 SerdeModuleEncoding, SerdeModuleEncodingBase64, SupportedApiVersionsSummary,
43 admin_api_endpoint, public_api_endpoint,
44};
45use fedimint_core::net::api_announcement::{
46 ApiAnnouncement, SignedApiAnnouncement, SignedApiAnnouncementSubmission,
47};
48use fedimint_core::net::auth::GuardianAuthToken;
49use fedimint_core::secp256k1::{PublicKey, SECP256K1};
50use fedimint_core::session_outcome::{
51 ConsensusItem, 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::version::non_zero_version_hash;
59use fedimint_core::{ChainId, OutPoint, OutPointRange, PeerId, TransactionId, secp256k1};
60use fedimint_logging::LOG_NET_API;
61use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
62use fedimint_server_core::dashboard_ui::{
63 IDashboardApi, P2PConnectionStatus, ServerBitcoinRpcStatus,
64};
65use fedimint_server_core::{DynServerModule, ServerModuleRegistry, ServerModuleRegistryExt};
66use futures::StreamExt;
67use tokio::sync::watch::{self, Receiver, Sender};
68use tracing::{debug, info, warn};
69
70use crate::config::io::{CONSENSUS_CONFIG, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG};
71use crate::config::{ServerConfig, legacy_consensus_config_hash};
72use crate::consensus::db::{
73 AcceptedItemKey, AcceptedItemPrefix, AcceptedTransactionKey, SignedSessionOutcomeKey,
74};
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, get_api_urls};
80use crate::net::p2p::{P2PConnectionState, P2PStatusReceivers};
81
82const MAX_OUTPUTS_OUTCOMES_BATCH: usize = 1024;
90
91const MAX_BACKUP_FUTURE_TIMESTAMP_SECS: u64 = 60 * 60;
96
97fn backup_timestamp_is_too_far_in_future(timestamp: SystemTime, now: SystemTime) -> bool {
98 timestamp
99 .duration_since(now)
100 .is_ok_and(|lead| lead > Duration::from_secs(MAX_BACKUP_FUTURE_TIMESTAMP_SECS))
101}
102
103fn checked_outputs_outcomes_count(outpoint_range: OutPointRange) -> Result<usize> {
109 let count = outpoint_range
110 .checked_count()
111 .context("Outpoint range is descending or too large")?;
112
113 ensure!(
114 count <= MAX_OUTPUTS_OUTCOMES_BATCH,
115 "Outpoint range must cover at most {MAX_OUTPUTS_OUTCOMES_BATCH} outputs, got {count}"
116 );
117
118 Ok(count)
119}
120
121fn legacy_peer_status(
135 p2p_receiver: &watch::Receiver<P2PConnectionState>,
136 ci_receiver: &watch::Receiver<Option<u64>>,
137 session_count: u64,
138) -> LegacyPeerStatus {
139 let p2p_connected = p2p_receiver.borrow().connected.is_some();
140 let last_contribution = *ci_receiver.borrow();
141
142 LegacyPeerStatus {
143 connection_status: if p2p_connected {
144 LegacyP2PConnectionStatus::Connected
145 } else {
146 LegacyP2PConnectionStatus::Disconnected
147 },
148 last_contribution,
149 flagged: last_contribution.unwrap_or(0) + 1 < session_count,
150 }
151}
152
153#[derive(Clone)]
154pub struct ConsensusApi {
155 pub cfg: ServerConfig,
157 pub db: Database,
159 pub modules: ServerModuleRegistry,
161 pub client_cfg: ClientConfig,
163 pub force_api_secret: Option<String>,
164 pub submission_sender: async_channel::Sender<ConsensusItem>,
166 pub shutdown_receiver: Receiver<Option<u64>>,
167 pub shutdown_sender: Sender<Option<u64>>,
168 pub ord_latency_receiver: watch::Receiver<Option<Duration>>,
169 pub p2p_status_receivers: P2PStatusReceivers,
170 pub ci_status_receivers: BTreeMap<PeerId, Receiver<Option<u64>>>,
171 pub bitcoin_rpc_connection: ServerBitcoinRpcMonitor,
172 pub supported_api_versions: SupportedApiVersionsSummary,
173 pub auth_ui: Option<ApiAuth>,
174 pub auth_api: Option<ApiAuth>,
175 pub code_version_str: String,
176 pub code_version_hash: String,
177 pub task_group: TaskGroup,
178}
179
180impl ConsensusApi {
181 pub fn api_versions_summary(&self) -> &SupportedApiVersionsSummary {
182 &self.supported_api_versions
183 }
184
185 pub fn get_active_api_secret(&self) -> Option<String> {
186 self.force_api_secret.clone()
189 }
190
191 pub async fn submit_transaction(
195 &self,
196 transaction: Transaction,
197 ) -> Result<TransactionId, TransactionError> {
198 let txid = transaction.tx_hash();
199
200 debug!(target: LOG_NET_API, %txid, "Received a submitted transaction");
201
202 let mut dbtx = self.db.begin_transaction_nc().await;
204 if dbtx
206 .get_value(&AcceptedTransactionKey(txid))
207 .await
208 .is_some()
209 {
210 debug!(target: LOG_NET_API, %txid, "Transaction already accepted");
211 return Ok(txid);
212 }
213
214 dbtx.ignore_uncommitted();
216
217 process_transaction_with_dbtx(
218 self.modules.clone(),
219 &mut dbtx,
220 &transaction,
221 self.cfg.consensus.version,
222 TxProcessingMode::Submission,
223 )
224 .await
225 .inspect_err(|err| {
226 debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
227 })?;
228
229 let mut session_index = get_finished_session_count_static(&mut dbtx).await;
230
231 let mut item_index = dbtx.find_by_prefix(&AcceptedItemPrefix).await.count().await as u64;
232
233 drop(dbtx);
234
235 let _ = self
236 .submission_sender
237 .send(ConsensusItem::Transaction(transaction.clone()))
238 .await
239 .inspect_err(|err| {
240 warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
241 });
242
243 loop {
244 let accepted_item_key = AcceptedItemKey(item_index);
245 let signed_session_outcome_key = SignedSessionOutcomeKey(session_index);
246
247 let session_completed = tokio::select! {
248 _ = self.db.wait_key_exists(&accepted_item_key) => false,
249 _ = self.db.wait_key_exists(&signed_session_outcome_key) => true,
250 };
251
252 let mut dbtx = self.db.begin_transaction_nc().await;
253
254 if dbtx
257 .get_value(&AcceptedTransactionKey(txid))
258 .await
259 .is_some()
260 {
261 return Ok(txid);
262 }
263
264 dbtx.ignore_uncommitted();
265
266 process_transaction_with_dbtx(
267 self.modules.clone(),
268 &mut dbtx,
269 &transaction,
270 self.cfg.consensus.version,
271 TxProcessingMode::Submission,
272 )
273 .await
274 .inspect_err(|err| {
275 debug!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Transaction rejected");
276 })?;
277
278 drop(dbtx);
279
280 if session_completed {
281 let _ = self
285 .submission_sender
286 .send(ConsensusItem::Transaction(transaction.clone()))
287 .await
288 .inspect_err(|err| {
289 warn!(target: LOG_NET_API, %txid, err = %err.fmt_compact(), "Unable to submit the tx into consensus");
290 });
291
292 session_index += 1;
293 item_index = 0;
294 } else {
295 item_index += 1;
296 }
297 }
298 }
299
300 pub async fn await_transaction(
301 &self,
302 txid: TransactionId,
303 ) -> (Vec<ModuleInstanceId>, DatabaseTransaction<'_, Committable>) {
304 debug!(target: LOG_NET_API, %txid, "Awaiting transaction acceptance");
305 self.db
306 .wait_key_check(&AcceptedTransactionKey(txid), std::convert::identity)
307 .await
308 }
309
310 pub async fn await_output_outcome(
311 &self,
312 outpoint: OutPoint,
313 ) -> Result<SerdeModuleEncoding<DynOutputOutcome>> {
314 debug!(target: LOG_NET_API, %outpoint, "Awaiting output outcome");
315 let (module_ids, mut dbtx) = self.await_transaction(outpoint.txid).await;
316
317 let module_id = module_ids
318 .into_iter()
319 .nth(outpoint.out_idx as usize)
320 .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
321
322 #[allow(deprecated)]
323 let outcome = self
324 .modules
325 .get_expect(module_id)
326 .output_status(
327 &mut dbtx.to_ref_with_prefix_module_id(module_id).0.into_nc(),
328 outpoint,
329 module_id,
330 )
331 .await
332 .context("No output outcome for outpoint")?;
333
334 Ok((&outcome).into())
335 }
336
337 pub async fn await_outputs_outcomes(
338 &self,
339 outpoint_range: OutPointRange,
340 ) -> Result<Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>>> {
341 let count = checked_outputs_outcomes_count(outpoint_range)?;
344
345 let (module_ids, mut dbtx) = self.await_transaction(outpoint_range.txid()).await;
347
348 let mut outcomes = Vec::with_capacity(count);
349
350 for outpoint in outpoint_range {
351 let module_id = module_ids
352 .get(outpoint.out_idx as usize)
353 .with_context(|| format!("Outpoint index out of bounds {outpoint:?}"))?;
354
355 #[allow(deprecated)]
356 let outcome = self
357 .modules
358 .get_expect(*module_id)
359 .output_status(
360 &mut dbtx.to_ref_with_prefix_module_id(*module_id).0.into_nc(),
361 outpoint,
362 *module_id,
363 )
364 .await
365 .map(|outcome| (&outcome).into());
366
367 outcomes.push(outcome);
368 }
369
370 Ok(outcomes)
371 }
372
373 pub async fn session_count(&self) -> u64 {
374 get_finished_session_count_static(&mut self.db.begin_transaction_nc().await).await
375 }
376
377 pub async fn await_signed_session_outcome(&self, index: u64) -> SignedSessionOutcome {
378 self.db
379 .wait_key_check(&SignedSessionOutcomeKey(index), std::convert::identity)
380 .await
381 .0
382 }
383
384 pub async fn session_status(&self, session_index: u64) -> SessionStatusV2 {
385 let mut dbtx = self.db.begin_transaction_nc().await;
386
387 match session_index.cmp(&get_finished_session_count_static(&mut dbtx).await) {
388 Ordering::Greater => SessionStatusV2::Initial,
389 Ordering::Equal => SessionStatusV2::Pending(
390 dbtx.find_by_prefix(&AcceptedItemPrefix)
391 .await
392 .map(|entry| entry.1)
393 .collect()
394 .await,
395 ),
396 Ordering::Less => SessionStatusV2::Complete(
397 dbtx.get_value(&SignedSessionOutcomeKey(session_index))
398 .await
399 .expect("There are no gaps in session outcomes"),
400 ),
401 }
402 }
403
404 pub async fn get_federation_status(&self) -> ApiResult<LegacyFederationStatus> {
405 let session_count = self.session_count().await;
406 let scheduled_shutdown = self.shutdown_receiver.borrow().to_owned();
407
408 let status_by_peer = self
409 .p2p_status_receivers
410 .iter()
411 .map(|(peer, p2p_receiver)| {
412 let ci_receiver = self.ci_status_receivers.get(peer).unwrap();
413
414 (
415 *peer,
416 legacy_peer_status(p2p_receiver, ci_receiver, session_count),
417 )
418 })
419 .collect::<BTreeMap<_, _>>();
420
421 let peers_flagged = status_by_peer
422 .values()
423 .filter(|status| status.flagged)
424 .count() as u64;
425
426 let peers_online = status_by_peer
427 .values()
428 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Connected)
429 .count() as u64;
430
431 let peers_offline = status_by_peer
432 .values()
433 .filter(|status| status.connection_status == LegacyP2PConnectionStatus::Disconnected)
434 .count() as u64;
435
436 Ok(LegacyFederationStatus {
437 session_count,
438 status_by_peer,
439 peers_online,
440 peers_offline,
441 peers_flagged,
442 scheduled_shutdown,
443 })
444 }
445
446 fn shutdown(&self, index: Option<u64>) {
447 self.shutdown_sender.send_replace(index);
448 }
449
450 async fn get_federation_audit(&self) -> ApiResult<AuditSummary> {
451 let mut dbtx = self.db.begin_transaction_nc().await;
452 dbtx.ignore_uncommitted();
456
457 let mut audit = Audit::default();
458 let mut module_instance_id_to_kind = BTreeMap::new();
459 for (module_instance_id, kind, module) in self.modules.iter_modules() {
460 module_instance_id_to_kind.insert(module_instance_id, kind.as_str().to_string());
461 module
462 .audit(
463 &mut dbtx.to_ref_with_prefix_module_id(module_instance_id).0,
464 &mut audit,
465 module_instance_id,
466 )
467 .await;
468 }
469 Ok(AuditSummary::from_audit(
470 &audit,
471 &module_instance_id_to_kind,
472 ))
473 }
474
475 fn get_guardian_config_backup(&self, _auth: &GuardianAuthToken) -> GuardianConfigBackup {
479 let mut tar_archive_builder = tar::Builder::new(Vec::new());
480
481 let mut append = |name: &Path, data: &[u8]| {
482 let mut header = tar::Header::new_gnu();
483 header.set_path(name).expect("Error setting path");
484 header.set_size(data.len() as u64);
485 header.set_mode(0o644);
486 header.set_cksum();
487 tar_archive_builder
488 .append(&header, data)
489 .expect("Error adding data to tar archive");
490 };
491
492 append(
493 &PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
494 &serde_json::to_vec(&self.cfg.local).expect("Error encoding local config"),
495 );
496
497 append(
498 &PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
499 &serde_json::to_vec(&self.cfg.consensus).expect("Error encoding consensus config"),
500 );
501
502 append(
503 &PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
504 &serde_json::to_vec(&self.cfg.private).expect("Error encoding private config"),
505 );
506
507 let tar_archive_bytes = tar_archive_builder
508 .into_inner()
509 .expect("Error building tar archive");
510
511 GuardianConfigBackup { tar_archive_bytes }
512 }
513
514 async fn handle_backup_request(
515 &self,
516 dbtx: &mut DatabaseTransaction<'_>,
517 request: SignedBackupRequest,
518 ) -> Result<(), ApiError> {
519 let request = request
520 .verify_valid(SECP256K1)
521 .map_err(|_| ApiError::bad_request("invalid request".into()))?;
522
523 if backup_timestamp_is_too_far_in_future(request.timestamp, fedimint_core::time::now()) {
524 return Err(ApiError::bad_request(
525 "timestamp too far in the future".into(),
526 ));
527 }
528
529 if request.payload.len() > BACKUP_REQUEST_MAX_PAYLOAD_SIZE_BYTES {
530 return Err(ApiError::bad_request("snapshot too large".into()));
531 }
532 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request");
533 if let Some(prev) = dbtx.get_value(&ClientBackupKey(request.id)).await
534 && request.timestamp <= prev.timestamp
535 {
536 debug!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Received client backup request with old timestamp - ignoring");
537 return Err(ApiError::bad_request("timestamp too small".into()));
538 }
539
540 info!(target: LOG_NET_API, id = %request.id, len = request.payload.len(), "Storing new client backup");
541 let overwritten = dbtx
542 .insert_entry(
543 &ClientBackupKey(request.id),
544 &ClientBackupSnapshot {
545 timestamp: request.timestamp,
546 data: request.payload.clone(),
547 },
548 )
549 .await
550 .is_some();
551 BACKUP_WRITE_SIZE_BYTES.observe(request.payload.len() as f64);
552 if !overwritten {
553 dbtx.on_commit(|| STORED_BACKUPS_COUNT.inc());
554 }
555
556 Ok(())
557 }
558
559 async fn handle_recover_request(
560 &self,
561 dbtx: &mut DatabaseTransaction<'_>,
562 id: PublicKey,
563 ) -> Option<ClientBackupSnapshot> {
564 dbtx.get_value(&ClientBackupKey(id)).await
565 }
566
567 async fn api_announcements(&self) -> BTreeMap<PeerId, SignedApiAnnouncement> {
570 self.db
571 .begin_transaction_nc()
572 .await
573 .find_by_prefix(&ApiAnnouncementPrefix)
574 .await
575 .map(|(announcement_key, announcement)| (announcement_key.0, announcement))
576 .collect()
577 .await
578 }
579
580 fn fedimintd_version(&self) -> String {
582 self.code_version_str.clone()
583 }
584
585 async fn submit_api_announcement(
588 &self,
589 peer_id: PeerId,
590 announcement: SignedApiAnnouncement,
591 ) -> Result<(), ApiError> {
592 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
593 return Err(ApiError::bad_request("Peer not in federation".into()));
594 };
595
596 if !announcement.verify(SECP256K1, peer_key) {
597 return Err(ApiError::bad_request("Invalid signature".into()));
598 }
599
600 self.db
602 .autocommit(
603 |dbtx, _| {
604 let announcement = announcement.clone();
605 Box::pin(async move {
606 if let Some(existing_announcement) =
607 dbtx.get_value(&ApiAnnouncementKey(peer_id)).await
608 {
609 if existing_announcement.api_announcement
614 == announcement.api_announcement
615 {
616 return Ok(());
617 }
618
619 if existing_announcement.api_announcement.nonce
622 >= announcement.api_announcement.nonce
623 {
624 return Err(ApiError::bad_request(
625 "Outdated or redundant announcement".into(),
626 ));
627 }
628 }
629
630 dbtx.insert_entry(&ApiAnnouncementKey(peer_id), &announcement)
631 .await;
632 Ok(())
633 })
634 },
635 None,
636 )
637 .await
638 .map_err(|e| match e {
639 fedimint_core::db::AutocommitError::ClosureError { error, .. } => error,
640 fedimint_core::db::AutocommitError::CommitFailed { last_error, .. } => {
641 ApiError::server_error(format!("Database commit failed: {last_error}"))
642 }
643 })
644 }
645
646 async fn sign_api_announcement(&self, new_url: SafeUrl) -> SignedApiAnnouncement {
647 self.db
648 .autocommit(
649 |dbtx, _| {
650 let new_url_inner = new_url.clone();
651 Box::pin(async move {
652 let new_nonce = dbtx
653 .get_value(&ApiAnnouncementKey(self.cfg.local.identity))
654 .await
655 .map_or(0, |a| a.api_announcement.nonce + 1);
656 let announcement = ApiAnnouncement {
657 api_url: new_url_inner,
658 nonce: new_nonce,
659 };
660 let ctx = secp256k1::Secp256k1::new();
661 let signed_announcement = announcement
662 .sign(&ctx, &self.cfg.private.broadcast_secret_key.keypair(&ctx));
663
664 dbtx.insert_entry(
665 &ApiAnnouncementKey(self.cfg.local.identity),
666 &signed_announcement,
667 )
668 .await;
669
670 Result::<_, ()>::Ok(signed_announcement)
671 })
672 },
673 None,
674 )
675 .await
676 .expect("Will not terminate on error")
677 }
678
679 async fn guardian_metadata_list(
680 &self,
681 ) -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
682 use crate::net::api::guardian_metadata::{GuardianMetadataKey, GuardianMetadataPrefix};
683
684 self.db
685 .begin_transaction_nc()
686 .await
687 .find_by_prefix(&GuardianMetadataPrefix)
688 .await
689 .map(|(key, metadata): (GuardianMetadataKey, _)| (key.0, metadata))
690 .collect()
691 .await
692 }
693
694 async fn submit_guardian_metadata(
695 &self,
696 peer_id: PeerId,
697 metadata: fedimint_core::net::guardian_metadata::SignedGuardianMetadata,
698 ) -> Result<(), ApiError> {
699 use crate::net::api::guardian_metadata::GuardianMetadataKey;
700
701 let Some(peer_key) = self.cfg.consensus.broadcast_public_keys.get(&peer_id) else {
702 return Err(ApiError::bad_request("Peer not in federation".into()));
703 };
704
705 let now = fedimint_core::time::duration_since_epoch();
706 if let Err(e) = metadata.verify(SECP256K1, peer_key, now) {
707 return Err(ApiError::bad_request(format!(
708 "Invalid signature or timestamp: {e}"
709 )));
710 }
711
712 let mut dbtx = self.db.begin_transaction().await;
713
714 if let Some(existing_metadata) = dbtx.get_value(&GuardianMetadataKey(peer_id)).await {
715 if existing_metadata.bytes == metadata.bytes {
719 return Ok(());
720 }
721
722 if metadata.guardian_metadata().timestamp_secs
724 <= existing_metadata.guardian_metadata().timestamp_secs
725 {
726 return Err(ApiError::bad_request(
727 "New metadata timestamp is not newer than existing".into(),
728 ));
729 }
730 }
731
732 dbtx.insert_entry(&GuardianMetadataKey(peer_id), &metadata)
733 .await;
734 dbtx.commit_tx().await;
735
736 Ok(())
737 }
738
739 async fn sign_guardian_metadata(
740 &self,
741 new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
742 ) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
743 sign_guardian_metadata_preserving_iroh_endpoint(
744 &self.db,
745 self.cfg.local.identity,
746 &self.cfg.private.broadcast_secret_key,
747 new_metadata,
748 )
749 .await
750 }
751
752 async fn get_invite_code(&self, api_secret: Option<String>) -> InviteCode {
753 let identity = self.cfg.local.identity;
754 let mut api_urls = get_api_urls(&self.db, &self.cfg.consensus).await;
755
756 InviteCode::new(
757 api_urls
758 .remove(&identity)
759 .expect("API URL for our identity must be present"),
760 identity,
761 self.cfg.calculate_federation_id(),
762 api_secret,
763 )
764 }
765}
766
767async fn sign_guardian_metadata_preserving_iroh_endpoint(
768 db: &Database,
769 identity: PeerId,
770 broadcast_secret_key: &secp256k1::SecretKey,
771 new_metadata: fedimint_core::net::guardian_metadata::GuardianMetadata,
772) -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
773 use crate::net::api::guardian_metadata::GuardianMetadataKey;
774
775 db.autocommit(
776 |dbtx, _| {
777 let mut new_metadata = new_metadata.clone();
778 Box::pin(async move {
779 new_metadata.iroh_next_endpoint = dbtx
780 .get_value(&GuardianMetadataKey(identity))
781 .await
782 .and_then(|metadata| metadata.guardian_metadata().iroh_next_endpoint.clone());
783
784 let ctx = secp256k1::Secp256k1::new();
785 let signed_metadata = new_metadata.sign(&ctx, &broadcast_secret_key.keypair(&ctx));
786
787 dbtx.insert_entry(&GuardianMetadataKey(identity), &signed_metadata)
788 .await;
789
790 Result::<_, ()>::Ok(signed_metadata)
791 })
792 },
793 None,
794 )
795 .await
796 .expect("Will not terminate on error")
797}
798
799#[async_trait]
800impl HasApiContext<ConsensusApi> for ConsensusApi {
801 async fn context(
802 &self,
803 request: &ApiRequestErased,
804 id: Option<ModuleInstanceId>,
805 ) -> (&ConsensusApi, ApiEndpointContext) {
806 let mut db = self.db.clone();
807 if let Some(id) = id {
808 db = self.db.with_prefix_module_id(id).0;
809 }
810 let has_auth = match (&self.auth_api, &request.auth) {
811 (Some(server_auth), Some(req_auth)) => server_auth.verify(req_auth.as_str()),
812 _ => false,
813 };
814
815 (self, ApiEndpointContext::new(db, has_auth))
816 }
817}
818
819#[async_trait]
820impl HasApiContext<DynServerModule> for ConsensusApi {
821 async fn context(
822 &self,
823 request: &ApiRequestErased,
824 id: Option<ModuleInstanceId>,
825 ) -> (&DynServerModule, ApiEndpointContext) {
826 let (_, context): (&ConsensusApi, _) = self.context(request, id).await;
827 (
828 self.modules.get_expect(id.expect("required module id")),
829 context,
830 )
831 }
832}
833
834#[async_trait]
835impl IDashboardApi for ConsensusApi {
836 fn auth_ui(&self) -> Option<ApiAuth> {
837 self.auth_ui.clone()
838 }
839
840 async fn guardian_id(&self) -> PeerId {
841 self.cfg.local.identity
842 }
843
844 async fn guardian_names(&self) -> BTreeMap<PeerId, String> {
845 self.cfg
846 .consensus
847 .api_endpoints()
848 .iter()
849 .map(|(peer_id, endpoint)| (*peer_id, endpoint.name.clone()))
850 .collect()
851 }
852
853 async fn federation_name(&self) -> String {
854 self.cfg
855 .consensus
856 .meta
857 .get(META_FEDERATION_NAME_KEY)
858 .cloned()
859 .expect("Federation name must be set")
860 }
861
862 async fn session_count(&self) -> u64 {
863 self.session_count().await
864 }
865
866 async fn get_session_status(&self, session_idx: u64) -> SessionStatusV2 {
867 self.session_status(session_idx).await
868 }
869
870 async fn consensus_ord_latency(&self) -> Option<Duration> {
871 *self.ord_latency_receiver.borrow()
872 }
873
874 async fn p2p_connection_status(&self) -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
875 self.p2p_status_receivers
876 .iter()
877 .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
878 .collect()
879 }
880
881 async fn federation_invite_code(&self) -> String {
882 self.get_invite_code(self.get_active_api_secret())
883 .await
884 .to_string()
885 }
886
887 async fn federation_audit(&self) -> AuditSummary {
888 self.get_federation_audit()
889 .await
890 .expect("Failed to get federation audit")
891 }
892
893 async fn bitcoin_rpc_url(&self) -> SafeUrl {
894 self.bitcoin_rpc_connection.url()
895 }
896
897 async fn bitcoin_rpc_status(&self) -> Option<ServerBitcoinRpcStatus> {
898 self.bitcoin_rpc_connection.status()
899 }
900
901 async fn download_guardian_config_backup(
902 &self,
903 guardian_auth: &GuardianAuthToken,
904 ) -> GuardianConfigBackup {
905 self.get_guardian_config_backup(guardian_auth)
906 }
907
908 fn get_module_by_kind(&self, kind: ModuleKind) -> Option<&DynServerModule> {
909 self.modules
910 .iter_modules()
911 .find_map(|(_, module_kind, module)| {
912 if *module_kind == kind {
913 Some(module)
914 } else {
915 None
916 }
917 })
918 }
919
920 async fn fedimintd_version(&self) -> String {
921 self.code_version_str.clone()
922 }
923
924 async fn fedimintd_version_hash(&self) -> Option<String> {
925 non_zero_version_hash(&self.code_version_hash).map(str::to_owned)
926 }
927}
928
929pub fn server_endpoints() -> Vec<ApiEndpoint<ConsensusApi>> {
930 vec![
931 public_api_endpoint! {
932 VERSION_ENDPOINT,
933 ApiVersion::new(0, 0),
934 async |fedimint: &ConsensusApi, _context, _v: ()| -> SupportedApiVersionsSummary {
935 Ok(fedimint.api_versions_summary().to_owned())
936 }
937 },
938 public_api_endpoint! {
939 SUBMIT_TRANSACTION_ENDPOINT,
940 ApiVersion::new(0, 0),
941 async |fedimint: &ConsensusApi, _context, transaction: SerdeTransaction| -> SerdeModuleEncoding<TransactionSubmissionOutcome> {
942 let transaction = transaction
943 .try_into_inner(&fedimint.modules.decoder_registry())
944 .map_err(|e| ApiError::bad_request(e.to_string()))?;
945
946 Ok((&TransactionSubmissionOutcome(fedimint.submit_transaction(transaction).await)).into())
949 }
950 },
951 public_api_endpoint! {
952 AWAIT_TRANSACTION_ENDPOINT,
953 ApiVersion::new(0, 0),
954 async |fedimint: &ConsensusApi, _context, tx_hash: TransactionId| -> TransactionId {
955 fedimint.await_transaction(tx_hash).await;
956
957 Ok(tx_hash)
958 }
959 },
960 public_api_endpoint! {
961 AWAIT_OUTPUT_OUTCOME_ENDPOINT,
962 ApiVersion::new(0, 0),
963 async |fedimint: &ConsensusApi, _context, outpoint: OutPoint| -> SerdeModuleEncoding<DynOutputOutcome> {
964 let outcome = fedimint
965 .await_output_outcome(outpoint)
966 .await
967 .map_err(|e| ApiError::bad_request(e.to_string()))?;
968
969 Ok(outcome)
970 }
971 },
972 public_api_endpoint! {
973 AWAIT_OUTPUTS_OUTCOMES_ENDPOINT,
974 ApiVersion::new(0, 8),
975 async |fedimint: &ConsensusApi, _context, outpoint_range: OutPointRange| -> Vec<Option<SerdeModuleEncoding<DynOutputOutcome>>> {
976 let outcomes = fedimint
977 .await_outputs_outcomes(outpoint_range)
978 .await
979 .map_err(|e| ApiError::bad_request(e.to_string()))?;
980
981 Ok(outcomes)
982 }
983 },
984 public_api_endpoint! {
985 INVITE_CODE_ENDPOINT,
986 ApiVersion::new(0, 0),
987 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
988 Ok(fedimint.get_invite_code(fedimint.get_active_api_secret()).await.to_string())
989 }
990 },
991 public_api_endpoint! {
992 FEDERATION_ID_ENDPOINT,
993 ApiVersion::new(0, 2),
994 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
995 Ok(fedimint.cfg.calculate_federation_id().to_string())
996 }
997 },
998 public_api_endpoint! {
999 CLIENT_CONFIG_ENDPOINT,
1000 ApiVersion::new(0, 0),
1001 async |fedimint: &ConsensusApi, _context, _v: ()| -> ClientConfig {
1002 Ok(fedimint.client_cfg.clone())
1003 }
1004 },
1005 public_api_endpoint! {
1007 CLIENT_CONFIG_JSON_ENDPOINT,
1008 ApiVersion::new(0, 0),
1009 async |fedimint: &ConsensusApi, _context, _v: ()| -> JsonClientConfig {
1010 Ok(fedimint.client_cfg.to_json())
1011 }
1012 },
1013 public_api_endpoint! {
1014 SERVER_CONFIG_CONSENSUS_HASH_ENDPOINT,
1015 ApiVersion::new(0, 0),
1016 async |fedimint: &ConsensusApi, _context, _v: ()| -> sha256::Hash {
1017 Ok(legacy_consensus_config_hash(&fedimint.cfg.consensus))
1018 }
1019 },
1020 public_api_endpoint! {
1021 STATUS_ENDPOINT,
1022 ApiVersion::new(0, 0),
1023 async |fedimint: &ConsensusApi, _context, _v: ()| -> StatusResponse {
1024 Ok(StatusResponse {
1025 server: ServerStatusLegacy::ConsensusRunning,
1026 federation: Some(fedimint.get_federation_status().await?)
1027 })}
1028 },
1029 public_api_endpoint! {
1030 SETUP_STATUS_ENDPOINT,
1031 ApiVersion::new(0, 0),
1032 async |_f: &ConsensusApi, _c, _v: ()| -> SetupStatus {
1033 Ok(SetupStatus::ConsensusIsRunning)
1034 }
1035 },
1036 public_api_endpoint! {
1037 CONSENSUS_ORD_LATENCY_ENDPOINT,
1038 ApiVersion::new(0, 0),
1039 async |fedimint: &ConsensusApi, _c, _v: ()| -> Option<Duration> {
1040 Ok(*fedimint.ord_latency_receiver.borrow())
1041 }
1042 },
1043 public_api_endpoint! {
1044 P2P_CONNECTION_STATUS_ENDPOINT,
1045 ApiVersion::new(0, 0),
1046 async |fedimint: &ConsensusApi, _c, _v: ()| -> BTreeMap<PeerId, Option<P2PConnectionStatus>> {
1047 Ok(fedimint.p2p_status_receivers
1048 .iter()
1049 .map(|(peer, receiver)| (*peer, receiver.borrow().connected.clone()))
1050 .collect())
1051 }
1052 },
1053 public_api_endpoint! {
1054 SESSION_COUNT_ENDPOINT,
1055 ApiVersion::new(0, 0),
1056 async |fedimint: &ConsensusApi, _context, _v: ()| -> u64 {
1057 Ok(fedimint.session_count().await)
1058 }
1059 },
1060 public_api_endpoint! {
1061 AWAIT_SESSION_OUTCOME_ENDPOINT,
1062 ApiVersion::new(0, 0),
1063 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionOutcome> {
1064 Ok((&fedimint.await_signed_session_outcome(index).await.session_outcome).into())
1065 }
1066 },
1067 public_api_endpoint! {
1068 AWAIT_SIGNED_SESSION_OUTCOME_ENDPOINT,
1069 ApiVersion::new(0, 0),
1070 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SignedSessionOutcome> {
1071 Ok((&fedimint.await_signed_session_outcome(index).await).into())
1072 }
1073 },
1074 public_api_endpoint! {
1075 SESSION_STATUS_ENDPOINT,
1076 ApiVersion::new(0, 1),
1077 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncoding<SessionStatus> {
1078 Ok((&SessionStatus::from(fedimint.session_status(index).await)).into())
1079 }
1080 },
1081 public_api_endpoint! {
1082 SESSION_STATUS_V2_ENDPOINT,
1083 ApiVersion::new(0, 5),
1084 async |fedimint: &ConsensusApi, _context, index: u64| -> SerdeModuleEncodingBase64<SessionStatusV2> {
1085 Ok((&fedimint.session_status(index).await).into())
1086 }
1087 },
1088 admin_api_endpoint! {
1089 SHUTDOWN_ENDPOINT,
1090 ApiVersion::new(0, 3),
1091 async |fedimint: &ConsensusApi, context, index: Option<u64>| -> () {
1092 fedimint.shutdown(index);
1093 Ok(())
1094 }
1095 },
1096 admin_api_endpoint! {
1097 AUDIT_ENDPOINT,
1098 ApiVersion::new(0, 0),
1099 async |fedimint: &ConsensusApi, context, _v: ()| -> AuditSummary {
1100 Ok(fedimint.get_federation_audit().await?)
1101 }
1102 },
1103 admin_api_endpoint! {
1104 GUARDIAN_CONFIG_BACKUP_ENDPOINT,
1105 ApiVersion::new(0, 2),
1106 async |fedimint: &ConsensusApi, context, auth, _v: ()| -> GuardianConfigBackup {
1107 Ok(fedimint.get_guardian_config_backup(&auth))
1108 }
1109 },
1110 public_api_endpoint! {
1111 BACKUP_ENDPOINT,
1112 ApiVersion::new(0, 0),
1113 async |fedimint: &ConsensusApi, context, request: SignedBackupRequest| -> () {
1114 let db = context.db();
1115 let mut dbtx = db.begin_transaction().await;
1116 fedimint
1117 .handle_backup_request(&mut dbtx.to_ref_nc(), request).await?;
1118 dbtx.commit_tx_result().await?;
1119 Ok(())
1120
1121 }
1122 },
1123 public_api_endpoint! {
1124 RECOVER_ENDPOINT,
1125 ApiVersion::new(0, 0),
1126 async |fedimint: &ConsensusApi, context, id: PublicKey| -> Option<ClientBackupSnapshot> {
1127 let db = context.db();
1128 let mut dbtx = db.begin_transaction_nc().await;
1129 Ok(fedimint
1130 .handle_recover_request(&mut dbtx, id).await)
1131 }
1132 },
1133 admin_api_endpoint! {
1134 AUTH_ENDPOINT,
1135 ApiVersion::new(0, 0),
1136 async |_fedimint: &ConsensusApi, context, _v: ()| -> () {
1137 Ok(())
1138 }
1139 },
1140 public_api_endpoint! {
1141 API_ANNOUNCEMENTS_ENDPOINT,
1142 ApiVersion::new(0, 3),
1143 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, SignedApiAnnouncement> {
1144 Ok(fedimint.api_announcements().await)
1145 }
1146 },
1147 public_api_endpoint! {
1148 SUBMIT_API_ANNOUNCEMENT_ENDPOINT,
1149 ApiVersion::new(0, 3),
1150 async |fedimint: &ConsensusApi, _context, submission: SignedApiAnnouncementSubmission| -> () {
1151 fedimint.submit_api_announcement(submission.peer_id, submission.signed_api_announcement).await
1152 }
1153 },
1154 admin_api_endpoint! {
1155 SIGN_API_ANNOUNCEMENT_ENDPOINT,
1156 ApiVersion::new(0, 3),
1157 async |fedimint: &ConsensusApi, context, new_url: SafeUrl| -> SignedApiAnnouncement {
1158 Ok(fedimint.sign_api_announcement(new_url).await)
1159 }
1160 },
1161 public_api_endpoint! {
1162 GUARDIAN_METADATA_ENDPOINT,
1163 ApiVersion::new(0, 9),
1164 async |fedimint: &ConsensusApi, _context, _v: ()| -> BTreeMap<PeerId, fedimint_core::net::guardian_metadata::SignedGuardianMetadata> {
1165 Ok(fedimint.guardian_metadata_list().await)
1166 }
1167 },
1168 public_api_endpoint! {
1169 SUBMIT_GUARDIAN_METADATA_ENDPOINT,
1170 ApiVersion::new(0, 9),
1171 async |fedimint: &ConsensusApi, _context, submission: fedimint_core::net::guardian_metadata::SignedGuardianMetadataSubmission| -> () {
1172 fedimint.submit_guardian_metadata(submission.peer_id, submission.signed_guardian_metadata).await
1173 }
1174 },
1175 admin_api_endpoint! {
1176 SIGN_GUARDIAN_METADATA_ENDPOINT,
1177 ApiVersion::new(0, 9),
1178 async |fedimint: &ConsensusApi, context, metadata: fedimint_core::net::guardian_metadata::GuardianMetadata| -> fedimint_core::net::guardian_metadata::SignedGuardianMetadata {
1179 Ok(fedimint.sign_guardian_metadata(metadata).await)
1180 }
1181 },
1182 public_api_endpoint! {
1183 FEDIMINTD_VERSION_ENDPOINT,
1184 ApiVersion::new(0, 4),
1185 async |fedimint: &ConsensusApi, _context, _v: ()| -> String {
1186 Ok(fedimint.fedimintd_version())
1187 }
1188 },
1189 admin_api_endpoint! {
1190 BACKUP_STATISTICS_ENDPOINT,
1191 ApiVersion::new(0, 5),
1192 async |_fedimint: &ConsensusApi, context, _v: ()| -> BackupStatistics {
1193 let db = context.db();
1194 let mut dbtx = db.begin_transaction_nc().await;
1195 Ok(backup_statistics_static(&mut dbtx).await)
1196 }
1197 },
1198 public_api_endpoint! {
1199 CHAIN_ID_ENDPOINT,
1200 ApiVersion::new(0, 9),
1201 async |fedimint: &ConsensusApi, _context, _v: ()| -> ChainId {
1202 fedimint
1203 .bitcoin_rpc_connection
1204 .get_chain_id()
1205 .await
1206 .map_err(|e| ApiError::server_error(e.to_string()))
1207 }
1208 },
1209 ]
1210}
1211
1212pub(crate) async fn backup_statistics_static(
1213 dbtx: &mut DatabaseTransaction<'_>,
1214) -> BackupStatistics {
1215 const DAY_SECS: u64 = 24 * 60 * 60;
1216 const WEEK_SECS: u64 = 7 * DAY_SECS;
1217 const MONTH_SECS: u64 = 30 * DAY_SECS;
1218 const QUARTER_SECS: u64 = 3 * MONTH_SECS;
1219
1220 let mut backup_stats = BackupStatistics::default();
1221
1222 let mut all_backups_stream = dbtx.find_by_prefix(&ClientBackupKeyPrefix).await;
1223 while let Some((_, backup)) = all_backups_stream.next().await {
1224 backup_stats.num_backups += 1;
1225 backup_stats.total_size += backup.data.len();
1226
1227 let age_secs = backup.timestamp.elapsed().unwrap_or_default().as_secs();
1228 if age_secs < DAY_SECS {
1229 backup_stats.refreshed_1d += 1;
1230 }
1231 if age_secs < WEEK_SECS {
1232 backup_stats.refreshed_1w += 1;
1233 }
1234 if age_secs < MONTH_SECS {
1235 backup_stats.refreshed_1m += 1;
1236 }
1237 if age_secs < QUARTER_SECS {
1238 backup_stats.refreshed_3m += 1;
1239 }
1240 }
1241
1242 backup_stats
1243}
1244
1245#[cfg(test)]
1246mod tests {
1247 use std::sync::Arc;
1248 use std::sync::atomic::Ordering::Relaxed;
1249 use std::sync::atomic::{AtomicBool, AtomicUsize};
1250 use std::thread;
1251 use std::time::Instant;
1252
1253 use fedimint_core::db::IRawDatabaseExt as _;
1254 use fedimint_core::db::mem_impl::MemDatabase;
1255 use fedimint_core::net::guardian_metadata::GuardianMetadata;
1256 use fedimint_core::{BitcoinHash as _, IdxRange, TransactionId};
1257
1258 use super::*;
1259 use crate::net::api::guardian_metadata::GuardianMetadataKey;
1260
1261 #[test]
1265 fn outputs_outcomes_range_is_bounded() {
1266 let txid = TransactionId::from_slice(&[0; 32]).expect("32 bytes is a valid txid");
1267 let range = |start, end| OutPointRange::new(txid, IdxRange::from(start..end));
1268
1269 assert_eq!(
1270 checked_outputs_outcomes_count(range(0, MAX_OUTPUTS_OUTCOMES_BATCH as u64))
1271 .expect("a range at the limit is served"),
1272 MAX_OUTPUTS_OUTCOMES_BATCH
1273 );
1274
1275 for rejected in [
1276 range(0, u64::MAX),
1277 range(0, MAX_OUTPUTS_OUTCOMES_BATCH as u64 + 1),
1278 range(u64::MAX, 0),
1281 range(5, 4),
1282 ] {
1283 assert!(
1284 checked_outputs_outcomes_count(rejected).is_err(),
1285 "{rejected:?} must be rejected"
1286 );
1287 }
1288 }
1289
1290 #[test]
1299 fn legacy_peer_status_does_not_deadlock_against_a_writer() {
1300 const READERS: usize = 8;
1301 const READS_PER_THREAD: u64 = 100_000;
1302
1303 let (p2p_sender, p2p_receiver) = watch::channel(P2PConnectionState {
1304 connected: None,
1305 last_error: None,
1306 });
1307 let (ci_sender, ci_receiver) = watch::channel(None);
1308 let stop = Arc::new(AtomicBool::new(false));
1309
1310 let writers = [
1311 {
1312 let stop = stop.clone();
1315 thread::spawn(move || {
1316 let mut connected = false;
1317 while !stop.load(Relaxed) {
1318 connected = !connected;
1319 p2p_sender.send_replace(P2PConnectionState {
1320 connected: connected.then_some(P2PConnectionStatus {
1321 conn_type: None,
1322 rtt: None,
1323 }),
1324 last_error: None,
1325 });
1326 }
1327 })
1328 },
1329 {
1330 let stop = stop.clone();
1331 thread::spawn(move || {
1332 let mut session = 0;
1333 while !stop.load(Relaxed) {
1334 session += 1;
1335 ci_sender.send_replace(Some(session));
1336 }
1337 })
1338 },
1339 ];
1340
1341 let finished_readers = Arc::new(AtomicUsize::new(0));
1342
1343 for _ in 0..READERS {
1344 let p2p_receiver = p2p_receiver.clone();
1345 let ci_receiver = ci_receiver.clone();
1346 let finished_readers = finished_readers.clone();
1347
1348 thread::spawn(move || {
1349 for _ in 0..READS_PER_THREAD {
1350 legacy_peer_status(&p2p_receiver, &ci_receiver, u64::MAX);
1351 }
1352
1353 finished_readers.fetch_add(1, Relaxed);
1354 });
1355 }
1356
1357 let deadline = Instant::now() + Duration::from_secs(60);
1360 while finished_readers.load(Relaxed) < READERS {
1361 if Instant::now() >= deadline {
1362 stop.store(true, Relaxed);
1367
1368 panic!(
1369 "only {} of {READERS} reader threads finished before the deadline, \
1370 which means peer status reads are wedged behind a writer",
1371 finished_readers.load(Relaxed)
1372 );
1373 }
1374
1375 thread::sleep(Duration::from_millis(10));
1376 }
1377
1378 stop.store(true, Relaxed);
1379 for writer in writers {
1380 writer.join().expect("writer thread panicked");
1381 }
1382 }
1383
1384 #[test]
1387 fn legacy_peer_status_flags_peers_lagging_the_session_count() {
1388 let peer_status = |connected: bool, last_contribution, session_count| {
1389 let (_p2p_sender, p2p_receiver) = watch::channel(P2PConnectionState {
1390 connected: connected.then_some(P2PConnectionStatus {
1391 conn_type: None,
1392 rtt: None,
1393 }),
1394 last_error: None,
1395 });
1396 let (_ci_sender, ci_receiver) = watch::channel(last_contribution);
1397
1398 legacy_peer_status(&p2p_receiver, &ci_receiver, session_count)
1399 };
1400
1401 let current = peer_status(true, Some(9), 10);
1402 assert_eq!(
1403 current.connection_status,
1404 LegacyP2PConnectionStatus::Connected
1405 );
1406 assert_eq!(current.last_contribution, Some(9));
1407 assert!(!current.flagged, "a peer one behind the session is current");
1408
1409 assert!(
1410 peer_status(true, Some(8), 10).flagged,
1411 "a peer two behind the session is flagged"
1412 );
1413 assert!(
1414 peer_status(true, None, 10).flagged,
1415 "a peer that has never contributed is flagged"
1416 );
1417 assert!(
1418 !peer_status(true, None, 1).flagged,
1419 "no peer has contributed to the first session yet"
1420 );
1421
1422 assert_eq!(
1423 peer_status(false, Some(9), 10).connection_status,
1424 LegacyP2PConnectionStatus::Disconnected
1425 );
1426 }
1427
1428 #[test]
1429 fn backup_timestamp_future_limit_is_strict() {
1430 let now = SystemTime::UNIX_EPOCH;
1431 let at_limit = now
1432 .checked_add(Duration::from_secs(MAX_BACKUP_FUTURE_TIMESTAMP_SECS))
1433 .expect("UNIX epoch plus one hour is representable");
1434 let beyond_limit = at_limit
1435 .checked_add(Duration::from_nanos(1))
1436 .expect("one nanosecond past the limit is representable");
1437 let past = now
1438 .checked_sub(Duration::from_nanos(1))
1439 .expect("one nanosecond before the UNIX epoch is representable");
1440
1441 assert!(!backup_timestamp_is_too_far_in_future(now, now));
1442 assert!(!backup_timestamp_is_too_far_in_future(past, now));
1443 assert!(!backup_timestamp_is_too_far_in_future(at_limit, now));
1444 assert!(backup_timestamp_is_too_far_in_future(beyond_limit, now));
1445 }
1446
1447 #[tokio::test]
1448 async fn admin_metadata_update_preserves_persisted_iroh_endpoint() {
1449 let db: Database = MemDatabase::new().into_database();
1450 let identity = PeerId::from(0);
1451 let broadcast_secret_key =
1452 secp256k1::SecretKey::from_slice(&[42; 32]).expect("valid test key");
1453 let ctx = secp256k1::Secp256k1::new();
1454
1455 let existing = GuardianMetadata::new(
1456 vec!["wss://old.example".parse().expect("valid URL")],
1457 "old-pkarr".to_owned(),
1458 1,
1459 )
1460 .with_iroh_next_endpoint("persisted-iroh-id".to_owned())
1461 .sign(&ctx, &broadcast_secret_key.keypair(&ctx));
1462 let mut dbtx = db.begin_transaction().await;
1463 dbtx.insert_entry(&GuardianMetadataKey(identity), &existing)
1464 .await;
1465 dbtx.commit_tx().await;
1466
1467 let updated = GuardianMetadata::new(
1468 vec!["wss://new.example".parse().expect("valid URL")],
1469 "new-pkarr".to_owned(),
1470 2,
1471 );
1472 let signed = sign_guardian_metadata_preserving_iroh_endpoint(
1473 &db,
1474 identity,
1475 &broadcast_secret_key,
1476 updated,
1477 )
1478 .await;
1479
1480 assert_eq!(
1481 signed.guardian_metadata().iroh_next_endpoint.as_deref(),
1482 Some("persisted-iroh-id")
1483 );
1484 assert_eq!(
1485 signed.guardian_metadata().api_urls,
1486 vec!["wss://new.example".parse().expect("valid URL")]
1487 );
1488 assert_eq!(signed.guardian_metadata().pkarr_id_z32, "new-pkarr");
1489 assert_eq!(
1490 db.begin_transaction_nc()
1491 .await
1492 .get_value(&GuardianMetadataKey(identity))
1493 .await
1494 .expect("metadata was persisted")
1495 .tagged_hash(),
1496 signed.tagged_hash()
1497 );
1498 }
1499}