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