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 anyhow::Context;
78use std::sync::Arc;
79
80use crate::{
81    CardanoTransactionSnapshot, CardanoTransactionSnapshotListItem, CardanoTransactionsProofs,
82    MithrilResult,
83};
84
85/// HTTP client for CardanoTransactionsAPI from the aggregator
86pub struct CardanoTransactionClient {
87    aggregator_requester: Arc<dyn CardanoTransactionAggregatorRequest>,
88}
89
90/// Define the requests against an aggregator related to Cardano transactions.
91#[cfg_attr(test, mockall::automock)]
92#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
93#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
94pub trait CardanoTransactionAggregatorRequest: Send + Sync {
95    /// Get a proof of membership for the given transactions hashes from the aggregator.
96    async fn get_proof(
97        &self,
98        hashes: &[String],
99    ) -> MithrilResult<Option<CardanoTransactionsProofs>>;
100
101    /// Fetch the list of latest signed Cardano transactions snapshots from the aggregator
102    async fn list_latest_snapshots(&self)
103    -> MithrilResult<Vec<CardanoTransactionSnapshotListItem>>;
104
105    /// Fetch a Cardano transactions snapshot by its hash from the aggregator.
106    async fn get_snapshot(&self, hash: &str) -> MithrilResult<Option<CardanoTransactionSnapshot>>;
107}
108
109impl CardanoTransactionClient {
110    /// Constructs a new `CardanoTransactionClient`.
111    pub fn new(aggregator_requester: Arc<dyn CardanoTransactionAggregatorRequest>) -> Self {
112        Self {
113            aggregator_requester,
114        }
115    }
116
117    /// Get proofs that the given subset of transactions is included in the Cardano transactions set.
118    pub async fn get_proofs<T: ToString>(
119        &self,
120        transactions_hashes: &[T],
121    ) -> MithrilResult<CardanoTransactionsProofs> {
122        let transactions_hashes: Vec<String> =
123            transactions_hashes.iter().map(|h| h.to_string()).collect();
124
125        self.aggregator_requester
126            .get_proof(&transactions_hashes)
127            .await?
128            .with_context(|| {
129                format!("No proof found for transactions hashes: {transactions_hashes:?}",)
130            })
131    }
132
133    /// Fetch a list of signed Cardano transaction snapshots.
134    pub async fn list_snapshots(&self) -> MithrilResult<Vec<CardanoTransactionSnapshotListItem>> {
135        self.aggregator_requester.list_latest_snapshots().await
136    }
137
138    /// Get the given Cardano transaction snapshot data. If it cannot be found, a None is returned.
139    pub async fn get_snapshot(
140        &self,
141        hash: &str,
142    ) -> MithrilResult<Option<CardanoTransactionSnapshot>> {
143        self.aggregator_requester.get_snapshot(hash).await
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use mockall::predicate::eq;
150
151    use mithril_common::test::mock_extensions::MockBuilder;
152
153    use crate::common::{BlockNumber, test::Dummy};
154    use crate::{
155        CardanoTransactionSnapshot, CardanoTransactionSnapshotListItem, CardanoTransactionsProofs,
156        CardanoTransactionsSetProof,
157    };
158
159    use super::*;
160
161    #[tokio::test]
162    async fn get_cardano_transactions_snapshot_list() {
163        let aggregator_requester =
164            MockBuilder::<MockCardanoTransactionAggregatorRequest>::configure(|mock| {
165                let messages = vec![
166                    CardanoTransactionSnapshotListItem {
167                        hash: "hash-123".to_string(),
168                        ..Dummy::dummy()
169                    },
170                    CardanoTransactionSnapshotListItem {
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 = CardanoTransactionClient::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_transactions_snapshot() {
188        let aggregator_requester =
189            MockBuilder::<MockCardanoTransactionAggregatorRequest>::configure(|mock| {
190                let message = CardanoTransactionSnapshot {
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 = CardanoTransactionClient::new(aggregator_requester);
200
201        let cardano_transaction_snapshot = client
202            .get_snapshot("hash-123")
203            .await
204            .unwrap()
205            .expect("This test returns a cardano transaction snapshot");
206
207        assert_eq!("hash-123", &cardano_transaction_snapshot.hash);
208        assert_eq!("mk-123", &cardano_transaction_snapshot.merkle_root);
209    }
210
211    #[tokio::test]
212    async fn test_get_proof_ok() {
213        let certificate_hash = "cert-hash-123".to_string();
214        let set_proof = CardanoTransactionsSetProof::dummy();
215        let expected_transactions_proofs = CardanoTransactionsProofs::new(
216            &certificate_hash,
217            vec![set_proof.clone()],
218            vec![],
219            BlockNumber(99999),
220        );
221
222        let aggregator_requester =
223            MockBuilder::<MockCardanoTransactionAggregatorRequest>::configure(|mock| {
224                let message = expected_transactions_proofs.clone();
225                mock.expect_get_proof()
226                    .with(eq(message
227                        .certified_transactions
228                        .iter()
229                        .flat_map(|tx| tx.transactions_hashes.clone())
230                        .collect::<Vec<_>>()))
231                    .return_once(|_| Ok(Some(message)));
232            });
233        let client = CardanoTransactionClient::new(aggregator_requester);
234
235        let transactions_proofs = client
236            .get_proofs(
237                &set_proof
238                    .transactions_hashes
239                    .iter()
240                    .map(|h| h.as_str())
241                    .collect::<Vec<_>>(),
242            )
243            .await
244            .unwrap();
245
246        assert_eq!(expected_transactions_proofs, transactions_proofs);
247    }
248
249    #[tokio::test]
250    async fn test_get_proof_ko() {
251        let aggregator_requester =
252            MockBuilder::<MockCardanoTransactionAggregatorRequest>::configure(|mock| {
253                mock.expect_get_proof()
254                    .return_once(move |_| Err(anyhow::anyhow!("an error")));
255            });
256        let client = CardanoTransactionClient::new(aggregator_requester);
257
258        client
259            .get_proofs(&["tx-123"])
260            .await
261            .expect_err("The certificate client should fail here.");
262    }
263}