// Copyright (C) Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! Assignment criteria VRF generation and checking. use itertools::Itertools; use parity_scale_codec::{Decode, Encode}; use polkadot_node_primitives::approval::{ self as approval_types, v1::{AssignmentCert, AssignmentCertKind, DelayTranche, RelayVRFStory}, v2::{ AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, VrfPreOutput, VrfProof, VrfSignature, }, }; use polkadot_primitives::{ AssignmentId, AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, SessionInfo, ValidatorIndex, }; use rand::{seq::SliceRandom, SeedableRng}; use rand_chacha::ChaCha20Rng; use sc_keystore::LocalKeystore; use sp_application_crypto::ByteArray; use merlin::Transcript; use schnorrkel::vrf::VRFInOut; use std::{ cmp::min, collections::{hash_map::Entry, HashMap}, }; use super::LOG_TARGET; /// Details pertaining to our assignment on a block. #[derive(Debug, Clone, Encode, Decode, PartialEq)] pub struct OurAssignment { cert: AssignmentCertV2, tranche: DelayTranche, validator_index: ValidatorIndex, // Whether the assignment has been triggered already. triggered: bool, } impl OurAssignment { pub fn cert(&self) -> &AssignmentCertV2 { &self.cert } pub fn tranche(&self) -> DelayTranche { self.tranche } pub(crate) fn validator_index(&self) -> ValidatorIndex { self.validator_index } pub(crate) fn triggered(&self) -> bool { self.triggered } pub(crate) fn mark_triggered(&mut self) { self.triggered = true; } } impl From for OurAssignment { fn from(entry: crate::approval_db::v2::OurAssignment) -> Self { OurAssignment { cert: entry.cert, tranche: entry.tranche, validator_index: entry.validator_index, triggered: entry.triggered, } } } impl From for crate::approval_db::v2::OurAssignment { fn from(entry: OurAssignment) -> Self { Self { cert: entry.cert, tranche: entry.tranche, validator_index: entry.validator_index, triggered: entry.triggered, } } } // Combines the relay VRF story with a sample number if any. fn relay_vrf_modulo_transcript_inner( mut transcript: Transcript, relay_vrf_story: RelayVRFStory, sample: Option, ) -> Transcript { transcript.append_message(b"RC-VRF", &relay_vrf_story.0); if let Some(sample) = sample { sample.using_encoded(|s| transcript.append_message(b"sample", s)); } transcript } fn relay_vrf_modulo_transcript_v1(relay_vrf_story: RelayVRFStory, sample: u32) -> Transcript { relay_vrf_modulo_transcript_inner( Transcript::new(approval_types::v1::RELAY_VRF_MODULO_CONTEXT), relay_vrf_story, Some(sample), ) } fn relay_vrf_modulo_transcript_v2(relay_vrf_story: RelayVRFStory) -> Transcript { relay_vrf_modulo_transcript_inner( Transcript::new(approval_types::v2::RELAY_VRF_MODULO_CONTEXT), relay_vrf_story, None, ) } /// A hard upper bound on num_cores * target_checkers / num_validators const MAX_MODULO_SAMPLES: usize = 40; /// Takes the VRF output as input and returns a Vec of cores the validator is assigned /// to as a tranche0 checker. fn relay_vrf_modulo_cores( vrf_in_out: &VRFInOut, // Configuration - `relay_vrf_modulo_samples`. num_samples: u32, // Configuration - `n_cores`. max_cores: u32, ) -> Vec { let rand_chacha = ChaCha20Rng::from_seed(vrf_in_out.make_bytes::<::Seed>( approval_types::v2::CORE_RANDOMNESS_CONTEXT, )); generate_samples(rand_chacha, num_samples as usize, max_cores as usize) } /// Generates `num_samples` randomly from (0..max_cores) range /// /// Note! The algorithm can't change because validators on the other /// side won't be able to check the assignments until they update. /// This invariant is tested with `generate_samples_invariant`, so the /// tests will catch any subtle changes in the implementation of this function /// and its dependencies. fn generate_samples( mut rand_chacha: ChaCha20Rng, num_samples: usize, max_cores: usize, ) -> Vec { if num_samples as usize > MAX_MODULO_SAMPLES { gum::warn!( target: LOG_TARGET, n_cores = max_cores, num_samples, max_modulo_samples = MAX_MODULO_SAMPLES, "`num_samples` is greater than `MAX_MODULO_SAMPLES`", ); } if 2 * num_samples > max_cores { gum::debug!( target: LOG_TARGET, n_cores = max_cores, num_samples, max_modulo_samples = MAX_MODULO_SAMPLES, "Suboptimal configuration `num_samples` should be less than `n_cores` / 2", ); } let num_samples = min(MAX_MODULO_SAMPLES, min(num_samples, max_cores)); let mut random_cores = (0..max_cores as u32).map(|val| val.into()).collect::>(); let (samples, _) = random_cores.partial_shuffle(&mut rand_chacha, num_samples as usize); samples.into_iter().map(|val| *val).collect_vec() } fn relay_vrf_modulo_core(vrf_in_out: &VRFInOut, n_cores: u32) -> CoreIndex { let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::v1::CORE_RANDOMNESS_CONTEXT); // interpret as little-endian u32. let random_core = u32::from_le_bytes(bytes) % n_cores; CoreIndex(random_core) } fn relay_vrf_delay_transcript(relay_vrf_story: RelayVRFStory, core_index: CoreIndex) -> Transcript { let mut t = Transcript::new(approval_types::v1::RELAY_VRF_DELAY_CONTEXT); t.append_message(b"RC-VRF", &relay_vrf_story.0); core_index.0.using_encoded(|s| t.append_message(b"core", s)); t } fn relay_vrf_delay_tranche( vrf_in_out: &VRFInOut, num_delay_tranches: u32, zeroth_delay_tranche_width: u32, ) -> DelayTranche { let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::v1::TRANCHE_RANDOMNESS_CONTEXT); // interpret as little-endian u32 and reduce by the number of tranches. let wide_tranche = u32::from_le_bytes(bytes) % (num_delay_tranches + zeroth_delay_tranche_width); // Consolidate early results to tranche zero so tranche zero is extra wide. wide_tranche.saturating_sub(zeroth_delay_tranche_width) } fn assigned_core_transcript(core_index: CoreIndex) -> Transcript { let mut t = Transcript::new(approval_types::v1::ASSIGNED_CORE_CONTEXT); core_index.0.using_encoded(|s| t.append_message(b"core", s)); t } /// Information about the world assignments are being produced in. #[derive(Clone, Debug)] pub struct Config { /// The assignment public keys for validators. assignment_keys: Vec, /// The groups of validators assigned to each core. validator_groups: IndexedVec>, /// The number of availability cores used by the protocol during this session. n_cores: u32, /// The zeroth delay tranche width. zeroth_delay_tranche_width: u32, /// The number of samples we do of `relay_vrf_modulo`. relay_vrf_modulo_samples: u32, /// The number of delay tranches in total. n_delay_tranches: u32, } impl<'a> From<&'a SessionInfo> for Config { fn from(s: &'a SessionInfo) -> Self { Config { assignment_keys: s.assignment_keys.clone(), validator_groups: s.validator_groups.clone(), n_cores: s.n_cores, zeroth_delay_tranche_width: s.zeroth_delay_tranche_width, relay_vrf_modulo_samples: s.relay_vrf_modulo_samples, n_delay_tranches: s.n_delay_tranches, } } } /// A trait for producing and checking assignments. Used to mock. pub(crate) trait AssignmentCriteria { fn compute_assignments( &self, keystore: &LocalKeystore, relay_vrf_story: RelayVRFStory, config: &Config, leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>, enable_v2_assignments: bool, ) -> HashMap; fn check_assignment_cert( &self, claimed_core_bitfield: CoreBitfield, validator_index: ValidatorIndex, config: &Config, relay_vrf_story: RelayVRFStory, assignment: &AssignmentCertV2, // Backing groups for each "leaving core". backing_groups: Vec, ) -> Result; } pub(crate) struct RealAssignmentCriteria; impl AssignmentCriteria for RealAssignmentCriteria { fn compute_assignments( &self, keystore: &LocalKeystore, relay_vrf_story: RelayVRFStory, config: &Config, leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>, enable_v2_assignments: bool, ) -> HashMap { compute_assignments(keystore, relay_vrf_story, config, leaving_cores, enable_v2_assignments) } fn check_assignment_cert( &self, claimed_core_bitfield: CoreBitfield, validator_index: ValidatorIndex, config: &Config, relay_vrf_story: RelayVRFStory, assignment: &AssignmentCertV2, backing_groups: Vec, ) -> Result { check_assignment_cert( claimed_core_bitfield, validator_index, config, relay_vrf_story, assignment, backing_groups, ) } } /// Compute the assignments for a given block. Returns a map containing all assignments to cores in /// the block. If more than one assignment targets the given core, only the earliest assignment is /// kept. /// /// The `leaving_cores` parameter indicates all cores within the block where a candidate was /// included, as well as the group index backing those. /// /// The current description of the protocol assigns every validator to check every core. But at /// different times. The idea is that most assignments are never triggered and fall by the wayside. /// /// This will not assign to anything the local validator was part of the backing group for. pub fn compute_assignments( keystore: &LocalKeystore, relay_vrf_story: RelayVRFStory, config: &Config, leaving_cores: impl IntoIterator + Clone, enable_v2_assignments: bool, ) -> HashMap { if config.n_cores == 0 || config.assignment_keys.is_empty() || config.validator_groups.is_empty() { gum::trace!( target: LOG_TARGET, n_cores = config.n_cores, has_assignment_keys = !config.assignment_keys.is_empty(), has_validator_groups = !config.validator_groups.is_empty(), "Not producing assignments because config is degenerate", ); return HashMap::new() } let (index, assignments_key): (ValidatorIndex, AssignmentPair) = { let key = config.assignment_keys.iter().enumerate().find_map(|(i, p)| { match keystore.key_pair(p) { Ok(Some(pair)) => Some((ValidatorIndex(i as _), pair)), Ok(None) => None, Err(sc_keystore::Error::Unavailable) => None, Err(sc_keystore::Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => None, Err(e) => { gum::warn!(target: LOG_TARGET, "Encountered keystore error: {:?}", e); None }, } }); match key { None => { gum::trace!(target: LOG_TARGET, "No assignment key"); return HashMap::new() }, Some(k) => k, } }; // Ignore any cores where the assigned group is our own. let leaving_cores = leaving_cores .into_iter() .filter(|(_, _, g)| !is_in_backing_group(&config.validator_groups, index, *g)) .map(|(c_hash, core, _)| (c_hash, core)) .collect::>(); gum::trace!( target: LOG_TARGET, assignable_cores = leaving_cores.len(), "Assigning to candidates from different backing groups" ); let assignments_key: &sp_application_crypto::sr25519::Pair = assignments_key.as_ref(); let assignments_key: &schnorrkel::Keypair = assignments_key.as_ref(); let mut assignments = HashMap::new(); // First run `RelayVRFModulo` for each sample. if enable_v2_assignments { compute_relay_vrf_modulo_assignments_v2( &assignments_key, index, config, relay_vrf_story.clone(), leaving_cores.clone(), &mut assignments, ); } else { compute_relay_vrf_modulo_assignments_v1( &assignments_key, index, config, relay_vrf_story.clone(), leaving_cores.clone(), &mut assignments, ); } // Then run `RelayVRFDelay` once for the whole block. compute_relay_vrf_delay_assignments( &assignments_key, index, config, relay_vrf_story, leaving_cores, &mut assignments, ); assignments } fn compute_relay_vrf_modulo_assignments_v1( assignments_key: &schnorrkel::Keypair, validator_index: ValidatorIndex, config: &Config, relay_vrf_story: RelayVRFStory, leaving_cores: impl IntoIterator + Clone, assignments: &mut HashMap, ) { for rvm_sample in 0..config.relay_vrf_modulo_samples { let mut core = CoreIndex::default(); let maybe_assignment = { // Extra scope to ensure borrowing instead of moving core // into closure. let core = &mut core; assignments_key.vrf_sign_extra_after_check( relay_vrf_modulo_transcript_v1(relay_vrf_story.clone(), rvm_sample), |vrf_in_out| { *core = relay_vrf_modulo_core(&vrf_in_out, config.n_cores); if let Some((candidate_hash, _)) = leaving_cores.clone().into_iter().find(|(_, c)| c == core) { gum::trace!( target: LOG_TARGET, ?candidate_hash, ?core, ?validator_index, tranche = 0, "RelayVRFModulo Assignment." ); Some(assigned_core_transcript(*core)) } else { None } }, ) }; if let Some((vrf_in_out, vrf_proof, _)) = maybe_assignment { // Sanity: `core` is always initialized to non-default here, as the closure above // has been executed. let cert = AssignmentCert { kind: AssignmentCertKind::RelayVRFModulo { sample: rvm_sample }, vrf: VrfSignature { pre_output: VrfPreOutput(vrf_in_out.to_preout()), proof: VrfProof(vrf_proof), }, }; // All assignments of type RelayVRFModulo have tranche 0. assignments.entry(core).or_insert(OurAssignment { cert: cert.into(), tranche: 0, validator_index, triggered: false, }); } } } fn assigned_cores_transcript(core_bitfield: &CoreBitfield) -> Transcript { let mut t = Transcript::new(approval_types::v2::ASSIGNED_CORE_CONTEXT); core_bitfield.using_encoded(|s| t.append_message(b"cores", s)); t } fn compute_relay_vrf_modulo_assignments_v2( assignments_key: &schnorrkel::Keypair, validator_index: ValidatorIndex, config: &Config, relay_vrf_story: RelayVRFStory, leaving_cores: Vec<(CandidateHash, CoreIndex)>, assignments: &mut HashMap, ) { let mut assigned_cores = Vec::new(); let leaving_cores = leaving_cores.iter().map(|(_, core)| core).collect::>(); let maybe_assignment = { let assigned_cores = &mut assigned_cores; assignments_key.vrf_sign_extra_after_check( relay_vrf_modulo_transcript_v2(relay_vrf_story.clone()), |vrf_in_out| { *assigned_cores = relay_vrf_modulo_cores( &vrf_in_out, config.relay_vrf_modulo_samples, config.n_cores, ) .into_iter() .filter(|core| leaving_cores.contains(&core)) .collect::>(); if !assigned_cores.is_empty() { gum::trace!( target: LOG_TARGET, ?assigned_cores, ?validator_index, tranche = 0, "RelayVRFModuloCompact Assignment." ); let assignment_bitfield: CoreBitfield = assigned_cores .clone() .try_into() .expect("Just checked `!assigned_cores.is_empty()`; qed"); Some(assigned_cores_transcript(&assignment_bitfield)) } else { None } }, ) }; if let Some(assignment) = maybe_assignment.map(|(vrf_in_out, vrf_proof, _)| { let assignment_bitfield: CoreBitfield = assigned_cores .clone() .try_into() .expect("Just checked `!assigned_cores.is_empty()`; qed"); let cert = AssignmentCertV2 { kind: AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield: assignment_bitfield.clone(), }, vrf: VrfSignature { pre_output: VrfPreOutput(vrf_in_out.to_preout()), proof: VrfProof(vrf_proof), }, }; // All assignments of type RelayVRFModulo have tranche 0. OurAssignment { cert, tranche: 0, validator_index, triggered: false } }) { for core_index in assigned_cores { assignments.insert(core_index, assignment.clone()); } } } fn compute_relay_vrf_delay_assignments( assignments_key: &schnorrkel::Keypair, validator_index: ValidatorIndex, config: &Config, relay_vrf_story: RelayVRFStory, leaving_cores: impl IntoIterator, assignments: &mut HashMap, ) { for (candidate_hash, core) in leaving_cores { let (vrf_in_out, vrf_proof, _) = assignments_key.vrf_sign(relay_vrf_delay_transcript(relay_vrf_story.clone(), core)); let tranche = relay_vrf_delay_tranche( &vrf_in_out, config.n_delay_tranches, config.zeroth_delay_tranche_width, ); let cert = AssignmentCertV2 { kind: AssignmentCertKindV2::RelayVRFDelay { core_index: core }, vrf: VrfSignature { pre_output: VrfPreOutput(vrf_in_out.to_preout()), proof: VrfProof(vrf_proof), }, }; let our_assignment = OurAssignment { cert, tranche, validator_index, triggered: false }; let used = match assignments.entry(core) { Entry::Vacant(e) => { let _ = e.insert(our_assignment); true }, Entry::Occupied(mut e) => if e.get().tranche > our_assignment.tranche { e.insert(our_assignment); true } else { false }, }; if used { gum::trace!( target: LOG_TARGET, ?candidate_hash, ?core, ?validator_index, tranche, "RelayVRFDelay Assignment", ); } } } /// Assignment invalid. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InvalidAssignment(pub(crate) InvalidAssignmentReason); impl std::fmt::Display for InvalidAssignment { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Invalid Assignment: {:?}", self.0) } } impl std::error::Error for InvalidAssignment {} /// Failure conditions when checking an assignment cert. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum InvalidAssignmentReason { ValidatorIndexOutOfBounds, SampleOutOfBounds, CoreIndexOutOfBounds, InvalidAssignmentKey, IsInBackingGroup, VRFModuloCoreIndexMismatch, VRFModuloOutputMismatch, VRFDelayCoreIndexMismatch, VRFDelayOutputMismatch, InvalidArguments, /// Assignment vrf check resulted in 0 assigned cores. NullAssignment, } /// Checks the crypto of an assignment cert. Failure conditions: /// * Validator index out of bounds /// * VRF signature check fails /// * VRF output doesn't match assigned cores /// * Core is not covered by extra data in signature /// * Core index out of bounds /// * Sample is out of bounds /// * Validator is present in backing group. /// /// This function does not check whether the core is actually a valid assignment or not. That should /// be done outside the scope of this function. pub(crate) fn check_assignment_cert( claimed_core_indices: CoreBitfield, validator_index: ValidatorIndex, config: &Config, relay_vrf_story: RelayVRFStory, assignment: &AssignmentCertV2, backing_groups: Vec, ) -> Result { use InvalidAssignmentReason as Reason; let validator_public = config .assignment_keys .get(validator_index.0 as usize) .ok_or(InvalidAssignment(Reason::ValidatorIndexOutOfBounds))?; let public = schnorrkel::PublicKey::from_bytes(validator_public.as_slice()) .map_err(|_| InvalidAssignment(Reason::InvalidAssignmentKey))?; // Check that we have all backing groups for claimed cores. if claimed_core_indices.count_ones() == 0 || claimed_core_indices.count_ones() != backing_groups.len() { return Err(InvalidAssignment(Reason::InvalidArguments)) } // Check that the validator was not part of the backing group // and not already assigned. for (claimed_core, backing_group) in claimed_core_indices.iter_ones().zip(backing_groups.iter()) { if claimed_core >= config.n_cores as usize { return Err(InvalidAssignment(Reason::CoreIndexOutOfBounds)) } let is_in_backing = is_in_backing_group(&config.validator_groups, validator_index, *backing_group); if is_in_backing { return Err(InvalidAssignment(Reason::IsInBackingGroup)) } } let vrf_pre_output = &assignment.vrf.pre_output; let vrf_proof = &assignment.vrf.proof; let first_claimed_core_index = claimed_core_indices.first_one().expect("Checked above; qed") as u32; match &assignment.kind { AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => { // Check that claimed core bitfield match the one from certificate. if &claimed_core_indices != core_bitfield { return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch)) } let (vrf_in_out, _) = public .vrf_verify_extra( relay_vrf_modulo_transcript_v2(relay_vrf_story), &vrf_pre_output.0, &vrf_proof.0, assigned_cores_transcript(core_bitfield), ) .map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?; let resulting_cores = relay_vrf_modulo_cores( &vrf_in_out, config.relay_vrf_modulo_samples, config.n_cores, ); // Currently validators can opt out of checking specific cores. // This is the same issue to how validator can opt out and not send their assignments in // the first place. Ensure that the `vrf_in_out` actually includes all of the claimed // cores. for claimed_core_index in claimed_core_indices.iter_ones() { if !resulting_cores.contains(&CoreIndex(claimed_core_index as u32)) { gum::debug!( target: LOG_TARGET, ?resulting_cores, ?claimed_core_indices, vrf_modulo_cores = ?resulting_cores, "Assignment claimed cores mismatch", ); return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch)) } } Ok(0) }, AssignmentCertKindV2::RelayVRFModulo { sample } => { if *sample >= config.relay_vrf_modulo_samples { return Err(InvalidAssignment(Reason::SampleOutOfBounds)) } // Enforce claimed candidates is 1. if claimed_core_indices.count_ones() != 1 { gum::warn!( target: LOG_TARGET, ?claimed_core_indices, "`RelayVRFModulo` assignment must always claim 1 core", ); return Err(InvalidAssignment(Reason::InvalidArguments)) } let (vrf_in_out, _) = public .vrf_verify_extra( relay_vrf_modulo_transcript_v1(relay_vrf_story, *sample), &vrf_pre_output.0, &vrf_proof.0, assigned_core_transcript(CoreIndex(first_claimed_core_index)), ) .map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?; let core = relay_vrf_modulo_core(&vrf_in_out, config.n_cores); // ensure that the `vrf_in_out` actually gives us the claimed core. if core.0 == first_claimed_core_index { Ok(0) } else { gum::debug!( target: LOG_TARGET, ?core, ?claimed_core_indices, "Assignment claimed cores mismatch", ); Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch)) } }, AssignmentCertKindV2::RelayVRFDelay { core_index } => { // Enforce claimed candidates is 1. if claimed_core_indices.count_ones() != 1 { gum::debug!( target: LOG_TARGET, ?claimed_core_indices, "`RelayVRFDelay` assignment must always claim 1 core", ); return Err(InvalidAssignment(Reason::InvalidArguments)) } if core_index.0 != first_claimed_core_index { return Err(InvalidAssignment(Reason::VRFDelayCoreIndexMismatch)) } let (vrf_in_out, _) = public .vrf_verify( relay_vrf_delay_transcript(relay_vrf_story, *core_index), &vrf_pre_output.0, &vrf_proof.0, ) .map_err(|_| InvalidAssignment(Reason::VRFDelayOutputMismatch))?; Ok(relay_vrf_delay_tranche( &vrf_in_out, config.n_delay_tranches, config.zeroth_delay_tranche_width, )) }, } } fn is_in_backing_group( validator_groups: &IndexedVec>, validator: ValidatorIndex, group: GroupIndex, ) -> bool { validator_groups.get(group).map_or(false, |g| g.contains(&validator)) } /// Migration helpers. impl From for OurAssignment { fn from(value: crate::approval_db::v1::OurAssignment) -> Self { Self { cert: value.cert.into(), tranche: value.tranche, validator_index: value.validator_index, // Whether the assignment has been triggered already. triggered: value.triggered, } } } #[cfg(test)] mod tests { use super::*; use crate::import::tests::garbage_vrf_signature; use polkadot_primitives::{Hash, ASSIGNMENT_KEY_TYPE_ID}; use sp_application_crypto::sr25519; use sp_core::crypto::Pair as PairT; use sp_keyring::sr25519::Keyring as Sr25519Keyring; use sp_keystore::Keystore; // sets up a keystore with the given keyring accounts. fn make_keystore(accounts: &[Sr25519Keyring]) -> LocalKeystore { let store = LocalKeystore::in_memory(); for s in accounts.iter().copied().map(|k| k.to_seed()) { store.sr25519_generate_new(ASSIGNMENT_KEY_TYPE_ID, Some(s.as_str())).unwrap(); } store } fn assignment_keys(accounts: &[Sr25519Keyring]) -> Vec { assignment_keys_plus_random(accounts, 0) } fn assignment_keys_plus_random( accounts: &[Sr25519Keyring], random: usize, ) -> Vec { let gen_random = (0..random).map(|_| AssignmentId::from(sr25519::Pair::generate().0.public())); accounts .iter() .map(|k| AssignmentId::from(k.public())) .chain(gen_random) .collect() } fn basic_groups( n_validators: usize, n_groups: usize, ) -> IndexedVec> { let size = n_validators / n_groups; let big_groups = n_validators % n_groups; let scraps = n_groups * size; (0..n_groups) .map(|i| { (i * size..(i + 1) * size) .chain(if i < big_groups { Some(scraps + i) } else { None }) .map(|j| ValidatorIndex(j as _)) .collect::>() }) .collect() } #[test] fn assignments_produced_for_non_backing() { let keystore = make_keystore(&[Sr25519Keyring::Alice]); let c_a = CandidateHash(Hash::repeat_byte(0)); let c_b = CandidateHash(Hash::repeat_byte(1)); let relay_vrf_story = RelayVRFStory([42u8; 32]); let assignments = compute_assignments( &keystore, relay_vrf_story, &Config { assignment_keys: assignment_keys(&[ Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie, ]), validator_groups: IndexedVec::>::from(vec![ vec![ValidatorIndex(0)], vec![ValidatorIndex(1), ValidatorIndex(2)], ]), n_cores: 2, zeroth_delay_tranche_width: 10, relay_vrf_modulo_samples: 10, n_delay_tranches: 40, }, vec![(c_a, CoreIndex(0), GroupIndex(1)), (c_b, CoreIndex(1), GroupIndex(0))], false, ); // Note that alice is in group 0, which was the backing group for core 1. // Alice should have self-assigned to check core 0 but not 1. assert_eq!(assignments.len(), 1); assert!(assignments.get(&CoreIndex(0)).is_some()); } #[test] fn assign_to_nonzero_core() { let keystore = make_keystore(&[Sr25519Keyring::Alice]); let c_a = CandidateHash(Hash::repeat_byte(0)); let c_b = CandidateHash(Hash::repeat_byte(1)); let relay_vrf_story = RelayVRFStory([42u8; 32]); let assignments = compute_assignments( &keystore, relay_vrf_story, &Config { assignment_keys: assignment_keys(&[ Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie, ]), validator_groups: IndexedVec::>::from(vec![ vec![ValidatorIndex(0)], vec![ValidatorIndex(1), ValidatorIndex(2)], ]), n_cores: 2, zeroth_delay_tranche_width: 10, relay_vrf_modulo_samples: 10, n_delay_tranches: 40, }, vec![(c_a, CoreIndex(0), GroupIndex(0)), (c_b, CoreIndex(1), GroupIndex(1))], false, ); assert_eq!(assignments.len(), 1); assert!(assignments.get(&CoreIndex(1)).is_some()); } #[test] fn succeeds_empty_for_0_cores() { let keystore = make_keystore(&[Sr25519Keyring::Alice]); let relay_vrf_story = RelayVRFStory([42u8; 32]); let assignments = compute_assignments( &keystore, relay_vrf_story, &Config { assignment_keys: assignment_keys(&[ Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie, ]), validator_groups: Default::default(), n_cores: 0, zeroth_delay_tranche_width: 10, relay_vrf_modulo_samples: 10, n_delay_tranches: 40, }, vec![], false, ); assert!(assignments.is_empty()); } #[derive(Debug)] struct MutatedAssignment { cores: CoreBitfield, cert: AssignmentCertV2, groups: Vec, own_group: GroupIndex, val_index: ValidatorIndex, config: Config, } // This fails if the closure requests to skip everything. fn check_mutated_assignments( n_validators: usize, n_cores: usize, rotation_offset: usize, f: impl Fn(&mut MutatedAssignment) -> Option, // None = skip ) { let keystore = make_keystore(&[Sr25519Keyring::Alice]); let group_for_core = |i| GroupIndex(((i + rotation_offset) % n_cores) as _); let config = Config { assignment_keys: assignment_keys_plus_random( &[Sr25519Keyring::Alice], n_validators - 1, ), validator_groups: basic_groups(n_validators, n_cores), n_cores: n_cores as u32, zeroth_delay_tranche_width: 10, relay_vrf_modulo_samples: 15, n_delay_tranches: 40, }; let relay_vrf_story = RelayVRFStory([42u8; 32]); let mut assignments = compute_assignments( &keystore, relay_vrf_story.clone(), &config, (0..n_cores) .map(|i| { ( CandidateHash(Hash::repeat_byte(i as u8)), CoreIndex(i as u32), group_for_core(i), ) }) .collect::>(), false, ); // Extend with v2 assignments as well assignments.extend(compute_assignments( &keystore, relay_vrf_story.clone(), &config, (0..n_cores) .map(|i| { ( CandidateHash(Hash::repeat_byte(i as u8)), CoreIndex(i as u32), group_for_core(i), ) }) .collect::>(), true, )); let mut counted = 0; for (core, assignment) in assignments { let cores = match assignment.cert.kind.clone() { AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => core_bitfield, AssignmentCertKindV2::RelayVRFModulo { sample: _ } => core.into(), AssignmentCertKindV2::RelayVRFDelay { core_index } => core_index.into(), }; let mut mutated = MutatedAssignment { cores: cores.clone(), groups: cores.iter_ones().map(|core| group_for_core(core)).collect(), cert: assignment.cert, own_group: GroupIndex(0), val_index: ValidatorIndex(0), config: config.clone(), }; let expected = match f(&mut mutated) { None => continue, Some(e) => e, }; counted += 1; let is_good = check_assignment_cert( mutated.cores, mutated.val_index, &mutated.config, relay_vrf_story.clone(), &mutated.cert, mutated.groups, ) .is_ok(); assert_eq!(expected, is_good); } assert!(counted > 0); } #[test] fn computed_assignments_pass_checks() { check_mutated_assignments(200, 100, 25, |_| Some(true)); } #[test] fn check_rejects_claimed_core_out_of_bounds() { check_mutated_assignments(200, 100, 25, |m| { m.cores = CoreIndex(100).into(); Some(false) }); } #[test] fn check_rejects_in_backing_group() { check_mutated_assignments(200, 100, 25, |m| { m.groups[0] = m.own_group; Some(false) }); } #[test] fn check_rejects_nonexistent_key() { check_mutated_assignments(200, 100, 25, |m| { m.val_index.0 += 200; Some(false) }); } #[test] fn check_rejects_delay_bad_vrf() { check_mutated_assignments(40, 100, 8, |m| { let vrf_signature = garbage_vrf_signature(); match m.cert.kind.clone() { AssignmentCertKindV2::RelayVRFDelay { .. } => { m.cert.vrf = vrf_signature; Some(false) }, _ => None, // skip everything else. } }); } #[test] fn check_rejects_modulo_bad_vrf() { check_mutated_assignments(200, 100, 25, |m| { let vrf_signature = garbage_vrf_signature(); match m.cert.kind.clone() { AssignmentCertKindV2::RelayVRFModulo { .. } => { m.cert.vrf = vrf_signature; Some(false) }, AssignmentCertKindV2::RelayVRFModuloCompact { .. } => { m.cert.vrf = vrf_signature; Some(false) }, _ => None, // skip everything else. } }); } #[test] fn check_rejects_modulo_sample_out_of_bounds() { check_mutated_assignments(200, 100, 25, |m| { match m.cert.kind.clone() { AssignmentCertKindV2::RelayVRFModulo { sample } => { m.config.relay_vrf_modulo_samples = sample; Some(false) }, AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield: _ } => Some(true), _ => None, // skip everything else. } }); } #[test] fn check_rejects_delay_claimed_core_wrong() { check_mutated_assignments(200, 100, 25, |m| { match m.cert.kind.clone() { AssignmentCertKindV2::RelayVRFDelay { .. } => { // for core in &mut m.cores { // core.0 = (core.0 + 1) % 100; // } m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into(); Some(false) }, _ => None, // skip everything else. } }); } #[test] fn check_rejects_modulo_core_wrong() { check_mutated_assignments(200, 100, 25, |m| { match m.cert.kind.clone() { AssignmentCertKindV2::RelayVRFModulo { .. } | AssignmentCertKindV2::RelayVRFModuloCompact { .. } => { m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into(); Some(false) }, _ => None, // skip everything else. } }); } #[test] fn generate_samples_invariant() { let seed = [ 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, 0, 0, 0, 2, 92, ]; let rand_chacha = ChaCha20Rng::from_seed(seed); let samples = generate_samples(rand_chacha.clone(), 6, 100); let expected = vec![19, 79, 17, 75, 66, 30].into_iter().map(Into::into).collect_vec(); assert_eq!(samples, expected); let samples = generate_samples(rand_chacha.clone(), 6, 7); let expected = vec![0, 3, 6, 5, 4, 2].into_iter().map(Into::into).collect_vec(); assert_eq!(samples, expected); let samples = generate_samples(rand_chacha.clone(), 6, 12); let expected = vec![2, 4, 7, 5, 11, 3].into_iter().map(Into::into).collect_vec(); assert_eq!(samples, expected); let samples = generate_samples(rand_chacha.clone(), 1, 100); let expected = vec![30].into_iter().map(Into::into).collect_vec(); assert_eq!(samples, expected); let samples = generate_samples(rand_chacha.clone(), 0, 100); let expected = vec![]; assert_eq!(samples, expected); let samples = generate_samples(rand_chacha, MAX_MODULO_SAMPLES + 1, 100); let expected = vec![ 42, 54, 55, 93, 64, 27, 49, 15, 83, 71, 62, 1, 43, 77, 97, 41, 7, 69, 0, 88, 59, 14, 23, 87, 47, 4, 51, 12, 74, 56, 50, 44, 9, 82, 19, 79, 17, 75, 66, 30, ] .into_iter() .map(Into::into) .collect_vec(); assert_eq!(samples, expected); } }