mithril_common/test_utils/
cardano_transactions_builder.rs

1use crate::entities::{BlockNumber, BlockRange, CardanoTransaction, SlotNumber};
2
3/// Builder to easily build transactions with consistent values.
4///
5/// Note: the names generated for the transaction hashes and block hashes are not meaningful.
6///
7/// # Example 'build_transactions'
8///
9/// ```
10///     use mithril_common::entities::{BlockNumber, CardanoTransaction, SlotNumber};
11///     use mithril_common::test_utils::CardanoTransactionsBuilder;
12///
13///     let txs = CardanoTransactionsBuilder::new()
14///         .max_transactions_per_block(3)
15///         .blocks_per_block_range(2)
16///         .build_transactions(8);
17///
18///     assert_eq!(8, txs.len());
19///     assert_eq!(
20///         vec![
21///             CardanoTransaction::new("tx-hash-0-100", BlockNumber(0), SlotNumber(100), "block-hash-0"),
22///             CardanoTransaction::new("tx-hash-0-101", BlockNumber(0), SlotNumber(101), "block-hash-0"),
23///             CardanoTransaction::new("tx-hash-0-102", BlockNumber(0), SlotNumber(102), "block-hash-0"),
24///             CardanoTransaction::new("tx-hash-1-103", BlockNumber(1), SlotNumber(103), "block-hash-1"),
25///             CardanoTransaction::new("tx-hash-1-104", BlockNumber(1), SlotNumber(104), "block-hash-1"),
26///             CardanoTransaction::new("tx-hash-1-105", BlockNumber(1), SlotNumber(105), "block-hash-1"),
27///             CardanoTransaction::new("tx-hash-15-106", BlockNumber(15), SlotNumber(106), "block-hash-15"),
28///             CardanoTransaction::new("tx-hash-15-107", BlockNumber(15), SlotNumber(107), "block-hash-15")
29///         ],
30///         txs
31///     );
32/// ```
33///
34/// # Example 'build_block_ranges'
35///
36/// ```
37///     use mithril_common::entities::{BlockNumber, CardanoTransaction, SlotNumber};
38///     use mithril_common::test_utils::CardanoTransactionsBuilder;
39///
40///     let txs = CardanoTransactionsBuilder::new()
41///         .max_transactions_per_block(3)
42///         .blocks_per_block_range(2)
43///         .build_block_ranges(2);
44///
45///     assert_eq!(3 * 2 * 2, txs.len());
46///     assert_eq!(
47///         vec![
48///             CardanoTransaction::new("tx-hash-0-100", BlockNumber(0), SlotNumber(100), "block-hash-0"),
49///             CardanoTransaction::new("tx-hash-0-101", BlockNumber(0), SlotNumber(101), "block-hash-0"),
50///             CardanoTransaction::new("tx-hash-0-102", BlockNumber(0), SlotNumber(102), "block-hash-0"),
51///             CardanoTransaction::new("tx-hash-1-103", BlockNumber(1), SlotNumber(103), "block-hash-1"),
52///             CardanoTransaction::new("tx-hash-1-104", BlockNumber(1), SlotNumber(104), "block-hash-1"),
53///             CardanoTransaction::new("tx-hash-1-105", BlockNumber(1), SlotNumber(105), "block-hash-1"),
54///             CardanoTransaction::new("tx-hash-15-106", BlockNumber(15), SlotNumber(106), "block-hash-15"),
55///             CardanoTransaction::new("tx-hash-15-107", BlockNumber(15), SlotNumber(107), "block-hash-15"),
56///             CardanoTransaction::new("tx-hash-15-108", BlockNumber(15), SlotNumber(108), "block-hash-15"),
57///             CardanoTransaction::new("tx-hash-16-109", BlockNumber(16), SlotNumber(109), "block-hash-16"),
58///             CardanoTransaction::new("tx-hash-16-110", BlockNumber(16), SlotNumber(110), "block-hash-16"),
59///             CardanoTransaction::new("tx-hash-16-111", BlockNumber(16), SlotNumber(111), "block-hash-16"),
60///         ],
61///         txs
62///     );
63/// ```
64pub struct CardanoTransactionsBuilder {
65    max_transactions_per_block: usize,
66    max_blocks_per_block_range: usize,
67}
68
69impl Default for CardanoTransactionsBuilder {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl CardanoTransactionsBuilder {
76    /// [CardanoTransactionsBuilder] constructor.
77    pub fn new() -> Self {
78        Self {
79            max_transactions_per_block: 1,
80            max_blocks_per_block_range: 1,
81        }
82    }
83
84    /// Define how many transactions we generate in each block.
85    pub fn max_transactions_per_block(mut self, transactions_per_block: usize) -> Self {
86        self.max_transactions_per_block = transactions_per_block;
87        self
88    }
89
90    /// Define how many blocks we generate in each block_range.
91    /// If we set too many blocks for a block_range, this function panic.
92    pub fn blocks_per_block_range(mut self, blocks_per_block_range: usize) -> Self {
93        if blocks_per_block_range > *BlockRange::LENGTH as usize {
94            panic!(
95                "blocks_per_block_range should be less than {}",
96                BlockRange::LENGTH
97            );
98        }
99        self.max_blocks_per_block_range = blocks_per_block_range;
100        self
101    }
102
103    /// Build the number of transactions requested.
104    pub fn build_transactions(self, transactions_count: usize) -> Vec<CardanoTransaction> {
105        let mut transactions = Vec::new();
106        let first_transaction_number = 100;
107        for tx_index in 0..transactions_count {
108            let block_number = self.block_number_from_transaction_index(tx_index);
109            let slot_number = SlotNumber(tx_index as u64 + first_transaction_number);
110            transactions.push(self.create_transaction(slot_number, block_number))
111        }
112
113        transactions
114    }
115
116    /// Build a list of transactions to get the number of block range requested.
117    pub fn build_block_ranges(self, block_ranges_count: usize) -> Vec<CardanoTransaction> {
118        let nb_txs =
119            block_ranges_count * self.max_blocks_per_block_range * self.max_transactions_per_block;
120
121        self.build_transactions(nb_txs)
122    }
123
124    fn block_number_from_transaction_index(&self, tx_index: usize) -> BlockNumber {
125        let max_transactions_per_block_range =
126            self.max_transactions_per_block * self.max_blocks_per_block_range;
127        let index_block_range = tx_index / max_transactions_per_block_range;
128        let block_index_global = tx_index as u64 / self.max_transactions_per_block as u64;
129        let block_index_in_block_range =
130            block_index_global % self.max_blocks_per_block_range as u64;
131
132        index_block_range as u64 * BlockRange::LENGTH + block_index_in_block_range
133    }
134
135    /// Create a transaction with a given index and block number.
136    fn create_transaction(
137        &self,
138        slot_number: SlotNumber,
139        block_number: BlockNumber,
140    ) -> CardanoTransaction {
141        CardanoTransaction::new(
142            format!("tx-hash-{}-{}", block_number, slot_number),
143            block_number,
144            slot_number,
145            format!("block-hash-{block_number}"),
146        )
147    }
148}
149
150#[cfg(test)]
151mod test {
152    use std::collections::{HashMap, HashSet};
153
154    use super::*;
155
156    fn count_distinct_values<T, R>(list: &[T], extract_value: &dyn Fn(&T) -> R) -> usize
157    where
158        R: Eq + std::hash::Hash,
159    {
160        list.iter().map(extract_value).collect::<HashSet<R>>().len()
161    }
162
163    fn group_by<'a, T, R>(list: &'a [T], extract_value: &dyn Fn(&T) -> R) -> HashMap<R, Vec<&'a T>>
164    where
165        R: Eq + std::hash::Hash,
166    {
167        let mut grouped_by_block = HashMap::new();
168        for t in list {
169            grouped_by_block
170                .entry(extract_value(t))
171                .or_insert(Vec::new())
172                .push(t);
173        }
174        grouped_by_block
175    }
176
177    #[test]
178    fn return_given_number_of_transactions_with_distinct_values() {
179        let txs = CardanoTransactionsBuilder::new().build_transactions(3);
180
181        assert_eq!(txs.len(), 3);
182
183        assert_eq!(
184            3,
185            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
186        );
187        assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number));
188        assert_eq!(3, count_distinct_values(&txs, &|t| t.slot_number));
189        assert_eq!(3, count_distinct_values(&txs, &|t| t.block_hash.clone()));
190    }
191
192    #[test]
193    fn return_all_transactions_in_same_block_when_ask_less_transactions_than_transactions_per_block(
194    ) {
195        let txs = CardanoTransactionsBuilder::new()
196            .max_transactions_per_block(10)
197            .build_transactions(3);
198
199        assert_eq!(txs.len(), 3);
200
201        assert_eq!(
202            3,
203            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
204        );
205        assert_eq!(1, count_distinct_values(&txs, &|t| t.block_number));
206        assert_eq!(1, count_distinct_values(&txs, &|t| t.block_hash.clone()));
207    }
208
209    #[test]
210    fn return_no_more_transactions_in_a_same_block_than_number_per_block_requested() {
211        let txs = CardanoTransactionsBuilder::new()
212            .max_transactions_per_block(3)
213            .build_transactions(12);
214
215        assert_eq!(txs.len(), 12);
216
217        assert_eq!(
218            12,
219            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
220        );
221        assert_eq!(4, count_distinct_values(&txs, &|t| t.block_number));
222        assert_eq!(4, count_distinct_values(&txs, &|t| t.block_hash.clone()));
223    }
224
225    #[test]
226    fn only_the_last_block_is_not_full_when_we_can_not_fill_all_blocks() {
227        let txs = CardanoTransactionsBuilder::new()
228            .max_transactions_per_block(5)
229            .build_transactions(12);
230
231        assert_eq!(txs.len(), 12);
232
233        assert_eq!(
234            12,
235            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
236        );
237        assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number));
238
239        let grouped_by_block = group_by(&txs, &|t| t.block_number);
240        let mut txs_per_block: Vec<_> = grouped_by_block.values().map(|v| v.len()).collect();
241        txs_per_block.sort();
242        assert_eq!(vec![2, 5, 5], txs_per_block);
243    }
244
245    #[test]
246    fn generate_one_block_range_return_one_transaction_by_default() {
247        let txs = CardanoTransactionsBuilder::new().build_block_ranges(1);
248        assert_eq!(txs.len(), 1);
249    }
250
251    #[test]
252    fn build_block_ranges_return_the_number_of_block_ranges_requested() {
253        let block_ranges = 3;
254        let txs = CardanoTransactionsBuilder::new().build_block_ranges(block_ranges);
255
256        assert_eq!(txs.len(), 3);
257
258        assert_eq!(
259            3,
260            count_distinct_values(&txs, &|t| BlockRange::start(t.block_number))
261        );
262    }
263
264    #[test]
265    fn build_block_ranges_return_many_transactions_per_block_when_requested() {
266        let txs = CardanoTransactionsBuilder::new()
267            .max_transactions_per_block(5)
268            .build_block_ranges(3);
269
270        assert_eq!(txs.len(), 3 * 5);
271
272        assert_eq!(
273            3 * 5,
274            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
275        );
276
277        assert_eq!(
278            3,
279            count_distinct_values(&txs, &|t| BlockRange::start(t.block_number))
280        );
281        assert_eq!(3, count_distinct_values(&txs, &|t| t.block_number));
282        assert_eq!(3, count_distinct_values(&txs, &|t| t.block_hash.clone()));
283    }
284
285    #[test]
286    fn build_block_ranges_with_many_blocks_per_block_ranges() {
287        let txs = CardanoTransactionsBuilder::new()
288            .max_transactions_per_block(5)
289            .blocks_per_block_range(2)
290            .build_block_ranges(3);
291
292        assert_eq!(txs.len(), 3 * 2 * 5);
293
294        assert_eq!(
295            3 * 2 * 5,
296            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
297        );
298
299        assert_eq!(
300            3,
301            count_distinct_values(&txs, &|t| BlockRange::start(t.block_number))
302        );
303        assert_eq!(3 * 2, count_distinct_values(&txs, &|t| t.block_number));
304        assert_eq!(
305            3 * 2,
306            count_distinct_values(&txs, &|t| t.block_hash.clone())
307        );
308    }
309
310    #[test]
311    fn build_transactions_with_many_blocks_per_block_ranges() {
312        let txs = CardanoTransactionsBuilder::new()
313            .max_transactions_per_block(5)
314            .blocks_per_block_range(2)
315            .build_transactions(18);
316
317        // block range 1 - block 0  - 1, 2, 3, 4, 5
318        // block range 1 - block 1  - 6, 7, 8, 7, 10
319        // block range 2 - block 15 - 11, 12, 13, 14, 15
320        // block range 2 - block 16 - 16, 17, 18
321
322        assert_eq!(txs.len(), 18);
323
324        assert_eq!(
325            18,
326            count_distinct_values(&txs, &|t| t.transaction_hash.clone())
327        );
328
329        assert_eq!(
330            2,
331            count_distinct_values(&txs, &|t| BlockRange::start(t.block_number))
332        );
333        assert_eq!(4, count_distinct_values(&txs, &|t| t.block_number));
334        assert_eq!(4, count_distinct_values(&txs, &|t| t.block_hash.clone()));
335    }
336
337    #[test]
338    #[should_panic]
339    fn should_panic_when_too_many_blocks_per_block_range() {
340        CardanoTransactionsBuilder::new().blocks_per_block_range(*BlockRange::LENGTH as usize + 1);
341    }
342}