near_primitives/validator_mandates/
mod.rs

1use crate::types::{ValidatorId, validator_stake::ValidatorStake};
2use borsh::{BorshDeserialize, BorshSerialize};
3use near_primitives_core::types::Balance;
4use near_schema_checker_lib::ProtocolSchema;
5
6mod compute_price;
7
8/// Represents the configuration of [`ValidatorMandates`]. Its parameters are expected to remain
9/// valid for one epoch.
10#[derive(
11    BorshSerialize,
12    BorshDeserialize,
13    Default,
14    Copy,
15    Clone,
16    Debug,
17    PartialEq,
18    Eq,
19    serde::Serialize,
20    ProtocolSchema,
21)]
22pub struct ValidatorMandatesConfig {
23    /// The desired number of mandates required per shard.
24    target_mandates_per_shard: usize,
25    /// The number of shards for the referenced epoch.
26    num_shards: usize,
27}
28
29impl ValidatorMandatesConfig {
30    /// Constructs a new configuration.
31    ///
32    /// # Panics
33    ///
34    /// Panics in the following cases:
35    ///
36    /// - If `stake_per_mandate` is 0 as this would lead to division by 0.
37    /// - If `num_shards` is zero.
38    pub fn new(target_mandates_per_shard: usize, num_shards: usize) -> Self {
39        assert!(num_shards > 0, "there should be at least one shard");
40        Self { target_mandates_per_shard, num_shards }
41    }
42}
43
44/// The mandates for a set of validators given a [`ValidatorMandatesConfig`].
45///
46/// A mandate is a liability for a validator to validate a shard. Depending on its stake and the
47/// `stake_per_mandate` specified in `ValidatorMandatesConfig`, a validator may hold multiple
48/// mandates. Each mandate may be assigned to a different shard. The assignment of mandates to
49/// shards is calculated with [`Self::sample`], typically at every height.
50///
51/// See #9983 for context and links to resources that introduce mandates.
52#[derive(
53    BorshSerialize,
54    BorshDeserialize,
55    Default,
56    Clone,
57    Debug,
58    PartialEq,
59    Eq,
60    serde::Serialize,
61    ProtocolSchema,
62)]
63pub struct ValidatorMandates {
64    /// The configuration applied to the mandates.
65    config: ValidatorMandatesConfig,
66    /// The amount of stake a whole mandate is worth.
67    stake_per_mandate: Balance,
68    /// Each element represents a validator mandate held by the validator with the given id.
69    ///
70    /// The id of a validator who holds `n >= 0` mandates occurs `n` times in the vector.
71    mandates: Vec<ValidatorId>,
72    /// Each element represents a partial validator mandate held by the validator with the given id.
73    /// For example, an element `(1, 42)` represents the partial mandate of the validator with id 1
74    /// which has a weight of 42.
75    ///
76    /// Validators whose stake can be distributed across mandates without remainder are not
77    /// represented in this vector.
78    partials: Vec<(ValidatorId, Balance)>,
79}
80
81impl ValidatorMandates {
82    /// Initiates mandates corresponding to the provided `validators`. The validators must be sorted
83    /// by id in ascending order, so the validator with `ValidatorId` equal to `i` is given by
84    /// `validators[i]`.
85    ///
86    /// Only full mandates are assigned, partial mandates are dropped. For example, when the stake
87    /// required for a mandate is 5 and a validator has staked 12, then it will obtain 2 mandates.
88    pub fn new(config: ValidatorMandatesConfig, validators: &[ValidatorStake]) -> Self {
89        let stakes: Vec<Balance> = validators.iter().map(|v| v.stake()).collect();
90        let stake_per_mandate = compute_price::compute_mandate_price(config, &stakes);
91        let num_mandates_per_validator: Vec<u16> =
92            validators.iter().map(|v| v.num_mandates(stake_per_mandate)).collect();
93        let num_total_mandates =
94            num_mandates_per_validator.iter().map(|&num| usize::from(num)).sum();
95        let mut mandates: Vec<ValidatorId> = Vec::with_capacity(num_total_mandates);
96
97        for i in 0..validators.len() {
98            for _ in 0..num_mandates_per_validator[i] {
99                // Each validator's position corresponds to its id.
100                mandates.push(i as ValidatorId);
101            }
102        }
103
104        // Not counting partials towards `required_mandates` as the weight of partials and its
105        // distribution across shards may vary widely.
106        //
107        // Construct vector with capacity as most likely some validators' stake will not be evenly
108        // divided by `config.stake_per_mandate`, i.e. some validators will have partials.
109        let mut partials = Vec::with_capacity(validators.len());
110        for i in 0..validators.len() {
111            let partial_weight = validators[i].partial_mandate_weight(stake_per_mandate);
112            if partial_weight > 0 {
113                partials.push((i as ValidatorId, partial_weight));
114            }
115        }
116
117        Self { config, stake_per_mandate, mandates, partials }
118    }
119}
120
121#[cfg(feature = "rand")]
122mod validator_mandates_sample {
123    use super::*;
124    use itertools::Itertools;
125    use rand::{Rng, seq::SliceRandom};
126
127    impl ValidatorMandates {
128        /// Returns a validator assignment obtained by shuffling mandates and assigning them to shards.
129        /// Shard ids are shuffled as well in this process to avoid a bias lower shard ids, see
130        /// [`ShuffledShardIds`].
131        ///
132        /// It clones mandates since [`ValidatorMandates`] is supposed to be valid for an epoch, while a
133        /// new assignment is calculated at every height.
134        pub fn sample<R>(&self, rng: &mut R) -> ChunkValidatorStakeAssignment
135        where
136            R: Rng + ?Sized,
137        {
138            // Shuffling shard ids to avoid a bias towards lower ids, see [`ShuffledShardIds`]. We
139            // do two separate shuffles for full and partial mandates to reduce the likelihood of
140            // assigning fewer full _and_ partial mandates to the _same_ shard.
141            let shard_ids_for_mandates = ShuffledShardIds::new(rng, self.config.num_shards);
142            let shard_ids_for_partials = ShuffledShardIds::new(rng, self.config.num_shards);
143
144            let shuffled_mandates = self.shuffled_mandates(rng);
145            let shuffled_partials = self.shuffled_partials(rng);
146
147            // Distribute shuffled mandates and partials across shards. For each shard with `shard_id`
148            // in `[0, num_shards)`, we take the elements of the vector with index `i` such that `i %
149            // num_shards == shard_id`.
150            //
151            // Assume, for example, there are 10 mandates and 4 shards. Then for `shard_id = 1` we
152            // collect the mandates with indices 1, 5, and 9.
153            let stake_per_mandate = self.stake_per_mandate;
154            let mut stake_assignment_per_shard =
155                vec![std::collections::HashMap::new(); self.config.num_shards];
156            for shard_id in 0..self.config.num_shards {
157                // Achieve shard id shuffling by writing to the position of the alias of `shard_id`.
158                let mandates_assignment =
159                    &mut stake_assignment_per_shard[shard_ids_for_mandates.get_alias(shard_id)];
160
161                // For the current `shard_id`, collect mandates with index `i` such that
162                // `i % num_shards == shard_id`.
163                for idx in (shard_id..shuffled_mandates.len()).step_by(self.config.num_shards) {
164                    let validator_id = shuffled_mandates[idx];
165                    *mandates_assignment.entry(validator_id).or_default() += stake_per_mandate;
166                }
167
168                // Achieve shard id shuffling by writing to the position of the alias of `shard_id`.
169                let partials_assignment =
170                    &mut stake_assignment_per_shard[shard_ids_for_partials.get_alias(shard_id)];
171
172                // For the current `shard_id`, collect partials with index `i` such that
173                // `i % num_shards == shard_id`.
174                for idx in (shard_id..shuffled_partials.len()).step_by(self.config.num_shards) {
175                    let (validator_id, partial_weight) = shuffled_partials[idx];
176                    *partials_assignment.entry(validator_id).or_default() += partial_weight;
177                }
178            }
179
180            // Deterministically shuffle the validator order for each shard
181            let mut ordered_stake_assignment_per_shard = Vec::with_capacity(self.config.num_shards);
182            for shard_id in 0..self.config.num_shards {
183                // first sort the validators by id then shuffle using rng
184                let stake_assignment = &stake_assignment_per_shard[shard_id];
185                let mut ordered_validator_ids = stake_assignment.keys().sorted().collect_vec();
186                ordered_validator_ids.shuffle(rng);
187                let ordered_mandate_assignment = ordered_validator_ids
188                    .into_iter()
189                    .map(|validator_id| (*validator_id, stake_assignment[validator_id]))
190                    .collect_vec();
191                ordered_stake_assignment_per_shard.push(ordered_mandate_assignment);
192            }
193
194            ordered_stake_assignment_per_shard
195        }
196
197        /// Clones the contained mandates and shuffles them. Cloning is required as a shuffle happens at
198        /// every height while the `ValidatorMandates` are to be valid for an epoch.
199        pub(super) fn shuffled_mandates<R>(&self, rng: &mut R) -> Vec<ValidatorId>
200        where
201            R: Rng + ?Sized,
202        {
203            let mut shuffled_mandates = self.mandates.clone();
204            shuffled_mandates.shuffle(rng);
205            shuffled_mandates
206        }
207
208        /// Clones the contained partials and shuffles them. Cloning is required as a shuffle happens at
209        /// every height while the `ValidatorMandates` are to be valid for an epoch.
210        pub(super) fn shuffled_partials<R>(&self, rng: &mut R) -> Vec<(ValidatorId, Balance)>
211        where
212            R: Rng + ?Sized,
213        {
214            let mut shuffled_partials = self.partials.clone();
215            shuffled_partials.shuffle(rng);
216            shuffled_partials
217        }
218    }
219}
220
221/// Represents an assignment of [`ValidatorMandates`] for a specific height.
222///
223/// Contains one vec per shard, with the position in the vector corresponding to `shard_id` in
224/// `0..num_shards`. Each element is a tuple of `ValidatorId`, total stake they have in the
225/// corresponding shards. A validator whose id is not in any vec has not been assigned to the shard.
226///
227/// For example, `mandates_per_shard[0]` gives us the entries of shard with id 0.
228/// Elements of `mandates_per_shard[0]` can be [(validator3, stake), (validator7, stake)]
229pub type ChunkValidatorStakeAssignment = Vec<Vec<(ValidatorId, Balance)>>;
230
231/// When assigning mandates first to shards with lower ids, the shards with higher ids might end up
232/// with fewer assigned mandates.
233///
234/// Assumes shard ids are in `[0, num_shards)`.
235///
236/// # Example
237///
238/// Assume there are 3 shards and 5 mandates. Assigning to shards with lower ids first, the first
239/// two shards get 2 mandates each. For the third shard only 1 mandate remains.
240///
241/// # Shuffling to avoid bias
242///
243/// When mandates cannot be distributed evenly across shards, some shards will be assigned one
244/// mandate less than others. Shuffling shard ids prevents a bias towards lower shard ids, as it is
245/// no longer predictable which shard(s) will be assigned one mandate less.
246#[cfg(feature = "rand")]
247#[derive(Default, Clone, Debug, PartialEq, Eq)]
248struct ShuffledShardIds {
249    /// Contains the shard ids `[0, num_shards)` in shuffled order.
250    shuffled_ids: Vec<usize>,
251}
252
253#[cfg(feature = "rand")]
254impl ShuffledShardIds {
255    fn new<R>(rng: &mut R, num_shards: usize) -> Self
256    where
257        R: rand::Rng + ?Sized,
258    {
259        use rand::seq::SliceRandom;
260
261        let mut shuffled_ids = (0..num_shards).collect::<Vec<_>>();
262        shuffled_ids.shuffle(rng);
263        Self { shuffled_ids }
264    }
265
266    /// Gets the alias of `shard_id` corresponding to the current shuffling.
267    ///
268    /// # Panics
269    ///
270    /// Panics if `shard_id >= num_shards`.
271    fn get_alias(&self, shard_id: usize) -> usize {
272        self.shuffled_ids[shard_id]
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use near_crypto::PublicKey;
279    use near_primitives_core::types::Balance;
280    use rand::SeedableRng;
281    use rand_chacha::ChaCha8Rng;
282
283    use crate::{
284        types::ValidatorId, types::validator_stake::ValidatorStake,
285        validator_mandates::ValidatorMandatesConfig,
286    };
287
288    use super::{ChunkValidatorStakeAssignment, ShuffledShardIds, ValidatorMandates};
289
290    /// Returns a new, fixed RNG to be used only in tests. Using a fixed RNG facilitates testing as
291    /// it makes outcomes based on that RNG deterministic.
292    fn new_fixed_rng() -> ChaCha8Rng {
293        ChaCha8Rng::seed_from_u64(42)
294    }
295
296    #[test]
297    fn test_validator_mandates_config_new() {
298        let target_mandates_per_shard = 400;
299        let num_shards = 4;
300        assert_eq!(
301            ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards),
302            ValidatorMandatesConfig { target_mandates_per_shard, num_shards },
303        )
304    }
305
306    /// Constructs some `ValidatorStakes` for usage in tests.
307    ///
308    /// # Properties of the corresponding `ValidatorMandates`
309    ///
310    /// The mandates are (verified in [`test_validator_mandates_new`]):
311    /// `vec![0, 0, 0, 1, 1, 3, 4, 4, 4]`
312    ///
313    /// The partials are (verified in [`test_validator_mandates_new`]):
314    /// `vec![(1, 7), (2, 9), (3, 2), (4, 5), (5, 4), (6, 6)]`
315    fn new_validator_stakes() -> Vec<ValidatorStake> {
316        let new_vs = |account_id: &str, balance: Balance| -> ValidatorStake {
317            ValidatorStake::new(
318                account_id.parse().unwrap(),
319                PublicKey::empty(near_crypto::KeyType::ED25519),
320                balance,
321            )
322        };
323
324        vec![
325            new_vs("account_0", 30),
326            new_vs("account_1", 27),
327            new_vs("account_2", 9),
328            new_vs("account_3", 12),
329            new_vs("account_4", 35),
330            new_vs("account_5", 4),
331            new_vs("account_6", 6),
332        ]
333    }
334
335    #[test]
336    fn test_validator_mandates_new() {
337        let validators = new_validator_stakes();
338        let config = ValidatorMandatesConfig::new(3, 4);
339        let mandates = ValidatorMandates::new(config, &validators);
340
341        // With 3 mandates per shard and 4 shards, we are looking for around 12 total mandates.
342        // The total stake in `new_validator_stakes` is 123, so to get 12 mandates we need a price
343        // close to 10. But the algorithm for computing price tries to make the number of _whole_
344        // mandates equal to 12, and there are validators with partial mandates in the distribution,
345        // therefore the price is set a little lower than 10.
346        assert_eq!(mandates.stake_per_mandate, 8);
347
348        // At 8 stake per mandate, the first validator holds three mandates, and so on.
349        // Note that "account_5" and "account_6" hold no mandate as both their stakes are below the threshold.
350        let expected_mandates: Vec<ValidatorId> = vec![0, 0, 0, 1, 1, 1, 2, 3, 4, 4, 4, 4];
351        assert_eq!(mandates.mandates, expected_mandates);
352
353        // The number of whole mandates is exactly equal to our target
354        assert_eq!(mandates.mandates.len(), config.num_shards * config.target_mandates_per_shard);
355
356        // At 8 stake per mandate, the first validator a partial mandate with weight 6, the second
357        // validator holds a partial mandate with weight 3, and so on.
358        let expected_partials: Vec<(ValidatorId, Balance)> =
359            vec![(0, 6), (1, 3), (2, 1), (3, 4), (4, 3), (5, 4), (6, 6)];
360        assert_eq!(mandates.partials, expected_partials);
361    }
362
363    #[test]
364    fn test_validator_mandates_shuffled_mandates() {
365        // Testing with different `num_shards` values to verify the shuffles used in other tests.
366        assert_validator_mandates_shuffled_mandates(3, vec![0, 1, 4, 4, 3, 1, 4, 0, 0]);
367        assert_validator_mandates_shuffled_mandates(4, vec![0, 0, 2, 1, 3, 4, 1, 1, 0, 4, 4, 4]);
368    }
369
370    fn assert_validator_mandates_shuffled_mandates(
371        num_shards: usize,
372        expected_assignment: Vec<ValidatorId>,
373    ) {
374        let validators = new_validator_stakes();
375        let config = ValidatorMandatesConfig::new(3, num_shards);
376        let mandates = ValidatorMandates::new(config, &validators);
377        let mut rng = new_fixed_rng();
378
379        // Call methods that modify `rng` before shuffling mandates to emulate what happens in
380        // [`ValidatorMandates::sample`]. Then `expected_assignment` below equals the shuffled
381        // mandates assigned to shards in `test_validator_mandates_sample_*`.
382        let _shard_ids_for_mandates = ShuffledShardIds::new(&mut rng, config.num_shards);
383        let _shard_ids_for_partials = ShuffledShardIds::new(&mut rng, config.num_shards);
384        let assignment = mandates.shuffled_mandates(&mut rng);
385        assert_eq!(
386            assignment, expected_assignment,
387            "Unexpected shuffling for num_shards = {num_shards}"
388        );
389    }
390
391    #[test]
392    fn test_validator_mandates_shuffled_partials() {
393        // Testing with different `num_shards` values to verify the shuffles used in other tests.
394        assert_validator_mandates_shuffled_partials(
395            3,
396            vec![(3, 2), (4, 5), (1, 7), (2, 9), (5, 4), (6, 6)],
397        );
398        assert_validator_mandates_shuffled_partials(
399            4,
400            vec![(5, 4), (3, 4), (0, 6), (2, 1), (1, 3), (4, 3), (6, 6)],
401        );
402    }
403
404    fn assert_validator_mandates_shuffled_partials(
405        num_shards: usize,
406        expected_assignment: Vec<(ValidatorId, Balance)>,
407    ) {
408        let validators = new_validator_stakes();
409        let config = ValidatorMandatesConfig::new(3, num_shards);
410        let mandates = ValidatorMandates::new(config, &validators);
411        let mut rng = new_fixed_rng();
412
413        // Call methods that modify `rng` before shuffling mandates to emulate what happens in
414        // [`ValidatorMandates::sample`]. Then `expected_assignment` below equals the shuffled
415        // partials assigned to shards in `test_validator_mandates_sample_*`.
416        let _shard_ids_for_mandates = ShuffledShardIds::new(&mut rng, config.num_shards);
417        let _shard_ids_for_partials = ShuffledShardIds::new(&mut rng, config.num_shards);
418        let _ = mandates.shuffled_mandates(&mut rng);
419        let assignment = mandates.shuffled_partials(&mut rng);
420        assert_eq!(
421            assignment, expected_assignment,
422            "Unexpected shuffling for num_shards = {num_shards}"
423        );
424    }
425
426    /// Test mandates per shard are collected correctly for `num_mandates % num_shards == 0` and
427    /// `num_partials % num_shards == 0`.
428    #[test]
429    fn test_validator_mandates_sample_even() {
430        // Choosing `num_shards` such that mandates and partials are distributed evenly.
431        // Assignments in `test_validator_mandates_shuffled_*` can be used to construct
432        // `expected_assignment` below.
433        // Note that shard ids are shuffled too, see `test_shuffled_shard_ids_new`.
434        let config = ValidatorMandatesConfig::new(3, 3);
435        let expected_assignment = vec![
436            vec![(1, 17), (4, 10), (6, 06), (0, 10)],
437            vec![(4, 05), (5, 04), (0, 10), (1, 10), (3, 10)],
438            vec![(0, 10), (2, 09), (4, 20), (3, 02)],
439        ];
440        assert_validator_mandates_sample(config, expected_assignment);
441    }
442
443    /// Test mandates per shard are collected correctly for `num_mandates % num_shards != 0` and
444    /// `num_partials % num_shards != 0`.
445    #[test]
446    fn test_validator_mandates_sample_uneven() {
447        // Choosing `num_shards` such that mandates and partials are distributed unevenly.
448        // Assignments in `test_validator_mandates_shuffled_*` can be used to construct
449        // `expected_assignment` below.
450        // Note that shard ids are shuffled too, see `test_shuffled_shard_ids_new`.
451        let config = ValidatorMandatesConfig::new(3, 4);
452        let expected_mandates_per_shards = vec![
453            vec![(3, 8), (6, 6), (0, 22)],
454            vec![(4, 8), (2, 9), (1, 08)],
455            vec![(0, 8), (3, 4), (4, 19)],
456            vec![(4, 8), (5, 4), (1, 19)],
457        ];
458        assert_validator_mandates_sample(config, expected_mandates_per_shards);
459    }
460
461    /// Asserts mandates per shard are collected correctly.
462    fn assert_validator_mandates_sample(
463        config: ValidatorMandatesConfig,
464        expected_assignment: ChunkValidatorStakeAssignment,
465    ) {
466        let validators = new_validator_stakes();
467        let mandates = ValidatorMandates::new(config, &validators);
468
469        let mut rng = new_fixed_rng();
470        let assignment = mandates.sample(&mut rng);
471
472        assert_eq!(assignment, expected_assignment);
473    }
474
475    #[test]
476    fn test_shuffled_shard_ids_new() {
477        // Testing with different `num_shards` values to verify the shuffles used in other tests.
478        // Doing two shuffles for each `num_shards` with the same RNG since shard ids are shuffled
479        // twice (once for full and once for partial mandates).
480        let mut rng_3_shards = new_fixed_rng();
481        assert_shuffled_shard_ids(&mut rng_3_shards, 3, vec![2, 1, 0], "3 shards, 1st shuffle");
482        assert_shuffled_shard_ids(&mut rng_3_shards, 3, vec![2, 1, 0], "3 shards, 2nd shuffle");
483        let mut rng_4_shards = new_fixed_rng();
484        assert_shuffled_shard_ids(&mut rng_4_shards, 4, vec![0, 2, 1, 3], "4 shards, 1st shuffle");
485        assert_shuffled_shard_ids(&mut rng_4_shards, 4, vec![3, 2, 0, 1], "4 shards, 2nd shuffle");
486    }
487
488    fn assert_shuffled_shard_ids(
489        rng: &mut ChaCha8Rng,
490        num_shards: usize,
491        expected_shuffling: Vec<usize>,
492        test_descriptor: &str,
493    ) {
494        let shuffled_ids_full_mandates = ShuffledShardIds::new(rng, num_shards);
495        assert_eq!(
496            shuffled_ids_full_mandates,
497            ShuffledShardIds { shuffled_ids: expected_shuffling },
498            "Unexpected shuffling for {test_descriptor}",
499        );
500    }
501
502    #[test]
503    fn test_shuffled_shard_ids_get_alias() {
504        let mut rng = new_fixed_rng();
505        let shuffled_ids = ShuffledShardIds::new(&mut rng, 4);
506        // See [`test_shuffled_shard_ids_new`] for the result of this shuffling.
507        assert_eq!(shuffled_ids.get_alias(1), 2);
508    }
509
510    #[test]
511    fn test_deterministic_shuffle() {
512        let config = ValidatorMandatesConfig::new(3, 4);
513        let validators = new_validator_stakes();
514        let mandates = ValidatorMandates::new(config, &validators);
515
516        let mut rng1 = new_fixed_rng();
517        let assignment1 = mandates.sample(&mut rng1);
518
519        let mut rng2 = new_fixed_rng();
520        let assignment2 = mandates.sample(&mut rng2);
521
522        // Two assignments with the same RNG should be equal.
523        assert_eq!(assignment1, assignment2);
524    }
525}