Skip to main content

fedimint_client/
api_announcements.rs

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
46/// Fetches API URL announcements from guardians, validates them and updates the
47/// DB if any new more upt to date ones are found.
48pub(crate) async fn run_api_announcement_refresh_task(client_inner: Arc<Client>) {
49    // Wait for the guardian keys to be available
50    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            // Check once an hour if there are new announcements
70            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
87/// Fetch responses from at least `num_responses_required` of peers.
88///
89/// Will wait a little bit extra in hopes of collecting more than strictly
90/// needed responses.
91pub(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    // Keep trying, initially somewhat aggressively, but after a while retry very
99    // slowly, because chances for response are getting lower and lower.
100    let mut backoff = custom_backoff(Duration::from_millis(200), Duration::from_secs(600), None);
101
102    // Make a single request to a peer after a delay
103    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            // If any of the announcements is invalid something is fishy with that
121            // guardian and we ignore all its responses
122            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            // If we don't have enough responses yet, we wait
154            requests.next().await
155        } else {
156            // if we do have responses we need, we wait opportunistically just for a small
157            // duration if any other responses are ready anyway, just to not
158            // throw them away
159            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
224/// Returns a list of all peers and their respective API URLs taking into
225/// account guardian metadata and API announcements overwriting the URLs
226/// contained in the original configuration.
227///
228/// Priority order:
229/// 1. Guardian metadata (if available) - uses first URL from api_urls
230/// 2. API announcement (if available)
231/// 3. Configured URL (fallback)
232pub 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    // Load guardian metadata for all peers
240    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    // Load API announcements for all peers
248    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    // For each peer: prefer guardian metadata, then API announcement, then config.
256    // When the client supports iroh-next and the guardian advertises an
257    // iroh-next endpoint, replace the original iroh:// identity with the
258    // advertised identity. The original identity is not retained as a fallback.
259    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 the resolved URL is iroh:// and iroh-next endpoint preference is
275            // enabled, swap in the advertised iroh-next endpoint.
276            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/// Why a peer's API announcements were not accepted.
311#[derive(Debug, thiserror::Error)]
312enum FetchApiAnnouncementsError {
313    /// The peer could not be asked.
314    #[error("Fetching API announcements from peer {peer_id} failed")]
315    Request {
316        peer_id: PeerId,
317        #[source]
318        source: ServerError,
319    },
320
321    /// The peer sent an announcement of a guardian the client does not know.
322    #[error("Guardian public key not found for peer {peer_id}")]
323    UnknownGuardian { peer_id: PeerId },
324
325    /// The peer sent an announcement whose signature does not verify.
326    #[error("Failed to verify announcement for peer {peer_id}")]
327    InvalidSignature { peer_id: PeerId },
328}