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.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
31fn 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 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 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 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_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 let params = CardanoTransactionProofQueryParams {
440 transaction_hashes: format!("{tx1},{tx2},{tx2},{tx1},{tx2}",),
441 };
442
443 assert_equivalent(params.sanitize(), vec![tx1, tx2]);
444 }
445}