mithril_client/
client.rs

1use anyhow::{anyhow, Context};
2use reqwest::Url;
3use serde::{Deserialize, Serialize};
4use slog::{o, Logger};
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use mithril_common::api_version::APIVersionProvider;
9use mithril_common::MITHRIL_ORIGIN_TAG_HEADER;
10
11use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
12#[cfg(feature = "unstable")]
13use crate::cardano_database_client::CardanoDatabaseClient;
14use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
15use crate::cardano_transaction_client::CardanoTransactionClient;
16#[cfg(feature = "unstable")]
17use crate::certificate_client::CertificateVerifierCache;
18use crate::certificate_client::{
19    CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
20};
21use crate::feedback::{FeedbackReceiver, FeedbackSender};
22#[cfg(feature = "fs")]
23use crate::file_downloader::{
24    FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
25};
26use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
27use crate::snapshot_client::SnapshotClient;
28#[cfg(feature = "fs")]
29use crate::utils::AncillaryVerifier;
30use crate::MithrilResult;
31
32#[cfg(target_family = "wasm")]
33const fn one_week_in_seconds() -> u32 {
34    604800
35}
36
37/// Options that can be used to configure the client.
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct ClientOptions {
40    /// HTTP headers to include in the client requests.
41    pub http_headers: Option<HashMap<String, String>>,
42
43    /// Tag to retrieve the origin of the client requests.
44    #[cfg(target_family = "wasm")]
45    #[cfg_attr(target_family = "wasm", serde(default))]
46    pub origin_tag: Option<String>,
47
48    /// Whether to enable unstable features in the WASM client.
49    #[cfg(target_family = "wasm")]
50    #[cfg_attr(target_family = "wasm", serde(default))]
51    pub unstable: bool,
52
53    /// Whether to enable certificate chain verification caching in the WASM client.
54    ///
55    /// `unstable` must be set to `true` for this option to have any effect.
56    ///
57    /// DANGER: This feature is highly experimental and insecure, and it must not be used in production
58    #[cfg(target_family = "wasm")]
59    #[cfg_attr(target_family = "wasm", serde(default))]
60    pub enable_certificate_chain_verification_cache: bool,
61
62    /// Duration in seconds of certificate chain verification cache in the WASM client.
63    ///
64    /// Default to one week (604800 seconds).
65    ///
66    /// `enable_certificate_chain_verification_cache` and `unstable` must both be set to `true`
67    /// for this option to have any effect.
68    #[cfg(target_family = "wasm")]
69    #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
70    pub certificate_chain_verification_cache_duration_in_seconds: u32,
71}
72
73impl ClientOptions {
74    /// Instantiate a new [ClientOptions].
75    pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
76        Self {
77            http_headers,
78            #[cfg(target_family = "wasm")]
79            origin_tag: None,
80            #[cfg(target_family = "wasm")]
81            unstable: false,
82            #[cfg(target_family = "wasm")]
83            enable_certificate_chain_verification_cache: false,
84            #[cfg(target_family = "wasm")]
85            certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
86        }
87    }
88
89    /// Enable unstable features in the WASM client.
90    #[cfg(target_family = "wasm")]
91    pub fn with_unstable_features(self, unstable: bool) -> Self {
92        Self { unstable, ..self }
93    }
94}
95
96/// Structure that aggregates the available clients for each of the Mithril types of certified data.
97///
98/// Use the [ClientBuilder] to instantiate it easily.
99#[derive(Clone)]
100pub struct Client {
101    certificate_client: Arc<CertificateClient>,
102    mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
103    snapshot_client: Arc<SnapshotClient>,
104    #[cfg(feature = "unstable")]
105    cardano_database_client: Arc<CardanoDatabaseClient>,
106    cardano_transaction_client: Arc<CardanoTransactionClient>,
107    cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
108}
109
110impl Client {
111    /// Get the client that fetches and verifies Mithril certificates.
112    pub fn certificate(&self) -> Arc<CertificateClient> {
113        self.certificate_client.clone()
114    }
115
116    /// Get the client that fetches Mithril stake distributions.
117    pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
118        self.mithril_stake_distribution_client.clone()
119    }
120
121    #[deprecated(since = "0.11.9", note = "supersede by `cardano_database`")]
122    /// Get the client that fetches and downloads Mithril snapshots.
123    pub fn snapshot(&self) -> Arc<SnapshotClient> {
124        self.cardano_database()
125    }
126
127    /// Get the client that fetches and downloads Mithril snapshots.
128    pub fn cardano_database(&self) -> Arc<SnapshotClient> {
129        self.snapshot_client.clone()
130    }
131
132    /// Get the client that fetches and downloads Cardano database snapshots.
133    #[cfg(feature = "unstable")]
134    pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
135        self.cardano_database_client.clone()
136    }
137
138    /// Get the client that fetches and verifies Mithril Cardano transaction proof.
139    pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
140        self.cardano_transaction_client.clone()
141    }
142
143    /// Get the client that fetches Cardano stake distributions.
144    pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
145        self.cardano_stake_distribution_client.clone()
146    }
147}
148
149/// Builder than can be used to create a [Client] easily or with custom dependencies.
150pub struct ClientBuilder {
151    aggregator_endpoint: Option<String>,
152    genesis_verification_key: String,
153    origin_tag: Option<String>,
154    #[cfg(feature = "fs")]
155    ancillary_verification_key: Option<String>,
156    aggregator_client: Option<Arc<dyn AggregatorClient>>,
157    certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
158    #[cfg(feature = "fs")]
159    http_file_downloader: Option<Arc<dyn FileDownloader>>,
160    #[cfg(feature = "unstable")]
161    certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
162    logger: Option<Logger>,
163    feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
164    options: ClientOptions,
165}
166
167impl ClientBuilder {
168    /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given
169    /// endpoint and with the given genesis verification key.
170    pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder {
171        Self {
172            aggregator_endpoint: Some(endpoint.to_string()),
173            genesis_verification_key: genesis_verification_key.to_string(),
174            origin_tag: None,
175            #[cfg(feature = "fs")]
176            ancillary_verification_key: None,
177            aggregator_client: None,
178            certificate_verifier: None,
179            #[cfg(feature = "fs")]
180            http_file_downloader: None,
181            #[cfg(feature = "unstable")]
182            certificate_verifier_cache: None,
183            logger: None,
184            feedback_receivers: vec![],
185            options: ClientOptions::default(),
186        }
187    }
188
189    /// Constructs a new `ClientBuilder` without any dependency set.
190    ///
191    /// Use [ClientBuilder::aggregator] if you don't need to set a custom [AggregatorClient]
192    /// to request data from the aggregator.
193    pub fn new(genesis_verification_key: &str) -> ClientBuilder {
194        Self {
195            aggregator_endpoint: None,
196            genesis_verification_key: genesis_verification_key.to_string(),
197            origin_tag: None,
198            #[cfg(feature = "fs")]
199            ancillary_verification_key: None,
200            aggregator_client: None,
201            certificate_verifier: None,
202            #[cfg(feature = "fs")]
203            http_file_downloader: None,
204            #[cfg(feature = "unstable")]
205            certificate_verifier_cache: None,
206            logger: None,
207            feedback_receivers: vec![],
208            options: ClientOptions::default(),
209        }
210    }
211
212    /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`.
213    ///
214    /// The builder will try to create the missing dependencies using default implementations
215    /// if possible.
216    pub fn build(self) -> MithrilResult<Client> {
217        let logger = self
218            .logger
219            .clone()
220            .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
221
222        let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
223
224        let aggregator_client = match self.aggregator_client {
225            None => Arc::new(self.build_aggregator_client(logger.clone())?),
226            Some(client) => client,
227        };
228
229        let certificate_verifier = match self.certificate_verifier {
230            None => Arc::new(
231                MithrilCertificateVerifier::new(
232                    aggregator_client.clone(),
233                    &self.genesis_verification_key,
234                    feedback_sender.clone(),
235                    #[cfg(feature = "unstable")]
236                    self.certificate_verifier_cache,
237                    logger.clone(),
238                )
239                .with_context(|| "Building certificate verifier failed")?,
240            ),
241            Some(verifier) => verifier,
242        };
243        let certificate_client = Arc::new(CertificateClient::new(
244            aggregator_client.clone(),
245            certificate_verifier,
246            logger.clone(),
247        ));
248
249        let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
250            aggregator_client.clone(),
251        ));
252
253        #[cfg(feature = "fs")]
254        let http_file_downloader = match self.http_file_downloader {
255            None => Arc::new(RetryDownloader::new(
256                Arc::new(
257                    HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
258                        .with_context(|| "Building http file downloader failed")?,
259                ),
260                FileDownloadRetryPolicy::default(),
261            )),
262            Some(http_file_downloader) => http_file_downloader,
263        };
264
265        #[cfg(feature = "fs")]
266        let ancillary_verifier = match self.ancillary_verification_key {
267            None => None,
268            Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
269                verification_key
270                    .try_into()
271                    .with_context(|| "Building ancillary verifier failed")?,
272            ))),
273        };
274
275        let snapshot_client = Arc::new(SnapshotClient::new(
276            aggregator_client.clone(),
277            #[cfg(feature = "fs")]
278            http_file_downloader.clone(),
279            #[cfg(feature = "fs")]
280            ancillary_verifier.clone(),
281            #[cfg(feature = "fs")]
282            feedback_sender.clone(),
283            #[cfg(feature = "fs")]
284            logger.clone(),
285        ));
286
287        #[cfg(feature = "unstable")]
288        let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
289            aggregator_client.clone(),
290            #[cfg(feature = "fs")]
291            http_file_downloader,
292            #[cfg(feature = "fs")]
293            ancillary_verifier,
294            #[cfg(feature = "fs")]
295            feedback_sender,
296            #[cfg(feature = "fs")]
297            logger,
298        ));
299
300        let cardano_transaction_client =
301            Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
302
303        let cardano_stake_distribution_client =
304            Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
305
306        Ok(Client {
307            certificate_client,
308            mithril_stake_distribution_client,
309            snapshot_client,
310            #[cfg(feature = "unstable")]
311            cardano_database_client,
312            cardano_transaction_client,
313            cardano_stake_distribution_client,
314        })
315    }
316
317    fn build_aggregator_client(
318        &self,
319        logger: Logger,
320    ) -> Result<AggregatorHTTPClient, anyhow::Error> {
321        let endpoint = self
322            .aggregator_endpoint.as_ref()
323            .ok_or(anyhow!("No aggregator endpoint set: \
324                    You must either provide an aggregator endpoint or your own AggregatorClient implementation"))?;
325        let endpoint_url = Url::parse(endpoint).with_context(|| {
326            format!("Invalid aggregator endpoint, it must be a correctly formed url: '{endpoint}'")
327        })?;
328
329        let headers = self.compute_http_headers();
330
331        AggregatorHTTPClient::new(
332            endpoint_url,
333            APIVersionProvider::compute_all_versions_sorted()
334                .with_context(|| "Could not compute aggregator api versions")?,
335            logger,
336            Some(headers),
337        )
338        .with_context(|| "Building aggregator client failed")
339    }
340
341    fn compute_http_headers(&self) -> HashMap<String, String> {
342        let mut headers = self.options.http_headers.clone().unwrap_or_default();
343        if let Some(origin_tag) = self.origin_tag.clone() {
344            headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
345        }
346
347        headers
348    }
349
350    /// Set the [AggregatorClient] that will be used to request data to the aggregator.
351    pub fn with_aggregator_client(
352        mut self,
353        aggregator_client: Arc<dyn AggregatorClient>,
354    ) -> ClientBuilder {
355        self.aggregator_client = Some(aggregator_client);
356        self
357    }
358
359    /// Set the [CertificateVerifier] that will be used to validate certificates.
360    pub fn with_certificate_verifier(
361        mut self,
362        certificate_verifier: Arc<dyn CertificateVerifier>,
363    ) -> ClientBuilder {
364        self.certificate_verifier = Some(certificate_verifier);
365        self
366    }
367
368    cfg_unstable! {
369        /// Set the [CertificateVerifierCache] that will be used to cache certificate validation results.
370        ///
371        /// Passing a `None` value will disable the cache if any was previously set.
372        pub fn with_certificate_verifier_cache(
373            mut self,
374            certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
375        ) -> ClientBuilder {
376            self.certificate_verifier_cache = certificate_verifier_cache;
377            self
378        }
379    }
380
381    cfg_fs! {
382        /// Set the [FileDownloader] that will be used to download artifacts with HTTP.
383        pub fn with_http_file_downloader(
384            mut self,
385            http_file_downloader: Arc<dyn FileDownloader>,
386        ) -> ClientBuilder {
387            self.http_file_downloader = Some(http_file_downloader);
388            self
389        }
390
391        /// Set the ancillary verification key to use when verifying the downloaded ancillary files.
392        pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
393            mut self,
394            ancillary_verification_key: T,
395        ) -> ClientBuilder {
396            self.ancillary_verification_key = ancillary_verification_key.into();
397            self
398        }
399    }
400
401    /// Set the [Logger] to use.
402    pub fn with_logger(mut self, logger: Logger) -> Self {
403        self.logger = Some(logger);
404        self
405    }
406
407    /// Set the origin tag.
408    pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
409        self.origin_tag = origin_tag;
410        self
411    }
412
413    /// Add a [feedback receiver][FeedbackReceiver] to receive [events][crate::feedback::MithrilEvent]
414    /// for tasks that can have a long duration (ie: snapshot download or a long certificate chain
415    /// validation).
416    pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
417        self.feedback_receivers.push(receiver);
418        self
419    }
420
421    /// Sets the options to be used by the client.
422    pub fn with_options(mut self, options: ClientOptions) -> Self {
423        self.options = options;
424        self
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[tokio::test]
433    async fn compute_http_headers_returns_options_http_headers() {
434        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
435        let client_builder = ClientBuilder::new("").with_options(ClientOptions {
436            http_headers: Some(http_headers.clone()),
437        });
438
439        let computed_headers = client_builder.compute_http_headers();
440
441        assert_eq!(computed_headers, http_headers);
442    }
443
444    #[tokio::test]
445    async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
446        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
447        let client_builder = ClientBuilder::new("")
448            .with_options(ClientOptions {
449                http_headers: Some(http_headers.clone()),
450            })
451            .with_origin_tag(Some("CLIENT_TAG".to_string()));
452
453        let computed_headers = client_builder.compute_http_headers();
454
455        assert_eq!(
456            computed_headers,
457            HashMap::from([
458                ("Key".to_string(), "Value".to_string()),
459                (
460                    MITHRIL_ORIGIN_TAG_HEADER.to_string(),
461                    "CLIENT_TAG".to_string()
462                )
463            ])
464        );
465    }
466
467    #[tokio::test]
468    async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
469        let builder = ClientBuilder::new("")
470            .with_options(ClientOptions { http_headers: None })
471            .with_origin_tag(Some("TEST".to_string()));
472        assert_eq!(None, builder.options.http_headers);
473        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
474
475        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
476        let builder = ClientBuilder::new("")
477            .with_options(ClientOptions {
478                http_headers: Some(http_headers.clone()),
479            })
480            .with_origin_tag(Some("TEST".to_string()));
481        assert_eq!(Some(http_headers), builder.options.http_headers);
482        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
483    }
484
485    #[tokio::test]
486    async fn test_with_origin_tag_can_be_unset() {
487        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
488        let client_options = ClientOptions {
489            http_headers: Some(http_headers.clone()),
490        };
491        let builder = ClientBuilder::new("")
492            .with_options(client_options)
493            .with_origin_tag(None);
494
495        assert_eq!(Some(http_headers), builder.options.http_headers);
496        assert_eq!(None, builder.origin_tag);
497    }
498}