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, 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
18pub 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
43pub enum StakeDistributionGenerationMethod {
45 RandomDistribution {
47 seed: [u8; 32],
49 min_stake: Stake,
51 },
52
53 Custom(StakeDistribution),
57
58 Uniform(Stake),
60}
61
62impl MithrilFixtureBuilder {
63 pub fn with_protocol_parameters(mut self, protocol_parameters: ProtocolParameters) -> Self {
65 self.protocol_parameters = protocol_parameters;
66 self
67 }
68
69 pub fn with_signers(mut self, number_of_signers: usize) -> Self {
71 self.number_of_signers = number_of_signers;
72 self
73 }
74
75 pub fn disable_signers_certification(mut self) -> Self {
78 self.enable_signers_certification = false;
79 self
80 }
81
82 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 pub fn with_party_id_seed(mut self, seed: [u8; 32]) -> Self {
93 self.party_id_seed = seed;
94 self
95 }
96
97 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 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 #[test]
327 fn verify_kes_key_cache_content() {
328 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}