mithril_aggregator/http_server/routes/
proof_routes.rs1use serde::{Deserialize, Serialize};
2use warp::Filter;
3
4use crate::http_server::routes::middlewares;
5use crate::http_server::routes::router::RouterState;
6
7#[derive(Deserialize, Serialize, Debug)]
8struct CardanoTransactionProofQueryParams {
9 transaction_hashes: String,
10}
11
12impl CardanoTransactionProofQueryParams {
13 pub fn split_transactions_hashes(&self) -> Vec<String> {
14 self.transaction_hashes
15 .split(',')
16 .map(|s| s.to_string())
17 .collect()
18 }
19
20 pub fn sanitize(&self) -> Vec<String> {
21 let mut transaction_hashes = self.split_transactions_hashes();
22 transaction_hashes.sort();
23 transaction_hashes.dedup();
24 transaction_hashes
25 }
26}
27
28pub fn routes(
29 router_state: &RouterState,
30) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
31 proof_cardano_transaction(router_state)
32}
33
34fn proof_cardano_transaction(
36 router_state: &RouterState,
37) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
38 warp::path!("proof" / "cardano-transaction")
39 .and(warp::get())
40 .and(warp::query::<CardanoTransactionProofQueryParams>())
41 .and(middlewares::with_logger(router_state))
42 .and(middlewares::with_signed_entity_service(router_state))
43 .and(middlewares::validators::with_prover_transactions_hash_validator(router_state))
44 .and(middlewares::with_prover_service(router_state))
45 .and(middlewares::with_metrics_service(router_state))
46 .and_then(handlers::proof_cardano_transaction)
47}
48
49mod handlers {
50 use mithril_common::{
51 entities::CardanoTransactionsSnapshot, messages::CardanoTransactionsProofsMessage,
52 signable_builder::SignedEntity, StdResult,
53 };
54 use slog::{debug, warn, Logger};
55 use std::{convert::Infallible, sync::Arc};
56 use warp::http::StatusCode;
57
58 use crate::{
59 http_server::{routes::reply, validators::ProverTransactionsHashValidator},
60 message_adapters::ToCardanoTransactionsProofsMessageAdapter,
61 services::{ProverService, SignedEntityService},
62 unwrap_to_internal_server_error, MetricsService,
63 };
64
65 use super::CardanoTransactionProofQueryParams;
66
67 pub async fn proof_cardano_transaction(
68 transaction_parameters: CardanoTransactionProofQueryParams,
69 logger: Logger,
70 signed_entity_service: Arc<dyn SignedEntityService>,
71 validator: ProverTransactionsHashValidator,
72 prover_service: Arc<dyn ProverService>,
73 metrics_service: Arc<MetricsService>,
74 ) -> Result<impl warp::Reply, Infallible> {
75 metrics_service
76 .get_proof_cardano_transaction_total_proofs_served_since_startup()
77 .increment();
78
79 let transaction_hashes = transaction_parameters.split_transactions_hashes();
80 debug!(
81 logger, ">> proof_cardano_transaction";
82 "transaction_hashes" => &transaction_parameters.transaction_hashes
83 );
84
85 if let Err(error) = validator.validate(&transaction_hashes) {
86 warn!(logger, "proof_cardano_transaction::bad_request");
87 return Ok(reply::bad_request(error.label, error.message));
88 }
89
90 let sanitized_hashes = transaction_parameters.sanitize();
91
92 metrics_service
94 .get_proof_cardano_transaction_total_transactions_served_since_startup()
95 .increment_by(sanitized_hashes.len().try_into().unwrap_or(0));
96
97 match unwrap_to_internal_server_error!(
98 signed_entity_service
99 .get_last_cardano_transaction_snapshot()
100 .await,
101 logger => "proof_cardano_transaction::error"
102 ) {
103 Some(signed_entity) => {
104 let message = unwrap_to_internal_server_error!(
105 build_response_message(prover_service, signed_entity, sanitized_hashes).await,
106 logger => "proof_cardano_transaction"
107 );
108 Ok(reply::json(&message, StatusCode::OK))
109 }
110 None => {
111 warn!(logger, "proof_cardano_transaction::not_found");
112 Ok(reply::empty(StatusCode::NOT_FOUND))
113 }
114 }
115 }
116
117 pub async fn build_response_message(
118 prover_service: Arc<dyn ProverService>,
119 signed_entity: SignedEntity<CardanoTransactionsSnapshot>,
120 transaction_hashes: Vec<String>,
121 ) -> StdResult<CardanoTransactionsProofsMessage> {
122 let transactions_set_proofs = prover_service
123 .compute_transactions_proofs(
124 signed_entity.artifact.block_number,
125 transaction_hashes.as_slice(),
126 )
127 .await?;
128 let message = ToCardanoTransactionsProofsMessageAdapter::try_adapt(
129 signed_entity,
130 transactions_set_proofs,
131 transaction_hashes,
132 )?;
133
134 Ok(message)
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use anyhow::anyhow;
141 use serde_json::Value::Null;
142 use std::sync::Arc;
143 use std::vec;
144 use warp::{
145 http::{Method, StatusCode},
146 test::request,
147 };
148
149 use mithril_common::{
150 entities::{BlockNumber, CardanoTransactionsSetProof, CardanoTransactionsSnapshot},
151 signable_builder::SignedEntity,
152 test_utils::{apispec::APISpec, assert_equivalent, fake_data},
153 };
154
155 use crate::services::MockProverService;
156 use crate::{initialize_dependencies, services::MockSignedEntityService};
157
158 use super::*;
159
160 fn setup_router(
161 state: RouterState,
162 ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
163 let cors = warp::cors()
164 .allow_any_origin()
165 .allow_headers(vec!["content-type"])
166 .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
167
168 warp::any().and(routes(&state).with(cors))
169 }
170
171 #[tokio::test]
172 async fn build_response_message_return_latest_block_number_from_artifact_beacon() {
173 let mut mock_prover_service = MockProverService::new();
175 mock_prover_service
176 .expect_compute_transactions_proofs()
177 .returning(|_, _| Ok(vec![CardanoTransactionsSetProof::dummy()]));
178
179 let cardano_transactions_snapshot =
180 CardanoTransactionsSnapshot::new(String::new(), BlockNumber(2309));
181
182 let signed_entity = SignedEntity::<CardanoTransactionsSnapshot> {
183 artifact: cardano_transactions_snapshot,
184 ..SignedEntity::<CardanoTransactionsSnapshot>::dummy()
185 };
186
187 let transaction_hashes = vec![];
189 let message = handlers::build_response_message(
190 Arc::new(mock_prover_service),
191 signed_entity,
192 transaction_hashes,
193 )
194 .await
195 .unwrap();
196
197 assert_eq!(message.latest_block_number, 2309)
199 }
200
201 #[tokio::test]
202 async fn test_proof_cardano_transaction_increments_proofs_metrics() {
203 let method = Method::GET.as_str();
204 let path = "/proof/cardano-transaction";
205 let dependency_manager = Arc::new(initialize_dependencies!().await);
206 let initial_proofs_counter_value = dependency_manager
207 .metrics_service
208 .get_proof_cardano_transaction_total_proofs_served_since_startup()
209 .get();
210 let initial_transactions_counter_value = dependency_manager
211 .metrics_service
212 .get_proof_cardano_transaction_total_transactions_served_since_startup()
213 .get();
214
215 request()
216 .method(method)
217 .path(&format!(
218 "{path}?transaction_hashes={},{},{}",
219 fake_data::transaction_hashes()[0],
220 fake_data::transaction_hashes()[1],
221 fake_data::transaction_hashes()[2]
222 ))
223 .reply(&setup_router(RouterState::new_with_dummy_config(
224 dependency_manager.clone(),
225 )))
226 .await;
227
228 assert_eq!(
229 initial_proofs_counter_value + 1,
230 dependency_manager
231 .metrics_service
232 .get_proof_cardano_transaction_total_proofs_served_since_startup()
233 .get()
234 );
235 assert_eq!(
236 initial_transactions_counter_value + 3,
237 dependency_manager
238 .metrics_service
239 .get_proof_cardano_transaction_total_transactions_served_since_startup()
240 .get()
241 );
242 }
243
244 #[tokio::test]
245 async fn proof_cardano_transaction_ok() {
246 let mut dependency_manager = initialize_dependencies!().await;
247 let mut mock_signed_entity_service = MockSignedEntityService::new();
248 mock_signed_entity_service
249 .expect_get_last_cardano_transaction_snapshot()
250 .returning(|| Ok(Some(SignedEntity::<CardanoTransactionsSnapshot>::dummy())));
251 dependency_manager.signed_entity_service = Arc::new(mock_signed_entity_service);
252
253 let mut mock_prover_service = MockProverService::new();
254 mock_prover_service
255 .expect_compute_transactions_proofs()
256 .returning(|_, _| Ok(vec![CardanoTransactionsSetProof::dummy()]));
257 dependency_manager.prover_service = Arc::new(mock_prover_service);
258
259 let method = Method::GET.as_str();
260 let path = "/proof/cardano-transaction";
261
262 let response = request()
263 .method(method)
264 .path(&format!(
265 "{path}?transaction_hashes={},{}",
266 fake_data::transaction_hashes()[0],
267 fake_data::transaction_hashes()[1]
268 ))
269 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
270 dependency_manager,
271 ))))
272 .await;
273
274 APISpec::verify_conformity(
275 APISpec::get_all_spec_files(),
276 method,
277 path,
278 "application/json",
279 &Null,
280 &response,
281 &StatusCode::OK,
282 )
283 .unwrap();
284 }
285
286 #[tokio::test]
287 async fn proof_cardano_transaction_not_found() {
288 let dependency_manager = initialize_dependencies!().await;
289
290 let method = Method::GET.as_str();
291 let path = "/proof/cardano-transaction";
292
293 let response = request()
294 .method(method)
295 .path(&format!(
296 "{path}?transaction_hashes={},{}",
297 fake_data::transaction_hashes()[0],
298 fake_data::transaction_hashes()[1]
299 ))
300 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
301 dependency_manager,
302 ))))
303 .await;
304
305 APISpec::verify_conformity(
306 APISpec::get_all_spec_files(),
307 method,
308 path,
309 "application/json",
310 &Null,
311 &response,
312 &StatusCode::NOT_FOUND,
313 )
314 .unwrap();
315 }
316
317 #[tokio::test]
318 async fn proof_cardano_transaction_ko() {
319 let mut dependency_manager = initialize_dependencies!().await;
320 let mut mock_signed_entity_service = MockSignedEntityService::new();
321 mock_signed_entity_service
322 .expect_get_last_cardano_transaction_snapshot()
323 .returning(|| Err(anyhow!("Error")));
324 dependency_manager.signed_entity_service = Arc::new(mock_signed_entity_service);
325
326 let method = Method::GET.as_str();
327 let path = "/proof/cardano-transaction";
328
329 let response = request()
330 .method(method)
331 .path(&format!(
332 "{path}?transaction_hashes={},{}",
333 fake_data::transaction_hashes()[0],
334 fake_data::transaction_hashes()[1]
335 ))
336 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
337 dependency_manager,
338 ))))
339 .await;
340
341 APISpec::verify_conformity(
342 APISpec::get_all_spec_files(),
343 method,
344 path,
345 "application/json",
346 &Null,
347 &response,
348 &StatusCode::INTERNAL_SERVER_ERROR,
349 )
350 .unwrap();
351 }
352
353 #[tokio::test]
354 async fn proof_cardano_transaction_return_bad_request_with_invalid_hashes() {
355 let dependency_manager = initialize_dependencies!().await;
356
357 let method = Method::GET.as_str();
358 let path = "/proof/cardano-transaction";
359
360 let response = request()
361 .method(method)
362 .path(&format!(
363 "{path}?transaction_hashes=invalid%3A%2F%2Fid,,tx-456"
364 ))
365 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
366 dependency_manager,
367 ))))
368 .await;
369
370 APISpec::verify_conformity(
371 APISpec::get_all_spec_files(),
372 method,
373 path,
374 "application/json",
375 &Null,
376 &response,
377 &StatusCode::BAD_REQUEST,
378 )
379 .unwrap();
380 }
381
382 #[tokio::test]
383 async fn proof_cardano_transaction_route_deduplicate_hashes() {
384 let tx = fake_data::transaction_hashes()[0].to_string();
385 let mut dependency_manager = initialize_dependencies!().await;
386 let mut mock_signed_entity_service = MockSignedEntityService::new();
387 mock_signed_entity_service
388 .expect_get_last_cardano_transaction_snapshot()
389 .returning(|| Ok(Some(SignedEntity::<CardanoTransactionsSnapshot>::dummy())));
390 dependency_manager.signed_entity_service = Arc::new(mock_signed_entity_service);
391
392 let mut mock_prover_service = MockProverService::new();
393 let txs_expected = vec![tx.clone()];
394 mock_prover_service
395 .expect_compute_transactions_proofs()
396 .withf(move |_, transaction_hashes| transaction_hashes == txs_expected)
397 .returning(|_, _| Ok(vec![CardanoTransactionsSetProof::dummy()]));
398 dependency_manager.prover_service = Arc::new(mock_prover_service);
399
400 let method = Method::GET.as_str();
401 let path = "/proof/cardano-transaction";
402
403 let response = request()
404 .method(method)
405 .path(&format!("{path}?transaction_hashes={tx},{tx}",))
406 .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
407 dependency_manager,
408 ))))
409 .await;
410
411 assert_eq!(StatusCode::OK, response.status());
412 }
413
414 #[test]
415 fn sanitize_cardano_transaction_proof_query_params_remove_duplicate() {
416 let tx1 = fake_data::transaction_hashes()[0].to_string();
417 let tx2 = fake_data::transaction_hashes()[1].to_string();
418
419 let params = CardanoTransactionProofQueryParams {
422 transaction_hashes: format!("{tx1},{tx2},{tx2},{tx1},{tx2}",),
423 };
424
425 assert_equivalent(params.sanitize(), vec![tx1, tx2]);
426 }
427}