referrerpolicy=no-referrer-when-downgrade

polkadot_node_core_approval_voting/
criteria.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot 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// Polkadot 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 Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Assignment criteria VRF generation and checking.
18
19use codec::Encode;
20use itertools::Itertools;
21pub use polkadot_node_primitives::approval::criteria::{
22	AssignmentCriteria, Config, InvalidAssignment, InvalidAssignmentReason, OurAssignment,
23};
24use polkadot_node_primitives::approval::{
25	self as approval_types,
26	v1::{DelayTranche, RelayVRFStory},
27	v2::{
28		AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, VrfPreOutput, VrfProof, VrfSignature,
29	},
30};
31
32use polkadot_primitives::{
33	AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, ValidatorIndex,
34};
35use rand::{seq::SliceRandom, SeedableRng};
36use rand_chacha::ChaCha20Rng;
37use sc_keystore::LocalKeystore;
38use sp_application_crypto::ByteArray;
39
40use merlin::Transcript;
41use schnorrkel::vrf::VRFInOut;
42
43use std::{
44	cmp::min,
45	collections::{hash_map::Entry, HashMap},
46};
47
48use super::LOG_TARGET;
49
50impl From<crate::approval_db::v2::OurAssignment> for OurAssignment {
51	fn from(entry: crate::approval_db::v2::OurAssignment) -> Self {
52		OurAssignment::new(entry.cert, entry.tranche, entry.validator_index, entry.triggered)
53	}
54}
55
56impl From<OurAssignment> for crate::approval_db::v2::OurAssignment {
57	fn from(entry: OurAssignment) -> Self {
58		Self {
59			tranche: entry.tranche(),
60			validator_index: entry.validator_index(),
61			triggered: entry.triggered(),
62			cert: entry.into_cert(),
63		}
64	}
65}
66
67// Combines the relay VRF story with a sample number if any.
68fn relay_vrf_modulo_transcript_inner(
69	mut transcript: Transcript,
70	relay_vrf_story: RelayVRFStory,
71	sample: Option<u32>,
72) -> Transcript {
73	transcript.append_message(b"RC-VRF", &relay_vrf_story.0);
74
75	if let Some(sample) = sample {
76		sample.using_encoded(|s| transcript.append_message(b"sample", s));
77	}
78
79	transcript
80}
81
82fn relay_vrf_modulo_transcript_v2(relay_vrf_story: RelayVRFStory) -> Transcript {
83	relay_vrf_modulo_transcript_inner(
84		Transcript::new(approval_types::v2::RELAY_VRF_MODULO_CONTEXT),
85		relay_vrf_story,
86		None,
87	)
88}
89
90/// A hard upper bound on num_cores * target_checkers / num_validators
91const MAX_MODULO_SAMPLES: usize = 40;
92
93/// Takes the VRF output as input and returns a Vec of cores the validator is assigned
94/// to as a tranche0 checker.
95fn relay_vrf_modulo_cores(
96	vrf_in_out: &VRFInOut,
97	// Configuration - `relay_vrf_modulo_samples`.
98	num_samples: u32,
99	// Configuration - `n_cores`.
100	max_cores: u32,
101) -> Vec<CoreIndex> {
102	let rand_chacha =
103		ChaCha20Rng::from_seed(vrf_in_out.make_bytes::<<ChaCha20Rng as SeedableRng>::Seed>(
104			approval_types::v2::CORE_RANDOMNESS_CONTEXT,
105		));
106	generate_samples(rand_chacha, num_samples as usize, max_cores as usize)
107}
108
109/// Generates `num_samples` randomly from (0..max_cores) range
110///
111/// Note! The algorithm can't change because validators on the other
112/// side won't be able to check the assignments until they update.
113/// This invariant is tested with `generate_samples_invariant`, so the
114/// tests will catch any subtle changes in the implementation of this function
115/// and its dependencies.
116fn generate_samples(
117	mut rand_chacha: ChaCha20Rng,
118	num_samples: usize,
119	max_cores: usize,
120) -> Vec<CoreIndex> {
121	if num_samples as usize > MAX_MODULO_SAMPLES {
122		gum::warn!(
123			target: LOG_TARGET,
124			n_cores = max_cores,
125			num_samples,
126			max_modulo_samples = MAX_MODULO_SAMPLES,
127			"`num_samples` is greater than `MAX_MODULO_SAMPLES`",
128		);
129	}
130
131	if 2 * num_samples > max_cores {
132		gum::debug!(
133			target: LOG_TARGET,
134			n_cores = max_cores,
135			num_samples,
136			max_modulo_samples = MAX_MODULO_SAMPLES,
137			"Suboptimal configuration `num_samples` should be less than `n_cores` / 2",
138		);
139	}
140
141	let num_samples = min(MAX_MODULO_SAMPLES, min(num_samples, max_cores));
142
143	let mut random_cores = (0..max_cores as u32).map(|val| val.into()).collect::<Vec<CoreIndex>>();
144	let (samples, _) = random_cores.partial_shuffle(&mut rand_chacha, num_samples as usize);
145	samples.into_iter().map(|val| *val).collect_vec()
146}
147
148fn relay_vrf_delay_transcript(relay_vrf_story: RelayVRFStory, core_index: CoreIndex) -> Transcript {
149	let mut t = Transcript::new(approval_types::v1::RELAY_VRF_DELAY_CONTEXT);
150	t.append_message(b"RC-VRF", &relay_vrf_story.0);
151	core_index.0.using_encoded(|s| t.append_message(b"core", s));
152	t
153}
154
155fn relay_vrf_delay_tranche(
156	vrf_in_out: &VRFInOut,
157	num_delay_tranches: u32,
158	zeroth_delay_tranche_width: u32,
159) -> DelayTranche {
160	let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::v1::TRANCHE_RANDOMNESS_CONTEXT);
161
162	// interpret as little-endian u32 and reduce by the number of tranches.
163	let wide_tranche =
164		u32::from_le_bytes(bytes) % (num_delay_tranches + zeroth_delay_tranche_width);
165
166	// Consolidate early results to tranche zero so tranche zero is extra wide.
167	wide_tranche.saturating_sub(zeroth_delay_tranche_width)
168}
169
170pub struct RealAssignmentCriteria;
171
172impl AssignmentCriteria for RealAssignmentCriteria {
173	fn compute_assignments(
174		&self,
175		keystore: &LocalKeystore,
176		relay_vrf_story: RelayVRFStory,
177		config: &Config,
178		leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
179	) -> HashMap<CoreIndex, OurAssignment> {
180		compute_assignments(keystore, relay_vrf_story, config, leaving_cores)
181	}
182
183	fn check_assignment_cert(
184		&self,
185		claimed_core_bitfield: CoreBitfield,
186		validator_index: ValidatorIndex,
187		config: &Config,
188		relay_vrf_story: RelayVRFStory,
189		assignment: &AssignmentCertV2,
190		backing_groups: Vec<GroupIndex>,
191	) -> Result<DelayTranche, InvalidAssignment> {
192		check_assignment_cert(
193			claimed_core_bitfield,
194			validator_index,
195			config,
196			relay_vrf_story,
197			assignment,
198			backing_groups,
199		)
200	}
201}
202
203/// Compute the assignments for a given block. Returns a map containing all assignments to cores in
204/// the block. If more than one assignment targets the given core, only the earliest assignment is
205/// kept.
206///
207/// The `leaving_cores` parameter indicates all cores within the block where a candidate was
208/// included, as well as the group index backing those.
209///
210/// The current description of the protocol assigns every validator to check every core. But at
211/// different times. The idea is that most assignments are never triggered and fall by the wayside.
212///
213/// This will not assign to anything the local validator was part of the backing group for.
214pub fn compute_assignments(
215	keystore: &LocalKeystore,
216	relay_vrf_story: RelayVRFStory,
217	config: &Config,
218	leaving_cores: impl IntoIterator<Item = (CandidateHash, CoreIndex, GroupIndex)> + Clone,
219) -> HashMap<CoreIndex, OurAssignment> {
220	if config.n_cores == 0 ||
221		config.assignment_keys.is_empty() ||
222		config.validator_groups.is_empty()
223	{
224		gum::trace!(
225			target: LOG_TARGET,
226			n_cores = config.n_cores,
227			has_assignment_keys = !config.assignment_keys.is_empty(),
228			has_validator_groups = !config.validator_groups.is_empty(),
229			"Not producing assignments because config is degenerate",
230		);
231
232		return HashMap::new();
233	}
234
235	let (index, assignments_key): (ValidatorIndex, AssignmentPair) = {
236		let key = config.assignment_keys.iter().enumerate().find_map(|(i, p)| {
237			match keystore.key_pair(p) {
238				Ok(Some(pair)) => Some((ValidatorIndex(i as _), pair)),
239				Ok(None) => None,
240				Err(sc_keystore::Error::Unavailable) => None,
241				Err(sc_keystore::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => None,
242				Err(e) => {
243					gum::warn!(target: LOG_TARGET, "Encountered keystore error: {:?}", e);
244					None
245				},
246			}
247		});
248
249		match key {
250			None => {
251				gum::trace!(target: LOG_TARGET, "No assignment key");
252				return HashMap::new();
253			},
254			Some(k) => k,
255		}
256	};
257
258	// Ignore any cores where the assigned group is our own.
259	let leaving_cores = leaving_cores
260		.into_iter()
261		.filter(|(_, _, g)| !is_in_backing_group(&config.validator_groups, index, *g))
262		.map(|(c_hash, core, _)| (c_hash, core))
263		.collect::<Vec<_>>();
264
265	gum::trace!(
266		target: LOG_TARGET,
267		assignable_cores = leaving_cores.len(),
268		"Assigning to candidates from different backing groups"
269	);
270
271	let assignments_key: &sp_application_crypto::sr25519::Pair = assignments_key.as_ref();
272	let assignments_key: &schnorrkel::Keypair = assignments_key.as_ref();
273
274	let mut assignments = HashMap::new();
275
276	// First run `RelayVRFModuloCompact` for the whole block.
277	compute_relay_vrf_modulo_assignments_v2(
278		&assignments_key,
279		index,
280		config,
281		relay_vrf_story.clone(),
282		leaving_cores.clone(),
283		&mut assignments,
284	);
285
286	// Then run `RelayVRFDelay` once for the whole block.
287	compute_relay_vrf_delay_assignments(
288		&assignments_key,
289		index,
290		config,
291		relay_vrf_story,
292		leaving_cores,
293		&mut assignments,
294	);
295
296	assignments
297}
298
299fn assigned_cores_transcript(core_bitfield: &CoreBitfield) -> Transcript {
300	let mut t = Transcript::new(approval_types::v2::ASSIGNED_CORE_CONTEXT);
301	core_bitfield.using_encoded(|s| t.append_message(b"cores", s));
302	t
303}
304
305fn compute_relay_vrf_modulo_assignments_v2(
306	assignments_key: &schnorrkel::Keypair,
307	validator_index: ValidatorIndex,
308	config: &Config,
309	relay_vrf_story: RelayVRFStory,
310	leaving_cores: Vec<(CandidateHash, CoreIndex)>,
311	assignments: &mut HashMap<CoreIndex, OurAssignment>,
312) {
313	let mut assigned_cores = Vec::new();
314	let leaving_cores = leaving_cores.iter().map(|(_, core)| core).collect::<Vec<_>>();
315
316	let maybe_assignment = {
317		let assigned_cores = &mut assigned_cores;
318		assignments_key.vrf_sign_extra_after_check(
319			relay_vrf_modulo_transcript_v2(relay_vrf_story.clone()),
320			|vrf_in_out| {
321				*assigned_cores = relay_vrf_modulo_cores(
322					&vrf_in_out,
323					config.relay_vrf_modulo_samples,
324					config.n_cores,
325				)
326				.into_iter()
327				.filter(|core| leaving_cores.contains(&core))
328				.collect::<Vec<CoreIndex>>();
329
330				if !assigned_cores.is_empty() {
331					gum::trace!(
332						target: LOG_TARGET,
333						?assigned_cores,
334						?validator_index,
335						tranche = 0,
336						"RelayVRFModuloCompact Assignment."
337					);
338
339					let assignment_bitfield: CoreBitfield = assigned_cores
340						.clone()
341						.try_into()
342						.expect("Just checked `!assigned_cores.is_empty()`; qed");
343
344					Some(assigned_cores_transcript(&assignment_bitfield))
345				} else {
346					None
347				}
348			},
349		)
350	};
351
352	if let Some(assignment) = maybe_assignment.map(|(vrf_in_out, vrf_proof, _)| {
353		let assignment_bitfield: CoreBitfield = assigned_cores
354			.clone()
355			.try_into()
356			.expect("Just checked `!assigned_cores.is_empty()`; qed");
357
358		let cert = AssignmentCertV2 {
359			kind: AssignmentCertKindV2::RelayVRFModuloCompact {
360				core_bitfield: assignment_bitfield.clone(),
361			},
362			vrf: VrfSignature {
363				pre_output: VrfPreOutput(vrf_in_out.to_preout()),
364				proof: VrfProof(vrf_proof),
365			},
366		};
367
368		// All assignments of type RelayVRFModulo have tranche 0.
369		OurAssignment::new(cert, 0, validator_index, false)
370	}) {
371		for core_index in assigned_cores {
372			assignments.insert(core_index, assignment.clone());
373		}
374	}
375}
376
377fn compute_relay_vrf_delay_assignments(
378	assignments_key: &schnorrkel::Keypair,
379	validator_index: ValidatorIndex,
380	config: &Config,
381	relay_vrf_story: RelayVRFStory,
382	leaving_cores: impl IntoIterator<Item = (CandidateHash, CoreIndex)>,
383	assignments: &mut HashMap<CoreIndex, OurAssignment>,
384) {
385	for (candidate_hash, core) in leaving_cores {
386		let (vrf_in_out, vrf_proof, _) =
387			assignments_key.vrf_sign(relay_vrf_delay_transcript(relay_vrf_story.clone(), core));
388
389		let tranche = relay_vrf_delay_tranche(
390			&vrf_in_out,
391			config.n_delay_tranches,
392			config.zeroth_delay_tranche_width,
393		);
394
395		let cert = AssignmentCertV2 {
396			kind: AssignmentCertKindV2::RelayVRFDelay { core_index: core },
397			vrf: VrfSignature {
398				pre_output: VrfPreOutput(vrf_in_out.to_preout()),
399				proof: VrfProof(vrf_proof),
400			},
401		};
402
403		let our_assignment = OurAssignment::new(cert, tranche, validator_index, false);
404
405		let used = match assignments.entry(core) {
406			Entry::Vacant(e) => {
407				let _ = e.insert(our_assignment);
408				true
409			},
410			Entry::Occupied(mut e) => {
411				if e.get().tranche() > our_assignment.tranche() {
412					e.insert(our_assignment);
413					true
414				} else {
415					false
416				}
417			},
418		};
419
420		if used {
421			gum::trace!(
422				target: LOG_TARGET,
423				?candidate_hash,
424				?core,
425				?validator_index,
426				tranche,
427				"RelayVRFDelay Assignment",
428			);
429		}
430	}
431}
432
433/// Checks the crypto of an assignment cert. Failure conditions:
434///   * Validator index out of bounds
435///   * VRF signature check fails
436///   * VRF output doesn't match assigned cores
437///   * Core is not covered by extra data in signature
438///   * Core index out of bounds
439///   * Sample is out of bounds
440///   * Validator is present in backing group.
441///
442/// This function does not check whether the core is actually a valid assignment or not. That should
443/// be done outside the scope of this function.
444pub(crate) fn check_assignment_cert(
445	claimed_core_indices: CoreBitfield,
446	validator_index: ValidatorIndex,
447	config: &Config,
448	relay_vrf_story: RelayVRFStory,
449	assignment: &AssignmentCertV2,
450	backing_groups: Vec<GroupIndex>,
451) -> Result<DelayTranche, InvalidAssignment> {
452	use InvalidAssignmentReason as Reason;
453
454	let validator_public = config
455		.assignment_keys
456		.get(validator_index.0 as usize)
457		.ok_or(InvalidAssignment(Reason::ValidatorIndexOutOfBounds))?;
458
459	let public = schnorrkel::PublicKey::from_bytes(validator_public.as_slice())
460		.map_err(|_| InvalidAssignment(Reason::InvalidAssignmentKey))?;
461
462	// Check that we have all backing groups for claimed cores.
463	if claimed_core_indices.count_ones() == 0 ||
464		claimed_core_indices.count_ones() != backing_groups.len()
465	{
466		return Err(InvalidAssignment(Reason::InvalidArguments));
467	}
468
469	// Check that the validator was not part of the backing group
470	// and not already assigned.
471	for (claimed_core, backing_group) in claimed_core_indices.iter_ones().zip(backing_groups.iter())
472	{
473		if claimed_core >= config.n_cores as usize {
474			return Err(InvalidAssignment(Reason::CoreIndexOutOfBounds));
475		}
476
477		let is_in_backing =
478			is_in_backing_group(&config.validator_groups, validator_index, *backing_group);
479
480		if is_in_backing {
481			return Err(InvalidAssignment(Reason::IsInBackingGroup));
482		}
483	}
484
485	let vrf_pre_output = &assignment.vrf.pre_output;
486	let vrf_proof = &assignment.vrf.proof;
487	let first_claimed_core_index =
488		claimed_core_indices.first_one().expect("Checked above; qed") as u32;
489
490	match &assignment.kind {
491		AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => {
492			// Check that claimed core bitfield match the one from certificate.
493			if &claimed_core_indices != core_bitfield {
494				return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch));
495			}
496
497			let (vrf_in_out, _) = public
498				.vrf_verify_extra(
499					relay_vrf_modulo_transcript_v2(relay_vrf_story),
500					&vrf_pre_output.0,
501					&vrf_proof.0,
502					assigned_cores_transcript(core_bitfield),
503				)
504				.map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?;
505
506			let resulting_cores = relay_vrf_modulo_cores(
507				&vrf_in_out,
508				config.relay_vrf_modulo_samples,
509				config.n_cores,
510			);
511
512			// Currently validators can opt out of checking specific cores.
513			// This is the same issue to how validator can opt out and not send their assignments in
514			// the first place. Ensure that the `vrf_in_out` actually includes all of the claimed
515			// cores.
516			for claimed_core_index in claimed_core_indices.iter_ones() {
517				if !resulting_cores.contains(&CoreIndex(claimed_core_index as u32)) {
518					gum::debug!(
519						target: LOG_TARGET,
520						?resulting_cores,
521						?claimed_core_indices,
522						vrf_modulo_cores = ?resulting_cores,
523						"Assignment claimed cores mismatch",
524					);
525					return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch));
526				}
527			}
528
529			Ok(0)
530		},
531		AssignmentCertKindV2::RelayVRFDelay { core_index } => {
532			// Enforce claimed candidates is 1.
533			if claimed_core_indices.count_ones() != 1 {
534				gum::debug!(
535					target: LOG_TARGET,
536					?claimed_core_indices,
537					"`RelayVRFDelay` assignment must always claim 1 core",
538				);
539				return Err(InvalidAssignment(Reason::InvalidArguments));
540			}
541
542			if core_index.0 != first_claimed_core_index {
543				return Err(InvalidAssignment(Reason::VRFDelayCoreIndexMismatch));
544			}
545
546			let (vrf_in_out, _) = public
547				.vrf_verify(
548					relay_vrf_delay_transcript(relay_vrf_story, *core_index),
549					&vrf_pre_output.0,
550					&vrf_proof.0,
551				)
552				.map_err(|_| InvalidAssignment(Reason::VRFDelayOutputMismatch))?;
553
554			Ok(relay_vrf_delay_tranche(
555				&vrf_in_out,
556				config.n_delay_tranches,
557				config.zeroth_delay_tranche_width,
558			))
559		},
560	}
561}
562
563fn is_in_backing_group(
564	validator_groups: &IndexedVec<GroupIndex, Vec<ValidatorIndex>>,
565	validator: ValidatorIndex,
566	group: GroupIndex,
567) -> bool {
568	validator_groups.get(group).map_or(false, |g| g.contains(&validator))
569}
570
571#[cfg(test)]
572mod tests {
573	use super::*;
574	use crate::import::tests::garbage_vrf_signature;
575	use polkadot_primitives::{AssignmentId, Hash, ASSIGNMENT_KEY_TYPE_ID};
576	use sp_application_crypto::sr25519;
577	use sp_core::crypto::Pair as PairT;
578	use sp_keyring::sr25519::Keyring as Sr25519Keyring;
579	use sp_keystore::Keystore;
580
581	// sets up a keystore with the given keyring accounts.
582	fn make_keystore(accounts: &[Sr25519Keyring]) -> LocalKeystore {
583		let store = LocalKeystore::in_memory();
584
585		for s in accounts.iter().copied().map(|k| k.to_seed()) {
586			store.sr25519_generate_new(ASSIGNMENT_KEY_TYPE_ID, Some(s.as_str())).unwrap();
587		}
588
589		store
590	}
591
592	fn assignment_keys(accounts: &[Sr25519Keyring]) -> Vec<AssignmentId> {
593		assignment_keys_plus_random(accounts, 0)
594	}
595
596	fn assignment_keys_plus_random(
597		accounts: &[Sr25519Keyring],
598		random: usize,
599	) -> Vec<AssignmentId> {
600		let gen_random =
601			(0..random).map(|_| AssignmentId::from(sr25519::Pair::generate().0.public()));
602
603		accounts
604			.iter()
605			.map(|k| AssignmentId::from(k.public()))
606			.chain(gen_random)
607			.collect()
608	}
609
610	fn basic_groups(
611		n_validators: usize,
612		n_groups: usize,
613	) -> IndexedVec<GroupIndex, Vec<ValidatorIndex>> {
614		let size = n_validators / n_groups;
615		let big_groups = n_validators % n_groups;
616		let scraps = n_groups * size;
617
618		(0..n_groups)
619			.map(|i| {
620				(i * size..(i + 1) * size)
621					.chain(if i < big_groups { Some(scraps + i) } else { None })
622					.map(|j| ValidatorIndex(j as _))
623					.collect::<Vec<_>>()
624			})
625			.collect()
626	}
627
628	#[test]
629	fn assignments_produced_for_non_backing() {
630		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
631
632		let c_a = CandidateHash(Hash::repeat_byte(0));
633		let c_b = CandidateHash(Hash::repeat_byte(1));
634
635		let relay_vrf_story = RelayVRFStory([42u8; 32]);
636		let assignments = compute_assignments(
637			&keystore,
638			relay_vrf_story,
639			&Config {
640				assignment_keys: assignment_keys(&[
641					Sr25519Keyring::Alice,
642					Sr25519Keyring::Bob,
643					Sr25519Keyring::Charlie,
644				]),
645				validator_groups: IndexedVec::<GroupIndex, Vec<ValidatorIndex>>::from(vec![
646					vec![ValidatorIndex(0)],
647					vec![ValidatorIndex(1), ValidatorIndex(2)],
648				]),
649				n_cores: 2,
650				zeroth_delay_tranche_width: 10,
651				relay_vrf_modulo_samples: 10,
652				n_delay_tranches: 40,
653			},
654			vec![(c_a, CoreIndex(0), GroupIndex(1)), (c_b, CoreIndex(1), GroupIndex(0))],
655		);
656
657		// Note that alice is in group 0, which was the backing group for core 1.
658		// Alice should have self-assigned to check core 0 but not 1.
659		assert_eq!(assignments.len(), 1);
660		assert!(assignments.get(&CoreIndex(0)).is_some());
661	}
662
663	#[test]
664	fn assign_to_nonzero_core() {
665		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
666
667		let c_a = CandidateHash(Hash::repeat_byte(0));
668		let c_b = CandidateHash(Hash::repeat_byte(1));
669
670		let relay_vrf_story = RelayVRFStory([42u8; 32]);
671		let assignments = compute_assignments(
672			&keystore,
673			relay_vrf_story,
674			&Config {
675				assignment_keys: assignment_keys(&[
676					Sr25519Keyring::Alice,
677					Sr25519Keyring::Bob,
678					Sr25519Keyring::Charlie,
679				]),
680				validator_groups: IndexedVec::<GroupIndex, Vec<ValidatorIndex>>::from(vec![
681					vec![ValidatorIndex(0)],
682					vec![ValidatorIndex(1), ValidatorIndex(2)],
683				]),
684				n_cores: 2,
685				zeroth_delay_tranche_width: 10,
686				relay_vrf_modulo_samples: 10,
687				n_delay_tranches: 40,
688			},
689			vec![(c_a, CoreIndex(0), GroupIndex(0)), (c_b, CoreIndex(1), GroupIndex(1))],
690		);
691
692		assert_eq!(assignments.len(), 1);
693		assert!(assignments.get(&CoreIndex(1)).is_some());
694	}
695
696	#[test]
697	fn succeeds_empty_for_0_cores() {
698		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
699
700		let relay_vrf_story = RelayVRFStory([42u8; 32]);
701		let assignments = compute_assignments(
702			&keystore,
703			relay_vrf_story,
704			&Config {
705				assignment_keys: assignment_keys(&[
706					Sr25519Keyring::Alice,
707					Sr25519Keyring::Bob,
708					Sr25519Keyring::Charlie,
709				]),
710				validator_groups: Default::default(),
711				n_cores: 0,
712				zeroth_delay_tranche_width: 10,
713				relay_vrf_modulo_samples: 10,
714				n_delay_tranches: 40,
715			},
716			vec![],
717		);
718
719		assert!(assignments.is_empty());
720	}
721
722	#[derive(Debug)]
723	struct MutatedAssignment {
724		cores: CoreBitfield,
725		cert: AssignmentCertV2,
726		groups: Vec<GroupIndex>,
727		own_group: GroupIndex,
728		val_index: ValidatorIndex,
729		config: Config,
730	}
731
732	// This fails if the closure requests to skip everything.
733	fn check_mutated_assignments(
734		n_validators: usize,
735		n_cores: usize,
736		rotation_offset: usize,
737		f: impl Fn(&mut MutatedAssignment) -> Option<bool>, // None = skip
738	) {
739		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
740
741		let group_for_core = |i| GroupIndex(((i + rotation_offset) % n_cores) as _);
742
743		let config = Config {
744			assignment_keys: assignment_keys_plus_random(
745				&[Sr25519Keyring::Alice],
746				n_validators - 1,
747			),
748			validator_groups: basic_groups(n_validators, n_cores),
749			n_cores: n_cores as u32,
750			zeroth_delay_tranche_width: 10,
751			relay_vrf_modulo_samples: 15,
752			n_delay_tranches: 40,
753		};
754
755		let relay_vrf_story = RelayVRFStory([42u8; 32]);
756		let assignments = compute_assignments(
757			&keystore,
758			relay_vrf_story.clone(),
759			&config,
760			(0..n_cores)
761				.map(|i| {
762					(
763						CandidateHash(Hash::repeat_byte(i as u8)),
764						CoreIndex(i as u32),
765						group_for_core(i),
766					)
767				})
768				.collect::<Vec<_>>(),
769		);
770
771		let mut counted = 0;
772		for (_core, assignment) in assignments {
773			let cores = match assignment.cert().kind.clone() {
774				AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => core_bitfield,
775				AssignmentCertKindV2::RelayVRFDelay { core_index } => core_index.into(),
776			};
777
778			let mut mutated = MutatedAssignment {
779				cores: cores.clone(),
780				groups: cores.iter_ones().map(|core| group_for_core(core)).collect(),
781				cert: assignment.into_cert(),
782				own_group: GroupIndex(0),
783				val_index: ValidatorIndex(0),
784				config: config.clone(),
785			};
786			let expected = match f(&mut mutated) {
787				None => continue,
788				Some(e) => e,
789			};
790
791			counted += 1;
792
793			let is_good = check_assignment_cert(
794				mutated.cores,
795				mutated.val_index,
796				&mutated.config,
797				relay_vrf_story.clone(),
798				&mutated.cert,
799				mutated.groups,
800			)
801			.is_ok();
802
803			assert_eq!(expected, is_good);
804		}
805
806		assert!(counted > 0);
807	}
808
809	#[test]
810	fn computed_assignments_pass_checks() {
811		check_mutated_assignments(200, 100, 25, |_| Some(true));
812	}
813
814	#[test]
815	fn check_rejects_claimed_core_out_of_bounds() {
816		check_mutated_assignments(200, 100, 25, |m| {
817			m.cores = CoreIndex(100).into();
818			Some(false)
819		});
820	}
821
822	#[test]
823	fn check_rejects_in_backing_group() {
824		check_mutated_assignments(200, 100, 25, |m| {
825			m.groups[0] = m.own_group;
826			Some(false)
827		});
828	}
829
830	#[test]
831	fn check_rejects_nonexistent_key() {
832		check_mutated_assignments(200, 100, 25, |m| {
833			m.val_index.0 += 200;
834			Some(false)
835		});
836	}
837
838	#[test]
839	fn check_rejects_delay_bad_vrf() {
840		check_mutated_assignments(40, 100, 8, |m| {
841			let vrf_signature = garbage_vrf_signature();
842			match m.cert.kind.clone() {
843				AssignmentCertKindV2::RelayVRFDelay { .. } => {
844					m.cert.vrf = vrf_signature;
845					Some(false)
846				},
847				_ => None, // skip everything else.
848			}
849		});
850	}
851
852	#[test]
853	fn check_rejects_modulo_bad_vrf() {
854		check_mutated_assignments(200, 100, 25, |m| {
855			let vrf_signature = garbage_vrf_signature();
856			match m.cert.kind.clone() {
857				AssignmentCertKindV2::RelayVRFModuloCompact { .. } => {
858					m.cert.vrf = vrf_signature;
859					Some(false)
860				},
861				_ => None, // skip everything else.
862			}
863		});
864	}
865
866	#[test]
867	fn check_rejects_delay_claimed_core_wrong() {
868		check_mutated_assignments(200, 100, 25, |m| {
869			match m.cert.kind.clone() {
870				AssignmentCertKindV2::RelayVRFDelay { .. } => {
871					// for core in &mut m.cores {
872					// 	core.0 = (core.0 + 1) % 100;
873					// }
874					m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into();
875					Some(false)
876				},
877				_ => None, // skip everything else.
878			}
879		});
880	}
881
882	#[test]
883	fn check_rejects_modulo_core_wrong() {
884		check_mutated_assignments(200, 100, 25, |m| {
885			match m.cert.kind.clone() {
886				AssignmentCertKindV2::RelayVRFModuloCompact { .. } => {
887					m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into();
888
889					Some(false)
890				},
891				_ => None, // skip everything else.
892			}
893		});
894	}
895
896	#[test]
897	fn generate_samples_invariant() {
898		let seed = [
899			1, 0, 52, 0, 0, 0, 0, 0, 1, 0, 10, 0, 22, 32, 0, 0, 2, 0, 55, 49, 0, 11, 0, 0, 3, 0, 0,
900			0, 0, 0, 2, 92,
901		];
902		let rand_chacha = ChaCha20Rng::from_seed(seed);
903
904		let samples = generate_samples(rand_chacha.clone(), 6, 100);
905		let expected = vec![19, 79, 17, 75, 66, 30].into_iter().map(Into::into).collect_vec();
906		assert_eq!(samples, expected);
907
908		let samples = generate_samples(rand_chacha.clone(), 6, 7);
909		let expected = vec![0, 3, 6, 5, 4, 2].into_iter().map(Into::into).collect_vec();
910		assert_eq!(samples, expected);
911
912		let samples = generate_samples(rand_chacha.clone(), 6, 12);
913		let expected = vec![2, 4, 7, 5, 11, 3].into_iter().map(Into::into).collect_vec();
914		assert_eq!(samples, expected);
915
916		let samples = generate_samples(rand_chacha.clone(), 1, 100);
917		let expected = vec![30].into_iter().map(Into::into).collect_vec();
918		assert_eq!(samples, expected);
919
920		let samples = generate_samples(rand_chacha.clone(), 0, 100);
921		let expected = vec![];
922		assert_eq!(samples, expected);
923
924		let samples = generate_samples(rand_chacha, MAX_MODULO_SAMPLES + 1, 100);
925		let expected = vec![
926			42, 54, 55, 93, 64, 27, 49, 15, 83, 71, 62, 1, 43, 77, 97, 41, 7, 69, 0, 88, 59, 14,
927			23, 87, 47, 4, 51, 12, 74, 56, 50, 44, 9, 82, 19, 79, 17, 75, 66, 30,
928		]
929		.into_iter()
930		.map(Into::into)
931		.collect_vec();
932		assert_eq!(samples, expected);
933	}
934}