mithril_aggregator/http_server/routes/
proof_routes.rs

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