mithril_common/test/builder/
fixture_builder.rs1use 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
17pub 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
42pub enum StakeDistributionGenerationMethod {
44 RandomDistribution {
46 seed: [u8; 32],
48 min_stake: Stake,
50 },
51
52 Custom(StakeDistribution),
56
57 Uniform(Stake),
59}
60
61impl MithrilFixtureBuilder {
62 pub fn with_protocol_parameters(mut self, protocol_parameters: ProtocolParameters) -> Self {
64 self.protocol_parameters = protocol_parameters;
65 self
66 }
67
68 pub fn with_signers(mut self, number_of_signers: usize) -> Self {
70 self.number_of_signers = number_of_signers;
71 self
72 }
73
74 pub fn disable_signers_certification(mut self) -> Self {
77 self.enable_signers_certification = false;
78 self
79 }
80
81 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 pub fn with_party_id_seed(mut self, seed: [u8; 32]) -> Self {
92 self.party_id_seed = seed;
93 self
94 }
95
96 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 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 #[test]
326 fn verify_kes_key_cache_content() {
327 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}