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