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