mithril_client/
cardano_transaction_client.rs

1//! A client to retrieve from an aggregator cryptographic proofs of membership for a subset of Cardano transactions.
2//!
3//! In order to do so it defines a [CardanoTransactionClient] which exposes the following features:
4//!  - [get_proofs][CardanoTransactionClient::get_proofs]: get a [cryptographic proof][CardanoTransactionsProofs]
5//!    that the transactions with given hash are included in the global Cardano transactions set.
6//!  - [get][CardanoTransactionClient::get_snapshot]: get a [Cardano transaction snapshot][CardanoTransactionSnapshot]
7//!    data from its hash.
8//!  - [list][CardanoTransactionClient::list_snapshots]: get the list of the latest available Cardano transaction
9//!    snapshot.
10//!
11//!  **Important:** Verifying a proof **only** means that its cryptography is valid, in order to certify that a Cardano
12//! transactions subset is valid, the associated proof must be tied to a valid Mithril certificate (see the example below).
13//!
14//! # Get and verify Cardano transaction proof
15//!
16//! To get and verify a Cardano transaction proof using the [ClientBuilder][crate::client::ClientBuilder].
17//!
18//! ```no_run
19//! # async fn run() -> mithril_client::MithrilResult<()> {
20//! use mithril_client::{ClientBuilder, MessageBuilder};
21//!
22//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
23//!
24//! // 1 - Get a proof from the aggregator and verify it
25//! let cardano_transaction_proof = client.cardano_transaction().get_proofs(&["tx-1", "tx-2"]).await?;
26//! println!("Mithril could not certify the following transactions : {:?}", &cardano_transaction_proof.non_certified_transactions);
27//!
28//! let verified_transactions = cardano_transaction_proof.verify()?;
29//!
30//! // 2 - Verify its associated certificate chain
31//! let certificate = client.certificate().verify_chain(&cardano_transaction_proof.certificate_hash).await?;
32//!
33//! // 3 - Ensure that the proof is indeed signed in the associated certificate
34//! let message = MessageBuilder::new().compute_cardano_transactions_proofs_message(&certificate, &verified_transactions);
35//! if certificate.match_message(&message) {
36//!     // All green, Mithril certifies that those transactions are part of the Cardano transactions set.
37//!     println!("Certified transactions : {:?}", verified_transactions.certified_transactions());
38//! }
39//! #    Ok(())
40//! # }
41//! ```
42//!
43//! # Get a Cardano transaction snapshot
44//!
45//! To get a Cardano transaction snapshot using the [ClientBuilder][crate::client::ClientBuilder].
46//!
47//! ```no_run
48//! # async fn run() -> mithril_client::MithrilResult<()> {
49//! use mithril_client::ClientBuilder;
50//!
51//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
52//! let cardano_transaction_snapshot = client.cardano_transaction().get_snapshot("CARDANO_TRANSACTION_SNAPSHOT_HASH").await?.unwrap();
53//!
54//! println!("Cardano transaction snapshot hash={}, epoch={}", cardano_transaction_snapshot.hash, cardano_transaction_snapshot.epoch);
55//! #    Ok(())
56//! # }
57//! ```
58//!
59//! # List available Cardano transaction snapshots
60//!
61//! To list latest available Cardano transaction snapshots using the [ClientBuilder][crate::client::ClientBuilder].
62//!
63//! ```no_run
64//! # async fn run() -> mithril_client::MithrilResult<()> {
65//! use mithril_client::ClientBuilder;
66//!
67//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
68//! let cardano_transaction_snapshots = client.cardano_transaction().list_snapshots().await?;
69//!
70//! for cardano_transaction_snapshot in cardano_transaction_snapshots {
71//!     println!("Cardano transaction snapshot hash={}, epoch={}", cardano_transaction_snapshot.hash, cardano_transaction_snapshot.epoch);
72//! }
73//! #    Ok(())
74//! # }
75//! ```
76
77use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
78use crate::{
79    CardanoTransactionSnapshot, CardanoTransactionSnapshotListItem, CardanoTransactionsProofs,
80    MithrilResult,
81};
82use anyhow::Context;
83use std::sync::Arc;
84
85/// HTTP client for CardanoTransactionsAPI from the Aggregator
86pub struct CardanoTransactionClient {
87    aggregator_client: Arc<dyn AggregatorClient>,
88}
89
90impl CardanoTransactionClient {
91    /// Constructs a new `CardanoTransactionClient`.
92    pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
93        Self { aggregator_client }
94    }
95
96    /// Get proofs that the given subset of transactions is included in the Cardano transactions set.
97    pub async fn get_proofs<T: ToString>(
98        &self,
99        transactions_hashes: &[T],
100    ) -> MithrilResult<CardanoTransactionsProofs> {
101        match self
102            .aggregator_client
103            .get_content(AggregatorRequest::GetTransactionsProofs {
104                transactions_hashes: transactions_hashes.iter().map(|h| h.to_string()).collect(),
105            })
106            .await
107        {
108            Ok(content) => {
109                let transactions_proofs: CardanoTransactionsProofs = serde_json::from_str(&content)
110                    .with_context(
111                        || "CardanoTransactionProof Client can not deserialize transactions proofs",
112                    )?;
113
114                Ok(transactions_proofs)
115            }
116            Err(e) => Err(e.into()),
117        }
118    }
119
120    /// Fetch a list of signed Cardano transaction snapshots.
121    pub async fn list_snapshots(&self) -> MithrilResult<Vec<CardanoTransactionSnapshotListItem>> {
122        let response = self
123            .aggregator_client
124            .get_content(AggregatorRequest::ListCardanoTransactionSnapshots)
125            .await
126            .with_context(|| "CardanoTransactionClient Client can not get the artifact list")?;
127        let items = serde_json::from_str::<Vec<CardanoTransactionSnapshotListItem>>(&response)
128            .with_context(|| "CardanoTransactionClient Client can not deserialize artifact list")?;
129
130        Ok(items)
131    }
132
133    /// Get the given Cardano transaction snapshot data. If it cannot be found, a None is returned.
134    pub async fn get_snapshot(
135        &self,
136        hash: &str,
137    ) -> MithrilResult<Option<CardanoTransactionSnapshot>> {
138        match self
139            .aggregator_client
140            .get_content(AggregatorRequest::GetCardanoTransactionSnapshot {
141                hash: hash.to_string(),
142            })
143            .await
144        {
145            Ok(content) => {
146                let cardano_transaction_snapshot: CardanoTransactionSnapshot =
147                    serde_json::from_str(&content).with_context(
148                        || "CardanoTransactionClient Client can not deserialize artifact",
149                    )?;
150
151                Ok(Some(cardano_transaction_snapshot))
152            }
153            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
154            Err(e) => Err(e.into()),
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use std::sync::Arc;
162
163    use anyhow::anyhow;
164    use chrono::{DateTime, Utc};
165    use mockall::predicate::eq;
166
167    use mithril_common::test::double::Dummy;
168
169    use crate::aggregator_client::{AggregatorClientError, MockAggregatorClient};
170    use crate::common::{BlockNumber, Epoch};
171    use crate::{
172        CardanoTransactionSnapshot, CardanoTransactionSnapshotListItem, CardanoTransactionsProofs,
173        CardanoTransactionsSetProof,
174    };
175
176    use super::*;
177
178    fn fake_messages() -> Vec<CardanoTransactionSnapshotListItem> {
179        vec![
180            CardanoTransactionSnapshotListItem {
181                merkle_root: "mk-123".to_string(),
182                epoch: Epoch(1),
183                block_number: BlockNumber(24),
184                hash: "hash-123".to_string(),
185                certificate_hash: "cert-hash-123".to_string(),
186                created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
187                    .unwrap()
188                    .with_timezone(&Utc),
189            },
190            CardanoTransactionSnapshotListItem {
191                merkle_root: "mk-456".to_string(),
192                epoch: Epoch(1),
193                block_number: BlockNumber(24),
194                hash: "hash-456".to_string(),
195                certificate_hash: "cert-hash-456".to_string(),
196                created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
197                    .unwrap()
198                    .with_timezone(&Utc),
199            },
200        ]
201    }
202
203    #[tokio::test]
204    async fn get_cardano_transactions_snapshot_list() {
205        let message = fake_messages();
206        let mut http_client = MockAggregatorClient::new();
207        http_client
208            .expect_get_content()
209            .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
210        let client = CardanoTransactionClient::new(Arc::new(http_client));
211        let items = client.list_snapshots().await.unwrap();
212
213        assert_eq!(2, items.len());
214        assert_eq!("hash-123".to_string(), items[0].hash);
215        assert_eq!("hash-456".to_string(), items[1].hash);
216    }
217
218    #[tokio::test]
219    async fn get_cardano_transactions_snapshot() {
220        let mut http_client = MockAggregatorClient::new();
221        let message = CardanoTransactionSnapshot {
222            merkle_root: "mk-123".to_string(),
223            epoch: Epoch(1),
224            block_number: BlockNumber(24),
225            hash: "hash-123".to_string(),
226            certificate_hash: "cert-hash-123".to_string(),
227            created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
228                .unwrap()
229                .with_timezone(&Utc),
230        };
231        let expected = message.clone();
232        http_client
233            .expect_get_content()
234            .with(eq(AggregatorRequest::GetCardanoTransactionSnapshot {
235                hash: "hash-123".to_string(),
236            }))
237            .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
238        let client = CardanoTransactionClient::new(Arc::new(http_client));
239        let cardano_transaction_snapshot = client
240            .get_snapshot("hash-123")
241            .await
242            .unwrap()
243            .expect("This test returns a cardano transaction snapshot");
244
245        assert_eq!(expected, cardano_transaction_snapshot);
246    }
247
248    #[tokio::test]
249    async fn test_get_proof_ok() {
250        let mut aggregator_client = MockAggregatorClient::new();
251        let certificate_hash = "cert-hash-123".to_string();
252        let set_proof = CardanoTransactionsSetProof::dummy();
253        let transactions_proofs = CardanoTransactionsProofs::new(
254            &certificate_hash,
255            vec![set_proof.clone()],
256            vec![],
257            BlockNumber(99999),
258        );
259        let expected_transactions_proofs = transactions_proofs.clone();
260        aggregator_client
261            .expect_get_content()
262            .return_once(move |_| Ok(serde_json::to_string(&transactions_proofs).unwrap()))
263            .times(1);
264
265        let cardano_tx_client = CardanoTransactionClient::new(Arc::new(aggregator_client));
266        let transactions_proofs = cardano_tx_client
267            .get_proofs(
268                &set_proof
269                    .transactions_hashes
270                    .iter()
271                    .map(|h| h.as_str())
272                    .collect::<Vec<_>>(),
273            )
274            .await
275            .unwrap();
276
277        assert_eq!(expected_transactions_proofs, transactions_proofs);
278    }
279
280    #[tokio::test]
281    async fn test_get_proof_ko() {
282        let mut aggregator_client = MockAggregatorClient::new();
283        aggregator_client
284            .expect_get_content()
285            .return_once(move |_| {
286                Err(AggregatorClientError::RemoteServerTechnical(anyhow!(
287                    "an error"
288                )))
289            })
290            .times(1);
291
292        let cardano_tx_client = CardanoTransactionClient::new(Arc::new(aggregator_client));
293        cardano_tx_client
294            .get_proofs(&["tx-123"])
295            .await
296            .expect_err("The certificate client should fail here.");
297    }
298}