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}