Skip to main content

mithril_client/
cardano_block_client.rs

1//! A client to retrieve from an aggregator cryptographic proofs of membership for a subset of Cardano blocks.
2//!
3//! In order to do so it defines a [CardanoBlockClient] which exposes the following features:
4//!  - [get_proof][CardanoBlockClient::get_proof]: get a [cryptographic proof][CardanoBlocksProofs]
5//!    that the blocks with given hash are included in the global Cardano blocks set.
6//!  - [get][CardanoBlockClient::get_snapshot]: get a [Cardano block snapshot][CardanoBlocksTransactionsSnapshot]
7//!    data from its hash.
8//!  - [list][CardanoBlockClient::list_snapshots]: get the list of the latest available Cardano block
9//!    snapshot.
10//!
11//!  **Important:** Verifying a proof **only** means that its cryptography is valid, in order to certify that a Cardano
12//! blocks subset is valid, the associated proof must be tied to a valid Mithril certificate (see the example below).
13//!
14//! # Get and verify Cardano block proof
15//!
16//! To get and verify a Cardano block proof using the [ClientBuilder][crate::client::ClientBuilder].
17//!
18//! ```no_run
19//! #[cfg(feature = "unstable")]
20//! # async fn run() -> mithril_client::MithrilResult<()> {
21//! use mithril_client::{ClientBuilder, MessageBuilder};
22//!
23//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
24//!
25//! // 1 - Get a proof from the aggregator and verify it
26//! let cardano_block_proof = client.cardano_block().get_proof(&["block-1", "block-2"]).await?;
27//! println!("Mithril could not certify the following blocks : {:?}", &cardano_block_proof.non_certified_blocks);
28//!
29//! let verified_blocks = cardano_block_proof.verify()?;
30//!
31//! // 2 - Verify its associated certificate chain
32//! let certificate = client.certificate().verify_chain(&cardano_block_proof.certificate_hash).await?;
33//!
34//! // 3 - Ensure that the proof is indeed signed in the associated certificate
35//! let message = MessageBuilder::new().compute_cardano_blocks_proofs_message(&certificate, &verified_blocks);
36//! if certificate.match_message(&message) {
37//!     // All green, Mithril certifies that those blocks are part of the Cardano blocks set.
38//!     println!("Certified blocks : {:?}", verified_blocks.certified_blocks());
39//! }
40//! #    Ok(())
41//! # }
42//! ```
43//!
44//! # Get a Cardano block snapshot
45//!
46//! To get a Cardano block snapshot using the [ClientBuilder][crate::client::ClientBuilder].
47//!
48//! ```no_run
49//!  #[cfg(feature = "unstable")]
50//! # async fn run() -> mithril_client::MithrilResult<()> {
51//! use mithril_client::ClientBuilder;
52//!
53//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
54//! let cardano_block_snapshot = client.cardano_block().get_snapshot("CARDANO_BLOCK_SNAPSHOT_HASH").await?.unwrap();
55//!
56//! println!("Cardano block snapshot hash={}, epoch={}, block_number_signed={}", cardano_block_snapshot.hash, cardano_block_snapshot.epoch, cardano_block_snapshot.block_number_signed);
57//! #    Ok(())
58//! # }
59//! ```
60//!
61//! # List available Cardano block snapshots
62//!
63//! To list latest available Cardano block snapshots using the [ClientBuilder][crate::client::ClientBuilder].
64//!
65//! ```no_run
66//!  #[cfg(feature = "unstable")]
67//! # async fn run() -> mithril_client::MithrilResult<()> {
68//! use mithril_client::ClientBuilder;
69//!
70//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
71//! let cardano_block_snapshots = client.cardano_block().list_snapshots().await?;
72//!
73//! for cardano_block_snapshot in cardano_block_snapshots {
74//!     println!("Cardano block snapshot hash={}, epoch={}, block_number_signed={}", cardano_block_snapshot.hash, cardano_block_snapshot.epoch, cardano_block_snapshot.block_number_signed);
75//! }
76//! #    Ok(())
77//! # }
78//! ```
79
80use anyhow::Context;
81use std::sync::Arc;
82
83use crate::{
84    CardanoBlocksProofs, CardanoBlocksTransactionsSnapshot,
85    CardanoBlocksTransactionsSnapshotListItem, MithrilResult,
86};
87
88/// HTTP client for CardanoBlocksAPI from the aggregator
89pub struct CardanoBlockClient {
90    aggregator_requester: Arc<dyn CardanoBlockAggregatorRequest>,
91}
92
93/// Define the requests against an aggregator related to Cardano blocks.
94#[cfg_attr(test, mockall::automock)]
95#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
96#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
97pub trait CardanoBlockAggregatorRequest: Send + Sync {
98    /// Get a proof of membership for the given blocks hashes from the aggregator.
99    async fn get_proof(&self, hashes: &[String]) -> MithrilResult<Option<CardanoBlocksProofs>>;
100
101    /// Fetch the list of latest signed Cardano blocks snapshots from the aggregator
102    async fn list_latest_snapshots(
103        &self,
104    ) -> MithrilResult<Vec<CardanoBlocksTransactionsSnapshotListItem>>;
105
106    /// Fetch a Cardano blocks snapshot by its hash from the aggregator.
107    async fn get_snapshot(
108        &self,
109        hash: &str,
110    ) -> MithrilResult<Option<CardanoBlocksTransactionsSnapshot>>;
111}
112
113impl CardanoBlockClient {
114    /// Constructs a new `CardanoBlockClient`.
115    pub fn new(aggregator_requester: Arc<dyn CardanoBlockAggregatorRequest>) -> Self {
116        Self {
117            aggregator_requester,
118        }
119    }
120
121    /// Get proofs that the given subset of blocks is included in the Cardano blocks set.
122    pub async fn get_proof<T: ToString>(
123        &self,
124        blocks_hashes: &[T],
125    ) -> MithrilResult<CardanoBlocksProofs> {
126        let blocks_hashes: Vec<String> = blocks_hashes.iter().map(|h| h.to_string()).collect();
127
128        self.aggregator_requester
129            .get_proof(&blocks_hashes)
130            .await?
131            .with_context(|| format!("No proof found for blocks hashes: {blocks_hashes:?}",))
132    }
133
134    /// Fetch a list of signed Cardano transaction snapshots.
135    pub async fn list_snapshots(
136        &self,
137    ) -> MithrilResult<Vec<CardanoBlocksTransactionsSnapshotListItem>> {
138        self.aggregator_requester.list_latest_snapshots().await
139    }
140
141    /// Get the given Cardano Blocks transaction snapshot data. If it cannot be found, a None is returned.
142    pub async fn get_snapshot(
143        &self,
144        hash: &str,
145    ) -> MithrilResult<Option<CardanoBlocksTransactionsSnapshot>> {
146        self.aggregator_requester.get_snapshot(hash).await
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use mockall::predicate::eq;
153
154    use mithril_common::test::mock_extensions::MockBuilder;
155
156    use crate::common::test::Dummy;
157    use crate::{CardanoBlock, MkSetProof};
158
159    use super::*;
160
161    #[tokio::test]
162    async fn get_cardano_blocks_snapshot_list() {
163        let aggregator_requester =
164            MockBuilder::<MockCardanoBlockAggregatorRequest>::configure(|mock| {
165                let messages = vec![
166                    CardanoBlocksTransactionsSnapshotListItem {
167                        hash: "hash-123".to_string(),
168                        ..Dummy::dummy()
169                    },
170                    CardanoBlocksTransactionsSnapshotListItem {
171                        hash: "hash-456".to_string(),
172                        ..Dummy::dummy()
173                    },
174                ];
175                mock.expect_list_latest_snapshots().return_once(|| Ok(messages));
176            });
177        let client = CardanoBlockClient::new(aggregator_requester);
178
179        let items = client.list_snapshots().await.unwrap();
180
181        assert_eq!(2, items.len());
182        assert_eq!("hash-123".to_string(), items[0].hash);
183        assert_eq!("hash-456".to_string(), items[1].hash);
184    }
185
186    #[tokio::test]
187    async fn get_cardano_blocks_snapshot() {
188        let aggregator_requester =
189            MockBuilder::<MockCardanoBlockAggregatorRequest>::configure(|mock| {
190                let message = CardanoBlocksTransactionsSnapshot {
191                    hash: "hash-123".to_string(),
192                    merkle_root: "mk-123".to_string(),
193                    ..Dummy::dummy()
194                };
195                mock.expect_get_snapshot()
196                    .with(eq(message.hash.clone()))
197                    .return_once(|_| Ok(Some(message)));
198            });
199        let client = CardanoBlockClient::new(aggregator_requester);
200
201        let cardano_block_snapshot = client
202            .get_snapshot("hash-123")
203            .await
204            .unwrap()
205            .expect("This test returns a cardano block snapshot");
206        assert_eq!("hash-123", &cardano_block_snapshot.hash);
207        assert_eq!("mk-123", &cardano_block_snapshot.merkle_root);
208    }
209
210    #[tokio::test]
211    async fn test_get_proof_ok() {
212        let set_proof = MkSetProof::<CardanoBlock>::dummy();
213        let expected_blocks_proofs = CardanoBlocksProofs {
214            certified_blocks: Some(set_proof.clone()),
215            ..Dummy::dummy()
216        };
217
218        let aggregator_requester =
219            MockBuilder::<MockCardanoBlockAggregatorRequest>::configure(|mock| {
220                let message = expected_blocks_proofs.clone();
221                mock.expect_get_proof()
222                    .with(eq(message
223                        .certified_blocks
224                        .iter()
225                        .flat_map(|proof| proof.items.clone())
226                        .map(|block| block.block_hash)
227                        .collect::<Vec<_>>()))
228                    .return_once(|_| Ok(Some(message)));
229            });
230        let client = CardanoBlockClient::new(aggregator_requester);
231
232        let blocks_proofs = client
233            .get_proof(
234                &set_proof
235                    .items
236                    .iter()
237                    .map(|h| h.block_hash.clone())
238                    .collect::<Vec<_>>(),
239            )
240            .await
241            .unwrap();
242
243        assert_eq!(expected_blocks_proofs, blocks_proofs);
244    }
245
246    #[tokio::test]
247    async fn test_get_proof_ko() {
248        let aggregator_requester =
249            MockBuilder::<MockCardanoBlockAggregatorRequest>::configure(|mock| {
250                mock.expect_get_proof()
251                    .return_once(move |_| Err(anyhow::anyhow!("an error")));
252            });
253        let client = CardanoBlockClient::new(aggregator_requester);
254
255        client
256            .get_proof(&["tx-123"])
257            .await
258            .expect_err("The certificate client should fail here.");
259    }
260}