encointer_ceremonies_assignment/
lib.rs

1//! Everything regarding meetup assignments
2
3#![cfg_attr(not(feature = "std"), no_std)]
4
5use crate::math::{checked_ceil_division, checked_mod_inv, checked_modulo, find_prime_below};
6use encointer_primitives::{
7	ceremonies::{AssignmentParams, MeetupIndexType, MeetupTimeOffsetType, ParticipantIndexType},
8	communities::{Location, LossyFrom},
9	RandomNumberGenerator,
10};
11use sp_runtime::{
12	traits::{AtLeast32Bit, Hash},
13	SaturatedConversion,
14};
15use sp_std::{prelude::Vec, vec};
16
17pub mod math;
18
19/// Assigns a participant to a meetup.
20pub fn assignment_fn(
21	participant_index: ParticipantIndexType,
22	assignment_params: AssignmentParams,
23	assignment_count: u64,
24) -> Option<MeetupIndexType> {
25	participant_index
26		.checked_mul(assignment_params.s1)?
27		.checked_add(assignment_params.s2)
28		.and_then(|div| checked_modulo::<u64>(div, assignment_params.m))
29		.and_then(|div| checked_modulo::<u64>(div, assignment_count))
30}
31
32/// Generates randomized `[AssignmentParams]` for `num_participants` to be distributed across
33/// `num_meetups`.
34pub fn generate_assignment_function_params<Hashing: Hash>(
35	num_participants: u64,
36	num_meetups: u64,
37	random_source: &mut RandomNumberGenerator<Hashing>,
38) -> AssignmentParams {
39	let max_skips = 200;
40	let m = find_prime_below(num_participants) as u32;
41	let mut skip_count = 0;
42	let mut s1 = random_source.pick_non_zero_u32(m - 1); //safe; m > 1, since prime
43	let mut s2 = random_source.pick_non_zero_u32(m - 1);
44
45	while skip_count <= max_skips {
46		s1 = random_source.pick_non_zero_u32(m - 1);
47		s2 = random_source.pick_non_zero_u32(m - 1);
48		if validate_equal_mapping(
49			num_participants,
50			AssignmentParams { m: m as u64, s1: s1 as u64, s2: s2 as u64 },
51			num_meetups,
52		) {
53			break;
54		} else {
55			skip_count += 1; // safe; skip_count <= 200;
56		}
57	}
58	AssignmentParams { m: m as u64, s1: s1 as u64, s2: s2 as u64 }
59}
60
61// Todo add documentation to this function.
62fn validate_equal_mapping(
63	num_participants: u64,
64	assignment_params: AssignmentParams,
65	meetup_count: u64,
66) -> bool {
67	if num_participants < 2 {
68		return true;
69	}
70
71	let mut meetup_index_count: Vec<u64> = vec![0; meetup_count as usize];
72	let meetup_index_count_max =
73		checked_ceil_division(num_participants - assignment_params.m, meetup_count).unwrap_or(0);
74
75	for i in assignment_params.m..num_participants {
76		let meetup_index = match assignment_fn(i, assignment_params, meetup_count) {
77			Some(i) => i as usize,
78			None => return false,
79		};
80
81		meetup_index_count[meetup_index] += 1; // safe; <= num_participants
82		if meetup_index_count[meetup_index] > meetup_index_count_max {
83			return false;
84		}
85	}
86	true
87}
88
89/// Performs the inverse function of `assignment_fn` for all participants in a meetup.
90///
91/// Returns all participants with `assignment_params` belonging to the meetup with `meetup_index`
92/// given the `meetup_count` and `participant_count`.
93pub fn assignment_fn_inverse(
94	meetup_index: u64,
95	assignment_params: AssignmentParams,
96	assignment_count: u64,
97	participant_count: u64,
98) -> Option<Vec<ParticipantIndexType>> {
99	if assignment_count == 0 {
100		return Some(vec![]);
101	}
102
103	let mut max_index = assignment_params.m.saturating_sub(meetup_index) / assignment_count;
104	let mut result: Vec<ParticipantIndexType> = Vec::with_capacity(max_index as usize);
105	// ceil
106	if (assignment_params.m.saturating_sub(meetup_index) as i64).rem_euclid(assignment_count as i64) !=
107		0
108	{
109		max_index += 1; //safe; m prime below num_participants
110	}
111
112	for i in 0..max_index {
113		let t2 = checked_mod_inv(assignment_params.s1 as i64, assignment_params.m as i64)?;
114
115		let t3 = match t3(assignment_count, i, meetup_index, assignment_params, t2) {
116			Some(t3) => t3,
117			None => continue,
118		};
119
120		if t3 >= participant_count {
121			continue;
122		}
123
124		result.push(t3);
125
126		if let Some(t3_plus_m) = t3.checked_add(assignment_params.m) {
127			if t3_plus_m < participant_count {
128				result.push(t3_plus_m)
129			}
130		}
131	}
132	Some(result)
133}
134
135fn t3(
136	n: u64,
137	current_index: u64,
138	meetup_index: MeetupIndexType,
139	params: AssignmentParams,
140	t2: i64,
141) -> Option<u64> {
142	let t3 = (n as i64)
143		.checked_mul(current_index as i64)?
144		.checked_add(meetup_index as i64)?
145		.checked_sub(params.s2 as i64)?
146		.checked_rem_euclid(params.m as i64)?
147		.checked_mul(t2)?
148		.checked_rem_euclid(params.m as i64)?;
149
150	Some(t3 as u64)
151}
152
153pub fn meetup_index(
154	participant_index: ParticipantIndexType,
155	params: AssignmentParams,
156	meetup_count: MeetupIndexType,
157) -> Option<MeetupIndexType> {
158	Some(assignment_fn(participant_index, params, meetup_count)? + 1)
159}
160
161pub fn get_meetup_location_index(
162	meetup_index: MeetupIndexType,
163	locations: &[Location],
164	location_assignment_params: AssignmentParams,
165) -> Option<MeetupIndexType> {
166	assignment_fn(meetup_index, location_assignment_params, locations.len() as u64)
167}
168
169pub fn meetup_location(
170	meetup_index: MeetupIndexType,
171	locations: Vec<Location>,
172	location_assignment_params: AssignmentParams,
173) -> Option<Location> {
174	let location_idx =
175		get_meetup_location_index(meetup_index, &locations, location_assignment_params)?;
176
177	if location_idx < locations.len() as u64 {
178		Some(locations[(location_idx) as usize])
179	} else {
180		None
181	}
182}
183
184pub fn meetup_time<Moment: Copy + AtLeast32Bit>(
185	location: Location,
186	attesting_start: Moment,
187	one_day: Moment,
188	offset: MeetupTimeOffsetType,
189) -> Moment {
190	let one_day_u64: u64 = one_day.saturated_into();
191	let per_degree: i64 = one_day_u64 as i64 / 360i64;
192
193	// The meetups start at high sun at 180 degrees and during one day the meetup locations travel
194	// along the globe until the very last meetup happens at high sun at -180 degrees.
195	// So we scale the range 180...-180 to 0...360
196	// rounding to the lower integer degree. Max error: 240s = 4min
197	let lon: i64 = (i64::lossy_from(location.lon.round_to_zero()) - 180i64).abs();
198
199	let lon_time = lon * per_degree;
200	let attesting_start_u64: u64 = attesting_start.saturated_into();
201	let meetup_time = attesting_start_u64 as i64 + lon_time + offset as i64;
202	(meetup_time as u64).saturated_into()
203}
204
205#[cfg(test)]
206mod tests {
207	use super::*;
208	use encointer_primitives::communities::Degree;
209	#[test]
210	fn meetup_time_works() {
211		let attesting_start = 1671408000000u64; // Mon Dec 19 2022 00:00:00 UTC
212		let moments_per_day = 86400000u64; // ms per day
213		let meetup_time_offset = -2100000; // 35 minutes
214		let greenbay_location =
215			Location { lat: Degree::from_num(0.0), lon: Degree::from_num(-88.15) };
216		assert_eq!(
217			{
218				meetup_time(greenbay_location, attesting_start, moments_per_day, meetup_time_offset)
219			},
220			1671470220000u64
221		) // Mon Dec 19 2022 17:17:00 UTC
222	}
223
224	#[test]
225	fn assignment_fn_works() {
226		assert_eq!(assignment_fn(6, AssignmentParams { m: 4, s1: 5, s2: 3 }, 5).unwrap(), 1)
227	}
228
229	#[test]
230	fn validate_equal_mapping_works() {
231		assert!(!validate_equal_mapping(
232			2761,
233			AssignmentParams { m: 2753, s1: 2326, s2: 1099 },
234			427
235		));
236		assert!(validate_equal_mapping(
237			2761,
238			AssignmentParams { m: 2753, s1: 2325, s2: 1099 },
239			427
240		));
241	}
242
243	#[test]
244	fn assignment_fn_inverse_works() {
245		let mut s1 = 78u64;
246		let mut s2 = 23u64;
247		let mut n = 12u64;
248		let mut num_participants = 118u64;
249		let mut m = 113u64;
250
251		let mut assignment_params = AssignmentParams { m, s1, s2 };
252		check_assignment(num_participants, assignment_params, n);
253
254		s1 = 1u64;
255		s2 = 1u64;
256		n = 2u64;
257		num_participants = 20u64;
258		m = 19u64;
259		assignment_params = AssignmentParams { m, s1, s2 };
260		check_assignment(num_participants, assignment_params, n);
261		s1 = 1u64;
262		s2 = 1u64;
263		n = 1u64;
264		num_participants = 10u64;
265		m = 7u64;
266		assignment_params = AssignmentParams { m, s1, s2 };
267		check_assignment(num_participants, assignment_params, n);
268
269		// in the case where there is only one participant, m will be 2 because it is the smallest
270		// prime number
271		s1 = 1u64;
272		s2 = 1u64;
273		n = 1u64;
274		num_participants = 1u64;
275		m = 2u64;
276		assignment_params = AssignmentParams { m, s1, s2 };
277		check_assignment(num_participants, assignment_params, n);
278	}
279
280	fn check_assignment(num_participants: u64, assignment_params: AssignmentParams, n: u64) {
281		let mut locations: Vec<u64> = vec![0; num_participants as usize];
282
283		for i in 0..num_participants {
284			locations[i as usize] = assignment_fn(i, assignment_params, n).unwrap();
285		}
286
287		let mut assigned_participants: Vec<bool> = vec![false; num_participants as usize];
288
289		// inverse function yields the same result
290		for i in 0..n {
291			let participants =
292				assignment_fn_inverse(i, assignment_params, n, num_participants).unwrap();
293			for p in participants {
294				assigned_participants[p as usize] = true;
295				assert_eq!(locations[p as usize], i)
296			}
297		}
298
299		// all participants were assigned
300		for val in assigned_participants {
301			assert!(val);
302		}
303	}
304}