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