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::{
168            assert_equivalent,
169            double::{Dummy, fake_data},
170        },
171    };
172
173    use crate::services::MockProverService;
174    use crate::{initialize_dependencies, services::MockSignedEntityService};
175
176    use super::*;
177
178    fn setup_router(
179        state: RouterState,
180    ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
181        let cors = warp::cors()
182            .allow_any_origin()
183            .allow_headers(vec!["content-type"])
184            .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]);
185
186        warp::any().and(routes(&state).with(cors))
187    }
188
189    #[tokio::test]
190    async fn build_response_message_return_latest_block_number_from_artifact_beacon() {
191        // Arrange
192        let mut mock_prover_service = MockProverService::new();
193        mock_prover_service
194            .expect_compute_transactions_proofs()
195            .returning(|_, _| Ok(vec![CardanoTransactionsSetProof::dummy()]));
196
197        let cardano_transactions_snapshot =
198            CardanoTransactionsSnapshot::new(String::new(), BlockNumber(2309));
199
200        let signed_entity = SignedEntity::<CardanoTransactionsSnapshot> {
201            artifact: cardano_transactions_snapshot,
202            ..Dummy::dummy()
203        };
204
205        // Action
206        let transaction_hashes = vec![];
207        let message = handlers::build_response_message(
208            Arc::new(mock_prover_service),
209            signed_entity,
210            transaction_hashes,
211        )
212        .await
213        .unwrap();
214
215        // Assert
216        assert_eq!(message.latest_block_number, 2309)
217    }
218
219    #[tokio::test]
220    async fn test_proof_cardano_transaction_increments_proofs_metrics() {
221        let method = Method::GET.as_str();
222        let path = "/proof/cardano-transaction";
223        let dependency_manager = Arc::new(initialize_dependencies!().await);
224        let initial_proofs_counter_value = dependency_manager
225            .metrics_service
226            .get_proof_cardano_transaction_total_proofs_served_since_startup()
227            .get(&["TEST", "CLI"]);
228        let initial_transactions_counter_value = dependency_manager
229            .metrics_service
230            .get_proof_cardano_transaction_total_transactions_served_since_startup()
231            .get(&["TEST", "CLI"]);
232
233        request()
234            .method(method)
235            .path(&format!(
236                "{path}?transaction_hashes={},{},{}",
237                fake_data::transaction_hashes()[0],
238                fake_data::transaction_hashes()[1],
239                fake_data::transaction_hashes()[2]
240            ))
241            .header(MITHRIL_ORIGIN_TAG_HEADER, "TEST")
242            .header(MITHRIL_CLIENT_TYPE_HEADER, "CLI")
243            .reply(&setup_router(RouterState::new_with_origin_tag_white_list(
244                dependency_manager.clone(),
245                &["TEST"],
246            )))
247            .await;
248
249        assert_eq!(
250            initial_proofs_counter_value + 1,
251            dependency_manager
252                .metrics_service
253                .get_proof_cardano_transaction_total_proofs_served_since_startup()
254                .get(&["TEST", "CLI"])
255        );
256        assert_eq!(
257            initial_transactions_counter_value + 3,
258            dependency_manager
259                .metrics_service
260                .get_proof_cardano_transaction_total_transactions_served_since_startup()
261                .get(&["TEST", "CLI"])
262        );
263    }
264
265    #[tokio::test]
266    async fn proof_cardano_transaction_ok() {
267        let mut dependency_manager = initialize_dependencies!().await;
268        let mut mock_signed_entity_service = MockSignedEntityService::new();
269        mock_signed_entity_service
270            .expect_get_last_cardano_transaction_snapshot()
271            .returning(|| Ok(Some(SignedEntity::<CardanoTransactionsSnapshot>::dummy())));
272        dependency_manager.signed_entity_service = Arc::new(mock_signed_entity_service);
273
274        let mut mock_prover_service = MockProverService::new();
275        mock_prover_service
276            .expect_compute_transactions_proofs()
277            .returning(|_, _| Ok(vec![CardanoTransactionsSetProof::dummy()]));
278        dependency_manager.prover_service = Arc::new(mock_prover_service);
279
280        let method = Method::GET.as_str();
281        let path = "/proof/cardano-transaction";
282
283        let response = request()
284            .method(method)
285            .path(&format!(
286                "{path}?transaction_hashes={},{}",
287                fake_data::transaction_hashes()[0],
288                fake_data::transaction_hashes()[1]
289            ))
290            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
291                dependency_manager,
292            ))))
293            .await;
294
295        APISpec::verify_conformity(
296            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
297            method,
298            path,
299            "application/json",
300            &Null,
301            &response,
302            &StatusCode::OK,
303        )
304        .unwrap();
305    }
306
307    #[tokio::test]
308    async fn proof_cardano_transaction_not_found() {
309        let dependency_manager = initialize_dependencies!().await;
310
311        let method = Method::GET.as_str();
312        let path = "/proof/cardano-transaction";
313
314        let response = request()
315            .method(method)
316            .path(&format!(
317                "{path}?transaction_hashes={},{}",
318                fake_data::transaction_hashes()[0],
319                fake_data::transaction_hashes()[1]
320            ))
321            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
322                dependency_manager,
323            ))))
324            .await;
325
326        APISpec::verify_conformity(
327            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
328            method,
329            path,
330            "application/json",
331            &Null,
332            &response,
333            &StatusCode::NOT_FOUND,
334        )
335        .unwrap();
336    }
337
338    #[tokio::test]
339    async fn proof_cardano_transaction_ko() {
340        let mut dependency_manager = initialize_dependencies!().await;
341        let mut mock_signed_entity_service = MockSignedEntityService::new();
342        mock_signed_entity_service
343            .expect_get_last_cardano_transaction_snapshot()
344            .returning(|| Err(anyhow!("Error")));
345        dependency_manager.signed_entity_service = Arc::new(mock_signed_entity_service);
346
347        let method = Method::GET.as_str();
348        let path = "/proof/cardano-transaction";
349
350        let response = request()
351            .method(method)
352            .path(&format!(
353                "{path}?transaction_hashes={},{}",
354                fake_data::transaction_hashes()[0],
355                fake_data::transaction_hashes()[1]
356            ))
357            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
358                dependency_manager,
359            ))))
360            .await;
361
362        APISpec::verify_conformity(
363            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
364            method,
365            path,
366            "application/json",
367            &Null,
368            &response,
369            &StatusCode::INTERNAL_SERVER_ERROR,
370        )
371        .unwrap();
372    }
373
374    #[tokio::test]
375    async fn proof_cardano_transaction_return_bad_request_with_invalid_hashes() {
376        let dependency_manager = initialize_dependencies!().await;
377
378        let method = Method::GET.as_str();
379        let path = "/proof/cardano-transaction";
380
381        let response = request()
382            .method(method)
383            .path(&format!(
384                "{path}?transaction_hashes=invalid%3A%2F%2Fid,,tx-456"
385            ))
386            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
387                dependency_manager,
388            ))))
389            .await;
390
391        APISpec::verify_conformity(
392            APISpec::get_default_spec_file_from(crate::http_server::API_SPEC_LOCATION),
393            method,
394            path,
395            "application/json",
396            &Null,
397            &response,
398            &StatusCode::BAD_REQUEST,
399        )
400        .unwrap();
401    }
402
403    #[tokio::test]
404    async fn proof_cardano_transaction_route_deduplicate_hashes() {
405        let tx = fake_data::transaction_hashes()[0].to_string();
406        let mut dependency_manager = initialize_dependencies!().await;
407        let mut mock_signed_entity_service = MockSignedEntityService::new();
408        mock_signed_entity_service
409            .expect_get_last_cardano_transaction_snapshot()
410            .returning(|| Ok(Some(SignedEntity::<CardanoTransactionsSnapshot>::dummy())));
411        dependency_manager.signed_entity_service = Arc::new(mock_signed_entity_service);
412
413        let mut mock_prover_service = MockProverService::new();
414        let txs_expected = vec![tx.clone()];
415        mock_prover_service
416            .expect_compute_transactions_proofs()
417            .withf(move |_, transaction_hashes| transaction_hashes == txs_expected)
418            .returning(|_, _| Ok(vec![CardanoTransactionsSetProof::dummy()]));
419        dependency_manager.prover_service = Arc::new(mock_prover_service);
420
421        let method = Method::GET.as_str();
422        let path = "/proof/cardano-transaction";
423
424        let response = request()
425            .method(method)
426            .path(&format!("{path}?transaction_hashes={tx},{tx}",))
427            .reply(&setup_router(RouterState::new_with_dummy_config(Arc::new(
428                dependency_manager,
429            ))))
430            .await;
431
432        assert_eq!(StatusCode::OK, response.status());
433    }
434
435    #[test]
436    fn sanitize_cardano_transaction_proof_query_params_remove_duplicate() {
437        let tx1 = fake_data::transaction_hashes()[0].to_string();
438        let tx2 = fake_data::transaction_hashes()[1].to_string();
439
440        // We are testing on an unordered list of transaction hashes
441        // as some rust dedup methods only remove consecutive duplicates
442        let params = CardanoTransactionProofQueryParams {
443            transaction_hashes: format!("{tx1},{tx2},{tx2},{tx1},{tx2}",),
444        };
445
446        assert_equivalent!(params.sanitize(), vec![tx1, tx2]);
447    }
448}