Runtime: allow backing multiple candidates of same parachain on different cores (#3231)

Fixes https://github.com/paritytech/polkadot-sdk/issues/3144

Builds on top of https://github.com/paritytech/polkadot-sdk/pull/3229

### Summary
Some preparations for Runtime to support elastic scaling, guarded by
config node features bit `FeatureIndex::ElasticScalingMVP`. This PR
introduces a per-candidate `CoreIndex` but does it in a hacky way to
avoid changing `CandidateCommitments`, `CandidateReceipts` primitives
and networking protocols.

#### Including `CoreIndex` in `BackedCandidate`
If the `ElasticScalingMVP` feature bit is enabled then
`BackedCandidate::validator_indices` is extended by 8 bits.
The value stored in these bits represents the assumed core index for the
candidate.

It is temporary solution which works by creating a mapping from
`BackedCandidate` to `CoreIndex` by assuming the `CoreIndex` can be
discovered by checking in which validator group the validator that
signed the statement is.

TODO:
- [x] fix tests
- [x] add new tests
- [x] Bump runtime API for Kusama, so we have that node features thing!
-> https://github.com/polkadot-fellows/runtimes/pull/194

---------

Signed-off-by: Andrei Sandu <andrei-mihail@parity.io>
Signed-off-by: alindima <alin@parity.io>
Co-authored-by: alindima <alin@parity.io>
This commit is contained in:
Andrei Sandu
2024-02-23 23:35:48 +07:00
committed by GitHub
parent 6fc1d41d44
commit 2431001ec0
21 changed files with 1642 additions and 350 deletions
+188 -20
View File
@@ -16,7 +16,7 @@
//! `V6` Primitives.
use bitvec::vec::BitVec;
use bitvec::{field::BitField, slice::BitSlice, vec::BitVec};
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_std::{
@@ -707,19 +707,50 @@ pub type UncheckedSignedAvailabilityBitfields = Vec<UncheckedSignedAvailabilityB
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
pub struct BackedCandidate<H = Hash> {
/// The candidate referred to.
pub candidate: CommittedCandidateReceipt<H>,
candidate: CommittedCandidateReceipt<H>,
/// The validity votes themselves, expressed as signatures.
pub validity_votes: Vec<ValidityAttestation>,
/// The indices of the validators within the group, expressed as a bitfield.
pub validator_indices: BitVec<u8, bitvec::order::Lsb0>,
validity_votes: Vec<ValidityAttestation>,
/// The indices of the validators within the group, expressed as a bitfield. May be extended
/// beyond the backing group size to contain the assigned core index, if ElasticScalingMVP is
/// enabled.
validator_indices: BitVec<u8, bitvec::order::Lsb0>,
}
impl<H> BackedCandidate<H> {
/// Get a reference to the descriptor of the para.
/// Constructor
pub fn new(
candidate: CommittedCandidateReceipt<H>,
validity_votes: Vec<ValidityAttestation>,
validator_indices: BitVec<u8, bitvec::order::Lsb0>,
core_index: Option<CoreIndex>,
) -> Self {
let mut instance = Self { candidate, validity_votes, validator_indices };
if let Some(core_index) = core_index {
instance.inject_core_index(core_index);
}
instance
}
/// Get a reference to the descriptor of the candidate.
pub fn descriptor(&self) -> &CandidateDescriptor<H> {
&self.candidate.descriptor
}
/// Get a reference to the committed candidate receipt of the candidate.
pub fn candidate(&self) -> &CommittedCandidateReceipt<H> {
&self.candidate
}
/// Get a reference to the validity votes of the candidate.
pub fn validity_votes(&self) -> &[ValidityAttestation] {
&self.validity_votes
}
/// Get a mutable reference to validity votes of the para.
pub fn validity_votes_mut(&mut self) -> &mut Vec<ValidityAttestation> {
&mut self.validity_votes
}
/// Compute this candidate's hash.
pub fn hash(&self) -> CandidateHash
where
@@ -735,6 +766,48 @@ impl<H> BackedCandidate<H> {
{
self.candidate.to_plain()
}
/// Get a copy of the validator indices and the assumed core index, if any.
pub fn validator_indices_and_core_index(
&self,
core_index_enabled: bool,
) -> (&BitSlice<u8, bitvec::order::Lsb0>, Option<CoreIndex>) {
// This flag tells us if the block producers must enable Elastic Scaling MVP hack.
// It extends `BackedCandidate::validity_indices` to store a 8 bit core index.
if core_index_enabled {
let core_idx_offset = self.validator_indices.len().saturating_sub(8);
if core_idx_offset > 0 {
let (validator_indices_slice, core_idx_slice) =
self.validator_indices.split_at(core_idx_offset);
return (
validator_indices_slice,
Some(CoreIndex(core_idx_slice.load::<u8>() as u32)),
);
}
}
(&self.validator_indices, None)
}
/// Inject a core index in the validator_indices bitvec.
fn inject_core_index(&mut self, core_index: CoreIndex) {
let core_index_to_inject: BitVec<u8, bitvec::order::Lsb0> =
BitVec::from_vec(vec![core_index.0 as u8]);
self.validator_indices.extend(core_index_to_inject);
}
/// Update the validator indices and core index in the candidate.
pub fn set_validator_indices_and_core_index(
&mut self,
new_indices: BitVec<u8, bitvec::order::Lsb0>,
maybe_core_index: Option<CoreIndex>,
) {
self.validator_indices = new_indices;
if let Some(core_index) = maybe_core_index {
self.inject_core_index(core_index);
}
}
}
/// Verify the backing of the given candidate.
@@ -748,44 +821,42 @@ impl<H> BackedCandidate<H> {
/// Returns either an error, indicating that one of the signatures was invalid or that the index
/// was out-of-bounds, or the number of signatures checked.
pub fn check_candidate_backing<H: AsRef<[u8]> + Clone + Encode + core::fmt::Debug>(
backed: &BackedCandidate<H>,
candidate_hash: CandidateHash,
validity_votes: &[ValidityAttestation],
validator_indices: &BitSlice<u8, bitvec::order::Lsb0>,
signing_context: &SigningContext<H>,
group_len: usize,
validator_lookup: impl Fn(usize) -> Option<ValidatorId>,
) -> Result<usize, ()> {
if backed.validator_indices.len() != group_len {
if validator_indices.len() != group_len {
log::debug!(
target: LOG_TARGET,
"Check candidate backing: indices mismatch: group_len = {} , indices_len = {}",
group_len,
backed.validator_indices.len(),
validator_indices.len(),
);
return Err(())
}
if backed.validity_votes.len() > group_len {
if validity_votes.len() > group_len {
log::debug!(
target: LOG_TARGET,
"Check candidate backing: Too many votes, expected: {}, found: {}",
group_len,
backed.validity_votes.len(),
validity_votes.len(),
);
return Err(())
}
// this is known, even in runtime, to be blake2-256.
let hash = backed.candidate.hash();
let mut signed = 0;
for ((val_in_group_idx, _), attestation) in backed
.validator_indices
for ((val_in_group_idx, _), attestation) in validator_indices
.iter()
.enumerate()
.filter(|(_, signed)| **signed)
.zip(backed.validity_votes.iter())
.zip(validity_votes.iter())
{
let validator_id = validator_lookup(val_in_group_idx).ok_or(())?;
let payload = attestation.signed_payload(hash, signing_context);
let payload = attestation.signed_payload(candidate_hash, signing_context);
let sig = attestation.signature();
if sig.verify(&payload[..], &validator_id) {
@@ -801,11 +872,11 @@ pub fn check_candidate_backing<H: AsRef<[u8]> + Clone + Encode + core::fmt::Debu
}
}
if signed != backed.validity_votes.len() {
if signed != validity_votes.len() {
log::error!(
target: LOG_TARGET,
"Check candidate backing: Too many signatures, expected = {}, found = {}",
backed.validity_votes.len() ,
validity_votes.len(),
signed,
);
return Err(())
@@ -1884,6 +1955,34 @@ pub enum PvfExecKind {
#[cfg(test)]
mod tests {
use super::*;
use bitvec::bitvec;
use primitives::sr25519;
pub fn dummy_committed_candidate_receipt() -> CommittedCandidateReceipt {
let zeros = Hash::zero();
CommittedCandidateReceipt {
descriptor: CandidateDescriptor {
para_id: 0.into(),
relay_parent: zeros,
collator: CollatorId::from(sr25519::Public::from_raw([0; 32])),
persisted_validation_data_hash: zeros,
pov_hash: zeros,
erasure_root: zeros,
signature: CollatorSignature::from(sr25519::Signature([0u8; 64])),
para_head: zeros,
validation_code_hash: ValidationCode(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).hash(),
},
commitments: CandidateCommitments {
head_data: HeadData(vec![]),
upward_messages: vec![].try_into().expect("empty vec fits within bounds"),
new_validation_code: None,
horizontal_messages: vec![].try_into().expect("empty vec fits within bounds"),
processed_downward_messages: 0,
hrmp_watermark: 0_u32,
},
}
}
#[test]
fn group_rotation_info_calculations() {
@@ -1958,4 +2057,73 @@ mod tests {
assert!(zero_b.leading_zeros() >= zero_u.leading_zeros());
}
#[test]
fn test_backed_candidate_injected_core_index() {
let initial_validator_indices = bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1];
let mut candidate = BackedCandidate::new(
dummy_committed_candidate_receipt(),
vec![],
initial_validator_indices.clone(),
None,
);
// No core index supplied, ElasticScalingMVP is off.
let (validator_indices, core_index) = candidate.validator_indices_and_core_index(false);
assert_eq!(validator_indices, initial_validator_indices.as_bitslice());
assert!(core_index.is_none());
// No core index supplied, ElasticScalingMVP is on. Still, decoding will be ok if backing
// group size is <= 8, to give a chance to parachains that don't have multiple cores
// assigned.
let (validator_indices, core_index) = candidate.validator_indices_and_core_index(true);
assert_eq!(validator_indices, initial_validator_indices.as_bitslice());
assert!(core_index.is_none());
let encoded_validator_indices = candidate.validator_indices.clone();
candidate.set_validator_indices_and_core_index(validator_indices.into(), core_index);
assert_eq!(candidate.validator_indices, encoded_validator_indices);
// No core index supplied, ElasticScalingMVP is on. Decoding is corrupted if backing group
// size larger than 8.
let candidate = BackedCandidate::new(
dummy_committed_candidate_receipt(),
vec![],
bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1, 0, 1, 0, 1, 0],
None,
);
let (validator_indices, core_index) = candidate.validator_indices_and_core_index(true);
assert_eq!(validator_indices, bitvec![u8, bitvec::order::Lsb0; 0].as_bitslice());
assert!(core_index.is_some());
// Core index supplied, ElasticScalingMVP is off. Core index will be treated as normal
// validator indices. Runtime will check against this.
let candidate = BackedCandidate::new(
dummy_committed_candidate_receipt(),
vec![],
bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1],
Some(CoreIndex(10)),
);
let (validator_indices, core_index) = candidate.validator_indices_and_core_index(false);
assert_eq!(
validator_indices,
bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0]
);
assert!(core_index.is_none());
// Core index supplied, ElasticScalingMVP is on.
let mut candidate = BackedCandidate::new(
dummy_committed_candidate_receipt(),
vec![],
bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1],
Some(CoreIndex(10)),
);
let (validator_indices, core_index) = candidate.validator_indices_and_core_index(true);
assert_eq!(validator_indices, bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1]);
assert_eq!(core_index, Some(CoreIndex(10)));
let encoded_validator_indices = candidate.validator_indices.clone();
candidate.set_validator_indices_and_core_index(validator_indices.into(), core_index);
assert_eq!(candidate.validator_indices, encoded_validator_indices);
}
}