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 crate::aggregator_client::{AggregatorClientError, MockAggregatorClient};
168    use crate::common::{BlockNumber, Epoch};
169    use crate::{
170        CardanoTransactionSnapshot, CardanoTransactionSnapshotListItem, CardanoTransactionsProofs,
171        CardanoTransactionsSetProof,
172    };
173
174    use super::*;
175
176    fn fake_messages() -> Vec<CardanoTransactionSnapshotListItem> {
177        vec![
178            CardanoTransactionSnapshotListItem {
179                merkle_root: "mk-123".to_string(),
180                epoch: Epoch(1),
181                block_number: BlockNumber(24),
182                hash: "hash-123".to_string(),
183                certificate_hash: "cert-hash-123".to_string(),
184                created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
185                    .unwrap()
186                    .with_timezone(&Utc),
187            },
188            CardanoTransactionSnapshotListItem {
189                merkle_root: "mk-456".to_string(),
190                epoch: Epoch(1),
191                block_number: BlockNumber(24),
192                hash: "hash-456".to_string(),
193                certificate_hash: "cert-hash-456".to_string(),
194                created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
195                    .unwrap()
196                    .with_timezone(&Utc),
197            },
198        ]
199    }
200
201    #[tokio::test]
202    async fn get_cardano_transactions_snapshot_list() {
203        let message = fake_messages();
204        let mut http_client = MockAggregatorClient::new();
205        http_client
206            .expect_get_content()
207            .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
208        let client = CardanoTransactionClient::new(Arc::new(http_client));
209        let items = client.list_snapshots().await.unwrap();
210
211        assert_eq!(2, items.len());
212        assert_eq!("hash-123".to_string(), items[0].hash);
213        assert_eq!("hash-456".to_string(), items[1].hash);
214    }
215
216    #[tokio::test]
217    async fn get_cardano_transactions_snapshot() {
218        let mut http_client = MockAggregatorClient::new();
219        let message = CardanoTransactionSnapshot {
220            merkle_root: "mk-123".to_string(),
221            epoch: Epoch(1),
222            block_number: BlockNumber(24),
223            hash: "hash-123".to_string(),
224            certificate_hash: "cert-hash-123".to_string(),
225            created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
226                .unwrap()
227                .with_timezone(&Utc),
228        };
229        let expected = message.clone();
230        http_client
231            .expect_get_content()
232            .with(eq(AggregatorRequest::GetCardanoTransactionSnapshot {
233                hash: "hash-123".to_string(),
234            }))
235            .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
236        let client = CardanoTransactionClient::new(Arc::new(http_client));
237        let cardano_transaction_snapshot = client
238            .get_snapshot("hash-123")
239            .await
240            .unwrap()
241            .expect("This test returns a cardano transaction snapshot");
242
243        assert_eq!(expected, cardano_transaction_snapshot);
244    }
245
246    #[tokio::test]
247    async fn test_get_proof_ok() {
248        let mut aggregator_client = MockAggregatorClient::new();
249        let certificate_hash = "cert-hash-123".to_string();
250        let set_proof = CardanoTransactionsSetProof::dummy();
251        let transactions_proofs = CardanoTransactionsProofs::new(
252            &certificate_hash,
253            vec![set_proof.clone()],
254            vec![],
255            BlockNumber(99999),
256        );
257        let expected_transactions_proofs = transactions_proofs.clone();
258        aggregator_client
259            .expect_get_content()
260            .return_once(move |_| Ok(serde_json::to_string(&transactions_proofs).unwrap()))
261            .times(1);
262
263        let cardano_tx_client = CardanoTransactionClient::new(Arc::new(aggregator_client));
264        let transactions_proofs = cardano_tx_client
265            .get_proofs(
266                &set_proof
267                    .transactions_hashes
268                    .iter()
269                    .map(|h| h.as_str())
270                    .collect::<Vec<_>>(),
271            )
272            .await
273            .unwrap();
274
275        assert_eq!(expected_transactions_proofs, transactions_proofs);
276    }
277
278    #[tokio::test]
279    async fn test_get_proof_ko() {
280        let mut aggregator_client = MockAggregatorClient::new();
281        aggregator_client
282            .expect_get_content()
283            .return_once(move |_| {
284                Err(AggregatorClientError::RemoteServerTechnical(anyhow!(
285                    "an error"
286                )))
287            })
288            .times(1);
289
290        let cardano_tx_client = CardanoTransactionClient::new(Arc::new(aggregator_client));
291        cardano_tx_client
292            .get_proofs(&["tx-123"])
293            .await
294            .expect_err("The certificate client should fail here.");
295    }
296}