1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use fedimint_api_client::api::{DynGlobalApi, ServerError};
6use fedimint_connectors::iroh_next_endpoint_url;
7use fedimint_core::config::ClientConfig;
8use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
9use fedimint_core::encoding::{Decodable, Encodable};
10use fedimint_core::envs::is_running_in_test_env;
11use fedimint_core::net::api_announcement::SignedApiAnnouncement;
12use fedimint_core::net::guardian_metadata::SignedGuardianMetadata;
13use fedimint_core::runtime::{self, sleep};
14use fedimint_core::secp256k1::SECP256K1;
15use fedimint_core::util::backoff_util::custom_backoff;
16use fedimint_core::util::{FmtCompact as _, SafeUrl};
17use fedimint_core::{NumPeersExt as _, PeerId, impl_db_lookup, impl_db_record};
18use fedimint_logging::LOG_CLIENT;
19use futures::stream::{FuturesUnordered, StreamExt as _};
20use tracing::{debug, warn};
21
22use crate::Client;
23use crate::db::DbKeyPrefix;
24use crate::guardian_metadata::GuardianMetadataPrefix;
25
26#[cfg(test)]
27mod tests;
28
29#[derive(Clone, Debug, Encodable, Decodable)]
30pub struct ApiAnnouncementKey(pub PeerId);
31
32#[derive(Clone, Debug, Encodable, Decodable)]
33pub struct ApiAnnouncementPrefix;
34
35impl_db_record!(
36 key = ApiAnnouncementKey,
37 value = SignedApiAnnouncement,
38 db_prefix = DbKeyPrefix::ApiUrlAnnouncement,
39 notify_on_modify = false,
40);
41impl_db_lookup!(
42 key = ApiAnnouncementKey,
43 query_prefix = ApiAnnouncementPrefix
44);
45
46pub(crate) async fn run_api_announcement_refresh_task(client_inner: Arc<Client>) {
49 let guardian_pub_keys = client_inner.get_guardian_public_keys_blocking().await;
51 loop {
52 let api: &DynGlobalApi = &client_inner.api;
53 let results = fetch_api_announcements_from_at_least_num_of_peers(
54 1,
55 api,
56 &guardian_pub_keys,
57 if is_running_in_test_env() {
58 Duration::from_millis(1)
59 } else {
60 Duration::from_secs(30)
61 },
62 )
63 .await;
64 store_api_announcements_updates_from_peers(client_inner.db(), &results).await;
65
66 let duration = if is_running_in_test_env() {
67 Duration::from_secs(1)
68 } else {
69 Duration::from_secs(3600)
71 };
72 sleep(duration).await;
73 }
74}
75
76pub(crate) async fn store_api_announcements_updates_from_peers(
77 db: &Database,
78 updates: &[BTreeMap<PeerId, SignedApiAnnouncement>],
79) {
80 for announcements in updates {
81 store_api_announcement_updates(db, announcements).await;
82 }
83}
84
85pub(crate) type PeersSignedApiAnnouncements = BTreeMap<PeerId, SignedApiAnnouncement>;
86
87pub(crate) async fn fetch_api_announcements_from_at_least_num_of_peers(
92 num_responses_required: usize,
93 api: &DynGlobalApi,
94 guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
95 extra_response_wait: Duration,
96) -> Vec<PeersSignedApiAnnouncements> {
97 let num_peers = guardian_pub_keys.to_num_peers();
98 let mut backoff = custom_backoff(Duration::from_millis(200), Duration::from_secs(600), None);
101
102 async fn make_request(
104 delay: Duration,
105 peer_id: PeerId,
106 api: &DynGlobalApi,
107 guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
108 ) -> (
109 PeerId,
110 Result<PeersSignedApiAnnouncements, FetchApiAnnouncementsError>,
111 ) {
112 runtime::sleep(delay).await;
113
114 let result = async {
115 let announcements = api
116 .api_announcements(peer_id)
117 .await
118 .map_err(|source| FetchApiAnnouncementsError::Request { peer_id, source })?;
119
120 for (peer_id, announcement) in &announcements {
123 let Some(guardian_pub_key) = guardian_pub_keys.get(peer_id) else {
124 return Err(FetchApiAnnouncementsError::UnknownGuardian { peer_id: *peer_id });
125 };
126
127 if !announcement.verify(SECP256K1, guardian_pub_key) {
128 return Err(FetchApiAnnouncementsError::InvalidSignature { peer_id: *peer_id });
129 }
130 }
131 Ok(announcements)
132 }
133 .await;
134
135 (peer_id, result)
136 }
137
138 let mut requests = FuturesUnordered::new();
139
140 for peer_id in num_peers.peer_ids() {
141 requests.push(make_request(
142 Duration::ZERO,
143 peer_id,
144 api,
145 guardian_pub_keys,
146 ));
147 }
148
149 let mut responses = Vec::new();
150
151 loop {
152 let next_response = if responses.len() < num_responses_required {
153 requests.next().await
155 } else {
156 fedimint_core::runtime::timeout(extra_response_wait, requests.next())
160 .await
161 .ok()
162 .flatten()
163 };
164
165 let Some((peer_id, response)) = next_response else {
166 break;
167 };
168
169 match response {
170 Err(err) => {
171 debug!(
172 target: LOG_CLIENT,
173 %peer_id,
174 err = %err.fmt_compact(),
175 "Failed to fetch API announcements from peer"
176 );
177 requests.push(make_request(
178 backoff.next().expect("Keeps retrying"),
179 peer_id,
180 api,
181 guardian_pub_keys,
182 ));
183 }
184 Ok(announcements) => {
185 responses.push(announcements);
186 }
187 }
188 }
189
190 responses
191}
192
193pub(crate) async fn store_api_announcement_updates(
194 db: &Database,
195 announcements: &BTreeMap<PeerId, SignedApiAnnouncement>,
196) {
197 db
198 .autocommit(
199 |dbtx, _|{
200 let announcements_inner = announcements.clone();
201 Box::pin(async move {
202 for (peer, new_announcement) in announcements_inner {
203 let replace_current_announcement = dbtx
204 .get_value(&ApiAnnouncementKey(peer))
205 .await.is_none_or(|current_announcement| {
206 current_announcement.api_announcement.nonce
207 < new_announcement.api_announcement.nonce
208 });
209 if replace_current_announcement {
210 debug!(target: LOG_CLIENT, ?peer, %new_announcement.api_announcement.api_url, "Updating API announcement");
211 dbtx.insert_entry(&ApiAnnouncementKey(peer), &new_announcement)
212 .await;
213 }
214 }
215
216 Result::<(), ()>::Ok(())
217 })},
218 None,
219 )
220 .await
221 .expect("Will never return an error");
222}
223
224pub async fn get_api_urls(
233 db: &Database,
234 cfg: &ClientConfig,
235 client_iroh_next_enabled: bool,
236) -> BTreeMap<PeerId, SafeUrl> {
237 let mut dbtx = db.begin_transaction_nc().await;
238
239 let guardian_metadata: BTreeMap<PeerId, SignedGuardianMetadata> = dbtx
241 .find_by_prefix(&GuardianMetadataPrefix)
242 .await
243 .map(|(key, metadata)| (key.0, metadata))
244 .collect()
245 .await;
246
247 let api_announcements: BTreeMap<PeerId, SignedApiAnnouncement> = dbtx
249 .find_by_prefix(&ApiAnnouncementPrefix)
250 .await
251 .map(|(key, announcement)| (key.0, announcement))
252 .collect()
253 .await;
254
255 cfg.global
260 .api_endpoints
261 .iter()
262 .filter_map(|(peer_id, peer_url)| {
263 let metadata = guardian_metadata.get(peer_id);
264
265 let mut url = metadata
266 .and_then(|m| m.guardian_metadata().api_urls.first().cloned())
267 .or_else(|| {
268 api_announcements
269 .get(peer_id)
270 .map(|a| a.api_announcement.api_url.clone())
271 })
272 .unwrap_or_else(|| peer_url.url.clone());
273
274 if url.scheme() == "iroh"
277 && client_iroh_next_enabled
278 && let Some(m) = metadata
279 {
280 let gm = m.guardian_metadata();
281 if let Some(endpoint) = &gm.iroh_next_endpoint {
282 match iroh_next_endpoint_url(endpoint) {
283 Ok(next_url) => {
284 debug!(
285 target: LOG_CLIENT,
286 %peer_id,
287 %next_url,
288 "Using iroh-next endpoint from guardian metadata",
289 );
290 url = next_url;
291 }
292 Err(err) => {
293 warn!(
294 target: LOG_CLIENT,
295 %peer_id,
296 err = %err.fmt_compact(),
297 "Ignoring peer with invalid advertised iroh-next endpoint",
298 );
299 return None;
300 }
301 }
302 }
303 }
304
305 Some((*peer_id, url))
306 })
307 .collect()
308}
309
310#[derive(Debug, thiserror::Error)]
312enum FetchApiAnnouncementsError {
313 #[error("Fetching API announcements from peer {peer_id} failed")]
315 Request {
316 peer_id: PeerId,
317 #[source]
318 source: ServerError,
319 },
320
321 #[error("Guardian public key not found for peer {peer_id}")]
323 UnknownGuardian { peer_id: PeerId },
324
325 #[error("Failed to verify announcement for peer {peer_id}")]
327 InvalidSignature { peer_id: PeerId },
328}