Skip to main content

fedimint_client/
guardian_metadata.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use fedimint_api_client::api::{DynGlobalApi, ServerError};
6use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::envs::is_running_in_test_env;
9use fedimint_core::net::guardian_metadata::{SignedGuardianMetadata, VerificationError};
10use fedimint_core::runtime::{self, sleep};
11use fedimint_core::secp256k1::SECP256K1;
12use fedimint_core::util::FmtCompact as _;
13use fedimint_core::util::backoff_util::custom_backoff;
14use fedimint_core::{NumPeersExt as _, PeerId, impl_db_lookup, impl_db_record};
15use fedimint_logging::LOG_CLIENT;
16use futures::stream::{FuturesUnordered, StreamExt as _};
17use tracing::debug;
18
19use crate::Client;
20use crate::db::DbKeyPrefix;
21
22#[derive(Clone, Debug, Encodable, Decodable)]
23pub struct GuardianMetadataKey(pub PeerId);
24
25#[derive(Clone, Debug, Encodable, Decodable)]
26pub struct GuardianMetadataPrefix;
27
28impl_db_record!(
29    key = GuardianMetadataKey,
30    value = SignedGuardianMetadata,
31    db_prefix = DbKeyPrefix::GuardianMetadata,
32    notify_on_modify = false,
33);
34impl_db_lookup!(
35    key = GuardianMetadataKey,
36    query_prefix = GuardianMetadataPrefix
37);
38
39/// Fetches guardian metadata from guardians, validates them and updates the
40/// DB if any new more up to date ones are found.
41pub(crate) async fn run_guardian_metadata_refresh_task(client_inner: Arc<Client>) {
42    // Wait for the guardian keys to be available
43    let guardian_pub_keys = client_inner.get_guardian_public_keys_blocking().await;
44    loop {
45        let api: &DynGlobalApi = &client_inner.api;
46        let results = fetch_guardian_metadata_from_at_least_num_of_peers(
47            1,
48            api,
49            &guardian_pub_keys,
50            if is_running_in_test_env() {
51                Duration::from_millis(1)
52            } else {
53                Duration::from_secs(30)
54            },
55        )
56        .await;
57        store_guardian_metadata_updates_from_peers(client_inner.db(), &guardian_pub_keys, &results)
58            .await;
59
60        let duration = if is_running_in_test_env() {
61            Duration::from_secs(1)
62        } else {
63            // Check once an hour if there are new metadata
64            Duration::from_secs(3600)
65        };
66        sleep(duration).await;
67    }
68}
69
70pub(crate) async fn store_guardian_metadata_updates_from_peers(
71    db: &Database,
72    guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
73    updates: &[BTreeMap<PeerId, SignedGuardianMetadata>],
74) {
75    for metadata_map in updates {
76        store_guardian_metadata_updates(db, guardian_pub_keys, metadata_map).await;
77    }
78}
79
80pub(crate) type PeersSignedGuardianMetadata = BTreeMap<PeerId, SignedGuardianMetadata>;
81
82/// Fetch responses from at least `num_responses_required` of peers.
83///
84/// Will wait a little bit extra in hopes of collecting more than strictly
85/// needed responses.
86pub(crate) async fn fetch_guardian_metadata_from_at_least_num_of_peers(
87    num_responses_required: usize,
88    api: &DynGlobalApi,
89    guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
90    extra_response_wait: Duration,
91) -> Vec<PeersSignedGuardianMetadata> {
92    let num_peers = guardian_pub_keys.to_num_peers();
93    // Keep trying, initially somewhat aggressively, but after a while retry very
94    // slowly, because chances for response are getting lower and lower.
95    let mut backoff = custom_backoff(Duration::from_millis(200), Duration::from_secs(600), None);
96
97    // Make a single request to a peer after a delay
98    async fn make_request(
99        delay: Duration,
100        peer_id: PeerId,
101        api: &DynGlobalApi,
102        guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
103    ) -> (
104        PeerId,
105        Result<PeersSignedGuardianMetadata, FetchGuardianMetadataError>,
106    ) {
107        runtime::sleep(delay).await;
108
109        let result = async {
110            let metadata_map = api
111                .guardian_metadata(peer_id)
112                .await
113                .map_err(|source| FetchGuardianMetadataError::Request { peer_id, source })?;
114
115            // If any of the metadata is invalid something is fishy with that
116            // guardian and we ignore all its responses
117            for (peer_id, metadata) in &metadata_map {
118                let Some(guardian_pub_key) = guardian_pub_keys.get(peer_id) else {
119                    return Err(FetchGuardianMetadataError::UnknownGuardian { peer_id: *peer_id });
120                };
121
122                let now = fedimint_core::time::duration_since_epoch();
123                if let Err(source) = metadata.verify(SECP256K1, guardian_pub_key, now) {
124                    return Err(FetchGuardianMetadataError::InvalidMetadata {
125                        peer_id: *peer_id,
126                        source,
127                    });
128                }
129            }
130            Ok(metadata_map)
131        }
132        .await;
133
134        (peer_id, result)
135    }
136
137    let mut requests = FuturesUnordered::new();
138
139    for peer_id in num_peers.peer_ids() {
140        requests.push(make_request(
141            Duration::ZERO,
142            peer_id,
143            api,
144            guardian_pub_keys,
145        ));
146    }
147
148    let mut responses = Vec::new();
149
150    loop {
151        let next_response = if responses.len() < num_responses_required {
152            // If we don't have enough responses yet, we wait
153            requests.next().await
154        } else {
155            // if we do have responses we need, we wait opportunistically just for a small
156            // duration if any other responses are ready anyway, just to not
157            // throw them away
158            fedimint_core::runtime::timeout(extra_response_wait, requests.next())
159                .await
160                .ok()
161                .flatten()
162        };
163
164        let Some((peer_id, response)) = next_response else {
165            break;
166        };
167
168        match response {
169            Err(err) => {
170                debug!(
171                    target: LOG_CLIENT,
172                    %peer_id,
173                    err = %err.fmt_compact(),
174                    "Failed to fetch guardian metadata from peer"
175                );
176                requests.push(make_request(
177                    backoff.next().expect("Keeps retrying"),
178                    peer_id,
179                    api,
180                    guardian_pub_keys,
181                ));
182            }
183            Ok(metadata) => {
184                responses.push(metadata);
185            }
186        }
187    }
188
189    responses
190}
191
192pub(crate) async fn store_guardian_metadata_updates(
193    db: &Database,
194    guardian_pub_keys: &BTreeMap<PeerId, bitcoin::secp256k1::PublicKey>,
195    metadata_map: &BTreeMap<PeerId, SignedGuardianMetadata>,
196) {
197    let now = fedimint_core::time::duration_since_epoch();
198
199    db.autocommit(
200        |dbtx, _| {
201            let metadata_map_inner = metadata_map.clone();
202            let guardian_pub_keys_inner = guardian_pub_keys.clone();
203            Box::pin(async move {
204                for (peer, new_metadata) in metadata_map_inner {
205                    // Verify signature before storing
206                    let Some(guardian_pub_key) = guardian_pub_keys_inner.get(&peer) else {
207                        debug!(
208                            target: LOG_CLIENT,
209                            ?peer,
210                            "Skipping metadata update: guardian public key not found"
211                        );
212                        continue;
213                    };
214
215                    if let Err(e) = new_metadata.verify(SECP256K1, guardian_pub_key, now) {
216                        debug!(
217                            target: LOG_CLIENT,
218                            ?peer,
219                            err = %e.fmt_compact(),
220                            "Skipping metadata update: verification failed"
221                        );
222                        continue;
223                    }
224
225                    let replace_current_metadata = dbtx
226                        .get_value(&GuardianMetadataKey(peer))
227                        .await
228                        .is_none_or(|current_metadata| {
229                            // Replace if new metadata has a newer timestamp
230                            current_metadata.guardian_metadata().timestamp_secs
231                                < new_metadata.guardian_metadata().timestamp_secs
232                        });
233                    if replace_current_metadata {
234                        debug!(target: LOG_CLIENT, ?peer, "Updating guardian metadata");
235                        dbtx.insert_entry(&GuardianMetadataKey(peer), &new_metadata)
236                            .await;
237                    }
238                }
239
240                Result::<(), ()>::Ok(())
241            })
242        },
243        None,
244    )
245    .await
246    .expect("Will never return an error");
247}
248
249/// Why a peer's guardian metadata was not accepted.
250#[derive(Debug, thiserror::Error)]
251enum FetchGuardianMetadataError {
252    /// The peer could not be asked.
253    #[error("Fetching guardian metadata from peer {peer_id} failed")]
254    Request {
255        peer_id: PeerId,
256        #[source]
257        source: ServerError,
258    },
259
260    /// The peer sent metadata of a guardian the client does not know.
261    #[error("Guardian public key not found for peer {peer_id}")]
262    UnknownGuardian { peer_id: PeerId },
263
264    /// The peer sent metadata that does not verify.
265    #[error("Failed to verify metadata for peer {peer_id}")]
266    InvalidMetadata {
267        peer_id: PeerId,
268        #[source]
269        source: VerificationError,
270    },
271}