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
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
34/// GET /proof/cardano-transaction
35fn 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        // Fallback to 0, it should be impossible to have more than u32::MAX transactions.
93        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        // Arrange
174        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        // Action
188        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
198        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        // We are testing on an unordered list of transaction hashes
420        // as some rust dedup methods only remove consecutive duplicates
421        let params = CardanoTransactionProofQueryParams {
422            transaction_hashes: format!("{tx1},{tx2},{tx2},{tx1},{tx2}",),
423        };
424
425        assert_equivalent(params.sanitize(), vec![tx1, tx2]);
426    }
427}