mithril_common/entities/
signed_entity_config.rs

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