Skip to main content

encointer_meetup_validation/
lib.rs

1// Copyright (c) 2019 Alain Brenzikofer
2// This file is part of Encointer
3//
4// Encointer is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Encointer is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Encointer.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Validates the participant rewards distribution based on their votes and attestations.
18//!
19//! The participant vecs are vecs of participant ids. The `participant_vote` and
20//! `participant_attestations` vecs (and their derived vecs) are indexed by the participant index,
21//! i.e. `participant_votes[i]` holds the vote of participant i.
22
23#![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>>;
33// (a, b) : a is the number of attestations
34//			b are the participants that have those number of attestations
35type 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	// this check has to be made before we exclude participants based on attestations
63	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		// only consider participants present in the meetup
102		if !legit_participants.contains(&i) {
103			continue;
104		}
105		attestations.sort();
106		let mut expected_attestations = legit_participants.clone();
107		// remove self
108		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	// We want to get rid of all participants that did not vote (ie. have a vote of 0 (default
143	// storage value) because they did not receive any attestations). This needs to happen before we
144	// compute the majority vote, because otherwise it would be possible to receive a majority vote
145	// of 0 in the case where more than half of the participants did not show up.
146
147	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
171/// The algorithm works as follows:
172/// We find the participant with the fewest incoming/outgoing attestations
173/// If it if below the threhsold, we exclude the participant
174fn 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	// we will never loop more than the number of participants,
185	// because either participants are excluded or we break out of the loop
186	let max_iterations = participants_to_process.len();
187
188	for _ in 0..max_iterations {
189		// if all participants were excluded, exit the loop
190		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			// remove the participants from the included participants and the attestation vectors
235			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			// if all participants are above the threshold and therefore no participants were
241			// removed, we exit the loop
242			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	// sort by descending vote count
265	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	// filter out participants from the attestation vectors that are not in the participants vector
279	// anymore.
280	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	// sort ascending by value
328	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;