From 3dccee067425e797e44f9d85d8e8a4fda659d80e Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:35:21 +0100 Subject: [PATCH 1/8] refactor(common): extract `CardanoBlockTransactionMkTreeNode` to a dedicated file module --- mithril-common/src/entities/cardano_block.rs | 369 +----------------- .../cardano_block_transaction_mktree_node.rs | 365 +++++++++++++++++ mithril-common/src/entities/mod.rs | 2 + 3 files changed, 371 insertions(+), 365 deletions(-) create mode 100644 mithril-common/src/entities/cardano_block_transaction_mktree_node.rs diff --git a/mithril-common/src/entities/cardano_block.rs b/mithril-common/src/entities/cardano_block.rs index 505b0971de4..81aa1f9345b 100644 --- a/mithril-common/src/entities/cardano_block.rs +++ b/mithril-common/src/entities/cardano_block.rs @@ -1,7 +1,7 @@ -use std::cmp::Ordering; - -use crate::crypto_helper::MKTreeNode; -use crate::entities::{BlockHash, BlockNumber, CardanoTransaction, SlotNumber, TransactionHash}; +use crate::entities::{ + BlockHash, BlockNumber, CardanoBlockTransactionMkTreeNode, CardanoTransaction, SlotNumber, + TransactionHash, +}; /// Cardano block representation #[derive(Debug, Clone, PartialEq)] @@ -98,364 +98,3 @@ impl CardanoBlockWithTransactions { self.transactions_hashes.len() } } - -/// Leaf of the Merkle tree representing blocks and transactions -/// -/// When ordering in collections: -/// - all blocks are first, then all transactions -/// - blocks are ordered by block number, then slot number, then block hash -/// - transactions are ordered by block number, then slot number, then block hash, then transaction hash -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CardanoBlockTransactionMkTreeNode { - /// Leaf representing a block - Block { - /// Block hash - block_hash: BlockHash, - /// Block number - block_number: BlockNumber, - /// Slot number of the block - slot_number: SlotNumber, - }, - /// Leaf representing a transaction - Transaction { - /// Unique hash of the transaction - transaction_hash: TransactionHash, - /// Block number of the transaction - block_number: BlockNumber, - /// Slot number of the transaction - slot_number: SlotNumber, - /// Block hash of the transaction - block_hash: BlockHash, - }, -} - -impl CardanoBlockTransactionMkTreeNode { - fn leaf_identifier(&self) -> Vec { - match self { - Self::Block { - block_hash, - block_number, - slot_number, - } => format!("Block/{block_hash}/{block_number}/{slot_number}").into_bytes(), - Self::Transaction { - transaction_hash, - block_hash, - block_number, - slot_number, - } => format!("Tx/{transaction_hash}/{block_hash}/{block_number}/{slot_number}",) - .into_bytes(), - } - } -} - -impl From for MKTreeNode { - fn from(value: CardanoBlockTransactionMkTreeNode) -> Self { - MKTreeNode::new(value.leaf_identifier()) - } -} - -impl PartialOrd for CardanoBlockTransactionMkTreeNode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(Ord::cmp(self, other)) - } -} - -impl Ord for CardanoBlockTransactionMkTreeNode { - fn cmp(&self, other: &Self) -> Ordering { - use CardanoBlockTransactionMkTreeNode::{Block, Transaction}; - - match (self, other) { - (Block { .. }, Transaction { .. }) => Ordering::Less, - (Transaction { .. }, Block { .. }) => Ordering::Greater, - ( - Block { - block_number, - block_hash, - slot_number, - }, - Block { - block_number: other_block_number, - block_hash: other_block_hash, - slot_number: other_slot_number, - }, - ) => block_number - .cmp(other_block_number) - .then(slot_number.cmp(other_slot_number)) - .then(block_hash.cmp(other_block_hash)), - ( - Transaction { - block_number, - slot_number, - block_hash, - transaction_hash, - }, - Transaction { - block_number: other_block_number, - slot_number: other_slot_number, - block_hash: other_block_hash, - transaction_hash: other_transaction_hash, - }, - ) => block_number - .cmp(other_block_number) - .then(slot_number.cmp(other_slot_number)) - .then(block_hash.cmp(other_block_hash)) - .then(transaction_hash.cmp(other_transaction_hash)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! block_node { - (num: $block_number:expr, slot: $slot_number:expr) => { - block_node!(hash:format!("block_hash-{}", $block_number), num: $block_number, slot: $slot_number) - }; - (hash: $block_hash:expr, num: $block_number:expr, slot: $slot_number:expr) => { - CardanoBlockTransactionMkTreeNode::Block { - block_hash: $block_hash.to_string(), - block_number: BlockNumber($block_number), - slot_number: SlotNumber($slot_number), - } - }; - } - - macro_rules! tx_node { - (hash: $tx_hash:expr, block: $block_number:expr, slot: $slot_number:expr) => { - tx_node!(hash: $tx_hash, block_hash: format!("block_hash-{}", $tx_hash), block: $block_number, slot: $slot_number) - }; - (hash: $tx_hash:expr, block_hash: $block_hash:expr, block: $block_number:expr, slot: $slot_number:expr) => { - CardanoBlockTransactionMkTreeNode::Transaction { - transaction_hash: $tx_hash.to_string(), - block_hash: $block_hash.to_string(), - block_number: BlockNumber($block_number), - slot_number: SlotNumber($slot_number), - } - }; - } - - #[test] - fn block_node_leaf_identifier() { - // expected: "Block"/BlockHash/BlockNumber/SlotNumber - assert_eq!( - "Block/block_hash-5/5/6".to_string().into_bytes(), - block_node!(hash: "block_hash-5", num: 5, slot: 6).leaf_identifier() - ); - assert_eq!( - "Block/block_hash-10/9/13".to_string().into_bytes(), - block_node!(hash: "block_hash-10", num: 9, slot: 13).leaf_identifier() - ); - } - - #[test] - fn transaction_node_leaf_identifier() { - // expected: "Tx"/TransactionHash/BlockHash/BlockNumber/SlotNumber - assert_eq!( - "Tx/tx_hash-5/block_hash-5/5/6".to_string().into_bytes(), - tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6) - .leaf_identifier() - ); - assert_eq!( - "Tx/tx_hash-10/block_hash-10/9/13".to_string().into_bytes(), - tx_node!(hash: "tx_hash-10", block_hash: "block_hash-10", block: 9, slot: 13) - .leaf_identifier() - ); - } - - #[test] - fn convert_block_node_into_mktree_nodes() { - let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); - let expected: MKTreeNode = MKTreeNode::new(block.leaf_identifier()); - - let computed_node: MKTreeNode = block.into(); - assert_eq!(expected, computed_node); - assert_ne!( - computed_node, - block_node!(hash: "other", num: 5, slot: 6).into() - ); - assert_ne!( - computed_node, - block_node!(hash: "block_hash-10", num: 1000, slot: 6).into() - ); - assert_ne!( - computed_node, - block_node!(hash: "block_hash-10", num: 5, slot: 1000).into() - ); - } - - #[test] - fn convert_tx_node_into_mktree_nodes() { - let transaction = - tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); - let expected: MKTreeNode = MKTreeNode::new(transaction.leaf_identifier()); - - let computed_node: MKTreeNode = transaction.into(); - assert_eq!(expected, computed_node); - assert_ne!( - computed_node, - tx_node!(hash: "other", block_hash: "block_hash-5", block: 5, slot: 6).into(), - ); - assert_ne!( - computed_node, - tx_node!(hash: "tx_hash-5", block_hash: "other", block: 5, slot: 6).into(), - ); - assert_ne!( - computed_node, - tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 1000, slot: 6).into(), - ); - assert_ne!( - computed_node, - tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 1000).into(), - ); - } - - mod mk_tree_node_ordering { - use super::*; - - #[test] - fn same_value_yield_equal_order() { - let block = block_node!(num: 5, slot: 6); - let tx = tx_node!(hash: "tx_hash-1", block: 5, slot: 6); - - assert_eq!(Ordering::Equal, block.cmp(&block)); - assert_eq!(Ordering::Equal, tx.cmp(&tx)); - } - - #[test] - fn order_block_nodes_first_then_transaction_nodes() { - let block = block_node!(num: 5, slot: 6); - let tx = tx_node!(hash: "tx_hash-1", block: 5, slot: 6); - - assert_eq!(Ordering::Less, block.cmp(&tx)); - assert_eq!(Ordering::Greater, tx.cmp(&block)); - } - - #[test] - fn order_blocks_by_block_number_first() { - let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); - - assert_eq!( - Ordering::Less, - block.cmp(&block_node!(hash: "block_hash-1", num: 10, slot: 1)) - ); - assert_eq!( - Ordering::Greater, - block.cmp(&block_node!(hash: "block_hash-9", num: 1, slot: 9)) - ); - } - - #[test] - fn order_blocks_by_slot_number_second() { - let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); - - assert_eq!( - Ordering::Less, - block.cmp(&block_node!(hash: "block_hash-1", num: 5, slot: 9)) - ); - assert_eq!( - Ordering::Greater, - block.cmp(&block_node!(hash: "block_hash-9", num: 5, slot: 1)) - ); - } - - #[test] - fn order_blocks_by_block_hash_third() { - let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); - - assert_eq!( - Ordering::Less, - block.cmp(&block_node!(hash: "block_hash-9", num: 5, slot: 6)) - ); - assert_eq!( - Ordering::Greater, - block.cmp(&block_node!(hash: "block_hash-1", num: 5, slot: 6)) - ); - } - - #[test] - fn order_transactions_by_block_number_first() { - let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); - - assert_eq!( - Ordering::Less, - tx.cmp( - &tx_node!(hash: "tx_hash-1", block_hash: "block_hash-1", block: 10, slot: 1) - ) - ); - assert_eq!( - Ordering::Greater, - tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-9", block: 1, slot: 9)) - ); - } - - #[test] - fn order_transactions_by_slot_number_second() { - let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); - - assert_eq!( - Ordering::Less, - tx.cmp(&tx_node!(hash: "tx_hash-1", block_hash: "block_hash-1", block: 5, slot: 9)) - ); - assert_eq!( - Ordering::Greater, - tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-9", block: 5, slot: 1)) - ); - } - - #[test] - fn order_transactions_by_block_hash_third() { - let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); - - assert_eq!( - Ordering::Less, - tx.cmp(&tx_node!(hash: "tx_hash-1", block_hash: "block_hash-9", block: 5, slot: 6)) - ); - assert_eq!( - Ordering::Greater, - tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-1", block: 5, slot: 6)) - ); - } - - #[test] - fn order_transactions_by_transaction_hash_fourth() { - let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); - - assert_eq!( - Ordering::Less, - tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-5", block: 5, slot: 6)) - ); - assert_eq!( - Ordering::Greater, - tx.cmp(&tx_node!(hash: "tx_hash-1", block_hash: "block_hash-5", block: 5, slot: 6)) - ); - } - - #[test] - fn sorting_a_vec() { - let mut list = vec![ - tx_node!(hash: "tx_hash-70", block: 300, slot: 35), - tx_node!(hash: "tx_hash-200", block: 100, slot: 100), - tx_node!(hash: "tx_hash-50", block: 200, slot: 25), - block_node!(num: 100, slot: 100), - tx_node!(hash: "tx_hash-100", block: 100, slot: 100), - block_node!(num: 200, slot: 35), - block_node!(num: 200, slot: 25), - ]; - list.sort(); - - assert_eq!( - list, - vec![ - block_node!(num: 100, slot: 100), - block_node!(num: 200, slot: 25), - block_node!(num: 200, slot: 35), - tx_node!(hash: "tx_hash-100", block: 100, slot: 100), - tx_node!(hash: "tx_hash-200", block: 100, slot: 100), - tx_node!(hash: "tx_hash-50", block: 200, slot: 25), - tx_node!(hash: "tx_hash-70", block: 300, slot: 35), - ] - ); - } - } -} diff --git a/mithril-common/src/entities/cardano_block_transaction_mktree_node.rs b/mithril-common/src/entities/cardano_block_transaction_mktree_node.rs new file mode 100644 index 00000000000..8dbf23a4877 --- /dev/null +++ b/mithril-common/src/entities/cardano_block_transaction_mktree_node.rs @@ -0,0 +1,365 @@ +use std::cmp::Ordering; + +use crate::crypto_helper::MKTreeNode; +use crate::entities::{BlockHash, BlockNumber, SlotNumber, TransactionHash}; + +/// Leaf of the Merkle tree representing blocks and transactions +/// +/// When ordering in collections: +/// - all blocks are first, then all transactions +/// - blocks are ordered by block number, then slot number, then block hash +/// - transactions are ordered by block number, then slot number, then block hash, then transaction hash +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CardanoBlockTransactionMkTreeNode { + /// Leaf representing a block + Block { + /// Block hash + block_hash: BlockHash, + /// Block number + block_number: BlockNumber, + /// Slot number of the block + slot_number: SlotNumber, + }, + /// Leaf representing a transaction + Transaction { + /// Unique hash of the transaction + transaction_hash: TransactionHash, + /// Block number of the transaction + block_number: BlockNumber, + /// Slot number of the transaction + slot_number: SlotNumber, + /// Block hash of the transaction + block_hash: BlockHash, + }, +} + +impl CardanoBlockTransactionMkTreeNode { + fn leaf_identifier(&self) -> Vec { + match self { + Self::Block { + block_hash, + block_number, + slot_number, + } => format!("Block/{block_hash}/{block_number}/{slot_number}").into_bytes(), + Self::Transaction { + transaction_hash, + block_hash, + block_number, + slot_number, + } => format!("Tx/{transaction_hash}/{block_hash}/{block_number}/{slot_number}",) + .into_bytes(), + } + } +} + +impl From for MKTreeNode { + fn from(value: CardanoBlockTransactionMkTreeNode) -> Self { + MKTreeNode::new(value.leaf_identifier()) + } +} + +impl PartialOrd for CardanoBlockTransactionMkTreeNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl Ord for CardanoBlockTransactionMkTreeNode { + fn cmp(&self, other: &Self) -> Ordering { + use CardanoBlockTransactionMkTreeNode::{Block, Transaction}; + + match (self, other) { + (Block { .. }, Transaction { .. }) => Ordering::Less, + (Transaction { .. }, Block { .. }) => Ordering::Greater, + ( + Block { + block_number, + block_hash, + slot_number, + }, + Block { + block_number: other_block_number, + block_hash: other_block_hash, + slot_number: other_slot_number, + }, + ) => block_number + .cmp(other_block_number) + .then(slot_number.cmp(other_slot_number)) + .then(block_hash.cmp(other_block_hash)), + ( + Transaction { + block_number, + slot_number, + block_hash, + transaction_hash, + }, + Transaction { + block_number: other_block_number, + slot_number: other_slot_number, + block_hash: other_block_hash, + transaction_hash: other_transaction_hash, + }, + ) => block_number + .cmp(other_block_number) + .then(slot_number.cmp(other_slot_number)) + .then(block_hash.cmp(other_block_hash)) + .then(transaction_hash.cmp(other_transaction_hash)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! block_node { + (num: $block_number:expr, slot: $slot_number:expr) => { + block_node!(hash:format!("block_hash-{}", $block_number), num: $block_number, slot: $slot_number) + }; + (hash: $block_hash:expr, num: $block_number:expr, slot: $slot_number:expr) => { + CardanoBlockTransactionMkTreeNode::Block { + block_hash: $block_hash.to_string(), + block_number: BlockNumber($block_number), + slot_number: SlotNumber($slot_number), + } + }; + } + + macro_rules! tx_node { + (hash: $tx_hash:expr, block: $block_number:expr, slot: $slot_number:expr) => { + tx_node!(hash: $tx_hash, block_hash: format!("block_hash-{}", $tx_hash), block: $block_number, slot: $slot_number) + }; + (hash: $tx_hash:expr, block_hash: $block_hash:expr, block: $block_number:expr, slot: $slot_number:expr) => { + CardanoBlockTransactionMkTreeNode::Transaction { + transaction_hash: $tx_hash.to_string(), + block_hash: $block_hash.to_string(), + block_number: BlockNumber($block_number), + slot_number: SlotNumber($slot_number), + } + }; + } + + #[test] + fn block_node_leaf_identifier() { + // expected: "Block"/BlockHash/BlockNumber/SlotNumber + assert_eq!( + "Block/block_hash-5/5/6".to_string().into_bytes(), + block_node!(hash: "block_hash-5", num: 5, slot: 6).leaf_identifier() + ); + assert_eq!( + "Block/block_hash-10/9/13".to_string().into_bytes(), + block_node!(hash: "block_hash-10", num: 9, slot: 13).leaf_identifier() + ); + } + + #[test] + fn transaction_node_leaf_identifier() { + // expected: "Tx"/TransactionHash/BlockHash/BlockNumber/SlotNumber + assert_eq!( + "Tx/tx_hash-5/block_hash-5/5/6".to_string().into_bytes(), + tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6) + .leaf_identifier() + ); + assert_eq!( + "Tx/tx_hash-10/block_hash-10/9/13".to_string().into_bytes(), + tx_node!(hash: "tx_hash-10", block_hash: "block_hash-10", block: 9, slot: 13) + .leaf_identifier() + ); + } + + #[test] + fn convert_block_node_into_mktree_nodes() { + let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); + let expected: MKTreeNode = MKTreeNode::new(block.leaf_identifier()); + + let computed_node: MKTreeNode = block.into(); + assert_eq!(expected, computed_node); + assert_ne!( + computed_node, + block_node!(hash: "other", num: 5, slot: 6).into() + ); + assert_ne!( + computed_node, + block_node!(hash: "block_hash-10", num: 1000, slot: 6).into() + ); + assert_ne!( + computed_node, + block_node!(hash: "block_hash-10", num: 5, slot: 1000).into() + ); + } + + #[test] + fn convert_tx_node_into_mktree_nodes() { + let transaction = + tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); + let expected: MKTreeNode = MKTreeNode::new(transaction.leaf_identifier()); + + let computed_node: MKTreeNode = transaction.into(); + assert_eq!(expected, computed_node); + assert_ne!( + computed_node, + tx_node!(hash: "other", block_hash: "block_hash-5", block: 5, slot: 6).into(), + ); + assert_ne!( + computed_node, + tx_node!(hash: "tx_hash-5", block_hash: "other", block: 5, slot: 6).into(), + ); + assert_ne!( + computed_node, + tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 1000, slot: 6).into(), + ); + assert_ne!( + computed_node, + tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 1000).into(), + ); + } + + mod mk_tree_node_ordering { + use super::*; + + #[test] + fn same_value_yield_equal_order() { + let block = block_node!(num: 5, slot: 6); + let tx = tx_node!(hash: "tx_hash-1", block: 5, slot: 6); + + assert_eq!(Ordering::Equal, block.cmp(&block)); + assert_eq!(Ordering::Equal, tx.cmp(&tx)); + } + + #[test] + fn order_block_nodes_first_then_transaction_nodes() { + let block = block_node!(num: 5, slot: 6); + let tx = tx_node!(hash: "tx_hash-1", block: 5, slot: 6); + + assert_eq!(Ordering::Less, block.cmp(&tx)); + assert_eq!(Ordering::Greater, tx.cmp(&block)); + } + + #[test] + fn order_blocks_by_block_number_first() { + let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); + + assert_eq!( + Ordering::Less, + block.cmp(&block_node!(hash: "block_hash-1", num: 10, slot: 1)) + ); + assert_eq!( + Ordering::Greater, + block.cmp(&block_node!(hash: "block_hash-9", num: 1, slot: 9)) + ); + } + + #[test] + fn order_blocks_by_slot_number_second() { + let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); + + assert_eq!( + Ordering::Less, + block.cmp(&block_node!(hash: "block_hash-1", num: 5, slot: 9)) + ); + assert_eq!( + Ordering::Greater, + block.cmp(&block_node!(hash: "block_hash-9", num: 5, slot: 1)) + ); + } + + #[test] + fn order_blocks_by_block_hash_third() { + let block = block_node!(hash: "block_hash-5", num: 5, slot: 6); + + assert_eq!( + Ordering::Less, + block.cmp(&block_node!(hash: "block_hash-9", num: 5, slot: 6)) + ); + assert_eq!( + Ordering::Greater, + block.cmp(&block_node!(hash: "block_hash-1", num: 5, slot: 6)) + ); + } + + #[test] + fn order_transactions_by_block_number_first() { + let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); + + assert_eq!( + Ordering::Less, + tx.cmp( + &tx_node!(hash: "tx_hash-1", block_hash: "block_hash-1", block: 10, slot: 1) + ) + ); + assert_eq!( + Ordering::Greater, + tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-9", block: 1, slot: 9)) + ); + } + + #[test] + fn order_transactions_by_slot_number_second() { + let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); + + assert_eq!( + Ordering::Less, + tx.cmp(&tx_node!(hash: "tx_hash-1", block_hash: "block_hash-1", block: 5, slot: 9)) + ); + assert_eq!( + Ordering::Greater, + tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-9", block: 5, slot: 1)) + ); + } + + #[test] + fn order_transactions_by_block_hash_third() { + let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); + + assert_eq!( + Ordering::Less, + tx.cmp(&tx_node!(hash: "tx_hash-1", block_hash: "block_hash-9", block: 5, slot: 6)) + ); + assert_eq!( + Ordering::Greater, + tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-1", block: 5, slot: 6)) + ); + } + + #[test] + fn order_transactions_by_transaction_hash_fourth() { + let tx = tx_node!(hash: "tx_hash-5", block_hash: "block_hash-5", block: 5, slot: 6); + + assert_eq!( + Ordering::Less, + tx.cmp(&tx_node!(hash: "tx_hash-9", block_hash: "block_hash-5", block: 5, slot: 6)) + ); + assert_eq!( + Ordering::Greater, + tx.cmp(&tx_node!(hash: "tx_hash-1", block_hash: "block_hash-5", block: 5, slot: 6)) + ); + } + + #[test] + fn sorting_a_vec() { + let mut list = vec![ + tx_node!(hash: "tx_hash-70", block: 300, slot: 35), + tx_node!(hash: "tx_hash-200", block: 100, slot: 100), + tx_node!(hash: "tx_hash-50", block: 200, slot: 25), + block_node!(num: 100, slot: 100), + tx_node!(hash: "tx_hash-100", block: 100, slot: 100), + block_node!(num: 200, slot: 35), + block_node!(num: 200, slot: 25), + ]; + list.sort(); + + assert_eq!( + list, + vec![ + block_node!(num: 100, slot: 100), + block_node!(num: 200, slot: 25), + block_node!(num: 200, slot: 35), + tx_node!(hash: "tx_hash-100", block: 100, slot: 100), + tx_node!(hash: "tx_hash-200", block: 100, slot: 100), + tx_node!(hash: "tx_hash-50", block: 200, slot: 25), + tx_node!(hash: "tx_hash-70", block: 300, slot: 35), + ] + ); + } + } +} diff --git a/mithril-common/src/entities/mod.rs b/mithril-common/src/entities/mod.rs index 27a47aa5b3f..470019d56d8 100644 --- a/mithril-common/src/entities/mod.rs +++ b/mithril-common/src/entities/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod arithmetic_operation_wrapper; mod block_number; mod block_range; mod cardano_block; +mod cardano_block_transaction_mktree_node; mod cardano_blocks_transactions_snapshot; mod cardano_chain_point; mod cardano_database; @@ -38,6 +39,7 @@ mod type_alias; pub use block_number::BlockNumber; pub use block_range::{BlockRange, BlockRangeLength, BlockRangesSequence}; pub use cardano_block::*; +pub use cardano_block_transaction_mktree_node::*; pub use cardano_blocks_transactions_snapshot::CardanoBlocksTransactionsSnapshot; pub use cardano_chain_point::{BlockHash, ChainPoint}; pub use cardano_database::{ From 8106af09b1a5e2f6888d6c82f36c9e7bfc811b21 Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:38:28 +0100 Subject: [PATCH 2/8] test(cardano-node-chain): impl `BlockRangeRootRetriever` to `InMemoryChainDataStore` So it can be used in the new prover tests --- .../src/test/double/chain_data_store.rs | 150 +++++++++++++++++- 1 file changed, 148 insertions(+), 2 deletions(-) diff --git a/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs b/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs index 80414e1dd02..1092b43e7d3 100644 --- a/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs +++ b/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs @@ -1,14 +1,14 @@ use std::collections::BTreeSet; use std::ops::Range; - use tokio::sync::Mutex; use mithril_common::StdResult; -use mithril_common::crypto_helper::MKTreeNode; +use mithril_common::crypto_helper::{MKTreeNode, MKTreeStorer}; use mithril_common::entities::{ BlockNumber, BlockRange, CardanoBlockTransactionMkTreeNode, CardanoBlockWithTransactions, CardanoTransaction, ChainPoint, SlotNumber, }; +use mithril_common::signable_builder::BlockRangeRootRetriever; use crate::chain_importer::ChainDataStore; @@ -256,8 +256,29 @@ impl ChainDataStore for InMemoryChainDataStore { } } +#[async_trait::async_trait] +impl BlockRangeRootRetriever for InMemoryChainDataStore { + async fn retrieve_block_range_roots<'a>( + &'a self, + up_to_beacon: BlockNumber, + ) -> StdResult + 'a>> { + let block_ranges = self.block_range_roots.lock().await; + let result: Vec<_> = block_ranges + .iter() + .filter(|r| r.range.start < up_to_beacon) + .cloned() + .map(|r| (r.range, r.merkle_root)) + .collect(); + Ok(Box::new(result.into_iter())) + } +} + #[cfg(test)] mod tests { + use std::sync::Arc; + + use mithril_common::crypto_helper::MKTreeStoreInMemory; + use super::*; #[tokio::test] @@ -990,4 +1011,129 @@ mod tests { let remaining = store.get_all_block_with_txs().await; assert_eq!(blocks_with_tx[0..1].to_vec(), remaining); } + + mod retrieve_blocks_range_root { + use super::*; + + fn test_data_set() -> Vec<(BlockRange, MKTreeNode)> { + vec![ + ( + BlockRange::from_block_number(BlockNumber(15)), + MKTreeNode::from_hex("AAAA").unwrap(), + ), + ( + BlockRange::from_block_number(BlockNumber(30)), + MKTreeNode::from_hex("BBBB").unwrap(), + ), + ( + BlockRange::from_block_number(BlockNumber(45)), + MKTreeNode::from_hex("CCCC").unwrap(), + ), + ( + BlockRange::from_block_number(BlockNumber(60)), + MKTreeNode::from_hex("CCCC").unwrap(), + ), + ] + } + + #[tokio::test] + async fn returns_empty_when_store_empty() { + let store = Arc::new(InMemoryChainDataStore::builder().build()); + let retriever = store.clone() as Arc>; + + let iter = retriever + .retrieve_block_range_roots(BlockNumber(u64::MAX)) + .await + .unwrap(); + assert_eq!( + Vec::<(BlockRange, MKTreeNode)>::new(), + iter.collect::>() + ); + } + + #[tokio::test] + async fn up_to_above_all_stored_ranges_returns_all() { + let store = Arc::new( + InMemoryChainDataStore::builder() + .with_block_range_roots(&test_data_set()) + .build(), + ); + let retriever = store.clone() as Arc>; + + let stored_ranges: Vec<(BlockRange, MKTreeNode)> = retriever + .retrieve_block_range_roots(BlockNumber(u64::MAX)) + .await + .unwrap() + .collect(); + assert_eq!(&test_data_set(), &stored_ranges); + } + + #[tokio::test] + async fn up_to_below_start_of_the_first_range_returns_nothing() { + let store = Arc::new( + InMemoryChainDataStore::builder() + .with_block_range_roots(&test_data_set()) + .build(), + ); + let retriever = store.clone() as Arc>; + + let stored_ranges: Vec<(BlockRange, MKTreeNode)> = retriever + .retrieve_block_range_roots(BlockNumber(10)) + .await + .unwrap() + .collect(); + assert_eq!(&Vec::<(BlockRange, MKTreeNode)>::new(), &stored_ranges); + } + + #[tokio::test] + async fn up_to_right_below_start_of_the_third_range_returns_the_first_two_ranges() { + let store = Arc::new( + InMemoryChainDataStore::builder() + .with_block_range_roots(&test_data_set()) + .build(), + ); + let retriever = store.clone() as Arc>; + + let stored_ranges: Vec<(BlockRange, MKTreeNode)> = retriever + .retrieve_block_range_roots(BlockNumber(44)) + .await + .unwrap() + .collect(); + assert_eq!(&test_data_set()[0..2], &stored_ranges); + } + + #[tokio::test] + async fn up_to_right_at_start_of_the_third_range_returns_the_first_two_ranges() { + let store = Arc::new( + InMemoryChainDataStore::builder() + .with_block_range_roots(&test_data_set()) + .build(), + ); + let retriever = store.clone() as Arc>; + + let stored_ranges: Vec<(BlockRange, MKTreeNode)> = retriever + .retrieve_block_range_roots(BlockNumber(45)) + .await + .unwrap() + .collect(); + assert_eq!(&test_data_set()[0..2], &stored_ranges); + } + + #[tokio::test] + async fn up_to_right_after_start_of_the_third_range_returns_the_first_three_ranges() { + let store = Arc::new( + InMemoryChainDataStore::builder() + .with_block_range_roots(&test_data_set()) + .build(), + ); + let retriever = store.clone() as Arc>; + + let stored_ranges: Vec<(BlockRange, MKTreeNode)> = retriever + .retrieve_block_range_roots(BlockNumber(46)) + .await + .unwrap() + .collect(); + assert_eq!(&test_data_set()[0..3], &stored_ranges); + } + } } From 39674db8d7b6edd9786d36f279a63c6eaf49f74b Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:59:07 +0100 Subject: [PATCH 3/8] test(cardano-node-chain): add methods to retrieve data needed by the new prover to `InMemoryChainDataStore` To allow its usage in the new prover test --- .../src/test/double/chain_data_store.rs | 250 +++++++++++++++--- mithril-common/src/entities/cardano_block.rs | 10 + 2 files changed, 226 insertions(+), 34 deletions(-) diff --git a/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs b/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs index 1092b43e7d3..ad86239ba90 100644 --- a/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs +++ b/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs @@ -1,12 +1,13 @@ use std::collections::BTreeSet; use std::ops::Range; + use tokio::sync::Mutex; use mithril_common::StdResult; use mithril_common::crypto_helper::{MKTreeNode, MKTreeStorer}; use mithril_common::entities::{ - BlockNumber, BlockRange, CardanoBlockTransactionMkTreeNode, CardanoBlockWithTransactions, - CardanoTransaction, ChainPoint, SlotNumber, + BlockHash, BlockNumber, BlockRange, CardanoBlock, CardanoBlockTransactionMkTreeNode, + CardanoBlockWithTransactions, CardanoTransaction, ChainPoint, SlotNumber, TransactionHash, }; use mithril_common::signable_builder::BlockRangeRootRetriever; @@ -145,6 +146,54 @@ impl InMemoryChainDataStore { .map(|r| r.range.clone()) .collect() } + + /// Returns all [CardanoBlock] with the given block hashes. + pub async fn get_blocks_by_hashes(&self, block_hashes: &[BlockHash]) -> Vec { + let blocks_with_txs = self.blocks_with_txs.lock().await; + blocks_with_txs + .iter() + .filter(|block| block_hashes.contains(&block.block_hash)) + .cloned() + .map(Into::into) + .collect() + } + + /// Returns all [CardanoTransaction] with the given transaction hashes. + pub async fn get_transactions_by_hashes( + &self, + transaction_hashes: &[TransactionHash], + ) -> Vec { + let blocks_with_txs = self.blocks_with_txs.lock().await.clone(); + blocks_with_txs + .into_iter() + .flat_map(|block| block.into_transactions()) + .filter(|tx| transaction_hashes.contains(&tx.transaction_hash)) + .collect() + } + + /// Returns all [CardanoBlockTransactionMkTreeNode] contained in the given block ranges. + pub async fn get_blocks_with_transactions_in_block_ranges( + &self, + block_ranges: &[BlockRange], + ) -> BTreeSet { + let ranges: Vec> = + block_ranges.iter().map(|range| range.clone().into()).collect(); + self.get_blocks_with_transactions_in_ranges(&ranges).await + } + + /// Returns all [CardanoBlockTransactionMkTreeNode] contained in the given ranges. + pub async fn get_blocks_with_transactions_in_ranges( + &self, + block_ranges: &[Range], + ) -> BTreeSet { + let blocks = self.blocks_with_txs.lock().await; + blocks + .iter() + .filter(|block| block_ranges.iter().any(|range| range.contains(&block.block_number))) + .cloned() + .flat_map(|block| block.into_mk_tree_node()) + .collect() + } } #[async_trait::async_trait] @@ -179,13 +228,7 @@ impl ChainDataStore for InMemoryChainDataStore { &self, range: Range, ) -> StdResult> { - let blocks = self.blocks_with_txs.lock().await; - Ok(blocks - .iter() - .filter(|block| range.contains(&block.block_number)) - .cloned() - .flat_map(|block| block.into_mk_tree_node()) - .collect()) + Ok(self.get_blocks_with_transactions_in_ranges(&[range]).await) } async fn get_transactions_in_range( @@ -747,19 +790,18 @@ mod tests { } #[tokio::test] - async fn get_blocks_and_transactions_in_range_returns_empty_when_store_empty() { + async fn get_blocks_with_transactions_in_ranges_returns_empty_when_store_empty() { let store = InMemoryChainDataStore::default(); let transactions = store - .get_blocks_and_transactions_in_range(BlockNumber(0)..BlockNumber(100)) - .await - .unwrap(); + .get_blocks_with_transactions_in_ranges(&[BlockNumber(0)..BlockNumber(100)]) + .await; assert!(transactions.is_empty()); } #[tokio::test] - async fn get_blocks_and_transactions_in_range_filters_correctly() { + async fn get_blocks_with_transactions_in_ranges_filters_correctly() { fn into_expected_nodes( cbtx: &[CardanoBlockWithTransactions], ) -> BTreeSet { @@ -790,50 +832,190 @@ mod tests { .with_blocks_and_transactions(&blocks_with_tx) .build(); - // Range excludes all transactions + // One range that include no transactions { let result = store - .get_blocks_and_transactions_in_range(BlockNumber(0)..BlockNumber(10)) - .await - .unwrap(); + .get_blocks_with_transactions_in_ranges(&[BlockNumber(0)..BlockNumber(10)]) + .await; assert!(result.is_empty()); } - // Range after all transactions + // One range after all transactions { let result = store - .get_blocks_and_transactions_in_range(BlockNumber(13)..BlockNumber(21)) - .await - .unwrap(); + .get_blocks_with_transactions_in_ranges(&[BlockNumber(13)..BlockNumber(21)]) + .await; assert!(result.is_empty()); } - // Range includes the first two transactions (10, 11) + // One range includes the first two transactions (10, 11) { let result = store - .get_blocks_and_transactions_in_range(BlockNumber(9)..BlockNumber(12)) - .await - .unwrap(); + .get_blocks_with_transactions_in_ranges(&[BlockNumber(9)..BlockNumber(12)]) + .await; assert_eq!(into_expected_nodes(&blocks_with_tx[0..=1]), result); } - // Range includes all transactions + // One range includes all transactions { let result = store - .get_blocks_and_transactions_in_range(BlockNumber(10)..BlockNumber(13)) - .await - .unwrap(); + .get_blocks_with_transactions_in_ranges(&[BlockNumber(10)..BlockNumber(13)]) + .await; assert_eq!(into_expected_nodes(&blocks_with_tx), result); } - // Range includes the last two transactions (11, 12) + // One range includes the last two transactions (11, 12) { let result = store - .get_blocks_and_transactions_in_range(BlockNumber(11)..BlockNumber(14)) - .await - .unwrap(); + .get_blocks_with_transactions_in_ranges(&[BlockNumber(11)..BlockNumber(14)]) + .await; assert_eq!(into_expected_nodes(&blocks_with_tx[1..=2]), result); } + + // Two ranges, one that include the first transaction (10) and the other that include the last transaction (12) + { + let result = store + .get_blocks_with_transactions_in_ranges(&[ + BlockNumber(9)..BlockNumber(11), + BlockNumber(12)..BlockNumber(13), + ]) + .await; + assert_eq!( + into_expected_nodes(&[blocks_with_tx[0].clone(), blocks_with_tx[2].clone()]), + result + ); + } + } + + #[tokio::test] + async fn get_blocks_by_hashes_filters_correctly() { + let blocks_with_tx = vec![ + CardanoBlockWithTransactions::new( + "block-hash-1", + BlockNumber(10), + SlotNumber(50), + vec!["tx-hash-1"], + ), + CardanoBlockWithTransactions::new( + "block-hash-2", + BlockNumber(11), + SlotNumber(51), + vec!["tx-hash-2"], + ), + CardanoBlockWithTransactions::new( + "block-hash-3", + BlockNumber(12), + SlotNumber(52), + vec!["tx-hash-3"], + ), + ]; + let store = InMemoryChainDataStore::builder() + .with_blocks_and_transactions(&blocks_with_tx) + .build(); + + // Get one existing hash + { + let result = store.get_blocks_by_hashes(&["block-hash-1".to_string()]).await; + assert_eq!( + vec![CardanoBlock::new("block-hash-1", BlockNumber(10), SlotNumber(50))], + result + ); + } + // Get one non-existing hash + { + let result = store.get_blocks_by_hashes(&["block-hash-4".to_string()]).await; + assert_eq!(Vec::::new(), result); + } + // Get two existing hashes and one non-existing hash + { + let result = store + .get_blocks_by_hashes(&[ + "block-hash-1".to_string(), + "block-hash-3".to_string(), + "block-hash-4".to_string(), + ]) + .await; + assert_eq!( + vec![ + CardanoBlock::new("block-hash-1", BlockNumber(10), SlotNumber(50)), + CardanoBlock::new("block-hash-3", BlockNumber(12), SlotNumber(52)) + ], + result + ); + } + } + + #[tokio::test] + async fn get_transactions_by_hashes_filters_correctly() { + let blocks_with_tx = vec![ + CardanoBlockWithTransactions::new( + "block-hash-1", + BlockNumber(10), + SlotNumber(50), + vec!["tx-hash-1"], + ), + CardanoBlockWithTransactions::new( + "block-hash-2", + BlockNumber(11), + SlotNumber(51), + vec!["tx-hash-2"], + ), + CardanoBlockWithTransactions::new( + "block-hash-3", + BlockNumber(12), + SlotNumber(52), + vec!["tx-hash-3"], + ), + ]; + let store = InMemoryChainDataStore::builder() + .with_blocks_and_transactions(&blocks_with_tx) + .build(); + + // Get one existing hash + { + let result = store.get_transactions_by_hashes(&["tx-hash-1".to_string()]).await; + assert_eq!( + vec![CardanoTransaction::new( + "tx-hash-1", + BlockNumber(10), + SlotNumber(50), + "block-hash-1" + )], + result + ); + } + // Get one non-existing hash + { + let result = store.get_transactions_by_hashes(&["tx-hash-4".to_string()]).await; + assert_eq!(Vec::::new(), result); + } + // Get two existing hashes and one non-existing hash + { + let result = store + .get_transactions_by_hashes(&[ + "tx-hash-1".to_string(), + "tx-hash-3".to_string(), + "tx-hash-4".to_string(), + ]) + .await; + assert_eq!( + vec![ + CardanoTransaction::new( + "tx-hash-1", + BlockNumber(10), + SlotNumber(50), + "block-hash-1" + ), + CardanoTransaction::new( + "tx-hash-3", + BlockNumber(12), + SlotNumber(52), + "block-hash-3" + ) + ], + result + ); + } } #[tokio::test] diff --git a/mithril-common/src/entities/cardano_block.rs b/mithril-common/src/entities/cardano_block.rs index 81aa1f9345b..ad7d975ae62 100644 --- a/mithril-common/src/entities/cardano_block.rs +++ b/mithril-common/src/entities/cardano_block.rs @@ -98,3 +98,13 @@ impl CardanoBlockWithTransactions { self.transactions_hashes.len() } } + +impl From for CardanoBlock { + fn from(value: CardanoBlockWithTransactions) -> Self { + Self { + block_hash: value.block_hash, + block_number: value.block_number, + slot_number: value.slot_number, + } + } +} From 81aaf167880df0e7be3563350e9fe85bf212d2a2 Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:16:31 +0100 Subject: [PATCH 4/8] test(cardano-node-chain): add `InMemoryChainDataStore::compute_block_ranges` By leveraging tested `BlockRangeImporter` --- .../src/chain_importer/mod.rs | 1 + .../src/test/double/chain_data_store.rs | 94 +++++++++++++++++-- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/internal/cardano-node/mithril-cardano-node-chain/src/chain_importer/mod.rs b/internal/cardano-node/mithril-cardano-node-chain/src/chain_importer/mod.rs index 8d78d223a83..6051d26ff33 100644 --- a/internal/cardano-node/mithril-cardano-node-chain/src/chain_importer/mod.rs +++ b/internal/cardano-node/mithril-cardano-node-chain/src/chain_importer/mod.rs @@ -9,6 +9,7 @@ mod importer_with_pruner; mod service; pub use api::*; +pub(crate) use block_ranges_importer::*; pub use importer_by_chunk::*; pub use importer_with_pruner::*; pub use service::*; diff --git a/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs b/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs index ad86239ba90..fb7194271d3 100644 --- a/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs +++ b/internal/cardano-node/mithril-cardano-node-chain/src/test/double/chain_data_store.rs @@ -1,6 +1,8 @@ use std::collections::BTreeSet; use std::ops::Range; +use std::sync::Arc; +use slog::Logger; use tokio::sync::Mutex; use mithril_common::StdResult; @@ -11,7 +13,7 @@ use mithril_common::entities::{ }; use mithril_common::signable_builder::BlockRangeRootRetriever; -use crate::chain_importer::ChainDataStore; +use crate::chain_importer::{BlockRangeImporter, ChainDataStore}; /// In memory Block range root representation, for testing purposes. #[derive(Debug, PartialEq, Clone)] @@ -58,7 +60,7 @@ pub struct InMemoryChainDataStoreBuilder { } impl InMemoryChainDataStoreBuilder { - /// Set the initial blocks and transactions for the store. + /// Set the initial blocks and transactions for the store, replacing any existing data. pub fn with_blocks_and_transactions + Clone>( mut self, transactions: &[T], @@ -67,7 +69,7 @@ impl InMemoryChainDataStoreBuilder { self } - /// Set the initial block range roots for the store. + /// Set the initial block range roots for the store, replacing any existing data. pub fn with_block_range_roots + Clone>( mut self, block_range_roots: &[T], @@ -76,7 +78,7 @@ impl InMemoryChainDataStoreBuilder { self } - /// Set the initial block range roots for the store. + /// Set the initial block range roots for the store, replacing any existing data. pub fn with_legacy_block_range_roots + Clone>( mut self, block_range_roots: &[T], @@ -86,6 +88,32 @@ impl InMemoryChainDataStoreBuilder { self } + /// Computes the block ranges roots, new and legacy, up to the block range that includes the given + /// block number and based on the blocks given in [Self::with_blocks_and_transactions] + /// + /// Replace any existing block ranges roots and legacy block ranges roots. + pub async fn compute_block_ranges( + mut self, + up_to_block_range_that_include: BlockNumber, + ) -> Self { + // Leverage tested `BlockRangeImporter` to compute the block ranges roots. + let worker_store = Arc::new(InMemoryChainDataStore { + blocks_with_txs: Mutex::new(self.blocks_with_txs.clone()), + block_range_roots: Default::default(), + legacy_block_range_roots: Default::default(), + }); + + let discard_logger = Logger::root(slog::Discard, slog::o!()); + let importer = BlockRangeImporter::new(worker_store.clone(), discard_logger); + importer.run(up_to_block_range_that_include).await.unwrap(); + importer.run_legacy(up_to_block_range_that_include).await.unwrap(); + + self.block_range_roots = worker_store.get_all_block_range_root().await; + self.legacy_block_range_roots = worker_store.get_all_legacy_block_range_root().await; + + self + } + /// Creates a new [InMemoryChainDataStore] with the current builder's data. pub fn build(self) -> InMemoryChainDataStore { InMemoryChainDataStore { @@ -318,12 +346,66 @@ impl BlockRangeRootRetriever for InMemoryChainDataStore { #[cfg(test)] mod tests { - use std::sync::Arc; - use mithril_common::crypto_helper::MKTreeStoreInMemory; use super::*; + #[tokio::test] + async fn builder_can_compute_block_range_roots() { + let expected_computed_ranges = vec![ + BlockRange::from_block_number(BlockNumber(10)), + BlockRange::from_block_number(BlockNumber(25)), + ]; + let builder = InMemoryChainDataStore::builder().with_blocks_and_transactions(&[ + CardanoBlockWithTransactions::new( + "block_hash-10", + BlockNumber(10), + SlotNumber(50), + vec!["tx_hash-1"], + ), + CardanoBlockWithTransactions::new( + "block_hash-25", + BlockNumber(25), + SlotNumber(51), + vec!["tx_hash-2", "tx_hash-3"], + ), + CardanoBlockWithTransactions::new( + "block_hash-30", + BlockNumber(30), + SlotNumber(52), + vec!["tx_hash-4", "tx_hash-5"], + ), + ]); + + assert_eq!( + Vec::::new(), + builder.block_range_roots + ); + assert_eq!( + Vec::::new(), + builder.legacy_block_range_roots + ); + + let builder = builder.compute_block_ranges(BlockNumber(29)).await; + + assert_eq!( + expected_computed_ranges, + builder + .block_range_roots + .iter() + .map(|b| b.range.clone()) + .collect::>() + ); + assert_eq!( + expected_computed_ranges, + builder + .legacy_block_range_roots + .iter() + .map(|b| b.range.clone()) + .collect::>() + ); + } + #[tokio::test] async fn default_store_is_empty() { let store = InMemoryChainDataStore::default(); From 80413642acc5b4271148afe6523cdd7ccc9d0022 Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:56:58 +0100 Subject: [PATCH 5/8] refactor(common): rename `CardanoTransactionsBuilder::build_block_ranges` to `build_transactions_for_block_ranges` and restructure tests thoses changes prepare the extension of capabilities of the builder to also build `CardanoBlockWithTransaction`. --- .../src/services/prover_legacy.rs | 12 +- .../builder/cardano_transactions_builder.rs | 310 +++++++++--------- 2 files changed, 159 insertions(+), 163 deletions(-) diff --git a/mithril-aggregator/src/services/prover_legacy.rs b/mithril-aggregator/src/services/prover_legacy.rs index 8ae041c9191..4e09ab31919 100644 --- a/mithril-aggregator/src/services/prover_legacy.rs +++ b/mithril-aggregator/src/services/prover_legacy.rs @@ -373,7 +373,7 @@ mod tests { let transactions = CardanoTransactionsBuilder::new() .max_transactions_per_block(1) .blocks_per_block_range(3) - .build_block_ranges(5); + .build_transactions_for_block_ranges(5); let transactions_to_prove = test_data::filter_transactions_for_indices(&[1, 2, 4], &transactions); let test_data = test_data::build_test_data(&transactions_to_prove, &transactions); @@ -425,7 +425,7 @@ mod tests { let transactions = CardanoTransactionsBuilder::new() .max_transactions_per_block(1) .blocks_per_block_range(3) - .build_block_ranges(5); + .build_transactions_for_block_ranges(5); let transactions_to_prove = test_data::filter_transactions_for_indices(&[1, 2, 4], &transactions); let test_data = test_data::build_test_data(&transactions_to_prove, &transactions); @@ -467,7 +467,7 @@ mod tests { let transactions = CardanoTransactionsBuilder::new() .max_transactions_per_block(1) .blocks_per_block_range(3) - .build_block_ranges(5); + .build_transactions_for_block_ranges(5); let transactions_to_prove = test_data::filter_transactions_for_indices(&[], &transactions); let mut test_data = test_data::build_test_data(&transactions_to_prove, &transactions); test_data.transaction_hashes_to_prove = vec!["tx-unknown-123".to_string()]; @@ -514,7 +514,7 @@ mod tests { let transactions = CardanoTransactionsBuilder::new() .max_transactions_per_block(1) .blocks_per_block_range(3) - .build_block_ranges(5); + .build_transactions_for_block_ranges(5); let transactions_to_prove = test_data::filter_transactions_for_indices(&[1, 2, 4], &transactions); let transaction_hashes_unknown = @@ -574,7 +574,7 @@ mod tests { let transactions = CardanoTransactionsBuilder::new() .max_transactions_per_block(1) .blocks_per_block_range(3) - .build_block_ranges(5); + .build_transactions_for_block_ranges(5); let transactions_to_prove = test_data::filter_transactions_for_indices(&[1, 2, 4], &transactions); let test_data = test_data::build_test_data(&transactions_to_prove, &transactions); @@ -603,7 +603,7 @@ mod tests { let transactions = CardanoTransactionsBuilder::new() .max_transactions_per_block(1) .blocks_per_block_range(3) - .build_block_ranges(5); + .build_transactions_for_block_ranges(5); let transactions_to_prove = test_data::filter_transactions_for_indices(&[1, 2, 4], &transactions); let test_data = test_data::build_test_data(&transactions_to_prove, &transactions); diff --git a/mithril-common/src/test/builder/cardano_transactions_builder.rs b/mithril-common/src/test/builder/cardano_transactions_builder.rs index d5c6ec8fdcf..cabc6e9ba6a 100644 --- a/mithril-common/src/test/builder/cardano_transactions_builder.rs +++ b/mithril-common/src/test/builder/cardano_transactions_builder.rs @@ -31,7 +31,7 @@ use crate::entities::{BlockNumber, BlockRange, CardanoTransaction, SlotNumber}; /// ); /// ``` /// -/// # Example 'build_block_ranges' +/// # Example 'build_transactions_for_block_ranges' /// /// ``` /// use mithril_common::entities::{BlockNumber, CardanoTransaction, SlotNumber}; @@ -40,7 +40,7 @@ use crate::entities::{BlockNumber, BlockRange, CardanoTransaction, SlotNumber}; /// let txs = CardanoTransactionsBuilder::new() /// .max_transactions_per_block(3) /// .blocks_per_block_range(2) -/// .build_block_ranges(2); +/// .build_transactions_for_block_ranges(2); /// /// assert_eq!(3 * 2 * 2, txs.len()); /// assert_eq!( @@ -114,7 +114,10 @@ impl CardanoTransactionsBuilder { } /// Build a list of transactions to get the number of block range requested. - pub fn build_block_ranges(self, block_ranges_count: usize) -> Vec { + pub fn build_transactions_for_block_ranges( + self, + block_ranges_count: usize, + ) -> Vec { let nb_txs = block_ranges_count * self.max_blocks_per_block_range * self.max_transactions_per_block; @@ -153,187 +156,180 @@ mod test { use super::*; - fn count_distinct_values(list: &[T], extract_value: &dyn Fn(&T) -> R) -> usize + fn count_distinct_values(list: &[T], extract_value: &dyn Fn(T) -> R) -> usize where + T: Clone, R: Eq + std::hash::Hash, { - list.iter().map(extract_value).collect::>().len() + list.iter().cloned().map(extract_value).collect::>().len() } - fn group_by<'a, T, R>(list: &'a [T], extract_value: &dyn Fn(&T) -> R) -> HashMap> + fn group_by<'a, T, R>(list: &'a [T], extract_value: &dyn Fn(T) -> R) -> HashMap> where + T: Clone, R: Eq + std::hash::Hash, { let mut grouped_by_block = HashMap::new(); for t in list { - grouped_by_block.entry(extract_value(t)).or_insert(Vec::new()).push(t); + grouped_by_block + .entry(extract_value(t.clone())) + .or_insert(Vec::new()) + .push(t); } grouped_by_block } #[test] - fn return_given_number_of_transactions_with_distinct_values() { - let txs = CardanoTransactionsBuilder::new().build_transactions(3); - - assert_eq!(txs.len(), 3); - - assert_eq!( - 3, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number)); - assert_eq!(3, count_distinct_values(&txs, &|t| t.slot_number)); - assert_eq!(3, count_distinct_values(&txs, &|t| t.block_hash.clone())); + #[should_panic] + fn should_panic_when_too_many_blocks_per_block_range() { + CardanoTransactionsBuilder::new().blocks_per_block_range(*BlockRange::LENGTH as usize + 1); } - #[test] - fn return_all_transactions_in_same_block_when_ask_less_transactions_than_transactions_per_block() - { - let txs = CardanoTransactionsBuilder::new() - .max_transactions_per_block(10) - .build_transactions(3); - - assert_eq!(txs.len(), 3); - - assert_eq!( - 3, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - assert_eq!(1, count_distinct_values(&txs, &|t| t.block_number)); - assert_eq!(1, count_distinct_values(&txs, &|t| t.block_hash.clone())); - } + mod build_transactions { + use super::*; - #[test] - fn return_no_more_transactions_in_a_same_block_than_number_per_block_requested() { - let txs = CardanoTransactionsBuilder::new() - .max_transactions_per_block(3) - .build_transactions(12); - - assert_eq!(txs.len(), 12); - - assert_eq!( - 12, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - assert_eq!(4, count_distinct_values(&txs, &|t| t.block_number)); - assert_eq!(4, count_distinct_values(&txs, &|t| t.block_hash.clone())); - } + #[test] + fn return_given_number_of_transactions_with_distinct_values() { + let txs = CardanoTransactionsBuilder::new().build_transactions(3); - #[test] - fn only_the_last_block_is_not_full_when_we_can_not_fill_all_blocks() { - let txs = CardanoTransactionsBuilder::new() - .max_transactions_per_block(5) - .build_transactions(12); - - assert_eq!(txs.len(), 12); - - assert_eq!( - 12, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number)); - - let grouped_by_block = group_by(&txs, &|t| t.block_number); - let mut txs_per_block: Vec<_> = grouped_by_block.values().map(|v| v.len()).collect(); - txs_per_block.sort(); - assert_eq!(vec![2, 5, 5], txs_per_block); - } + assert_eq!(txs.len(), 3); - #[test] - fn generate_one_block_range_return_one_transaction_by_default() { - let txs = CardanoTransactionsBuilder::new().build_block_ranges(1); - assert_eq!(txs.len(), 1); - } + assert_eq!(3, count_distinct_values(&txs, &|t| t.transaction_hash)); + assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number)); + assert_eq!(3, count_distinct_values(&txs, &|t| t.slot_number)); + assert_eq!(3, count_distinct_values(&txs, &|t| t.block_hash)); + } - #[test] - fn build_block_ranges_return_the_number_of_block_ranges_requested() { - let block_ranges = 3; - let txs = CardanoTransactionsBuilder::new().build_block_ranges(block_ranges); + #[test] + fn return_all_transactions_in_same_block_when_ask_less_transactions_than_transactions_per_block() + { + let txs = CardanoTransactionsBuilder::new() + .max_transactions_per_block(10) + .build_transactions(3); - assert_eq!(txs.len(), 3); + assert_eq!(txs.len(), 3); - assert_eq!( - 3, - count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) - ); - } + assert_eq!(3, count_distinct_values(&txs, &|t| t.transaction_hash)); + assert_eq!(1, count_distinct_values(&txs, &|t| t.block_number)); + assert_eq!(1, count_distinct_values(&txs, &|t| t.block_hash)); + } - #[test] - fn build_block_ranges_return_many_transactions_per_block_when_requested() { - let txs = CardanoTransactionsBuilder::new() - .max_transactions_per_block(5) - .build_block_ranges(3); - - assert_eq!(txs.len(), 3 * 5); - - assert_eq!( - 3 * 5, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - - assert_eq!( - 3, - count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) - ); - assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number)); - assert_eq!(3, count_distinct_values(&txs, &|t| t.block_hash.clone())); - } + #[test] + fn return_no_more_transactions_in_a_same_block_than_number_per_block_requested() { + let txs = CardanoTransactionsBuilder::new() + .max_transactions_per_block(3) + .build_transactions(12); - #[test] - fn build_block_ranges_with_many_blocks_per_block_ranges() { - let txs = CardanoTransactionsBuilder::new() - .max_transactions_per_block(5) - .blocks_per_block_range(2) - .build_block_ranges(3); - - assert_eq!(txs.len(), 3 * 2 * 5); - - assert_eq!( - 3 * 2 * 5, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - - assert_eq!( - 3, - count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) - ); - assert_eq!(3 * 2, count_distinct_values(&txs, &|t| t.block_number)); - assert_eq!( - 3 * 2, - count_distinct_values(&txs, &|t| t.block_hash.clone()) - ); - } + assert_eq!(txs.len(), 12); - #[test] - fn build_transactions_with_many_blocks_per_block_ranges() { - let txs = CardanoTransactionsBuilder::new() - .max_transactions_per_block(5) - .blocks_per_block_range(2) - .build_transactions(18); - - // block range 1 - block 0 - 1, 2, 3, 4, 5 - // block range 1 - block 1 - 6, 7, 8, 7, 10 - // block range 2 - block 15 - 11, 12, 13, 14, 15 - // block range 2 - block 16 - 16, 17, 18 - - assert_eq!(txs.len(), 18); - - assert_eq!( - 18, - count_distinct_values(&txs, &|t| t.transaction_hash.clone()) - ); - - assert_eq!( - 2, - count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) - ); - assert_eq!(4, count_distinct_values(&txs, &|t| t.block_number)); - assert_eq!(4, count_distinct_values(&txs, &|t| t.block_hash.clone())); + assert_eq!(12, count_distinct_values(&txs, &|t| t.transaction_hash)); + assert_eq!(4, count_distinct_values(&txs, &|t| t.block_number)); + assert_eq!(4, count_distinct_values(&txs, &|t| t.block_hash)); + } + + #[test] + fn only_the_last_block_is_not_full_when_we_can_not_fill_all_blocks() { + let txs = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .build_transactions(12); + + assert_eq!(txs.len(), 12); + + assert_eq!(12, count_distinct_values(&txs, &|t| t.transaction_hash)); + assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number)); + + let grouped_by_block = group_by(&txs, &|t| t.block_number); + let mut txs_per_block: Vec<_> = grouped_by_block.values().map(|v| v.len()).collect(); + txs_per_block.sort(); + assert_eq!(vec![2, 5, 5], txs_per_block); + } + + #[test] + fn build_transactions_with_many_blocks_per_block_ranges() { + let txs = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .blocks_per_block_range(2) + .build_transactions(18); + + // block range 1 - block 0 - 1, 2, 3, 4, 5 + // block range 1 - block 1 - 6, 7, 8, 7, 10 + // block range 2 - block 15 - 11, 12, 13, 14, 15 + // block range 2 - block 16 - 16, 17, 18 + + assert_eq!(txs.len(), 18); + + assert_eq!(18, count_distinct_values(&txs, &|t| t.transaction_hash)); + + assert_eq!( + 2, + count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) + ); + assert_eq!(4, count_distinct_values(&txs, &|t| t.block_number)); + assert_eq!(4, count_distinct_values(&txs, &|t| t.block_hash)); + } } - #[test] - #[should_panic] - fn should_panic_when_too_many_blocks_per_block_range() { - CardanoTransactionsBuilder::new().blocks_per_block_range(*BlockRange::LENGTH as usize + 1); + mod build_transactions_for_block_ranges { + use super::*; + + #[test] + fn generate_transactions_for_one_block_range_return_one_transaction_by_default() { + let txs = CardanoTransactionsBuilder::new().build_transactions_for_block_ranges(1); + assert_eq!(txs.len(), 1); + } + + #[test] + fn build_transactions_for_block_ranges_return_the_number_of_block_ranges_requested() { + let block_ranges = 3; + let txs = + CardanoTransactionsBuilder::new().build_transactions_for_block_ranges(block_ranges); + + assert_eq!(txs.len(), 3); + + assert_eq!( + 3, + count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) + ); + } + + #[test] + fn build_transactions_for_block_ranges_return_many_transactions_per_block_when_requested() { + let txs = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .build_transactions_for_block_ranges(3); + + assert_eq!(txs.len(), 3 * 5); + + assert_eq!(3 * 5, count_distinct_values(&txs, &|t| t.transaction_hash)); + + assert_eq!( + 3, + count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) + ); + assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number)); + assert_eq!(3, count_distinct_values(&txs, &|t| t.block_hash)); + } + + #[test] + fn build_transactions_for_block_ranges_with_many_blocks_per_block_ranges() { + let txs = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .blocks_per_block_range(2) + .build_transactions_for_block_ranges(3); + + assert_eq!(txs.len(), 3 * 2 * 5); + + assert_eq!( + 3 * 2 * 5, + count_distinct_values(&txs, &|t| t.transaction_hash) + ); + + assert_eq!( + 3, + count_distinct_values(&txs, &|t| BlockRange::start(t.block_number)) + ); + assert_eq!(3 * 2, count_distinct_values(&txs, &|t| t.block_number)); + assert_eq!(3 * 2, count_distinct_values(&txs, &|t| t.block_hash)); + } } } From e9f49feaf665cb1e8dc9a28bea48571e20272f18 Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:01:10 +0100 Subject: [PATCH 6/8] test(common): refactor `CardanoTransactionsBuilder` to works internaly on blocks instead of transactions this will allow to extend it's capabilities to the builds of `CardanoBlockWithTransactions`. --- .../builder/cardano_transactions_builder.rs | 133 +++++++++++------- 1 file changed, 82 insertions(+), 51 deletions(-) diff --git a/mithril-common/src/test/builder/cardano_transactions_builder.rs b/mithril-common/src/test/builder/cardano_transactions_builder.rs index cabc6e9ba6a..f95fb5c95da 100644 --- a/mithril-common/src/test/builder/cardano_transactions_builder.rs +++ b/mithril-common/src/test/builder/cardano_transactions_builder.rs @@ -1,4 +1,6 @@ -use crate::entities::{BlockNumber, BlockRange, CardanoTransaction, SlotNumber}; +use crate::entities::{ + BlockNumber, BlockRange, CardanoBlockWithTransactions, CardanoTransaction, SlotNumber, +}; /// Builder to easily build transactions with consistent values. /// @@ -19,13 +21,13 @@ use crate::entities::{BlockNumber, BlockRange, CardanoTransaction, SlotNumber}; /// assert_eq!( /// vec![ /// CardanoTransaction::new("tx-hash-0-100", BlockNumber(0), SlotNumber(100), "block-hash-0"), -/// CardanoTransaction::new("tx-hash-0-101", BlockNumber(0), SlotNumber(101), "block-hash-0"), -/// CardanoTransaction::new("tx-hash-0-102", BlockNumber(0), SlotNumber(102), "block-hash-0"), -/// CardanoTransaction::new("tx-hash-1-103", BlockNumber(1), SlotNumber(103), "block-hash-1"), -/// CardanoTransaction::new("tx-hash-1-104", BlockNumber(1), SlotNumber(104), "block-hash-1"), -/// CardanoTransaction::new("tx-hash-1-105", BlockNumber(1), SlotNumber(105), "block-hash-1"), -/// CardanoTransaction::new("tx-hash-15-106", BlockNumber(15), SlotNumber(106), "block-hash-15"), -/// CardanoTransaction::new("tx-hash-15-107", BlockNumber(15), SlotNumber(107), "block-hash-15") +/// CardanoTransaction::new("tx-hash-0-101", BlockNumber(0), SlotNumber(100), "block-hash-0"), +/// CardanoTransaction::new("tx-hash-0-102", BlockNumber(0), SlotNumber(100), "block-hash-0"), +/// CardanoTransaction::new("tx-hash-1-103", BlockNumber(1), SlotNumber(101), "block-hash-1"), +/// CardanoTransaction::new("tx-hash-1-104", BlockNumber(1), SlotNumber(101), "block-hash-1"), +/// CardanoTransaction::new("tx-hash-1-105", BlockNumber(1), SlotNumber(101), "block-hash-1"), +/// CardanoTransaction::new("tx-hash-15-106", BlockNumber(15), SlotNumber(115), "block-hash-15"), +/// CardanoTransaction::new("tx-hash-15-107", BlockNumber(15), SlotNumber(115), "block-hash-15") /// ], /// txs /// ); @@ -46,17 +48,17 @@ use crate::entities::{BlockNumber, BlockRange, CardanoTransaction, SlotNumber}; /// assert_eq!( /// vec![ /// CardanoTransaction::new("tx-hash-0-100", BlockNumber(0), SlotNumber(100), "block-hash-0"), -/// CardanoTransaction::new("tx-hash-0-101", BlockNumber(0), SlotNumber(101), "block-hash-0"), -/// CardanoTransaction::new("tx-hash-0-102", BlockNumber(0), SlotNumber(102), "block-hash-0"), -/// CardanoTransaction::new("tx-hash-1-103", BlockNumber(1), SlotNumber(103), "block-hash-1"), -/// CardanoTransaction::new("tx-hash-1-104", BlockNumber(1), SlotNumber(104), "block-hash-1"), -/// CardanoTransaction::new("tx-hash-1-105", BlockNumber(1), SlotNumber(105), "block-hash-1"), -/// CardanoTransaction::new("tx-hash-15-106", BlockNumber(15), SlotNumber(106), "block-hash-15"), -/// CardanoTransaction::new("tx-hash-15-107", BlockNumber(15), SlotNumber(107), "block-hash-15"), -/// CardanoTransaction::new("tx-hash-15-108", BlockNumber(15), SlotNumber(108), "block-hash-15"), -/// CardanoTransaction::new("tx-hash-16-109", BlockNumber(16), SlotNumber(109), "block-hash-16"), -/// CardanoTransaction::new("tx-hash-16-110", BlockNumber(16), SlotNumber(110), "block-hash-16"), -/// CardanoTransaction::new("tx-hash-16-111", BlockNumber(16), SlotNumber(111), "block-hash-16"), +/// CardanoTransaction::new("tx-hash-0-101", BlockNumber(0), SlotNumber(100), "block-hash-0"), +/// CardanoTransaction::new("tx-hash-0-102", BlockNumber(0), SlotNumber(100), "block-hash-0"), +/// CardanoTransaction::new("tx-hash-1-103", BlockNumber(1), SlotNumber(101), "block-hash-1"), +/// CardanoTransaction::new("tx-hash-1-104", BlockNumber(1), SlotNumber(101), "block-hash-1"), +/// CardanoTransaction::new("tx-hash-1-105", BlockNumber(1), SlotNumber(101), "block-hash-1"), +/// CardanoTransaction::new("tx-hash-15-106", BlockNumber(15), SlotNumber(115), "block-hash-15"), +/// CardanoTransaction::new("tx-hash-15-107", BlockNumber(15), SlotNumber(115), "block-hash-15"), +/// CardanoTransaction::new("tx-hash-15-108", BlockNumber(15), SlotNumber(115), "block-hash-15"), +/// CardanoTransaction::new("tx-hash-16-109", BlockNumber(16), SlotNumber(116), "block-hash-16"), +/// CardanoTransaction::new("tx-hash-16-110", BlockNumber(16), SlotNumber(116), "block-hash-16"), +/// CardanoTransaction::new("tx-hash-16-111", BlockNumber(16), SlotNumber(116), "block-hash-16"), /// ], /// txs /// ); @@ -72,7 +74,16 @@ impl Default for CardanoTransactionsBuilder { } } +enum FillUntil { + BlockCountReached, + TransactionCountReached, +} + impl CardanoTransactionsBuilder { + const FIRST_TRANSACTION_NUMBER: usize = 100; + /// Offset between a generated block block_number and its slot_number. + const SLOT_NUMBER_OFFSET: u64 = 100; + /// [CardanoTransactionsBuilder] constructor. pub fn new() -> Self { Self { @@ -102,15 +113,15 @@ impl CardanoTransactionsBuilder { /// Build the number of transactions requested. pub fn build_transactions(self, transactions_count: usize) -> Vec { - let mut transactions = Vec::new(); - let first_transaction_number = 100; - for tx_index in 0..transactions_count { - let block_number = self.block_number_from_transaction_index(tx_index); - let slot_number = SlotNumber(tx_index as u64 + first_transaction_number); - transactions.push(self.create_transaction(slot_number, block_number)) - } - - transactions + let block_count = transactions_count / self.max_transactions_per_block; + self.build_blocks_with_transactions( + block_count, + transactions_count, + FillUntil::TransactionCountReached, + ) + .into_iter() + .flat_map(|b| b.into_transactions()) + .collect() } /// Build a list of transactions to get the number of block range requested. @@ -118,35 +129,55 @@ impl CardanoTransactionsBuilder { self, block_ranges_count: usize, ) -> Vec { + let block_count = block_ranges_count * self.max_blocks_per_block_range; let nb_txs = block_ranges_count * self.max_blocks_per_block_range * self.max_transactions_per_block; - self.build_transactions(nb_txs) + self.build_blocks_with_transactions(block_count, nb_txs, FillUntil::TransactionCountReached) + .into_iter() + .flat_map(|b| b.into_transactions()) + .collect() } - fn block_number_from_transaction_index(&self, tx_index: usize) -> BlockNumber { - let max_transactions_per_block_range = - self.max_transactions_per_block * self.max_blocks_per_block_range; - let index_block_range = tx_index / max_transactions_per_block_range; - let block_index_global = tx_index as u64 / self.max_transactions_per_block as u64; - let block_index_in_block_range = - block_index_global % self.max_blocks_per_block_range as u64; - - index_block_range as u64 * BlockRange::LENGTH + block_index_in_block_range - } + fn build_blocks_with_transactions( + self, + block_count: usize, + transactions_count: usize, + fill_behavior: FillUntil, + ) -> Vec { + let mut blocks = Vec::with_capacity(block_count); + let mut transactions_numbers: Vec<_> = (0..transactions_count) + .map(|i| i + Self::FIRST_TRANSACTION_NUMBER) + .collect(); + let mut block_number_offset = 0; + let mut current_block_range_index = 0; + + while match fill_behavior { + FillUntil::BlockCountReached => blocks.len() < block_count, + FillUntil::TransactionCountReached => !transactions_numbers.is_empty(), + } { + let block_number = BlockNumber(current_block_range_index) + block_number_offset; + let transactions_hashes: Vec<_> = transactions_numbers + .drain(..self.max_transactions_per_block.min(transactions_numbers.len())) + .map(|i| format!("tx-hash-{block_number}-{i}")) + .collect(); + + let block = CardanoBlockWithTransactions { + block_hash: format!("block-hash-{block_number}"), + block_number, + slot_number: SlotNumber(*block_number + Self::SLOT_NUMBER_OFFSET), + transactions_hashes, + }; + blocks.push(block); + + current_block_range_index += 1; + if (current_block_range_index as usize) == self.max_blocks_per_block_range { + current_block_range_index = 0; + block_number_offset += BlockRange::LENGTH; + } + } - /// Create a transaction with a given index and block number. - fn create_transaction( - &self, - slot_number: SlotNumber, - block_number: BlockNumber, - ) -> CardanoTransaction { - CardanoTransaction::new( - format!("tx-hash-{block_number}-{slot_number}"), - block_number, - slot_number, - format!("block-hash-{block_number}"), - ) + blocks } } From 2f057d7d183c8c13d1c08a1c76a59e5b58b9eb89 Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:16:49 +0100 Subject: [PATCH 7/8] test(common): add `build_blocks` and `build_blocks_for_block_ranges` to `CardanoTransactionsBuilder` --- .../builder/cardano_transactions_builder.rs | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/mithril-common/src/test/builder/cardano_transactions_builder.rs b/mithril-common/src/test/builder/cardano_transactions_builder.rs index f95fb5c95da..30979d51a80 100644 --- a/mithril-common/src/test/builder/cardano_transactions_builder.rs +++ b/mithril-common/src/test/builder/cardano_transactions_builder.rs @@ -63,6 +63,51 @@ use crate::entities::{ /// txs /// ); /// ``` +/// +/// # Example 'build_blocks' +/// +/// ``` +/// use mithril_common::entities::{BlockNumber, CardanoBlockWithTransactions, SlotNumber}; +/// use mithril_common::test::builder::CardanoTransactionsBuilder; +/// +/// let blocks = CardanoTransactionsBuilder::new() +/// .max_transactions_per_block(3) +/// .blocks_per_block_range(2) +/// .build_blocks(3); +/// +/// assert_eq!(3, blocks.len()); +/// assert_eq!( +/// vec![ +/// CardanoBlockWithTransactions::new("block-hash-0", BlockNumber(0), SlotNumber(100), vec!["tx-hash-0-100","tx-hash-0-101","tx-hash-0-102"]), +/// CardanoBlockWithTransactions::new("block-hash-1", BlockNumber(1), SlotNumber(101), vec!["tx-hash-1-103","tx-hash-1-104","tx-hash-1-105"]), +/// CardanoBlockWithTransactions::new("block-hash-15", BlockNumber(15), SlotNumber(115), vec!["tx-hash-15-106","tx-hash-15-107","tx-hash-15-108"]), +/// ], +/// blocks +/// ); +/// ``` +/// +/// # Example 'build_blocks_for_block_ranges' +/// +/// ``` +/// use mithril_common::entities::{BlockNumber, CardanoBlockWithTransactions, SlotNumber}; +/// use mithril_common::test::builder::CardanoTransactionsBuilder; +/// +/// let blocks = CardanoTransactionsBuilder::new() +/// .max_transactions_per_block(3) +/// .blocks_per_block_range(2) +/// .build_blocks_for_block_ranges(2); +/// +/// assert_eq!(2 * 2, blocks.len()); +/// assert_eq!( +/// vec![ +/// CardanoBlockWithTransactions::new("block-hash-0", BlockNumber(0), SlotNumber(100), vec!["tx-hash-0-100","tx-hash-0-101","tx-hash-0-102"]), +/// CardanoBlockWithTransactions::new("block-hash-1", BlockNumber(1), SlotNumber(101), vec!["tx-hash-1-103","tx-hash-1-104","tx-hash-1-105"]), +/// CardanoBlockWithTransactions::new("block-hash-15", BlockNumber(15), SlotNumber(115), vec!["tx-hash-15-106","tx-hash-15-107","tx-hash-15-108"]), +/// CardanoBlockWithTransactions::new("block-hash-16", BlockNumber(16), SlotNumber(116), vec!["tx-hash-16-109","tx-hash-16-110","tx-hash-16-111"]), +/// ], +/// blocks +/// ); +/// ``` pub struct CardanoTransactionsBuilder { max_transactions_per_block: usize, max_blocks_per_block_range: usize, @@ -139,6 +184,30 @@ impl CardanoTransactionsBuilder { .collect() } + /// Build the number of blocks requested. + pub fn build_blocks(self, block_count: usize) -> Vec { + let transactions_count = block_count * self.max_transactions_per_block; + self.build_blocks_with_transactions( + block_count, + transactions_count, + FillUntil::BlockCountReached, + ) + } + + /// Build a list of blocks to get the number of block range requested. + pub fn build_blocks_for_block_ranges( + self, + block_ranges_count: usize, + ) -> Vec { + let block_count = block_ranges_count * self.max_blocks_per_block_range; + let transactions_count = block_count * self.max_transactions_per_block; + self.build_blocks_with_transactions( + block_count, + transactions_count, + FillUntil::BlockCountReached, + ) + } + fn build_blocks_with_transactions( self, block_count: usize, @@ -195,6 +264,19 @@ mod test { list.iter().cloned().map(extract_value).collect::>().len() } + fn count_distinct_nested_values(list: &[T], extract_values: &dyn Fn(T) -> U) -> usize + where + T: Clone, + R: Eq + std::hash::Hash, + U: IntoIterator, + { + list.iter() + .cloned() + .flat_map(extract_values) + .collect::>() + .len() + } + fn group_by<'a, T, R>(list: &'a [T], extract_value: &dyn Fn(T) -> R) -> HashMap> where T: Clone, @@ -363,4 +445,168 @@ mod test { assert_eq!(3 * 2, count_distinct_values(&txs, &|t| t.block_hash)); } } + + mod build_blocks { + use super::*; + + #[test] + fn return_given_number_of_blocks_with_distinct_values() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(2) + .build_blocks(3); + + assert_eq!(blocks.len(), 3); + + assert_eq!(3, count_distinct_values(&blocks, &|b| b.block_hash)); + assert_eq!(3, count_distinct_values(&blocks, &|b| b.block_number)); + assert_eq!(3, count_distinct_values(&blocks, &|b| b.slot_number)); + assert_eq!( + 6, + count_distinct_nested_values(&blocks, &|b| b.transactions_hashes) + ); + } + + #[test] + fn return_empty_blocks_when_ask_0_transactions_per_block() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(0) + .build_blocks(3); + + assert_eq!(blocks.len(), 3); + assert!(blocks.iter().map(|b| b.transactions_hashes.len()).all(|n| n == 0)); + } + + #[test] + fn build_one_block_with_multiples_transactions() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .build_blocks(1); + + assert_eq!( + blocks, + vec![CardanoBlockWithTransactions::new( + "block-hash-0", + BlockNumber(0), + SlotNumber(100), + vec![ + "tx-hash-0-100", + "tx-hash-0-101", + "tx-hash-0-102", + "tx-hash-0-103", + "tx-hash-0-104" + ], + ),] + ); + } + + #[test] + fn build_a_block_on_three_different_block_ranges() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(1) + .blocks_per_block_range(1) + .build_blocks(3); + + assert_eq!( + blocks, + vec![ + CardanoBlockWithTransactions::new( + "block-hash-0", + BlockNumber(0), + SlotNumber(100), + vec!["tx-hash-0-100"] + ), + CardanoBlockWithTransactions::new( + "block-hash-15", + BlockNumber(15), + SlotNumber(115), + vec!["tx-hash-15-101"] + ), + CardanoBlockWithTransactions::new( + "block-hash-30", + BlockNumber(30), + SlotNumber(130), + vec!["tx-hash-30-102"] + ), + ] + ); + } + + #[test] + fn return_no_more_transactions_in_a_same_block_than_number_per_block_requested() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(3) + .build_blocks(4); + + assert_eq!(blocks.len(), 4); + + assert_eq!( + 12, + count_distinct_nested_values(&blocks, &|b| b.transactions_hashes) + ); + assert_eq!(4, count_distinct_values(&blocks, &|t| t.block_number)); + assert_eq!(4, count_distinct_values(&blocks, &|t| t.block_hash)); + } + } + + mod build_blocks_for_block_ranges { + use super::*; + + #[test] + fn generate_blocks_for_one_block_range_return_one_block_by_default() { + let blocks = CardanoTransactionsBuilder::new().build_blocks_for_block_ranges(1); + assert_eq!(blocks.len(), 1); + } + + #[test] + fn build_blocks_for_block_ranges_return_the_number_of_block_ranges_requested() { + let blocks = CardanoTransactionsBuilder::new().build_blocks_for_block_ranges(3); + + assert_eq!(blocks.len(), 3); + assert_eq!( + 3, + count_distinct_values(&blocks, &|b| BlockRange::start(b.block_number)) + ); + } + + #[test] + fn build_blocks_for_block_ranges_return_blocks_with_many_transactions_when_requested() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .build_blocks_for_block_ranges(3); + + assert_eq!(blocks.len(), 3); + assert_eq!( + 3 * 5, + count_distinct_nested_values(&blocks, &|b| b.transactions_hashes) + ); + + assert_eq!( + 3, + count_distinct_values(&blocks, &|b| BlockRange::start(b.block_number)) + ); + assert_eq!(3, count_distinct_values(&blocks, &|b| b.block_number)); + assert_eq!(3, count_distinct_values(&blocks, &|b| b.block_hash)); + } + + #[test] + fn build_blocks_for_block_ranges_with_many_blocks_per_block_ranges() { + let blocks = CardanoTransactionsBuilder::new() + .max_transactions_per_block(5) + .blocks_per_block_range(2) + .build_blocks_for_block_ranges(3); + + assert_eq!(blocks.len(), 3 * 2); + assert_eq!( + 3 * 2 * 5, + count_distinct_nested_values(&blocks, &|b| b.transactions_hashes) + ); + + assert_eq!( + 3, + count_distinct_values(&blocks, &|t| BlockRange::start(t.block_number)) + ); + assert_eq!(3 * 2, count_distinct_values(&blocks, &|b| b.block_number)); + assert_eq!(3 * 2, count_distinct_values(&blocks, &|b| b.block_hash)); + } + } } From f41e2707c4e31be01b7efbebdbadb0368d10f1b0 Mon Sep 17 00:00:00 2001 From: DJO <790521+Alenar@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:52:39 +0100 Subject: [PATCH 8/8] chore: upgrade crate versions * mithril-cardano-node-chain from `0.1.23` to `0.1.24` * mithril-aggregator from `0.8.32` to `0.8.33` * mithril-common from `0.6.54` to `0.6.55` --- Cargo.lock | 6 +++--- internal/cardano-node/mithril-cardano-node-chain/Cargo.toml | 2 +- mithril-aggregator/Cargo.toml | 2 +- mithril-common/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90b83d27a4c..9445e262ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3932,7 +3932,7 @@ dependencies = [ [[package]] name = "mithril-aggregator" -version = "0.8.32" +version = "0.8.33" dependencies = [ "anyhow", "async-trait", @@ -4078,7 +4078,7 @@ dependencies = [ [[package]] name = "mithril-cardano-node-chain" -version = "0.1.23" +version = "0.1.24" dependencies = [ "anyhow", "async-trait", @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.6.54" +version = "0.6.55" dependencies = [ "anyhow", "async-trait", diff --git a/internal/cardano-node/mithril-cardano-node-chain/Cargo.toml b/internal/cardano-node/mithril-cardano-node-chain/Cargo.toml index 541eaf5c197..0bfb652e60f 100644 --- a/internal/cardano-node/mithril-cardano-node-chain/Cargo.toml +++ b/internal/cardano-node/mithril-cardano-node-chain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-cardano-node-chain" -version = "0.1.23" +version = "0.1.24" authors.workspace = true documentation.workspace = true edition.workspace = true diff --git a/mithril-aggregator/Cargo.toml b/mithril-aggregator/Cargo.toml index 0f0978fd882..11784945c1a 100644 --- a/mithril-aggregator/Cargo.toml +++ b/mithril-aggregator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-aggregator" -version = "0.8.32" +version = "0.8.33" description = "A Mithril Aggregator server" authors = { workspace = true } edition = { workspace = true } diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 00701609c10..b80b2a70d5e 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-common" -version = "0.6.54" +version = "0.6.55" description = "Common types, interfaces, and utilities for Mithril nodes." authors = { workspace = true } edition = { workspace = true }