selection/
ariadne_v2.rs

1use crate::Weight;
2use alloc::vec::Vec;
3use core::iter::repeat_n;
4use rand::SeedableRng;
5use rand::seq::SliceRandom;
6use rand_chacha::ChaCha20Rng;
7
8/// Selects committee according to D-parameter and candidates lists.
9/// Resulting committee has size of `registered_seats + permissioned_seats`.
10/// If both `registered_candidates` and `permissioned_candidates` are not empty then the
11/// selected committee has exactly `registered_seats` assigned to `registered_candidates`.
12///
13/// Let `E_i` be the expected number of places in the resulting committee of candidate `i`
14/// calculated from D-parameter and its weight relative to other candidates.
15/// Then candidate `i` is guaranteed to get at least `floor[E_i]` seats.
16///
17/// Edge cases:
18/// If candidates of one type are missing, then their seats are assigned to candidates of other
19/// type. It is because D-parameter is desired not mandatory ratio.
20/// If `registered_seats` and `permissioned_seats` are 0, empty committee is returned.
21/// It is same as for original Ariadne.
22/// This function returns same selection regardless of input vectors ordering.
23pub fn select_authorities<SC>(
24	registered_seats: u16,
25	permissioned_seats: u16,
26	registered_candidates: Vec<(SC, Weight)>,
27	permissioned_candidates: Vec<SC>,
28	seed: <ChaCha20Rng as SeedableRng>::Seed,
29) -> Option<Vec<SC>>
30where
31	SC: Ord + Clone,
32{
33	let seats_total = permissioned_seats + registered_seats;
34	let mut rng = ChaCha20Rng::from_seed(seed);
35	let permissioned_candidates: Vec<(SC, Weight)> =
36		permissioned_candidates.into_iter().map(|c| (c, 1)).collect();
37
38	let mut selected = if !registered_candidates.is_empty() && !permissioned_candidates.is_empty() {
39		let registered_selected =
40			weighted_with_guaranteed_assignment(registered_candidates, registered_seats, &mut rng);
41		let permissioned_selected = weighted_with_guaranteed_assignment(
42			permissioned_candidates,
43			permissioned_seats,
44			&mut rng,
45		);
46		let mut selected = registered_selected;
47		selected.extend(permissioned_selected);
48		selected
49	} else if registered_candidates.is_empty() {
50		// in absence of registered candidates try to fill their seats with permissioned
51		weighted_with_guaranteed_assignment(permissioned_candidates, seats_total, &mut rng)
52	} else {
53		// in absence of permissioned candidate try to fill their seats with registered
54		weighted_with_guaranteed_assignment(registered_candidates, seats_total, &mut rng)
55	};
56	selected.shuffle(&mut rng);
57	if selected.is_empty() && seats_total > 0 { None } else { Some(selected) }
58}
59
60struct SelectGuaranteedResult<T> {
61	selected: Vec<T>,
62	remaining: Vec<(T, Weight)>,
63}
64
65/// Pseudo-random selection with repetitions, but for any candidate that has expected number
66/// of seats as P+Q, where P is non-negative integer and Q is in [0, 1), it guarantees at least
67/// P places.
68pub fn weighted_with_guaranteed_assignment<T: Clone + Ord>(
69	mut candidates: Vec<(T, Weight)>,
70	n: u16,
71	rng: &mut ChaCha20Rng,
72) -> Vec<T> {
73	if candidates.is_empty() || n == 0 {
74		return Vec::with_capacity(0);
75	}
76	candidates.sort();
77	let SelectGuaranteedResult { mut selected, remaining } = select_guaranteed(candidates, n);
78	let selected_count: u16 = selected.len().try_into().expect("selected count can not exceed u16");
79	selected.extend(select_remaining(remaining, n - selected_count, rng));
80	selected
81}
82
83fn select_guaranteed<T: Clone + Ord>(
84	weighted_candidates: Vec<(T, Weight)>,
85	n: u16,
86) -> SelectGuaranteedResult<T> {
87	let threshold: u128 = weighted_candidates.iter().map(|(_, weight)| weight).sum();
88	let scale: u128 = u128::from(n);
89	let scaled_candidates = weighted_candidates
90		.into_iter()
91		.filter(|(_, weight)| *weight > 0)
92		.map(|(c, weight)| (c, weight * scale));
93	let mut selected = Vec::with_capacity(n.into());
94	let mut remaining = Vec::with_capacity(n.into());
95	for (c, weight) in scaled_candidates {
96		let guaranteed: usize = (weight / threshold).try_into().expect("fits u16");
97		let remaining_weight = weight % threshold;
98		selected.extend(repeat_n(c.clone(), guaranteed));
99		if remaining_weight > 0 {
100			remaining.push((c.clone(), remaining_weight))
101		}
102	}
103	SelectGuaranteedResult { selected, remaining }
104}
105
106fn select_remaining<T: Clone>(
107	weighted_candidates: Vec<(T, Weight)>,
108	n: u16,
109	rng: &mut ChaCha20Rng,
110) -> Vec<T> {
111	crate::weighted_random::select_authorities(weighted_candidates, rng.get_seed(), n)
112		.unwrap_or_else(|| Vec::with_capacity(0))
113}
114
115#[cfg(test)]
116mod tests {
117	use super::*;
118	use crate::tests::*;
119	use quickcheck_macros::quickcheck;
120
121	#[quickcheck]
122	fn guaranteed_places_are_given_no_empty(seed: TestNonce) {
123		let big_stake = 9223372036854775807u128;
124		// P=3, so in each case P1 and P2 get 1 seat guaranteed. 1 is left for random selection.
125		// R1 has twice stake of R2 and R3, so it gets 2 seats, R2 and R3 1 seat. 1 is left for random selection.
126		let committee = select_authorities(
127			5,
128			3,
129			vec![("R1", big_stake * 2), ("R2", big_stake), ("R3", big_stake)],
130			vec!["P1", "P2"],
131			seed.0,
132		)
133		.unwrap();
134		let p1 = committee.iter().filter(|c| **c == "P1").count();
135		let r1 = committee.iter().filter(|c| **c == "R1").count();
136		let r2 = committee.iter().filter(|c| **c == "R2").count();
137		assert!(1 <= p1 && p1 <= 2);
138		assert!(2 <= r1 && r1 <= 3);
139		assert!(1 <= r2 && r2 <= 2);
140		assert_eq!(committee.len(), 8);
141	}
142
143	#[quickcheck]
144	fn permissioned_get_p_seats_registered_get_r_seats(p: u8, r: u8, seed: TestNonce) {
145		// There are more candidates of given type than places for them.
146		// No candidate has guaranteed place, only P:R ratio is guaranteed
147
148		let p: u16 = p.into();
149		let r: u16 = r.into();
150		let registered_candidates: Vec<_> =
151			(0..2 * r).into_iter().map(|i| (format!("R{i}"), 1)).collect();
152		let permissioned_candidates: Vec<_> =
153			(0..2 * r).into_iter().map(|i| format!("P{i}")).collect();
154		let result = select_authorities(
155			r.into(),
156			p.into(),
157			registered_candidates.clone(),
158			permissioned_candidates.clone(),
159			seed.0,
160		);
161		match result {
162			None => assert!(
163				(p > 0 || r > 0)
164					&& registered_candidates.is_empty()
165					&& permissioned_candidates.is_empty()
166			),
167			Some(committee) => {
168				let permissioned = committee.iter().filter(|c| c.starts_with("P")).count();
169				let registered = committee.iter().filter(|c| c.starts_with("R")).count();
170				assert_eq!(permissioned, p.into());
171				assert_eq!(registered, r.into());
172			},
173		}
174	}
175
176	#[quickcheck]
177	fn guaranteed_places_are_given_only_registered(seed: TestNonce) {
178		// committee size is 11, each candidate has 3 guaranteed places,
179		// and there are 2 places for random selection.
180		let committee =
181			select_authorities(5, 6, vec![("R1", 100), ("R2", 100), ("R3", 100)], vec![], seed.0)
182				.unwrap();
183		for key in vec!["R1", "R2", "R3"] {
184			let candidate_count = committee.iter().filter(|c| **c == key).count();
185			assert!(3 <= candidate_count && candidate_count <= 5)
186		}
187	}
188
189	#[quickcheck]
190	fn guaranteed_places_are_given_only_permissioned(seed: TestNonce) {
191		let permissioned = vec!["P1", "P2", "P3"];
192		// committee size is 10, so each has 3 guaranteed places,
193		// and there is 1 place for random selection
194		let committee = select_authorities(5, 5, vec![], permissioned, seed.0).unwrap();
195		for key in vec!["P1", "P2", "P3"] {
196			let candidate_count = committee.iter().filter(|c| **c == key).count();
197			assert!(3 <= candidate_count && candidate_count <= 4)
198		}
199	}
200
201	#[test]
202	fn remaining_seats_are_given_according_to_weights() {
203		// Each permissioned is expected to have 4/3 = 1.333 seats.
204		let permissioned = vec!["P1", "P2", "P3"];
205		// R1 is expected to have 0.4*3=1.2, R2 is expected to have 0.6*3=1.8 seats.
206		let registered = vec![("R1", 40), ("R2", 60)];
207
208		let mut p1_count = 0;
209		let mut r1_count = 0;
210		let mut r2_count = 0;
211		for i in 0u32..10000 {
212			let mut seed = [0u8; 32];
213			seed[0..4].copy_from_slice(&i.to_le_bytes());
214
215			let committee =
216				select_authorities(3, 4, registered.clone(), permissioned.clone(), seed).unwrap();
217			p1_count += committee.iter().filter(|c| **c == "P1").count();
218			r1_count += committee.iter().filter(|c| **c == "R1").count();
219			r2_count += committee.iter().filter(|c| **c == "R2").count();
220		}
221		let tolerance = 50;
222		assert!(13330 - tolerance <= p1_count && p1_count <= 13330 + tolerance);
223		assert!(12000 - tolerance <= r1_count && r1_count <= 12000 + tolerance);
224		assert!(18000 - tolerance <= r2_count && r2_count <= 18000 + tolerance);
225	}
226
227	#[test]
228	fn use_registered_candidates_when_r_is_0_and_there_are_no_permissioned_candidates() {
229		let committee =
230			select_authorities(0, 3, vec![("R1", 100), ("R2", 100)], vec![], [0u8; 32]).unwrap();
231		assert_eq!(committee.len(), 3)
232	}
233
234	#[test]
235	fn use_permissioned_candidates_when_p_is_0_and_there_are_no_registered_candidates() {
236		let committee = select_authorities(3, 0, vec![], vec!["P1", "P2"], [0u8; 32]).unwrap();
237		assert_eq!(committee.len(), 3)
238	}
239
240	#[quickcheck]
241	fn selects_empty_committee_for_0_seats(
242		trustless_candidates: Vec<(String, u128)>,
243		permissioned_candidates: Vec<String>,
244		seed: TestNonce,
245	) {
246		let committee =
247			select_authorities(0, 0, trustless_candidates, permissioned_candidates, seed.0)
248				.unwrap();
249		assert_eq!(committee, Vec::<String>::new());
250	}
251
252	#[quickcheck]
253	fn registered_candidates_order_does_not_matter(seed: TestNonce) {
254		let registered_candidates = vec![("a", 1), ("b", 1), ("c", 1), ("d", 1), ("e", 1)];
255		let mut reversed_candidates = registered_candidates.clone();
256		reversed_candidates.reverse();
257		let committee_1 = select_authorities(4, 0, registered_candidates, vec![], seed.0).unwrap();
258		let committee_2 = select_authorities(4, 0, reversed_candidates, vec![], seed.0).unwrap();
259		assert_eq!(committee_1, committee_2)
260	}
261
262	#[quickcheck]
263	fn permissioned_candidates_order_does_not_matter(seed: TestNonce) {
264		let candidates = vec![("a", 1), ("b", 1), ("c", 1), ("d", 1), ("e", 1)];
265		let mut reversed_candidates = candidates.clone();
266		reversed_candidates.reverse();
267		let committee_1 = select_authorities(0, 4, vec![], candidates, seed.0).unwrap();
268		let committee_2 = select_authorities(0, 4, vec![], reversed_candidates, seed.0).unwrap();
269		assert_eq!(committee_1, committee_2)
270	}
271}