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}