mithril_client/
client.rs

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