unc_primitives/
validator_mandates.rs

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/// Represents the configuration of [`ValidatorMandates`]. Its parameters are expected to remain
10/// valid for one epoch.
11#[derive(
12    BorshSerialize, BorshDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq, serde::Serialize,
13)]
14pub struct ValidatorMandatesConfig {
15    /// The amount of pledge that corresponds to one mandate.
16    pledge_per_mandate: Balance,
17    /// The minimum number of mandates required per shard.
18    min_mandates_per_shard: usize,
19    /// The number of shards for the referenced epoch.
20    num_shards: usize,
21}
22
23impl ValidatorMandatesConfig {
24    /// Constructs a new configuration.
25    ///
26    /// # Panics
27    ///
28    /// Panics in the following cases:
29    ///
30    /// - If `pledge_per_mandate` is 0 as this would lead to division by 0.
31    /// - If `num_shards` is zero.
32    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/// The mandates for a set of validators given a [`ValidatorMandatesConfig`].
44///
45/// A mandate is a liability for a validator to validate a shard. Depending on its pledge and the
46/// `pledge_per_mandate` specified in `ValidatorMandatesConfig`, a validator may hold multiple
47/// mandates. Each mandate may be assigned to a different shard. The assignment of mandates to
48/// shards is calculated with [`Self::sample`], typically at every height.
49///
50/// See #9983 for context and links to resources that introduce mandates.
51#[derive(
52    BorshSerialize, BorshDeserialize, Default, Clone, Debug, PartialEq, Eq, serde::Serialize,
53)]
54pub struct ValidatorMandates {
55    /// The configuration applied to the mandates.
56    config: ValidatorMandatesConfig,
57    /// Each element represents a validator mandate held by the validator with the given id.
58    ///
59    /// The id of a validator who holds `n >= 0` mandates occurs `n` times in the vector.
60    mandates: Vec<ValidatorId>,
61    /// Each element represents a partial validator mandate held by the validator with the given id.
62    /// For example, an element `(1, 42)` represents the partial mandate of the validator with id 1
63    /// which has a weight of 42.
64    ///
65    /// Validators whose pledge can be distributed across mandates without remainder are not
66    /// represented in this vector.
67    partials: Vec<(ValidatorId, Balance)>,
68}
69
70impl ValidatorMandates {
71    /// Initiates mandates corresponding to the provided `validators`. The validators must be sorted
72    /// by id in ascending order, so the validator with `ValidatorId` equal to `i` is given by
73    /// `validators[i]`.
74    ///
75    /// Only full mandates are assigned, partial mandates are dropped. For example, when the pledge
76    /// required for a mandate is 5 and a validator has pledging 12, then it will obtain 2 mandates.
77    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                // Each validator's position corresponds to its id.
87                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            // TODO(#10014) dynamically lower `pledge_per_mandate` to reach enough mandates
94            panic!(
95                "not enough validator mandates: got {}, need {}",
96                mandates.len(),
97                required_mandates
98            );
99        }
100
101        // Not counting partials towards `required_mandates` as the weight of partials and its
102        // distribution across shards may vary widely.
103        //
104        // Construct vector with capacity as most likely some validators' pledge will not be evenly
105        // divided by `config.pledge_per_mandate`, i.e. some validators will have partials.
106        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    /// Returns a validator assignment obtained by shuffling mandates and assigning them to shards.
118    /// Shard ids are shuffled as well in this process to avoid a bias lower shard ids, see
119    /// [`ShuffledShardIds`].
120    ///
121    /// It clones mandates since [`ValidatorMandates`] is supposed to be valid for an epoch, while a
122    /// new assignment is calculated at every height.
123    pub fn sample<R>(&self, rng: &mut R) -> ValidatorMandatesAssignment
124    where
125        R: Rng + ?Sized,
126    {
127        // Shuffling shard ids to avoid a bias towards lower ids, see [`ShuffledShardIds`]. We
128        // do two separate shuffes for full and partial mandates to reduce the likelihood of
129        // assigning fewer full _and_ partial mandates to the _same_ shard.
130        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        // Distribute shuffled mandates and partials across shards. For each shard with `shard_id`
137        // in `[0, num_shards)`, we take the elements of the vector with index `i` such that `i %
138        // num_shards == shard_id`.
139        //
140        // Assume, for example, there are 10 mandates and 4 shards. Then for `shard_id = 1` we
141        // collect the mandates with indices 1, 5, and 9.
142        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            // Achieve shard id shuffling by writing to the position of the alias of `shard_id`.
146            let mandates_assignment =
147                &mut mandates_per_shard[shard_ids_for_mandates.get_alias(shard_id)];
148
149            // For the current `shard_id`, collect mandates with index `i` such that
150            // `i % num_shards == shard_id`.
151            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            // Achieve shard id shuffling by writing to the position of the alias of `shard_id`.
162            let partials_assignment =
163                &mut mandates_per_shard[shard_ids_for_partials.get_alias(shard_id)];
164
165            // For the current `shard_id`, collect partials with index `i` such that
166            // `i % num_shards == shard_id`.
167            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    /// Clones the contained mandates and shuffles them. Cloning is required as a shuffle happens at
182    /// every height while the `ValidatorMandates` are to be valid for an epoch.
183    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    /// Clones the contained partials and shuffles them. Cloning is required as a shuffle happens at
193    /// every height while the `ValidatorMandates` are to be valid for an epoch.
194    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
204/// Represents an assignment of [`ValidatorMandates`] for a specific height.
205///
206/// Contains one map per shard, with the position in the vector corresponding to `shard_id` in
207/// `0..num_shards`. Each `HashMap` maps `ValidatorId`s to the number of mandates they have in the
208/// corresponding shards. A validator whose id is not in a map has not been assigned to the shard.
209///
210/// For example, `mandates_per_shard[0][1]` maps to the [`AssignmentWeights`] validator with
211/// `ValidatorId` 1 holds in shard with id 0.
212pub type ValidatorMandatesAssignment = Vec<HashMap<ValidatorId, AssignmentWeight>>;
213
214#[derive(Default, Clone, Debug, PartialEq, Eq)]
215pub struct AssignmentWeight {
216    pub num_mandates: u16,
217    /// Pledge assigned to this partial mandate.
218    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/// When assigning mandates first to shards with lower ids, the shards with higher ids might end up
228/// with fewer assigned mandates.
229///
230/// Assumes shard ids are in `[0, num_shards)`.
231///
232/// # Example
233///
234/// Assume there are 3 shards and 5 mandates. Assigning to shards with lower ids first, the first
235/// two shards get 2 mandates each. For the third shard only 1 mandate remains.
236///
237/// # Shuffling to avoid bias
238///
239/// When mandates cannot be distributed evenly across shards, some shards will be assigned one
240/// mandata less than others. Shuffling shard ids prevents a bias towards lower shard ids, as it is
241/// no longer predictable which shard(s) will be assigned one mandate less.
242#[derive(Default, Clone, Debug, PartialEq, Eq)]
243struct ShuffledShardIds {
244    /// Contains the shard ids `[0, num_shards)` in shuffled order.
245    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    /// Gets the alias of `shard_id` corresponding to the current shuffling.
259    ///
260    /// # Panics
261    ///
262    /// Panics if `shard_id >= num_shards`.
263    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    /// Returns a new, fixed RNG to be used only in tests. Using a fixed RNG facilitates testing as
287    /// it makes outcomes based on that RNG deterministic.
288    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    /// Constructs some `ValidatorPledges` for usage in tests.
304    ///
305    /// # Properties of the corresponding `ValidatorMandates`
306    ///
307    /// The mandates are (verified in [`test_validator_mandates_new`]):
308    /// `vec![0, 0, 0, 1, 1, 3, 4, 4, 4]`
309    ///
310    /// The partials are (verified in [`test_validator_mandates_new`]):
311    /// `vec![(1, 7), (2, 9), (3, 2), (4, 5), (5, 4), (6, 6)]`
312    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        // At 10 pledge per mandate, the first validator holds three mandates, and so on.
341        // Note that "account_2" holds no mandate as its pledge is below the threshold.
342        let expected_mandates: Vec<ValidatorId> = vec![0, 0, 0, 1, 1, 3, 4, 4, 4];
343        assert_eq!(mandates.mandates, expected_mandates);
344
345        // At 10 pledge per mandate, the first validator holds no partial mandate, the second
346        // validator holds a partial mandate with weight 7, and so on.
347        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        // Testing with different `num_shards` values to verify the shuffles used in other tests.
355        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        // Call methods that modify `rng` before shuffling mandates to emulate what happens in
369        // [`ValidatorMandates::sample`]. Then `expected_assignment` below equals the shuffled
370        // mandates assigned to shards in `test_validator_mandates_sample_*`.
371        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        // Testing with different `num_shards` values to verify the shuffles used in other tests.
383        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        // Call methods that modify `rng` before shuffling mandates to emulate what happens in
403        // [`ValidatorMandates::sample`]. Then `expected_assignment` below equals the shuffled
404        // partials assigned to shards in `test_validator_mandates_sample_*`.
405        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 mandates per shard are collected correctly for `num_mandates % num_shards == 0` and
416    /// `num_partials % num_shards == 0`.
417    #[test]
418    fn test_validator_mandates_sample_even() {
419        // Choosing `num_shards` such that mandates and partials are distributed evenly.
420        // Assignments in `test_validator_mandates_shuffled_*` can be used to construct
421        // `expected_assignment` below.
422        // Note that shard ids are shuffled too, see `test_shuffled_shard_ids_new`.
423        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 mandates per shard are collected correctly for `num_mandates % num_shards != 0` and
449    /// `num_partials % num_shards != 0`.
450    #[test]
451    fn test_validator_mandates_sample_uneven() {
452        // Choosing `num_shards` such that mandates and partials are distributed unevenly.
453        // Assignments in `test_validator_mandates_shuffled_*` can be used to construct
454        // `expected_assignment` below.
455        // Note that shard ids are shuffled too, see `test_shuffled_shard_ids_new`.
456        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    /// Asserts mandates per shard are collected correctly.
484    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        // Testing with different `num_shards` values to verify the shuffles used in other tests.
500        // Doing two shuffles for each `num_shards` with the same RNG since shard ids are shuffled
501        // twice (once for full and once for partial mandates).
502        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        // See [`test_shuffled_shard_ids_new`] for the result of this shuffling.
529        assert_eq!(shuffled_ids.get_alias(1), 2);
530    }
531}