From 74495dd062d8b205964331da631cde3dea6da573 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:50:46 +0200 Subject: [PATCH 01/17] feat: create `CardanoStakeDistributionMessage` Add test tooling for `CardanoStakeDistributionMessage` and `CardanoStakeDistribution` signed entity --- .../src/message_adapters/mod.rs | 3 + .../to_cardano_stake_distribution_message.rs | 46 +++++++++++ mithril-common/src/entities/signed_entity.rs | 18 +++++ .../messages/cardano_stake_distribution.rs | 81 +++++++++++++++++++ mithril-common/src/messages/mod.rs | 2 + mithril-common/src/test_utils/fake_data.rs | 15 +++- 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs create mode 100644 mithril-common/src/messages/cardano_stake_distribution.rs diff --git a/mithril-aggregator/src/message_adapters/mod.rs b/mithril-aggregator/src/message_adapters/mod.rs index 754055c9d46..6c1a4f4e049 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_stake_distribution_message; mod to_cardano_transaction_list_message; mod to_cardano_transaction_message; mod to_cardano_transactions_proof_message; @@ -13,6 +14,8 @@ mod to_snapshot_message; pub use from_register_signature::FromRegisterSingleSignatureAdapter; pub use from_register_signer::FromRegisterSignerAdapter; #[cfg(test)] +pub use to_cardano_stake_distribution_message::ToCardanoStakeDistributionMessageAdapter; +#[cfg(test)] pub use to_cardano_transaction_list_message::ToCardanoTransactionListMessageAdapter; #[cfg(test)] pub use to_cardano_transaction_message::ToCardanoTransactionMessageAdapter; diff --git a/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs new file mode 100644 index 00000000000..b383ff091ce --- /dev/null +++ b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs @@ -0,0 +1,46 @@ +use mithril_common::entities::{CardanoStakeDistribution, SignedEntity}; +use mithril_common::messages::{CardanoStakeDistributionMessage, ToMessageAdapter}; + +/// Adapter to convert [CardanoStakeDistribution] to [CardanoStakeDistributionMessage] instances +#[allow(dead_code)] +pub struct ToCardanoStakeDistributionMessageAdapter; + +impl ToMessageAdapter, CardanoStakeDistributionMessage> + for ToCardanoStakeDistributionMessageAdapter +{ + /// Method to trigger the conversion + fn adapt(from: SignedEntity) -> CardanoStakeDistributionMessage { + CardanoStakeDistributionMessage { + epoch: from.artifact.epoch, + hash: from.artifact.hash, + certificate_hash: from.certificate_id, + stake_distribution: from.artifact.stake_distribution, + created_at: from.created_at, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adapt_ok() { + let signed_entity = SignedEntity::::dummy(); + let cardano_stake_distribution_message_expected = CardanoStakeDistributionMessage { + epoch: signed_entity.artifact.epoch, + hash: signed_entity.artifact.hash.clone(), + certificate_hash: signed_entity.certificate_id.clone(), + stake_distribution: signed_entity.artifact.stake_distribution.clone(), + created_at: signed_entity.created_at, + }; + + let cardano_stake_distribution_message = + ToCardanoStakeDistributionMessageAdapter::adapt(signed_entity); + + assert_eq!( + cardano_stake_distribution_message_expected, + cardano_stake_distribution_message + ); + } +} diff --git a/mithril-common/src/entities/signed_entity.rs b/mithril-common/src/entities/signed_entity.rs index de499ee8c31..bcd96c83d48 100644 --- a/mithril-common/src/entities/signed_entity.rs +++ b/mithril-common/src/entities/signed_entity.rs @@ -7,6 +7,7 @@ use crate::signable_builder::Artifact; #[cfg(any(test, feature = "test_tools"))] use crate::test_utils::fake_data; +use super::CardanoStakeDistribution; #[cfg(any(test, feature = "test_tools"))] use super::{CardanoDbBeacon, Epoch}; @@ -83,3 +84,20 @@ impl SignedEntity { } } } + +impl SignedEntity { + cfg_test_tools! { + /// Create a dummy [SignedEntity] for [CardanoStakeDistribution] entity + pub fn dummy() -> Self { + SignedEntity { + signed_entity_id: "cardano-stake-distribution-id-123".to_string(), + signed_entity_type: SignedEntityType::CardanoStakeDistribution(Epoch(1)), + certificate_id: "certificate-hash-123".to_string(), + artifact: fake_data::cardano_stake_distributions(1)[0].to_owned(), + created_at: DateTime::parse_from_rfc3339("2024-07-29T16:15:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + } + } + } +} diff --git a/mithril-common/src/messages/cardano_stake_distribution.rs b/mithril-common/src/messages/cardano_stake_distribution.rs new file mode 100644 index 00000000000..0059b8a910e --- /dev/null +++ b/mithril-common/src/messages/cardano_stake_distribution.rs @@ -0,0 +1,81 @@ +use chrono::DateTime; +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +use crate::entities::Epoch; +use crate::entities::StakeDistribution; + +/// Message structure of a Cardano Stake Distribution +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct CardanoStakeDistributionMessage { + /// Epoch at the end of which the Cardano stake distribution is computed by the Cardano node + pub epoch: Epoch, + + /// Hash of the Cardano Stake Distribution + pub hash: String, + + /// Hash of the associated certificate + pub certificate_hash: String, + + /// Represents the list of participants in the Cardano chain with their associated stake + pub stake_distribution: StakeDistribution, + + /// DateTime of creation + pub created_at: DateTime, +} + +impl CardanoStakeDistributionMessage { + cfg_test_tools! { + /// Return a dummy test entity (test-only). + pub fn dummy() -> Self { + Self { + epoch: Epoch(1), + hash: "hash-123".to_string(), + certificate_hash: "cert-hash-123".to_string(), + stake_distribution: StakeDistribution::from([ + ("pool-123".to_string(), 1000), + ]), + created_at: DateTime::parse_from_rfc3339("2024-07-29T16:15:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn golden_message() -> CardanoStakeDistributionMessage { + CardanoStakeDistributionMessage { + epoch: Epoch(1), + hash: "hash-123".to_string(), + certificate_hash: "cert-hash-123".to_string(), + stake_distribution: StakeDistribution::from([ + ("pool-123".to_string(), 1000), + ("pool-456".to_string(), 2000), + ]), + created_at: DateTime::parse_from_rfc3339("2024-07-29T16:15:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + } + } + + // Test the backward compatibility with possible future upgrades. + #[test] + fn test_v1() { + let json = r#"{ + "epoch": 1, + "hash": "hash-123", + "certificate_hash": "cert-hash-123", + "stake_distribution": { "pool-123": 1000, "pool-456": 2000 }, + "created_at": "2024-07-29T16:15:05.618857482Z" + }"#; + let message: CardanoStakeDistributionMessage = serde_json::from_str(json).expect( + "This JSON is expected to be successfully parsed into a CardanoStakeDistributionMessage instance.", + ); + + assert_eq!(golden_message(), message); + } +} diff --git a/mithril-common/src/messages/mod.rs b/mithril-common/src/messages/mod.rs index 5346fa2daaa..0ff67ffd110 100644 --- a/mithril-common/src/messages/mod.rs +++ b/mithril-common/src/messages/mod.rs @@ -1,6 +1,7 @@ //! Messages module //! This module aims at providing shared structures for API communications. mod aggregator_features; +mod cardano_stake_distribution; mod cardano_transaction_snapshot; mod cardano_transaction_snapshot_list; mod cardano_transactions_proof; @@ -21,6 +22,7 @@ mod snapshot_list; pub use aggregator_features::{ AggregatorCapabilities, AggregatorFeaturesMessage, CardanoTransactionsProverCapabilities, }; +pub use cardano_stake_distribution::CardanoStakeDistributionMessage; pub use cardano_transaction_snapshot::CardanoTransactionSnapshotMessage; pub use cardano_transaction_snapshot_list::{ CardanoTransactionSnapshotListItemMessage, CardanoTransactionSnapshotListMessage, diff --git a/mithril-common/src/test_utils/fake_data.rs b/mithril-common/src/test_utils/fake_data.rs index ff28346d34a..1e0c2bf5db6 100644 --- a/mithril-common/src/test_utils/fake_data.rs +++ b/mithril-common/src/test_utils/fake_data.rs @@ -7,7 +7,7 @@ use crate::crypto_helper::{self, ProtocolMultiSignature}; use crate::entities::{ self, BlockNumber, CertificateMetadata, CertificateSignature, CompressionAlgorithm, Epoch, LotteryIndex, ProtocolMessage, ProtocolMessagePartKey, SignedEntityType, SingleSignatures, - SlotNumber, StakeDistributionParty, + SlotNumber, StakeDistribution, StakeDistributionParty, }; use crate::test_utils::MithrilFixtureBuilder; @@ -258,3 +258,16 @@ pub const fn transaction_hashes<'a>() -> [&'a str; 5] { "f4fd91dccc25fd63f2caebab3d3452bc4b2944fcc11652214a3e8f1d32b09713", ] } + +/// Fake Cardano Stake Distribution +pub fn cardano_stake_distributions(total: u64) -> Vec { + let stake_distribution = StakeDistribution::from([("pool-1".to_string(), 100)]); + + (1..total + 1) + .map(|epoch_idx| entities::CardanoStakeDistribution { + hash: format!("hash-epoch-{epoch_idx}"), + epoch: Epoch(epoch_idx), + stake_distribution: stake_distribution.clone(), + }) + .collect::>() +} From 54b8c2f05baaaac3f7c92e247fe4dd44e8f2478e Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:58:03 +0200 Subject: [PATCH 02/17] feat: implement `get_cardano_stake_distribution_message` for `MithrilMessageService` --- .../src/database/record/signed_entity.rs | 34 +++++++- mithril-aggregator/src/services/message.rs | 84 +++++++++++++++++-- 2 files changed, 106 insertions(+), 12 deletions(-) diff --git a/mithril-aggregator/src/database/record/signed_entity.rs b/mithril-aggregator/src/database/record/signed_entity.rs index 1b4773bc193..e6a1d83e16e 100644 --- a/mithril-aggregator/src/database/record/signed_entity.rs +++ b/mithril-aggregator/src/database/record/signed_entity.rs @@ -2,11 +2,14 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use mithril_common::crypto_helper::ProtocolParameters; -use mithril_common::entities::{BlockNumber, Epoch, SignedEntity, SignedEntityType, Snapshot}; +use mithril_common::entities::{ + BlockNumber, Epoch, SignedEntity, SignedEntityType, Snapshot, StakeDistribution, +}; use mithril_common::messages::{ - CardanoTransactionSnapshotListItemMessage, CardanoTransactionSnapshotMessage, - MithrilStakeDistributionListItemMessage, MithrilStakeDistributionMessage, - SignerWithStakeMessagePart, SnapshotListItemMessage, SnapshotMessage, + CardanoStakeDistributionMessage, CardanoTransactionSnapshotListItemMessage, + CardanoTransactionSnapshotMessage, MithrilStakeDistributionListItemMessage, + MithrilStakeDistributionMessage, SignerWithStakeMessagePart, SnapshotListItemMessage, + SnapshotMessage, }; use mithril_common::signable_builder::Artifact; use mithril_common::StdError; @@ -233,6 +236,29 @@ impl TryFrom for SnapshotListItemMessage { } } +impl TryFrom for CardanoStakeDistributionMessage { + type Error = StdError; + + fn try_from(value: SignedEntityRecord) -> Result { + #[derive(Deserialize)] + struct TmpCardanoStakeDistribution { + epoch: Epoch, + hash: String, + stake_distribution: StakeDistribution, + } + let artifact = serde_json::from_str::(&value.artifact)?; + let cardano_stake_distribution_message = CardanoStakeDistributionMessage { + epoch: artifact.epoch, + stake_distribution: artifact.stake_distribution, + hash: artifact.hash, + certificate_hash: value.certificate_id, + created_at: value.created_at, + }; + + Ok(cardano_stake_distribution_message) + } +} + impl SqLiteEntity for SignedEntityRecord { fn hydrate(row: sqlite::Row) -> Result where diff --git a/mithril-aggregator/src/services/message.rs b/mithril-aggregator/src/services/message.rs index 231aeab0810..b5d7c729dba 100644 --- a/mithril-aggregator/src/services/message.rs +++ b/mithril-aggregator/src/services/message.rs @@ -8,9 +8,10 @@ use thiserror::Error; use mithril_common::{ entities::SignedEntityTypeDiscriminants, messages::{ - CardanoTransactionSnapshotListMessage, CardanoTransactionSnapshotMessage, - CertificateListMessage, CertificateMessage, MithrilStakeDistributionListMessage, - MithrilStakeDistributionMessage, SnapshotListMessage, SnapshotMessage, + CardanoStakeDistributionMessage, CardanoTransactionSnapshotListMessage, + CardanoTransactionSnapshotMessage, CertificateListMessage, CertificateMessage, + MithrilStakeDistributionListMessage, MithrilStakeDistributionMessage, SnapshotListMessage, + SnapshotMessage, }, StdResult, }; @@ -74,6 +75,12 @@ pub trait MessageService: Sync + Send { &self, limit: usize, ) -> StdResult; + + /// Return the information regarding the Cardano stake distribution for the given identifier. + async fn get_cardano_stake_distribution_message( + &self, + signed_entity_id: &str, + ) -> StdResult>; } /// Implementation of the [MessageService] @@ -186,6 +193,18 @@ impl MessageService for MithrilMessageService { entities.into_iter().map(|i| i.try_into()).collect() } + + async fn get_cardano_stake_distribution_message( + &self, + signed_entity_id: &str, + ) -> StdResult> { + let signed_entity = self + .signed_entity_storer + .get_signed_entity(signed_entity_id) + .await?; + + signed_entity.map(|v| v.try_into()).transpose() + } } #[cfg(test)] @@ -193,8 +212,8 @@ mod tests { use std::sync::Arc; use mithril_common::entities::{ - CardanoTransactionsSnapshot, Certificate, Epoch, MithrilStakeDistribution, SignedEntity, - SignedEntityType, Snapshot, + CardanoStakeDistribution, CardanoTransactionsSnapshot, Certificate, Epoch, + MithrilStakeDistribution, SignedEntity, SignedEntityType, Snapshot, }; use mithril_common::messages::ToMessageAdapter; use mithril_common::test_utils::MithrilFixtureBuilder; @@ -203,9 +222,10 @@ mod tests { use crate::database::repository::MockSignedEntityStorer; use crate::dependency_injection::DependenciesBuilder; use crate::message_adapters::{ - ToCardanoTransactionListMessageAdapter, ToCardanoTransactionMessageAdapter, - ToMithrilStakeDistributionListMessageAdapter, ToMithrilStakeDistributionMessageAdapter, - ToSnapshotListMessageAdapter, ToSnapshotMessageAdapter, + ToCardanoStakeDistributionMessageAdapter, ToCardanoTransactionListMessageAdapter, + ToCardanoTransactionMessageAdapter, ToMithrilStakeDistributionListMessageAdapter, + ToMithrilStakeDistributionMessageAdapter, ToSnapshotListMessageAdapter, + ToSnapshotMessageAdapter, }; use crate::Configuration; @@ -499,4 +519,52 @@ mod tests { assert_eq!(message, response); } + + #[tokio::test] + async fn get_cardano_stake_distribution() { + let entity = SignedEntity::::dummy(); + let record = SignedEntityRecord { + signed_entity_id: entity.signed_entity_id.clone(), + signed_entity_type: SignedEntityType::CardanoStakeDistribution(entity.artifact.epoch), + certificate_id: entity.certificate_id.clone(), + artifact: serde_json::to_string(&entity.artifact).unwrap(), + created_at: entity.created_at, + }; + let message = ToCardanoStakeDistributionMessageAdapter::adapt(entity); + let configuration = Configuration::new_sample(); + let mut dep_builder = DependenciesBuilder::new(configuration); + let mut storer = MockSignedEntityStorer::new(); + storer + .expect_get_signed_entity() + .return_once(|_| Ok(Some(record))) + .once(); + dep_builder.signed_entity_storer = Some(Arc::new(storer)); + let service = dep_builder.get_message_service().await.unwrap(); + let response = service + .get_cardano_stake_distribution_message("whatever") + .await + .unwrap() + .expect("A CardanoStakeDistributionMessage was expected."); + + assert_eq!(message, response); + } + + #[tokio::test] + async fn get_cardano_stake_distribution_not_exist() { + let configuration = Configuration::new_sample(); + let mut dep_builder = DependenciesBuilder::new(configuration); + let mut storer = MockSignedEntityStorer::new(); + storer + .expect_get_signed_entity() + .return_once(|_| Ok(None)) + .once(); + dep_builder.signed_entity_storer = Some(Arc::new(storer)); + let service = dep_builder.get_message_service().await.unwrap(); + let response = service + .get_cardano_stake_distribution_message("whatever") + .await + .unwrap(); + + assert!(response.is_none()); + } } From 40d418b0daee0e4f8fee8932a19dc2f452ce6ac4 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:30:22 +0200 Subject: [PATCH 03/17] feat: create `CardanoStakeDistributionListItemMessage` --- .../src/message_adapters/mod.rs | 3 + ...cardano_stake_distribution_list_message.rs | 55 +++++++++++++++ .../cardano_stake_distribution_list.rs | 69 +++++++++++++++++++ mithril-common/src/messages/mod.rs | 4 ++ 4 files changed, 131 insertions(+) create mode 100644 mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs create mode 100644 mithril-common/src/messages/cardano_stake_distribution_list.rs diff --git a/mithril-aggregator/src/message_adapters/mod.rs b/mithril-aggregator/src/message_adapters/mod.rs index 6c1a4f4e049..4f6be088c5e 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_stake_distribution_list_message; mod to_cardano_stake_distribution_message; mod to_cardano_transaction_list_message; mod to_cardano_transaction_message; @@ -14,6 +15,8 @@ mod to_snapshot_message; pub use from_register_signature::FromRegisterSingleSignatureAdapter; pub use from_register_signer::FromRegisterSignerAdapter; #[cfg(test)] +pub use to_cardano_stake_distribution_list_message::ToCardanoStakeDistributionListMessageAdapter; +#[cfg(test)] pub use to_cardano_stake_distribution_message::ToCardanoStakeDistributionMessageAdapter; #[cfg(test)] pub use to_cardano_transaction_list_message::ToCardanoTransactionListMessageAdapter; diff --git a/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs new file mode 100644 index 00000000000..b8455c69868 --- /dev/null +++ b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs @@ -0,0 +1,55 @@ +use mithril_common::entities::{CardanoStakeDistribution, SignedEntity}; +use mithril_common::messages::{ + CardanoStakeDistributionListItemMessage, CardanoStakeDistributionListMessage, ToMessageAdapter, +}; + +/// Adapter to convert a list of [CardanoStakeDistribution] to [CardanoStakeDistributionListMessage] instances +#[allow(dead_code)] +pub struct ToCardanoStakeDistributionListMessageAdapter; + +impl + ToMessageAdapter< + Vec>, + CardanoStakeDistributionListMessage, + > for ToCardanoStakeDistributionListMessageAdapter +{ + /// Method to trigger the conversion + fn adapt( + snapshots: Vec>, + ) -> CardanoStakeDistributionListMessage { + snapshots + .into_iter() + .map(|entity| CardanoStakeDistributionListItemMessage { + epoch: entity.artifact.epoch, + hash: entity.artifact.hash, + certificate_hash: entity.certificate_id, + created_at: entity.created_at, + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adapt_ok() { + let signed_entity = SignedEntity::::dummy(); + let cardano_stake_distribution_list_message_expected = + vec![CardanoStakeDistributionListItemMessage { + epoch: signed_entity.artifact.epoch, + hash: signed_entity.artifact.hash.clone(), + certificate_hash: signed_entity.certificate_id.clone(), + created_at: signed_entity.created_at, + }]; + + let cardano_stake_distribution_list_message = + ToCardanoStakeDistributionListMessageAdapter::adapt(vec![signed_entity]); + + assert_eq!( + cardano_stake_distribution_list_message_expected, + cardano_stake_distribution_list_message + ); + } +} diff --git a/mithril-common/src/messages/cardano_stake_distribution_list.rs b/mithril-common/src/messages/cardano_stake_distribution_list.rs new file mode 100644 index 00000000000..55ea28b1252 --- /dev/null +++ b/mithril-common/src/messages/cardano_stake_distribution_list.rs @@ -0,0 +1,69 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::entities::Epoch; + +/// Message structure of a Cardano Stake Distribution list +pub type CardanoStakeDistributionListMessage = Vec; + +/// Message structure of a Cardano Stake Distribution list item +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct CardanoStakeDistributionListItemMessage { + /// Epoch at the end of which the Cardano stake distribution is computed by the Cardano node + pub epoch: Epoch, + + /// Hash of the Cardano Stake Distribution + pub hash: String, + + /// Hash of the associated certificate + pub certificate_hash: String, + + /// Date and time at which the Cardano Stake Distribution was created + pub created_at: DateTime, +} + +impl CardanoStakeDistributionListItemMessage { + /// Return a dummy test entity (test-only). + pub fn dummy() -> Self { + Self { + epoch: Epoch(1), + hash: "hash-123".to_string(), + certificate_hash: "certificate-hash-123".to_string(), + created_at: DateTime::parse_from_rfc3339("2024-07-29T16:15:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn golden_message() -> CardanoStakeDistributionListMessage { + vec![CardanoStakeDistributionListItemMessage { + epoch: Epoch(1), + hash: "hash-123".to_string(), + certificate_hash: "cert-hash-123".to_string(), + created_at: DateTime::parse_from_rfc3339("2024-07-29T16:15:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + }] + } + + // Test the backward compatibility with possible future upgrades. + #[test] + fn test_v1() { + let json = r#"[{ + "epoch": 1, + "hash": "hash-123", + "certificate_hash": "cert-hash-123", + "created_at": "2024-07-29T16:15:05.618857482Z" + }]"#; + let message: CardanoStakeDistributionListMessage = serde_json::from_str(json).expect( + "This JSON is expected to be successfully parsed into a CardanoStakeDistributionListMessage instance.", + ); + + assert_eq!(golden_message(), message); + } +} diff --git a/mithril-common/src/messages/mod.rs b/mithril-common/src/messages/mod.rs index 0ff67ffd110..31a0f8ac1ac 100644 --- a/mithril-common/src/messages/mod.rs +++ b/mithril-common/src/messages/mod.rs @@ -2,6 +2,7 @@ //! This module aims at providing shared structures for API communications. mod aggregator_features; mod cardano_stake_distribution; +mod cardano_stake_distribution_list; mod cardano_transaction_snapshot; mod cardano_transaction_snapshot_list; mod cardano_transactions_proof; @@ -23,6 +24,9 @@ pub use aggregator_features::{ AggregatorCapabilities, AggregatorFeaturesMessage, CardanoTransactionsProverCapabilities, }; pub use cardano_stake_distribution::CardanoStakeDistributionMessage; +pub use cardano_stake_distribution_list::{ + CardanoStakeDistributionListItemMessage, CardanoStakeDistributionListMessage, +}; pub use cardano_transaction_snapshot::CardanoTransactionSnapshotMessage; pub use cardano_transaction_snapshot_list::{ CardanoTransactionSnapshotListItemMessage, CardanoTransactionSnapshotListMessage, From 6a7ad54264155961803e21cd108222a43f1051b4 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:32:33 +0200 Subject: [PATCH 04/17] feat: implement `get_cardano_stake_distribution_list_message` for `MithrilMessageService` --- .../src/database/record/signed_entity.rs | 29 +++++++-- mithril-aggregator/src/services/message.rs | 63 ++++++++++++++++--- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/mithril-aggregator/src/database/record/signed_entity.rs b/mithril-aggregator/src/database/record/signed_entity.rs index e6a1d83e16e..a4e09815f52 100644 --- a/mithril-aggregator/src/database/record/signed_entity.rs +++ b/mithril-aggregator/src/database/record/signed_entity.rs @@ -6,10 +6,10 @@ use mithril_common::entities::{ BlockNumber, Epoch, SignedEntity, SignedEntityType, Snapshot, StakeDistribution, }; use mithril_common::messages::{ - CardanoStakeDistributionMessage, CardanoTransactionSnapshotListItemMessage, - CardanoTransactionSnapshotMessage, MithrilStakeDistributionListItemMessage, - MithrilStakeDistributionMessage, SignerWithStakeMessagePart, SnapshotListItemMessage, - SnapshotMessage, + CardanoStakeDistributionListItemMessage, CardanoStakeDistributionMessage, + CardanoTransactionSnapshotListItemMessage, CardanoTransactionSnapshotMessage, + MithrilStakeDistributionListItemMessage, MithrilStakeDistributionMessage, + SignerWithStakeMessagePart, SnapshotListItemMessage, SnapshotMessage, }; use mithril_common::signable_builder::Artifact; use mithril_common::StdError; @@ -259,6 +259,27 @@ impl TryFrom for CardanoStakeDistributionMessage { } } +impl TryFrom for CardanoStakeDistributionListItemMessage { + type Error = StdError; + + fn try_from(value: SignedEntityRecord) -> Result { + #[derive(Deserialize)] + struct TmpCardanoStakeDistribution { + epoch: Epoch, + hash: String, + } + let artifact = serde_json::from_str::(&value.artifact)?; + let message = CardanoStakeDistributionListItemMessage { + epoch: artifact.epoch, + hash: artifact.hash, + certificate_hash: value.certificate_id, + created_at: value.created_at, + }; + + Ok(message) + } +} + impl SqLiteEntity for SignedEntityRecord { fn hydrate(row: sqlite::Row) -> Result where diff --git a/mithril-aggregator/src/services/message.rs b/mithril-aggregator/src/services/message.rs index b5d7c729dba..726981954c6 100644 --- a/mithril-aggregator/src/services/message.rs +++ b/mithril-aggregator/src/services/message.rs @@ -8,10 +8,10 @@ use thiserror::Error; use mithril_common::{ entities::SignedEntityTypeDiscriminants, messages::{ - CardanoStakeDistributionMessage, CardanoTransactionSnapshotListMessage, - CardanoTransactionSnapshotMessage, CertificateListMessage, CertificateMessage, - MithrilStakeDistributionListMessage, MithrilStakeDistributionMessage, SnapshotListMessage, - SnapshotMessage, + CardanoStakeDistributionListMessage, CardanoStakeDistributionMessage, + CardanoTransactionSnapshotListMessage, CardanoTransactionSnapshotMessage, + CertificateListMessage, CertificateMessage, MithrilStakeDistributionListMessage, + MithrilStakeDistributionMessage, SnapshotListMessage, SnapshotMessage, }, StdResult, }; @@ -81,6 +81,12 @@ pub trait MessageService: Sync + Send { &self, signed_entity_id: &str, ) -> StdResult>; + + /// Return the list of the last Cardano stake distributions message + async fn get_cardano_stake_distribution_list_message( + &self, + limit: usize, + ) -> StdResult; } /// Implementation of the [MessageService] @@ -205,6 +211,19 @@ impl MessageService for MithrilMessageService { signed_entity.map(|v| v.try_into()).transpose() } + + async fn get_cardano_stake_distribution_list_message( + &self, + limit: usize, + ) -> StdResult { + let signed_entity_type_id = SignedEntityTypeDiscriminants::CardanoStakeDistribution; + let entities = self + .signed_entity_storer + .get_last_signed_entities_by_type(&signed_entity_type_id, limit) + .await?; + + entities.into_iter().map(|i| i.try_into()).collect() + } } #[cfg(test)] @@ -222,10 +241,10 @@ mod tests { use crate::database::repository::MockSignedEntityStorer; use crate::dependency_injection::DependenciesBuilder; use crate::message_adapters::{ - ToCardanoStakeDistributionMessageAdapter, ToCardanoTransactionListMessageAdapter, - ToCardanoTransactionMessageAdapter, ToMithrilStakeDistributionListMessageAdapter, - ToMithrilStakeDistributionMessageAdapter, ToSnapshotListMessageAdapter, - ToSnapshotMessageAdapter, + ToCardanoStakeDistributionListMessageAdapter, ToCardanoStakeDistributionMessageAdapter, + ToCardanoTransactionListMessageAdapter, ToCardanoTransactionMessageAdapter, + ToMithrilStakeDistributionListMessageAdapter, ToMithrilStakeDistributionMessageAdapter, + ToSnapshotListMessageAdapter, ToSnapshotMessageAdapter, }; use crate::Configuration; @@ -567,4 +586,32 @@ mod tests { assert!(response.is_none()); } + + #[tokio::test] + async fn get_cardano_stake_distribution_list_message() { + let entity = SignedEntity::::dummy(); + let records = vec![SignedEntityRecord { + signed_entity_id: entity.signed_entity_id.clone(), + signed_entity_type: SignedEntityType::CardanoStakeDistribution(entity.artifact.epoch), + certificate_id: entity.certificate_id.clone(), + artifact: serde_json::to_string(&entity.artifact).unwrap(), + created_at: entity.created_at, + }]; + let message = ToCardanoStakeDistributionListMessageAdapter::adapt(vec![entity]); + let configuration = Configuration::new_sample(); + let mut dep_builder = DependenciesBuilder::new(configuration); + let mut storer = MockSignedEntityStorer::new(); + storer + .expect_get_last_signed_entities_by_type() + .return_once(|_, _| Ok(records)) + .once(); + dep_builder.signed_entity_storer = Some(Arc::new(storer)); + let service = dep_builder.get_message_service().await.unwrap(); + let response = service + .get_cardano_stake_distribution_list_message(10) + .await + .unwrap(); + + assert_eq!(message, response); + } } From 5aaa62e5520233852c230d8320229bb1dcf1d32c Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:11:47 +0200 Subject: [PATCH 05/17] feat: add new query to get a signed entity by its inner epoch --- .../src/sqlite/condition.rs | 4 +- .../query/signed_entity/get_signed_entity.rs | 112 +++++++++++++++++- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/internal/mithril-persistence/src/sqlite/condition.rs b/internal/mithril-persistence/src/sqlite/condition.rs index c7a999b629a..851ca073e70 100644 --- a/internal/mithril-persistence/src/sqlite/condition.rs +++ b/internal/mithril-persistence/src/sqlite/condition.rs @@ -2,7 +2,7 @@ use sqlite::Value; use std::iter::repeat; /// Internal Boolean representation -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] enum BooleanCondition { /// Empty tree None, @@ -43,7 +43,7 @@ impl BooleanCondition { } /// Where condition builder. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct WhereCondition { /// Boolean condition internal tree condition: BooleanCondition, diff --git a/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs b/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs index 15dcaa03734..342c4ca3156 100644 --- a/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs +++ b/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs @@ -1,12 +1,13 @@ use sqlite::Value; -use mithril_common::entities::SignedEntityTypeDiscriminants; +use mithril_common::entities::{Epoch, SignedEntityTypeDiscriminants}; use mithril_common::StdResult; use mithril_persistence::sqlite::{Query, SourceAlias, SqLiteEntity, WhereCondition}; use crate::database::record::SignedEntityRecord; /// Simple queries to retrieve [SignedEntityRecord] from the sqlite database. +#[derive(Debug, PartialEq)] pub struct GetSignedEntityRecordQuery { condition: WhereCondition, } @@ -60,6 +61,31 @@ impl GetSignedEntityRecordQuery { ), }) } + + pub fn by_signed_entity_type_and_epoch( + signed_entity_type: &SignedEntityTypeDiscriminants, + epoch: Epoch, + ) -> Self { + let signed_entity_type_id: i64 = signed_entity_type.index() as i64; + let epoch = *epoch as i64; + + match signed_entity_type { + SignedEntityTypeDiscriminants::MithrilStakeDistribution + | SignedEntityTypeDiscriminants::CardanoStakeDistribution => Self { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and beacon = ?*", + vec![Value::Integer(signed_entity_type_id), Value::Integer(epoch)], + ), + }, + SignedEntityTypeDiscriminants::CardanoImmutableFilesFull + | SignedEntityTypeDiscriminants::CardanoTransactions => Self { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and json_extract(beacon, '$.epoch') = ?*", + vec![Value::Integer(signed_entity_type_id), Value::Integer(epoch)], + ), + }, + } + } } impl Query for GetSignedEntityRecordQuery { @@ -128,4 +154,88 @@ mod tests { signed_entity_records.iter().map(|c| c.to_owned()).collect(); assert_eq!(expected_signed_entity_records, signed_entity_records); } + + #[test] + fn by_signed_entity_type_and_epoch_with_mithril_stake_distribution() { + assert_eq!( + 0, + SignedEntityTypeDiscriminants::MithrilStakeDistribution.index() + ); + let expected = GetSignedEntityRecordQuery { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and beacon = ?*", + vec![Value::Integer(0), Value::Integer(4)], + ), + }; + + let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( + &SignedEntityTypeDiscriminants::MithrilStakeDistribution, + Epoch(4), + ); + + assert_eq!(expected, query); + } + + #[test] + fn by_signed_entity_type_and_epoch_with_cardano_stake_distribution() { + assert_eq!( + 1, + SignedEntityTypeDiscriminants::CardanoStakeDistribution.index() + ); + let expected = GetSignedEntityRecordQuery { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and beacon = ?*", + vec![Value::Integer(1), Value::Integer(4)], + ), + }; + + let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( + &SignedEntityTypeDiscriminants::CardanoStakeDistribution, + Epoch(4), + ); + + assert_eq!(expected, query); + } + + #[test] + fn by_signed_entity_type_and_epoch_with_cardano_immutable_files_full() { + assert_eq!( + 2, + SignedEntityTypeDiscriminants::CardanoImmutableFilesFull.index() + ); + let expected = GetSignedEntityRecordQuery { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and json_extract(beacon, '$.epoch') = ?*", + vec![Value::Integer(2), Value::Integer(4)], + ), + }; + + let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( + &SignedEntityTypeDiscriminants::CardanoImmutableFilesFull, + Epoch(4), + ); + + assert_eq!(expected, query); + } + + #[test] + fn by_signed_entity_type_and_epoch_with_cardano_transactions() { + assert_eq!( + 3, + SignedEntityTypeDiscriminants::CardanoTransactions.index() + ); + let expected = GetSignedEntityRecordQuery { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and json_extract(beacon, '$.epoch') = ?*", + vec![Value::Integer(3), Value::Integer(4)], + ), + }; + + let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( + &SignedEntityTypeDiscriminants::CardanoTransactions, + Epoch(4), + ); + + assert_eq!(expected, query); + } } From a3dcb032f5779fcecd83647241704e4a96c50638 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:14:13 +0200 Subject: [PATCH 06/17] feat: implement `get_cardano_stake_distribution_message_by_epoch` for `MithrilMessageService` --- .../src/database/record/signed_entity.rs | 20 +++ .../repository/signed_entity_store.rs | 114 +++++++++++++++++- mithril-aggregator/src/services/message.rs | 80 +++++++++++- 3 files changed, 210 insertions(+), 4 deletions(-) diff --git a/mithril-aggregator/src/database/record/signed_entity.rs b/mithril-aggregator/src/database/record/signed_entity.rs index a4e09815f52..b797bef94aa 100644 --- a/mithril-aggregator/src/database/record/signed_entity.rs +++ b/mithril-aggregator/src/database/record/signed_entity.rs @@ -2,6 +2,8 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use mithril_common::crypto_helper::ProtocolParameters; +#[cfg(test)] +use mithril_common::entities::CardanoStakeDistribution; use mithril_common::entities::{ BlockNumber, Epoch, SignedEntity, SignedEntityType, Snapshot, StakeDistribution, }; @@ -53,6 +55,24 @@ impl SignedEntityRecord { } } + pub(crate) fn from_cardano_stake_distribution( + cardano_stake_distribution: CardanoStakeDistribution, + ) -> Self { + let entity = serde_json::to_string(&cardano_stake_distribution).unwrap(); + + SignedEntityRecord { + signed_entity_id: cardano_stake_distribution.hash.clone(), + signed_entity_type: SignedEntityType::CardanoStakeDistribution( + cardano_stake_distribution.epoch, + ), + certificate_id: format!("certificate-{}", cardano_stake_distribution.hash), + artifact: entity, + created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + } + } + pub(crate) fn fake_records(number_if_records: usize) -> Vec { use mithril_common::test_utils::fake_data; diff --git a/mithril-aggregator/src/database/repository/signed_entity_store.rs b/mithril-aggregator/src/database/repository/signed_entity_store.rs index 777e444dcf2..5a3f7e56eae 100644 --- a/mithril-aggregator/src/database/repository/signed_entity_store.rs +++ b/mithril-aggregator/src/database/repository/signed_entity_store.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Context; use async_trait::async_trait; -use mithril_common::entities::SignedEntityTypeDiscriminants; +use mithril_common::entities::{Epoch, SignedEntityTypeDiscriminants}; use mithril_common::StdResult; use mithril_persistence::sqlite::{ConnectionExtensions, SqliteConnection}; @@ -44,6 +44,13 @@ pub trait SignedEntityStorer: Sync + Send { total: usize, ) -> StdResult>; + /// Get signed entities by signed entity type and epoch + async fn get_signed_entities_by_type_and_epoch( + &self, + signed_entity_type_id: &SignedEntityTypeDiscriminants, + epoch: Epoch, + ) -> StdResult>; + /// Perform an update for all the given signed entities. async fn update_signed_entities( &self, @@ -127,6 +134,18 @@ impl SignedEntityStorer for SignedEntityStore { Ok(signed_entities) } + async fn get_signed_entities_by_type_and_epoch( + &self, + signed_entity_type_id: &SignedEntityTypeDiscriminants, + epoch: Epoch, + ) -> StdResult> { + self.connection + .fetch_collect(GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( + signed_entity_type_id, + epoch, + )) + } + async fn update_signed_entities( &self, signed_entities: Vec, @@ -150,7 +169,11 @@ impl SignedEntityStorer for SignedEntityStore { #[cfg(test)] mod tests { - use mithril_common::entities::{MithrilStakeDistribution, SignedEntity, Snapshot}; + use chrono::DateTime; + use mithril_common::{ + entities::{Epoch, MithrilStakeDistribution, SignedEntity, Snapshot}, + test_utils::fake_data, + }; use crate::database::test_helper::{insert_signed_entities, main_db_connection}; @@ -310,4 +333,91 @@ mod tests { assert_eq!(records_to_update, updated_records); assert_eq!(expected_records, stored_records); } + + #[tokio::test] + async fn get_signed_entities_by_type_and_epoch_when_only_epoch_in_beacon() { + let mut cardano_stake_distributions = fake_data::cardano_stake_distributions(2); + let epoch_to_retrieve = cardano_stake_distributions[0].epoch; + cardano_stake_distributions[1].epoch = epoch_to_retrieve; + + let expected_records = cardano_stake_distributions + .iter() + .map(|cardano_stake_distribution| { + SignedEntityRecord::from_cardano_stake_distribution( + cardano_stake_distribution.clone(), + ) + }) + .collect::>(); + + let connection = main_db_connection().unwrap(); + insert_signed_entities(&connection, expected_records.clone()).unwrap(); + let store = SignedEntityStore::new(Arc::new(connection)); + + let records = store + .get_signed_entities_by_type_and_epoch( + &SignedEntityTypeDiscriminants::CardanoStakeDistribution, + epoch_to_retrieve, + ) + .await + .unwrap(); + + assert_eq!( + // Records are inserted older to earlier and queried the other way round + expected_records.into_iter().rev().collect::>(), + records + ); + } + + #[tokio::test] + async fn get_signed_entities_by_type_and_epoch_when_json_in_beacon() { + let mut snapshots = fake_data::snapshots(2); + let epoch_to_retrieve = snapshots[0].beacon.epoch; + snapshots[1].beacon.epoch = epoch_to_retrieve; + + let expected_records = snapshots + .iter() + .map(|snapshot| { + SignedEntityRecord::from_snapshot( + snapshot.clone(), + "whatever".to_string(), + DateTime::default(), + ) + }) + .collect::>(); + + let connection = main_db_connection().unwrap(); + insert_signed_entities(&connection, expected_records.clone()).unwrap(); + let store = SignedEntityStore::new(Arc::new(connection)); + + let records = store + .get_signed_entities_by_type_and_epoch( + &SignedEntityTypeDiscriminants::CardanoImmutableFilesFull, + epoch_to_retrieve, + ) + .await + .unwrap(); + + assert_eq!( + // Records are inserted older to earlier and queried the other way round + expected_records.into_iter().rev().collect::>(), + records + ); + } + + #[tokio::test] + async fn get_signed_entities_by_type_and_epoch_when_nothing_found() { + let epoch_to_retrieve = Epoch(4); + let connection = main_db_connection().unwrap(); + let store = SignedEntityStore::new(Arc::new(connection)); + + let record = store + .get_signed_entities_by_type_and_epoch( + &SignedEntityTypeDiscriminants::CardanoStakeDistribution, + epoch_to_retrieve, + ) + .await + .unwrap(); + + assert_eq!(record, vec![]); + } } diff --git a/mithril-aggregator/src/services/message.rs b/mithril-aggregator/src/services/message.rs index 726981954c6..e0df9da6478 100644 --- a/mithril-aggregator/src/services/message.rs +++ b/mithril-aggregator/src/services/message.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use thiserror::Error; use mithril_common::{ - entities::SignedEntityTypeDiscriminants, + entities::{Epoch, SignedEntityTypeDiscriminants}, messages::{ CardanoStakeDistributionListMessage, CardanoStakeDistributionMessage, CardanoTransactionSnapshotListMessage, CardanoTransactionSnapshotMessage, @@ -82,6 +82,12 @@ pub trait MessageService: Sync + Send { signed_entity_id: &str, ) -> StdResult>; + /// Return the information regarding the Cardano stake distribution for the given epoch. + async fn get_cardano_stake_distribution_message_by_epoch( + &self, + epoch: Epoch, + ) -> StdResult>; + /// Return the list of the last Cardano stake distributions message async fn get_cardano_stake_distribution_list_message( &self, @@ -212,6 +218,21 @@ impl MessageService for MithrilMessageService { signed_entity.map(|v| v.try_into()).transpose() } + async fn get_cardano_stake_distribution_message_by_epoch( + &self, + epoch: Epoch, + ) -> StdResult> { + let signed_entity_type_id = SignedEntityTypeDiscriminants::CardanoStakeDistribution; + let signed_entity = self + .signed_entity_storer + .get_signed_entities_by_type_and_epoch(&signed_entity_type_id, epoch) + .await? + .first() + .cloned(); + + signed_entity.map(|v| v.try_into()).transpose() + } + async fn get_cardano_stake_distribution_list_message( &self, limit: usize, @@ -232,7 +253,8 @@ mod tests { use mithril_common::entities::{ CardanoStakeDistribution, CardanoTransactionsSnapshot, Certificate, Epoch, - MithrilStakeDistribution, SignedEntity, SignedEntityType, Snapshot, + MithrilStakeDistribution, SignedEntity, SignedEntityType, SignedEntityTypeDiscriminants, + Snapshot, }; use mithril_common::messages::ToMessageAdapter; use mithril_common::test_utils::MithrilFixtureBuilder; @@ -587,6 +609,60 @@ mod tests { assert!(response.is_none()); } + #[tokio::test] + async fn get_cardano_stake_distribution_by_epoch() { + let entity = SignedEntity::::dummy(); + let record = SignedEntityRecord { + signed_entity_id: entity.signed_entity_id.clone(), + signed_entity_type: SignedEntityType::CardanoStakeDistribution(entity.artifact.epoch), + certificate_id: entity.certificate_id.clone(), + artifact: serde_json::to_string(&entity.artifact).unwrap(), + created_at: entity.created_at, + }; + let message = ToCardanoStakeDistributionMessageAdapter::adapt(entity.clone()); + let configuration = Configuration::new_sample(); + let mut dep_builder = DependenciesBuilder::new(configuration); + let mut storer = MockSignedEntityStorer::new(); + storer + .expect_get_signed_entities_by_type_and_epoch() + .withf(|discriminant, _| { + discriminant == &SignedEntityTypeDiscriminants::CardanoStakeDistribution + }) + .return_once(|_, _| Ok(vec![record])) + .once(); + dep_builder.signed_entity_storer = Some(Arc::new(storer)); + let service = dep_builder.get_message_service().await.unwrap(); + let response = service + .get_cardano_stake_distribution_message_by_epoch(Epoch(999)) + .await + .unwrap() + .expect("A CardanoStakeDistributionMessage was expected."); + + assert_eq!(message, response); + } + + #[tokio::test] + async fn get_cardano_stake_distribution_by_epoch_not_exist() { + let configuration = Configuration::new_sample(); + let mut dep_builder = DependenciesBuilder::new(configuration); + let mut storer = MockSignedEntityStorer::new(); + storer + .expect_get_signed_entities_by_type_and_epoch() + .withf(|discriminant, _| { + discriminant == &SignedEntityTypeDiscriminants::CardanoStakeDistribution + }) + .return_once(|_, _| Ok(vec![])) + .once(); + dep_builder.signed_entity_storer = Some(Arc::new(storer)); + let service = dep_builder.get_message_service().await.unwrap(); + let response = service + .get_cardano_stake_distribution_message_by_epoch(Epoch(999)) + .await + .unwrap(); + + assert!(response.is_none()); + } + #[tokio::test] async fn get_cardano_stake_distribution_list_message() { let entity = SignedEntity::::dummy(); From 02cc0ead271e55aa47e50c2967e6c98c8b77f645 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:08:06 +0200 Subject: [PATCH 07/17] feat: implement routes and update OpenAPI specs --- .../cardano_stake_distribution.rs | 446 ++++++++++++++++++ .../http_server/routes/artifact_routes/mod.rs | 1 + .../src/http_server/routes/router.rs | 3 + openapi.yaml | 175 +++++++ 4 files changed, 625 insertions(+) create mode 100644 mithril-aggregator/src/http_server/routes/artifact_routes/cardano_stake_distribution.rs diff --git a/mithril-aggregator/src/http_server/routes/artifact_routes/cardano_stake_distribution.rs b/mithril-aggregator/src/http_server/routes/artifact_routes/cardano_stake_distribution.rs new file mode 100644 index 00000000000..de03fa2d466 --- /dev/null +++ b/mithril-aggregator/src/http_server/routes/artifact_routes/cardano_stake_distribution.rs @@ -0,0 +1,446 @@ +use crate::http_server::routes::middlewares; +use crate::DependencyContainer; +use std::sync::Arc; +use warp::Filter; + +pub fn routes( + dependency_manager: Arc, +) -> impl Filter + Clone { + artifact_cardano_stake_distributions(dependency_manager.clone()) + .or(artifact_cardano_stake_distribution_by_id( + dependency_manager.clone(), + )) + .or(artifact_cardano_stake_distribution_by_epoch( + dependency_manager, + )) +} + +/// GET /artifact/cardano-stake-distributions +fn artifact_cardano_stake_distributions( + dependency_manager: Arc, +) -> impl Filter + Clone { + warp::path!("artifact" / "cardano-stake-distributions") + .and(warp::get()) + .and(middlewares::with_http_message_service(dependency_manager)) + .and_then(handlers::list_artifacts) +} + +/// GET /artifact/cardano-stake-distribution/:id +fn artifact_cardano_stake_distribution_by_id( + dependency_manager: Arc, +) -> impl Filter + Clone { + warp::path!("artifact" / "cardano-stake-distribution" / String) + .and(warp::get()) + .and(middlewares::with_http_message_service(dependency_manager)) + .and_then(handlers::get_artifact_by_signed_entity_id) +} + +/// GET /artifact/cardano-stake-distribution/epoch/:epoch +fn artifact_cardano_stake_distribution_by_epoch( + dependency_manager: Arc, +) -> impl Filter + Clone { + warp::path!("artifact" / "cardano-stake-distribution" / "epoch" / String) + .and(warp::get()) + .and(middlewares::with_http_message_service(dependency_manager)) + .and_then(handlers::get_artifact_by_epoch) +} + +pub mod handlers { + use crate::http_server::routes::reply; + use crate::services::MessageService; + + use mithril_common::entities::Epoch; + use slog_scope::{debug, warn}; + use std::convert::Infallible; + use std::sync::Arc; + use warp::http::StatusCode; + + pub const LIST_MAX_ITEMS: usize = 20; + + /// List CardanoStakeDistribution artifacts + pub async fn list_artifacts( + http_message_service: Arc, + ) -> Result { + debug!("⇄ HTTP SERVER: artifacts"); + + match http_message_service + .get_cardano_stake_distribution_list_message(LIST_MAX_ITEMS) + .await + { + Ok(message) => Ok(reply::json(&message, StatusCode::OK)), + Err(err) => { + warn!("list_artifacts_cardano_stake_distribution"; "error" => ?err); + Ok(reply::server_error(err)) + } + } + } + + /// Get Artifact by signed entity id + pub async fn get_artifact_by_signed_entity_id( + signed_entity_id: String, + http_message_service: Arc, + ) -> Result { + debug!("⇄ HTTP SERVER: artifact/{signed_entity_id}"); + + match http_message_service + .get_cardano_stake_distribution_message(&signed_entity_id) + .await + { + Ok(Some(message)) => Ok(reply::json(&message, StatusCode::OK)), + Ok(None) => { + warn!("get_cardano_stake_distribution_details::not_found"); + Ok(reply::empty(StatusCode::NOT_FOUND)) + } + Err(err) => { + warn!("get_cardano_stake_distribution_details::error"; "error" => ?err); + Ok(reply::server_error(err)) + } + } + } + + /// Get Artifact by epoch + pub async fn get_artifact_by_epoch( + epoch: String, + http_message_service: Arc, + ) -> Result { + debug!("⇄ HTTP SERVER: artifact/epoch/{epoch}"); + + let artifact_epoch = match epoch.parse::() { + Ok(epoch) => Epoch(epoch), + Err(err) => { + warn!("get_artifact_by_epoch::invalid_epoch"; "error" => ?err); + return Ok(reply::bad_request( + "invalid_epoch".to_string(), + err.to_string(), + )); + } + }; + + match http_message_service + .get_cardano_stake_distribution_message_by_epoch(artifact_epoch) + .await + { + Ok(Some(message)) => Ok(reply::json(&message, StatusCode::OK)), + Ok(None) => { + warn!("get_cardano_stake_distribution_details_by_epoch::not_found"); + Ok(reply::empty(StatusCode::NOT_FOUND)) + } + Err(err) => { + warn!("get_cardano_stake_distribution_details_by_epoch::error"; "error" => ?err); + Ok(reply::server_error(err)) + } + } + } +} + +#[cfg(test)] +pub mod tests { + use anyhow::anyhow; + use serde_json::Value::Null; + use warp::{ + http::{Method, StatusCode}, + test::request, + }; + + use mithril_common::{ + messages::{CardanoStakeDistributionListItemMessage, CardanoStakeDistributionMessage}, + test_utils::apispec::APISpec, + }; + + use crate::{ + http_server::SERVER_BASE_PATH, initialize_dependencies, services::MockMessageService, + }; + + use super::*; + + 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 test_cardano_stake_distributions_returns_ok() { + let message = vec![CardanoStakeDistributionListItemMessage::dummy()]; + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_list_message() + .return_once(|_| Ok(message)) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let path = "/artifact/cardano-stake-distributions"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{path}")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + &StatusCode::OK, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distributions_returns_ko_500_when_error() { + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_list_message() + .return_once(|_| Err(anyhow!("an error occured"))) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let path = "/artifact/cardano-stake-distributions"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{path}")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + &StatusCode::INTERNAL_SERVER_ERROR, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_returns_ok() { + let message = CardanoStakeDistributionMessage::dummy(); + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_message() + .return_once(|_| Ok(Some(message))) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let path = "/artifact/cardano-stake-distribution/{hash}"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{path}")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + &StatusCode::OK, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_returns_404_not_found_when_no_record() { + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_message() + .return_once(|_| Ok(None)) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let path = "/artifact/cardano-stake-distribution/{hash}"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{path}")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + &StatusCode::NOT_FOUND, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_returns_ko_500_when_error() { + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_message() + .return_once(|_| Err(anyhow!("an error occured"))) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let path = "/artifact/cardano-stake-distribution/{hash}"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{path}")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + path, + "application/json", + &Null, + &response, + &StatusCode::INTERNAL_SERVER_ERROR, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_by_epoch_returns_ok() { + let message = CardanoStakeDistributionMessage::dummy(); + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_message_by_epoch() + .return_once(|_| Ok(Some(message))) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let base_path = "/artifact/cardano-stake-distribution/epoch"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{base_path}/123")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + &format!("{base_path}/{{epoch}}"), + "application/json", + &Null, + &response, + &StatusCode::OK, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_by_epoch_returns_400_bad_request_when_invalid_epoch() { + let mock_http_message_service = MockMessageService::new(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let base_path = "/artifact/cardano-stake-distribution/epoch"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{base_path}/invalid-epoch")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + &format!("{base_path}/{{epoch}}"), + "application/json", + &Null, + &response, + &StatusCode::BAD_REQUEST, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_by_epoch_returns_404_not_found_when_no_record() { + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_message_by_epoch() + .return_once(|_| Ok(None)) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let base_path = "/artifact/cardano-stake-distribution/epoch"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{base_path}/123")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + &format!("{base_path}/{{epoch}}"), + "application/json", + &Null, + &response, + &StatusCode::NOT_FOUND, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_cardano_stake_distribution_by_epoch_returns_ko_500_when_error() { + let mut mock_http_message_service = MockMessageService::new(); + mock_http_message_service + .expect_get_cardano_stake_distribution_message_by_epoch() + .return_once(|_| Err(anyhow!("an error occured"))) + .once(); + let mut dependency_manager = initialize_dependencies().await; + dependency_manager.message_service = Arc::new(mock_http_message_service); + + let method = Method::GET.as_str(); + let base_path = "/artifact/cardano-stake-distribution/epoch"; + + let response = request() + .method(method) + .path(&format!("/{SERVER_BASE_PATH}{base_path}/123")) + .reply(&setup_router(Arc::new(dependency_manager))) + .await; + + APISpec::verify_conformity( + APISpec::get_all_spec_files(), + method, + &format!("{base_path}/{{epoch}}"), + "application/json", + &Null, + &response, + &StatusCode::INTERNAL_SERVER_ERROR, + ) + .unwrap(); + } +} diff --git a/mithril-aggregator/src/http_server/routes/artifact_routes/mod.rs b/mithril-aggregator/src/http_server/routes/artifact_routes/mod.rs index 198ff6d1757..2d6b7b8a3ce 100644 --- a/mithril-aggregator/src/http_server/routes/artifact_routes/mod.rs +++ b/mithril-aggregator/src/http_server/routes/artifact_routes/mod.rs @@ -1,3 +1,4 @@ +pub mod cardano_stake_distribution; pub mod cardano_transaction; pub mod mithril_stake_distribution; pub mod snapshot; diff --git a/mithril-aggregator/src/http_server/routes/router.rs b/mithril-aggregator/src/http_server/routes/router.rs index 0854eb164f2..fcedfb2a06e 100644 --- a/mithril-aggregator/src/http_server/routes/router.rs +++ b/mithril-aggregator/src/http_server/routes/router.rs @@ -49,6 +49,9 @@ pub fn routes( .or(artifact_routes::mithril_stake_distribution::routes( dependency_manager.clone(), )) + .or(artifact_routes::cardano_stake_distribution::routes( + dependency_manager.clone(), + )) .or(artifact_routes::cardano_transaction::routes( dependency_manager.clone(), )) diff --git a/openapi.yaml b/openapi.yaml index 52093d0d2c7..2216333a451 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -290,6 +290,91 @@ paths: schema: $ref: "#/components/schemas/Error" + /artifact/cardano-stake-distributions: + get: + summary: Get most recent Cardano stake distributions + description: | + Returns the list of the most recent Cardano stake distributions + responses: + "200": + description: Cardano stake distribution found + content: + application/json: + schema: + $ref: "#/components/schemas/CardanoStakeDistributionListMessage" + "412": + description: API version mismatch + default: + description: Cardano stake distribution retrieval error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /artifact/cardano-stake-distribution/{hash}: + get: + summary: Get Cardano stake distribution information + description: | + Returns the information of a Cardano stake distribution + parameters: + - name: hash + in: path + description: Hash of the Cardano stake distribution to retrieve + required: true + schema: + type: string + format: bytes + example: "6da2b104ed68481ef829d72d72c2f6a20142916d17985e01774b14ed49f0fea1" + responses: + "200": + description: Cardano stake distribution found + content: + application/json: + schema: + $ref: "#/components/schemas/CardanoStakeDistributionMessage" + "404": + description: Cardano stake distribution not found + "412": + description: API version mismatch + default: + description: Cardano stake distribution retrieval error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /artifact/cardano-stake-distribution/epoch/{epoch}: + get: + summary: Get Cardano stake distribution information for a specific epoch + description: | + Returns the information of a Cardano stake distribution at a given epoch + parameters: + - name: epoch + in: path + description: Epoch of the Cardano stake distribution to retrieve + required: true + schema: + type: integer + format: int64 + example: 419 + responses: + "200": + description: Cardano stake distribution found + content: + application/json: + schema: + $ref: "#/components/schemas/CardanoStakeDistributionMessage" + "404": + description: Cardano stake distribution not found + "412": + description: API version mismatch + default: + description: Cardano stake distribution retrieval error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /artifact/cardano-transactions: get: summary: Get most recent Cardano transactions set snapshots @@ -1590,6 +1675,96 @@ components: "protocol_parameters": { "k": 5, "m": 100, "phi_f": 0.65 } } + StakeDistribution: + description: The list of Stake Pool Operator pool identifiers with their associated stake in the Cardano chain + properties: + code: + type: string + text: + type: integer + example: + { + "pool15ka28a4a3qxgcgh60wavkylku4vqjg385jezsrqxlafyrhahf02": 1192520901428, + "pool1aymf474uv528zafxlpfg3yr55zp267wj5mpu4qt557z5k5frn9p": 1009503382720 + } + + CardanoStakeDistributionListMessage: + description: CardanoStakeDistributionListMessage represents a list of Cardano stake distribution + type: array + items: + type: object + additionalProperties: false + required: + - epoch + - hash + - certificate_hash + - created_at + properties: + epoch: + $ref: "#/components/schemas/Epoch" + hash: + description: Hash of the Cardano stake distribution + type: string + format: bytes + certificate_hash: + description: Hash of the associated certificate + type: string + format: bytes + created_at: + description: Date and time at which the Cardano stake distribution was created + type: string + format: date-time, + example: + { + "epoch": 123, + "hash": "6367ee65d0d1272e6e70736a1ea2cae34015874517f6328364f6b73930966732", + "certificate_hash": "7905e83ab5d7bc082c1bbc3033bfd19c539078830d19080d1f241c70aa532572", + "created_at": "2022-06-14T10:52:31Z" + } + + CardanoStakeDistributionMessage: + description: This message represents a Cardano stake distribution. + type: object + additionalProperties: false + required: + - epoch + - hash + - certificate_hash + - stake_distribution + - created_at + properties: + epoch: + $ref: "#/components/schemas/Epoch" + hash: + description: Hash of the Cardano stake distribution + type: string + format: bytes + certificate_hash: + description: Hash of the associated certificate + type: string + format: bytes + stake_distribution: + description: The list of Stake Pool Operator pool identifiers with their associated stake in the Cardano chain + type: object + additionalProperties: + $ref: "#/components/schemas/StakeDistribution" + created_at: + description: Date and time of the entity creation + type: string + format: date-time, + example: + { + "epoch": 123, + "hash": "6367ee65d0d1272e6e70736a1ea2cae34015874517f6328364f6b73930966732", + "certificate_hash": "7905e83ab5d7bc082c1bbc3033bfd19c539078830d19080d1f241c70aa532572", + "stake_distribution": + { + "pool15ka28a4a3qxgcgh60wavkylku4vqjg385jezsrqxlafyrhahf02": 1192520901428, + "pool1aymf474uv528zafxlpfg3yr55zp267wj5mpu4qt557z5k5frn9p": 1009503382720 + }, + "created_at": "2022-06-14T10:52:31Z" + } + CardanoTransactionSnapshotListMessage: description: CardanoTransactionSnapshotListMessage represents a list of Cardano transactions set snapshots type: array From 0cc8ff6d8ad5eba4871f6f1717e0ebbc24a4fcf5 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:14:38 +0200 Subject: [PATCH 08/17] test: extend E2E test to check the `CardanoStakeDistribution` certification --- .../src/assertions/check.rs | 72 +++++++++++++++++++ .../mithril-end-to-end/src/end_to_end_spec.rs | 30 +++++++- .../src/mithril/infrastructure.rs | 10 +++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs b/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs index 68c2b6b1e2f..9c9f2b2c5cd 100644 --- a/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs +++ b/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs @@ -6,6 +6,7 @@ use anyhow::{anyhow, Context}; use mithril_common::{ entities::{Epoch, TransactionHash}, messages::{ + CardanoStakeDistributionListMessage, CardanoStakeDistributionMessage, CardanoTransactionSnapshotListMessage, CardanoTransactionSnapshotMessage, CertificateMessage, MithrilStakeDistributionListMessage, MithrilStakeDistributionMessage, SnapshotMessage, @@ -237,6 +238,77 @@ pub async fn assert_signer_is_signing_cardano_transactions( } } +pub async fn assert_node_producing_cardano_stake_distribution( + aggregator_endpoint: &str, +) -> StdResult { + let url = format!("{aggregator_endpoint}/artifact/cardano-stake-distributions"); + info!("Waiting for the aggregator to produce a Cardano stake distribution"); + + match attempt!(45, Duration::from_millis(2000), { + match reqwest::get(url.clone()).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await.as_deref() { + Ok([stake_distribution, ..]) => Ok(Some(stake_distribution.hash.clone())), + Ok(&[]) => Ok(None), + Err(err) => Err(anyhow!("Invalid Cardano stake distribution body : {err}",)), + }, + s => Err(anyhow!("Unexpected status code from Aggregator: {s}")), + }, + Err(err) => Err(anyhow!(err).context(format!("Request to `{url}` failed"))), + } + }) { + AttemptResult::Ok(hash) => { + info!("Aggregator produced a Cardano stake distribution"; "hash" => &hash); + Ok(hash) + } + AttemptResult::Err(error) => Err(error), + AttemptResult::Timeout() => Err(anyhow!( + "Timeout exhausted assert_node_producing_cardano_stake_distribution, no response from `{url}`" + )), + } +} + +pub async fn assert_signer_is_signing_cardano_stake_distribution( + aggregator_endpoint: &str, + hash: &str, + expected_epoch_min: Epoch, +) -> StdResult { + let url = format!("{aggregator_endpoint}/artifact/cardano-stake-distribution/{hash}"); + info!( + "Asserting the aggregator is signing the Cardano stake distribution message `{}` with an expected min epoch of `{}`", + hash, + expected_epoch_min + ); + + match attempt!(10, Duration::from_millis(1000), { + match reqwest::get(url.clone()).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(stake_distribution) => match stake_distribution.epoch { + epoch if epoch >= expected_epoch_min => Ok(Some(stake_distribution)), + epoch => Err(anyhow!( + "Minimum expected Cardano stake distribution epoch not reached : {epoch} < {expected_epoch_min}" + )), + }, + Err(err) => Err(anyhow!(err).context("Invalid Cardano stake distribution body",)), + }, + StatusCode::NOT_FOUND => Ok(None), + s => Err(anyhow!("Unexpected status code from Aggregator: {s}")), + }, + Err(err) => Err(anyhow!(err).context(format!("Request to `{url}` failed"))), + } + }) { + AttemptResult::Ok(cardano_stake_distribution) => { + info!("Signer signed a Cardano stake distribution"; "certificate_hash" => &cardano_stake_distribution.certificate_hash); + Ok(cardano_stake_distribution.certificate_hash) + } + AttemptResult::Err(error) => Err(error), + AttemptResult::Timeout() => Err(anyhow!( + "Timeout exhausted assert_signer_is_signing_cardano_stake_distribution, no response from `{url}`" + )), + } +} + pub async fn assert_is_creating_certificate_with_enough_signers( aggregator_endpoint: &str, certificate_hash: &str, diff --git a/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs b/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs index 4397db3f630..81c52ea0481 100644 --- a/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs +++ b/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs @@ -71,6 +71,7 @@ impl<'a> Spec<'a> { ) .await?; + let expected_epoch_min = target_epoch - 3; // Verify that mithril stake distribution artifacts are produced and signed correctly { let hash = @@ -79,7 +80,7 @@ impl<'a> Spec<'a> { let certificate_hash = assertions::assert_signer_is_signing_mithril_stake_distribution( &aggregator_endpoint, &hash, - target_epoch - 3, + expected_epoch_min, ) .await?; assertions::assert_is_creating_certificate_with_enough_signers( @@ -99,7 +100,7 @@ impl<'a> Spec<'a> { let certificate_hash = assertions::assert_signer_is_signing_snapshot( &aggregator_endpoint, &digest, - target_epoch - 3, + expected_epoch_min, ) .await?; @@ -121,7 +122,7 @@ impl<'a> Spec<'a> { let certificate_hash = assertions::assert_signer_is_signing_cardano_transactions( &aggregator_endpoint, &hash, - target_epoch - 3, + expected_epoch_min, ) .await?; @@ -141,6 +142,29 @@ impl<'a> Spec<'a> { .await?; } + // Verify that Cardano stake distribution artifacts are produced and signed correctly + if self.infrastructure.is_signing_cardano_stake_distribution() { + { + let hash = assertions::assert_node_producing_cardano_stake_distribution( + &aggregator_endpoint, + ) + .await?; + let certificate_hash = + assertions::assert_signer_is_signing_cardano_stake_distribution( + &aggregator_endpoint, + &hash, + expected_epoch_min, + ) + .await?; + assertions::assert_is_creating_certificate_with_enough_signers( + &aggregator_endpoint, + &certificate_hash, + self.infrastructure.signers().len(), + ) + .await?; + } + } + Ok(()) } } diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs index 1a6bea1875c..8e1e67fad5b 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs @@ -41,6 +41,7 @@ pub struct MithrilInfrastructure { cardano_chain_observer: Arc, run_only_mode: bool, is_signing_cardano_transactions: bool, + is_signing_cardano_stake_distribution: bool, } impl MithrilInfrastructure { @@ -90,6 +91,11 @@ impl MithrilInfrastructure { .as_ref() .to_string(), ), + is_signing_cardano_stake_distribution: config.signed_entity_types.contains( + &SignedEntityTypeDiscriminants::CardanoStakeDistribution + .as_ref() + .to_string(), + ), }) } @@ -285,6 +291,10 @@ impl MithrilInfrastructure { self.is_signing_cardano_transactions } + pub fn is_signing_cardano_stake_distribution(&self) -> bool { + self.is_signing_cardano_stake_distribution + } + pub async fn tail_logs(&self, number_of_line: u64) -> StdResult<()> { self.aggregator().tail_logs(number_of_line).await?; for signer in self.signers() { From ed0a7faf01d23900830948071590aad1b03fa288 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:27:28 +0200 Subject: [PATCH 09/17] chore: reference the feature in the CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1822ee933ae..18076aef6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ As a minor extension, we have adopted a slightly different versioning convention - **UNSTABLE** Cardano stake distribution certification: - - Implement the signable and artifact builders for the signed entity type `CardanoStakeDistribution` + - Implement the signable and artifact builders for the signed entity type `CardanoStakeDistribution`. + - Implement the HTTP routes related to the signed entity type `CardanoStakeDistribution` on the aggregator REST API. - Crates versions: From 7d0c27e32f050bafa67b47bbea1756b03c5f01b0 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:55:49 +0200 Subject: [PATCH 10/17] feat: add migration to create a unique index on `signed_entity` table on `signed_entity_type_id` and `beacon` --- mithril-aggregator/src/database/migration.rs | 8 ++++++++ mithril-common/src/test_utils/fake_data.rs | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mithril-aggregator/src/database/migration.rs b/mithril-aggregator/src/database/migration.rs index bc9a1624d09..3e17a951ee1 100644 --- a/mithril-aggregator/src/database/migration.rs +++ b/mithril-aggregator/src/database/migration.rs @@ -752,5 +752,13 @@ pragma foreign_keys=true; SignedEntityTypeDiscriminants::CardanoTransactions.index() ), ), + // Migration 26 + // Alter `signed_entity` table, add a unique index on `signed_entity_type_id` and `beacon` + SqlMigration::new( + 26, + r#" +create unique index signed_entity_unique_index on signed_entity(signed_entity_type_id, beacon); +"#, + ), ] } diff --git a/mithril-common/src/test_utils/fake_data.rs b/mithril-common/src/test_utils/fake_data.rs index 1e0c2bf5db6..c1330feacae 100644 --- a/mithril-common/src/test_utils/fake_data.rs +++ b/mithril-common/src/test_utils/fake_data.rs @@ -202,7 +202,8 @@ pub fn snapshots(total: u64) -> Vec { (1..total + 1) .map(|snapshot_id| { let digest = format!("1{snapshot_id}").repeat(20); - let beacon = beacon(); + let mut beacon = beacon(); + beacon.immutable_file_number += snapshot_id; let certificate_hash = "123".to_string(); let size = snapshot_id * 100000; let cardano_node_version = Version::parse("1.0.0").unwrap(); From 8816650c4cdc3d03d99599a7506a6d4b87120a41 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:30:24 +0200 Subject: [PATCH 11/17] feat: add specific query function to get a `CardanoStakeDistribution` by epoch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Fauvel --- .../query/signed_entity/get_signed_entity.rs | 100 +++++++++++++++++- .../src/database/record/signed_entity.rs | 29 ++++- mithril-common/src/test_utils/fake_data.rs | 20 ++-- 3 files changed, 138 insertions(+), 11 deletions(-) diff --git a/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs b/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs index 342c4ca3156..a03e5e02748 100644 --- a/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs +++ b/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs @@ -1,6 +1,6 @@ use sqlite::Value; -use mithril_common::entities::{Epoch, SignedEntityTypeDiscriminants}; +use mithril_common::entities::{Epoch, SignedEntityType, SignedEntityTypeDiscriminants}; use mithril_common::StdResult; use mithril_persistence::sqlite::{Query, SourceAlias, SqLiteEntity, WhereCondition}; @@ -62,6 +62,19 @@ impl GetSignedEntityRecordQuery { }) } + pub fn cardano_stake_distribution_by_epoch(epoch: Epoch) -> Self { + let signed_entity_type_id = + SignedEntityTypeDiscriminants::CardanoStakeDistribution.index() as i64; + let epoch = *epoch as i64; + + Self { + condition: WhereCondition::new( + "signed_entity_type_id = ?* and beacon = ?*", + vec![Value::Integer(signed_entity_type_id), Value::Integer(epoch)], + ), + } + } + pub fn by_signed_entity_type_and_epoch( signed_entity_type: &SignedEntityTypeDiscriminants, epoch: Epoch, @@ -106,13 +119,96 @@ impl Query for GetSignedEntityRecordQuery { #[cfg(test)] mod tests { - use mithril_common::entities::{CardanoDbBeacon, SignedEntityType}; + use chrono::DateTime; + use mithril_common::{ + entities::{CardanoDbBeacon, SignedEntityType}, + test_utils::fake_data, + }; use mithril_persistence::sqlite::ConnectionExtensions; + use sqlite::ConnectionThreadSafe; use crate::database::test_helper::{insert_signed_entities, main_db_connection}; use super::*; + fn create_database_with_cardano_stake_distributions>( + cardano_stake_distributions: Vec, + ) -> (ConnectionThreadSafe, Vec) { + let records = cardano_stake_distributions + .into_iter() + .map(|cardano_stake_distribution| cardano_stake_distribution.into()) + .collect::>(); + + let connection = create_database(&records); + + (connection, records) + } + + fn create_database(records: &[SignedEntityRecord]) -> ConnectionThreadSafe { + let connection = main_db_connection().unwrap(); + insert_signed_entities(&connection, records.to_vec()).unwrap(); + connection + } + + #[test] + fn cardano_stake_distribution_by_epoch_returns_records_filtered_by_epoch() { + let mut cardano_stake_distributions = fake_data::cardano_stake_distributions(3); + cardano_stake_distributions[0].epoch = Epoch(3); + cardano_stake_distributions[1].epoch = Epoch(4); + cardano_stake_distributions[2].epoch = Epoch(5); + + let (connection, records) = + create_database_with_cardano_stake_distributions(cardano_stake_distributions); + + let records_retrieved: Vec = connection + .fetch_collect( + GetSignedEntityRecordQuery::cardano_stake_distribution_by_epoch(Epoch(4)), + ) + .unwrap(); + + assert_eq!(vec![records[1].clone()], records_retrieved); + } + + #[test] + fn cardano_stake_distribution_by_epoch_returns_records_returns_only_cardano_stake_distribution_records( + ) { + let cardano_stake_distributions_record: SignedEntityRecord = { + let mut cardano_stake_distribution = fake_data::cardano_stake_distribution(Epoch(4)); + cardano_stake_distribution.hash = "hash-123".to_string(); + cardano_stake_distribution.into() + }; + + let snapshots_record = { + let mut snapshot = fake_data::snapshots(1)[0].clone(); + snapshot.beacon.epoch = Epoch(4); + SignedEntityRecord::from_snapshot(snapshot, "whatever".to_string(), DateTime::default()) + }; + + let mithril_stake_distribution_record: SignedEntityRecord = { + let mithril_stake_distributions = fake_data::mithril_stake_distributions(1); + let mut mithril_stake_distribution = mithril_stake_distributions[0].clone(); + mithril_stake_distribution.epoch = Epoch(4); + mithril_stake_distribution.into() + }; + + let connection = create_database(&[ + cardano_stake_distributions_record.clone(), + snapshots_record, + mithril_stake_distribution_record, + ]); + + let records_retrieved: Vec = connection + .fetch_collect( + GetSignedEntityRecordQuery::cardano_stake_distribution_by_epoch(Epoch(4)), + ) + .unwrap(); + + assert_eq!( + vec![cardano_stake_distributions_record.clone()], + records_retrieved, + ); + } + #[test] fn test_get_signed_entity_records() { let signed_entity_records = SignedEntityRecord::fake_records(5); diff --git a/mithril-aggregator/src/database/record/signed_entity.rs b/mithril-aggregator/src/database/record/signed_entity.rs index b797bef94aa..b83c3f0600d 100644 --- a/mithril-aggregator/src/database/record/signed_entity.rs +++ b/mithril-aggregator/src/database/record/signed_entity.rs @@ -5,7 +5,8 @@ use mithril_common::crypto_helper::ProtocolParameters; #[cfg(test)] use mithril_common::entities::CardanoStakeDistribution; use mithril_common::entities::{ - BlockNumber, Epoch, SignedEntity, SignedEntityType, Snapshot, StakeDistribution, + BlockNumber, Epoch, MithrilStakeDistribution, SignedEntity, SignedEntityType, Snapshot, + StakeDistribution, }; use mithril_common::messages::{ CardanoStakeDistributionListItemMessage, CardanoStakeDistributionMessage, @@ -37,6 +38,32 @@ pub struct SignedEntityRecord { pub created_at: DateTime, } +#[cfg(test)] +impl From for SignedEntityRecord { + fn from(cardano_stake_distribution: CardanoStakeDistribution) -> Self { + SignedEntityRecord::from_cardano_stake_distribution(cardano_stake_distribution) + } +} + +#[cfg(test)] +impl From for SignedEntityRecord { + fn from(mithril_stake_distribution: MithrilStakeDistribution) -> Self { + let entity = serde_json::to_string(&mithril_stake_distribution).unwrap(); + + SignedEntityRecord { + signed_entity_id: mithril_stake_distribution.hash.clone(), + signed_entity_type: SignedEntityType::MithrilStakeDistribution( + mithril_stake_distribution.epoch, + ), + certificate_id: format!("certificate-{}", mithril_stake_distribution.hash), + artifact: entity, + created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z") + .unwrap() + .with_timezone(&Utc), + } + } +} + #[cfg(test)] impl SignedEntityRecord { pub(crate) fn from_snapshot( diff --git a/mithril-common/src/test_utils/fake_data.rs b/mithril-common/src/test_utils/fake_data.rs index c1330feacae..262a9e5da67 100644 --- a/mithril-common/src/test_utils/fake_data.rs +++ b/mithril-common/src/test_utils/fake_data.rs @@ -260,15 +260,19 @@ pub const fn transaction_hashes<'a>() -> [&'a str; 5] { ] } -/// Fake Cardano Stake Distribution +/// Fake Cardano Stake Distributions pub fn cardano_stake_distributions(total: u64) -> Vec { - let stake_distribution = StakeDistribution::from([("pool-1".to_string(), 100)]); - (1..total + 1) - .map(|epoch_idx| entities::CardanoStakeDistribution { - hash: format!("hash-epoch-{epoch_idx}"), - epoch: Epoch(epoch_idx), - stake_distribution: stake_distribution.clone(), - }) + .map(|epoch_idx| cardano_stake_distribution(Epoch(epoch_idx))) .collect::>() } + +/// Fake Cardano Stake Distribution +pub fn cardano_stake_distribution(epoch: Epoch) -> entities::CardanoStakeDistribution { + let stake_distribution = StakeDistribution::from([("pool-1".to_string(), 100)]); + entities::CardanoStakeDistribution { + hash: format!("hash-epoch-{epoch}"), + epoch, + stake_distribution, + } +} From c213fcee8cc12e04f2396fbba6f133820eaa681e Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:47:49 +0200 Subject: [PATCH 12/17] feat: wire `MithrilMessageService` with new implementation to get a `CardanoStakeDistribution` by epoch Remove previous implementation --- .../src/sqlite/condition.rs | 4 +- .../query/signed_entity/get_signed_entity.rs | 112 +----------------- .../src/database/record/signed_entity.rs | 7 +- .../repository/signed_entity_store.rs | 96 +++------------ mithril-aggregator/src/services/message.rs | 24 ++-- 5 files changed, 31 insertions(+), 212 deletions(-) diff --git a/internal/mithril-persistence/src/sqlite/condition.rs b/internal/mithril-persistence/src/sqlite/condition.rs index 851ca073e70..c7a999b629a 100644 --- a/internal/mithril-persistence/src/sqlite/condition.rs +++ b/internal/mithril-persistence/src/sqlite/condition.rs @@ -2,7 +2,7 @@ use sqlite::Value; use std::iter::repeat; /// Internal Boolean representation -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] enum BooleanCondition { /// Empty tree None, @@ -43,7 +43,7 @@ impl BooleanCondition { } /// Where condition builder. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct WhereCondition { /// Boolean condition internal tree condition: BooleanCondition, diff --git a/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs b/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs index a03e5e02748..ee68de31868 100644 --- a/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs +++ b/mithril-aggregator/src/database/query/signed_entity/get_signed_entity.rs @@ -1,13 +1,12 @@ use sqlite::Value; -use mithril_common::entities::{Epoch, SignedEntityType, SignedEntityTypeDiscriminants}; +use mithril_common::entities::{Epoch, SignedEntityTypeDiscriminants}; use mithril_common::StdResult; use mithril_persistence::sqlite::{Query, SourceAlias, SqLiteEntity, WhereCondition}; use crate::database::record::SignedEntityRecord; /// Simple queries to retrieve [SignedEntityRecord] from the sqlite database. -#[derive(Debug, PartialEq)] pub struct GetSignedEntityRecordQuery { condition: WhereCondition, } @@ -74,31 +73,6 @@ impl GetSignedEntityRecordQuery { ), } } - - pub fn by_signed_entity_type_and_epoch( - signed_entity_type: &SignedEntityTypeDiscriminants, - epoch: Epoch, - ) -> Self { - let signed_entity_type_id: i64 = signed_entity_type.index() as i64; - let epoch = *epoch as i64; - - match signed_entity_type { - SignedEntityTypeDiscriminants::MithrilStakeDistribution - | SignedEntityTypeDiscriminants::CardanoStakeDistribution => Self { - condition: WhereCondition::new( - "signed_entity_type_id = ?* and beacon = ?*", - vec![Value::Integer(signed_entity_type_id), Value::Integer(epoch)], - ), - }, - SignedEntityTypeDiscriminants::CardanoImmutableFilesFull - | SignedEntityTypeDiscriminants::CardanoTransactions => Self { - condition: WhereCondition::new( - "signed_entity_type_id = ?* and json_extract(beacon, '$.epoch') = ?*", - vec![Value::Integer(signed_entity_type_id), Value::Integer(epoch)], - ), - }, - } - } } impl Query for GetSignedEntityRecordQuery { @@ -250,88 +224,4 @@ mod tests { signed_entity_records.iter().map(|c| c.to_owned()).collect(); assert_eq!(expected_signed_entity_records, signed_entity_records); } - - #[test] - fn by_signed_entity_type_and_epoch_with_mithril_stake_distribution() { - assert_eq!( - 0, - SignedEntityTypeDiscriminants::MithrilStakeDistribution.index() - ); - let expected = GetSignedEntityRecordQuery { - condition: WhereCondition::new( - "signed_entity_type_id = ?* and beacon = ?*", - vec![Value::Integer(0), Value::Integer(4)], - ), - }; - - let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( - &SignedEntityTypeDiscriminants::MithrilStakeDistribution, - Epoch(4), - ); - - assert_eq!(expected, query); - } - - #[test] - fn by_signed_entity_type_and_epoch_with_cardano_stake_distribution() { - assert_eq!( - 1, - SignedEntityTypeDiscriminants::CardanoStakeDistribution.index() - ); - let expected = GetSignedEntityRecordQuery { - condition: WhereCondition::new( - "signed_entity_type_id = ?* and beacon = ?*", - vec![Value::Integer(1), Value::Integer(4)], - ), - }; - - let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( - &SignedEntityTypeDiscriminants::CardanoStakeDistribution, - Epoch(4), - ); - - assert_eq!(expected, query); - } - - #[test] - fn by_signed_entity_type_and_epoch_with_cardano_immutable_files_full() { - assert_eq!( - 2, - SignedEntityTypeDiscriminants::CardanoImmutableFilesFull.index() - ); - let expected = GetSignedEntityRecordQuery { - condition: WhereCondition::new( - "signed_entity_type_id = ?* and json_extract(beacon, '$.epoch') = ?*", - vec![Value::Integer(2), Value::Integer(4)], - ), - }; - - let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( - &SignedEntityTypeDiscriminants::CardanoImmutableFilesFull, - Epoch(4), - ); - - assert_eq!(expected, query); - } - - #[test] - fn by_signed_entity_type_and_epoch_with_cardano_transactions() { - assert_eq!( - 3, - SignedEntityTypeDiscriminants::CardanoTransactions.index() - ); - let expected = GetSignedEntityRecordQuery { - condition: WhereCondition::new( - "signed_entity_type_id = ?* and json_extract(beacon, '$.epoch') = ?*", - vec![Value::Integer(3), Value::Integer(4)], - ), - }; - - let query = GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( - &SignedEntityTypeDiscriminants::CardanoTransactions, - Epoch(4), - ); - - assert_eq!(expected, query); - } } diff --git a/mithril-aggregator/src/database/record/signed_entity.rs b/mithril-aggregator/src/database/record/signed_entity.rs index b83c3f0600d..771e1a4424b 100644 --- a/mithril-aggregator/src/database/record/signed_entity.rs +++ b/mithril-aggregator/src/database/record/signed_entity.rs @@ -2,12 +2,11 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use mithril_common::crypto_helper::ProtocolParameters; -#[cfg(test)] -use mithril_common::entities::CardanoStakeDistribution; use mithril_common::entities::{ - BlockNumber, Epoch, MithrilStakeDistribution, SignedEntity, SignedEntityType, Snapshot, - StakeDistribution, + BlockNumber, Epoch, SignedEntity, SignedEntityType, Snapshot, StakeDistribution, }; +#[cfg(test)] +use mithril_common::entities::{CardanoStakeDistribution, MithrilStakeDistribution}; use mithril_common::messages::{ CardanoStakeDistributionListItemMessage, CardanoStakeDistributionMessage, CardanoTransactionSnapshotListItemMessage, CardanoTransactionSnapshotMessage, diff --git a/mithril-aggregator/src/database/repository/signed_entity_store.rs b/mithril-aggregator/src/database/repository/signed_entity_store.rs index 5a3f7e56eae..84b64a0d938 100644 --- a/mithril-aggregator/src/database/repository/signed_entity_store.rs +++ b/mithril-aggregator/src/database/repository/signed_entity_store.rs @@ -44,12 +44,11 @@ pub trait SignedEntityStorer: Sync + Send { total: usize, ) -> StdResult>; - /// Get signed entities by signed entity type and epoch - async fn get_signed_entities_by_type_and_epoch( + /// Get Cardano stake distribution signed entity by epoch + async fn get_cardano_stake_distribution_signed_entity_by_epoch( &self, - signed_entity_type_id: &SignedEntityTypeDiscriminants, epoch: Epoch, - ) -> StdResult>; + ) -> StdResult>; /// Perform an update for all the given signed entities. async fn update_signed_entities( @@ -134,16 +133,12 @@ impl SignedEntityStorer for SignedEntityStore { Ok(signed_entities) } - async fn get_signed_entities_by_type_and_epoch( + async fn get_cardano_stake_distribution_signed_entity_by_epoch( &self, - signed_entity_type_id: &SignedEntityTypeDiscriminants, epoch: Epoch, - ) -> StdResult> { + ) -> StdResult> { self.connection - .fetch_collect(GetSignedEntityRecordQuery::by_signed_entity_type_and_epoch( - signed_entity_type_id, - epoch, - )) + .fetch_first(GetSignedEntityRecordQuery::cardano_stake_distribution_by_epoch(epoch)) } async fn update_signed_entities( @@ -169,7 +164,6 @@ impl SignedEntityStorer for SignedEntityStore { #[cfg(test)] mod tests { - use chrono::DateTime; use mithril_common::{ entities::{Epoch, MithrilStakeDistribution, SignedEntity, Snapshot}, test_utils::fake_data, @@ -335,89 +329,35 @@ mod tests { } #[tokio::test] - async fn get_signed_entities_by_type_and_epoch_when_only_epoch_in_beacon() { - let mut cardano_stake_distributions = fake_data::cardano_stake_distributions(2); - let epoch_to_retrieve = cardano_stake_distributions[0].epoch; - cardano_stake_distributions[1].epoch = epoch_to_retrieve; - - let expected_records = cardano_stake_distributions - .iter() - .map(|cardano_stake_distribution| { - SignedEntityRecord::from_cardano_stake_distribution( - cardano_stake_distribution.clone(), - ) - }) - .collect::>(); - + async fn get_cardano_stake_distribution_signed_entity_by_epoch_when_nothing_found() { + let epoch_to_retrieve = Epoch(4); let connection = main_db_connection().unwrap(); - insert_signed_entities(&connection, expected_records.clone()).unwrap(); let store = SignedEntityStore::new(Arc::new(connection)); - let records = store - .get_signed_entities_by_type_and_epoch( - &SignedEntityTypeDiscriminants::CardanoStakeDistribution, - epoch_to_retrieve, - ) + let record = store + .get_cardano_stake_distribution_signed_entity_by_epoch(epoch_to_retrieve) .await .unwrap(); - assert_eq!( - // Records are inserted older to earlier and queried the other way round - expected_records.into_iter().rev().collect::>(), - records - ); + assert_eq!(None, record); } #[tokio::test] - async fn get_signed_entities_by_type_and_epoch_when_json_in_beacon() { - let mut snapshots = fake_data::snapshots(2); - let epoch_to_retrieve = snapshots[0].beacon.epoch; - snapshots[1].beacon.epoch = epoch_to_retrieve; - - let expected_records = snapshots - .iter() - .map(|snapshot| { - SignedEntityRecord::from_snapshot( - snapshot.clone(), - "whatever".to_string(), - DateTime::default(), - ) - }) - .collect::>(); - - let connection = main_db_connection().unwrap(); - insert_signed_entities(&connection, expected_records.clone()).unwrap(); - let store = SignedEntityStore::new(Arc::new(connection)); - - let records = store - .get_signed_entities_by_type_and_epoch( - &SignedEntityTypeDiscriminants::CardanoImmutableFilesFull, - epoch_to_retrieve, - ) - .await - .unwrap(); + async fn get_cardano_stake_distribution_signed_entity_by_epoch_when_signed_entity_found_for_epoch( + ) { + let cardano_stake_distribution = fake_data::cardano_stake_distribution(Epoch(4)); - assert_eq!( - // Records are inserted older to earlier and queried the other way round - expected_records.into_iter().rev().collect::>(), - records - ); - } + let expected_record: SignedEntityRecord = cardano_stake_distribution.into(); - #[tokio::test] - async fn get_signed_entities_by_type_and_epoch_when_nothing_found() { - let epoch_to_retrieve = Epoch(4); let connection = main_db_connection().unwrap(); + insert_signed_entities(&connection, vec![expected_record.clone()]).unwrap(); let store = SignedEntityStore::new(Arc::new(connection)); let record = store - .get_signed_entities_by_type_and_epoch( - &SignedEntityTypeDiscriminants::CardanoStakeDistribution, - epoch_to_retrieve, - ) + .get_cardano_stake_distribution_signed_entity_by_epoch(Epoch(4)) .await .unwrap(); - assert_eq!(record, vec![]); + assert_eq!(Some(expected_record), record); } } diff --git a/mithril-aggregator/src/services/message.rs b/mithril-aggregator/src/services/message.rs index e0df9da6478..e40c1aac014 100644 --- a/mithril-aggregator/src/services/message.rs +++ b/mithril-aggregator/src/services/message.rs @@ -222,13 +222,10 @@ impl MessageService for MithrilMessageService { &self, epoch: Epoch, ) -> StdResult> { - let signed_entity_type_id = SignedEntityTypeDiscriminants::CardanoStakeDistribution; let signed_entity = self .signed_entity_storer - .get_signed_entities_by_type_and_epoch(&signed_entity_type_id, epoch) - .await? - .first() - .cloned(); + .get_cardano_stake_distribution_signed_entity_by_epoch(epoch) + .await?; signed_entity.map(|v| v.try_into()).transpose() } @@ -253,8 +250,7 @@ mod tests { use mithril_common::entities::{ CardanoStakeDistribution, CardanoTransactionsSnapshot, Certificate, Epoch, - MithrilStakeDistribution, SignedEntity, SignedEntityType, SignedEntityTypeDiscriminants, - Snapshot, + MithrilStakeDistribution, SignedEntity, SignedEntityType, Snapshot, }; use mithril_common::messages::ToMessageAdapter; use mithril_common::test_utils::MithrilFixtureBuilder; @@ -624,11 +620,8 @@ mod tests { let mut dep_builder = DependenciesBuilder::new(configuration); let mut storer = MockSignedEntityStorer::new(); storer - .expect_get_signed_entities_by_type_and_epoch() - .withf(|discriminant, _| { - discriminant == &SignedEntityTypeDiscriminants::CardanoStakeDistribution - }) - .return_once(|_, _| Ok(vec![record])) + .expect_get_cardano_stake_distribution_signed_entity_by_epoch() + .return_once(|_| Ok(Some(record))) .once(); dep_builder.signed_entity_storer = Some(Arc::new(storer)); let service = dep_builder.get_message_service().await.unwrap(); @@ -647,11 +640,8 @@ mod tests { let mut dep_builder = DependenciesBuilder::new(configuration); let mut storer = MockSignedEntityStorer::new(); storer - .expect_get_signed_entities_by_type_and_epoch() - .withf(|discriminant, _| { - discriminant == &SignedEntityTypeDiscriminants::CardanoStakeDistribution - }) - .return_once(|_, _| Ok(vec![])) + .expect_get_cardano_stake_distribution_signed_entity_by_epoch() + .return_once(|_| Ok(None)) .once(); dep_builder.signed_entity_storer = Some(Arc::new(storer)); let service = dep_builder.get_message_service().await.unwrap(); From 3b440ded30f374e0d3d74603047961aa919c92ab Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:37:56 +0200 Subject: [PATCH 13/17] fix: use the epoch from the beacon of the `CardanoStakeDistribution` signed entity instead of using the epoch of the artifact to build the messages --- .../src/database/record/signed_entity.rs | 10 ++++++---- .../to_cardano_stake_distribution_list_message.rs | 4 +++- .../to_cardano_stake_distribution_message.rs | 4 +++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/mithril-aggregator/src/database/record/signed_entity.rs b/mithril-aggregator/src/database/record/signed_entity.rs index 771e1a4424b..ef15c2cd17d 100644 --- a/mithril-aggregator/src/database/record/signed_entity.rs +++ b/mithril-aggregator/src/database/record/signed_entity.rs @@ -288,13 +288,14 @@ impl TryFrom for CardanoStakeDistributionMessage { fn try_from(value: SignedEntityRecord) -> Result { #[derive(Deserialize)] struct TmpCardanoStakeDistribution { - epoch: Epoch, hash: String, stake_distribution: StakeDistribution, } let artifact = serde_json::from_str::(&value.artifact)?; let cardano_stake_distribution_message = CardanoStakeDistributionMessage { - epoch: artifact.epoch, + // The epoch stored in the signed entity type beacon corresponds to epoch + // at the end of which the Cardano stake distribution is computed by the Cardano node. + epoch: value.signed_entity_type.get_epoch(), stake_distribution: artifact.stake_distribution, hash: artifact.hash, certificate_hash: value.certificate_id, @@ -311,12 +312,13 @@ impl TryFrom for CardanoStakeDistributionListItemMessage { fn try_from(value: SignedEntityRecord) -> Result { #[derive(Deserialize)] struct TmpCardanoStakeDistribution { - epoch: Epoch, hash: String, } let artifact = serde_json::from_str::(&value.artifact)?; let message = CardanoStakeDistributionListItemMessage { - epoch: artifact.epoch, + // The epoch stored in the signed entity type beacon corresponds to epoch + // at the end of which the Cardano stake distribution is computed by the Cardano node. + epoch: value.signed_entity_type.get_epoch(), hash: artifact.hash, certificate_hash: value.certificate_id, created_at: value.created_at, diff --git a/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs index b8455c69868..08278d491a5 100644 --- a/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs +++ b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_list_message.rs @@ -20,7 +20,9 @@ impl snapshots .into_iter() .map(|entity| CardanoStakeDistributionListItemMessage { - epoch: entity.artifact.epoch, + // The epoch stored in the signed entity type beacon corresponds to epoch + // at the end of which the Cardano stake distribution is computed by the Cardano node. + epoch: entity.signed_entity_type.get_epoch(), hash: entity.artifact.hash, certificate_hash: entity.certificate_id, created_at: entity.created_at, diff --git a/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs index b383ff091ce..d0e0dd838a2 100644 --- a/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs +++ b/mithril-aggregator/src/message_adapters/to_cardano_stake_distribution_message.rs @@ -11,7 +11,9 @@ impl ToMessageAdapter, CardanoStakeDistri /// Method to trigger the conversion fn adapt(from: SignedEntity) -> CardanoStakeDistributionMessage { CardanoStakeDistributionMessage { - epoch: from.artifact.epoch, + // The epoch stored in the signed entity type beacon corresponds to epoch + // at the end of which the Cardano stake distribution is computed by the Cardano node. + epoch: from.signed_entity_type.get_epoch(), hash: from.artifact.hash, certificate_hash: from.certificate_id, stake_distribution: from.artifact.stake_distribution, From 63fdc2838b49e9e97b17f640bd56cc7579d54023 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:23:27 +0200 Subject: [PATCH 14/17] fix: env variable `DELEGATION_ROUND` is not assigned in `AMOUNT_STAKED` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Fauvel --- .../mithril-devnet/mkfiles/mkfiles-mithril-delegation.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mithril-test-lab/mithril-devnet/mkfiles/mkfiles-mithril-delegation.sh b/mithril-test-lab/mithril-devnet/mkfiles/mkfiles-mithril-delegation.sh index cc91916fd3d..c8756f705e0 100644 --- a/mithril-test-lab/mithril-devnet/mkfiles/mkfiles-mithril-delegation.sh +++ b/mithril-test-lab/mithril-devnet/mkfiles/mkfiles-mithril-delegation.sh @@ -85,7 +85,7 @@ done # Prepare transactions for delegating to stake pools for N in ${POOL_NODES_N}; do cat >> delegate.sh < Date: Fri, 2 Aug 2024 18:46:34 +0200 Subject: [PATCH 15/17] docs: add description in the OpenAPI specs for the epoch used the `CardanoStakeDistribution` messages --- openapi.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 2216333a451..b0903a574ea 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1701,6 +1701,7 @@ components: - created_at properties: epoch: + description: Epoch at the end of which the Cardano stake distribution is computed by the Cardano node $ref: "#/components/schemas/Epoch" hash: description: Hash of the Cardano stake distribution @@ -1734,6 +1735,7 @@ components: - created_at properties: epoch: + description: Epoch at the end of which the Cardano stake distribution is computed by the Cardano node $ref: "#/components/schemas/Epoch" hash: description: Hash of the Cardano stake distribution From 66a4aa1f675ade351bc2aa9cdac140f9fdb17502 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:34:25 +0200 Subject: [PATCH 16/17] test: add integration test to verify the signed stake distribution --- .../src/services/signed_entity.rs | 26 +++ ...ardano_stake_distribution_verify_stakes.rs | 184 ++++++++++++++++++ .../test_extensions/aggregator_observer.rs | 8 +- 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 mithril-aggregator/tests/cardano_stake_distribution_verify_stakes.rs diff --git a/mithril-aggregator/src/services/signed_entity.rs b/mithril-aggregator/src/services/signed_entity.rs index dba456a0c4d..2482be8c592 100644 --- a/mithril-aggregator/src/services/signed_entity.rs +++ b/mithril-aggregator/src/services/signed_entity.rs @@ -68,6 +68,13 @@ pub trait SignedEntityService: Send + Sync { &self, signed_entity_id: &str, ) -> StdResult>>; + + /// Return a list of signed Cardano stake distribution order by creation + /// date descending. + async fn get_last_signed_cardano_stake_distributions( + &self, + total: usize, + ) -> StdResult>>; } /// Mithril ArtifactBuilder Service @@ -358,6 +365,25 @@ impl SignedEntityService for MithrilSignedEntityService { Ok(entity) } + + async fn get_last_signed_cardano_stake_distributions( + &self, + total: usize, + ) -> StdResult>> { + let signed_entities_records = self + .get_last_signed_entities( + total, + &SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ) + .await?; + let mut signed_entities: Vec> = Vec::new(); + + for record in signed_entities_records { + signed_entities.push(record.try_into()?); + } + + Ok(signed_entities) + } } #[cfg(test)] diff --git a/mithril-aggregator/tests/cardano_stake_distribution_verify_stakes.rs b/mithril-aggregator/tests/cardano_stake_distribution_verify_stakes.rs new file mode 100644 index 00000000000..6d991f93ba0 --- /dev/null +++ b/mithril-aggregator/tests/cardano_stake_distribution_verify_stakes.rs @@ -0,0 +1,184 @@ +mod test_extensions; + +use mithril_aggregator::Configuration; +use mithril_common::entities::SignerWithStake; +use mithril_common::{ + entities::{ + BlockNumber, CardanoDbBeacon, ChainPoint, Epoch, ProtocolParameters, SignedEntityType, + SignedEntityTypeDiscriminants, SlotNumber, StakeDistribution, StakeDistributionParty, + TimePoint, + }, + test_utils::MithrilFixtureBuilder, +}; +use test_extensions::{utilities::get_test_dir, ExpectedCertificate, RuntimeTester}; + +#[tokio::test] +async fn cardano_stake_distribution_verify_stakes() { + let protocol_parameters = ProtocolParameters { + k: 5, + m: 150, + phi_f: 0.95, + }; + let configuration = Configuration { + protocol_parameters: protocol_parameters.clone(), + signed_entity_types: Some( + SignedEntityTypeDiscriminants::CardanoStakeDistribution.to_string(), + ), + data_stores_directory: get_test_dir("cardano_stake_distribution_verify_stakes"), + ..Configuration::new_sample() + }; + let mut tester = RuntimeTester::build( + TimePoint::new( + 2, + 1, + ChainPoint::new(SlotNumber(10), BlockNumber(1), "block_hash-1"), + ), + configuration, + ) + .await; + + comment!("create signers & declare the initial stake distribution"); + let fixture = MithrilFixtureBuilder::default() + .with_signers(5) + .with_protocol_parameters(protocol_parameters.clone()) + .build(); + let signers = &fixture.signers_fixture(); + + tester.init_state_from_fixture(&fixture).await.unwrap(); + + comment!("Bootstrap the genesis certificate"); + tester.register_genesis_certificate(&fixture).await.unwrap(); + + assert_last_certificate_eq!( + tester, + ExpectedCertificate::new_genesis( + CardanoDbBeacon::new("devnet".to_string(), 2, 1), + fixture.compute_and_encode_avk() + ) + ); + + comment!("Start the runtime state machine and register signers"); + cycle!(tester, "ready"); + tester.register_signers(signers).await.unwrap(); + + comment!("Increase epoch and register signers with a different stake distribution"); + tester.increase_epoch().await.unwrap(); + let signers_with_updated_stake_distribution = fixture + .signers_with_stake() + .iter() + .map(|signer_with_stake| { + SignerWithStake::from_signer( + signer_with_stake.to_owned().into(), + signer_with_stake.stake + 999, + ) + }) + .collect::>(); + let updated_stake_distribution: StakeDistribution = signers_with_updated_stake_distribution + .iter() + .map(|s| (s.party_id.clone(), s.stake)) + .collect(); + tester + .chain_observer + .set_signers(signers_with_updated_stake_distribution) + .await; + cycle!(tester, "idle"); + cycle!(tester, "ready"); + cycle!(tester, "signing"); + tester.register_signers(signers).await.unwrap(); + tester + .send_single_signatures( + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + signers, + ) + .await + .unwrap(); + + comment!("The state machine should issue a certificate for the MithrilStakeDistribution"); + cycle!(tester, "ready"); + assert_last_certificate_eq!( + tester, + ExpectedCertificate::new( + CardanoDbBeacon::new("devnet".to_string(), 3, 1), + StakeDistributionParty::from_signers(fixture.signers_with_stake()).as_slice(), + fixture.compute_and_encode_avk(), + SignedEntityType::MithrilStakeDistribution(Epoch(3)), + ExpectedCertificate::genesis_identifier(&CardanoDbBeacon::new( + "devnet".to_string(), + 2, + 1 + )), + ) + ); + + comment!("Increase epoch and register signers with a different stake distribution"); + let signers_with_updated_stake_distribution = fixture + .signers_with_stake() + .iter() + .map(|signer_with_stake| { + SignerWithStake::from_signer( + signer_with_stake.to_owned().into(), + signer_with_stake.stake + 9999, + ) + }) + .collect::>(); + tester + .chain_observer + .set_signers(signers_with_updated_stake_distribution) + .await; + tester.increase_epoch().await.unwrap(); + cycle!(tester, "idle"); + cycle!(tester, "ready"); + cycle!(tester, "signing"); + tester + .send_single_signatures( + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + signers, + ) + .await + .unwrap(); + + comment!("The state machine should issue a certificate for the MithrilStakeDistribution"); + cycle!(tester, "ready"); + assert_last_certificate_eq!( + tester, + ExpectedCertificate::new( + CardanoDbBeacon::new("devnet".to_string(), 4, 1), + StakeDistributionParty::from_signers(fixture.signers_with_stake()).as_slice(), + fixture.compute_and_encode_avk(), + SignedEntityType::MithrilStakeDistribution(Epoch(4)), + ExpectedCertificate::identifier(&SignedEntityType::MithrilStakeDistribution(Epoch(3))), + ) + ); + + cycle!(tester, "signing"); + tester + .send_single_signatures( + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + signers, + ) + .await + .unwrap(); + + comment!("The state machine should issue a certificate for the CardanoStakeDistribution"); + cycle!(tester, "ready"); + assert_last_certificate_eq!( + tester, + ExpectedCertificate::new( + CardanoDbBeacon::new("devnet".to_string(), 4, 1), + StakeDistributionParty::from_signers(fixture.signers_with_stake()).as_slice(), + fixture.compute_and_encode_avk(), + SignedEntityType::CardanoStakeDistribution(Epoch(3)), + ExpectedCertificate::identifier(&SignedEntityType::MithrilStakeDistribution(Epoch(4))), + ) + ); + + comment!("The message service should return the expected stake distribution"); + let message = tester + .dependencies + .message_service + .get_cardano_stake_distribution_message_by_epoch(Epoch(3)) + .await + .unwrap() + .unwrap(); + assert_eq!(updated_stake_distribution, message.stake_distribution); +} diff --git a/mithril-aggregator/tests/test_extensions/aggregator_observer.rs b/mithril-aggregator/tests/test_extensions/aggregator_observer.rs index def6e353b66..af879a3e031 100644 --- a/mithril-aggregator/tests/test_extensions/aggregator_observer.rs +++ b/mithril-aggregator/tests/test_extensions/aggregator_observer.rs @@ -140,7 +140,13 @@ impl AggregatorObserver { .await? .map(|s| s.signed_entity_type) .as_ref()), - _ => Ok(false), + SignedEntityType::CardanoStakeDistribution(_) => Ok(Some(signed_entity_type_expected) + == self + .signed_entity_service + .get_last_signed_cardano_stake_distributions(1) + .await? + .first() + .map(|s| &s.signed_entity_type)), } } } From 66397703c6413d57f46f65cddcf4390996212a21 Mon Sep 17 00:00:00 2001 From: Damien Lachaume <135982616+dlachaume@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:40:52 +0200 Subject: [PATCH 17/17] chore: bump crates versions --- Cargo.lock | 6 +++--- mithril-aggregator/Cargo.toml | 2 +- mithril-common/Cargo.toml | 2 +- mithril-test-lab/mithril-end-to-end/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c75c03f25d..cf322886c97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3539,7 +3539,7 @@ dependencies = [ [[package]] name = "mithril-aggregator" -version = "0.5.53" +version = "0.5.54" dependencies = [ "anyhow", "async-trait", @@ -3695,7 +3695,7 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.4.42" +version = "0.4.43" dependencies = [ "anyhow", "async-trait", @@ -3767,7 +3767,7 @@ dependencies = [ [[package]] name = "mithril-end-to-end" -version = "0.4.27" +version = "0.4.28" dependencies = [ "anyhow", "async-recursion", diff --git a/mithril-aggregator/Cargo.toml b/mithril-aggregator/Cargo.toml index 9ee89452071..bd7b60aab7f 100644 --- a/mithril-aggregator/Cargo.toml +++ b/mithril-aggregator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-aggregator" -version = "0.5.53" +version = "0.5.54" description = "A Mithril Aggregator server" authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 4286c22068f..3bb12275ae4 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-common" -version = "0.4.42" +version = "0.4.43" description = "Common types, interfaces, and utilities for Mithril nodes." authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-test-lab/mithril-end-to-end/Cargo.toml b/mithril-test-lab/mithril-end-to-end/Cargo.toml index e3f949c9f04..e2dd0d9136a 100644 --- a/mithril-test-lab/mithril-end-to-end/Cargo.toml +++ b/mithril-test-lab/mithril-end-to-end/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-end-to-end" -version = "0.4.27" +version = "0.4.28" authors = { workspace = true } edition = { workspace = true } documentation = { workspace = true }