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