1#![cfg_attr(not(feature = "std"), no_std)]
24
25use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
26use scale_info::TypeInfo;
27use serde::{Deserialize, Serialize};
28use sp_std::{vec, vec::Vec};
29
30type ParticipantIndex = usize;
31type Participants = Vec<ParticipantIndex>;
32type Attestations = Vec<Vec<ParticipantIndex>>;
33type ParticipantGroup = (usize, Participants);
36
37pub fn get_participant_judgements(
38 participants: &Participants,
39 participant_votes: &Vec<u32>,
40 participant_attestations: &Attestations,
41 attestation_threshold_fn: fn(usize) -> usize,
42) -> Result<ParticipantJudgements, MeetupValidationError> {
43 let mut participant_judgements = ParticipantJudgements {
44 legit: participants.clone(),
45 excluded: vec![],
46 early_rewards_possible: false,
47 };
48 participant_judgements.exclude_participants(get_excluded_participants_no_vote(
49 &participant_judgements.legit,
50 participant_votes,
51 )?);
52
53 let (n_confirmed, _num_votes, vote_is_unanimous) =
54 find_majority_vote(&participant_judgements.legit, participant_votes)?;
55
56 participant_judgements.exclude_participants(get_excluded_participants_wrong_vote(
57 &participant_judgements.legit,
58 participant_votes,
59 n_confirmed,
60 )?);
61
62 let early_rewards_possible = early_rewards_possible(
64 participant_judgements.legit.clone(),
65 participant_attestations.clone(),
66 participants.len(),
67 n_confirmed,
68 vote_is_unanimous,
69 );
70
71 participant_judgements.exclude_participants(get_excluded_participants_num_attestations(
72 &participant_judgements.legit,
73 participant_attestations.clone(),
74 attestation_threshold_fn,
75 )?);
76
77 participant_judgements.early_rewards_possible = early_rewards_possible;
78 Ok(participant_judgements)
79}
80
81fn vote_yields_majority(num_participants: usize, n_confirmed: u32) -> bool {
82 (n_confirmed as usize).saturating_mul(2) > num_participants
83}
84
85fn num_attestations_matches_vote(
86 legit_participants: &Participants,
87 participant_attestations: &Attestations,
88 n_confirmed: u32,
89) -> bool {
90 participant_attestations
91 .iter()
92 .enumerate()
93 .all(|(i, v)| !(legit_participants.contains(&i)) || v.len() == (n_confirmed - 1) as usize)
94}
95
96fn attestation_graph_is_fully_connected(
97 legit_participants: Participants,
98 participant_attestations: Attestations,
99) -> bool {
100 for (i, mut attestations) in participant_attestations.into_iter().enumerate() {
101 if !legit_participants.contains(&i) {
103 continue;
104 }
105 attestations.sort();
106 let mut expected_attestations = legit_participants.clone();
107 expected_attestations.retain(|&p| p != i);
109 expected_attestations.sort();
110
111 if attestations != expected_attestations {
112 return false;
113 }
114 }
115 true
116}
117
118fn early_rewards_possible(
119 legit_participants: Participants,
120 participant_attestations: Attestations,
121 num_total_participants: usize,
122 n_confirmed: u32,
123 vote_is_unanimous: bool,
124) -> bool {
125 if vote_is_unanimous &&
126 vote_yields_majority(num_total_participants, n_confirmed) &&
127 num_attestations_matches_vote(
128 &legit_participants,
129 &participant_attestations,
130 n_confirmed,
131 ) && attestation_graph_is_fully_connected(legit_participants, participant_attestations)
132 {
133 return true;
134 }
135 false
136}
137
138fn get_excluded_participants_no_vote(
139 participants: &Vec<ParticipantIndex>,
140 participant_votes: &Vec<u32>,
141) -> Result<Vec<(ParticipantIndex, ExclusionReason)>, MeetupValidationError> {
142 let mut excluded_participants: Vec<(ParticipantIndex, ExclusionReason)> = vec![];
148 for i in participants {
149 match participant_votes.get_or_err(*i)? {
150 v if v > &0 => continue,
151 _ => excluded_participants.push((*i, ExclusionReason::NoVote)),
152 }
153 }
154 Ok(excluded_participants)
155}
156
157fn get_excluded_participants_wrong_vote(
158 participants: &Participants,
159 participant_votes: &Vec<u32>,
160 n_confirmed: u32,
161) -> Result<Vec<(ParticipantIndex, ExclusionReason)>, MeetupValidationError> {
162 let mut excluded_participants: Vec<(ParticipantIndex, ExclusionReason)> = vec![];
163 for i in participants {
164 if participant_votes.get_or_err(*i)? != &n_confirmed {
165 excluded_participants.push((*i, ExclusionReason::WrongVote))
166 }
167 }
168 Ok(excluded_participants)
169}
170
171fn get_excluded_participants_num_attestations(
175 participants: &Participants,
176 participant_attestations: Attestations,
177 threshold_fn: fn(usize) -> usize,
178) -> Result<Vec<(usize, ExclusionReason)>, MeetupValidationError> {
179 let mut relevant_attestations = filter_attestations(participants, participant_attestations);
180
181 let mut excluded_participants: Vec<(ParticipantIndex, ExclusionReason)> = vec![];
182 let mut participants_to_process: Vec<ParticipantIndex> = participants.clone();
183
184 let max_iterations = participants_to_process.len();
187
188 for _ in 0..max_iterations {
189 if participants_to_process.is_empty() {
191 return Ok(excluded_participants);
192 };
193
194 let participants_grouped_by_outgoing_attestations =
195 group_participants_by_num_outgoing_attestations(
196 participants_to_process.clone(),
197 &relevant_attestations,
198 )?;
199 let participants_grouped_by_incoming_attestations =
200 group_participants_by_num_incoming_attestations(
201 participants_to_process.clone(),
202 &relevant_attestations,
203 )?;
204
205 let min_num_outgoing_attestations =
206 participants_grouped_by_outgoing_attestations.get_or_err(0)?.0;
207 let min_num_incoming_attestations =
208 participants_grouped_by_incoming_attestations.get_or_err(0)?.0;
209
210 let mut maybe_participants_to_exclude_with_reason: Option<(
211 &Participants,
212 ExclusionReason,
213 )> = None;
214 if min_num_incoming_attestations < min_num_outgoing_attestations {
215 if min_num_incoming_attestations < threshold_fn(participants_to_process.len()) {
216 maybe_participants_to_exclude_with_reason = Some((
217 &participants_grouped_by_incoming_attestations.get_or_err(0)?.1,
218 ExclusionReason::TooFewIncomingAttestations,
219 ));
220 }
221 } else if min_num_outgoing_attestations < threshold_fn(participants_to_process.len()) {
222 maybe_participants_to_exclude_with_reason = Some((
223 &participants_grouped_by_outgoing_attestations.get_or_err(0)?.1,
224 ExclusionReason::TooFewOutgoingAttestations,
225 ));
226 }
227 if let Some((participants_to_exclude, exclusion_reason)) =
228 maybe_participants_to_exclude_with_reason
229 {
230 participants_to_exclude
231 .iter()
232 .for_each(|p| excluded_participants.push((*p, exclusion_reason)));
233
234 participants_to_process.retain(|k| !participants_to_exclude.contains(k));
236 relevant_attestations =
237 filter_attestations(&participants_to_process, relevant_attestations.clone());
238 continue;
239 } else {
240 break;
243 }
244 }
245 Ok(excluded_participants)
246}
247
248fn find_majority_vote(
249 participants: &Participants,
250 participant_votes: &Vec<u32>,
251) -> Result<(u32, u32, bool), MeetupValidationError> {
252 let mut n_vote_candidates: Vec<(u32, u32)> = vec![];
253 for i in participants {
254 let this_vote = participant_votes.get_or_err(*i)?;
255 match n_vote_candidates.iter().position(|&(n, _c)| n == *this_vote) {
256 Some(idx) => n_vote_candidates[idx].1 = n_vote_candidates.get_or_err(idx)?.1 + 1,
257 _ => n_vote_candidates.insert(0, (*this_vote, 1)),
258 };
259 }
260
261 if n_vote_candidates.is_empty() {
262 return Err(MeetupValidationError::BallotEmpty);
263 }
264 n_vote_candidates.sort_by(|a, b| b.1.cmp(&a.1));
266 if n_vote_candidates.get_or_err(0)?.1 < 3 {
267 return Err(MeetupValidationError::NoDependableVote);
268 }
269 let (n_confirmed, vote_count) = n_vote_candidates.get_or_err(0)?;
270 let vote_is_unanimous = n_vote_candidates.len() == 1;
271 Ok((*n_confirmed, *vote_count, vote_is_unanimous))
272}
273
274fn filter_attestations(
275 participants: &Participants,
276 participant_attestations: Attestations,
277) -> Attestations {
278 participant_attestations
281 .into_iter()
282 .map(|mut a| {
283 a.retain(|j| participants.contains(j));
284 a
285 })
286 .collect()
287}
288
289fn group_participants_by_num_incoming_attestations(
290 participants: Participants,
291 participant_attestations: &Attestations,
292) -> Result<Vec<ParticipantGroup>, MeetupValidationError> {
293 let num_incoming_attestations: Participants = (0..participant_attestations.len())
294 .map(|p| {
295 participant_attestations
296 .iter()
297 .enumerate()
298 .filter(|(idx, a)| &p != idx && a.contains(&p))
299 .count()
300 })
301 .collect();
302
303 group_indices_by_value(participants, &num_incoming_attestations)
304}
305
306fn group_participants_by_num_outgoing_attestations(
307 participants: Participants,
308 participant_attestations: &Attestations,
309) -> Result<Vec<ParticipantGroup>, MeetupValidationError> {
310 let num_outgoing_attestations: Participants =
311 participant_attestations.iter().map(|a| a.len()).collect();
312
313 group_indices_by_value(participants, &num_outgoing_attestations)
314}
315
316fn group_indices_by_value(
317 indices: Participants,
318 values: &Vec<usize>,
319) -> Result<Vec<ParticipantGroup>, MeetupValidationError> {
320 if let Some(max) = indices.iter().max() {
321 if max >= &values.len() {
322 return Err(MeetupValidationError::IndexOutOfBounds);
323 }
324 }
325
326 let mut sorted_indices: Participants = indices;
327 sorted_indices.sort_by(|a, b| (values[*a] as i32).cmp(&(values[*b] as i32)));
329
330 let mut grouped_indices: Vec<ParticipantGroup> = vec![];
331 for p in sorted_indices {
332 let value = values.get_or_err(p)?;
333 let last = grouped_indices.last_mut();
334 if let Some((_, group)) = last.filter(|(k, _)| k == value) {
335 group.push(p);
336 } else {
337 grouped_indices.push((*value, vec![p]));
338 }
339 }
340 Ok(grouped_indices)
341}
342
343trait GetOrErr {
344 type Item;
345 type Error;
346 fn get_or_err(&self, i: usize) -> Result<&Self::Item, Self::Error>;
347}
348
349impl<T> GetOrErr for Vec<T> {
350 type Item = T;
351 type Error = MeetupValidationError;
352 fn get_or_err(&self, i: usize) -> Result<&Self::Item, Self::Error> {
353 self.get(i).ok_or(Self::Error::IndexOutOfBounds)
354 }
355}
356
357#[derive(
358 Encode,
359 Decode,
360 DecodeWithMemTracking,
361 Clone,
362 Copy,
363 PartialEq,
364 Eq,
365 Debug,
366 TypeInfo,
367 MaxEncodedLen,
368 Serialize,
369 Deserialize,
370)]
371#[serde(rename_all = "camelCase")]
372pub enum MeetupValidationError {
373 BallotEmpty,
374 NoDependableVote,
375 IndexOutOfBounds,
376}
377#[derive(
378 Encode,
379 Decode,
380 DecodeWithMemTracking,
381 Clone,
382 Copy,
383 PartialEq,
384 Eq,
385 Debug,
386 TypeInfo,
387 MaxEncodedLen,
388 Serialize,
389 Deserialize,
390)]
391#[serde(rename_all = "camelCase")]
392pub enum ExclusionReason {
393 NoVote,
394 WrongVote,
395 TooFewIncomingAttestations,
396 TooFewOutgoingAttestations,
397}
398
399#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct ExcludedParticipant {
402 pub index: usize,
403 pub reason: ExclusionReason,
404}
405#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
406#[serde(rename_all = "camelCase")]
407pub struct ParticipantJudgements {
408 pub legit: Vec<usize>,
409 pub excluded: Vec<ExcludedParticipant>,
410 pub early_rewards_possible: bool,
411}
412
413impl ParticipantJudgements {
414 pub fn exclude_participants(&mut self, excluded: Vec<(usize, ExclusionReason)>) {
415 self.legit.retain(|&i| !excluded.iter().any(|p| p.0 == i));
416 for p in excluded {
417 self.excluded.push(ExcludedParticipant { index: p.0, reason: p.1 })
418 }
419 }
420}
421
422#[cfg(test)]
423mod tests;
424
425#[cfg(test)]
426mod meetup_scenario_tests;