diff --git a/Cargo.lock b/Cargo.lock index ca6f23efff3..f2ccfd23141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3288,7 +3288,7 @@ dependencies = [ [[package]] name = "mithril-aggregator" -version = "0.4.29" +version = "0.4.30" dependencies = [ "anyhow", "async-trait", @@ -3426,7 +3426,7 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.2.156" +version = "0.2.157" dependencies = [ "anyhow", "async-trait", diff --git a/mithril-aggregator/Cargo.toml b/mithril-aggregator/Cargo.toml index cb0789aad78..b155c39080c 100644 --- a/mithril-aggregator/Cargo.toml +++ b/mithril-aggregator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-aggregator" -version = "0.4.29" +version = "0.4.30" description = "A Mithril Aggregator server" authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-aggregator/src/database/provider/cardano_transaction.rs b/mithril-aggregator/src/database/provider/cardano_transaction.rs index 3695572f72d..48052cca578 100644 --- a/mithril-aggregator/src/database/provider/cardano_transaction.rs +++ b/mithril-aggregator/src/database/provider/cardano_transaction.rs @@ -13,6 +13,8 @@ use async_trait::async_trait; use sqlite::{Row, Value}; use std::{iter::repeat, sync::Arc}; +use crate::services::TransactionsRetriever; + /// Cardano Transaction record is the representation of a cardano transaction. #[derive(Debug, PartialEq, Clone)] pub struct CardanoTransactionRecord { @@ -36,6 +38,16 @@ impl From for CardanoTransactionRecord { } } +impl From for CardanoTransaction { + fn from(other: CardanoTransactionRecord) -> CardanoTransaction { + CardanoTransaction { + transaction_hash: other.transaction_hash, + block_number: other.block_number, + immutable_file_number: other.immutable_file_number, + } + } +} + impl SqLiteEntity for CardanoTransactionRecord { fn hydrate(row: Row) -> Result where @@ -186,6 +198,15 @@ impl CardanoTransactionRepository { Self { connection } } + /// Return all the [CardanoTransactionRecord]s in the database. + pub async fn get_all_transactions(&self) -> StdResult> { + let provider = CardanoTransactionProvider::new(&self.connection); + let filters = WhereCondition::default(); + let transactions = provider.find(filters)?; + + Ok(transactions.collect()) + } + /// Return the [CardanoTransactionRecord] for the given transaction hash. pub async fn get_transaction( &self, @@ -242,6 +263,17 @@ impl TransactionStore for CardanoTransactionRepository { } } +#[async_trait] +impl TransactionsRetriever for CardanoTransactionRepository { + async fn get_all(&self) -> StdResult> { + self.get_all_transactions().await.map(|v| { + v.into_iter() + .map(|record| record.into()) + .collect::>() + }) + } +} + #[cfg(test)] mod tests { use mithril_common::sqlite::SourceAlias; @@ -463,6 +495,38 @@ mod tests { ); } + #[tokio::test] + async fn repository_store_transactions_and_get_all_stored_transactions() { + let connection = get_connection().await; + let repository = CardanoTransactionRepository::new(connection.clone()); + + let cardano_transactions = vec![ + CardanoTransaction { + transaction_hash: "tx-hash-123".to_string(), + block_number: 10, + immutable_file_number: 99, + }, + CardanoTransaction { + transaction_hash: "tx-hash-456".to_string(), + block_number: 11, + immutable_file_number: 100, + }, + ]; + repository + .store_transactions(&cardano_transactions) + .await + .unwrap(); + + let transactions_result = repository.get_all().await.unwrap(); + let transactions_expected = cardano_transactions + .iter() + .rev() + .cloned() + .collect::>(); + + assert_eq!(transactions_expected, transactions_result); + } + #[tokio::test] async fn repository_store_transactions_doesnt_erase_existing_data() { let connection = get_connection().await; diff --git a/mithril-aggregator/src/dependency_injection/builder.rs b/mithril-aggregator/src/dependency_injection/builder.rs index 82dabcf57dd..baf32a49e04 100644 --- a/mithril-aggregator/src/dependency_injection/builder.rs +++ b/mithril-aggregator/src/dependency_injection/builder.rs @@ -59,8 +59,9 @@ use crate::{ http_server::routes::router, services::{ CertifierService, MessageService, MithrilCertifierService, MithrilEpochService, - MithrilMessageService, MithrilSignedEntityService, MithrilStakeDistributionService, - MithrilTickerService, SignedEntityService, StakeDistributionService, TickerService, + MithrilMessageService, MithrilProverService, MithrilSignedEntityService, + MithrilStakeDistributionService, MithrilTickerService, ProverService, SignedEntityService, + StakeDistributionService, TickerService, }, tools::{CExplorerSignerRetriever, GcpFileUploader, GenesisToolsDependency, SignersImporter}, AggregatorConfig, AggregatorRunner, AggregatorRuntime, CertificatePendingStore, @@ -130,6 +131,9 @@ pub struct DependenciesBuilder { /// Beacon provider service. pub beacon_provider: Option>, + /// Cardano transactions repository. + pub transaction_repository: Option>, + /// Cardano transactions store. pub transaction_store: Option>, @@ -207,6 +211,9 @@ pub struct DependenciesBuilder { /// HTTP Message service pub message_service: Option>, + + /// Prover service + pub prover_service: Option>, } impl DependenciesBuilder { @@ -228,6 +235,7 @@ impl DependenciesBuilder { chain_observer: None, beacon_provider: None, transaction_parser: None, + transaction_repository: None, transaction_store: None, immutable_digester: None, immutable_file_observer: None, @@ -252,6 +260,7 @@ impl DependenciesBuilder { epoch_service: None, signed_entity_storer: None, message_service: None, + prover_service: None, } } @@ -677,7 +686,7 @@ impl DependenciesBuilder { self.create_logger().await } - async fn build_transaction_store(&mut self) -> Result> { + async fn build_transaction_repository(&mut self) -> Result> { let transaction_store = CardanoTransactionRepository::new( self.get_sqlite_connection_cardano_transaction().await?, ); @@ -685,6 +694,23 @@ impl DependenciesBuilder { Ok(Arc::new(transaction_store)) } + /// Transaction repository. + pub async fn get_transaction_repository( + &mut self, + ) -> Result> { + if self.transaction_repository.is_none() { + self.transaction_repository = Some(self.build_transaction_repository().await?); + } + + Ok(self.transaction_repository.as_ref().cloned().unwrap()) + } + + async fn build_transaction_store(&mut self) -> Result> { + let transaction_store = self.get_transaction_repository().await?; + + Ok(transaction_store as Arc) + } + /// Transaction store. pub async fn get_transaction_store(&mut self) -> Result> { if self.transaction_store.is_none() { @@ -1171,6 +1197,7 @@ impl DependenciesBuilder { message_service: self.get_message_service().await?, transaction_parser: self.get_transaction_parser().await?, transaction_store: self.get_transaction_store().await?, + prover_service: self.get_prover_service().await?, }; Ok(dependency_manager) @@ -1374,6 +1401,23 @@ impl DependenciesBuilder { Ok(self.message_service.as_ref().cloned().unwrap()) } + /// build Prover service + pub async fn build_prover_service(&mut self) -> Result> { + let transaction_retriever = self.get_transaction_repository().await?; + let service = MithrilProverService::new(transaction_retriever); + + Ok(Arc::new(service)) + } + + /// [ProverService] service + pub async fn get_prover_service(&mut self) -> Result> { + if self.prover_service.is_none() { + self.prover_service = Some(self.build_prover_service().await?); + } + + Ok(self.prover_service.as_ref().cloned().unwrap()) + } + /// Remove the dependencies builder from memory to release Arc instances. pub async fn vanish(self) { self.drop_sqlite_connections().await; diff --git a/mithril-aggregator/src/dependency_injection/containers.rs b/mithril-aggregator/src/dependency_injection/containers.rs index 8c4e64c06bb..35d1699cfe5 100644 --- a/mithril-aggregator/src/dependency_injection/containers.rs +++ b/mithril-aggregator/src/dependency_injection/containers.rs @@ -22,7 +22,10 @@ use crate::{ database::provider::{CertificateRepository, SignedEntityStorer, SignerGetter, StakePoolStore}, event_store::{EventMessage, TransmitterService}, multi_signer::MultiSigner, - services::{CertifierService, SignedEntityService, StakeDistributionService, TickerService}, + services::{ + CertifierService, ProverService, SignedEntityService, StakeDistributionService, + TickerService, + }, signer_registerer::SignerRecorder, snapshot_uploaders::SnapshotUploader, CertificatePendingStore, ProtocolParametersStorer, SignerRegisterer, @@ -151,6 +154,9 @@ pub struct DependencyContainer { /// HTTP message service pub message_service: Arc, + + /// Prover service + pub prover_service: Arc, } #[doc(hidden)] diff --git a/mithril-aggregator/src/http_server/routes/middlewares.rs b/mithril-aggregator/src/http_server/routes/middlewares.rs index b3d6e5ed4c0..7727e210deb 100644 --- a/mithril-aggregator/src/http_server/routes/middlewares.rs +++ b/mithril-aggregator/src/http_server/routes/middlewares.rs @@ -2,7 +2,9 @@ use crate::{ database::provider::SignerGetter, dependency_injection::EpochServiceWrapper, event_store::{EventMessage, TransmitterService}, - services::{CertifierService, MessageService, SignedEntityService, TickerService}, + services::{ + CertifierService, MessageService, ProverService, SignedEntityService, TickerService, + }, CertificatePendingStore, Configuration, DependencyContainer, SignerRegisterer, VerificationKeyStorer, }; @@ -102,3 +104,10 @@ pub fn with_http_message_service( ) -> impl Filter,), Error = Infallible> + Clone { warp::any().map(move || dependency_manager.message_service.clone()) } + +/// With Prover service +pub fn with_prover_service( + dependency_manager: Arc, +) -> impl Filter,), Error = Infallible> + Clone { + warp::any().map(move || dependency_manager.prover_service.clone()) +} diff --git a/mithril-aggregator/src/http_server/routes/mod.rs b/mithril-aggregator/src/http_server/routes/mod.rs index 42f935091d3..50b54fcc00d 100644 --- a/mithril-aggregator/src/http_server/routes/mod.rs +++ b/mithril-aggregator/src/http_server/routes/mod.rs @@ -2,6 +2,7 @@ mod artifact_routes; mod certificate_routes; mod epoch_routes; mod middlewares; +mod proof_routes; pub(crate) mod reply; mod root_routes; pub mod router; diff --git a/mithril-aggregator/src/http_server/routes/proof_routes.rs b/mithril-aggregator/src/http_server/routes/proof_routes.rs new file mode 100644 index 00000000000..21e8239306d --- /dev/null +++ b/mithril-aggregator/src/http_server/routes/proof_routes.rs @@ -0,0 +1,169 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use warp::Filter; + +use crate::http_server::routes::middlewares; +use crate::DependencyContainer; + +#[derive(Deserialize, Serialize, Debug)] +struct CardanoTransactionProofQueryParams { + transaction_hashes: String, +} + +impl CardanoTransactionProofQueryParams { + pub fn split_transactions_hashes(&self) -> Vec<&str> { + self.transaction_hashes.split(',').collect() + } +} + +pub fn routes( + dependency_manager: Arc, +) -> impl Filter + Clone { + proof_cardano_transaction(dependency_manager) +} + +/// GET /proof/cardano-transaction +fn proof_cardano_transaction( + dependency_manager: Arc, +) -> impl Filter + Clone { + warp::path!("proof" / "cardano-transaction") + .and(warp::get()) + .and(warp::query::()) + .and(middlewares::with_prover_service(dependency_manager)) + .and_then(handlers::proof_cardano_transaction) +} + +mod handlers { + use std::{convert::Infallible, sync::Arc}; + + use reqwest::StatusCode; + use slog_scope::{debug, warn}; + + use crate::{ + http_server::routes::reply, message_adapters::ToCardanoTransactionsProofsMessageAdapter, + services::ProverService, + }; + + use super::CardanoTransactionProofQueryParams; + + pub async fn proof_cardano_transaction( + transaction_parameters: CardanoTransactionProofQueryParams, + prover_service: Arc, + ) -> Result { + let transaction_hashes = transaction_parameters + .split_transactions_hashes() + .iter() + .map(|s| s.to_string()) + .collect::>(); + debug!( + "⇄ HTTP SERVER: proof_cardano_transaction?transaction_hashes={}", + transaction_parameters.transaction_hashes + ); + + match prover_service + .compute_transactions_proofs(transaction_hashes.as_slice()) + .await + { + Ok(transactions_set_proofs) => Ok(reply::json( + &ToCardanoTransactionsProofsMessageAdapter::adapt( + transactions_set_proofs, + transaction_hashes, + ), + StatusCode::OK, + )), + Err(err) => { + warn!("proof_cardano_transaction::error"; "error" => ?err); + Ok(reply::internal_server_error(err)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use mithril_common::test_utils::apispec::APISpec; + + use anyhow::anyhow; + use serde_json::Value::Null; + use warp::{http::Method, test::request}; + + use crate::{ + dependency_injection::DependenciesBuilder, http_server::SERVER_BASE_PATH, + services::MockProverService, Configuration, + }; + + fn setup_router( + dependency_manager: Arc, + ) -> impl Filter + Clone { + let cors = warp::cors() + .allow_any_origin() + .allow_headers(vec!["content-type"]) + .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS]); + + warp::any() + .and(warp::path(SERVER_BASE_PATH)) + .and(routes(dependency_manager).with(cors)) + } + + #[tokio::test] + async fn proof_cardano_transaction_ok() { + let config = Configuration::new_sample(); + let mut builder = DependenciesBuilder::new(config); + let dependency_manager = builder.build_dependency_container().await.unwrap(); + + let method = Method::GET.as_str(); + let path = "/proof/cardano-transaction"; + + let response = request() + .method(method) + .path(&format!( + "/{SERVER_BASE_PATH}{path}?transaction_hashes=tx-123,tx-456" + )) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + ); + } + + #[tokio::test] + async fn proof_cardano_transaction_ko() { + let config = Configuration::new_sample(); + let mut builder = DependenciesBuilder::new(config); + let mut dependency_manager = builder.build_dependency_container().await.unwrap(); + let mut mock_prover_service = MockProverService::new(); + mock_prover_service + .expect_compute_transactions_proofs() + .returning(|_| Err(anyhow!("Error"))) + .times(1); + dependency_manager.prover_service = Arc::new(mock_prover_service); + + let method = Method::GET.as_str(); + let path = "/proof/cardano-transaction"; + + let response = request() + .method(method) + .path(&format!( + "/{SERVER_BASE_PATH}{path}?transaction_hashes=tx-123,tx-456" + )) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + ); + } +} diff --git a/mithril-aggregator/src/http_server/routes/router.rs b/mithril-aggregator/src/http_server/routes/router.rs index 4b0ad3869af..9c4fded4520 100644 --- a/mithril-aggregator/src/http_server/routes/router.rs +++ b/mithril-aggregator/src/http_server/routes/router.rs @@ -15,7 +15,7 @@ use warp::http::Method; use warp::reject::Reject; use warp::{Filter, Rejection, Reply}; -use super::middlewares; +use super::{middlewares, proof_routes}; #[derive(Debug)] pub struct VersionMismatchError; @@ -49,6 +49,7 @@ pub fn routes( .or(artifact_routes::mithril_stake_distribution::routes( dependency_manager.clone(), )) + .or(proof_routes::routes(dependency_manager.clone())) .or(signer_routes::routes(dependency_manager.clone())) .or(signatures_routes::routes(dependency_manager.clone())) .or(epoch_routes::routes(dependency_manager.clone())) diff --git a/mithril-aggregator/src/message_adapters/mod.rs b/mithril-aggregator/src/message_adapters/mod.rs index cb734c8edda..13b110c5b6d 100644 --- a/mithril-aggregator/src/message_adapters/mod.rs +++ b/mithril-aggregator/src/message_adapters/mod.rs @@ -1,5 +1,6 @@ mod from_register_signature; mod from_register_signer; +mod to_cardano_transactions_proof_message; mod to_certificate_pending_message; mod to_epoch_settings_message; mod to_mithril_stake_distribution_list_message; @@ -9,6 +10,7 @@ mod to_snapshot_message; pub use from_register_signature::FromRegisterSingleSignatureAdapter; pub use from_register_signer::FromRegisterSignerAdapter; +pub use to_cardano_transactions_proof_message::ToCardanoTransactionsProofsMessageAdapter; pub use to_certificate_pending_message::ToCertificatePendingMessageAdapter; pub use to_epoch_settings_message::ToEpochSettingsMessageAdapter; #[cfg(test)] diff --git a/mithril-aggregator/src/message_adapters/to_cardano_transactions_proof_message.rs b/mithril-aggregator/src/message_adapters/to_cardano_transactions_proof_message.rs new file mode 100644 index 00000000000..729593c5629 --- /dev/null +++ b/mithril-aggregator/src/message_adapters/to_cardano_transactions_proof_message.rs @@ -0,0 +1,88 @@ +use mithril_common::{ + entities::{CardanoTransactionsSetProof, TransactionHash}, + messages::CardanoTransactionsProofsMessage, +}; + +/// Adapter to spawn [CardanoTransactionsProofsMessage] from [CardanoTransactionsProofs] instances. +pub struct ToCardanoTransactionsProofsMessageAdapter; + +impl ToCardanoTransactionsProofsMessageAdapter { + /// Turn an entity instance into message. + pub fn adapt( + transactions_set_proofs: Vec, + transaction_hashes_to_certify: Vec, + ) -> CardanoTransactionsProofsMessage { + let transactions_hashes_certified = transactions_set_proofs + .iter() + .flat_map(|proof| proof.transactions_hashes().to_vec()) + .collect::>(); + let transactions_hashes_not_certified = transaction_hashes_to_certify + .iter() + .filter(|hash| !transactions_hashes_certified.contains(hash)) + .cloned() + .collect::>(); + CardanoTransactionsProofsMessage::new( + transactions_set_proofs, + transactions_hashes_not_certified, + ) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Context; + use mithril_common::{ + crypto_helper::{MKProof, MKTree, MKTreeNode, MKTreeStore}, + StdResult, + }; + + use super::*; + + fn build_proof(leaves: &[MKTreeNode]) -> StdResult { + let store = MKTreeStore::default(); + let mktree = + MKTree::new(leaves, &store).with_context(|| "MKTree creation should not fail")?; + mktree.compute_proof(leaves) + } + + #[test] + fn test_simple_message() { + let transaction_hashes = &[ + "tx-1".to_string(), + "tx-2".to_string(), + "tx-3".to_string(), + "tx-4".to_string(), + "tx-5".to_string(), + "tx-6".to_string(), + "tx-7".to_string(), + ]; + let transactions_hashes_certified = &transaction_hashes[0..5]; + let transactions_hashes_non_certified = &transaction_hashes[5..]; + + let mut transactions_set_proofs = Vec::new(); + for transaction_hashes_in_chunk in transactions_hashes_certified.chunks(2) { + let mk_proof = build_proof( + transaction_hashes_in_chunk + .iter() + .map(|h| h.to_owned().into()) + .collect::>() + .as_slice(), + ) + .unwrap(); + transactions_set_proofs.push(CardanoTransactionsSetProof::new( + transaction_hashes_in_chunk.to_vec(), + mk_proof, + )) + } + + let message = ToCardanoTransactionsProofsMessageAdapter::adapt( + transactions_set_proofs.clone(), + transaction_hashes.to_vec(), + ); + let expected_message = CardanoTransactionsProofsMessage::new( + transactions_set_proofs, + transactions_hashes_non_certified.to_vec(), + ); + assert_eq!(expected_message, message); + } +} diff --git a/mithril-aggregator/src/services/mod.rs b/mithril-aggregator/src/services/mod.rs index 1b9173cb5f0..2891f6e6143 100644 --- a/mithril-aggregator/src/services/mod.rs +++ b/mithril-aggregator/src/services/mod.rs @@ -12,6 +12,7 @@ mod certifier; mod epoch_service; mod message; +mod prover; mod signed_entity; mod stake_distribution; mod ticker; @@ -19,6 +20,7 @@ mod ticker; pub use certifier::*; pub use epoch_service::*; pub use message::*; +pub use prover::*; pub use signed_entity::*; pub use stake_distribution::*; pub use ticker::*; diff --git a/mithril-aggregator/src/services/prover.rs b/mithril-aggregator/src/services/prover.rs new file mode 100644 index 00000000000..eb49b592a9d --- /dev/null +++ b/mithril-aggregator/src/services/prover.rs @@ -0,0 +1,184 @@ +use std::sync::Arc; + +use anyhow::Context; +use async_trait::async_trait; + +use mithril_common::{ + crypto_helper::{MKTree, MKTreeNode, MKTreeStore}, + entities::{CardanoTransaction, CardanoTransactionsSetProof, TransactionHash}, + StdResult, +}; + +#[cfg(test)] +use mockall::automock; + +/// Prover service is the cryptographic engine in charge of producing cryptographic proofs for transactions +#[cfg_attr(test, automock)] +#[async_trait] +pub trait ProverService: Sync + Send { + /// Compute the cryptographic proofs for the given transactions + async fn compute_transactions_proofs( + &self, + transaction_hashes: &[TransactionHash], + ) -> StdResult>; +} + +/// Transactions retriever +#[cfg_attr(test, automock)] +#[async_trait] +pub trait TransactionsRetriever: Sync + Send { + /// Get all transactions + async fn get_all(&self) -> StdResult>; +} + +/// Mithril prover +pub struct MithrilProverService { + transaction_retriever: Arc, +} + +impl MithrilProverService { + /// Create a new Mithril prover + pub fn new(transaction_retriever: Arc) -> Self { + Self { + transaction_retriever, + } + } +} + +#[async_trait] +impl ProverService for MithrilProverService { + async fn compute_transactions_proofs( + &self, + transaction_hashes: &[TransactionHash], + ) -> StdResult> { + let transactions = self.transaction_retriever.get_all().await?; + let mk_leaves_all: Vec = + transactions.iter().map(|t| t.to_owned().into()).collect(); + let store = MKTreeStore::default(); + let mktree = MKTree::new(&mk_leaves_all, &store) + .with_context(|| "MKTree creation should not fail")?; + + let mut transaction_hashes_certified = vec![]; + for transaction_hash in transaction_hashes { + let mk_leaf = transaction_hash.to_string().into(); + if mktree.compute_proof(&[mk_leaf]).is_ok() { + transaction_hashes_certified.push(transaction_hash.to_string()); + } + } + + if !transaction_hashes_certified.is_empty() { + let mk_leaves: Vec = transaction_hashes_certified + .iter() + .map(|h| h.to_owned().into()) + .collect(); + let mk_proof = mktree.compute_proof(&mk_leaves)?; + let transactions_set_proof_batch = + CardanoTransactionsSetProof::new(transaction_hashes_certified, mk_proof); + + Ok(vec![transactions_set_proof_batch]) + } else { + Ok(vec![]) + } + } +} + +#[cfg(test)] +mod tests { + use anyhow::anyhow; + use mithril_common::entities::CardanoTransaction; + + use super::*; + + fn generate_transactions( + total_transactions: usize, + ) -> (Vec, Vec) { + let mut hashes = vec![]; + let mut transactions = vec![]; + + for i in 1..=total_transactions { + let hash = format!("tx-{}", i); + transactions.push(CardanoTransaction::new(&hash, 10 * i as u64, i as u64)); + hashes.push(hash); + } + + (hashes, transactions) + } + + #[tokio::test] + async fn compute_proof_for_one_set_with_multiple_transactions() { + let (transaction_hashes, transactions) = generate_transactions(3); + let mut transaction_retriever = MockTransactionsRetriever::new(); + transaction_retriever + .expect_get_all() + .return_once(move || Ok(transactions)); + let prover = MithrilProverService::new(Arc::new(transaction_retriever)); + let transactions_set_proof = prover + .compute_transactions_proofs(&transaction_hashes) + .await + .unwrap(); + + assert_eq!(transactions_set_proof.len(), 1); + assert_eq!( + transactions_set_proof[0].transactions_hashes(), + transaction_hashes + ); + transactions_set_proof[0].verify().unwrap(); + } + + #[tokio::test] + async fn cant_compute_proof_for_unknown_transaction() { + let (transaction_hashes, _transactions) = generate_transactions(3); + let mut transaction_retriever = MockTransactionsRetriever::new(); + transaction_retriever + .expect_get_all() + .returning(|| Ok(vec![])); + let prover = MithrilProverService::new(Arc::new(transaction_retriever)); + let transactions_set_proof = prover + .compute_transactions_proofs(&transaction_hashes) + .await + .unwrap(); + + assert_eq!(transactions_set_proof.len(), 0); + } + + #[tokio::test] + async fn compute_proof_for_one_set_of_three_known_transactions_and_two_unkwnows() { + let (transaction_hashes, transactions) = generate_transactions(5); + // The last two are not in the "store" + let transactions = transactions[0..=2].to_vec(); + let mut transaction_retriever = MockTransactionsRetriever::new(); + transaction_retriever + .expect_get_all() + .return_once(move || Ok(transactions)); + let prover = MithrilProverService::new(Arc::new(transaction_retriever)); + let transactions_set_proof = prover + .compute_transactions_proofs(&transaction_hashes) + .await + .unwrap(); + + assert_eq!(transactions_set_proof.len(), 1); + assert_eq!( + transactions_set_proof[0].transactions_hashes(), + &transaction_hashes[0..=2].to_vec() + ); + transactions_set_proof[0].verify().unwrap(); + } + + // this one can't be done right now because we don't have a merkle tree of merkle tree yet + // todo: compute_proof_for_multiple_set_with_multiple_transactions + + #[tokio::test] + async fn cant_compute_proof_if_retriever_fail() { + let (transaction_hashes, _transactions) = generate_transactions(3); + let mut transaction_retriever = MockTransactionsRetriever::new(); + transaction_retriever + .expect_get_all() + .returning(|| Err(anyhow!("Error"))); + + let prover = MithrilProverService::new(Arc::new(transaction_retriever)); + prover + .compute_transactions_proofs(&transaction_hashes) + .await + .expect_err("Should have failed because of its retriever"); + } +} diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 5314ea24f8f..94c0d75bd94 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-common" -version = "0.2.156" +version = "0.2.157" description = "Common types, interfaces, and utilities for Mithril nodes." authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-common/src/crypto_helper/merkle_tree.rs b/mithril-common/src/crypto_helper/merkle_tree.rs index fcc29832042..e25783bc4ca 100644 --- a/mithril-common/src/crypto_helper/merkle_tree.rs +++ b/mithril-common/src/crypto_helper/merkle_tree.rs @@ -89,7 +89,7 @@ impl Merge for MergeMKTreeNode { } /// A Merkle proof -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct MKProof { inner_root: MKTreeNode, inner_leaves: Vec<(MKTreeLeafPosition, MKTreeNode)>, @@ -108,6 +108,15 @@ impl MKProof { .then_some(()) .ok_or(anyhow!("Invalid MKProof")) } + + /// Check if the proof contains the given leaves + pub fn contains(&self, leaves: &[MKTreeNode]) -> StdResult<()> { + leaves + .iter() + .all(|leaf| self.inner_leaves.iter().any(|(_, l)| l == leaf)) + .then_some(()) + .ok_or(anyhow!("Leaves not found in the MKProof")) + } } /// A Merkle tree store @@ -175,8 +184,23 @@ impl<'a> MKTree<'a> { #[cfg(test)] mod tests { + use anyhow::Context; + use super::*; + fn generate_leaves(total_leaves: usize) -> Vec { + (0..total_leaves) + .map(|i| format!("test-{i}").into()) + .collect() + } + + fn build_proof(leaves: &[MKTreeNode]) -> StdResult { + let store = MKTreeStore::default(); + let mktree = + MKTree::new(leaves, &store).with_context(|| "MKTree creation should not fail")?; + mktree.compute_proof(leaves) + } + #[test] fn test_golden_merkle_root() { let leaves = vec!["golden-1", "golden-2", "golden-3", "golden-4", "golden-5"]; @@ -194,31 +218,17 @@ mod tests { #[test] fn test_should_accept_valid_proof_generated_by_merkle_tree() { - let total_leaves = 100000; - let leaves = (0..total_leaves) - .map(|i| format!("test-{i}").into()) - .collect::>(); - let store = MKTreeStore::default(); - let mktree = MKTree::new(&leaves, &store).expect("MKTree creation should not fail"); + let leaves = generate_leaves(100000); let leaves_to_verify = &[leaves[0].to_owned(), leaves[3].to_owned()]; - let proof = mktree - .compute_proof(leaves_to_verify) - .expect("MKProof generation should not fail"); + let proof = build_proof(leaves_to_verify).expect("MKProof generation should not fail"); proof.verify().expect("The MKProof should be valid"); } #[test] fn test_should_reject_invalid_proof_generated_by_merkle_tree() { - let total_leaves = 100000; - let leaves = (0..total_leaves) - .map(|i| format!("test-{i}").into()) - .collect::>(); - let store = MKTreeStore::default(); - let mktree = MKTree::new(&leaves, &store).expect("MKTree creation should not fail"); + let leaves = generate_leaves(100000); let leaves_to_verify = &[leaves[0].to_owned(), leaves[3].to_owned()]; - let mut proof = mktree - .compute_proof(leaves_to_verify) - .expect("MKProof generation should not fail"); + let mut proof = build_proof(leaves_to_verify).expect("MKProof generation should not fail"); proof.inner_root = leaves[10].to_owned(); proof.verify().expect_err("The MKProof should be invalid"); } @@ -233,4 +243,31 @@ mod tests { assert_eq!(node_str.to_string(), expected_str); assert_eq!(node_string.to_string(), expected_string); } + + #[test] + fn contains_leaves() { + let mut leaves_to_verify = generate_leaves(10); + let leaves_not_verified = leaves_to_verify.drain(3..6).collect::>(); + let proof = build_proof(&leaves_to_verify).expect("MKProof generation should not fail"); + + // contains everything + proof.contains(&leaves_to_verify).unwrap(); + + // contains subpart + proof.contains(&leaves_to_verify[0..2]).unwrap(); + + // don't contains all not verified + proof.contains(&leaves_not_verified).unwrap_err(); + + // don't contains subpart of not verified + proof.contains(&leaves_not_verified[1..2]).unwrap_err(); + + // fail if part verified and part unverified + proof + .contains(&[ + leaves_to_verify[2].to_owned(), + leaves_not_verified[0].to_owned(), + ]) + .unwrap_err(); + } } diff --git a/mithril-common/src/entities/cardano_transactions_set_proof.rs b/mithril-common/src/entities/cardano_transactions_set_proof.rs new file mode 100644 index 00000000000..71562de4ce6 --- /dev/null +++ b/mithril-common/src/entities/cardano_transactions_set_proof.rs @@ -0,0 +1,108 @@ +use crate::{crypto_helper::MKProof, entities::TransactionHash, StdResult}; + +use serde::{Deserialize, Serialize}; + +/// A cryptographic proof of a set of Cardano transactions is included in the global Cardano transactions set +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct CardanoTransactionsSetProof { + /// Hashes of the certified transactions + transactions_hashes: Vec, + + /// Proof of the transactions + transactions_proof: MKProof, +} + +impl CardanoTransactionsSetProof { + /// CardanoTransactionsSetProof factory + pub fn new(transactions_hashes: Vec, transactions_proof: MKProof) -> Self { + Self { + transactions_hashes, + transactions_proof, + } + } + + /// Get the hashes of the transactions certified by this proof + pub fn transactions_hashes(&self) -> &[TransactionHash] { + &self.transactions_hashes + } + + /// Verify that transactions set proof is valid + pub fn verify(&self) -> StdResult<()> { + self.transactions_proof.verify()?; + self.transactions_proof.contains( + self.transactions_hashes + .iter() + .map(|h| h.to_owned().into()) + .collect::>() + .as_slice(), + )?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Context; + + use crate::crypto_helper::{MKTree, MKTreeNode, MKTreeStore}; + + use super::*; + + fn build_proof + Clone>( + leaves: &[T], + leaves_to_verify: &[T], + ) -> StdResult { + let leaves = to_mknode(leaves); + let leaves_to_verify = to_mknode(leaves_to_verify); + + let store = MKTreeStore::default(); + let mktree = + MKTree::new(&leaves, &store).with_context(|| "MKTree creation should not fail")?; + mktree.compute_proof(&leaves_to_verify) + } + + fn to_mknode + Clone>(hashes: &[T]) -> Vec { + hashes.iter().map(|h| h.clone().into()).collect() + } + + #[test] + fn should_verify_where_all_hashes_are_contained_in_the_proof() { + let transaction_hashes = vec![ + "tx-1".to_string(), + "tx-2".to_string(), + "tx-3".to_string(), + "tx-4".to_string(), + "tx-5".to_string(), + ]; + let transaction_hashes_to_verify = &transaction_hashes[0..2]; + let transactions_proof = + build_proof(&transaction_hashes, transaction_hashes_to_verify).unwrap(); + + let proof = CardanoTransactionsSetProof::new( + transaction_hashes_to_verify.to_vec(), + transactions_proof, + ); + proof.verify().expect("The proof should be valid"); + } + + #[test] + fn shouldnt_verify_where_at_least_one_hash_is_not_contained_in_the_proof() { + let transaction_hashes = vec![ + "tx-1".to_string(), + "tx-2".to_string(), + "tx-3".to_string(), + "tx-4".to_string(), + "tx-5".to_string(), + ]; + let transactions_proof = + build_proof(&transaction_hashes, &transaction_hashes[0..2]).unwrap(); + let transaction_hashes_not_verified = &transaction_hashes[1..3]; + + let proof = CardanoTransactionsSetProof::new( + transaction_hashes_not_verified.to_vec(), + transactions_proof, + ); + proof.verify().expect_err("The proof should be invalid"); + } +} diff --git a/mithril-common/src/entities/mod.rs b/mithril-common/src/entities/mod.rs index f2d11bc785e..b5ae12d2ae5 100644 --- a/mithril-common/src/entities/mod.rs +++ b/mithril-common/src/entities/mod.rs @@ -4,6 +4,7 @@ mod beacon; mod cardano_network; mod cardano_transaction; mod cardano_transactions_commitment; +mod cardano_transactions_set_proof; mod certificate; mod certificate_metadata; mod certificate_pending; @@ -24,6 +25,7 @@ pub use beacon::{Beacon, BeaconComparison, BeaconComparisonError}; pub use cardano_network::CardanoNetwork; pub use cardano_transaction::{BlockNumber, CardanoTransaction, TransactionHash}; pub use cardano_transactions_commitment::CardanoTransactionsCommitment; +pub use cardano_transactions_set_proof::CardanoTransactionsSetProof; pub use certificate::{Certificate, CertificateSignature}; pub use certificate_metadata::{CertificateMetadata, StakeDistributionParty}; pub use certificate_pending::CertificatePending; diff --git a/mithril-common/src/messages/cardano_transactions_proof.rs b/mithril-common/src/messages/cardano_transactions_proof.rs new file mode 100644 index 00000000000..a85bb740747 --- /dev/null +++ b/mithril-common/src/messages/cardano_transactions_proof.rs @@ -0,0 +1,25 @@ +use crate::entities::{CardanoTransactionsSetProof, TransactionHash}; +use serde::{Deserialize, Serialize}; + +/// A cryptographic proof for a set of Cardano transactions +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct CardanoTransactionsProofsMessage { + /// Transactions that have been certified + certified_transactions: Vec, + + /// Transactions that could not be certified + non_certified_transactions: Vec, +} + +impl CardanoTransactionsProofsMessage { + /// Create a new `CardanoTransactionsProofsMessage` + pub fn new( + certified_transactions: Vec, + non_certified_transactions: Vec, + ) -> Self { + Self { + certified_transactions, + non_certified_transactions, + } + } +} diff --git a/mithril-common/src/messages/mod.rs b/mithril-common/src/messages/mod.rs index 214b18453f6..e6e69dd2c01 100644 --- a/mithril-common/src/messages/mod.rs +++ b/mithril-common/src/messages/mod.rs @@ -1,5 +1,6 @@ //! Messages module //! This module aims at providing shared structures for API communications. +mod cardano_transactions_proof; mod certificate; mod certificate_list; mod certificate_pending; @@ -14,6 +15,7 @@ mod snapshot; mod snapshot_download; mod snapshot_list; +pub use cardano_transactions_proof::CardanoTransactionsProofsMessage; pub use certificate::CertificateMessage; pub use certificate_list::{ CertificateListItemMessage, CertificateListItemMessageMetadata, CertificateListMessage, diff --git a/openapi.yaml b/openapi.yaml index 1657b33d139..3beb40ee31e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4,7 +4,7 @@ info: # `mithril-common/src/lib.rs` file. If you plan to update it # here to reflect changes in the API, please also update the constant in the # Rust file. - version: 0.1.13 + version: 0.1.14 title: Mithril Aggregator Server description: | The REST API provided by a Mithril Aggregator Node in a Mithril network. @@ -264,6 +264,39 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + + /proof/cardano-transaction: + get: + summary: Get the proofs of a Cardano transaction list + description: | + Returns the transaction hashes and the corresponding proofs + parameters: + - name: transaction_hashes + in: query + description: Hashes of the Cardano transactions to retrieve proofs for + required: true + schema: + type: array + items: + type: string + format: bytes + example: "6dbb104ed68481ef829a26a20142916d17985e01774d72d72c2f" + explode: false + responses: + "200": + description: Cardano transaction proofs found + content: + application/json: + schema: + $ref: "#/components/schemas/CardanoTransactionProofMessage" + "412": + description: API version mismatch + default: + description: Cardano transaction proofs retrieval error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /signers/registered/{epoch}: get: @@ -1388,6 +1421,48 @@ components: "protocol_parameters": { "k": 5, "m": 100, "phi_f": 0.65 } } + CardanoTransactionProofMessage: + description: This message represents proofs for Cardano Transactions. + type: object + additionalProperties: false + required: + - certified_transactions + - non_certified_transactions + properties: + certified_transactions: + description: Proofs for certified Cardano transactions + type: array + items: + type: object + required: + - transaction_hashes + - proof + properties: + transaction_hashes: + type: array + items: + description: Hash of the Cardano transactions + type: string + format: bytes + proof: + description: Proof for the Cardano transactions + type: string + format: bytes + non_certified_transactions: + type: array + items: + description: Hash of the non certified Cardano transactions + type: string + format: bytes + example: + { + "certified_transactions": [{ + "transaction_hashes": ["6367ee65d0d1272e6e70736a1ea2cae34015874517f6328364f6b73930966732", "5d0d1272e6e70736a1ea2cae34015876367ee64517f6328364f6b73930966732"], + "proof": "5b73136372c38302c37342c3136362c313535b5b323136362c313535b5b3232352c3230332c3235352c313030262c38322c39382c32c39332c3138342c313532352c3230332c3235352c313030262c33136362c313535b5b3232352c3230332c3235352c313030262c38322c39382c32c39332c3138342c31358322c39382c32c39332c3138342c3135362c3136362c32312c3131312c3232312c36332c3137372c3232332c3232332c31392c3537" + }], + "non_certified_transactions": ["732d0d1272e6e70736367ee6f6328364f6b739309666a1ea2cae34015874517"], + } + Error: description: Internal error representation type: object