mithril_client/
client.rs

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