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}