diff --git a/Cargo.lock b/Cargo.lock index 080d25ae7bd..2cccee7e789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -872,6 +872,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ckb-merkle-mountain-range" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15193decfa1e0b151ce19e42d118db048459a27720fb3de7d3103c30adccb12" +dependencies = [ + "cfg-if", +] + [[package]] name = "clap" version = "4.4.18" @@ -3279,7 +3288,7 @@ dependencies = [ [[package]] name = "mithril-aggregator" -version = "0.4.28" +version = "0.4.29" dependencies = [ "anyhow", "async-trait", @@ -3417,13 +3426,14 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.2.153" +version = "0.2.154" dependencies = [ "anyhow", "async-trait", "bech32", "blake2 0.10.6", "chrono", + "ckb-merkle-mountain-range", "criterion", "digest 0.10.7", "ed25519-dalek", diff --git a/mithril-aggregator/Cargo.toml b/mithril-aggregator/Cargo.toml index c2e2b4cdc72..cb0789aad78 100644 --- a/mithril-aggregator/Cargo.toml +++ b/mithril-aggregator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-aggregator" -version = "0.4.28" +version = "0.4.29" description = "A Mithril Aggregator server" authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-aggregator/src/artifact_builder/cardano_transactions.rs b/mithril-aggregator/src/artifact_builder/cardano_transactions.rs index 1bf22a4209e..19c265bd43b 100644 --- a/mithril-aggregator/src/artifact_builder/cardano_transactions.rs +++ b/mithril-aggregator/src/artifact_builder/cardano_transactions.rs @@ -36,17 +36,23 @@ impl ArtifactBuilder for CardanoTransacti .with_context(|| { format!( "Can not compute CardanoTransactionsCommitment artifact for signed_entity: {:?}", - SignedEntityType::CardanoTransactions(beacon) + SignedEntityType::CardanoTransactions(beacon.clone()) ) })?; - Ok(CardanoTransactionsCommitment::new(merkle_root.to_string())) + Ok(CardanoTransactionsCommitment::new( + merkle_root.to_string(), + beacon, + )) } } #[cfg(test)] mod tests { - use mithril_common::{entities::ProtocolMessage, test_utils::fake_data}; + use mithril_common::{ + entities::ProtocolMessage, + test_utils::fake_data::{self}, + }; use super::*; @@ -65,10 +71,11 @@ mod tests { let cardano_transaction_artifact_builder = CardanoTransactionsArtifactBuilder::new(); let artifact = cardano_transaction_artifact_builder - .compute_artifact(Beacon::default(), &certificate) + .compute_artifact(certificate.beacon.clone(), &certificate) .await .unwrap(); - let artifact_expected = CardanoTransactionsCommitment::new("merkleroot".to_string()); + let artifact_expected = + CardanoTransactionsCommitment::new("merkleroot".to_string(), certificate.beacon); assert_eq!(artifact_expected, artifact); } diff --git a/mithril-aggregator/src/services/signed_entity.rs b/mithril-aggregator/src/services/signed_entity.rs index eac53999471..3e18ad15298 100644 --- a/mithril-aggregator/src/services/signed_entity.rs +++ b/mithril-aggregator/src/services/signed_entity.rs @@ -405,7 +405,8 @@ mod tests { #[tokio::test] async fn build_artifact_for_cardano_transactions_store_nothing_in_db() { - let expected = CardanoTransactionsCommitment::new("merkle_root".to_string()); + let expected = + CardanoTransactionsCommitment::new("merkle_root".to_string(), Beacon::default()); let mut mock_signed_entity_storer = MockSignedEntityStorer::new(); mock_signed_entity_storer .expect_store_signed_entity() diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 9173d0279b1..ea2c1754309 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-common" -version = "0.2.153" +version = "0.2.154" description = "Common types, interfaces, and utilities for Mithril nodes." authors = { workspace = true } edition = { workspace = true } @@ -22,6 +22,7 @@ async-trait = "0.1.73" bech32 = "0.9.1" blake2 = "0.10.6" chrono = { version = "0.4.31", features = ["serde"] } +ckb-merkle-mountain-range = "0.6.0" digest = "0.10.7" ed25519-dalek = { version = "2.0.0", features = ["rand_core", "serde"] } fixed = "1.24.0" diff --git a/mithril-common/src/crypto_helper/merkle_tree.rs b/mithril-common/src/crypto_helper/merkle_tree.rs new file mode 100644 index 00000000000..fcc29832042 --- /dev/null +++ b/mithril-common/src/crypto_helper/merkle_tree.rs @@ -0,0 +1,236 @@ +use anyhow::anyhow; +use blake2::{Blake2s256, Digest}; +use ckb_merkle_mountain_range::{util::MemStore, Merge, MerkleProof, Result as MMRResult, MMR}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, ops::Deref}; + +use crate::{StdError, StdResult}; + +/// Alias for a byte +type Bytes = Vec; + +/// Alias for a Merkle tree leaf position +type MKTreeLeafPosition = u64; + +/// A node of a Merkle tree +#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)] +pub struct MKTreeNode { + hash: Bytes, +} + +impl MKTreeNode { + /// MKTreeNode factory + pub fn new(hash: Bytes) -> Self { + Self { hash } + } + + /// Create a MKTreeNode from a hex representation + pub fn from_hex(hex: &str) -> StdResult { + let hash = hex::decode(hex)?; + Ok(Self { hash }) + } + + /// Create a hex representation of the MKTreeNode + pub fn to_hex(&self) -> String { + hex::encode(&self.hash) + } +} + +impl Deref for MKTreeNode { + type Target = Bytes; + + fn deref(&self) -> &Self::Target { + &self.hash + } +} + +impl From for MKTreeNode { + fn from(other: String) -> Self { + Self { + hash: other.as_str().into(), + } + } +} + +impl From<&str> for MKTreeNode { + fn from(other: &str) -> Self { + Self { + hash: other.as_bytes().to_vec(), + } + } +} + +impl TryFrom> for MKTreeNode { + type Error = StdError; + fn try_from(other: MKTree) -> Result { + other.compute_root() + } +} + +impl ToString for MKTreeNode { + fn to_string(&self) -> String { + String::from_utf8_lossy(&self.hash).to_string() + } +} + +struct MergeMKTreeNode {} + +impl Merge for MergeMKTreeNode { + type Item = MKTreeNode; + + fn merge(lhs: &Self::Item, rhs: &Self::Item) -> MMRResult { + let mut hasher = Blake2s256::new(); + hasher.update(lhs.deref()); + hasher.update(rhs.deref()); + let hash_merge = hasher.finalize(); + + Ok(Self::Item::new(hash_merge.to_vec())) + } +} + +/// A Merkle proof +#[derive(Serialize, Deserialize)] +pub struct MKProof { + inner_root: MKTreeNode, + inner_leaves: Vec<(MKTreeLeafPosition, MKTreeNode)>, + inner_proof_size: u64, + inner_proof_items: Vec, +} + +impl MKProof { + /// Verification of a Merkle proof + pub fn verify(&self) -> StdResult<()> { + MerkleProof::::new( + self.inner_proof_size, + self.inner_proof_items.clone(), + ) + .verify(self.inner_root.to_owned(), self.inner_leaves.to_owned())? + .then_some(()) + .ok_or(anyhow!("Invalid MKProof")) + } +} + +/// A Merkle tree store +pub type MKTreeStore = MemStore; + +/// A Merkle tree +pub struct MKTree<'a> { + inner_leaves: HashMap<&'a MKTreeNode, MKTreeLeafPosition>, + inner_tree: MMR, +} + +impl<'a> MKTree<'a> { + /// MKTree factory + pub fn new(leaves: &'a [MKTreeNode], store: &'a MKTreeStore) -> StdResult { + let mut inner_tree = MMR::::new(0, store); + let mut inner_leaves = HashMap::new(); + for leaf in leaves { + let inner_tree_position = inner_tree.push(leaf.to_owned())?; + inner_leaves.insert(leaf, inner_tree_position); + } + inner_tree.commit()?; + + Ok(Self { + inner_leaves, + inner_tree, + }) + } + + /// Number of leaves in the Merkle tree + pub fn total_leaves(&self) -> usize { + self.inner_leaves.len() + } + + /// Generate root of the Merkle tree + pub fn compute_root(&self) -> StdResult { + Ok(self.inner_tree.get_root()?) + } + + /// Generate Merkle proof of memberships in the tree + pub fn compute_proof(&self, leaves: &[MKTreeNode]) -> StdResult { + let inner_leaves = leaves + .iter() + .map(|leaf| { + if let Some(leaf_position) = self.inner_leaves.get(leaf) { + Ok((*leaf_position, leaf.to_owned())) + } else { + Err(anyhow!("Leaf not found in the Merkle tree")) + } + }) + .collect::>>()?; + let proof = self.inner_tree.gen_proof( + inner_leaves + .iter() + .map(|(leaf_position, _leaf)| *leaf_position) + .collect(), + )?; + return Ok(MKProof { + inner_root: self.compute_root()?, + inner_leaves, + inner_proof_size: proof.mmr_size(), + inner_proof_items: proof.proof_items().to_vec(), + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_golden_merkle_root() { + let leaves = vec!["golden-1", "golden-2", "golden-3", "golden-4", "golden-5"]; + let leaves: Vec = leaves.into_iter().map(|l| l.into()).collect(); + let store = MKTreeStore::default(); + let mktree = MKTree::new(&leaves, &store).expect("MKTree creation should not fail"); + let mkroot = mktree + .compute_root() + .expect("MKRoot generation should not fail"); + assert_eq!( + "3bbced153528697ecde7345a22e50115306478353619411523e804f2323fd921", + mkroot.to_hex() + ); + } + + #[test] + fn test_should_accept_valid_proof_generated_by_merkle_tree() { + let total_leaves = 100000; + let leaves = (0..total_leaves) + .map(|i| format!("test-{i}").into()) + .collect::>(); + let store = MKTreeStore::default(); + let mktree = MKTree::new(&leaves, &store).expect("MKTree creation should not fail"); + let leaves_to_verify = &[leaves[0].to_owned(), leaves[3].to_owned()]; + let proof = mktree + .compute_proof(leaves_to_verify) + .expect("MKProof generation should not fail"); + proof.verify().expect("The MKProof should be valid"); + } + + #[test] + fn test_should_reject_invalid_proof_generated_by_merkle_tree() { + let total_leaves = 100000; + let leaves = (0..total_leaves) + .map(|i| format!("test-{i}").into()) + .collect::>(); + let store = MKTreeStore::default(); + let mktree = MKTree::new(&leaves, &store).expect("MKTree creation should not fail"); + let leaves_to_verify = &[leaves[0].to_owned(), leaves[3].to_owned()]; + let mut proof = mktree + .compute_proof(leaves_to_verify) + .expect("MKProof generation should not fail"); + proof.inner_root = leaves[10].to_owned(); + proof.verify().expect_err("The MKProof should be invalid"); + } + + #[test] + fn tree_node_from_to_string() { + let expected_str = "my_string"; + let expected_string = expected_str.to_string(); + let node_str: MKTreeNode = expected_str.into(); + let node_string: MKTreeNode = expected_string.clone().into(); + + assert_eq!(node_str.to_string(), expected_str); + assert_eq!(node_string.to_string(), expected_string); + } +} diff --git a/mithril-common/src/crypto_helper/mod.rs b/mithril-common/src/crypto_helper/mod.rs index 998b5df8523..de9b72086b9 100644 --- a/mithril-common/src/crypto_helper/mod.rs +++ b/mithril-common/src/crypto_helper/mod.rs @@ -5,6 +5,7 @@ mod codec; mod conversions; mod era; mod genesis; +mod merkle_tree; #[cfg(feature = "test_tools")] pub mod tests_setup; mod types; @@ -22,6 +23,7 @@ pub use era::{ EraMarkersVerifierSignature, EraMarkersVerifierVerificationKey, }; pub use genesis::{ProtocolGenesisError, ProtocolGenesisSigner, ProtocolGenesisVerifier}; +pub use merkle_tree::{MKProof, MKTree, MKTreeNode, MKTreeStore}; pub use types::*; /// The current protocol version diff --git a/mithril-common/src/entities/cardano_transaction.rs b/mithril-common/src/entities/cardano_transaction.rs index 066c413d5d3..dad91cd1309 100644 --- a/mithril-common/src/entities/cardano_transaction.rs +++ b/mithril-common/src/entities/cardano_transaction.rs @@ -1,3 +1,5 @@ +use crate::crypto_helper::MKTreeNode; + use super::ImmutableFileNumber; /// TransactionHash is the unique identifier of a cardano transaction. @@ -18,3 +20,50 @@ pub struct CardanoTransaction { /// Immutable file number of the transaction pub immutable_file_number: ImmutableFileNumber, } + +impl CardanoTransaction { + /// CardanoTransaction factory + pub fn new( + hash: &str, + block_number: BlockNumber, + immutable_file_number: ImmutableFileNumber, + ) -> Self { + Self { + transaction_hash: hash.to_owned(), + block_number, + immutable_file_number, + } + } +} + +impl From for MKTreeNode { + fn from(other: CardanoTransaction) -> Self { + (&other).into() + } +} + +impl From<&CardanoTransaction> for MKTreeNode { + fn from(other: &CardanoTransaction) -> Self { + MKTreeNode::new(other.transaction_hash.as_bytes().to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_cardano_transaction_to_merkle_tree_node() { + let transaction = CardanoTransaction { + transaction_hash: "tx-hash-123".to_string(), + block_number: 1, + immutable_file_number: 1, + }; + let computed_mktree_node: MKTreeNode = transaction.into(); + let expected_mk_tree_node = MKTreeNode::new("tx-hash-123".as_bytes().to_vec()); + let non_expected_mk_tree_node = MKTreeNode::new("tx-hash-456".as_bytes().to_vec()); + + assert_eq!(expected_mk_tree_node, computed_mktree_node); + assert_ne!(non_expected_mk_tree_node, computed_mktree_node); + } +} diff --git a/mithril-common/src/entities/cardano_transactions_commitment.rs b/mithril-common/src/entities/cardano_transactions_commitment.rs index d90e3da20af..dab686eb0dd 100644 --- a/mithril-common/src/entities/cardano_transactions_commitment.rs +++ b/mithril-common/src/entities/cardano_transactions_commitment.rs @@ -1,23 +1,32 @@ +use crate::signable_builder::Artifact; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; -use crate::signable_builder::Artifact; +use super::Beacon; /// Commitment of a set of Cardano transactions #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct CardanoTransactionsCommitment { merkle_root: String, + beacon: Beacon, } impl CardanoTransactionsCommitment { /// Creates a new [CardanoTransactionsCommitment] - pub fn new(merkle_root: String) -> Self { - Self { merkle_root } + pub fn new(merkle_root: String, beacon: Beacon) -> Self { + Self { + merkle_root, + beacon, + } } } #[typetag::serde] impl Artifact for CardanoTransactionsCommitment { fn get_id(&self) -> String { - self.merkle_root.clone() + let mut hasher = Sha256::new(); + hasher.update(self.merkle_root.clone().as_bytes()); + hasher.update(self.beacon.compute_hash().as_bytes()); + hex::encode(hasher.finalize()) } } diff --git a/mithril-common/src/entities/signed_entity.rs b/mithril-common/src/entities/signed_entity.rs index 6bd1b069b46..c8a20a6f89d 100644 --- a/mithril-common/src/entities/signed_entity.rs +++ b/mithril-common/src/entities/signed_entity.rs @@ -70,7 +70,7 @@ impl SignedEntity { signed_entity_id: "snapshot-id-123".to_string(), signed_entity_type: SignedEntityType::CardanoTransactions(Beacon::default()), certificate_id: "certificate-hash-123".to_string(), - artifact: CardanoTransactionsCommitment::new("mkroot123".to_string()), + artifact: CardanoTransactionsCommitment::new("mkroot123".to_string(), Beacon::default()), created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z") .unwrap() .with_timezone(&Utc), diff --git a/mithril-common/src/signable_builder/cardano_transactions.rs b/mithril-common/src/signable_builder/cardano_transactions.rs index 5fdaea7111f..35d2655281e 100644 --- a/mithril-common/src/signable_builder/cardano_transactions.rs +++ b/mithril-common/src/signable_builder/cardano_transactions.rs @@ -3,11 +3,13 @@ use std::{ sync::Arc, }; +use anyhow::Context; use async_trait::async_trait; use slog::{debug, Logger}; use crate::{ cardano_transaction_parser::TransactionParser, + crypto_helper::{MKTree, MKTreeNode, MKTreeStore}, entities::{Beacon, CardanoTransaction, ProtocolMessage, ProtocolMessagePartKey}, signable_builder::SignableBuilder, StdResult, @@ -47,6 +49,18 @@ impl CardanoTransactionsSignableBuilder { dirpath: dirpath.to_owned(), } } + + fn compute_merkle_root(&self, transactions: &[CardanoTransaction]) -> StdResult { + let store = MKTreeStore::default(); + let leaves = transactions.iter().map(|tx| tx.into()).collect::>(); + let mk_tree = MKTree::new(&leaves, &store) + .with_context(|| "CardanoTransactionsSignableBuilder failed to compute MKTree")?; + let mk_root = mk_tree + .compute_root() + .with_context(|| "CardanoTransactionsSignableBuilder failed to compute MKTree root")?; + + Ok(mk_root) + } } #[async_trait] @@ -75,10 +89,12 @@ impl SignableBuilder for CardanoTransactionsSignableBuilder { .await?; } + let mk_root = self.compute_merkle_root(&transactions)?; + let mut protocol_message = ProtocolMessage::new(); protocol_message.set_message_part( ProtocolMessagePartKey::CardanoTransactionsMerkleRoot, - format!("{beacon}-{}", transactions.len()), + mk_root.to_hex(), ); Ok(protocol_message) @@ -100,18 +116,95 @@ mod tests { slog::Logger::root(Arc::new(drain), slog::o!()) } + #[tokio::test] + async fn test_compute_merkle_root() { + let transaction_1 = CardanoTransaction::new("tx-hash-123", 1, 1); + let transaction_2 = CardanoTransaction::new("tx-hash-456", 2, 1); + let transaction_3 = CardanoTransaction::new("tx-hash-789", 3, 1); + let transaction_4 = CardanoTransaction::new("tx-hash-abc", 4, 1); + + let transactions_set_reference = vec![ + transaction_1.clone(), + transaction_2.clone(), + transaction_3.clone(), + ]; + let cardano_transaction_signable_builder = CardanoTransactionsSignableBuilder::new( + Arc::new(DumbTransactionParser::new( + transactions_set_reference.clone(), + )), + Arc::new(MockTransactionStore::new()), + Path::new("/tmp"), + create_logger(), + ); + + let merkle_root_reference = cardano_transaction_signable_builder + .compute_merkle_root(&transactions_set_reference) + .unwrap(); + { + let transactions_set = vec![transaction_1.clone()]; + let mk_root = cardano_transaction_signable_builder + .compute_merkle_root(&transactions_set) + .unwrap(); + assert_ne!(merkle_root_reference, mk_root); + } + { + let transactions_set = vec![transaction_1.clone(), transaction_2.clone()]; + let mk_root = cardano_transaction_signable_builder + .compute_merkle_root(&transactions_set) + .unwrap(); + assert_ne!(merkle_root_reference, mk_root); + } + { + let transactions_set = vec![ + transaction_1.clone(), + transaction_2.clone(), + transaction_3.clone(), + transaction_4.clone(), + ]; + let mk_root = cardano_transaction_signable_builder + .compute_merkle_root(&transactions_set) + .unwrap(); + assert_ne!(merkle_root_reference, mk_root); + } + + { + // Transactions in a different order returns a different merkle root. + let transactions_set = vec![ + transaction_1.clone(), + transaction_3.clone(), + transaction_2.clone(), + ]; + let mk_root = cardano_transaction_signable_builder + .compute_merkle_root(&transactions_set) + .unwrap(); + assert_ne!(merkle_root_reference, mk_root); + } + } + #[tokio::test] async fn test_compute_signable() { let beacon = Beacon::default(); - let transactions_count = 0; - let transaction_parser = Arc::new(DumbTransactionParser::new(vec![])); - let transaction_store = Arc::new(MockTransactionStore::new()); + let transactions = vec![ + CardanoTransaction::new("tx-hash-123", 1, 1), + CardanoTransaction::new("tx-hash-456", 2, 1), + CardanoTransaction::new("tx-hash-789", 3, 1), + ]; + let transaction_parser = Arc::new(DumbTransactionParser::new(transactions.clone())); + let mut mock_transaction_store = MockTransactionStore::new(); + mock_transaction_store + .expect_store_transactions() + .times(1) + .returning(|_| Ok(())); + let transaction_store = Arc::new(mock_transaction_store); let cardano_transactions_signable_builder = CardanoTransactionsSignableBuilder::new( transaction_parser, transaction_store, Path::new("/tmp"), create_logger(), ); + let mk_root = cardano_transactions_signable_builder + .compute_merkle_root(&transactions) + .unwrap(); let signable = cardano_transactions_signable_builder .compute_protocol_message(beacon.clone()) .await @@ -119,7 +212,7 @@ mod tests { let mut signable_expected = ProtocolMessage::new(); signable_expected.set_message_part( ProtocolMessagePartKey::CardanoTransactionsMerkleRoot, - format!("{beacon}-{transactions_count}"), + mk_root.to_hex(), ); assert_eq!(signable_expected, signable); }