Files
pezkuwi-subxt/polkadot/node/core/approval-voting/src/criteria.rs
T
Andrei Sandu 0570b6fa9e approval-voting improvement: include all tranche0 assignments in one certificate (#1178)
**_PR migrated from https://github.com/paritytech/polkadot/pull/6782_** 

This PR will upgrade the network protocol to version 3 -> VStaging which
will later be renamed to V3. This version introduces a new kind of
assignment certificate that will be used for tranche0 assignments.
Instead of issuing/importing one tranche0 assignment per candidate,
there will be just one certificate per relay chain block per validator.
However, we will not be sending out the new assignment certificates,
yet. So everything should work exactly as before. Once the majority of
the validators have been upgraded to the new protocol version we will
enable the new certificates (starting at a specific relay chain block)
with a new client update.

There are still a few things that need to be done:

- [x] Use bitfield instead of Vec<CandidateIndex>:
https://github.com/paritytech/polkadot/pull/6802
  - [x] Fix existing approval-distribution and approval-voting tests
  - [x] Fix bitfield-distribution and statement-distribution tests
  - [x] Fix network bridge tests
  - [x] Implement todos in the code
  - [x] Add tests to cover new code
  - [x] Update metrics
  - [x] Remove the approval distribution aggression levels: TBD PR
  - [x] Parachains DB migration 
  - [x] Test network protocol upgrade on Versi
  - [x] Versi Load test
  - [x] Add Zombienet test
  - [x] Documentation updates
- [x] Fix for sending DistributeAssignment for each candidate claimed by
a v2 assignment (warning: Importing locally an already known assignment)
 - [x]  Fix AcceptedDuplicate
 - [x] Fix DB migration so that we can still keep old data.
 - [x] Final Versi burn in

---------

Signed-off-by: Andrei Sandu <andrei-mihail@parity.io>
Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
Co-authored-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
2023-11-06 15:21:32 +02:00

1234 lines
34 KiB
Rust

// 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 <http://www.gnu.org/licenses/>.
//! 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, VrfOutput, 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(crate) fn cert(&self) -> &AssignmentCertV2 {
&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::v2::OurAssignment> 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<OurAssignment> 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<u32>,
) -> 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<CoreIndex> {
let rand_chacha =
ChaCha20Rng::from_seed(vrf_in_out.make_bytes::<<ChaCha20Rng as SeedableRng>::Seed>(
approval_types::v2::CORE_RANDOMNESS_CONTEXT,
));
generate_samples(rand_chacha, num_samples as usize, max_cores as usize)
}
/// Generates `num_sumples` 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<CoreIndex> {
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::<Vec<CoreIndex>>();
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(crate) struct Config {
/// The assignment public keys for validators.
assignment_keys: Vec<AssignmentId>,
/// The groups of validators assigned to each core.
validator_groups: IndexedVec<GroupIndex, 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,
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)>,
) -> HashMap<CoreIndex, OurAssignment>;
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<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<(CandidateHash, CoreIndex, GroupIndex)>,
) -> HashMap<CoreIndex, OurAssignment> {
compute_assignments(keystore, relay_vrf_story, config, leaving_cores, false)
}
fn check_assignment_cert(
&self,
claimed_core_bitfield: CoreBitfield,
validator_index: ValidatorIndex,
config: &Config,
relay_vrf_story: RelayVRFStory,
assignment: &AssignmentCertV2,
backing_groups: Vec<GroupIndex>,
) -> Result<DelayTranche, InvalidAssignment> {
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(crate) fn compute_assignments(
keystore: &LocalKeystore,
relay_vrf_story: RelayVRFStory,
config: &Config,
leaving_cores: impl IntoIterator<Item = (CandidateHash, CoreIndex, GroupIndex)> + Clone,
enable_v2_assignments: bool,
) -> HashMap<CoreIndex, OurAssignment> {
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::<Vec<_>>();
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<Item = (CandidateHash, CoreIndex)> + Clone,
assignments: &mut HashMap<CoreIndex, OurAssignment>,
) {
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 {
output: VrfOutput(vrf_in_out.to_output()),
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<CoreIndex, OurAssignment>,
) {
let mut assigned_cores = Vec::new();
let leaving_cores = leaving_cores.iter().map(|(_, core)| core).collect::<Vec<_>>();
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::<Vec<CoreIndex>>();
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 {
output: VrfOutput(vrf_in_out.to_output()),
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<Item = (CandidateHash, CoreIndex)>,
assignments: &mut HashMap<CoreIndex, OurAssignment>,
) {
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 {
output: VrfOutput(vrf_in_out.to_output()),
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<GroupIndex>,
) -> Result<DelayTranche, InvalidAssignment> {
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_output = &assignment.vrf.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_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_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_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<GroupIndex, Vec<ValidatorIndex>>,
validator: ValidatorIndex,
group: GroupIndex,
) -> bool {
validator_groups.get(group).map_or(false, |g| g.contains(&validator))
}
/// Migration helpers.
impl From<crate::approval_db::v1::OurAssignment> 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<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,
) -> IndexedVec<GroupIndex, 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| ValidatorIndex(j as _))
.collect::<Vec<_>>()
})
.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::<GroupIndex, Vec<ValidatorIndex>>::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::<GroupIndex, Vec<ValidatorIndex>>::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<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 = 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::<Vec<_>>(),
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::<Vec<_>>(),
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);
}
}