diff --git a/Cargo.lock b/Cargo.lock index 49cc06580d9..2680114c761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2067,7 +2067,7 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.2.11" +version = "0.2.12" dependencies = [ "async-trait", "bech32", diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 6709df96c21..12235e7b8cd 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-common" -version = "0.2.11" +version = "0.2.12" authors = { workspace = true } edition = { workspace = true } documentation = { workspace = true } diff --git a/mithril-common/src/beacon_provider.rs b/mithril-common/src/beacon_provider.rs index 27044873e8c..1bac48e8b39 100644 --- a/mithril-common/src/beacon_provider.rs +++ b/mithril-common/src/beacon_provider.rs @@ -79,7 +79,7 @@ impl BeaconProvider for BeaconProviderImpl { #[cfg(test)] mod tests { - use crate::chain_observer::{ChainObserver, ChainObserverError}; + use crate::chain_observer::{ChainAddress, ChainObserver, ChainObserverError, TxDatum}; use crate::digesters::DumbImmutableFileObserver; use crate::entities::{Epoch, StakeDistribution}; @@ -89,6 +89,13 @@ mod tests { #[async_trait] impl ChainObserver for DumbChainObserver { + async fn get_current_datums( + &self, + _address: &ChainAddress, + ) -> Result, ChainObserverError> { + Ok(Vec::new()) + } + async fn get_current_epoch(&self) -> Result, ChainObserverError> { Ok(Some(Epoch(42))) } diff --git a/mithril-common/src/chain_observer/cli_observer.rs b/mithril-common/src/chain_observer/cli_observer.rs index c4b88c1f885..3d64efea43e 100644 --- a/mithril-common/src/chain_observer/cli_observer.rs +++ b/mithril-common/src/chain_observer/cli_observer.rs @@ -1,18 +1,22 @@ use async_trait::async_trait; use nom::IResult; +use rand_core::RngCore; use serde_json::Value; +use std::collections::HashMap; use std::error::Error; use std::fs; use std::path::PathBuf; use tokio::process::Command; use crate::chain_observer::interface::*; +use crate::chain_observer::{ChainAddress, TxDatum}; use crate::crypto_helper::{KESPeriod, OpCert, SerDeShelleyFileFormat}; use crate::entities::{Epoch, StakeDistribution}; use crate::CardanoNetwork; #[async_trait] pub trait CliRunner { + async fn launch_utxo(&self, address: &str) -> Result>; async fn launch_stake_distribution(&self) -> Result>; async fn launch_stake_snapshot( &self, @@ -43,6 +47,29 @@ impl CardanoCliRunner { } } + fn random_out_file() -> Result> { + let mut rng = rand_core::OsRng; + let dir = std::env::temp_dir().join("cardano-cli-runner"); + if !dir.exists() { + fs::create_dir_all(&dir)?; + } + Ok(dir.join(format!("{}.out", rng.next_u64()))) + } + + fn command_for_utxo(&self, address: &str, out_file: PathBuf) -> Command { + let mut command = self.get_command(); + command + .arg("query") + .arg("utxo") + .arg("--address") + .arg(address) + .arg("--out-file") + .arg(out_file); + self.post_config_command(&mut command); + + command + } + fn command_for_stake_distribution(&self) -> Command { let mut command = self.get_command(); command.arg("query").arg("stake-distribution"); @@ -110,6 +137,27 @@ impl CardanoCliRunner { #[async_trait] impl CliRunner for CardanoCliRunner { + async fn launch_utxo(&self, address: &str) -> Result> { + let out_file = Self::random_out_file()?; + let output = self + .command_for_utxo(address, out_file.clone()) + .output() + .await?; + + if output.status.success() { + Ok(fs::read_to_string(out_file)?.trim().to_string()) + } else { + let message = String::from_utf8_lossy(&output.stderr); + + Err(format!( + "Error launching command {:?}, error = '{}'", + self.command_for_utxo(address, out_file), + message + ) + .into()) + } + } + async fn launch_stake_distribution(&self) -> Result> { let output = self.command_for_stake_distribution().output().await?; @@ -255,6 +303,30 @@ impl ChainObserver for CardanoCliChainObserver { } } + async fn get_current_datums( + &self, + address: &ChainAddress, + ) -> Result, ChainObserverError> { + let output = self + .cli_runner + .launch_utxo(address) + .await + .map_err(ChainObserverError::General)?; + let v: HashMap = serde_json::from_str(&output).map_err(|e| { + ChainObserverError::InvalidContent( + format!("Error: {e:?}, output was = '{output}'").into(), + ) + })?; + + Ok(v.values() + .filter_map(|v| { + v.get("inlineDatum") + .filter(|datum| !datum.is_null()) + .map(|datum| TxDatum(datum.to_string())) + }) + .collect()) + } + async fn get_current_stake_distribution( &self, ) -> Result, ChainObserverError> { @@ -337,6 +409,45 @@ mod tests { #[async_trait] impl CliRunner for TestCliRunner { + async fn launch_utxo( + &self, + _address: &str, + ) -> Result> { + let output = r#" +{ + "1fd4d3e131afe3c8b212772a3f3083d2fbc6b2a7b20e54e4ff08e001598818d8#0": { + "address": "addr_test1vpcr3he05gemue6eyy0c9clajqnnww8aa2l3jszjdlszjhq093qrn", + "datum": null, + "inlineDatum": { + "constructor": 0, + "fields": [ + { + "bytes": "5b0a20207b0a20202020226e616d65223a20227468616c6573222c0a202020202265706f6368223a203132330a20207d2c0a20207b0a20202020226e616d65223a20227079746861676f726173222c0a202020202265706f6368223a206e756c6c0a20207d0a5d0a" + } + ] + }, + "inlineDatumhash": "b97cbaa0dc5b41864c83c2f625d9bc2a5f3e6b5cd5071c14a2090e630e188c80", + "referenceScript": null, + "value": { + "lovelace": 10000000 + } + }, + "1fd4d3e131afe3c8b212772a3f3083d2fbc6b2a7b20e54e4ff08e001598818d8#1": { + "address": "addr_test1vpcr3he05gemue6eyy0c9clajqnnww8aa2l3jszjdlszjhq093qrn", + "datum": null, + "datumhash": null, + "inlineDatum": null, + "referenceScript": null, + "value": { + "lovelace": 9989656678 + } + } +} +"#; + + Ok(output.to_string()) + } + async fn launch_stake_distribution(&self) -> Result> { let output = r#" PoolId Stake frac @@ -471,6 +582,14 @@ pool1qz2vzszautc2c8mljnqre2857dpmheq7kgt6vav0s38tvvhxm6w 1.051e-6 ); } + #[tokio::test] + async fn test_get_current_datums() { + let observer = CardanoCliChainObserver::new(Box::new(TestCliRunner {})); + let address = "addrtest_123456".to_string(); + let datums = observer.get_current_datums(&address).await.unwrap(); + assert_eq!(vec![TxDatum("{\"constructor\":0,\"fields\":[{\"bytes\":\"5b0a20207b0a20202020226e616d65223a20227468616c6573222c0a202020202265706f6368223a203132330a20207d2c0a20207b0a20202020226e616d65223a20227079746861676f726173222c0a202020202265706f6368223a206e756c6c0a20207d0a5d0a\"}]}".to_string())], datums); + } + #[tokio::test] async fn test_get_current_stake_value() { let observer = CardanoCliChainObserver::new(Box::new(TestCliRunner {})); diff --git a/mithril-common/src/chain_observer/fake_observer.rs b/mithril-common/src/chain_observer/fake_observer.rs index 5501f78f60d..95ba2293bb4 100644 --- a/mithril-common/src/chain_observer/fake_observer.rs +++ b/mithril-common/src/chain_observer/fake_observer.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use tokio::sync::RwLock; use crate::chain_observer::interface::*; +use crate::chain_observer::{ChainAddress, TxDatum}; use crate::crypto_helper::{KESPeriod, OpCert}; use crate::{entities::*, test_utils::fake_data}; @@ -16,6 +17,11 @@ pub struct FakeObserver { /// /// [get_current_epoch]: ChainObserver::get_current_epoch pub current_beacon: RwLock>, + + /// A list of [TxDatum], used by [get_current_datums] + /// + /// [get_current_datums]: ChainObserver::get_current_datums + pub datums: RwLock>, } impl FakeObserver { @@ -24,6 +30,7 @@ impl FakeObserver { Self { signers: RwLock::new(vec![]), current_beacon: RwLock::new(current_beacon), + datums: RwLock::new(vec![]), } } @@ -44,6 +51,13 @@ impl FakeObserver { let mut signers = self.signers.write().await; *signers = new_signers; } + + /// Set the datums that will used to compute the result of + /// [get_current_datums][ChainObserver::get_current_datums]. + pub async fn set_datums(&self, new_datums: Vec) { + let mut datums = self.datums.write().await; + *datums = new_datums; + } } impl Default for FakeObserver { @@ -57,6 +71,14 @@ impl Default for FakeObserver { #[async_trait] impl ChainObserver for FakeObserver { + async fn get_current_datums( + &self, + _address: &ChainAddress, + ) -> Result, ChainObserverError> { + let datums = self.datums.read().await; + Ok(datums.to_vec()) + } + async fn get_current_epoch(&self) -> Result, ChainObserverError> { Ok(self .current_beacon @@ -116,4 +138,21 @@ mod tests { "get current stake distribution should not fail and should not be empty" ); } + + #[tokio::test] + async fn test_get_current_datums() { + let fake_address = "addr_test_123456".to_string(); + let fake_datums = vec![ + TxDatum("tx_datum_1".to_string()), + TxDatum("tx_datum_2".to_string()), + ]; + let fake_observer = FakeObserver::new(None); + fake_observer.set_datums(fake_datums.clone()).await; + let datums = fake_observer + .get_current_datums(&fake_address) + .await + .expect("get_current_datums should not fail"); + + assert_eq!(fake_datums, datums); + } } diff --git a/mithril-common/src/chain_observer/interface.rs b/mithril-common/src/chain_observer/interface.rs index c0774f24694..cf580d083a8 100644 --- a/mithril-common/src/chain_observer/interface.rs +++ b/mithril-common/src/chain_observer/interface.rs @@ -7,6 +7,8 @@ use mockall::automock; use std::error::Error as StdError; use thiserror::Error; +use super::{ChainAddress, TxDatum}; + /// [ChainObserver] related errors. #[derive(Debug, Error)] pub enum ChainObserverError { @@ -23,6 +25,12 @@ pub enum ChainObserverError { #[automock] #[async_trait] pub trait ChainObserver: Sync + Send { + /// Retrieve the datums associated to and address + async fn get_current_datums( + &self, + address: &ChainAddress, + ) -> Result, ChainObserverError>; + /// Retrieve the current epoch of the Cardano network async fn get_current_epoch(&self) -> Result, ChainObserverError>; diff --git a/mithril-common/src/chain_observer/mod.rs b/mithril-common/src/chain_observer/mod.rs index 27e9a1203a5..2e85adbd97f 100644 --- a/mithril-common/src/chain_observer/mod.rs +++ b/mithril-common/src/chain_observer/mod.rs @@ -4,8 +4,10 @@ mod cli_observer; #[cfg(any(test, feature = "test_only"))] mod fake_observer; mod interface; +mod model; pub use cli_observer::{CardanoCliChainObserver, CardanoCliRunner}; #[cfg(any(test, feature = "test_only"))] pub use fake_observer::FakeObserver; pub use interface::{ChainObserver, ChainObserverError}; +pub use model::{ChainAddress, TxDatum}; diff --git a/mithril-common/src/chain_observer/model.rs b/mithril-common/src/chain_observer/model.rs new file mode 100644 index 00000000000..b957f3105ac --- /dev/null +++ b/mithril-common/src/chain_observer/model.rs @@ -0,0 +1,139 @@ +use serde_json::Value; +use std::{collections::HashMap, error::Error as StdError}; +use thiserror::Error; + +/// [ChainAddress] represents an on chain address +pub type ChainAddress = String; + +/// [TxDatum] related errors. +#[derive(Debug, Error)] +pub enum TxDatumError { + /// Generic [TxDatum] error. + #[error("general error {0}")] + _General(Box), + + /// Error raised when the content could not be parsed. + #[error("could not parse content: {0}")] + InvalidContent(Box), +} + +/// [TxDatum] represents transaction Datum +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TxDatum(pub String); + +impl TxDatum { + /// Retrieves the nth field of the datum with given type + pub fn get_nth_field_by_type( + &self, + type_name: &str, + index: usize, + ) -> Result> { + let tx_datum_raw = &self.0; + // 1- Parse the Utxo raw data to a hashmap + let v: HashMap = serde_json::from_str(tx_datum_raw).map_err(|e| { + TxDatumError::InvalidContent( + format!("Error: {e:?}, tx datum was = '{tx_datum_raw}'").into(), + ) + })?; + // 2- Convert the 'fields' entry to a vec of json objects + let fields = v.get("fields").ok_or_else(|| { + TxDatumError::InvalidContent( + format!("Error: missing 'fields' entry, tx datum was = '{tx_datum_raw}'").into(), + ) + })?.as_array().ok_or_else(|| { + TxDatumError::InvalidContent( + format!("Error: 'fields' entry is not correctly structured, tx datum was = '{tx_datum_raw}'").into(), + ) + })?; + // 3- Filter the vec (keep the ones that match the given type), and retrieve the nth entry of this filtered vec + let field_value = fields + .iter() + .filter(|&field| field.get(type_name).is_some()) + .nth(index) + .ok_or_else(|| { + TxDatumError::InvalidContent( + format!( + "Error: missing field at index {index}, tx datum was = '{tx_datum_raw}'" + ) + .into(), + ) + })? + .get(type_name) + .unwrap(); + + Ok(field_value.to_owned()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn dummy_tx_datum() -> TxDatum { + TxDatum("{\"constructor\":0,\"fields\":[{\"bytes\":\"bytes0\"}, {\"int\":0}, {\"int\":1}, {\"bytes\":\"bytes1\"}, {\"bytes\":\"bytes2\"}, {\"int\":2}]}".to_string()) + } + + #[test] + fn test_can_retrieve_field_raw_value_bytes() { + let tx_datum = dummy_tx_datum(); + assert_eq!( + "bytes0", + tx_datum + .get_nth_field_by_type("bytes", 0) + .unwrap() + .as_str() + .unwrap() + ); + assert_eq!( + "bytes1", + tx_datum + .get_nth_field_by_type("bytes", 1) + .unwrap() + .as_str() + .unwrap() + ); + assert_eq!( + "bytes2", + tx_datum + .get_nth_field_by_type("bytes", 2) + .unwrap() + .as_str() + .unwrap() + ); + tx_datum + .get_nth_field_by_type("bytes", 100) + .expect_err("should have returned an error"); + } + + #[test] + fn test_can_retrieve_field_raw_value_int() { + let tx_datum = dummy_tx_datum(); + assert_eq!( + 0, + tx_datum + .get_nth_field_by_type("int", 0) + .unwrap() + .as_u64() + .unwrap() + ); + assert_eq!( + 1, + tx_datum + .get_nth_field_by_type("int", 1) + .unwrap() + .as_u64() + .unwrap() + ); + assert_eq!( + 2, + tx_datum + .get_nth_field_by_type("int", 2) + .unwrap() + .as_u64() + .unwrap() + ); + tx_datum + .get_nth_field_by_type("int", 100) + .expect_err("should have returned an error"); + } +} diff --git a/mithril-common/src/crypto_helper/era.rs b/mithril-common/src/crypto_helper/era.rs new file mode 100644 index 00000000000..9d7d7ab1edb --- /dev/null +++ b/mithril-common/src/crypto_helper/era.rs @@ -0,0 +1,177 @@ +use ed25519_dalek::{ExpandedSecretKey, SignatureError}; +use rand_chacha_dalek_compat::rand_core::{self, CryptoRng, RngCore, SeedableRng}; +use rand_chacha_dalek_compat::ChaCha20Rng; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Alias of [Ed25519:PublicKey](https://docs.rs/ed25519-dalek/latest/ed25519_dalek/struct.PublicKey.html). +pub type EraMarkersVerifierVerificationKey = ed25519_dalek::PublicKey; + +/// Alias of [Ed25519:SecretKey](https://docs.rs/ed25519-dalek/latest/ed25519_dalek/struct.SecretKey.html). +pub type EraMarkersVerifierSecretKey = ed25519_dalek::SecretKey; + +/// Alias of [Ed25519:Signature](https://docs.rs/ed25519-dalek/latest/ed25519_dalek/struct.Signature.html). +pub type EraMarkersVerifierSignature = ed25519_dalek::Signature; + +#[derive(Error, Debug)] +/// [EraMarkersSigner] and [EraMarkersVerifier] related errors. +pub enum EraMarkersVerifierError { + /// Error raised when a Signature verification fail + #[error("era markers signature verification error: '{0}'")] + SignatureVerification(#[from] SignatureError), +} + +/// A cryptographic signer that is responsible for signing the [EreMarker]s +#[derive(Debug, Serialize, Deserialize)] +pub struct EraMarkersSigner { + pub(crate) secret_key: EraMarkersVerifierSecretKey, +} + +impl EraMarkersSigner { + /// EraMarkersSigner factory + pub fn create_test_signer(mut rng: R) -> Self + where + R: CryptoRng + RngCore, + { + let secret_key = EraMarkersVerifierSecretKey::generate(&mut rng); + Self::from_secret_key(secret_key) + } + + /// EraMarkersSigner deterministic + pub fn create_deterministic_signer() -> Self { + let rng = ChaCha20Rng::from_seed([0u8; 32]); + Self::create_test_signer(rng) + } + + /// EraMarkersSigner non deterministic + pub fn create_non_deterministic_signer() -> Self { + let rng = rand_core::OsRng; + Self::create_test_signer(rng) + } + + /// EraMarkersSigner from EraMarkersVerifierSecretKey + pub fn from_secret_key(secret_key: EraMarkersVerifierSecretKey) -> Self { + Self { secret_key } + } + + /// Create a an expanded secret key + fn create_expanded_secret_key(&self) -> ExpandedSecretKey { + ExpandedSecretKey::from(&self.secret_key) + } + + /// Create a EraMarkersVerifierVerificationKey + fn create_verification_key( + &self, + expanded_secret_key: &ExpandedSecretKey, + ) -> EraMarkersVerifierVerificationKey { + let verification_key: EraMarkersVerifierVerificationKey = expanded_secret_key.into(); + verification_key + } + + /// Create a EraMarkersVerifier + pub fn create_verifier(&self) -> EraMarkersVerifier { + let expanded_secret_key = self.create_expanded_secret_key(); + let verification_key = self.create_verification_key(&expanded_secret_key); + EraMarkersVerifier::from_verification_key(verification_key) + } + + /// Signs a message and returns a EraMarkersVerifierSignature + pub fn sign(&self, message: &[u8]) -> EraMarkersVerifierSignature { + let expanded_secret_key = self.create_expanded_secret_key(); + let verification_key = self.create_verification_key(&expanded_secret_key); + expanded_secret_key.sign(message, &verification_key) + } +} + +/// An era markers verifier that checks the authenticity of era markers stored on the chain +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EraMarkersVerifier { + pub(crate) verification_key: EraMarkersVerifierVerificationKey, +} + +impl EraMarkersVerifier { + /// EraMarkersVerifier from EraMarkersVerifierVerificationKey + pub fn from_verification_key(verification_key: EraMarkersVerifierVerificationKey) -> Self { + Self { verification_key } + } + + /// EraMarkersVerifier to EraMarkersVerifierVerificationKey + pub fn to_verification_key(&self) -> EraMarkersVerifierVerificationKey { + self.verification_key + } + + /// Verifies the signature of a message + pub fn verify( + &self, + message: &[u8], + signature: &EraMarkersVerifierSignature, + ) -> Result<(), EraMarkersVerifierError> { + Ok(self.verification_key.verify_strict(message, signature)?) + } +} + +#[cfg(test)] +mod tests { + use super::super::codec::{key_decode_hex, key_encode_hex}; + use super::*; + + #[test] + fn test_generate_test_deterministic_keypair() { + let signer = EraMarkersSigner::create_deterministic_signer(); + let verifier = signer.create_verifier(); + let signer_2 = EraMarkersSigner::create_deterministic_signer(); + let verifier_2 = signer.create_verifier(); + assert_eq!(signer.secret_key.as_bytes(), signer_2.secret_key.as_bytes()); + assert_eq!( + verifier.verification_key.as_bytes(), + verifier_2.verification_key.as_bytes() + ); + + println!( + "Deterministic Verification Key={}", + key_encode_hex(verifier.verification_key.as_bytes()).unwrap() + ); + println!( + "Deterministic Secret Key=={}", + key_encode_hex(signer.secret_key.as_bytes()).unwrap() + ); + } + + #[test] + fn test_generate_test_non_deterministic_keypair() { + let signer = EraMarkersSigner::create_non_deterministic_signer(); + let verifier = signer.create_verifier(); + + println!( + "Non Deterministic Verification Key={}", + key_encode_hex(verifier.verification_key.as_bytes()).unwrap() + ); + println!( + "Non Deterministic Secret Key=={}", + key_encode_hex(signer.secret_key.as_bytes()).unwrap() + ); + } + + #[test] + fn test_codec_keypair() { + let signer = EraMarkersSigner::create_deterministic_signer(); + let verifier = signer.create_verifier(); + let secret_key_encoded = key_encode_hex(signer.secret_key.as_bytes()).unwrap(); + let verification_key_encoded = + key_encode_hex(verifier.verification_key.as_bytes()).unwrap(); + let secret_key_decoded: EraMarkersVerifierSecretKey = + key_decode_hex(&secret_key_encoded).unwrap(); + let verification_key_decoded: EraMarkersVerifierVerificationKey = + key_decode_hex(&verification_key_encoded).unwrap(); + let signer_decoded = EraMarkersSigner::from_secret_key(secret_key_decoded); + let verifier_decoded = EraMarkersVerifier::from_verification_key(verification_key_decoded); + + let message: &[u8] = b"some message."; + let signature = signer_decoded.sign(message); + let verify_signature = verifier_decoded.verify(message, &signature); + assert!( + verify_signature.is_ok(), + "signature verification should not fail" + ); + } +} diff --git a/mithril-common/src/crypto_helper/mod.rs b/mithril-common/src/crypto_helper/mod.rs index 3901987632c..66d4346cc9a 100644 --- a/mithril-common/src/crypto_helper/mod.rs +++ b/mithril-common/src/crypto_helper/mod.rs @@ -3,6 +3,7 @@ mod cardano; mod codec; mod conversions; +mod era; mod genesis; #[cfg(any(test, feature = "test_only"))] pub mod tests_setup; @@ -12,6 +13,10 @@ mod types; pub use cardano::ColdKeyGenerator; pub use cardano::{KESPeriod, OpCert, SerDeShelleyFileFormat}; pub use codec::*; +pub use era::{ + EraMarkersSigner, EraMarkersVerifier, EraMarkersVerifierError, EraMarkersVerifierSecretKey, + EraMarkersVerifierSignature, EraMarkersVerifierVerificationKey, +}; pub use genesis::{ProtocolGenesisError, ProtocolGenesisSigner, ProtocolGenesisVerifier}; pub use types::*; diff --git a/mithril-common/src/era/adapters/cardano_chain.rs b/mithril-common/src/era/adapters/cardano_chain.rs new file mode 100644 index 00000000000..58796bc9ff2 --- /dev/null +++ b/mithril-common/src/era/adapters/cardano_chain.rs @@ -0,0 +1,205 @@ +use crate::{ + chain_observer::{ChainAddress, ChainObserver}, + crypto_helper::{ + key_decode_hex, EraMarkersSigner, EraMarkersVerifier, EraMarkersVerifierSignature, + EraMarkersVerifierVerificationKey, + }, + era::{EraMarker, EraReaderAdapter}, +}; +use async_trait::async_trait; +use hex::{FromHex, ToHex}; +use serde::{Deserialize, Serialize}; +use std::error::Error as StdError; +use std::sync::Arc; +use thiserror::Error; + +type GeneralError = Box; + +type HexEncodeEraMarkerSignature = String; + +/// [EraMarkersPayload] related errors. +#[derive(Debug, Error)] +pub enum EraMarkersPayloadError { + /// Error raised when the message serialization fails + #[error("could not serialize message: {0}")] + SerializeMessage(GeneralError), + + /// Error raised when the signature deserialization fails + #[error("could not deserialize signature: {0}")] + DeserializeSignature(GeneralError), + + /// 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: {0}")] + VerifySignature(GeneralError), + + /// Error raised when the signing the markers + #[error("could not create signature: {0}")] + CreateSignature(GeneralError), +} + +/// Era markers payload +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EraMarkersPayload { + markers: Vec, + signature: Option, +} + +impl EraMarkersPayload { + fn message_to_bytes(&self) -> Result, EraMarkersPayloadError> { + serde_json::to_vec(&self.markers) + .map_err(|e| EraMarkersPayloadError::SerializeMessage(e.into())) + } + + fn deserialize_signature(&self) -> Result { + EraMarkersVerifierSignature::from_bytes( + &Vec::from_hex( + self.signature + .as_ref() + .ok_or(EraMarkersPayloadError::MissingSignature)?, + ) + .map_err(|e| EraMarkersPayloadError::DeserializeSignature(e.into()))?, + ) + .map_err(|e| EraMarkersPayloadError::DeserializeSignature(e.into())) + } + + /// Verify the signature an era markers payload + pub fn verify_signature( + &self, + verification_key: EraMarkersVerifierVerificationKey, + ) -> Result<(), EraMarkersPayloadError> { + let markers_verifier = EraMarkersVerifier::from_verification_key(verification_key); + + markers_verifier + .verify(&self.message_to_bytes()?, &self.deserialize_signature()?) + .map_err(|e| EraMarkersPayloadError::VerifySignature(e.into())) + } + + /// Sign an era markers payload + #[allow(dead_code)] + pub fn sign(self, signer: &EraMarkersSigner) -> Result { + let signature = signer + .sign( + &self + .message_to_bytes() + .map_err(|e| EraMarkersPayloadError::CreateSignature(e.into()))?, + ) + .encode_hex::(); + + Ok(Self { + markers: self.markers, + signature: Some(signature), + }) + } +} + +/// Cardano Chain adapter retrieves era markers on chain +pub struct CardanoChainAdapter { + address: ChainAddress, + chain_observer: Arc, + verification_key: EraMarkersVerifierVerificationKey, +} + +impl CardanoChainAdapter { + /// CardanoChainAdapter factory + pub fn new( + address: ChainAddress, + chain_observer: Arc, + verification_key: EraMarkersVerifierVerificationKey, + ) -> Self { + Self { + address, + chain_observer, + verification_key, + } + } +} + +#[async_trait] +impl EraReaderAdapter for CardanoChainAdapter { + async fn read(&self) -> Result, GeneralError> { + let tx_datums = self + .chain_observer + .get_current_datums(&self.address) + .await?; + let markers_list = tx_datums + .into_iter() + .filter_map(|datum| datum.get_nth_field_by_type("bytes", 0).ok()) + .filter_map(|field_value| field_value.as_str().map(|s| s.to_string())) + .filter_map(|field_value_str| key_decode_hex(&field_value_str).ok()) + .filter_map(|era_markers_payload: EraMarkersPayload| { + era_markers_payload + .verify_signature(self.verification_key) + .ok() + .map(|_| era_markers_payload.markers) + }) + .collect::>>(); + + Ok(markers_list.first().unwrap_or(&Vec::new()).to_owned()) + } +} + +#[cfg(test)] +mod test { + use crate::chain_observer::{FakeObserver, TxDatum}; + use crate::crypto_helper::{key_encode_hex, EraMarkersSigner}; + use crate::entities::Epoch; + + use super::*; + + fn dummy_tx_datums_from_markers_payload(payloads: Vec) -> Vec { + payloads + .into_iter() + .map(|payload| { + TxDatum(format!( + "{{\"constructor\":0,\"fields\":[{{\"bytes\":\"{}\"}}]}}", + key_encode_hex(payload).unwrap() + )) + }) + .collect() + } + + #[tokio::test] + async fn test_cardano_chain_adapter() { + let era_markers_signer = EraMarkersSigner::create_deterministic_signer(); + let fake_address = "addr_test_123456".to_string(); + let era_marker_payload_1 = EraMarkersPayload { + markers: vec![ + EraMarker::new("thales", Some(Epoch(1))), + EraMarker::new("pythagoras", None), + ], + signature: None, + }; + let era_marker_payload_2 = EraMarkersPayload { + markers: vec![ + EraMarker::new("thales", Some(Epoch(1))), + EraMarker::new("pythagoras", Some(Epoch(2))), + ], + signature: None, + }; + let mut fake_datums = dummy_tx_datums_from_markers_payload(vec![ + era_marker_payload_1, + era_marker_payload_2 + .clone() + .sign(&era_markers_signer) + .unwrap(), + ]); + fake_datums.push(TxDatum("not_valid_datum".to_string())); + let chain_observer = FakeObserver::default(); + chain_observer.set_datums(fake_datums.clone()).await; + let cardano_chain_adapter = CardanoChainAdapter::new( + fake_address, + Arc::new(chain_observer), + era_markers_signer.create_verifier().to_verification_key(), + ); + let markers = cardano_chain_adapter + .read() + .await + .expect("CardanoChainAdapter read should not fail"); + let expected_markers = era_marker_payload_2.markers.to_owned(); + assert_eq!(expected_markers, markers); + } +} diff --git a/mithril-common/src/era/adapters/mod.rs b/mithril-common/src/era/adapters/mod.rs index 54bef8b8b1e..c1f98e12baf 100644 --- a/mithril-common/src/era/adapters/mod.rs +++ b/mithril-common/src/era/adapters/mod.rs @@ -1,6 +1,8 @@ //! Module dedicated to EraReaderAdapter implementations. mod bootstrap; +mod cardano_chain; mod dummy; pub use bootstrap::BootstrapAdapter as EraReaderBootstrapAdapter; +pub use cardano_chain::CardanoChainAdapter; pub use dummy::DummyAdapter as EraReaderDummyAdapter; diff --git a/mithril-common/src/era/era_reader.rs b/mithril-common/src/era/era_reader.rs index b784ad6f669..d5cf1c7c43d 100644 --- a/mithril-common/src/era/era_reader.rs +++ b/mithril-common/src/era/era_reader.rs @@ -1,13 +1,13 @@ -use std::{error::Error as StdError, str::FromStr}; - use crate::entities::Epoch; use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::{error::Error as StdError, str::FromStr}; use thiserror::Error; use super::{supported_era::UnsupportedEraError, SupportedEra}; /// Value object that represents a tag of Era change. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct EraMarker { /// Era name pub name: String, diff --git a/mithril-test-lab/mithril-devnet/devnet-mkfiles.sh b/mithril-test-lab/mithril-devnet/devnet-mkfiles.sh index 24df0d71338..facbaca421d 100755 --- a/mithril-test-lab/mithril-devnet/devnet-mkfiles.sh +++ b/mithril-test-lab/mithril-devnet/devnet-mkfiles.sh @@ -1004,7 +1004,7 @@ done NODE_IX=0 for NODE in ${POOL_NODES}; do NODE_ID=$(( $NODE_IX + 1)) -if [ `expr $NODE_IX % 2` == 0 || -z "${WITH_UNCERTIFIED_SIGNERS}" ]; then +if [ `expr $NODE_IX % 2` == 0 ] || [ -z "${WITH_UNCERTIFIED_SIGNERS}" ]; then # 50% of signers with key certification cat >> ${NODE}/info.json <