mithril_common/test/builder/
fixture_builder.rs

1use kes_summed_ed25519::{PublicKey as KesPublicKey, kes::Sum6Kes, traits::KesSk};
2use rand_chacha::ChaCha20Rng;
3use rand_core::{RngCore, SeedableRng};
4
5use crate::{
6    crypto_helper::{
7        ColdKeyGenerator, KesPeriod, OpCert, ProtocolStakeDistribution, SerDeShelleyFileFormat,
8        Sum6KesBytes,
9    },
10    entities::{PartyId, ProtocolParameters, Stake, StakeDistribution},
11    test::{
12        builder::MithrilFixture,
13        crypto_helper,
14        double::{fake_data, precomputed_kes_key},
15    },
16};
17
18/// A builder of mithril types.
19pub struct MithrilFixtureBuilder {
20    protocol_parameters: ProtocolParameters,
21    enable_signers_certification: bool,
22    number_of_signers: usize,
23    stake_distribution_generation_method: StakeDistributionGenerationMethod,
24    party_id_seed: [u8; 32],
25}
26
27impl Default for MithrilFixtureBuilder {
28    fn default() -> Self {
29        Self {
30            protocol_parameters: fake_data::protocol_parameters(),
31            enable_signers_certification: true,
32            number_of_signers: 5,
33            stake_distribution_generation_method:
34                StakeDistributionGenerationMethod::RandomDistribution {
35                    seed: [0u8; 32],
36                    min_stake: 1,
37                },
38            party_id_seed: [0u8; 32],
39        }
40    }
41}
42
43/// Methods that can be used to generate the stake distribution.
44pub enum StakeDistributionGenerationMethod {
45    /// Each party will have a random stake.
46    RandomDistribution {
47        /// The randomizer seed
48        seed: [u8; 32],
49        /// The minimum stake
50        min_stake: Stake,
51    },
52
53    /// Use a custom stake distribution
54    ///
55    /// Important: this will overwrite the number of signers set by with_signers.
56    Custom(StakeDistribution),
57
58    /// Make a stake distribution where all parties will have the given stake
59    Uniform(Stake),
60}
61
62impl MithrilFixtureBuilder {
63    /// Set the protocol_parameters.
64    pub fn with_protocol_parameters(mut self, protocol_parameters: ProtocolParameters) -> Self {
65        self.protocol_parameters = protocol_parameters;
66        self
67    }
68
69    /// Set the number of signers that will be generated.
70    pub fn with_signers(mut self, number_of_signers: usize) -> Self {
71        self.number_of_signers = number_of_signers;
72        self
73    }
74
75    /// If set the generated signers won't be certified (meaning that they won't
76    /// have a operational certificate).
77    pub fn disable_signers_certification(mut self) -> Self {
78        self.enable_signers_certification = false;
79        self
80    }
81
82    /// Set the generation method used to compute the stake distribution.
83    pub fn with_stake_distribution(
84        mut self,
85        stake_distribution_generation_method: StakeDistributionGenerationMethod,
86    ) -> Self {
87        self.stake_distribution_generation_method = stake_distribution_generation_method;
88        self
89    }
90
91    /// Set the seed used to generated the party ids
92    pub fn with_party_id_seed(mut self, seed: [u8; 32]) -> Self {
93        self.party_id_seed = seed;
94        self
95    }
96
97    /// Transform the specified parameters to a [MithrilFixture].
98    pub fn build(self) -> MithrilFixture {
99        let protocol_stake_distribution = self.generate_stake_distribution();
100        let signers = crypto_helper::setup_signers_from_stake_distribution(
101            &protocol_stake_distribution,
102            &self.protocol_parameters.clone().into(),
103        );
104
105        MithrilFixture::new(
106            self.protocol_parameters,
107            signers,
108            protocol_stake_distribution,
109        )
110    }
111
112    fn generate_stake_distribution(&self) -> ProtocolStakeDistribution {
113        let signers_party_ids = self.generate_party_ids();
114
115        match &self.stake_distribution_generation_method {
116            StakeDistributionGenerationMethod::RandomDistribution { seed, min_stake } => {
117                let mut stake_rng = ChaCha20Rng::from_seed(*seed);
118
119                signers_party_ids
120                    .into_iter()
121                    .map(|party_id| {
122                        let stake = min_stake + stake_rng.next_u64() % 999;
123                        (party_id, stake)
124                    })
125                    .collect::<Vec<_>>()
126            }
127            StakeDistributionGenerationMethod::Custom(stake_distribution) => stake_distribution
128                .clone()
129                .into_iter()
130                .collect::<ProtocolStakeDistribution>(),
131            StakeDistributionGenerationMethod::Uniform(stake) => signers_party_ids
132                .into_iter()
133                .map(|party_id| (party_id, *stake))
134                .collect::<ProtocolStakeDistribution>(),
135        }
136    }
137
138    fn generate_party_ids(&self) -> Vec<PartyId> {
139        match self.stake_distribution_generation_method {
140            StakeDistributionGenerationMethod::Custom(_) => vec![],
141            _ => {
142                let signers_party_ids = (0..self.number_of_signers).map(|party_index| {
143                    if self.enable_signers_certification {
144                        self.build_party_with_operational_certificate(party_index)
145                    } else {
146                        party_index.to_string()
147                    }
148                });
149                signers_party_ids.collect::<Vec<_>>()
150            }
151        }
152    }
153
154    fn provide_kes_key(kes_key_seed: &mut [u8]) -> (Sum6KesBytes, KesPublicKey) {
155        if let Some((kes_bytes, kes_verification_key)) =
156            MithrilFixtureBuilder::cached_kes_key(kes_key_seed)
157        {
158            (kes_bytes, kes_verification_key)
159        } else {
160            println!(
161                "KES key not found in test cache, generating a new one for the seed {kes_key_seed:?}."
162            );
163            MithrilFixtureBuilder::generate_kes_key(kes_key_seed)
164        }
165    }
166
167    fn cached_kes_key(kes_key_seed: &[u8]) -> Option<(Sum6KesBytes, KesPublicKey)> {
168        precomputed_kes_key::cached_kes_key(kes_key_seed).map(
169            |(kes_bytes, kes_verification_key)| {
170                let kes_verification_key = KesPublicKey::from_bytes(&kes_verification_key).unwrap();
171                let kes_bytes = Sum6KesBytes(kes_bytes);
172
173                (kes_bytes, kes_verification_key)
174            },
175        )
176    }
177
178    fn generate_kes_key(kes_key_seed: &mut [u8]) -> (Sum6KesBytes, KesPublicKey) {
179        let mut key_buffer = [0u8; Sum6Kes::SIZE + 4];
180
181        let (kes_secret_key, kes_verification_key) = Sum6Kes::keygen(&mut key_buffer, kes_key_seed);
182        let mut kes_bytes = Sum6KesBytes([0u8; Sum6Kes::SIZE + 4]);
183        kes_bytes.0.copy_from_slice(&kes_secret_key.clone_sk());
184
185        (kes_bytes, kes_verification_key)
186    }
187
188    fn generate_cold_key_seed(&self, party_index: usize) -> Vec<u8> {
189        let mut cold_key_seed: Vec<_> = (party_index)
190            .to_le_bytes()
191            .iter()
192            .zip(self.party_id_seed)
193            .map(|(v1, v2)| v1 + v2)
194            .collect();
195        cold_key_seed.resize(32, 0);
196
197        cold_key_seed
198    }
199
200    fn build_party_with_operational_certificate(&self, party_index: usize) -> PartyId {
201        let cold_key_seed = self.generate_cold_key_seed(party_index).to_vec();
202        let mut kes_key_seed = cold_key_seed.clone();
203
204        let keypair =
205            ColdKeyGenerator::create_deterministic_keypair(cold_key_seed.try_into().unwrap());
206        let (kes_bytes, kes_verification_key) =
207            MithrilFixtureBuilder::provide_kes_key(&mut kes_key_seed);
208        let operational_certificate = OpCert::new(kes_verification_key, 0, KesPeriod(0), keypair);
209        let party_id = operational_certificate
210            .compute_protocol_party_id()
211            .expect("compute protocol party id should not fail");
212        let temp_dir = crypto_helper::setup_temp_directory_for_signer(&party_id, true)
213            .expect("setup temp directory should return a value");
214        if !temp_dir.join("kes.sk").exists() {
215            kes_bytes
216                .to_file(temp_dir.join("kes.sk"))
217                .expect("KES secret key file export should not fail");
218        }
219        if !temp_dir.join("opcert.cert").exists() {
220            operational_certificate
221                .to_file(temp_dir.join("opcert.cert"))
222                .expect("operational certificate file export should not fail");
223        }
224        party_id
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use std::collections::BTreeSet;
232
233    #[test]
234    fn with_protocol_params() {
235        let protocol_parameters = ProtocolParameters::new(1, 10, 0.56);
236        let result = MithrilFixtureBuilder::default()
237            .with_protocol_parameters(protocol_parameters.clone())
238            .build();
239
240        assert_eq!(protocol_parameters, result.protocol_parameters());
241    }
242
243    #[test]
244    fn with_signers() {
245        let result = MithrilFixtureBuilder::default().with_signers(4).build();
246
247        assert_eq!(4, result.signers_with_stake().len());
248    }
249
250    #[test]
251    fn random_stake_distribution_generates_as_many_signers_as_parties() {
252        let result = MithrilFixtureBuilder::default()
253            .with_stake_distribution(StakeDistributionGenerationMethod::RandomDistribution {
254                seed: [0u8; 32],
255                min_stake: 1,
256            })
257            .with_signers(4)
258            .build();
259
260        assert_eq!(4, result.stake_distribution().len());
261    }
262
263    #[test]
264    fn uniform_stake_distribution() {
265        let expected_stake = 10;
266        let stake_distribution = MithrilFixtureBuilder::default()
267            .with_stake_distribution(StakeDistributionGenerationMethod::Uniform(expected_stake))
268            .with_signers(5)
269            .build()
270            .stake_distribution();
271
272        assert!(
273            stake_distribution.iter().all(|(_, stake)| *stake == expected_stake),
274            "Generated stake distribution doesn't have uniform stakes: {stake_distribution:?}"
275        );
276    }
277
278    #[test]
279    fn each_parties_generated_with_random_stake_distribution_have_different_stakes() {
280        let result = MithrilFixtureBuilder::default()
281            .with_stake_distribution(StakeDistributionGenerationMethod::RandomDistribution {
282                seed: [0u8; 32],
283                min_stake: 1,
284            })
285            .with_signers(5)
286            .build();
287        let stakes = result.stake_distribution();
288
289        // BtreeSet dedup values
290        assert_eq!(stakes.len(), BTreeSet::from_iter(stakes.values()).len());
291    }
292
293    #[test]
294    fn dont_generate_party_ids_for_custom_stake_distribution() {
295        let stake_distribution = StakeDistribution::from_iter([("party".to_owned(), 4)]);
296        let builder = MithrilFixtureBuilder::default()
297            .with_stake_distribution(StakeDistributionGenerationMethod::Custom(
298                stake_distribution,
299            ))
300            .with_signers(5);
301
302        assert_eq!(Vec::<PartyId>::new(), builder.generate_party_ids());
303    }
304
305    #[test]
306    fn changing_party_id_seed_change_all_builded_party_ids() {
307        let first_signers = MithrilFixtureBuilder::default()
308            .with_signers(10)
309            .build()
310            .signers_with_stake();
311        let different_party_id_seed_signers = MithrilFixtureBuilder::default()
312            .with_signers(10)
313            .with_party_id_seed([1u8; 32])
314            .build()
315            .signers_with_stake();
316        let first_party_ids: Vec<&PartyId> = first_signers.iter().map(|s| &s.party_id).collect();
317
318        for party_id in different_party_id_seed_signers.iter().map(|s| &s.party_id) {
319            assert!(!first_party_ids.contains(&party_id));
320        }
321    }
322
323    /// Verify that there is a cached kes key for a number of party id.
324    /// If the cache is not up to date, the test will generate the code that can be copied/pasted into the [precomputed_kes_key] module.
325    /// The number of party id that should be in cache is defined with `precomputed_number`
326    #[test]
327    fn verify_kes_key_cache_content() {
328        // Generate code that should be in the `cached_kes_key` function of the `precomputed_kes_key.rs` file.
329        // It can be copied and pasted to update the cache.
330        fn generate_code(party_ids: &Vec<(&[u8], [u8; 612], KesPublicKey)>) -> String {
331            party_ids
332                .iter()
333                .map(|(key, i, p)| format!("{:?} => ({:?}, {:?}),", key, i, p.as_bytes()))
334                .collect::<Vec<_>>()
335                .join("\n")
336        }
337
338        let precomputed_number = 10;
339
340        let fixture = MithrilFixtureBuilder::default();
341        let cold_keys: Vec<_> = (0..precomputed_number)
342            .map(|party_index| fixture.generate_cold_key_seed(party_index))
343            .collect();
344
345        let computed_keys_key: Vec<_> = cold_keys
346            .iter()
347            .map(|cold_key| {
348                let mut kes_key_seed: Vec<u8> = cold_key.clone();
349                let (kes_bytes, kes_verification_key) =
350                    MithrilFixtureBuilder::generate_kes_key(&mut kes_key_seed);
351
352                (cold_key.as_slice(), kes_bytes.0, kes_verification_key)
353            })
354            .collect();
355
356        let cached_kes_key: Vec<_> = cold_keys
357            .iter()
358            .filter_map(|cold_key| {
359                MithrilFixtureBuilder::cached_kes_key(cold_key).map(
360                    |(kes_bytes, kes_verification_key)| {
361                        (cold_key.as_slice(), kes_bytes.0, kes_verification_key)
362                    },
363                )
364            })
365            .collect();
366
367        let expected_code = generate_code(&computed_keys_key);
368        let actual_code = generate_code(&cached_kes_key);
369
370        assert_eq!(
371            computed_keys_key, cached_kes_key,
372            "Precomputed KES keys should be:\n{expected_code}\nbut seems to be:\n{actual_code}"
373        );
374
375        let kes_key_seed = fixture.generate_cold_key_seed(precomputed_number);
376        assert!(
377            MithrilFixtureBuilder::cached_kes_key(kes_key_seed.as_slice()).is_none(),
378            "We checked precomputed KES keys up to {precomputed_number} but it seems to be more."
379        );
380    }
381}