1use std::collections::HashMap;
2
3use crate::types::validator_power_and_pledge::ValidatorPowerAndPledge;
4use crate::types::ValidatorId;
5use borsh::{BorshDeserialize, BorshSerialize};
6use rand::{seq::SliceRandom, Rng};
7use unc_primitives_core::types::Balance;
8
9#[derive(
12 BorshSerialize, BorshDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq, serde::Serialize,
13)]
14pub struct ValidatorMandatesConfig {
15 pledge_per_mandate: Balance,
17 min_mandates_per_shard: usize,
19 num_shards: usize,
21}
22
23impl ValidatorMandatesConfig {
24 pub fn new(
33 pledge_per_mandate: Balance,
34 min_mandates_per_shard: usize,
35 num_shards: usize,
36 ) -> Self {
37 assert!(pledge_per_mandate > 0, "pledge_per_mandate of 0 would lead to division by 0");
38 assert!(num_shards > 0, "there should be at least one shard");
39 Self { pledge_per_mandate, min_mandates_per_shard, num_shards }
40 }
41}
42
43#[derive(
52 BorshSerialize, BorshDeserialize, Default, Clone, Debug, PartialEq, Eq, serde::Serialize,
53)]
54pub struct ValidatorMandates {
55 config: ValidatorMandatesConfig,
57 mandates: Vec<ValidatorId>,
61 partials: Vec<(ValidatorId, Balance)>,
68}
69
70impl ValidatorMandates {
71 pub fn new(config: ValidatorMandatesConfig, validators: &[ValidatorPowerAndPledge]) -> Self {
78 let num_mandates_per_validator: Vec<u16> =
79 validators.iter().map(|v| v.num_mandates(config.pledge_per_mandate)).collect();
80 let num_total_mandates =
81 num_mandates_per_validator.iter().map(|&num| usize::from(num)).sum();
82 let mut mandates: Vec<ValidatorId> = Vec::with_capacity(num_total_mandates);
83
84 for i in 0..validators.len() {
85 for _ in 0..num_mandates_per_validator[i] {
86 mandates.push(i as ValidatorId);
88 }
89 }
90
91 let required_mandates = config.min_mandates_per_shard * config.num_shards;
92 if mandates.len() < required_mandates {
93 panic!(
95 "not enough validator mandates: got {}, need {}",
96 mandates.len(),
97 required_mandates
98 );
99 }
100
101 let mut partials = Vec::with_capacity(validators.len());
107 for i in 0..validators.len() {
108 let partial_weight = validators[i].partial_mandate_weight(config.pledge_per_mandate);
109 if partial_weight > 0 {
110 partials.push((i as ValidatorId, partial_weight));
111 }
112 }
113
114 Self { config, mandates, partials }
115 }
116
117 pub fn sample<R>(&self, rng: &mut R) -> ValidatorMandatesAssignment
124 where
125 R: Rng + ?Sized,
126 {
127 let shard_ids_for_mandates = ShuffledShardIds::new(rng, self.config.num_shards);
131 let shard_ids_for_partials = ShuffledShardIds::new(rng, self.config.num_shards);
132
133 let shuffled_mandates = self.shuffled_mandates(rng);
134 let shuffled_partials = self.shuffled_partials(rng);
135
136 let mut mandates_per_shard: ValidatorMandatesAssignment =
143 vec![HashMap::new(); self.config.num_shards];
144 for shard_id in 0..self.config.num_shards {
145 let mandates_assignment =
147 &mut mandates_per_shard[shard_ids_for_mandates.get_alias(shard_id)];
148
149 for idx in (shard_id..shuffled_mandates.len()).step_by(self.config.num_shards) {
152 let validator_id = shuffled_mandates[idx];
153 mandates_assignment
154 .entry(validator_id)
155 .and_modify(|assignment_weight| {
156 assignment_weight.num_mandates += 1;
157 })
158 .or_insert(AssignmentWeight::new(1, 0));
159 }
160
161 let partials_assignment =
163 &mut mandates_per_shard[shard_ids_for_partials.get_alias(shard_id)];
164
165 for idx in (shard_id..shuffled_partials.len()).step_by(self.config.num_shards) {
168 let (validator_id, partial_weight) = shuffled_partials[idx];
169 partials_assignment
170 .entry(validator_id)
171 .and_modify(|assignment_weight| {
172 assignment_weight.partial_weight += partial_weight;
173 })
174 .or_insert(AssignmentWeight::new(0, partial_weight));
175 }
176 }
177
178 mandates_per_shard
179 }
180
181 fn shuffled_mandates<R>(&self, rng: &mut R) -> Vec<ValidatorId>
184 where
185 R: Rng + ?Sized,
186 {
187 let mut shuffled_mandates = self.mandates.clone();
188 shuffled_mandates.shuffle(rng);
189 shuffled_mandates
190 }
191
192 fn shuffled_partials<R>(&self, rng: &mut R) -> Vec<(ValidatorId, Balance)>
195 where
196 R: Rng + ?Sized,
197 {
198 let mut shuffled_partials = self.partials.clone();
199 shuffled_partials.shuffle(rng);
200 shuffled_partials
201 }
202}
203
204pub type ValidatorMandatesAssignment = Vec<HashMap<ValidatorId, AssignmentWeight>>;
213
214#[derive(Default, Clone, Debug, PartialEq, Eq)]
215pub struct AssignmentWeight {
216 pub num_mandates: u16,
217 pub partial_weight: Balance,
219}
220
221impl AssignmentWeight {
222 pub fn new(num_mandates: u16, partial_weight: Balance) -> Self {
223 Self { num_mandates, partial_weight }
224 }
225}
226
227#[derive(Default, Clone, Debug, PartialEq, Eq)]
243struct ShuffledShardIds {
244 shuffled_ids: Vec<usize>,
246}
247
248impl ShuffledShardIds {
249 fn new<R>(rng: &mut R, num_shards: usize) -> Self
250 where
251 R: Rng + ?Sized,
252 {
253 let mut shuffled_ids = (0..num_shards).collect::<Vec<_>>();
254 shuffled_ids.shuffle(rng);
255 Self { shuffled_ids }
256 }
257
258 fn get_alias(&self, shard_id: usize) -> usize {
264 self.shuffled_ids[shard_id]
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use std::collections::HashMap;
271
272 use rand::SeedableRng;
273 use rand_chacha::ChaCha8Rng;
274 use unc_crypto::PublicKey;
275 use unc_primitives_core::types::{Balance, Power};
276
277 use crate::{
278 types::{validator_power_and_pledge::ValidatorPowerAndPledge, ValidatorId},
279 validator_mandates::ValidatorMandatesConfig,
280 };
281
282 use super::{
283 AssignmentWeight, ShuffledShardIds, ValidatorMandates, ValidatorMandatesAssignment,
284 };
285
286 fn new_fixed_rng() -> ChaCha8Rng {
289 ChaCha8Rng::seed_from_u64(42)
290 }
291
292 #[test]
293 fn test_validator_mandates_config_new() {
294 let pledge_per_mandate = 10;
295 let min_mandates_per_shard = 400;
296 let num_shards = 4;
297 assert_eq!(
298 ValidatorMandatesConfig::new(pledge_per_mandate, min_mandates_per_shard, num_shards),
299 ValidatorMandatesConfig { pledge_per_mandate, min_mandates_per_shard, num_shards },
300 )
301 }
302
303 fn new_validator_power_and_pledges() -> Vec<ValidatorPowerAndPledge> {
313 let new_vs =
314 |account_id: &str, power: Power, balance: Balance| -> ValidatorPowerAndPledge {
315 ValidatorPowerAndPledge::new(
316 account_id.parse().unwrap(),
317 PublicKey::empty(unc_crypto::KeyType::ED25519),
318 power,
319 balance,
320 )
321 };
322
323 vec![
324 new_vs("account_0", 30, 30),
325 new_vs("account_1", 27, 27),
326 new_vs("account_2", 9, 9),
327 new_vs("account_3", 12, 12),
328 new_vs("account_4", 35, 35),
329 new_vs("account_5", 4, 4),
330 new_vs("account_6", 6, 6),
331 ]
332 }
333
334 #[test]
335 fn test_validator_mandates_new() {
336 let validators = new_validator_power_and_pledges();
337 let config = ValidatorMandatesConfig::new(10, 1, 4);
338 let mandates = ValidatorMandates::new(config, &validators);
339
340 let expected_mandates: Vec<ValidatorId> = vec![0, 0, 0, 1, 1, 3, 4, 4, 4];
343 assert_eq!(mandates.mandates, expected_mandates);
344
345 let expected_partials: Vec<(ValidatorId, Balance)> =
348 vec![(1, 7), (2, 9), (3, 2), (4, 5), (5, 4), (6, 6)];
349 assert_eq!(mandates.partials, expected_partials);
350 }
351
352 #[test]
353 fn test_validator_mandates_shuffled_mandates() {
354 assert_validator_mandates_shuffled_mandates(3, vec![0, 1, 4, 4, 3, 1, 4, 0, 0]);
356 assert_validator_mandates_shuffled_mandates(4, vec![0, 4, 1, 1, 0, 0, 4, 3, 4]);
357 }
358
359 fn assert_validator_mandates_shuffled_mandates(
360 num_shards: usize,
361 expected_assignment: Vec<ValidatorId>,
362 ) {
363 let validators = new_validator_power_and_pledges();
364 let config = ValidatorMandatesConfig::new(10, 1, num_shards);
365 let mandates = ValidatorMandates::new(config, &validators);
366 let mut rng = new_fixed_rng();
367
368 let _shard_ids_for_mandates = ShuffledShardIds::new(&mut rng, config.num_shards);
372 let _shard_ids_for_partials = ShuffledShardIds::new(&mut rng, config.num_shards);
373 let assignment = mandates.shuffled_mandates(&mut rng);
374 assert_eq!(
375 assignment, expected_assignment,
376 "Unexpected shuffling for num_shards = {num_shards}"
377 );
378 }
379
380 #[test]
381 fn test_validator_mandates_shuffled_partials() {
382 assert_validator_mandates_shuffled_partials(
384 3,
385 vec![(3, 2), (4, 5), (1, 7), (2, 9), (5, 4), (6, 6)],
386 );
387 assert_validator_mandates_shuffled_partials(
388 4,
389 vec![(5, 4), (4, 5), (1, 7), (3, 2), (2, 9), (6, 6)],
390 );
391 }
392
393 fn assert_validator_mandates_shuffled_partials(
394 num_shards: usize,
395 expected_assignment: Vec<(ValidatorId, Balance)>,
396 ) {
397 let validators = new_validator_power_and_pledges();
398 let config = ValidatorMandatesConfig::new(10, 1, num_shards);
399 let mandates = ValidatorMandates::new(config, &validators);
400 let mut rng = new_fixed_rng();
401
402 let _shard_ids_for_mandates = ShuffledShardIds::new(&mut rng, config.num_shards);
406 let _shard_ids_for_partials = ShuffledShardIds::new(&mut rng, config.num_shards);
407 let _ = mandates.shuffled_mandates(&mut rng);
408 let assignment = mandates.shuffled_partials(&mut rng);
409 assert_eq!(
410 assignment, expected_assignment,
411 "Unexpected shuffling for num_shards = {num_shards}"
412 );
413 }
414
415 #[test]
418 fn test_validator_mandates_sample_even() {
419 let config = ValidatorMandatesConfig::new(10, 1, 3);
424 let expected_assignment: ValidatorMandatesAssignment = vec![
425 HashMap::from([
426 (4, AssignmentWeight::new(1, 0)),
427 (1, AssignmentWeight::new(1, 7)),
428 (0, AssignmentWeight::new(1, 0)),
429 (6, AssignmentWeight::new(0, 6)),
430 ]),
431 HashMap::from([
432 (1, AssignmentWeight::new(1, 0)),
433 (3, AssignmentWeight::new(1, 0)),
434 (0, AssignmentWeight::new(1, 0)),
435 (4, AssignmentWeight::new(0, 5)),
436 (5, AssignmentWeight::new(0, 4)),
437 ]),
438 HashMap::from([
439 (0, AssignmentWeight::new(1, 0)),
440 (4, AssignmentWeight::new(2, 0)),
441 (3, AssignmentWeight::new(0, 2)),
442 (2, AssignmentWeight::new(0, 9)),
443 ]),
444 ];
445 assert_validator_mandates_sample(config, expected_assignment);
446 }
447
448 #[test]
451 fn test_validator_mandates_sample_uneven() {
452 let config = ValidatorMandatesConfig::new(10, 1, 4);
457 let expected_mandates_per_shards: ValidatorMandatesAssignment = vec![
458 HashMap::from([
459 (0, AssignmentWeight::new(2, 0)),
460 (4, AssignmentWeight::new(1, 0)),
461 (1, AssignmentWeight::new(0, 7)),
462 ]),
463 HashMap::from([
464 (1, AssignmentWeight::new(1, 0)),
465 (4, AssignmentWeight::new(1, 0)),
466 (3, AssignmentWeight::new(0, 2)),
467 ]),
468 HashMap::from([
469 (4, AssignmentWeight::new(1, 5)),
470 (0, AssignmentWeight::new(1, 0)),
471 (6, AssignmentWeight::new(0, 6)),
472 ]),
473 HashMap::from([
474 (1, AssignmentWeight::new(1, 0)),
475 (3, AssignmentWeight::new(1, 0)),
476 (5, AssignmentWeight::new(0, 4)),
477 (2, AssignmentWeight::new(0, 9)),
478 ]),
479 ];
480 assert_validator_mandates_sample(config, expected_mandates_per_shards);
481 }
482
483 fn assert_validator_mandates_sample(
485 config: ValidatorMandatesConfig,
486 expected_assignment: ValidatorMandatesAssignment,
487 ) {
488 let validators = new_validator_power_and_pledges();
489 let mandates = ValidatorMandates::new(config, &validators);
490
491 let mut rng = new_fixed_rng();
492 let assignment = mandates.sample(&mut rng);
493
494 assert_eq!(assignment, expected_assignment);
495 }
496
497 #[test]
498 fn test_shuffled_shard_ids_new() {
499 let mut rng_3_shards = new_fixed_rng();
503 assert_shuffled_shard_ids(&mut rng_3_shards, 3, vec![2, 1, 0], "3 shards, 1st shuffle");
504 assert_shuffled_shard_ids(&mut rng_3_shards, 3, vec![2, 1, 0], "3 shards, 2nd shuffle");
505 let mut rng_4_shards = new_fixed_rng();
506 assert_shuffled_shard_ids(&mut rng_4_shards, 4, vec![0, 2, 1, 3], "4 shards, 1st shuffle");
507 assert_shuffled_shard_ids(&mut rng_4_shards, 4, vec![3, 2, 0, 1], "4 shards, 2nd shuffle");
508 }
509
510 fn assert_shuffled_shard_ids(
511 rng: &mut ChaCha8Rng,
512 num_shards: usize,
513 expected_shuffling: Vec<usize>,
514 test_descriptor: &str,
515 ) {
516 let shuffled_ids_full_mandates = ShuffledShardIds::new(rng, num_shards);
517 assert_eq!(
518 shuffled_ids_full_mandates,
519 ShuffledShardIds { shuffled_ids: expected_shuffling },
520 "Unexpected shuffling for {test_descriptor}",
521 );
522 }
523
524 #[test]
525 fn test_shuffled_shard_ids_get_alias() {
526 let mut rng = new_fixed_rng();
527 let shuffled_ids = ShuffledShardIds::new(&mut rng, 4);
528 assert_eq!(shuffled_ids.get_alias(1), 2);
530 }
531}