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		if used {
516			gum::trace!(
517				target: LOG_TARGET,
518				?candidate_hash,
519				?core,
520				?validator_index,
521				tranche,
522				"RelayVRFDelay Assignment",
523			);
524		}
525	}
526}
527
528/// Checks the crypto of an assignment cert. Failure conditions:
529///   * Validator index out of bounds
530///   * VRF signature check fails
531///   * VRF output doesn't match assigned cores
532///   * Core is not covered by extra data in signature
533///   * Core index out of bounds
534///   * Sample is out of bounds
535///   * Validator is present in backing group.
536///
537/// This function does not check whether the core is actually a valid assignment or not. That should
538/// be done outside the scope of this function.
539pub(crate) fn check_assignment_cert(
540	claimed_core_indices: CoreBitfield,
541	validator_index: ValidatorIndex,
542	config: &Config,
543	relay_vrf_story: RelayVRFStory,
544	assignment: &AssignmentCertV2,
545	backing_groups: Vec<GroupIndex>,
546) -> Result<DelayTranche, InvalidAssignment> {
547	use InvalidAssignmentReason as Reason;
548
549	let validator_public = config
550		.assignment_keys
551		.get(validator_index.0 as usize)
552		.ok_or(InvalidAssignment(Reason::ValidatorIndexOutOfBounds))?;
553
554	let public = schnorrkel::PublicKey::from_bytes(validator_public.as_slice())
555		.map_err(|_| InvalidAssignment(Reason::InvalidAssignmentKey))?;
556
557	// Check that we have all backing groups for claimed cores.
558	if claimed_core_indices.count_ones() == 0 ||
559		claimed_core_indices.count_ones() != backing_groups.len()
560	{
561		return Err(InvalidAssignment(Reason::InvalidArguments))
562	}
563
564	// Check that the validator was not part of the backing group
565	// and not already assigned.
566	for (claimed_core, backing_group) in claimed_core_indices.iter_ones().zip(backing_groups.iter())
567	{
568		if claimed_core >= config.n_cores as usize {
569			return Err(InvalidAssignment(Reason::CoreIndexOutOfBounds))
570		}
571
572		let is_in_backing =
573			is_in_backing_group(&config.validator_groups, validator_index, *backing_group);
574
575		if is_in_backing {
576			return Err(InvalidAssignment(Reason::IsInBackingGroup))
577		}
578	}
579
580	let vrf_pre_output = &assignment.vrf.pre_output;
581	let vrf_proof = &assignment.vrf.proof;
582	let first_claimed_core_index =
583		claimed_core_indices.first_one().expect("Checked above; qed") as u32;
584
585	match &assignment.kind {
586		AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => {
587			// Check that claimed core bitfield match the one from certificate.
588			if &claimed_core_indices != core_bitfield {
589				return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch))
590			}
591
592			let (vrf_in_out, _) = public
593				.vrf_verify_extra(
594					relay_vrf_modulo_transcript_v2(relay_vrf_story),
595					&vrf_pre_output.0,
596					&vrf_proof.0,
597					assigned_cores_transcript(core_bitfield),
598				)
599				.map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?;
600
601			let resulting_cores = relay_vrf_modulo_cores(
602				&vrf_in_out,
603				config.relay_vrf_modulo_samples,
604				config.n_cores,
605			);
606
607			// Currently validators can opt out of checking specific cores.
608			// This is the same issue to how validator can opt out and not send their assignments in
609			// the first place. Ensure that the `vrf_in_out` actually includes all of the claimed
610			// cores.
611			for claimed_core_index in claimed_core_indices.iter_ones() {
612				if !resulting_cores.contains(&CoreIndex(claimed_core_index as u32)) {
613					gum::debug!(
614						target: LOG_TARGET,
615						?resulting_cores,
616						?claimed_core_indices,
617						vrf_modulo_cores = ?resulting_cores,
618						"Assignment claimed cores mismatch",
619					);
620					return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch))
621				}
622			}
623
624			Ok(0)
625		},
626		AssignmentCertKindV2::RelayVRFModulo { sample } => {
627			if *sample >= config.relay_vrf_modulo_samples {
628				return Err(InvalidAssignment(Reason::SampleOutOfBounds))
629			}
630
631			// Enforce claimed candidates is 1.
632			if claimed_core_indices.count_ones() != 1 {
633				gum::warn!(
634					target: LOG_TARGET,
635					?claimed_core_indices,
636					"`RelayVRFModulo` assignment must always claim 1 core",
637				);
638				return Err(InvalidAssignment(Reason::InvalidArguments))
639			}
640
641			let (vrf_in_out, _) = public
642				.vrf_verify_extra(
643					relay_vrf_modulo_transcript_v1(relay_vrf_story, *sample),
644					&vrf_pre_output.0,
645					&vrf_proof.0,
646					assigned_core_transcript(CoreIndex(first_claimed_core_index)),
647				)
648				.map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?;
649
650			let core = relay_vrf_modulo_core(&vrf_in_out, config.n_cores);
651			// ensure that the `vrf_in_out` actually gives us the claimed core.
652			if core.0 == first_claimed_core_index {
653				Ok(0)
654			} else {
655				gum::debug!(
656					target: LOG_TARGET,
657					?core,
658					?claimed_core_indices,
659					"Assignment claimed cores mismatch",
660				);
661				Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch))
662			}
663		},
664		AssignmentCertKindV2::RelayVRFDelay { core_index } => {
665			// Enforce claimed candidates is 1.
666			if claimed_core_indices.count_ones() != 1 {
667				gum::debug!(
668					target: LOG_TARGET,
669					?claimed_core_indices,
670					"`RelayVRFDelay` assignment must always claim 1 core",
671				);
672				return Err(InvalidAssignment(Reason::InvalidArguments))
673			}
674
675			if core_index.0 != first_claimed_core_index {
676				return Err(InvalidAssignment(Reason::VRFDelayCoreIndexMismatch))
677			}
678
679			let (vrf_in_out, _) = public
680				.vrf_verify(
681					relay_vrf_delay_transcript(relay_vrf_story, *core_index),
682					&vrf_pre_output.0,
683					&vrf_proof.0,
684				)
685				.map_err(|_| InvalidAssignment(Reason::VRFDelayOutputMismatch))?;
686
687			Ok(relay_vrf_delay_tranche(
688				&vrf_in_out,
689				config.n_delay_tranches,
690				config.zeroth_delay_tranche_width,
691			))
692		},
693	}
694}
695
696fn is_in_backing_group(
697	validator_groups: &IndexedVec<GroupIndex, Vec<ValidatorIndex>>,
698	validator: ValidatorIndex,
699	group: GroupIndex,
700) -> bool {
701	validator_groups.get(group).map_or(false, |g| g.contains(&validator))
702}
703
704/// Migration helpers.
705impl From<crate::approval_db::v1::OurAssignment> for OurAssignment {
706	fn from(value: crate::approval_db::v1::OurAssignment) -> Self {
707		Self::new(
708			value.cert.into(),
709			value.tranche,
710			value.validator_index,
711			// Whether the assignment has been triggered already.
712			value.triggered,
713		)
714	}
715}
716
717#[cfg(test)]
718mod tests {
719	use super::*;
720	use crate::import::tests::garbage_vrf_signature;
721	use polkadot_primitives::{AssignmentId, Hash, ASSIGNMENT_KEY_TYPE_ID};
722	use sp_application_crypto::sr25519;
723	use sp_core::crypto::Pair as PairT;
724	use sp_keyring::sr25519::Keyring as Sr25519Keyring;
725	use sp_keystore::Keystore;
726
727	// sets up a keystore with the given keyring accounts.
728	fn make_keystore(accounts: &[Sr25519Keyring]) -> LocalKeystore {
729		let store = LocalKeystore::in_memory();
730
731		for s in accounts.iter().copied().map(|k| k.to_seed()) {
732			store.sr25519_generate_new(ASSIGNMENT_KEY_TYPE_ID, Some(s.as_str())).unwrap();
733		}
734
735		store
736	}
737
738	fn assignment_keys(accounts: &[Sr25519Keyring]) -> Vec<AssignmentId> {
739		assignment_keys_plus_random(accounts, 0)
740	}
741
742	fn assignment_keys_plus_random(
743		accounts: &[Sr25519Keyring],
744		random: usize,
745	) -> Vec<AssignmentId> {
746		let gen_random =
747			(0..random).map(|_| AssignmentId::from(sr25519::Pair::generate().0.public()));
748
749		accounts
750			.iter()
751			.map(|k| AssignmentId::from(k.public()))
752			.chain(gen_random)
753			.collect()
754	}
755
756	fn basic_groups(
757		n_validators: usize,
758		n_groups: usize,
759	) -> IndexedVec<GroupIndex, Vec<ValidatorIndex>> {
760		let size = n_validators / n_groups;
761		let big_groups = n_validators % n_groups;
762		let scraps = n_groups * size;
763
764		(0..n_groups)
765			.map(|i| {
766				(i * size..(i + 1) * size)
767					.chain(if i < big_groups { Some(scraps + i) } else { None })
768					.map(|j| ValidatorIndex(j as _))
769					.collect::<Vec<_>>()
770			})
771			.collect()
772	}
773
774	#[test]
775	fn assignments_produced_for_non_backing() {
776		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
777
778		let c_a = CandidateHash(Hash::repeat_byte(0));
779		let c_b = CandidateHash(Hash::repeat_byte(1));
780
781		let relay_vrf_story = RelayVRFStory([42u8; 32]);
782		let assignments = compute_assignments(
783			&keystore,
784			relay_vrf_story,
785			&Config {
786				assignment_keys: assignment_keys(&[
787					Sr25519Keyring::Alice,
788					Sr25519Keyring::Bob,
789					Sr25519Keyring::Charlie,
790				]),
791				validator_groups: IndexedVec::<GroupIndex, Vec<ValidatorIndex>>::from(vec![
792					vec![ValidatorIndex(0)],
793					vec![ValidatorIndex(1), ValidatorIndex(2)],
794				]),
795				n_cores: 2,
796				zeroth_delay_tranche_width: 10,
797				relay_vrf_modulo_samples: 10,
798				n_delay_tranches: 40,
799			},
800			vec![(c_a, CoreIndex(0), GroupIndex(1)), (c_b, CoreIndex(1), GroupIndex(0))],
801			false,
802		);
803
804		// Note that alice is in group 0, which was the backing group for core 1.
805		// Alice should have self-assigned to check core 0 but not 1.
806		assert_eq!(assignments.len(), 1);
807		assert!(assignments.get(&CoreIndex(0)).is_some());
808	}
809
810	#[test]
811	fn assign_to_nonzero_core() {
812		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
813
814		let c_a = CandidateHash(Hash::repeat_byte(0));
815		let c_b = CandidateHash(Hash::repeat_byte(1));
816
817		let relay_vrf_story = RelayVRFStory([42u8; 32]);
818		let assignments = compute_assignments(
819			&keystore,
820			relay_vrf_story,
821			&Config {
822				assignment_keys: assignment_keys(&[
823					Sr25519Keyring::Alice,
824					Sr25519Keyring::Bob,
825					Sr25519Keyring::Charlie,
826				]),
827				validator_groups: IndexedVec::<GroupIndex, Vec<ValidatorIndex>>::from(vec![
828					vec![ValidatorIndex(0)],
829					vec![ValidatorIndex(1), ValidatorIndex(2)],
830				]),
831				n_cores: 2,
832				zeroth_delay_tranche_width: 10,
833				relay_vrf_modulo_samples: 10,
834				n_delay_tranches: 40,
835			},
836			vec![(c_a, CoreIndex(0), GroupIndex(0)), (c_b, CoreIndex(1), GroupIndex(1))],
837			false,
838		);
839
840		assert_eq!(assignments.len(), 1);
841		assert!(assignments.get(&CoreIndex(1)).is_some());
842	}
843
844	#[test]
845	fn succeeds_empty_for_0_cores() {
846		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
847
848		let relay_vrf_story = RelayVRFStory([42u8; 32]);
849		let assignments = compute_assignments(
850			&keystore,
851			relay_vrf_story,
852			&Config {
853				assignment_keys: assignment_keys(&[
854					Sr25519Keyring::Alice,
855					Sr25519Keyring::Bob,
856					Sr25519Keyring::Charlie,
857				]),
858				validator_groups: Default::default(),
859				n_cores: 0,
860				zeroth_delay_tranche_width: 10,
861				relay_vrf_modulo_samples: 10,
862				n_delay_tranches: 40,
863			},
864			vec![],
865			false,
866		);
867
868		assert!(assignments.is_empty());
869	}
870
871	#[derive(Debug)]
872	struct MutatedAssignment {
873		cores: CoreBitfield,
874		cert: AssignmentCertV2,
875		groups: Vec<GroupIndex>,
876		own_group: GroupIndex,
877		val_index: ValidatorIndex,
878		config: Config,
879	}
880
881	// This fails if the closure requests to skip everything.
882	fn check_mutated_assignments(
883		n_validators: usize,
884		n_cores: usize,
885		rotation_offset: usize,
886		f: impl Fn(&mut MutatedAssignment) -> Option<bool>, // None = skip
887	) {
888		let keystore = make_keystore(&[Sr25519Keyring::Alice]);
889
890		let group_for_core = |i| GroupIndex(((i + rotation_offset) % n_cores) as _);
891
892		let config = Config {
893			assignment_keys: assignment_keys_plus_random(
894				&[Sr25519Keyring::Alice],
895				n_validators - 1,
896			),
897			validator_groups: basic_groups(n_validators, n_cores),
898			n_cores: n_cores as u32,
899			zeroth_delay_tranche_width: 10,
900			relay_vrf_modulo_samples: 15,
901			n_delay_tranches: 40,
902		};
903
904		let relay_vrf_story = RelayVRFStory([42u8; 32]);
905		let mut assignments = compute_assignments(
906			&keystore,
907			relay_vrf_story.clone(),
908			&config,
909			(0..n_cores)
910				.map(|i| {
911					(
912						CandidateHash(Hash::repeat_byte(i as u8)),
913						CoreIndex(i as u32),
914						group_for_core(i),
915					)
916				})
917				.collect::<Vec<_>>(),
918			false,
919		);
920
921		// Extend with v2 assignments as well
922		assignments.extend(compute_assignments(
923			&keystore,
924			relay_vrf_story.clone(),
925			&config,
926			(0..n_cores)
927				.map(|i| {
928					(
929						CandidateHash(Hash::repeat_byte(i as u8)),
930						CoreIndex(i as u32),
931						group_for_core(i),
932					)
933				})
934				.collect::<Vec<_>>(),
935			true,
936		));
937
938		let mut counted = 0;
939		for (core, assignment) in assignments {
940			let cores = match assignment.cert().kind.clone() {
941				AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => core_bitfield,
942				AssignmentCertKindV2::RelayVRFModulo { sample: _ } => core.into(),
943				AssignmentCertKindV2::RelayVRFDelay { core_index } => core_index.into(),
944			};
945
946			let mut mutated = MutatedAssignment {
947				cores: cores.clone(),
948				groups: cores.iter_ones().map(|core| group_for_core(core)).collect(),
949				cert: assignment.into_cert(),
950				own_group: GroupIndex(0),
951				val_index: ValidatorIndex(0),
952				config: config.clone(),
953			};
954			let expected = match f(&mut mutated) {
955				None => continue,
956				Some(e) => e,
957			};
958
959			counted += 1;
960
961			let is_good = check_assignment_cert(
962				mutated.cores,
963				mutated.val_index,
964				&mutated.config,
965				relay_vrf_story.clone(),
966				&mutated.cert,
967				mutated.groups,
968			)
969			.is_ok();
970
971			assert_eq!(expected, is_good);
972		}
973
974		assert!(counted > 0);
975	}
976
977	#[test]
978	fn computed_assignments_pass_checks() {
979		check_mutated_assignments(200, 100, 25, |_| Some(true));
980	}
981
982	#[test]
983	fn check_rejects_claimed_core_out_of_bounds() {
984		check_mutated_assignments(200, 100, 25, |m| {
985			m.cores = CoreIndex(100).into();
986			Some(false)
987		});
988	}
989
990	#[test]
991	fn check_rejects_in_backing_group() {
992		check_mutated_assignments(200, 100, 25, |m| {
993			m.groups[0] = m.own_group;
994			Some(false)
995		});
996	}
997
998	#[test]
999	fn check_rejects_nonexistent_key() {
1000		check_mutated_assignments(200, 100, 25, |m| {
1001			m.val_index.0 += 200;
1002			Some(false)
1003		});
1004	}
1005
1006	#[test]
1007	fn check_rejects_delay_bad_vrf() {
1008		check_mutated_assignments(40, 100, 8, |m| {
1009			let vrf_signature = garbage_vrf_signature();
1010			match m.cert.kind.clone() {
1011				AssignmentCertKindV2::RelayVRFDelay { .. } => {
1012					m.cert.vrf = vrf_signature;
1013					Some(false)
1014				},
1015				_ => None, // skip everything else.
1016			}
1017		});
1018	}
1019
1020	#[test]
1021	fn check_rejects_modulo_bad_vrf() {
1022		check_mutated_assignments(200, 100, 25, |m| {
1023			let vrf_signature = garbage_vrf_signature();
1024			match m.cert.kind.clone() {
1025				AssignmentCertKindV2::RelayVRFModulo { .. } => {
1026					m.cert.vrf = vrf_signature;
1027					Some(false)
1028				},
1029				AssignmentCertKindV2::RelayVRFModuloCompact { .. } => {
1030					m.cert.vrf = vrf_signature;
1031					Some(false)
1032				},
1033				_ => None, // skip everything else.
1034			}
1035		});
1036	}
1037
1038	#[test]
1039	fn check_rejects_modulo_sample_out_of_bounds() {
1040		check_mutated_assignments(200, 100, 25, |m| {
1041			match m.cert.kind.clone() {
1042				AssignmentCertKindV2::RelayVRFModulo { sample } => {
1043					m.config.relay_vrf_modulo_samples = sample;
1044					Some(false)
1045				},
1046				AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield: _ } => Some(true),
1047				_ => None, // skip everything else.
1048			}
1049		});
1050	}
1051
1052	#[test]
1053	fn check_rejects_delay_claimed_core_wrong() {
1054		check_mutated_assignments(200, 100, 25, |m| {
1055			match m.cert.kind.clone() {
1056				AssignmentCertKindV2::RelayVRFDelay { .. } => {
1057					// for core in &mut m.cores {
1058					// 	core.0 = (core.0 + 1) % 100;
1059					// }
1060					m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into();
1061					Some(false)
1062				},
1063				_ => None, // skip everything else.
1064			}
1065		});
1066	}
1067
1068	#[test]
1069	fn check_rejects_modulo_core_wrong() {
1070		check_mutated_assignments(200, 100, 25, |m| {
1071			match m.cert.kind.clone() {
1072				AssignmentCertKindV2::RelayVRFModulo { .. } |
1073				AssignmentCertKindV2::RelayVRFModuloCompact { .. } => {
1074					m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into();
1075
1076					Some(false)
1077				},
1078				_ => None, // skip everything else.
1079			}
1080		});
1081	}
1082
1083	#[test]
1084	fn generate_samples_invariant() {
1085		let seed = [
1086			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,
1087			0, 0, 0, 2, 92,
1088		];
1089		let rand_chacha = ChaCha20Rng::from_seed(seed);
1090
1091		let samples = generate_samples(rand_chacha.clone(), 6, 100);
1092		let expected = vec![19, 79, 17, 75, 66, 30].into_iter().map(Into::into).collect_vec();
1093		assert_eq!(samples, expected);
1094
1095		let samples = generate_samples(rand_chacha.clone(), 6, 7);
1096		let expected = vec![0, 3, 6, 5, 4, 2].into_iter().map(Into::into).collect_vec();
1097		assert_eq!(samples, expected);
1098
1099		let samples = generate_samples(rand_chacha.clone(), 6, 12);
1100		let expected = vec![2, 4, 7, 5, 11, 3].into_iter().map(Into::into).collect_vec();
1101		assert_eq!(samples, expected);
1102
1103		let samples = generate_samples(rand_chacha.clone(), 1, 100);
1104		let expected = vec![30].into_iter().map(Into::into).collect_vec();
1105		assert_eq!(samples, expected);
1106
1107		let samples = generate_samples(rand_chacha.clone(), 0, 100);
1108		let expected = vec![];
1109		assert_eq!(samples, expected);
1110
1111		let samples = generate_samples(rand_chacha, MAX_MODULO_SAMPLES + 1, 100);
1112		let expected = vec![
1113			42, 54, 55, 93, 64, 27, 49, 15, 83, 71, 62, 1, 43, 77, 97, 41, 7, 69, 0, 88, 59, 14,
1114			23, 87, 47, 4, 51, 12, 74, 56, 50, 44, 9, 82, 19, 79, 17, 75, 66, 30,
1115		]
1116		.into_iter()
1117		.map(Into::into)
1118		.collect_vec();
1119		assert_eq!(samples, expected);
1120	}
1121}