mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-15 18:31:05 +00:00
e48c687504
* skeleton * skeleton aux-schema module * start approval types * start aux schema with aux store * doc * finish basic types * start approval types * doc * finish basic types * write out schema types * add debug and codec impls to approval types * add debug and codec impls to approval types also add some key computation * add debug and codec impls to approval types * getters for block and candidate entries * grumbles * remove unused AssignmentId * load_decode utility * implement DB clearing * function for adding new block entry to aux store * start `canonicalize` implementation * more skeleton * finish implementing canonicalize * tag TODO * implement a test AuxStore * add allow(unused) * basic loading and deleting test * block_entry test function * add a test for `add_block_entry` * ensure range is exclusive at end * test clear() * test that add_block sets children * add a test for canonicalize * extract Pre-digest from header * utilities for extracting RelayVRFStory from the header-chain * add approval voting message types * approval distribution message type * subsystem skeleton * state struct * add futures-timer * prepare service for babe slot duration * more skeleton * better integrate AuxStore * RelayVRF -> RelayVRFStory * canonicalize * implement some tick functionality * guide: tweaks * check_approval * more tweaks and helpers * guide: add core index to candidate event * primitives: add core index to candidate event * runtime: add core index to candidate events * head handling (session window) * implement `determine_new_blocks` * add TODO * change error type on functions * compute RelayVRFModulo assignments * compute RelayVRFDelay assignments * fix delay tranche calc * assignment checking * pluralize * some dummy code for fetching assignments * guide: add babe epoch runtime API * implement a current_epoch() runtime API * compute assignments * candidate events get backing group * import blocks and assignments into DB * push block approval meta * add message types, no overseer integration yet * notify approval distribution of new blocks * refactor import into separate functions * impl tranches_to_approve * guide: improve function signatures * guide: remove Tick from ApprovalEntry * trigger and broadcast assignment * most of approval launching * remove byteorder crate * load blocks back to finality, except on startup * check unchecked assignments * add claimed core to approval voting message * fix checks * assign only to backing group * remove import_checked_assignment from guide * newline * import assignments * abstract out a bit * check and import approvals * check full approvals from assignment import too * comment * create a Transaction utility * must_use * use transaction in `check_full_approvals` * wire up wakeups * add Ord to CandidateHash * wakeup refactoring * return candidate info from add_block_entry * schedule wakeups * background task: do candidate validation * forward candidate validation requests * issue approval votes when requested * clean up a couple TODOs * fix up session caching * clean up last unimplemented!() items * fix remaining warnings * remove TODO * implement handle_approved_ancestor * update Cargo.lock * fix runtime API tests * guide: cleanup assignment checking * use claimed candidate index instead of core * extract time to a trait * tests module * write a mock clock for testing * allow swapping out the clock * make abstract over assignment criteria * add some skeleton tests and simplify params * fix backing group check * do backing group check inside check_assignment_cert * write some empty test functions to implement * add a test for non-backing * test that produced checks pass * some empty test ideas * runtime/inclusion: remove outdated TODO * fix compilation * av-store: fix tests * dummy cert * criteria tests * move `TestStore` to main tests file * fix unused warning * test harness beginnings * resolve slots renaming fallout * more compilation fixes * wip: extract pure data into a separate module * wip: extract pure data into a separate module * move types completely to v1 * add persisted_entries * add conversion trait impls * clean up some warnings * extract import logic to own module * schedule wakeups * experiment with Actions * uncomment approval-checking * separate module for approval checking utilities * port more code to use actions * get approval pipeline using actions * all logic is uncommented * main loop processes actions * all loop logic uncommented * separate function for handling actions * remove last unimplemented item * clean up warnings * State gives read-only access to underlying DB * tests for approval checking * tests for approval criteria * skeleton test module for import * list of import tests to do * some test glue code * test reject bad assignment * test slot too far in future * test reject assignment with unknown candidate * remove loads_blocks tests * determine_new_blocks back to finalized & harness * more coverage for determining new blocks * make `imported_block_info` have less reliance on State * candidate_info tests * tests for session caching * remove println * extricate DB and main TestStores * rewrite approval checking logic to counteract early delays * move state out of function * update approval-checking tests * tweak wakeups & scheduling logic * rename check_full_approvals * test that assignment import updates candidate * some approval import tests * some tests for check_and_apply_approval * add 'full' qualifier to avoid confusion * extract should-trigger logic to separate function * some tests for all triggering * tests for when we trigger assignments * test wakeups * add block utilities for testing * some more tests for approval updates * approved_ancestor tests * new action type for launch approval * process-wakeup tests * clean up some warnings * fix in_future test * approval checking tests * tighten up too-far-in-future * special-case genesis when caching sessions * fix bitfield len Co-authored-by: Andronik Ordian <write@reusable.software>
783 lines
21 KiB
Rust
783 lines
21 KiB
Rust
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
|
|
//! Assignment criteria VRF generation and checking.
|
|
|
|
use polkadot_node_primitives::approval::{
|
|
self as approval_types, AssignmentCert, AssignmentCertKind, DelayTranche, RelayVRFStory,
|
|
};
|
|
use polkadot_primitives::v1::{
|
|
CoreIndex, ValidatorIndex, SessionInfo, AssignmentPair, AssignmentId, GroupIndex,
|
|
};
|
|
use sc_keystore::LocalKeystore;
|
|
use parity_scale_codec::{Encode, Decode};
|
|
use sp_application_crypto::Public;
|
|
|
|
use merlin::Transcript;
|
|
use schnorrkel::vrf::VRFInOut;
|
|
|
|
use std::collections::HashMap;
|
|
use std::collections::hash_map::Entry;
|
|
|
|
use super::LOG_TARGET;
|
|
|
|
/// Details pertaining to our assignment on a block.
|
|
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
|
|
pub struct OurAssignment {
|
|
cert: AssignmentCert,
|
|
tranche: DelayTranche,
|
|
validator_index: ValidatorIndex,
|
|
// Whether the assignment has been triggered already.
|
|
triggered: bool,
|
|
}
|
|
|
|
impl OurAssignment {
|
|
pub(crate) fn cert(&self) -> &AssignmentCert {
|
|
&self.cert
|
|
}
|
|
|
|
pub(crate) 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<crate::approval_db::v1::OurAssignment> for OurAssignment {
|
|
fn from(entry: crate::approval_db::v1::OurAssignment) -> Self {
|
|
OurAssignment {
|
|
cert: entry.cert,
|
|
tranche: entry.tranche,
|
|
validator_index: entry.validator_index,
|
|
triggered: entry.triggered,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<OurAssignment> for crate::approval_db::v1::OurAssignment {
|
|
fn from(entry: OurAssignment) -> Self {
|
|
Self {
|
|
cert: entry.cert,
|
|
tranche: entry.tranche,
|
|
validator_index: entry.validator_index,
|
|
triggered: entry.triggered,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn relay_vrf_modulo_transcript(
|
|
relay_vrf_story: RelayVRFStory,
|
|
sample: u32,
|
|
) -> Transcript {
|
|
// combine the relay VRF story with a sample number.
|
|
let mut t = Transcript::new(approval_types::RELAY_VRF_MODULO_CONTEXT);
|
|
t.append_message(b"RC-VRF", &relay_vrf_story.0);
|
|
sample.using_encoded(|s| t.append_message(b"sample", s));
|
|
|
|
t
|
|
}
|
|
|
|
fn relay_vrf_modulo_core(
|
|
vrf_in_out: &VRFInOut,
|
|
n_cores: u32,
|
|
) -> CoreIndex {
|
|
let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::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::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::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::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)]
|
|
pub(crate) struct Config {
|
|
/// The assignment public keys for validators.
|
|
assignment_keys: Vec<AssignmentId>,
|
|
/// The groups of validators assigned to each core.
|
|
validator_groups: Vec<Vec<ValidatorIndex>>,
|
|
/// 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.clone(),
|
|
zeroth_delay_tranche_width: s.zeroth_delay_tranche_width.clone(),
|
|
relay_vrf_modulo_samples: s.relay_vrf_modulo_samples.clone(),
|
|
n_delay_tranches: s.n_delay_tranches.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<(CoreIndex, GroupIndex)>,
|
|
) -> HashMap<CoreIndex, OurAssignment>;
|
|
|
|
fn check_assignment_cert(
|
|
&self,
|
|
claimed_core_index: CoreIndex,
|
|
validator_index: ValidatorIndex,
|
|
config: &Config,
|
|
relay_vrf_story: RelayVRFStory,
|
|
assignment: &AssignmentCert,
|
|
backing_group: GroupIndex,
|
|
) -> Result<DelayTranche, InvalidAssignment>;
|
|
}
|
|
|
|
pub(crate) struct RealAssignmentCriteria;
|
|
|
|
impl AssignmentCriteria for RealAssignmentCriteria {
|
|
fn compute_assignments(
|
|
&self,
|
|
keystore: &LocalKeystore,
|
|
relay_vrf_story: RelayVRFStory,
|
|
config: &Config,
|
|
leaving_cores: Vec<(CoreIndex, GroupIndex)>,
|
|
) -> HashMap<CoreIndex, OurAssignment> {
|
|
compute_assignments(
|
|
keystore,
|
|
relay_vrf_story,
|
|
config,
|
|
leaving_cores,
|
|
)
|
|
}
|
|
|
|
fn check_assignment_cert(
|
|
&self,
|
|
claimed_core_index: CoreIndex,
|
|
validator_index: ValidatorIndex,
|
|
config: &Config,
|
|
relay_vrf_story: RelayVRFStory,
|
|
assignment: &AssignmentCert,
|
|
backing_group: GroupIndex,
|
|
) -> Result<DelayTranche, InvalidAssignment> {
|
|
check_assignment_cert(
|
|
claimed_core_index,
|
|
validator_index,
|
|
config,
|
|
relay_vrf_story,
|
|
assignment,
|
|
backing_group,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// 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(crate) fn compute_assignments(
|
|
keystore: &LocalKeystore,
|
|
relay_vrf_story: RelayVRFStory,
|
|
config: &Config,
|
|
leaving_cores: impl IntoIterator<Item = (CoreIndex, GroupIndex)> + Clone,
|
|
) -> HashMap<CoreIndex, OurAssignment> {
|
|
let (index, assignments_key): (ValidatorIndex, AssignmentPair) = {
|
|
let key = config.assignment_keys.iter().enumerate()
|
|
.filter_map(|(i, p)| match keystore.key_pair(p) {
|
|
Ok(pair) => Some((i as ValidatorIndex, pair)),
|
|
Err(sc_keystore::Error::PairNotFound(_)) => None,
|
|
Err(e) => {
|
|
tracing::warn!(target: LOG_TARGET, "Encountered keystore error: {:?}", e);
|
|
None
|
|
}
|
|
})
|
|
.next();
|
|
|
|
match key {
|
|
None => return Default::default(),
|
|
Some(k) => k,
|
|
}
|
|
};
|
|
|
|
// Ignore any cores where the assigned group is our own.
|
|
let leaving_cores = leaving_cores.into_iter()
|
|
.filter(|&(_, ref g)| !is_in_backing_group(&config.validator_groups, index, *g))
|
|
.map(|(c, _)| c)
|
|
.collect::<Vec<_>>();
|
|
|
|
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.
|
|
compute_relay_vrf_modulo_assignments(
|
|
&assignments_key,
|
|
index,
|
|
config,
|
|
relay_vrf_story.clone(),
|
|
leaving_cores.iter().cloned(),
|
|
&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(
|
|
assignments_key: &schnorrkel::Keypair,
|
|
validator_index: ValidatorIndex,
|
|
config: &Config,
|
|
relay_vrf_story: RelayVRFStory,
|
|
leaving_cores: impl IntoIterator<Item = CoreIndex> + Clone,
|
|
assignments: &mut HashMap<CoreIndex, OurAssignment>,
|
|
) {
|
|
for rvm_sample in 0..config.relay_vrf_modulo_samples {
|
|
let mut core = Default::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(relay_vrf_story.clone(), rvm_sample),
|
|
|vrf_in_out| {
|
|
*core = relay_vrf_modulo_core(&vrf_in_out, config.n_cores);
|
|
if leaving_cores.clone().into_iter().any(|c| c == *core) {
|
|
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: (approval_types::VRFOutput(vrf_in_out.to_output()), approval_types::VRFProof(vrf_proof)),
|
|
};
|
|
|
|
// All assignments of type RelayVRFModulo have tranche 0.
|
|
assignments.entry(core).or_insert(OurAssignment {
|
|
cert,
|
|
tranche: 0,
|
|
validator_index,
|
|
triggered: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compute_relay_vrf_delay_assignments(
|
|
assignments_key: &schnorrkel::Keypair,
|
|
validator_index: ValidatorIndex,
|
|
config: &Config,
|
|
relay_vrf_story: RelayVRFStory,
|
|
leaving_cores: impl IntoIterator<Item = CoreIndex>,
|
|
assignments: &mut HashMap<CoreIndex, OurAssignment>,
|
|
) {
|
|
for 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 = AssignmentCert {
|
|
kind: AssignmentCertKind::RelayVRFDelay { core_index: core },
|
|
vrf: (approval_types::VRFOutput(vrf_in_out.to_output()), approval_types::VRFProof(vrf_proof)),
|
|
};
|
|
|
|
let our_assignment = OurAssignment {
|
|
cert,
|
|
tranche,
|
|
validator_index,
|
|
triggered: false,
|
|
};
|
|
|
|
match assignments.entry(core) {
|
|
Entry::Vacant(e) => { let _ = e.insert(our_assignment); }
|
|
Entry::Occupied(mut e) => if e.get().tranche > our_assignment.tranche {
|
|
e.insert(our_assignment);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Assignment invalid.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct InvalidAssignment;
|
|
|
|
impl std::fmt::Display for InvalidAssignment {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
write!(f, "Invalid Assignment")
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for InvalidAssignment { }
|
|
|
|
/// Checks the crypto of an assignment cert. Failure conditions:
|
|
/// * Validator index out of bounds
|
|
/// * VRF signature check fails
|
|
/// * VRF output doesn't match assigned core
|
|
/// * 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 of the scope of this function.
|
|
pub(crate) fn check_assignment_cert(
|
|
claimed_core_index: CoreIndex,
|
|
validator_index: ValidatorIndex,
|
|
config: &Config,
|
|
relay_vrf_story: RelayVRFStory,
|
|
assignment: &AssignmentCert,
|
|
backing_group: GroupIndex,
|
|
) -> Result<DelayTranche, InvalidAssignment> {
|
|
let validator_public = config.assignment_keys
|
|
.get(validator_index as usize)
|
|
.ok_or(InvalidAssignment)?;
|
|
|
|
let public = schnorrkel::PublicKey::from_bytes(validator_public.as_slice())
|
|
.map_err(|_| InvalidAssignment)?;
|
|
|
|
if claimed_core_index.0 >= config.n_cores {
|
|
return Err(InvalidAssignment);
|
|
}
|
|
|
|
// Check that the validator was not part of the backing group
|
|
// and not already assigned.
|
|
let is_in_backing = is_in_backing_group(
|
|
&config.validator_groups,
|
|
validator_index,
|
|
backing_group,
|
|
);
|
|
|
|
if is_in_backing {
|
|
return Err(InvalidAssignment);
|
|
}
|
|
|
|
let &(ref vrf_output, ref vrf_proof) = &assignment.vrf;
|
|
match assignment.kind {
|
|
AssignmentCertKind::RelayVRFModulo { sample } => {
|
|
if sample >= config.relay_vrf_modulo_samples {
|
|
return Err(InvalidAssignment);
|
|
}
|
|
|
|
let (vrf_in_out, _) = public.vrf_verify_extra(
|
|
relay_vrf_modulo_transcript(relay_vrf_story, sample),
|
|
&vrf_output.0,
|
|
&vrf_proof.0,
|
|
assigned_core_transcript(claimed_core_index),
|
|
).map_err(|_| InvalidAssignment)?;
|
|
|
|
// ensure that the `vrf_in_out` actually gives us the claimed core.
|
|
if relay_vrf_modulo_core(&vrf_in_out, config.n_cores) == claimed_core_index {
|
|
Ok(0)
|
|
} else {
|
|
Err(InvalidAssignment)
|
|
}
|
|
}
|
|
AssignmentCertKind::RelayVRFDelay { core_index } => {
|
|
if core_index != claimed_core_index {
|
|
return Err(InvalidAssignment);
|
|
}
|
|
|
|
let (vrf_in_out, _) = public.vrf_verify(
|
|
relay_vrf_delay_transcript(relay_vrf_story, core_index),
|
|
&vrf_output.0,
|
|
&vrf_proof.0,
|
|
).map_err(|_| InvalidAssignment)?;
|
|
|
|
Ok(relay_vrf_delay_tranche(
|
|
&vrf_in_out,
|
|
config.n_delay_tranches,
|
|
config.zeroth_delay_tranche_width,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_in_backing_group(
|
|
validator_groups: &[Vec<ValidatorIndex>],
|
|
validator: ValidatorIndex,
|
|
group: GroupIndex,
|
|
) -> bool {
|
|
validator_groups.get(group.0 as usize).map_or(false, |g| g.contains(&validator))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use sp_keystore::CryptoStore;
|
|
use sp_keyring::sr25519::Keyring as Sr25519Keyring;
|
|
use sp_application_crypto::sr25519;
|
|
use sp_core::crypto::Pair as PairT;
|
|
use polkadot_primitives::v1::ASSIGNMENT_KEY_TYPE_ID;
|
|
use polkadot_node_primitives::approval::{VRFOutput, VRFProof};
|
|
|
|
// sets up a keystore with the given keyring accounts.
|
|
async 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()),
|
|
).await.unwrap();
|
|
}
|
|
|
|
store
|
|
}
|
|
|
|
fn assignment_keys(accounts: &[Sr25519Keyring]) -> Vec<AssignmentId> {
|
|
assignment_keys_plus_random(accounts, 0)
|
|
}
|
|
|
|
fn assignment_keys_plus_random(accounts: &[Sr25519Keyring], random: usize) -> Vec<AssignmentId> {
|
|
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) -> Vec<Vec<ValidatorIndex>> {
|
|
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| j as ValidatorIndex)
|
|
.collect::<Vec<_>>()
|
|
}).collect()
|
|
}
|
|
|
|
// used for generating assignments where the validity of the VRF doesn't matter.
|
|
fn garbage_vrf() -> (VRFOutput, VRFProof) {
|
|
let key = Sr25519Keyring::Alice.pair();
|
|
let key: &schnorrkel::Keypair = key.as_ref();
|
|
|
|
let (o, p, _) = key.vrf_sign(Transcript::new(b"test-garbage"));
|
|
(VRFOutput(o.to_output()), VRFProof(p))
|
|
}
|
|
|
|
#[test]
|
|
fn assignments_produced_for_non_backing() {
|
|
let keystore = futures::executor::block_on(
|
|
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: vec![vec![0], vec![1, 2]],
|
|
n_cores: 2,
|
|
zeroth_delay_tranche_width: 10,
|
|
relay_vrf_modulo_samples: 3,
|
|
n_delay_tranches: 40,
|
|
},
|
|
vec![(CoreIndex(0), GroupIndex(1)), (CoreIndex(1), GroupIndex(0))],
|
|
);
|
|
|
|
// 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 = futures::executor::block_on(
|
|
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: vec![vec![0], vec![1, 2]],
|
|
n_cores: 2,
|
|
zeroth_delay_tranche_width: 10,
|
|
relay_vrf_modulo_samples: 3,
|
|
n_delay_tranches: 40,
|
|
},
|
|
vec![(CoreIndex(0), GroupIndex(0)), (CoreIndex(1), GroupIndex(1))],
|
|
);
|
|
|
|
assert_eq!(assignments.len(), 1);
|
|
assert!(assignments.get(&CoreIndex(1)).is_some());
|
|
}
|
|
|
|
struct MutatedAssignment {
|
|
core: CoreIndex,
|
|
cert: AssignmentCert,
|
|
group: GroupIndex,
|
|
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<bool>, // None = skip
|
|
) {
|
|
let keystore = futures::executor::block_on(
|
|
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: 3,
|
|
n_delay_tranches: 40,
|
|
};
|
|
|
|
let relay_vrf_story = RelayVRFStory([42u8; 32]);
|
|
let assignments = compute_assignments(
|
|
&keystore,
|
|
relay_vrf_story.clone(),
|
|
&config,
|
|
(0..n_cores)
|
|
.map(|i| (
|
|
CoreIndex(i as u32),
|
|
group_for_core(i),
|
|
))
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
|
|
let mut counted = 0;
|
|
for (core, assignment) in assignments {
|
|
let mut mutated = MutatedAssignment {
|
|
core,
|
|
group: group_for_core(core.0 as _),
|
|
cert: assignment.cert,
|
|
own_group: GroupIndex(0),
|
|
val_index: 0,
|
|
config: config.clone(),
|
|
};
|
|
|
|
let expected = match f(&mut mutated) {
|
|
None => continue,
|
|
Some(e) => e,
|
|
};
|
|
|
|
counted += 1;
|
|
|
|
let is_good = check_assignment_cert(
|
|
mutated.core,
|
|
mutated.val_index,
|
|
&mutated.config,
|
|
relay_vrf_story.clone(),
|
|
&mutated.cert,
|
|
mutated.group,
|
|
).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.core.0 += 100;
|
|
Some(false)
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn check_rejects_in_backing_group() {
|
|
check_mutated_assignments(200, 100, 25, |m| {
|
|
m.group = m.own_group;
|
|
Some(false)
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn check_rejects_nonexistent_key() {
|
|
check_mutated_assignments(200, 100, 25, |m| {
|
|
m.val_index += 200;
|
|
Some(false)
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn check_rejects_delay_bad_vrf() {
|
|
check_mutated_assignments(40, 10, 8, |m| {
|
|
match m.cert.kind.clone() {
|
|
AssignmentCertKind::RelayVRFDelay { .. } => {
|
|
m.cert.vrf = garbage_vrf();
|
|
Some(false)
|
|
}
|
|
_ => None, // skip everything else.
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn check_rejects_modulo_bad_vrf() {
|
|
check_mutated_assignments(200, 100, 25, |m| {
|
|
match m.cert.kind.clone() {
|
|
AssignmentCertKind::RelayVRFModulo { .. } => {
|
|
m.cert.vrf = garbage_vrf();
|
|
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() {
|
|
AssignmentCertKind::RelayVRFModulo { sample } => {
|
|
m.config.relay_vrf_modulo_samples = sample;
|
|
Some(false)
|
|
}
|
|
_ => None, // skip everything else.
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn check_rejects_delay_claimed_core_wrong() {
|
|
check_mutated_assignments(200, 100, 25, |m| {
|
|
match m.cert.kind.clone() {
|
|
AssignmentCertKind::RelayVRFDelay { .. } => {
|
|
m.core = CoreIndex((m.core.0 + 1) % 100);
|
|
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() {
|
|
AssignmentCertKind::RelayVRFModulo { .. } => {
|
|
m.core = CoreIndex((m.core.0 + 1) % 100);
|
|
Some(false)
|
|
}
|
|
_ => None, // skip everything else.
|
|
}
|
|
});
|
|
}
|
|
}
|