diff --git a/Cargo.lock b/Cargo.lock index 875670d0ab9..6848ada5e0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4597,13 +4597,18 @@ version = "0.1.11" dependencies = [ "anyhow", "async-trait", + "ciborium", + "hex", "httpmock", "mithril-aggregator-client", + "mithril-cardano-node-chain", "mithril-common", + "serde", "serde_json", "slog", "slog-async", "slog-term", + "thiserror 2.0.18", "tokio", ] diff --git a/internal/mithril-protocol-config/Cargo.toml b/internal/mithril-protocol-config/Cargo.toml index d987c9f3437..5a471a9b942 100644 --- a/internal/mithril-protocol-config/Cargo.toml +++ b/internal/mithril-protocol-config/Cargo.toml @@ -12,13 +12,18 @@ include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +ciborium = "0.2.2" +hex = { workspace = true } mithril-aggregator-client = { path = "../mithril-aggregator-client" } +mithril-cardano-node-chain = { path = "../cardano-node/mithril-cardano-node-chain" } mithril-common = { path = "../../mithril-common" } +serde = { workspace = true } +serde_json = { workspace = true } slog = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } [dev-dependencies] httpmock = "0.8.3" -serde_json = { workspace = true } slog-async = { workspace = true } slog-term = { workspace = true } diff --git a/internal/mithril-protocol-config/src/adapters/cardano_chain.rs b/internal/mithril-protocol-config/src/adapters/cardano_chain.rs new file mode 100644 index 00000000000..e67fecb483f --- /dev/null +++ b/internal/mithril-protocol-config/src/adapters/cardano_chain.rs @@ -0,0 +1,132 @@ +use anyhow::Context; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use thiserror::Error; + +use mithril_cardano_node_chain::chain_observer::ChainObserver; +use mithril_cardano_node_chain::entities::ChainAddress; +use mithril_common::crypto_helper::{ + ProtocolConfigurationMarkersSigner, ProtocolConfigurationMarkersVerifierSignature, + ProtocolConfigurationMarkersVerifierVerificationKey, key_encode_hex, +}; +use mithril_common::{StdError, StdResult}; + +use crate::{ProtocolConfigurationMarker, ProtocolConfigurationReaderAdapter}; + +/// [ProtocolConfigurationMarkersPayload] related errors. +#[derive(Debug, Error)] +pub enum ProtocolConfigurationMarkersPayloadError { + /// Error raised when the message serialization fails + #[error("could not serialize message")] + SerializeMessage(#[source] StdError), + + /// Error raised when the signature deserialization fails + #[error("could not deserialize signature")] + DeserializeSignature(#[source] StdError), + + /// Error raised when the signature is missing + #[error("could not verify signature: signature is missing")] + MissingSignature, + + /// Error raised when the signature is invalid + #[error("could not verify signature")] + VerifySignature(#[source] StdError), + + /// Error raised when the signing the markers + #[error("could not create signature")] + CreateSignature(#[source] StdError), +} + +/// Protocol Configuration markers payload +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProtocolConfigurationMarkersPayload { + /// List of protocol configuration markers + pub markers: Vec, +} + +/// Signed Protocol Configuration markers payload +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SignedProtocolConfigurationMarkersPayload { + /// List of protocol configuration markers + pub markers: Vec, + + /// Protocol Configuration markers signature + pub signature: ProtocolConfigurationMarkersVerifierSignature, +} + +impl SignedProtocolConfigurationMarkersPayload { + /// Instanciate a new SignedProtocolConfigurationMarkersPayload with markers and signature + pub fn new( + markers: Vec, + signature: ProtocolConfigurationMarkersVerifierSignature, + ) -> Self { + Self { markers, signature } + } + + /// Encode this payload to a json hex string + pub fn to_json_hex(&self) -> StdResult { + key_encode_hex(self).with_context( + || "SignedProtocolConfigurationMarkersPayload could not be json hex encoded", + ) + } +} + +impl ProtocolConfigurationMarkersPayload { + /// Instanciate a new ProtocolConfigurationMarkersPayload with markers + pub fn new(markers: Vec) -> Self { + Self { markers } + } + + fn message_to_bytes(&self) -> Result, ProtocolConfigurationMarkersPayloadError> { + serde_json::to_vec(&self.markers) + .map_err(|e| ProtocolConfigurationMarkersPayloadError::SerializeMessage(e.into())) + } + + /// Sign an protocol configuration markers payload + pub fn sign( + self, + signer: &ProtocolConfigurationMarkersSigner, + ) -> Result + { + let signature = + signer.sign(&self.message_to_bytes().map_err(|e| { + ProtocolConfigurationMarkersPayloadError::CreateSignature(e.into()) + })?); + + Ok(SignedProtocolConfigurationMarkersPayload { + markers: self.markers, + signature, + }) + } +} + +/// Cardano Chain adapter retrieves protocol configuration markers on chain +pub struct CardanoChainAdapter { + address: ChainAddress, + chain_observer: Arc, + verification_key: ProtocolConfigurationMarkersVerifierVerificationKey, +} + +impl CardanoChainAdapter { + /// CardanoChainAdapter factory + pub fn new( + address: ChainAddress, + chain_observer: Arc, + verification_key: ProtocolConfigurationMarkersVerifierVerificationKey, + ) -> Self { + Self { + address, + chain_observer, + verification_key, + } + } +} + +#[async_trait] +impl ProtocolConfigurationReaderAdapter for CardanoChainAdapter { + async fn read(&self) -> StdResult> { + //TODO to implement + Ok(Vec::new()) + } +} diff --git a/internal/mithril-protocol-config/src/adapters/mod.rs b/internal/mithril-protocol-config/src/adapters/mod.rs new file mode 100644 index 00000000000..d05ffd38aee --- /dev/null +++ b/internal/mithril-protocol-config/src/adapters/mod.rs @@ -0,0 +1,9 @@ +//! Module dedicated to ProtocolConfigurationReaderAdapter implementations. + +mod cardano_chain; + +pub use cardano_chain::{ + CardanoChainAdapter as ProtocolConfigurationReaderCardanoChainAdapter, + ProtocolConfigurationMarkersPayload as ProtocolConfigurationMarkersPayloadCardanoChain, + SignedProtocolConfigurationMarkersPayload as SignedProtocolConfigurationMarkersPayloadCardanoChain, +}; diff --git a/internal/mithril-protocol-config/src/configuration_computer.rs b/internal/mithril-protocol-config/src/configuration_computer.rs new file mode 100644 index 00000000000..7f0181c33b6 --- /dev/null +++ b/internal/mithril-protocol-config/src/configuration_computer.rs @@ -0,0 +1,117 @@ +//! //! Model definitions for ProtocolConfigurationReader. + +use std::collections::BTreeMap; + +use mithril_common::entities::Epoch; + +use crate::ProtocolConfigurationForEpoch; + +/// [ConfigurationComputerFromMarkers] containing markers by epoch +#[derive(PartialEq, Clone, Debug)] +pub struct ConfigurationComputerFromMarkers { + /// BTreeMap assotiation of ProtocolConfigurationForEpoch to a coresponding Epoch + pub markers: BTreeMap, +} + +impl ConfigurationComputerFromMarkers { + /// Create a new [ConfigurationComputerFromMarkers] with the given markers. + pub fn new(markers: BTreeMap) -> Self { + Self { markers } + } + + /// retrieve configuration for given Epoch or fallback to last known configuration + pub fn get_network_configuration(&self, epoch: Epoch) -> Option { + self.markers + .range(..=epoch) + .next_back() + .map(|(_, marker)| marker.clone()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use mithril_common::{ + entities::{ + CardanoBlocksTransactionsSigningConfig, CardanoTransactionsSigningConfig, Epoch, + ProtocolParameters, SignedEntityTypeDiscriminants, + }, + test::double::Dummy, + }; + + use crate::ProtocolConfigurationForEpoch; + + use super::*; + + fn fake_config_for_epoch(epoch: Epoch) -> ProtocolConfigurationForEpoch { + ProtocolConfigurationForEpoch { + protocol_parameters: ProtocolParameters::new(*epoch, *epoch, 0.1), + enabled_signed_entity_types: SignedEntityTypeDiscriminants::all(), + cardano_transactions: Some(CardanoTransactionsSigningConfig::dummy()), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig::dummy()), + } + } + + #[derive(Debug)] + struct TestCase { + requested_epoch: Epoch, + expected_conf_epoch: Epoch, + } + + macro_rules! test_case { + ( + requested: $requested_epoch:expr, + expected: $expected_conf_epoch:expr + ) => { + TestCase { + requested_epoch: Epoch($requested_epoch), + expected_conf_epoch: Epoch($expected_conf_epoch), + } + }; + } + + #[test] + fn test_get_network_configuration_must_fallback_to_last_known_configuration_if_epoch_not_found() + { + let markers = BTreeMap::from([ + (Epoch(2), fake_config_for_epoch(Epoch(2))), + (Epoch(6), fake_config_for_epoch(Epoch(6))), + (Epoch(10), fake_config_for_epoch(Epoch(10))), + ]); + + fn test_cases() -> Vec { + vec![ + test_case!(requested: 3, expected: 2 ), + test_case!(requested: 5, expected: 2 ), + test_case!(requested: 6, expected: 6 ), + test_case!(requested: 7, expected: 6 ), + test_case!(requested: 9, expected: 6 ), + test_case!(requested: 10, expected: 10), + test_case!(requested: 11, expected: 10), + test_case!(requested: 12, expected: 10), + ] + } + + let configurations = ConfigurationComputerFromMarkers::new(markers); + + for test_case in test_cases() { + assert_eq!( + configurations.get_network_configuration(test_case.requested_epoch), + Some(fake_config_for_epoch(test_case.expected_conf_epoch)) + ); + } + } + + #[test] + fn test_get_network_configuration_return_none_if_no_fallback_conf_is_available() { + let markers = BTreeMap::from([ + (Epoch(6), fake_config_for_epoch(Epoch(6))), + (Epoch(10), fake_config_for_epoch(Epoch(10))), + ]); + + let configurations = ConfigurationComputerFromMarkers::new(markers); + + assert_eq!(configurations.get_network_configuration(Epoch(4)), None); + } +} diff --git a/internal/mithril-protocol-config/src/lib.rs b/internal/mithril-protocol-config/src/lib.rs index b03e1691ad0..10b61b314d2 100644 --- a/internal/mithril-protocol-config/src/lib.rs +++ b/internal/mithril-protocol-config/src/lib.rs @@ -1,7 +1,12 @@ #![warn(missing_docs)] //! This crate provides mechanisms to read and check the configuration parameters of a Mithril network. +pub mod adapters; +pub mod configuration_computer; pub mod http; pub mod interface; pub mod model; +mod protocol_configuration_reader; pub mod test; + +pub use protocol_configuration_reader::*; diff --git a/internal/mithril-protocol-config/src/model.rs b/internal/mithril-protocol-config/src/model.rs index 6c898fb4e9a..f9326687b22 100644 --- a/internal/mithril-protocol-config/src/model.rs +++ b/internal/mithril-protocol-config/src/model.rs @@ -1,5 +1,6 @@ //! Model definitions for Mithril Protocol Configuration. +use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use mithril_common::{ @@ -10,7 +11,7 @@ use mithril_common::{ messages::{ProtocolConfigurationMessage, SignedEntityTypeDiscriminantsMessage}, }; -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] /// Custom configuration for the signed entity types pub struct SignedEntityTypeConfiguration { @@ -38,7 +39,7 @@ pub struct MithrilNetworkConfiguration { } //A epoch configuration -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] /// A network configuration available for an epoch pub struct MithrilNetworkConfigurationForEpoch { /// Cryptographic protocol parameters (`k`, `m` and `phi_f`) @@ -66,7 +67,6 @@ impl From for MithrilNetworkConfigurationForEpoch } } } - #[cfg(test)] mod tests { use mithril_common::messages::{ diff --git a/internal/mithril-protocol-config/src/protocol_configuration_reader.rs b/internal/mithril-protocol-config/src/protocol_configuration_reader.rs new file mode 100644 index 00000000000..3f2f963495d --- /dev/null +++ b/internal/mithril-protocol-config/src/protocol_configuration_reader.rs @@ -0,0 +1,284 @@ +use anyhow::Context; +use async_trait::async_trait; +use hex::FromHex; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; +use thiserror::Error; + +use mithril_common::entities::{ + CardanoBlocksTransactionsSigningConfig, CardanoTransactionsSigningConfig, Epoch, + ProtocolParameters, SignedEntityTypeDiscriminants, +}; +use mithril_common::{StdError, StdResult}; + +use crate::configuration_computer::ConfigurationComputerFromMarkers; + +/// Parse error +#[derive(Error, Debug)] +#[error("Codec parse error")] +pub struct ProtocolConfigurationParseError(#[source] StdError); + +/// The cbor representation of a MithrilNetworkConfigurationForEpoch +pub type CborProtocolConfigurationForEpoch = String; + +/// Value object that represents a tag of Protocol Configuration. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProtocolConfigurationMarker { + /// Epoch + pub epoch: Epoch, + + /// Protocol parameters + pub configuration: CborProtocolConfigurationForEpoch, +} + +impl ProtocolConfigurationMarker { + /// instantiate a new [ProtocolConfigurationMarker]. + pub fn new(epoch: Epoch, protocol_configuration: CborProtocolConfigurationForEpoch) -> Self { + ProtocolConfigurationMarker { + epoch, + configuration: protocol_configuration, + } + } +} + +//A epoch configuration used by the reader and for the CBOR representation in the ProtocolConfigurationMarker +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +/// A network configuration available for an epoch +pub struct ProtocolConfigurationForEpoch { + /// Cryptographic protocol parameters (`k`, `m` and `phi_f`) + pub protocol_parameters: ProtocolParameters, + + /// List of available types of certifications + pub enabled_signed_entity_types: BTreeSet, + + //TODO do we want a sub struct like signed_entity_types_config in model ? + /// Signing configuration for Cardano transactions + pub cardano_transactions: Option, + + /// Signing configuration for Cardano blocks and transactions + pub cardano_blocks_transactions: Option, +} + +impl ProtocolConfigurationForEpoch { + /// Serialize the structure to a CBOR bytes representation. + fn to_cbor_bytes(&self) -> Result, ProtocolConfigurationParseError> { + let mut cursor = std::io::Cursor::new(Vec::new()); + ciborium::ser::into_writer(&self, &mut cursor) + .with_context(|| "ProtocolConfigurationForEpoch can not serialize data to cbor") + .map_err(ProtocolConfigurationParseError)?; + + Ok(cursor.into_inner()) + } + + /// Serialize the structure to a CBOR hex representation. + pub fn to_cbor_hex(&self) -> Result { + Ok(hex::encode(self.to_cbor_bytes()?)) + } + + /// Deserialize a type `T: Serialize + DeserializeOwned` from CBOR bytes representation. + fn from_cbor_bytes(bytes: &[u8]) -> Result { + let mut cursor = std::io::Cursor::new(&bytes); + let a: Self = ciborium::de::from_reader(&mut cursor) + .with_context(|| "ProtocolConfigurationForEpoch can not unserialize cbor data") + .map_err(ProtocolConfigurationParseError)?; + + Ok(a) + } + + /// Deserialize a type `T: Serialize + DeserializeOwned` from CBOR hex representation. + pub fn from_cbor_hex(hex: &str) -> Result { + let hex_vector = Vec::from_hex(hex) + .with_context(|| "ProtocolConfigurationForEpoch can not unserialize hex data") + .map_err(ProtocolConfigurationParseError)?; + + Self::from_cbor_bytes(&hex_vector) + .with_context(|| "ProtocolConfigurationForEpoch can not unserialize cbor data") + .map_err(ProtocolConfigurationParseError) + } +} + +/// Adapters are responsible of technically reading the information of +/// [ProtocolConfigurationMarker]s from a backend. +#[async_trait] +pub trait ProtocolConfigurationReaderAdapter: Sync + Send { + /// Read protocol configuration markers from the underlying adapter. + async fn read(&self) -> StdResult>; +} + +/// The ProtocolConfigurationReader is responsible of giving the current Protocol Configurations +/// It uses an [ProtocolConfigurationReaderAdapter] to read data from a backend. +pub struct ProtocolConfigurationReader { + adapter: Arc, +} + +/// Error type when [ProtocolConfigurationReader] fails to return a [todo]. +#[derive(Debug, Error)] +pub enum ProtocolConfigurationReaderError { + /// Underlying adapter fails to return data. + #[error("Adapter Error message: «{message}»")] + AdapterFailure { + /// context message + message: String, + + /// nested underlying adapter error + #[source] + error: StdError, + }, +} + +impl ProtocolConfigurationReader { + /// Instantiate a new [ProtocolConfigurationReader] with an [ProtocolConfigurationReaderAdapter]. + pub fn new(adapter: Arc) -> Self { + Self { adapter } + } + + /// Read protocol configuration markers from the underlying adapter. + pub async fn read_mithril_protocol_configurations( + &self, + ) -> Result { + let markers = self.adapter.read().await.map_err(|e| { + ProtocolConfigurationReaderError::AdapterFailure { + message: "Failed to read protocol configuration markers from adapter".to_string(), + error: e, + } + })?; + + let mut decoded_markers = BTreeMap::new(); + for marker in markers { + let configuration = ProtocolConfigurationForEpoch::from_cbor_hex(&marker.configuration) + .map_err(|e| ProtocolConfigurationReaderError::AdapterFailure { + message: format!( + "Failed to parse protocol configuration for epoch {}", + marker.epoch + ), + error: e.into(), + })?; + decoded_markers.insert(marker.epoch, configuration); + } + Ok(ConfigurationComputerFromMarkers::new(decoded_markers)) + } +} + +#[cfg(test)] +mod tests { + use mithril_common::{ + entities::{BlockNumber, BlockNumberOffset}, + test::double::Dummy, + }; + + use crate::test::double::ProtocolConfigurationReaderDummyAdapter; + + use super::*; + + #[test] + fn to_cbor_from_cbor_conversion() { + let mithril_network_configuration_for_epoch = ProtocolConfigurationForEpoch::dummy(); + let cbor = mithril_network_configuration_for_epoch.to_cbor_hex().unwrap(); + let mithril_network_configuration_for_epoch_from_cbor = + ProtocolConfigurationForEpoch::from_cbor_hex(&cbor).unwrap(); + assert_eq!( + mithril_network_configuration_for_epoch, + mithril_network_configuration_for_epoch_from_cbor + ); + } + + #[test] + fn cbor_golden_test() { + let expected_cbor = "a47370726f746f636f6c5f706172616d6574657273a3616b01616d02657068695f66fb4002666666666666781b656e61626c65645f7369676e65645f656e746974795f747970657384781843617264616e6f5374616b65446973747269627574696f6e6f43617264616e6f44617461626173657343617264616e6f5472616e73616374696f6e73781943617264616e6f426c6f636b735472616e73616374696f6e737463617264616e6f5f7472616e73616374696f6e73a27273656375726974795f706172616d657465720a647374657014781b63617264616e6f5f626c6f636b735f7472616e73616374696f6e73a27273656375726974795f706172616d65746572181e64737465701828"; + let mithril_network_configuration_for_epoch = ProtocolConfigurationForEpoch { + protocol_parameters: ProtocolParameters { + k: 1, + m: 2, + phi_f: 2.3, + }, + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ]), + cardano_transactions: Some(CardanoTransactionsSigningConfig { + security_parameter: BlockNumberOffset(10), + step: BlockNumber(20), + }), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig { + security_parameter: BlockNumberOffset(30), + step: BlockNumber(40), + }), + }; + let cbor = mithril_network_configuration_for_epoch.to_cbor_hex().unwrap(); + assert_eq!(&cbor, expected_cbor); + } + + fn get_basic_marker_sample() -> Vec { + vec![ProtocolConfigurationMarker { + epoch: Epoch(42), + configuration: ProtocolConfigurationForEpoch { + protocol_parameters: ProtocolParameters { + k: 1, + m: 2, + phi_f: 2.3, + }, + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ]), + cardano_transactions: Some(CardanoTransactionsSigningConfig { + security_parameter: BlockNumberOffset(10), + step: BlockNumber(20), + }), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig { + security_parameter: BlockNumberOffset(30), + step: BlockNumber(40), + }), + } + .to_cbor_hex() + .expect("shoud not fail"), + }] + } + + #[tokio::test] + async fn read_mithril_protocol_configurations() { + let cbor_markers: Vec = get_basic_marker_sample(); + let adapter = ProtocolConfigurationReaderDummyAdapter::default(); + adapter.set_markers(cbor_markers); + + let reader = ProtocolConfigurationReader::new(Arc::new(adapter)); + let configurations = reader.read_mithril_protocol_configurations().await.unwrap(); + let markers = configurations.markers; + + assert!(!markers.contains_key(&Epoch(41))); + assert!(markers.contains_key(&Epoch(42))); + assert!(!markers.contains_key(&Epoch(43))); + + let (_, configuration) = markers.get_key_value(&Epoch(42)).expect("should exist"); + + assert_eq!( + configuration, + &ProtocolConfigurationForEpoch { + protocol_parameters: ProtocolParameters { + k: 1, + m: 2, + phi_f: 2.3, + }, + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ]), + cardano_transactions: Some(CardanoTransactionsSigningConfig { + security_parameter: BlockNumberOffset(10), + step: BlockNumber(20), + }), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig { + security_parameter: BlockNumberOffset(30), + step: BlockNumber(40), + }), + } + ) + } +} diff --git a/internal/mithril-protocol-config/src/test/double/dummies.rs b/internal/mithril-protocol-config/src/test/double/dummies.rs index 20932af4c4a..89d1fb74913 100644 --- a/internal/mithril-protocol-config/src/test/double/dummies.rs +++ b/internal/mithril-protocol-config/src/test/double/dummies.rs @@ -8,8 +8,12 @@ use mithril_common::{ test::double::{Dummy, fake_data}, }; -use crate::model::{ - MithrilNetworkConfiguration, MithrilNetworkConfigurationForEpoch, SignedEntityTypeConfiguration, +use crate::{ + ProtocolConfigurationForEpoch, + model::{ + MithrilNetworkConfiguration, MithrilNetworkConfigurationForEpoch, + SignedEntityTypeConfiguration, + }, }; impl Dummy for MithrilNetworkConfiguration { @@ -42,6 +46,23 @@ impl Dummy for MithrilNetworkConfigurationForEpoch { } } +impl Dummy for ProtocolConfigurationForEpoch { + /// Return a dummy for [ProtocolConfigurationForEpoch] (test-only). + fn dummy() -> Self { + Self { + protocol_parameters: fake_data::protocol_parameters(), + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ]), + cardano_transactions: Some(CardanoTransactionsSigningConfig::dummy()), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig::dummy()), + } + } +} + impl Dummy for SignedEntityTypeConfiguration { /// Return a dummy [SignedEntityTypeConfiguration] (test-only). fn dummy() -> Self { diff --git a/internal/mithril-protocol-config/src/test/double/dummy_adatper.rs b/internal/mithril-protocol-config/src/test/double/dummy_adatper.rs new file mode 100644 index 00000000000..6818e8df4af --- /dev/null +++ b/internal/mithril-protocol-config/src/test/double/dummy_adatper.rs @@ -0,0 +1,93 @@ +use std::sync::RwLock; + +use async_trait::async_trait; +use mithril_common::StdResult; + +use crate::{ProtocolConfigurationMarker, ProtocolConfigurationReaderAdapter}; + +/// Dummy adapter is intended to be used in a test environment (end to end test) +/// to simulate retreiving protocol configurations +#[derive(Default)] +pub struct DummyAdapter { + markers: RwLock>, +} + +impl DummyAdapter { + /// Create a new instance directly from markers + pub fn from_markers(markers: Vec) -> Self { + let myself = Self::default(); + myself.set_markers(markers); + + myself + } + + /// Tells what markers should be sent back by the adapter. + pub fn set_markers(&self, markers: Vec) { + let mut my_markers = self.markers.write().unwrap(); + *my_markers = markers; + } +} + +#[async_trait] +impl ProtocolConfigurationReaderAdapter for DummyAdapter { + async fn read(&self) -> StdResult> { + let markers = self.markers.read().unwrap(); + + Ok((*markers.clone()).to_vec()) + } +} + +#[cfg(test)] +mod tests { + use mithril_common::entities::Epoch; + use mithril_common::test::double::Dummy; + + use crate::ProtocolConfigurationForEpoch; + + use super::*; + + #[tokio::test] + async fn empty_dummy_adapter() { + let adapter = DummyAdapter::default(); + + assert!( + adapter + .read() + .await + .expect("dummy adapter shall not fail reading") + .is_empty() + ); + } + + #[tokio::test] + async fn dummy_adapter_output() { + let markers = vec![ + ProtocolConfigurationMarker::new( + Epoch(1), + ProtocolConfigurationForEpoch::dummy() + .to_cbor_hex() + .expect("should not fail"), + ), + ProtocolConfigurationMarker::new( + Epoch(2), + ProtocolConfigurationForEpoch::dummy() + .to_cbor_hex() + .expect("should not fail"), + ), + ProtocolConfigurationMarker::new( + Epoch(3), + ProtocolConfigurationForEpoch::dummy() + .to_cbor_hex() + .expect("should not fail"), + ), + ]; + + let adapter = DummyAdapter::default(); + adapter.set_markers(markers.clone()); + + assert_eq!( + markers, + adapter.read().await.expect("dummy adapter shall not fail reading") + ); + } +} diff --git a/internal/mithril-protocol-config/src/test/double/mod.rs b/internal/mithril-protocol-config/src/test/double/mod.rs index b4dee94f746..b4eeb5da541 100644 --- a/internal/mithril-protocol-config/src/test/double/mod.rs +++ b/internal/mithril-protocol-config/src/test/double/mod.rs @@ -5,5 +5,7 @@ pub mod configuration_provider; mod configuration_provider_with_markers; mod dummies; +mod dummy_adatper; pub use configuration_provider_with_markers::*; +pub use dummy_adatper::DummyAdapter as ProtocolConfigurationReaderDummyAdapter; diff --git a/mithril-aggregator/src/commands/mod.rs b/mithril-aggregator/src/commands/mod.rs index 66694801ad8..4b666df6d6c 100644 --- a/mithril-aggregator/src/commands/mod.rs +++ b/mithril-aggregator/src/commands/mod.rs @@ -2,6 +2,7 @@ mod config_association; mod database_command; mod era_command; mod genesis_command; +mod protocol_configuration_command; mod serve_command; mod tools_command; @@ -14,6 +15,8 @@ use mithril_doc::{Documenter, GenerateDocCommands, StructDoc}; use slog::{Level, Logger, debug}; use std::{collections::HashMap, path::PathBuf}; +pub use protocol_configuration_command::*; + use crate::{DefaultConfiguration, extract_all}; /// Main command selector @@ -24,6 +27,7 @@ pub enum MainCommand { Serve(serve_command::ServeCommand), Tools(tools_command::ToolsCommand), Database(database_command::DatabaseCommand), + ProtocolConfiguration(protocol_configuration_command::ProtocolConfigurationCommand), #[clap(alias("doc"), hide(true))] GenerateDoc(GenerateDocCommands), } @@ -48,6 +52,7 @@ impl MainCommand { Self::Serve(cmd) => cmd.execute(root_logger, config_builder).await, Self::Tools(cmd) => cmd.execute(root_logger, config_builder).await, Self::Database(cmd) => cmd.execute(root_logger, config_builder).await, + Self::ProtocolConfiguration(cmd) => cmd.execute(root_logger, config_builder).await, Self::GenerateDoc(cmd) => { let commands_configs = Self::extract_config(Self::format_crate_name_to_config_key()); @@ -67,6 +72,8 @@ impl MainCommand { Genesis = { genesis_command::GenesisCommand }, Serve = { serve_command::ServeCommand }, Tools = { tools_command::ToolsCommand }, + ProtocolConfiguration = + { protocol_configuration_command::ProtocolConfigurationCommand }, GenerateDoc = {}, ) } @@ -82,6 +89,7 @@ impl MainCommand { MainCommand::Era(_) => CommandType::CommandLine, MainCommand::Tools(_) => CommandType::CommandLine, MainCommand::Database(_) => CommandType::CommandLine, + MainCommand::ProtocolConfiguration(_) => CommandType::CommandLine, MainCommand::GenerateDoc(_) => CommandType::CommandLine, } } diff --git a/mithril-aggregator/src/commands/protocol_configuration_command.rs b/mithril-aggregator/src/commands/protocol_configuration_command.rs new file mode 100644 index 00000000000..24a850adcf5 --- /dev/null +++ b/mithril-aggregator/src/commands/protocol_configuration_command.rs @@ -0,0 +1,419 @@ +use anyhow::Context; +use clap::{Parser, Subcommand}; +use config::{ConfigBuilder, Map, Value, builder::DefaultState}; +use serde::{Deserialize, Serialize}; +use slog::{Logger, debug}; +use std::{ + collections::{BTreeSet, HashMap}, + fs::{self, File}, + io::Write, + path::PathBuf, + sync::Arc, +}; +use thiserror::Error; + +use mithril_cardano_node_chain::chain_observer::ChainObserverType; +use mithril_cli_helper::serde_deserialization; +use mithril_common::StdResult; +use mithril_common::crypto_helper::{ + ProtocolConfigurationMarkersSigner, ProtocolConfigurationMarkersVerifierSecretKey, +}; +use mithril_common::entities::{ + CardanoBlocksTransactionsSigningConfig, CardanoTransactionsSigningConfig, Epoch, + HexEncodedProtocolConfigurationMarkersSecretKey, ProtocolParameters, + SignedEntityTypeDiscriminants, +}; +use mithril_doc::{Documenter, StructDoc}; + +use crate::{ + ConfigurationSource, ExecutionEnvironment, + configuration::ProtocolConfigurationReaderParameters, extract_all, +}; +use crate::{dependency_injection::DependenciesBuilder, tools::ProtocolConfigurationTools}; + +#[derive(Debug, Error)] +pub enum UserConfImportVerificationError { + #[error("Protocol parameters must be non-zero: {0:?}")] + ZeroValueProtocolParameters(ProtocolParameters), + + #[error("enabled_signed_entity_types contains '{0:?}' without any associed configuration")] + EnabledSignedEntityTypeWithoutConfiguration(SignedEntityTypeDiscriminants), +} + +#[derive(Debug, Clone, Deserialize, Documenter)] +pub struct ProtocolConfigurationParametersConfiguration { + /// Path of the socket opened by the Cardano node + #[example = "`/ipc/node.socket`"] + pub cardano_node_socket_path: PathBuf, + + /// Cardano network + #[example = "`mainnet` or `preprod` or `devnet`"] + network: String, + + /// Cardano chain observer type + pub chain_observer_type: ChainObserverType, + + /// Protocol configuration Reader Adapter Parameters + #[example = "\ + `{ \"address\": \"address\", \"verification_key\": \"key\" }`\ + "] + #[serde(deserialize_with = "serde_deserialization::string_or_struct")] + pub protocol_configuration_reader_adapter_params: ProtocolConfigurationReaderParameters, +} + +impl ConfigurationSource for ProtocolConfigurationParametersConfiguration { + fn environment(&self) -> ExecutionEnvironment { + ExecutionEnvironment::Production + } + + fn cardano_node_socket_path(&self) -> PathBuf { + self.cardano_node_socket_path.clone() + } + + fn network(&self) -> String { + self.network.clone() + } + + fn chain_observer_type(&self) -> ChainObserverType { + self.chain_observer_type.clone() + } + + fn protocol_configuration_reader_parameters(&self) -> ProtocolConfigurationReaderParameters { + self.protocol_configuration_reader_adapter_params.clone() + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct HumanReadableProtocolConfiguration { + pub epoch: Epoch, + pub protocol_parameters: ProtocolParameters, + pub cardano_transaction_signing_config: Option, + pub cardano_blocks_transactions_signing_config: Option, + pub enabled_signed_entity_types: BTreeSet, +} + +impl HumanReadableProtocolConfiguration { + pub fn new( + epoch: Epoch, + protocol_parameters: ProtocolParameters, + cardano_transaction_signing_config: Option, + cardano_blocks_transactions_signing_config: Option, + enabled_signed_entity_types: BTreeSet, + ) -> Self { + HumanReadableProtocolConfiguration { + epoch, + protocol_parameters, + cardano_transaction_signing_config, + cardano_blocks_transactions_signing_config, + enabled_signed_entity_types, + } + } +} + +#[derive(Parser, Debug, Clone)] +pub struct ProtocolConfigurationCommand { + /// commands + #[clap(subcommand)] + pub protocol_configuration_sub_command: ProtocolConfigurationSubCommand, +} + +impl ProtocolConfigurationCommand { + pub async fn execute( + &self, + root_logger: Logger, + config_builder: ConfigBuilder, + ) -> StdResult<()> { + self.protocol_configuration_sub_command + .execute(root_logger, config_builder) + .await + } + + pub fn extract_config(command_path: String) -> HashMap { + extract_all!( + command_path, + ProtocolConfigurationSubCommand, + ExportMarkers = { ExportProtocolConfigurationSubCommand }, + ImportMarkers = { ImportProtocolConfigurationSubCommand }, + ) + } +} + +#[derive(Debug, Clone, Subcommand)] +pub enum ProtocolConfigurationSubCommand { + /// Protocol configuration export command. + ExportMarkers(ExportProtocolConfigurationSubCommand), + + /// Protocol configuration import command. + ImportMarkers(ImportProtocolConfigurationSubCommand), +} + +impl ProtocolConfigurationSubCommand { + pub async fn execute( + &self, + root_logger: Logger, + config_builder: ConfigBuilder, + ) -> StdResult<()> { + match self { + Self::ExportMarkers(cmd) => cmd.execute(root_logger, config_builder).await, + Self::ImportMarkers(cmd) => cmd.execute(root_logger, config_builder).await, + } + } +} + +/// Protocol configuration export command +#[derive(Parser, Debug, Clone)] +pub struct ExportProtocolConfigurationSubCommand { + /// Target path + #[clap(long)] + target_path: PathBuf, +} + +impl ExportProtocolConfigurationSubCommand { + pub async fn execute( + &self, + root_logger: Logger, + config_builder: ConfigBuilder, + ) -> StdResult<()> { + Ok(()) + } + + pub fn extract_config(_parent: String) -> HashMap { + HashMap::new() + } +} + +/// Protocol configuration import command +#[derive(Parser, Debug, Clone)] +pub struct ImportProtocolConfigurationSubCommand { + /// Import path of the human readable configurations + #[clap(long, value_parser)] + pub import_path: PathBuf, + + /// target path of the tx datum file + #[clap(long, value_parser)] + pub target_path: PathBuf, + + /// Protocol Configuration Markers Secret Key + #[clap(long, env = "PROTOCOL_CONFIGURATION_MARKERS_SECRET_KEY")] + protocol_configuration_markers_secret_key: HexEncodedProtocolConfigurationMarkersSecretKey, +} + +impl ImportProtocolConfigurationSubCommand { + pub async fn execute( + &self, + root_logger: Logger, + config_builder: ConfigBuilder, + ) -> StdResult<()> { + // 0 conf & dependencies + let config: ProtocolConfigurationParametersConfiguration = config_builder + .build() + .with_context(|| "configuration build error")? + .try_deserialize() + .with_context(|| "configuration deserialize error")?; + debug!(root_logger, "EXPORT PROTOCOL CONFIGURATION command"; "config" => format!("{config:?}")); + + let mut dependencies_builder = + DependenciesBuilder::new(root_logger.clone(), Arc::new(config.clone())); + + let dependencies = dependencies_builder + .create_protocol_configuration_container() + .await + .with_context( + || "Dependencies Builder can not create protocol configuration command dependencies container", + )?; + + //1 - Read the protocol configurations from the file + println!( + "Reading file content {}", + &self.import_path.to_string_lossy() + ); + let json_protocol_configurations = fs::read_to_string(&self.import_path); + + //2 - Parse the json into a protocol configuration list using serde_json + println!("Json parsing ..."); + let protocol_configurations: Vec = + serde_json::from_str(&json_protocol_configurations?)?; + + //3 - Verify protocol config consistency, TODO could be move in ProtocolConfigurationTools ? + println!("Verifying protocol configuration consistency..."); + Self::verify_protocol_configurations(&protocol_configurations)?; //return a VerifiedProtocolConfigurations ? + + //3.2 Check epoch consistency on chain + + let tools = ProtocolConfigurationTools::from_dependencies(dependencies) + .await + .with_context(|| "protocol-configuration-tools: initialization error")?; + //tools.verify_configurations_against_chain(protocol_configurations.clone()); + + //4 - Generate Tx datum + println!("Generating Tx datum ..."); + let protocol_configuration_markers_signer = + Self::get_markers_signer(self.protocol_configuration_markers_secret_key.clone())?; + + let tx_datum = tools.generate_tx_datum( + protocol_configurations, + &protocol_configuration_markers_signer, + )?; + + //5 - TODO: check size < 10kb + + //6 - Write datum file + println!("Generating Tx datum output file..."); + let mut target_file = File::create(&self.target_path)?; + target_file.write_all(tx_datum.as_bytes())?; + + println!( + "Sucessfuly write Tx datum file at {}", + &self.target_path.to_string_lossy() + ); + + Ok(()) + } + + fn get_markers_signer( + secret_key: HexEncodedProtocolConfigurationMarkersSecretKey, + ) -> StdResult { + let markers_secret_key = + ProtocolConfigurationMarkersVerifierSecretKey::from_json_hex(&secret_key) + .with_context( + || "json hex decode of protocol configuration markers secret key failure", + )?; + Ok(ProtocolConfigurationMarkersSigner::from_secret_key( + markers_secret_key, + )) + } + + pub fn verify_protocol_configurations( + configurations: &Vec, + ) -> Result<(), UserConfImportVerificationError> { + //TODO verify non zero protocol parameters (other non zero attributes ?) + for config in configurations { + if config.protocol_parameters.k == 0 + || config.protocol_parameters.m == 0 + || config.protocol_parameters.phi_f == 0.0 + { + return Err( + UserConfImportVerificationError::ZeroValueProtocolParameters( + config.protocol_parameters.clone(), + ), + ); + } + if config + .enabled_signed_entity_types + .contains(&SignedEntityTypeDiscriminants::CardanoTransactions) + && config.cardano_transaction_signing_config.is_none() + { + return Err( + UserConfImportVerificationError::EnabledSignedEntityTypeWithoutConfiguration( + SignedEntityTypeDiscriminants::CardanoTransactions, + ), + ); + } + if config + .enabled_signed_entity_types + .contains(&SignedEntityTypeDiscriminants::CardanoBlocksTransactions) + && config.cardano_blocks_transactions_signing_config.is_none() + { + return Err( + UserConfImportVerificationError::EnabledSignedEntityTypeWithoutConfiguration( + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + ), + ); + } + } + + Ok(()) + } + + pub fn extract_config(_parent: String) -> HashMap { + HashMap::new() + } +} + +#[cfg(test)] +mod tests { + use mithril_common::{entities::ProtocolParameters, test::double::Dummy}; + + use super::*; + + mod verify_protocol_configurations { + + use super::*; + + #[test] + fn should_throw_error_with_protocol_parameter_at_zero() { + let protocol_parameters_list_to_test = [ + ProtocolParameters::new(0, 1, 0.123), + ProtocolParameters::new(1, 0, 0.123), + ProtocolParameters::new(1, 1, 0.0), + ]; + + for protocol_parameters in protocol_parameters_list_to_test { + let configurations = vec![HumanReadableProtocolConfiguration { + protocol_parameters: protocol_parameters.clone(), + ..Dummy::dummy() + }]; + + ImportProtocolConfigurationSubCommand::verify_protocol_configurations( + &configurations, + ) + .expect_err( + &format!( + "Protocol parameters must be non-zero: {:?}", + protocol_parameters + ) + .to_string(), + ); + } + } + + #[test] + fn shoud_throw_error_if_enabled_entity_types_contains_cardano_transactions_without_configuration() + { + let configurations = vec![HumanReadableProtocolConfiguration { + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + ]), + cardano_transaction_signing_config: None, + ..Dummy::dummy() + }]; + + ImportProtocolConfigurationSubCommand::verify_protocol_configurations(&configurations) + .expect_err("enabled_signed_entity_types contains 'CardanoTransactions' without any associed configuration"); + } + + #[test] + fn shoud_throw_error_if_enabled_entity_types_contains_cardano_blocks_transactions_without_configuration() + { + let configurations = vec![HumanReadableProtocolConfiguration { + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + ]), + cardano_blocks_transactions_signing_config: None, + ..Dummy::dummy() + }]; + + ImportProtocolConfigurationSubCommand::verify_protocol_configurations(&configurations) + .expect_err("enabled_signed_entity_types contains 'CardanoBlocksTransactions' without any associed configuration"); + } + } + + #[test] + fn import_subcommand_parses_flag() { + let signer_secret_key = ProtocolConfigurationMarkersSigner::create_deterministic_signer() + .secret_key() + .to_json_hex() + .expect("create_deterministic_signer for secret key should not fail"); + + ImportProtocolConfigurationSubCommand::try_parse_from([ + "import-markers", + "--import-path", + "tests/human_readable_protocol_configuration_toto.json", + "--target-path", + "/tests/protocol_configuration_tx_datum", + "--protocol-configuration-markers-secret-key", + &signer_secret_key, + ]) + .expect("CLI parse should succeed"); + } +} diff --git a/mithril-aggregator/src/configuration.rs b/mithril-aggregator/src/configuration.rs index 167508be0f9..200c42f0a99 100644 --- a/mithril-aggregator/src/configuration.rs +++ b/mithril-aggregator/src/configuration.rs @@ -9,7 +9,9 @@ use serde::Deserialize; use mithril_cardano_node_chain::chain_observer::ChainObserverType; use mithril_cli_helper::{register_config_value, serde_deserialization}; -use mithril_common::crypto_helper::{GenesisEd25519Signer, ManifestSigner}; +use mithril_common::crypto_helper::{ + GenesisEd25519Signer, ManifestSigner, ProtocolConfigurationMarkersVerifierVerificationKey, +}; use mithril_common::entities::{ BlockNumber, BlockNumberOffset, CardanoBlocksTransactionsSigningConfig, CardanoTransactionsSigningConfig, CompressionAlgorithm, ConfigSecret, @@ -196,6 +198,11 @@ pub trait ConfigurationSource { panic!("era_reader_adapter_params is not implemented."); } + /// Protocol configuration reader adapter parameters + fn protocol_configuration_reader_parameters(&self) -> ProtocolConfigurationReaderParameters { + panic!("protocol_configuration_reader_parameters is not implemented."); + } + /// Configuration of the ancillary files signer /// /// **IMPORTANT**: The cryptographic scheme used is ED25519 @@ -772,6 +779,24 @@ impl FromStr for AncillaryFilesSignerConfig { } } +/// Configuration of the protocol Configuration Reader +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ProtocolConfigurationReaderParameters { + /// Address + pub address: String, + + /// Verification key + pub verification_key: ProtocolConfigurationMarkersVerifierVerificationKey, +} + +impl FromStr for ProtocolConfigurationReaderParameters { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + impl ServeCommandConfiguration { /// Create a sample configuration mainly for tests pub fn new_sample(tmp_path: PathBuf) -> Self { diff --git a/mithril-aggregator/src/dependency_injection/builder/mod.rs b/mithril-aggregator/src/dependency_injection/builder/mod.rs index b20a56844b9..435d8a6329c 100644 --- a/mithril-aggregator/src/dependency_injection/builder/mod.rs +++ b/mithril-aggregator/src/dependency_injection/builder/mod.rs @@ -34,7 +34,10 @@ use mithril_common::{ }; use mithril_era::{EraChecker, EraReader, EraReaderAdapter}; use mithril_persistence::sqlite::{SqliteConnection, SqliteConnectionPool}; -use mithril_protocol_config::interface::MithrilNetworkConfigurationProvider; +use mithril_protocol_config::{ + ProtocolConfigurationReader, ProtocolConfigurationReaderAdapter, + interface::MithrilNetworkConfigurationProvider, +}; use mithril_signed_entity_lock::SignedEntityTypeLock; use mithril_ticker::TickerService; @@ -49,6 +52,7 @@ use crate::{ AggregatorCardanoChainDataRepository, CertificateRepository, EpochSettingsStore, OpenMessageRepository, SignedEntityStorer, SignerStore, StakePoolStore, }, + dependency_injection::ProtocolConfigurationCommandDependenciesContainer, event_store::{EventMessage, TransmitterService}, file_uploaders::FileUploader, http_server::routes::router::{self, RouterConfig, RouterState}, @@ -221,6 +225,12 @@ pub struct DependenciesBuilder { /// Era reader service pub era_reader: Option>, + /// Adapter for [ProtocolConfigurationReader] + pub protocol_configuration_reader_adapter: Option>, + + ///Protocol configuration reader service + pub protocol_configuration_reader: Option>, + /// Event Transmitter Service pub event_transmitter: Option>>, @@ -337,6 +347,8 @@ impl DependenciesBuilder { era_reader_adapter: None, era_checker: None, era_reader: None, + protocol_configuration_reader_adapter: None, + protocol_configuration_reader: None, event_transmitter: None, event_transmitter_channel: (None, None), api_version_provider: None, @@ -398,6 +410,7 @@ impl DependenciesBuilder { signer_registration_round_opener: self.get_signer_registration_round_opener().await?, era_checker: self.get_era_checker().await?, era_reader: self.get_era_reader().await?, + protocol_configuration_reader: self.get_protocol_configuration_reader().await?, event_transmitter: self.get_event_transmitter().await?, api_version_provider: self.get_api_version_provider().await?, stake_distribution_service: self.get_stake_distribution_service().await?, @@ -497,6 +510,23 @@ impl DependenciesBuilder { Ok(dependencies) } + /// Create dependencies for protocol configuration commands + pub async fn create_protocol_configuration_container( + &mut self, + ) -> Result { + let network = self.configuration.get_network().with_context( + || "Dependencies Builder can not get Cardano network while building protocol configuration container", + )?; + let dependencies = ProtocolConfigurationCommandDependenciesContainer { + network, + chain_observer: self.get_chain_observer().await?, + protocol_configuration_reader: self.get_protocol_configuration_reader().await?, + logger: self.root_logger(), + }; + + Ok(dependencies) + } + /// Create dependencies for database command pub async fn create_database_command_container( &mut self, diff --git a/mithril-aggregator/src/dependency_injection/builder/support/compatibility.rs b/mithril-aggregator/src/dependency_injection/builder/support/compatibility.rs index 9c25a9ae75b..c823f89b296 100644 --- a/mithril-aggregator/src/dependency_injection/builder/support/compatibility.rs +++ b/mithril-aggregator/src/dependency_injection/builder/support/compatibility.rs @@ -4,6 +4,9 @@ use mithril_common::api_version::APIVersionProvider; use mithril_common::entities::{Epoch, SupportedEra}; use mithril_era::adapters::{EraReaderAdapterBuilder, EraReaderDummyAdapter}; use mithril_era::{EraChecker, EraMarker, EraReader, EraReaderAdapter}; +use mithril_protocol_config::adapters::ProtocolConfigurationReaderCardanoChainAdapter; +use mithril_protocol_config::test::double::ProtocolConfigurationReaderDummyAdapter; +use mithril_protocol_config::{ProtocolConfigurationReader, ProtocolConfigurationReaderAdapter}; use crate::ExecutionEnvironment; use crate::dependency_injection::{DependenciesBuilder, DependenciesBuilderError, Result}; @@ -82,4 +85,34 @@ impl DependenciesBuilder { pub async fn get_era_checker(&mut self) -> Result> { get_dependency!(self.era_checker) } + + async fn build_protocol_configuration_reader( + &mut self, + ) -> Result> { + let protocol_configuration_adapter: Arc = + match self.configuration.environment() { + ExecutionEnvironment::Production => { + let parameters = self.configuration.protocol_configuration_reader_parameters(); + Arc::new(ProtocolConfigurationReaderCardanoChainAdapter::new( + parameters.address, + self.get_chain_observer().await?, + parameters.verification_key, + )) + } + _ => Arc::new(ProtocolConfigurationReaderDummyAdapter::from_markers( + vec![], //TODO + )), + }; + + Ok(Arc::new(ProtocolConfigurationReader::new( + protocol_configuration_adapter, + ))) + } + + /// [ProtocolConfigurationReader] service + pub async fn get_protocol_configuration_reader( + &mut self, + ) -> Result> { + get_dependency!(self.protocol_configuration_reader) + } } diff --git a/mithril-aggregator/src/dependency_injection/containers/mod.rs b/mithril-aggregator/src/dependency_injection/containers/mod.rs index e8109eec3a4..e64b8b7ab07 100644 --- a/mithril-aggregator/src/dependency_injection/containers/mod.rs +++ b/mithril-aggregator/src/dependency_injection/containers/mod.rs @@ -1,7 +1,9 @@ mod genesis; +mod protocol_configuration; mod serve; pub use genesis::GenesisCommandDependenciesContainer; +pub use protocol_configuration::ProtocolConfigurationCommandDependenciesContainer; pub use serve::*; use std::sync::Arc; diff --git a/mithril-aggregator/src/dependency_injection/containers/protocol_configuration.rs b/mithril-aggregator/src/dependency_injection/containers/protocol_configuration.rs new file mode 100644 index 00000000000..c5116eaff5d --- /dev/null +++ b/mithril-aggregator/src/dependency_injection/containers/protocol_configuration.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use mithril_protocol_config::ProtocolConfigurationReader; +use slog::Logger; + +use mithril_cardano_node_chain::chain_observer::ChainObserver; +use mithril_common::CardanoNetwork; + +/// Dependencies container for the protocol configuration commands +pub struct ProtocolConfigurationCommandDependenciesContainer { + /// Cardano network + pub network: CardanoNetwork, + + /// Chain observer + pub chain_observer: Arc, + + /// Protocol configuration reader + pub protocol_configuration_reader: Arc, + + /// Logger. + pub logger: Logger, +} diff --git a/mithril-aggregator/src/dependency_injection/containers/serve.rs b/mithril-aggregator/src/dependency_injection/containers/serve.rs index 98511d0b744..ffcbfc78b3d 100644 --- a/mithril-aggregator/src/dependency_injection/containers/serve.rs +++ b/mithril-aggregator/src/dependency_injection/containers/serve.rs @@ -1,3 +1,4 @@ +use mithril_protocol_config::ProtocolConfigurationReader; use slog::Logger; use std::sync::Arc; use tokio::sync::RwLock; @@ -68,6 +69,9 @@ pub struct ServeCommandDependenciesContainer { /// Era reader service pub(crate) era_reader: Arc, + /// Protocol configuration service + pub(crate) protocol_configuration_reader: Arc, + /// Event Transmitter Service pub(crate) event_transmitter: Arc>, diff --git a/mithril-aggregator/src/test/double/dummies.rs b/mithril-aggregator/src/test/double/dummies.rs index 73161f6c45e..2ca6384579d 100644 --- a/mithril-aggregator/src/test/double/dummies.rs +++ b/mithril-aggregator/src/test/double/dummies.rs @@ -1,7 +1,17 @@ +use std::collections::BTreeSet; + use chrono::Utc; use uuid::Uuid; -use mithril_common::test::double::{Dummy, fake_data}; +use mithril_common::{ + entities::{ + CardanoBlocksTransactionsSigningConfig, CardanoTransactionsSigningConfig, Epoch, + ProtocolParameters, SignedEntityTypeDiscriminants, + }, + test::double::{Dummy, fake_data}, +}; + +use crate::commands::HumanReadableProtocolConfiguration; mod record { use mithril_common::entities::{ProtocolMessage, SignedEntityType}; @@ -101,3 +111,20 @@ mod entities { } } } + +impl Dummy for HumanReadableProtocolConfiguration { + fn dummy() -> Self { + HumanReadableProtocolConfiguration::new( + Epoch(42), + ProtocolParameters::new(10, 20, 0.123), + Some(CardanoTransactionsSigningConfig::dummy()), + Some(CardanoBlocksTransactionsSigningConfig::dummy()), + BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + ]), + ) + } +} diff --git a/mithril-aggregator/src/tools/mod.rs b/mithril-aggregator/src/tools/mod.rs index 71ebc698ffd..eb363796d0c 100644 --- a/mithril-aggregator/src/tools/mod.rs +++ b/mithril-aggregator/src/tools/mod.rs @@ -3,6 +3,7 @@ mod era; pub mod file_archiver; pub mod file_size; mod genesis; +mod protocol_configuration; pub mod signer_importer; mod single_signature_authenticator; pub mod url_sanitizer; @@ -13,6 +14,7 @@ pub use era::EraTools; #[cfg(feature = "future_snark")] pub use genesis::GenesisSignedPayload; pub use genesis::GenesisTools; +pub use protocol_configuration::ProtocolConfigurationTools; pub use single_signature_authenticator::*; pub use vacuum_tracker::VacuumTracker; diff --git a/mithril-aggregator/src/tools/protocol_configuration.rs b/mithril-aggregator/src/tools/protocol_configuration.rs new file mode 100644 index 00000000000..e544930424f --- /dev/null +++ b/mithril-aggregator/src/tools/protocol_configuration.rs @@ -0,0 +1,594 @@ +use std::collections::BTreeMap; + +use anyhow::Context; +use mithril_cardano_node_chain::entities::{TxDatumBuilder, TxDatumFieldValue}; +use mithril_common::{ + StdResult, crypto_helper::ProtocolConfigurationMarkersSigner, entities::Epoch, +}; +use mithril_protocol_config::{ + ProtocolConfigurationForEpoch, ProtocolConfigurationMarker, + adapters::ProtocolConfigurationMarkersPayloadCardanoChain, + configuration_computer::ConfigurationComputerFromMarkers, +}; +use slog::{Logger, info, warn}; +use thiserror::Error; + +use crate::{ + commands::HumanReadableProtocolConfiguration, + dependency_injection::ProtocolConfigurationCommandDependenciesContainer, +}; + +const EPOCH_OFFSET: u64 = 3; +const DATUM_MAX_SIZE_KB: usize = 10; + +#[derive(Debug, Error)] +pub enum ProtocolConfigurationVerifierError { + #[error("Configuration to import for {0:?}, is not the same has configuration on chain")] + NotSameConfigurationForEpoch(Epoch), + + #[error("Size of datum is {0:?} KB (Maximum authorized size is {DATUM_MAX_SIZE_KB} KB")] + DatumMaxSizeExceeded(f64), +} + +type ProtocolConfigurationToolsResult = StdResult; + +/// Configuration for the protocol configuration tools. +pub struct ProtocolConfigurationToolsConfiguration { + /// Current epoch. + pub epoch: Epoch, + + //On chain configurations by Epoch. + pub on_chain_configurations: ConfigurationComputerFromMarkers, +} + +pub struct ProtocolConfigurationTools { + configuration: ProtocolConfigurationToolsConfiguration, + + logger: Logger, +} + +impl ProtocolConfigurationTools { + pub fn new(configuration: ProtocolConfigurationToolsConfiguration, logger: Logger) -> Self { + Self { + configuration, + logger, + } + } + + pub async fn from_dependencies( + dependencies: ProtocolConfigurationCommandDependenciesContainer, + ) -> StdResult { + let epoch = dependencies + .chain_observer + .get_current_epoch() + .await? + .with_context(|| "Chain observer can not retrieve current epoch")?; + + let on_chain_configurations = dependencies + .protocol_configuration_reader + .read_mithril_protocol_configurations() + .await?; + + let configuration = ProtocolConfigurationToolsConfiguration { + epoch, + on_chain_configurations, + }; + + Ok(Self::new(configuration, dependencies.logger)) + } + + /// Verify if configuration to import share same windows as on chain configuration for current epoch + pub fn verify_configurations_against_chain( + &self, + configurations_to_import: Vec, + ) -> Result<(), ProtocolConfigurationVerifierError> { + let current_epoch = self.configuration.epoch; + info!(&self.logger, "Current epoch is {}", current_epoch); + + let markers_from_chain = self.configuration.on_chain_configurations.clone(); + let markers_to_import = to_configuration_computer_from_markers(configurations_to_import); + + let epoch_range_to_verify = (current_epoch.0 - EPOCH_OFFSET)..=current_epoch.0; + info!( + &self.logger, + "Verifying configurations for epoch range [{:?}]", epoch_range_to_verify + ); + + for epoch in epoch_range_to_verify.map(Epoch) { + let marker_to_import = markers_to_import.get_network_configuration(epoch); + let marker_on_chain = markers_from_chain.get_network_configuration(epoch); + if marker_on_chain.is_some() { + if marker_to_import != marker_on_chain { + return Err( + ProtocolConfigurationVerifierError::NotSameConfigurationForEpoch(epoch), + ); + } + } else { + warn!( + &self.logger, + "No configuration found on chain for {}, this is normal behavior if there is no protocol configuration marker on chain", + epoch + ); + } + } + Ok(()) + } + + /// Generate TxDatum for Protocol Configuration + pub fn generate_tx_datum( + &self, + configurations: Vec, + protocol_configuration_markers_signer: &ProtocolConfigurationMarkersSigner, + ) -> ProtocolConfigurationToolsResult { + let mut markers: Vec = Vec::new(); + for configuration in configurations { + let protocol_configuration_for_epoch: ProtocolConfigurationForEpoch = + configuration.clone().into(); + let marker: ProtocolConfigurationMarker = ProtocolConfigurationMarker::new( + configuration.epoch, + protocol_configuration_for_epoch.to_cbor_hex()?, + ); + markers.push(marker); + } + let signed_markers_payload = ProtocolConfigurationMarkersPayloadCardanoChain::new(markers) + .sign(protocol_configuration_markers_signer)?; + + let tx_datum = TxDatumBuilder::new() + .add_field(TxDatumFieldValue::Bytes( + signed_markers_payload.to_json_hex()?, + )) + .build()?; + Ok(tx_datum.0) + } + + fn verify_tx_datum_size( + &self, + datum: String, + ) -> Result<(), ProtocolConfigurationVerifierError> { + let size_bytes = datum.len(); + let size_kb = size_bytes as f64 / 1024.0; + + println!("Datum size: {:.2} KB", size_kb); + + if size_bytes > DATUM_MAX_SIZE_KB * 1024 { + return Err(ProtocolConfigurationVerifierError::DatumMaxSizeExceeded( + size_kb, + )); + } + + Ok(()) + } +} + +impl From for ProtocolConfigurationForEpoch { + fn from(config: HumanReadableProtocolConfiguration) -> Self { + ProtocolConfigurationForEpoch { + protocol_parameters: config.protocol_parameters, + enabled_signed_entity_types: config.enabled_signed_entity_types, + cardano_transactions: config.cardano_transaction_signing_config, + cardano_blocks_transactions: config.cardano_blocks_transactions_signing_config, + } + } +} + +fn to_configuration_computer_from_markers( + configs: Vec, +) -> ConfigurationComputerFromMarkers { + let mut markers = BTreeMap::new(); + + for config in configs { + markers.insert(config.epoch, ProtocolConfigurationForEpoch::from(config)); + } + ConfigurationComputerFromMarkers::new(markers) +} + +#[cfg(test)] +mod tests { + use mithril_common::{ + entities::{ + BlockNumber, BlockNumberOffset, CardanoBlocksTransactionsSigningConfig, + CardanoTransactionsSigningConfig, Epoch, ProtocolParameters, + SignedEntityTypeDiscriminants, + }, + test::double::Dummy, + }; + use std::collections::{BTreeMap, BTreeSet}; + + use crate::test::TestLogger; + + use super::*; + + fn build_tools_dummy() -> ProtocolConfigurationTools { + let configuration = ProtocolConfigurationToolsConfiguration { + epoch: Epoch(30), + on_chain_configurations: ConfigurationComputerFromMarkers::new(BTreeMap::new()), + }; + ProtocolConfigurationTools::new(configuration, TestLogger::stdout()) + } + + fn build_tools( + current_epoch: Epoch, + on_chain_configurations: ConfigurationComputerFromMarkers, + logger: Logger, + ) -> ProtocolConfigurationTools { + let configuration = ProtocolConfigurationToolsConfiguration { + epoch: current_epoch, + on_chain_configurations, + }; + ProtocolConfigurationTools::new(configuration, logger) + } + + #[test] + fn test_from_human_readable_protocol_configuration() { + let human_readable_conf = HumanReadableProtocolConfiguration { + epoch: Epoch(42), + protocol_parameters: ProtocolParameters { + k: 9, + m: 77, + phi_f: 0.5, + }, + enabled_signed_entity_types: BTreeSet::from_iter(vec![ + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoTransactions, + ]), + cardano_transaction_signing_config: Some(CardanoTransactionsSigningConfig { + security_parameter: BlockNumberOffset(100), + step: BlockNumber(10), + }), + cardano_blocks_transactions_signing_config: Some( + CardanoBlocksTransactionsSigningConfig { + security_parameter: BlockNumberOffset(150), + step: BlockNumber(20), + }, + ), + }; + + let expected_protocol_configuration_for_epoch = ProtocolConfigurationForEpoch { + protocol_parameters: ProtocolParameters { + k: 9, + m: 77, + phi_f: 0.5, + }, + enabled_signed_entity_types: BTreeSet::from_iter(vec![ + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoTransactions, + ]), + cardano_transactions: Some(CardanoTransactionsSigningConfig { + security_parameter: BlockNumberOffset(100), + step: BlockNumber(10), + }), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig { + security_parameter: BlockNumberOffset(150), + step: BlockNumber(20), + }), + }; + + assert_eq!( + ProtocolConfigurationForEpoch::from(human_readable_conf), + expected_protocol_configuration_for_epoch + ); + } + + #[test] + fn generate_tx_datum_ok() { + let configurations = vec![HumanReadableProtocolConfiguration { + epoch: Epoch(42), + protocol_parameters: ProtocolParameters { + k: 9, + m: 77, + phi_f: 0.5, + }, + enabled_signed_entity_types: BTreeSet::from_iter(vec![ + SignedEntityTypeDiscriminants::MithrilStakeDistribution, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoTransactions, + ]), + cardano_transaction_signing_config: Some(CardanoTransactionsSigningConfig { + security_parameter: BlockNumberOffset(100), + step: BlockNumber(10), + }), + cardano_blocks_transactions_signing_config: None, + }]; + let signer = ProtocolConfigurationMarkersSigner::create_deterministic_signer(); + let tools = build_tools_dummy(); + assert!(tools.generate_tx_datum(configurations, &signer).is_ok()); + } + + #[test] + fn verify_tx_datum_size_is_ok_with_datum_under_10_kb() { + let tools = build_tools_dummy(); + assert!(tools.verify_tx_datum_size("tx datum under 10 kb".to_string()).is_ok()); + } + + #[test] + fn verify_tx_datum_size_is_ok_with_datum_from_dummy_configuration() { + let configurations = vec![ + HumanReadableProtocolConfiguration { + epoch: Epoch(42), + ..Dummy::dummy() + }, + HumanReadableProtocolConfiguration { + epoch: Epoch(53), + ..Dummy::dummy() + }, + ]; + let signer = ProtocolConfigurationMarkersSigner::create_deterministic_signer(); + let tools = build_tools_dummy(); + let datum = tools + .generate_tx_datum(configurations, &signer) + .expect("generate_tx_datum should not fail"); + + assert!(tools.verify_tx_datum_size(datum).is_ok()); + } + + mod verify_configurations_against_chain { + use super::*; + + /// instanciate a unique ProtocolConfigurationForEpoch based on char + fn fake_configuration(conf: char) -> ProtocolConfigurationForEpoch { + ProtocolConfigurationForEpoch { + protocol_parameters: ProtocolParameters { + k: conf as u64, + m: conf as u64, + phi_f: 1.2, + }, + cardano_transactions: Some(CardanoTransactionsSigningConfig::dummy()), + cardano_blocks_transactions: Some(CardanoBlocksTransactionsSigningConfig::dummy()), + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ]), + } + } + + /// Instanciate a HumanReadableProtocolConfiguration at epoch with a unique char configuration + fn fake_configuration_to_import( + epoch: Epoch, + conf: char, + ) -> HumanReadableProtocolConfiguration { + HumanReadableProtocolConfiguration { + epoch, + protocol_parameters: ProtocolParameters { + k: conf as u64, + m: conf as u64, + phi_f: 1.2, + }, + cardano_transaction_signing_config: Some(CardanoTransactionsSigningConfig::dummy()), + cardano_blocks_transactions_signing_config: Some( + CardanoBlocksTransactionsSigningConfig::dummy(), + ), + enabled_signed_entity_types: BTreeSet::from([ + SignedEntityTypeDiscriminants::CardanoTransactions, + SignedEntityTypeDiscriminants::CardanoBlocksTransactions, + SignedEntityTypeDiscriminants::CardanoDatabase, + SignedEntityTypeDiscriminants::CardanoStakeDistribution, + ]), + } + } + + fn build_on_chain_markers( + configurations: Vec<(Epoch, char)>, + ) -> ConfigurationComputerFromMarkers { + let mut on_chain_markers = BTreeMap::new(); + for conf in configurations { + on_chain_markers.insert(conf.0, fake_configuration(conf.1)); + } + ConfigurationComputerFromMarkers::new(on_chain_markers) + } + + fn build_configurations_to_import( + configurations: Vec<(Epoch, char)>, + ) -> Vec { + configurations + .iter() + .map(|conf| fake_configuration_to_import(conf.0, conf.1)) + .collect() + } + + #[test] + fn ok_with_only_one_same_epoch_conf_in_offset_window() { + let (logger, log_inspector) = TestLogger::memory(); + + let current_epoch = Epoch(47); + let mut on_chain_markers = BTreeMap::new(); + on_chain_markers.insert(Epoch(38), fake_configuration('A')); //conf outside offset window + on_chain_markers.insert(Epoch(44), fake_configuration('B')); //conf inside offset window + let on_chain_configurations = ConfigurationComputerFromMarkers::new(on_chain_markers); + + let configurations_to_import = + build_configurations_to_import(vec![(Epoch(44), 'B'), (Epoch(56), 'Z')]); + + let tools = build_tools(current_epoch, on_chain_configurations, logger); + assert!( + tools + .verify_configurations_against_chain(configurations_to_import) + .is_ok() + ); + + assert!( + log_inspector.contains_log("Verifying configurations for epoch range [44..=47]") + ) + } + + #[test] + fn ok_with_only_one_same_epoch_conf_outside_offset_window_with_fallback() { + let current_epoch = Epoch(47); + let mut on_chain_markers = BTreeMap::new(); + on_chain_markers.insert(Epoch(31), fake_configuration('A')); //conf outside offset window + on_chain_markers.insert(Epoch(38), fake_configuration('B')); //conf outside offset window + let on_chain_configurations = ConfigurationComputerFromMarkers::new(on_chain_markers); + + let configurations_to_import = vec![ + fake_configuration_to_import(Epoch(38), 'B'), + fake_configuration_to_import(Epoch(56), 'Z'), + ]; + + let tools = build_tools(current_epoch, on_chain_configurations, TestLogger::stdout()); + + assert!( + tools + .verify_configurations_against_chain(configurations_to_import) + .is_ok() + ); + } + + #[test] + fn ok_with_only_one_same_conf_at_different_epoch() { + let current_epoch = Epoch(47); + let mut on_chain_markers = BTreeMap::new(); + on_chain_markers.insert(Epoch(31), fake_configuration('A')); //conf outside offset window + on_chain_markers.insert(Epoch(38), fake_configuration('B')); //conf outside offset window + let on_chain_configurations = ConfigurationComputerFromMarkers::new(on_chain_markers); + + let configurations_to_import = vec![ + fake_configuration_to_import(Epoch(40), 'B'), + fake_configuration_to_import(Epoch(56), 'Z'), + ]; + + let tools = build_tools(current_epoch, on_chain_configurations, TestLogger::stdout()); + + assert!( + tools + .verify_configurations_against_chain(configurations_to_import) + .is_ok() + ); + } + + #[test] + fn ko_because_last_known_on_chain_configuration_b_for_offset_window_is_not_repeated() { + let current_epoch = Epoch(47); + let mut on_chain_markers = BTreeMap::new(); + on_chain_markers.insert(Epoch(31), fake_configuration('A')); //conf outside offset window + on_chain_markers.insert(Epoch(38), fake_configuration('B')); //conf outside offset window + let on_chain_configurations = ConfigurationComputerFromMarkers::new(on_chain_markers); + + let configurations_to_import = vec![ + fake_configuration_to_import(Epoch(40), 'C'), + fake_configuration_to_import(Epoch(56), 'Z'), + ]; + + let tools = build_tools(current_epoch, on_chain_configurations, TestLogger::stdout()); + let result = tools.verify_configurations_against_chain(configurations_to_import); + + assert!(matches!( + result.unwrap_err(), + ProtocolConfigurationVerifierError::NotSameConfigurationForEpoch(Epoch(44)) + )); + } + + #[test] + fn full_offset_window_have_to_be_repeated_if_it_have_different_configuration() { + let current_epoch = Epoch(47); + let mut on_chain_markers = BTreeMap::new(); + on_chain_markers.insert(Epoch(43), fake_configuration('A')); //conf outside offset window + on_chain_markers.insert(Epoch(44), fake_configuration('B')); //conf inside offset window + on_chain_markers.insert(Epoch(45), fake_configuration('C')); //conf inside offset window + on_chain_markers.insert(Epoch(46), fake_configuration('D')); //conf inside offset window + on_chain_markers.insert(Epoch(47), fake_configuration('E')); //conf inside offset window + let on_chain_configurations = ConfigurationComputerFromMarkers::new(on_chain_markers); + + let configurations_to_import = vec![ + fake_configuration_to_import(Epoch(44), 'B'), + fake_configuration_to_import(Epoch(45), 'C'), + fake_configuration_to_import(Epoch(46), 'D'), + fake_configuration_to_import(Epoch(47), 'E'), + fake_configuration_to_import(Epoch(53), 'Z'), + ]; + + let tools = build_tools( + current_epoch, + on_chain_configurations.clone(), + TestLogger::stdout(), + ); + assert!( + tools + .verify_configurations_against_chain(configurations_to_import) + .is_ok() + ); + + //It fail if one of epoch/conf from offset window is not repeated + let bad_configurations_to_import = vec![ + fake_configuration_to_import(Epoch(44), 'B'), + fake_configuration_to_import(Epoch(45), 'X'), + fake_configuration_to_import(Epoch(46), 'D'), + fake_configuration_to_import(Epoch(47), 'E'), + fake_configuration_to_import(Epoch(53), 'Z'), + ]; + + let tools = build_tools(current_epoch, on_chain_configurations, TestLogger::stdout()); + let result = tools.verify_configurations_against_chain(bad_configurations_to_import); + + assert!(matches!( + result.unwrap_err(), + ProtocolConfigurationVerifierError::NotSameConfigurationForEpoch(Epoch(45)) + )); + } + + #[test] + fn window_to_repeat_dont_have_to_be_exactly_at_same_epoch_as_long_as_it_can_fallback_to_same_configuration() + { + let current_epoch = Epoch(47); + let on_chain_configurations = + build_on_chain_markers(vec![(Epoch(30), 'A'), (Epoch(44), 'B'), (Epoch(47), 'B')]); + + let configurations_to_import = vec![ + fake_configuration_to_import(Epoch(32), 'A'), + fake_configuration_to_import(Epoch(40), 'B'), + fake_configuration_to_import(Epoch(53), 'Z'), + ]; + + let tools = build_tools( + current_epoch, + on_chain_configurations.clone(), + TestLogger::stdout(), + ); + assert!( + tools + .verify_configurations_against_chain(configurations_to_import) + .is_ok() + ); + } + + #[test] + fn verification_with_no_markers_on_chain_should_be_ok() { + let current_epoch = Epoch(47); + let on_chain_configurations = ConfigurationComputerFromMarkers::new(BTreeMap::new()); + + let configurations_to_import = vec![fake_configuration_to_import(Epoch(53), 'Z')]; + + let tools = build_tools( + current_epoch, + on_chain_configurations.clone(), + TestLogger::stdout(), + ); + assert!( + tools + .verify_configurations_against_chain(configurations_to_import) + .is_ok() + ); + } + + #[test] + fn verification_with_no_markers_to_import_should_be_ko() { + let current_epoch = Epoch(47); + let on_chain_configurations = + build_on_chain_markers(vec![(Epoch(30), 'A'), (Epoch(44), 'B')]); + + let tools = build_tools( + current_epoch, + on_chain_configurations.clone(), + TestLogger::stdout(), + ); + let result = tools.verify_configurations_against_chain(vec![]); + + assert!(matches!( + result.unwrap_err(), + ProtocolConfigurationVerifierError::NotSameConfigurationForEpoch(Epoch(44)) + )); + } + } +} diff --git a/mithril-aggregator/tests/human_readable_protocol_configuration.json b/mithril-aggregator/tests/human_readable_protocol_configuration.json new file mode 100644 index 00000000000..f51a88685b8 --- /dev/null +++ b/mithril-aggregator/tests/human_readable_protocol_configuration.json @@ -0,0 +1,43 @@ +[ + { + "epoch": 42, + "protocol_parameters": { + "k": 9, + "m": 77, + "phi_f": 0.5 + }, + "cardano_transactions_signing_config": { + "security_parameter": 100, + "step": 10 + }, + "enabled_signed_entity_types": + [ + "MithrilStakeDistribution", + "CardanoDatabase", + "CardanoTransactions" + ] + }, + { + "epoch": 43, + "protocol_parameters": { + "k": 10, + "m": 88, + "phi_f": 0.7 + }, + "cardano_transactions_signing_config": { + "security_parameter": 100, + "step": 10 + }, + "cardano_blocks_transactions_signing_config": { + "security_parameter": 150, + "step": 20 + }, + "enabled_signed_entity_types": + [ + "MithrilStakeDistribution", + "CardanoDatabase", + "CardanoTransactions", + "CardanoBlocksTransactions" + ] + } +] \ No newline at end of file diff --git a/mithril-common/src/crypto_helper/ed25519_alias.rs b/mithril-common/src/crypto_helper/ed25519_alias.rs index 73daf34b3c0..9d3694a3258 100644 --- a/mithril-common/src/crypto_helper/ed25519_alias.rs +++ b/mithril-common/src/crypto_helper/ed25519_alias.rs @@ -15,6 +15,15 @@ pub mod era { pub type EraMarkersVerifier = Ed25519Verifier; /// [EraMarkersSigner] and [EraMarkersVerifier] related errors. pub type EraMarkersVerifierError = Ed25519VerifierError; + + /// Protocol Configuration markers verifier secret key + pub type ProtocolConfigurationMarkersVerifierSecretKey = Ed25519SecretKey; + /// Protocol Configuration markers verifier verification key + pub type ProtocolConfigurationMarkersVerifierVerificationKey = Ed25519VerificationKey; + /// Protocol Configuration markers verifier signature + pub type ProtocolConfigurationMarkersVerifierSignature = Ed25519Signature; + /// A cryptographic signer that is responsible for signing the ProtocolConfigurationMarkers + pub type ProtocolConfigurationMarkersSigner = Ed25519Signer; } pub mod manifest { diff --git a/mithril-common/src/entities/type_alias.rs b/mithril-common/src/entities/type_alias.rs index 2c4b7b59610..80f211b2fe1 100644 --- a/mithril-common/src/entities/type_alias.rs +++ b/mithril-common/src/entities/type_alias.rs @@ -63,6 +63,9 @@ pub type HexEncodedDigest = HexEncodedKey; /// Hex encoded Era Markers Secret Key pub type HexEncodedEraMarkersSecretKey = HexEncodedKey; +/// Hex encoded Protocol Configuration Markers Secret Key +pub type HexEncodedProtocolConfigurationMarkersSecretKey = HexEncodedKey; + /// Number of SPOs pub type TotalSPOs = u32;