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