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::{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 if let Err(err) = {
54 let api: &DynGlobalApi = &client_inner.api;
55 let results = fetch_api_announcements_from_at_least_num_of_peers(
56 1,
57 api,
58 &guardian_pub_keys,
59 if is_running_in_test_env() {
60 Duration::from_millis(1)
61 } else {
62 Duration::from_secs(30)
63 },
64 )
65 .await;
66 store_api_announcements_updates_from_peers(client_inner.db(), &results).await
67 } {
68 debug!(target: LOG_CLIENT, err = %err.fmt_compact_anyhow(), "Refreshing api announcements failed");
69 }
70
71 let duration = if is_running_in_test_env() {
72 Duration::from_secs(1)
73 } else {
74 Duration::from_secs(3600)
76 };
77 sleep(duration).await;
78 }
79}
80
81pub(crate) async fn store_api_announcements_updates_from_peers(
82 db: &Database,
83 updates: &[BTreeMap<PeerId, SignedApiAnnouncement>],
84) -> Result<(), anyhow::Error> {
85 for announcements in updates {
86 store_api_announcement_updates(db, announcements).await;
87 }
88
89 Ok(())
90}
91
92pub(crate) type PeersSignedApiAnnouncements = BTreeMap<PeerId, SignedApiAnnouncement>;
93
94pub(crate) async fn fetch_api_announcements_from_at_least_num_of_peers(
99 num_responses_required: usize,
100 api: &DynGlobalApi,
101 guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
102 extra_response_wait: Duration,
103) -> Vec<PeersSignedApiAnnouncements> {
104 let num_peers = guardian_pub_keys.to_num_peers();
105 let mut backoff = custom_backoff(Duration::from_millis(200), Duration::from_secs(600), None);
108
109 async fn make_request(
111 delay: Duration,
112 peer_id: PeerId,
113 api: &DynGlobalApi,
114 guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
115 ) -> (PeerId, anyhow::Result<PeersSignedApiAnnouncements>) {
116 runtime::sleep(delay).await;
117
118 let result = async {
119 let announcements = api.api_announcements(peer_id).await.with_context(move || {
120 format!("Fetching API announcements from peer {peer_id} failed")
121 })?;
122
123 for (peer_id, announcement) in &announcements {
126 let Some(guardian_pub_key) = guardian_pub_keys.get(peer_id) else {
127 bail!("Guardian public key not found for peer {}", peer_id);
128 };
129
130 if !announcement.verify(SECP256K1, guardian_pub_key) {
131 bail!("Failed to verify announcement for peer {}", peer_id);
132 }
133 }
134 Ok(announcements)
135 }
136 .await;
137
138 (peer_id, result)
139 }
140
141 let mut requests = FuturesUnordered::new();
142
143 for peer_id in num_peers.peer_ids() {
144 requests.push(make_request(
145 Duration::ZERO,
146 peer_id,
147 api,
148 guardian_pub_keys,
149 ));
150 }
151
152 let mut responses = Vec::new();
153
154 loop {
155 let next_response = if responses.len() < num_responses_required {
156 requests.next().await
158 } else {
159 fedimint_core::runtime::timeout(extra_response_wait, requests.next())
163 .await
164 .ok()
165 .flatten()
166 };
167
168 let Some((peer_id, response)) = next_response else {
169 break;
170 };
171
172 match response {
173 Err(err) => {
174 debug!(
175 target: LOG_CLIENT,
176 %peer_id,
177 err = %err.fmt_compact_anyhow(),
178 "Failed to fetch API announcements from peer"
179 );
180 requests.push(make_request(
181 backoff.next().expect("Keeps retrying"),
182 peer_id,
183 api,
184 guardian_pub_keys,
185 ));
186 }
187 Ok(announcements) => {
188 responses.push(announcements);
189 }
190 }
191 }
192
193 responses
194}
195
196pub(crate) async fn store_api_announcement_updates(
197 db: &Database,
198 announcements: &BTreeMap<PeerId, SignedApiAnnouncement>,
199) {
200 db
201 .autocommit(
202 |dbtx, _|{
203 let announcements_inner = announcements.clone();
204 Box::pin(async move {
205 for (peer, new_announcement) in announcements_inner {
206 let replace_current_announcement = dbtx
207 .get_value(&ApiAnnouncementKey(peer))
208 .await.is_none_or(|current_announcement| {
209 current_announcement.api_announcement.nonce
210 < new_announcement.api_announcement.nonce
211 });
212 if replace_current_announcement {
213 debug!(target: LOG_CLIENT, ?peer, %new_announcement.api_announcement.api_url, "Updating API announcement");
214 dbtx.insert_entry(&ApiAnnouncementKey(peer), &new_announcement)
215 .await;
216 }
217 }
218
219 Result::<(), ()>::Ok(())
220 })},
221 None,
222 )
223 .await
224 .expect("Will never return an error");
225}
226
227pub async fn get_api_urls(
236 db: &Database,
237 cfg: &ClientConfig,
238 client_iroh_next_enabled: bool,
239) -> BTreeMap<PeerId, SafeUrl> {
240 let mut dbtx = db.begin_transaction_nc().await;
241
242 let guardian_metadata: BTreeMap<PeerId, SignedGuardianMetadata> = dbtx
244 .find_by_prefix(&GuardianMetadataPrefix)
245 .await
246 .map(|(key, metadata)| (key.0, metadata))
247 .collect()
248 .await;
249
250 let api_announcements: BTreeMap<PeerId, SignedApiAnnouncement> = dbtx
252 .find_by_prefix(&ApiAnnouncementPrefix)
253 .await
254 .map(|(key, announcement)| (key.0, announcement))
255 .collect()
256 .await;
257
258 cfg.global
263 .api_endpoints
264 .iter()
265 .filter_map(|(peer_id, peer_url)| {
266 let metadata = guardian_metadata.get(peer_id);
267
268 let mut url = metadata
269 .and_then(|m| m.guardian_metadata().api_urls.first().cloned())
270 .or_else(|| {
271 api_announcements
272 .get(peer_id)
273 .map(|a| a.api_announcement.api_url.clone())
274 })
275 .unwrap_or_else(|| peer_url.url.clone());
276
277 if url.scheme() == "iroh"
280 && client_iroh_next_enabled
281 && let Some(m) = metadata
282 {
283 let gm = m.guardian_metadata();
284 if let Some(endpoint) = &gm.iroh_next_endpoint {
285 match iroh_next_endpoint_url(endpoint) {
286 Ok(next_url) => {
287 debug!(
288 target: LOG_CLIENT,
289 %peer_id,
290 %next_url,
291 "Using iroh-next endpoint from guardian metadata",
292 );
293 url = next_url;
294 }
295 Err(err) => {
296 warn!(
297 target: LOG_CLIENT,
298 %peer_id,
299 err = %err.fmt_compact_anyhow(),
300 "Ignoring peer with invalid advertised iroh-next endpoint",
301 );
302 return None;
303 }
304 }
305 }
306 }
307
308 Some((*peer_id, url))
309 })
310 .collect()
311}