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