mithril_common/entities/
signed_entity_config.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::StdResult;
6use crate::entities::{
7    BlockNumber, BlockRange, CardanoDbBeacon, SignedEntityType, SignedEntityTypeDiscriminants,
8    TimePoint,
9};
10
11/// Convert [TimePoint] to [SignedEntityType] and list allowed signed entity types and
12/// discriminants.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct SignedEntityConfig {
15    /// List of discriminants that the node is allowed to sign
16    pub allowed_discriminants: BTreeSet<SignedEntityTypeDiscriminants>,
17    /// Cardano transactions signing configuration
18    pub cardano_transactions_signing_config: Option<CardanoTransactionsSigningConfig>,
19    /// Cardano blocks and transactions signing configuration
20    pub cardano_blocks_transactions_signing_config: Option<CardanoBlocksTransactionsSigningConfig>,
21}
22
23impl SignedEntityConfig {
24    /// Default allowed discriminants
25    ///
26    /// Appended to the allowed discriminants in the configuration.
27    pub const DEFAULT_ALLOWED_DISCRIMINANTS: [SignedEntityTypeDiscriminants; 1] =
28        [SignedEntityTypeDiscriminants::MithrilStakeDistribution];
29
30    /// Append to the given list of allowed signed entity types discriminants the [Self::DEFAULT_ALLOWED_DISCRIMINANTS]
31    /// if not already present.
32    pub fn append_allowed_signed_entity_types_discriminants(
33        discriminants: BTreeSet<SignedEntityTypeDiscriminants>,
34    ) -> BTreeSet<SignedEntityTypeDiscriminants> {
35        let mut discriminants = discriminants;
36        discriminants.append(&mut BTreeSet::from(Self::DEFAULT_ALLOWED_DISCRIMINANTS));
37        discriminants
38    }
39
40    /// Create the deduplicated list of allowed signed entity types discriminants.
41    ///
42    /// The list is the aggregation of [Self::DEFAULT_ALLOWED_DISCRIMINANTS] and
43    /// `allowed_discriminants`.
44    pub fn list_allowed_signed_entity_types_discriminants(
45        &self,
46    ) -> BTreeSet<SignedEntityTypeDiscriminants> {
47        let discriminants = self.allowed_discriminants.clone();
48        Self::append_allowed_signed_entity_types_discriminants(discriminants)
49    }
50
51    /// Convert this time point to a signed entity type based on the given discriminant.
52    pub fn time_point_to_signed_entity<D: Into<SignedEntityTypeDiscriminants>>(
53        &self,
54        discriminant: D,
55        time_point: &TimePoint,
56    ) -> StdResult<SignedEntityType> {
57        let signed_entity_type = match discriminant.into() {
58            SignedEntityTypeDiscriminants::MithrilStakeDistribution => {
59                SignedEntityType::MithrilStakeDistribution(time_point.epoch)
60            }
61            SignedEntityTypeDiscriminants::CardanoStakeDistribution => {
62                SignedEntityType::CardanoStakeDistribution(time_point.epoch.previous()?)
63            }
64            SignedEntityTypeDiscriminants::CardanoImmutableFilesFull => {
65                SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(
66                    *time_point.epoch,
67                    time_point.immutable_file_number,
68                ))
69            }
70            SignedEntityTypeDiscriminants::CardanoTransactions => {
71                match &self.cardano_transactions_signing_config {
72                    Some(config) => SignedEntityType::CardanoTransactions(
73                        time_point.epoch,
74                        config
75                            .compute_block_number_to_be_signed(time_point.chain_point.block_number),
76                    ),
77                    None => {
78                        anyhow::bail!(
79                            "Can't derive a `CardanoTransactions` signed entity type from a time point without a `CardanoTransactionsSigningConfig`"
80                        )
81                    }
82                }
83            }
84            SignedEntityTypeDiscriminants::CardanoBlocksTransactions => {
85                match &self.cardano_blocks_transactions_signing_config {
86                    Some(config) => SignedEntityType::CardanoBlocksTransactions(
87                        time_point.epoch,
88                        config
89                            .compute_block_number_to_be_signed(time_point.chain_point.block_number),
90                    ),
91                    None => {
92                        anyhow::bail!(
93                            "Can't derive a `CardanoBlocksTransactions` signed entity type from a time point without a `CardanoBlocksTransactionsSigningConfig`"
94                        )
95                    }
96                }
97            }
98            SignedEntityTypeDiscriminants::CardanoDatabase => SignedEntityType::CardanoDatabase(
99                CardanoDbBeacon::new(*time_point.epoch, time_point.immutable_file_number),
100            ),
101        };
102
103        Ok(signed_entity_type)
104    }
105
106    /// Create the deduplicated list of allowed signed entity types discriminants.
107    ///
108    /// The list is the aggregation of [Self::DEFAULT_ALLOWED_DISCRIMINANTS] and
109    /// `allowed_discriminants`.
110    pub fn list_allowed_signed_entity_types(
111        &self,
112        time_point: &TimePoint,
113    ) -> StdResult<Vec<SignedEntityType>> {
114        self.list_allowed_signed_entity_types_discriminants()
115            .into_iter()
116            .map(|discriminant| self.time_point_to_signed_entity(discriminant, time_point))
117            .collect()
118    }
119}
120
121/// Configuration for the signing of Cardano transactions
122///
123/// Allow to compute the block number to be signed based on the chain tip block number.
124///
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub struct CardanoTransactionsSigningConfig {
127    /// Number of blocks to discard from the tip of the chain when importing transactions.
128    pub security_parameter: BlockNumber,
129
130    /// The number of blocks between signature of the transactions.
131    ///
132    /// *Note: The step is adjusted to be a multiple of the block range length in order
133    /// to guarantee that the block number signed in a certificate is effectively signed.*
134    pub step: BlockNumber,
135}
136
137impl CardanoTransactionsSigningConfig {
138    /// Compute the block number to be signed based on the chain tip block number.
139    ///
140    pub fn compute_block_number_to_be_signed(&self, block_number: BlockNumber) -> BlockNumber {
141        compute_block_number_to_be_signed(block_number, self.security_parameter, self.step)
142    }
143}
144
145/// Configuration for the signing of Cardano blocks and transactions
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct CardanoBlocksTransactionsSigningConfig {
148    /// Number of blocks to discard from the tip of the chain when importing blocks and transactions.
149    pub security_parameter: BlockNumber,
150
151    /// The number of blocks between signature of the blocks and transactions.
152    ///
153    /// *Note: The step is adjusted to be a multiple of the block range length in order
154    /// to guarantee that the block number signed in a certificate is effectively signed.*
155    pub step: BlockNumber,
156}
157
158impl CardanoBlocksTransactionsSigningConfig {
159    /// Compute the block number to be signed based on the chain tip block number.
160    ///
161    pub fn compute_block_number_to_be_signed(&self, block_number: BlockNumber) -> BlockNumber {
162        compute_block_number_to_be_signed(block_number, self.security_parameter, self.step)
163    }
164}
165
166/// Compute the block number to be signed based on the chain tip block number.
167///
168/// The latest block number to be signed is the highest multiple of the step less or equal than the
169/// block number minus the security parameter.
170///
171/// The formula is as follows:
172///
173/// `block_number = ⌊(tip.block_number - security_parameter) / step⌋ × step - 1`
174///
175/// where `⌊x⌋` is the floor function which rounds to the greatest integer less than or equal to `x`.
176///
177/// *Notes:*
178/// * *The step is adjusted to be a multiple of the block range length in order
179///   to guarantee that the block number signed in a certificate is effectively signed.*
180/// * *1 is subtracted to the result because block range end is exclusive (ie: a BlockRange over
181///   `30..45` finish at 44 included, 45 is included in the next block range).*
182fn compute_block_number_to_be_signed(
183    block_number: BlockNumber,
184    security_parameter: BlockNumber,
185    step: BlockNumber,
186) -> BlockNumber {
187    // TODO: See if we can remove this adjustment by including a "partial" block range in
188    // the signed data.
189    let adjusted_step = BlockRange::from_block_number(step).start;
190    // We can't have a step lower than the block range length.
191    let adjusted_step = std::cmp::max(adjusted_step, BlockRange::LENGTH);
192
193    let block_number_to_be_signed =
194        (block_number - security_parameter) / adjusted_step * adjusted_step;
195    block_number_to_be_signed - 1
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::entities::{
201        CardanoDbBeacon, ChainPoint, Epoch, SignedEntityType, SlotNumber, TimePoint,
202    };
203    use crate::test::{double::Dummy, double::fake_data};
204
205    use super::*;
206
207    #[test]
208    fn given_discriminant_convert_to_signed_entity() {
209        let time_point = TimePoint {
210            epoch: Epoch(1),
211            immutable_file_number: 5,
212            chain_point: ChainPoint {
213                slot_number: SlotNumber(73),
214                block_number: BlockNumber(20),
215                block_hash: "block_hash-20".to_string(),
216            },
217        };
218        let config = SignedEntityConfig {
219            allowed_discriminants: SignedEntityTypeDiscriminants::all(),
220            cardano_transactions_signing_config: Some(CardanoTransactionsSigningConfig {
221                security_parameter: BlockNumber(0),
222                step: BlockNumber(15),
223            }),
224            cardano_blocks_transactions_signing_config: Some(
225                CardanoBlocksTransactionsSigningConfig {
226                    security_parameter: BlockNumber(1),
227                    step: BlockNumber(30),
228                },
229            ),
230        };
231
232        assert_eq!(
233            SignedEntityType::MithrilStakeDistribution(Epoch(1)),
234            config
235                .time_point_to_signed_entity(
236                    SignedEntityTypeDiscriminants::MithrilStakeDistribution,
237                    &time_point
238                )
239                .unwrap()
240        );
241
242        // An offset of -1 is applied to the epoch of the time point to get the epoch of the stake distribution to be signed
243        assert_eq!(
244            SignedEntityType::CardanoStakeDistribution(Epoch(0)),
245            config
246                .time_point_to_signed_entity(
247                    SignedEntityTypeDiscriminants::CardanoStakeDistribution,
248                    &time_point
249                )
250                .unwrap()
251        );
252
253        assert_eq!(
254            SignedEntityType::CardanoImmutableFilesFull(CardanoDbBeacon::new(1, 5)),
255            config
256                .time_point_to_signed_entity(
257                    SignedEntityTypeDiscriminants::CardanoImmutableFilesFull,
258                    &time_point
259                )
260                .unwrap()
261        );
262
263        // The block number to be signed is 14 because the step is 15, the block number is 20, and
264        // the security parameter is 0.
265        // This is further tested in the "computing_block_number_to_be_signed" tests below.
266        assert_eq!(
267            SignedEntityType::CardanoTransactions(Epoch(1), BlockNumber(14)),
268            config
269                .time_point_to_signed_entity(
270                    SignedEntityTypeDiscriminants::CardanoTransactions,
271                    &time_point
272                )
273                .unwrap()
274        );
275
276        assert_eq!(
277            SignedEntityType::CardanoDatabase(CardanoDbBeacon::new(1, 5)),
278            config
279                .time_point_to_signed_entity(
280                    SignedEntityTypeDiscriminants::CardanoDatabase,
281                    &time_point
282                )
283                .unwrap()
284        );
285    }
286
287    #[test]
288    fn can_not_convert_time_point_to_cardano_transaction_without_the_associated_config() {
289        let time_point = TimePoint {
290            epoch: Epoch(1),
291            immutable_file_number: 5,
292            chain_point: ChainPoint {
293                slot_number: SlotNumber(73),
294                block_number: BlockNumber(20),
295                block_hash: "block_hash-20".to_string(),
296            },
297        };
298        let config = SignedEntityConfig {
299            allowed_discriminants: SignedEntityTypeDiscriminants::all(),
300            cardano_transactions_signing_config: None,
301            cardano_blocks_transactions_signing_config: None,
302        };
303
304        let error = config
305            .time_point_to_signed_entity(
306                SignedEntityTypeDiscriminants::CardanoTransactions,
307                &time_point,
308            )
309            .unwrap_err();
310
311        let expected_error = "Can't derive a `CardanoTransactions` signed entity type from a time point without a `CardanoTransactionsSigningConfig`";
312        assert!(
313            error.to_string().contains(expected_error),
314            "Error message: {error:?}\nshould contains: {expected_error}\n"
315        );
316    }
317
318    #[test]
319    fn computing_block_number_to_be_signed() {
320        // **block_number = ((tip.block_number - k') / n) × n**
321        let block_number = BlockNumber(105);
322        let security_parameter = BlockNumber(0);
323        let step = BlockNumber(15);
324        assert_eq!(
325            compute_block_number_to_be_signed(block_number, security_parameter, step),
326            104
327        );
328
329        let block_number = BlockNumber(100);
330        let security_parameter = BlockNumber(5);
331        let step = BlockNumber(15);
332        assert_eq!(
333            compute_block_number_to_be_signed(block_number, security_parameter, step),
334            89
335        );
336
337        let block_number = BlockNumber(100);
338        let security_parameter = BlockNumber(85);
339        let step = BlockNumber(15);
340        assert_eq!(
341            compute_block_number_to_be_signed(block_number, security_parameter, step),
342            14
343        );
344
345        let block_number = BlockNumber(29);
346        let security_parameter = BlockNumber(0);
347        let step = BlockNumber(30);
348        assert_eq!(
349            compute_block_number_to_be_signed(block_number, security_parameter, step),
350            0
351        );
352    }
353
354    #[test]
355    fn computing_block_number_to_be_signed_should_not_overlow_on_security_parameter() {
356        let block_number = BlockNumber(50);
357        let security_parameter = BlockNumber(100);
358        let step = BlockNumber(30);
359        assert_eq!(
360            compute_block_number_to_be_signed(block_number, security_parameter, step),
361            0
362        );
363    }
364
365    #[test]
366    fn computing_block_number_to_be_signed_round_step_to_a_block_range_start() {
367        let block_number = BlockRange::LENGTH * 5 + 1;
368        let security_parameter = BlockNumber(0);
369        let step = BlockRange::LENGTH * 2 - 1;
370        assert_eq!(
371            compute_block_number_to_be_signed(block_number, security_parameter, step),
372            BlockRange::LENGTH * 5 - 1
373        );
374
375        let block_number = BlockRange::LENGTH * 5 + 1;
376        let security_parameter = BlockNumber(0);
377        let step = BlockRange::LENGTH * 2 + 1;
378        assert_eq!(
379            compute_block_number_to_be_signed(block_number, security_parameter, step),
380            BlockRange::LENGTH * 4 - 1
381        );
382
383        // Adjusted step is always at least BLOCK_RANGE_LENGTH.
384        let block_number = BlockRange::LENGTH * 10 - 1;
385        let security_parameter = BlockNumber(0);
386        let step = BlockRange::LENGTH - 1;
387        assert_eq!(
388            compute_block_number_to_be_signed(block_number, security_parameter, step),
389            BlockRange::LENGTH * 9 - 1
390        );
391
392        // Adjusted step is always at least BLOCK_RANGE_LENGTH.
393        let block_number = BlockRange::LENGTH - 1;
394        let security_parameter = BlockNumber(0);
395        let step = BlockRange::LENGTH - 1;
396        assert_eq!(
397            compute_block_number_to_be_signed(block_number, security_parameter, step),
398            0
399        );
400    }
401
402    #[test]
403    fn test_list_allowed_signed_entity_types_discriminant_without_specific_configuration() {
404        let config = SignedEntityConfig {
405            allowed_discriminants: BTreeSet::new(),
406            ..SignedEntityConfig::dummy()
407        };
408
409        let discriminants = config.list_allowed_signed_entity_types_discriminants();
410
411        assert_eq!(
412            BTreeSet::from(SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS),
413            discriminants
414        );
415    }
416
417    #[test]
418    fn test_list_allowed_signed_entity_types_discriminant_should_not_duplicate_a_signed_entity_discriminant_type_already_in_default_ones()
419     {
420        let config = SignedEntityConfig {
421            allowed_discriminants: BTreeSet::from([
422                SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS[0],
423            ]),
424            ..SignedEntityConfig::dummy()
425        };
426
427        let discriminants = config.list_allowed_signed_entity_types_discriminants();
428
429        assert_eq!(
430            BTreeSet::from(SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS),
431            discriminants
432        );
433    }
434
435    #[test]
436    fn test_list_allowed_signed_entity_types_discriminants_should_add_configured_discriminants() {
437        let config = SignedEntityConfig {
438            allowed_discriminants: BTreeSet::from([
439                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
440                SignedEntityTypeDiscriminants::CardanoTransactions,
441                SignedEntityTypeDiscriminants::CardanoDatabase,
442            ]),
443            ..SignedEntityConfig::dummy()
444        };
445
446        let discriminants = config.list_allowed_signed_entity_types_discriminants();
447
448        assert_eq!(
449            BTreeSet::from_iter(
450                [
451                    SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS.as_slice(),
452                    [
453                        SignedEntityTypeDiscriminants::CardanoStakeDistribution,
454                        SignedEntityTypeDiscriminants::CardanoTransactions,
455                        SignedEntityTypeDiscriminants::CardanoDatabase
456                    ]
457                    .as_slice()
458                ]
459                .concat()
460            ),
461            discriminants
462        );
463    }
464
465    #[test]
466    fn test_list_allowed_signed_entity_types_discriminants_with_multiple_identical_signed_entity_types_in_configuration_should_not_be_added_several_times()
467     {
468        let config = SignedEntityConfig {
469            allowed_discriminants: BTreeSet::from([
470                SignedEntityTypeDiscriminants::CardanoTransactions,
471                SignedEntityTypeDiscriminants::CardanoTransactions,
472                SignedEntityTypeDiscriminants::CardanoTransactions,
473            ]),
474            ..SignedEntityConfig::dummy()
475        };
476
477        let discriminants = config.list_allowed_signed_entity_types_discriminants();
478
479        assert_eq!(
480            BTreeSet::from_iter(
481                [
482                    SignedEntityConfig::DEFAULT_ALLOWED_DISCRIMINANTS.as_slice(),
483                    [SignedEntityTypeDiscriminants::CardanoTransactions].as_slice()
484                ]
485                .concat()
486            ),
487            discriminants
488        );
489    }
490
491    #[test]
492    fn test_list_allowed_signed_entity_types_with_specific_configuration() {
493        let beacon = fake_data::beacon();
494        let chain_point = ChainPoint {
495            block_number: BlockNumber(45),
496            ..ChainPoint::dummy()
497        };
498        let time_point = TimePoint::new(
499            *beacon.epoch,
500            beacon.immutable_file_number,
501            chain_point.clone(),
502        );
503        let config = SignedEntityConfig {
504            allowed_discriminants: BTreeSet::from([
505                SignedEntityTypeDiscriminants::CardanoStakeDistribution,
506                SignedEntityTypeDiscriminants::CardanoTransactions,
507                SignedEntityTypeDiscriminants::CardanoBlocksTransactions,
508            ]),
509            cardano_transactions_signing_config: Some(CardanoTransactionsSigningConfig {
510                security_parameter: BlockNumber(0),
511                step: BlockNumber(15),
512            }),
513            cardano_blocks_transactions_signing_config: Some(
514                CardanoBlocksTransactionsSigningConfig {
515                    security_parameter: BlockNumber(0),
516                    step: BlockNumber(15),
517                },
518            ),
519        };
520
521        let signed_entity_types = config.list_allowed_signed_entity_types(&time_point).unwrap();
522
523        assert_eq!(
524            vec![
525                SignedEntityType::MithrilStakeDistribution(beacon.epoch),
526                SignedEntityType::CardanoStakeDistribution(beacon.epoch - 1),
527                SignedEntityType::CardanoTransactions(beacon.epoch, chain_point.block_number - 1),
528                SignedEntityType::CardanoBlocksTransactions(
529                    beacon.epoch,
530                    chain_point.block_number - 1
531                ),
532            ],
533            signed_entity_types
534        );
535    }
536}