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