Newer
Older
// 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/>.
//! Provides glue code over the scheduler and inclusion modules, and accepting
//! one inherent per block that can include new para candidates and bitfields.
//!
//! Unlike other modules in this crate, it does not need to be initialized by the initializer,
//! as it has no initialization logic and its finalization logic depends only on the details of
//! this module.
use crate::{
disputes::DisputesHandler,
inclusion::{self, CandidateCheckContext},
scheduler::{self, FreedReason},
Tsvetomir Dimitrov
committed
shared::{self, AllowedRelayParentsTracker},
ParaId,
};
use bitvec::prelude::BitVec;
use frame_support::{
defensive,
dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo},
inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent},
pallet_prelude::*,
traits::Randomness,
};
use frame_system::pallet_prelude::*;
use pallet_babe::{self, ParentBlockRandomness};
effective_minimum_backing_votes, node_features::FeatureIndex, BackedCandidate, CandidateHash,
CandidateReceipt, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex,
DisputeStatementSet, HeadData, InherentData as ParachainsInherentData,
Andrei Sandu
committed
MultiDisputeStatementSet, ScrapedOnChainVotes, SessionIndex, SignedAvailabilityBitfields,
SigningContext, UncheckedSignedAvailabilityBitfield, UncheckedSignedAvailabilityBitfields,
ValidatorId, ValidatorIndex, ValidityAttestation, PARACHAINS_INHERENT_IDENTIFIER,
};
use rand::{seq::SliceRandom, SeedableRng};
use scale_info::TypeInfo;
use sp_runtime::traits::{Header as HeaderT, One};
use sp_std::{
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
prelude::*,
vec::Vec,
};
mod misc;
mod weights;
use self::weights::checked_multi_dispute_statement_sets_weight;
misc::{IndexedRetain, IsSortedBy},
backed_candidate_weight, backed_candidates_weight, dispute_statement_set_weight,
multi_dispute_statement_sets_weight, paras_inherent_total_weight, signed_bitfield_weight,
signed_bitfields_weight, TestWeightInfo, WeightInfo,
},
};
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod tests;
const LOG_TARGET: &str = "runtime::inclusion-inherent";
/// A bitfield concerning concluded disputes for candidates
/// associated to the core index equivalent to the bit position.
#[derive(Default, PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub(crate) struct DisputedBitfield(pub(crate) BitVec<u8, bitvec::order::Lsb0>);
impl From<BitVec<u8, bitvec::order::Lsb0>> for DisputedBitfield {
fn from(inner: BitVec<u8, bitvec::order::Lsb0>) -> Self {
Self(inner)
}
}
#[cfg(test)]
impl DisputedBitfield {
/// Create a new bitfield, where each bit is set to `false`.
pub fn zeros(n: usize) -> Self {
Self::from(BitVec::<u8, bitvec::order::Lsb0>::repeat(false, n))
/// The context in which the inherent data is checked or processed.
#[derive(PartialEq)]
pub enum ProcessInherentDataContext {
/// Enables filtering/limits weight of inherent up to maximum block weight.
/// Invariant: InherentWeight <= BlockWeight.
ProvideInherent,
/// Checks the InherentWeight invariant.
Enter,
}
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::config]
#[pallet::disable_frame_system_supertrait_check]
pub trait Config:
inclusion::Config + scheduler::Config + initializer::Config + pallet_babe::Config
{
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
#[pallet::error]
pub enum Error<T> {
/// Inclusion inherent called more than once per block.
TooManyInclusionInherents,
/// The hash of the submitted parent header doesn't correspond to the saved block hash of
/// the parent.
InvalidParentHeader,
/// The data given to the inherent will result in an overweight block.
InherentOverweight,
/// A candidate was filtered during inherent execution. This should have only been done
/// during creation.
CandidatesFilteredDuringExecution,
Andrei Sandu
committed
/// Too many candidates supplied.
UnscheduledCandidate,
}
/// Whether the paras inherent was included within this block.
///
/// The `Option<()>` is effectively a `bool`, but it never hits storage in the `None` variant
/// due to the guarantees of FRAME's storage APIs.
///
/// If this is `None` at the end of the block, we panic and render the block invalid.
#[pallet::storage]
pub(crate) type Included<T> = StorageValue<_, ()>;
/// Scraped on chain data for extracting resolved disputes as well as backing votes.
#[pallet::storage]
pub type OnChainVotes<T: Config> = StorageValue<_, ScrapedOnChainVotes<T::Hash>>;
/// Update the disputes statements set part of the on-chain votes.
pub(crate) fn set_scrapable_on_chain_disputes<T: Config>(
session: SessionIndex,
checked_disputes: CheckedMultiDisputeStatementSet,
) {
crate::paras_inherent::OnChainVotes::<T>::mutate(move |value| {
let disputes =
checked_disputes.into_iter().map(DisputeStatementSet::from).collect::<Vec<_>>();
let backing_validators_per_candidate = match value.take() {
Some(v) => v.backing_validators_per_candidate,
None => Vec::new(),
};
*value = Some(ScrapedOnChainVotes::<T::Hash> {
backing_validators_per_candidate,
disputes,
session,
});
})
}
/// Update the backing votes including part of the on-chain votes.
pub(crate) fn set_scrapable_on_chain_backings<T: Config>(
session: SessionIndex,
backing_validators_per_candidate: Vec<(
CandidateReceipt<T::Hash>,
Vec<(ValidatorIndex, ValidityAttestation)>,
)>,
) {
crate::paras_inherent::OnChainVotes::<T>::mutate(move |value| {
let disputes = match value.take() {
Some(v) => v.disputes,
None => MultiDisputeStatementSet::default(),
};
*value = Some(ScrapedOnChainVotes::<T::Hash> {
backing_validators_per_candidate,
disputes,
session,
});
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
T::DbWeight::get().reads_writes(1, 1) // in `on_finalize`.
fn on_finalize(_: BlockNumberFor<T>) {
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
if Included::<T>::take().is_none() {
panic!("Bitfields and heads must be included every block");
}
}
}
#[pallet::inherent]
impl<T: Config> ProvideInherent for Pallet<T> {
type Call = Call<T>;
type Error = MakeFatalError<()>;
const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER;
fn create_inherent(data: &InherentData) -> Option<Self::Call> {
let inherent_data = Self::create_inherent_inner(data)?;
Some(Call::enter { data: inherent_data })
}
fn is_inherent(call: &Self::Call) -> bool {
matches!(call, Call::enter { .. })
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Enter the paras inherent. This will process bitfields and backed candidates.
#[pallet::weight((
paras_inherent_total_weight::<T>(
data.backed_candidates.as_slice(),
&data.bitfields,
&data.disputes,
),
DispatchClass::Mandatory,
))]
pub fn enter(
origin: OriginFor<T>,
data: ParachainsInherentData<HeaderFor<T>>,
) -> DispatchResultWithPostInfo {
ensure_none(origin)?;
ensure!(!Included::<T>::exists(), Error::<T>::TooManyInclusionInherents);
Included::<T>::set(Some(()));
Self::process_inherent_data(data, ProcessInherentDataContext::Enter)
.map(|(_processed, post_info)| post_info)
}
}
}
impl<T: Config> Pallet<T> {
/// Create the `ParachainsInherentData` that gets passed to [`Self::enter`] in
/// [`Self::create_inherent`]. This code is pulled out of [`Self::create_inherent`] so it can be
/// unit tested.
fn create_inherent_inner(data: &InherentData) -> Option<ParachainsInherentData<HeaderFor<T>>> {
let parachains_inherent_data = match data.get_data(&Self::INHERENT_IDENTIFIER) {
Ok(Some(d)) => d,
Ok(None) => return None,
Err(_) => {
log::warn!(target: LOG_TARGET, "ParachainsInherentData failed to decode");
return None
},
};
match Self::process_inherent_data(
parachains_inherent_data,
ProcessInherentDataContext::ProvideInherent,
) {
Ok((processed, _)) => Some(processed),
Err(err) => {
log::warn!(target: LOG_TARGET, "Processing inherent data failed: {:?}", err);
None
},
}
}
/// Process inherent data.
///
/// The given inherent data is processed and state is altered accordingly. If any data could
/// not be applied (inconsistencies, weight limit, ...) it is removed.
/// When called from `create_inherent` the `context` must be set to
/// `ProcessInherentDataContext::ProvideInherent` so it guarantees the invariant that inherent
/// It is **mandatory** that calls from `enter` set `context` to
/// `ProcessInherentDataContext::Enter` to ensure the weight invariant is checked.
///
/// Returns: Result containing processed inherent data and weight, the processed inherent would
/// consume.
fn process_inherent_data(
data: ParachainsInherentData<HeaderFor<T>>,
context: ProcessInherentDataContext,
) -> sp_std::result::Result<
(ParachainsInherentData<HeaderFor<T>>, PostDispatchInfo),
DispatchErrorWithPostInfo,
> {
#[cfg(feature = "runtime-metrics")]
sp_io::init_tracing();
let ParachainsInherentData {
mut backed_candidates,
parent_header,
mut disputes,
} = data;
log::debug!(
target: LOG_TARGET,
"[process_inherent_data] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}",
bitfields.len(),
backed_candidates.len(),
disputes.len()
);
let parent_hash = frame_system::Pallet::<T>::parent_hash();
parent_header.hash().as_ref() == parent_hash.as_ref(),
Error::<T>::InvalidParentHeader,
);
let now = frame_system::Pallet::<T>::block_number();
let config = configuration::ActiveConfig::<T>::get();
// Before anything else, update the allowed relay-parents.
{
let parent_number = now - One::one();
let parent_storage_root = *parent_header.state_root();
shared::AllowedRelayParents::<T>::mutate(|tracker| {
tracker.update(
parent_hash,
parent_storage_root,
parent_number,
config.async_backing_params.allowed_ancestry_len,
);
});
}
let allowed_relay_parents = shared::AllowedRelayParents::<T>::get();
let candidates_weight = backed_candidates_weight::<T>(&backed_candidates);
let bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
Tsvetomir Dimitrov
committed
// Weight before filtering/sanitization
let all_weight_before = candidates_weight + bitfields_weight + disputes_weight;
METRICS.on_before_filter(all_weight_before.ref_time());
log::debug!(target: LOG_TARGET, "Size before filter: {}, candidates + bitfields: {}, disputes: {}", all_weight_before.proof_size(), candidates_weight.proof_size() + bitfields_weight.proof_size(), disputes_weight.proof_size());
log::debug!(target: LOG_TARGET, "Time weight before filter: {}, candidates + bitfields: {}, disputes: {}", all_weight_before.ref_time(), candidates_weight.ref_time() + bitfields_weight.ref_time(), disputes_weight.ref_time());
let current_session = shared::CurrentSessionIndex::<T>::get();
let expected_bits = scheduler::AvailabilityCores::<T>::get().len();
let validator_public = shared::ActiveValidatorKeys::<T>::get();
// We are assuming (incorrectly) to have all the weight (for the mandatory class or even
// full block) available to us. This can lead to slightly overweight blocks, which still
// works as the dispatch class for `enter` is `Mandatory`. By using the `Mandatory`
// dispatch class, the upper layers impose no limit on the weight of this inherent, instead
// we limit ourselves and make sure to stay within reasonable bounds. It might make sense
// to subtract BlockWeights::base_block to reduce chances of becoming overweight.
let max_block_weight = {
let dispatch_class = DispatchClass::Mandatory;
let max_block_weight_full = <T as frame_system::Config>::BlockWeights::get();
log::debug!(target: LOG_TARGET, "Max block weight: {}", max_block_weight_full.max_block);
// Get max block weight for the mandatory class if defined, otherwise total max weight
// of the block.
let max_weight = max_block_weight_full
.per_class
.get(dispatch_class)
.max_total
.unwrap_or(max_block_weight_full.max_block);
log::debug!(target: LOG_TARGET, "Used max block time weight: {}", max_weight);
let max_block_size_full = <T as frame_system::Config>::BlockLength::get();
let max_block_size = max_block_size_full.max.get(dispatch_class);
log::debug!(target: LOG_TARGET, "Used max block size: {}", max_block_size);
// Adjust proof size to max block size as we are tracking tx size.
max_weight.set_proof_size(*max_block_size as u64)
};
log::debug!(target: LOG_TARGET, "Used max block weight: {}", max_block_weight);
let entropy = compute_entropy::<T>(parent_hash);
let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into());
// Filter out duplicates and continue.
if let Err(()) = T::DisputesHandler::deduplicate_and_sort_dispute_data(&mut disputes) {
log::debug!(target: LOG_TARGET, "Found duplicate statement sets, retaining the first");
}
let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period;
let dispute_statement_set_valid = move |set: DisputeStatementSet| {
T::DisputesHandler::filter_dispute_data(set, post_conclusion_acceptance_period)
};
// Limit the disputes first, since the following statements depend on the votes include
// here.
let (checked_disputes_sets, checked_disputes_sets_consumed_weight) =
limit_and_sanitize_disputes::<T, _>(
dispute_statement_set_valid,
max_block_weight,
);
let all_weight_after = if context == ProcessInherentDataContext::ProvideInherent {
// Assure the maximum block weight is adhered, by limiting bitfields and backed
// candidates. Dispute statement sets were already limited before.
let non_disputes_weight = apply_weight_limit::<T>(
&mut backed_candidates,
&mut bitfields,
max_block_weight.saturating_sub(checked_disputes_sets_consumed_weight),
&mut rng,
);
let all_weight_after =
non_disputes_weight.saturating_add(checked_disputes_sets_consumed_weight);
METRICS.on_after_filter(all_weight_after.ref_time());
log::debug!(
target: LOG_TARGET,
"[process_inherent_data] after filter: bitfields.len(): {}, backed_candidates.len(): {}, checked_disputes_sets.len() {}",
bitfields.len(),
backed_candidates.len(),
checked_disputes_sets.len()
);
log::debug!(target: LOG_TARGET, "Size after filter: {}, candidates + bitfields: {}, disputes: {}", all_weight_after.proof_size(), non_disputes_weight.proof_size(), checked_disputes_sets_consumed_weight.proof_size());
log::debug!(target: LOG_TARGET, "Time weight after filter: {}, candidates + bitfields: {}, disputes: {}", all_weight_after.ref_time(), non_disputes_weight.ref_time(), checked_disputes_sets_consumed_weight.ref_time());
if all_weight_after.any_gt(max_block_weight) {
log::warn!(target: LOG_TARGET, "Post weight limiting weight is still too large, time: {}, size: {}", all_weight_after.ref_time(), all_weight_after.proof_size());
}
} else {
// This check is performed in the context of block execution. Ensures inherent weight
// invariants guaranteed by `create_inherent_data` for block authorship.
if all_weight_before.any_gt(max_block_weight) {
log::error!(
"Overweight para inherent data reached the runtime {:?}: {} > {}",
parent_hash,
max_block_weight
);
}
ensure!(all_weight_before.all_lte(max_block_weight), Error::<T>::InherentOverweight);
all_weight_before
};
// Note that `process_checked_multi_dispute_data` will iterate and import each
// dispute; so the input here must be reasonably bounded,
// which is guaranteed by the checks and weight limitation above.
// We don't care about fresh or not disputes
// this writes them to storage, so let's query it via those means
// if this fails for whatever reason, that's ok.
if let Err(e) =
T::DisputesHandler::process_checked_multi_dispute_data(&checked_disputes_sets)
{
log::warn!(target: LOG_TARGET, "MultiDisputesData failed to update: {:?}", e);
};
METRICS.on_disputes_imported(checked_disputes_sets.len() as u64);
set_scrapable_on_chain_disputes::<T>(current_session, checked_disputes_sets.clone());
if T::DisputesHandler::is_frozen() {
// Relay chain freeze, at this point we will not include any parachain blocks.
METRICS.on_relay_chain_freeze();
let disputes = checked_disputes_sets
.into_iter()
.map(|checked| checked.into())
.collect::<Vec<_>>();
let processed = ParachainsInherentData {
bitfields: Vec::new(),
backed_candidates: Vec::new(),
disputes,
parent_header,
// The relay chain we are currently on is invalid. Proceed no further on parachains.
return Ok((processed, Some(checked_disputes_sets_consumed_weight).into()))
}
// Contains the disputes that are concluded in the current session only,
// since these are the only ones that are relevant for the occupied cores
// and lightens the load on `free_disputed` significantly.
// Cores can't be occupied with candidates of the previous sessions, and only
// things with new votes can have just concluded. We only need to collect
// cores with disputes that conclude just now, because disputes that
// concluded longer ago have already had any corresponding cores cleaned up.
let current_concluded_invalid_disputes = checked_disputes_sets
.iter()
.map(AsRef::as_ref)
.filter(|dss| dss.session == current_session)
.map(|dss| (dss.session, dss.candidate_hash))
.filter(|(session, candidate)| {
<T>::DisputesHandler::concluded_invalid(*session, *candidate)
})
.map(|(_session, candidate)| candidate)
.collect::<BTreeSet<CandidateHash>>();
// Get the cores freed as a result of concluded invalid candidates.
let (freed_disputed, concluded_invalid_hashes): (Vec<CoreIndex>, BTreeSet<CandidateHash>) =
inclusion::Pallet::<T>::free_disputed(¤t_concluded_invalid_disputes)
.unzip();
// Create a bit index from the set of core indices where each index corresponds to
// a core index that was freed due to a dispute.
//
// I.e. 010100 would indicate, the candidates on Core 1 and 3 would be disputed.
let disputed_bitfield = create_disputed_bitfield(expected_bits, freed_disputed.iter());
let bitfields = sanitize_bitfields::<T>(
bitfields,
disputed_bitfield,
expected_bits,
parent_hash,
current_session,
&validator_public[..],
);
METRICS.on_bitfields_processed(bitfields.len() as u64);
// Process new availability bitfields, yielding any availability cores whose
// work has now concluded.
inclusion::Pallet::<T>::update_pending_availability_and_get_freed_cores(
&validator_public[..],
bitfields.clone(),
);
// Inform the disputes module of all included candidates.
for (_, candidate_hash) in &freed_concluded {
T::DisputesHandler::note_included(current_session, *candidate_hash, now);
}
METRICS.on_candidates_included(freed_concluded.len() as u64);
// Get the timed out candidates
let freed_timeout = if scheduler::Pallet::<T>::availability_timeout_check_required() {
inclusion::Pallet::<T>::free_timedout()
} else {
Vec::new()
};
if !freed_timeout.is_empty() {
log::debug!(target: LOG_TARGET, "Evicted timed out cores: {:?}", freed_timeout);
}
// We'll schedule paras again, given freed cores, and reasons for freeing.
let freed = freed_concluded
.into_iter()
.map(|(c, _hash)| (c, FreedReason::Concluded))
.chain(freed_disputed.into_iter().map(|core| (core, FreedReason::Concluded)))
.chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut)))
.collect::<BTreeMap<CoreIndex, FreedReason>>();
scheduler::Pallet::<T>::free_cores_and_fill_claimqueue(freed, now);
METRICS.on_candidates_processed_total(backed_candidates.len() as u64);
let core_index_enabled = configuration::ActiveConfig::<T>::get()
Andrei Sandu
committed
.node_features
.get(FeatureIndex::ElasticScalingMVP as usize)
.map(|b| *b)
.unwrap_or(false);
let mut scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>> = BTreeMap::new();
let mut total_scheduled_cores = 0;
for (core_idx, para_id) in scheduler::Pallet::<T>::scheduled_paras() {
Andrei Sandu
committed
total_scheduled_cores += 1;
scheduled.entry(para_id).or_default().insert(core_idx);
}
let initial_candidate_count = backed_candidates.len();
let backed_candidates_with_core = sanitize_backed_candidates::<T>(
Andrei Sandu
committed
backed_candidates,
&allowed_relay_parents,
concluded_invalid_hashes,
Andrei Sandu
committed
scheduled,
core_index_enabled,
);
let count = count_backed_candidates(&backed_candidates_with_core);
Andrei Sandu
committed
ensure!(count <= total_scheduled_cores, Error::<T>::UnscheduledCandidate);
METRICS.on_candidates_sanitized(count as u64);
Tsvetomir Dimitrov
committed
// In `Enter` context (invoked during execution) no more candidates should be filtered,
// because they have already been filtered during `ProvideInherent` context. Abort in such
// cases.
Andrei Sandu
committed
if context == ProcessInherentDataContext::Enter {
ensure!(
initial_candidate_count == count,
Error::<T>::CandidatesFilteredDuringExecution
);
Andrei Sandu
committed
}
// Process backed candidates according to scheduled cores.
let inclusion::ProcessedCandidates::<<HeaderFor<T> as HeaderT>::Hash> {
core_indices: occupied,
candidate_receipt_with_backing_validator_indices,
} = inclusion::Pallet::<T>::process_candidates(
&backed_candidates_with_core,
scheduler::Pallet::<T>::group_validators,
Andrei Sandu
committed
core_index_enabled,
// Note which of the scheduled cores were actually occupied by a backed candidate.
scheduler::Pallet::<T>::occupied(occupied.into_iter().map(|e| (e.0, e.1)).collect());
set_scrapable_on_chain_backings::<T>(
current_session,
candidate_receipt_with_backing_validator_indices,
);
let disputes = checked_disputes_sets
.into_iter()
.map(|checked| checked.into())
.collect::<Vec<_>>();
let bitfields = bitfields.into_iter().map(|v| v.into_unchecked()).collect();
Andrei Sandu
committed
let processed = ParachainsInherentData {
bitfields,
backed_candidates: backed_candidates_with_core.into_iter().fold(
Vec::with_capacity(count),
|mut acc, (_id, candidates)| {
acc.extend(candidates.into_iter().map(|(c, _)| c));
acc
},
),
Andrei Sandu
committed
disputes,
parent_header,
};
Ok((processed, Some(all_weight_after).into()))
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
}
}
/// Derive a bitfield from dispute
pub(super) fn create_disputed_bitfield<'a, I>(
expected_bits: usize,
freed_cores: I,
) -> DisputedBitfield
where
I: 'a + IntoIterator<Item = &'a CoreIndex>,
{
let mut bitvec = BitVec::repeat(false, expected_bits);
for core_idx in freed_cores {
let core_idx = core_idx.0 as usize;
if core_idx < expected_bits {
bitvec.set(core_idx, true);
}
}
DisputedBitfield::from(bitvec)
}
/// Select a random subset, with preference for certain indices.
///
/// Adds random items to the set until all candidates
/// are tried or the remaining weight is depleted.
///
/// Returns the weight of all selected items from `selectables`
/// as well as their indices in ascending order.
fn random_sel<X, F: Fn(&X) -> Weight>(
rng: &mut rand_chacha::ChaChaRng,
mut preferred_indices: Vec<usize>,
weight_fn: F,
weight_limit: Weight,
) -> (Weight, Vec<usize>) {
if selectables.is_empty() {
}
// all indices that are not part of the preferred set
let mut indices = (0..selectables.len())
.into_iter()
.filter(|idx| !preferred_indices.contains(idx))
.collect::<Vec<_>>();
let mut picked_indices = Vec::with_capacity(selectables.len().saturating_sub(1));
preferred_indices.shuffle(rng);
for preferred_idx in preferred_indices {
// preferred indices originate from outside
if let Some(item) = selectables.get(preferred_idx) {
let updated = weight_acc.saturating_add(weight_fn(item));
if updated.any_gt(weight_limit) {
continue
}
weight_acc = updated;
picked_indices.push(preferred_idx);
}
}
indices.shuffle(rng);
for idx in indices {
let item = &selectables[idx];
let updated = weight_acc.saturating_add(weight_fn(item));
if updated.any_gt(weight_limit) {
continue
}
weight_acc = updated;
picked_indices.push(idx);
}
// sorting indices, so the ordering is retained
// unstable sorting is fine, since there are no duplicates in indices
// and even if there were, they don't have an identity
picked_indices.sort_unstable();
(weight_acc, picked_indices)
}
/// Considers an upper threshold that the inherent data must not exceed.
///
/// If there is sufficient space, all bitfields and all candidates
/// will be included.
///
/// Otherwise tries to include all disputes, and then tries to fill the remaining space with
/// bitfields and then candidates.
/// The selection process is random. For candidates, there is an exception for code upgrades as they
/// are preferred. And for disputes, local and older disputes are preferred (see
/// `limit_and_sanitize_disputes`). for backed candidates, since with a increasing number of
/// parachains their chances of inclusion become slim. All backed candidates are checked
/// beforehand in `fn create_inherent_inner` which guarantees sanity.
///
/// Assumes disputes are already filtered by the time this is called.
///
/// Returns the total weight consumed by `bitfields` and `candidates`.
pub(crate) fn apply_weight_limit<T: Config + inclusion::Config>(
candidates: &mut Vec<BackedCandidate<<T>::Hash>>,
bitfields: &mut UncheckedSignedAvailabilityBitfields,
max_consumable_weight: Weight,
rng: &mut rand_chacha::ChaChaRng,
) -> Weight {
let total_candidates_weight = backed_candidates_weight::<T>(candidates.as_slice());
let total_bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
let total = total_bitfields_weight.saturating_add(total_candidates_weight);
// candidates + bitfields fit into the block
if max_consumable_weight.all_gte(total) {
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
// Invariant: block author provides candidate in the order in which they form a chain
// wrt elastic scaling. If the invariant is broken, we'd fail later when filtering candidates
// which are unchained.
let mut chained_candidates: Vec<Vec<_>> = Vec::new();
let mut current_para_id = None;
for candidate in sp_std::mem::take(candidates).into_iter() {
let candidate_para_id = candidate.descriptor().para_id;
if Some(candidate_para_id) == current_para_id {
let chain = chained_candidates
.last_mut()
.expect("if the current_para_id is Some, then vec is not empty; qed");
chain.push(candidate);
} else {
current_para_id = Some(candidate_para_id);
chained_candidates.push(vec![candidate]);
}
}
// Elastic scaling: we prefer chains that have a code upgrade among the candidates,
// as the candidates containing the upgrade tend to be large and hence stand no chance to
// be picked late while maintaining the weight bounds.
//
// Limitations: For simplicity if total weight of a chain of candidates is larger than
// the remaining weight, the chain will still not be included while it could still be possible
// to include part of that chain.
let preferred_chain_indices = chained_candidates
.filter_map(|(idx, candidates)| {
// Check if any of the candidate in chain contains a code upgrade.
if candidates
.iter()
.any(|candidate| candidate.candidate().commitments.new_validation_code.is_some())
{
Some(idx)
} else {
None
}
})
.collect::<Vec<usize>>();
// There is weight remaining to be consumed by a subset of chained candidates
// which are going to be picked now.
if let Some(max_consumable_by_candidates) =
max_consumable_weight.checked_sub(&total_bitfields_weight)
let (acc_candidate_weight, chained_indices) =
random_sel::<Vec<BackedCandidate<<T as frame_system::Config>::Hash>>, _>(
&chained_candidates,
preferred_chain_indices,
|candidates| backed_candidates_weight::<T>(&candidates),
max_consumable_by_candidates,
log::debug!(target: LOG_TARGET, "Indices Candidates: {:?}, size: {}", chained_indices, candidates.len());
chained_candidates
.indexed_retain(|idx, _backed_candidates| chained_indices.binary_search(&idx).is_ok());
// pick all bitfields, and
// fill the remaining space with candidates
let total_consumed = acc_candidate_weight.saturating_add(total_bitfields_weight);
*candidates = chained_candidates.into_iter().flatten().collect::<Vec<_>>();
return total_consumed
}
candidates.clear();
// insufficient space for even the bitfields alone, so only try to fit as many of those
// into the block and skip the candidates entirely
let (total_consumed, indices) = random_sel::<UncheckedSignedAvailabilityBitfield, _>(
|bitfield| signed_bitfield_weight::<T>(&bitfield),
max_consumable_weight,
log::debug!(target: LOG_TARGET, "Indices Bitfields: {:?}, size: {}", indices, bitfields.len());
bitfields.indexed_retain(|idx, _bitfield| indices.binary_search(&idx).is_ok());
}
/// Filter bitfields based on freed core indices, validity, and other sanity checks.
///
/// Do sanity checks on the bitfields:
///
/// 1. no more than one bitfield per validator
/// 2. bitfields are ascending by validator index.
/// 3. each bitfield has exactly `expected_bits`
/// 4. signature is valid
/// 5. remove any disputed core indices
///
/// If any of those is not passed, the bitfield is dropped.
pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
unchecked_bitfields: UncheckedSignedAvailabilityBitfields,
disputed_bitfield: DisputedBitfield,
expected_bits: usize,
parent_hash: T::Hash,
session_index: SessionIndex,
validators: &[ValidatorId],
) -> SignedAvailabilityBitfields {
let mut bitfields = Vec::with_capacity(unchecked_bitfields.len());
let mut last_index: Option<ValidatorIndex> = None;
if disputed_bitfield.0.len() != expected_bits {
// This is a system logic error that should never occur, but we want to handle it gracefully
// so we just drop all bitfields
log::error!(target: LOG_TARGET, "BUG: disputed_bitfield != expected_bits");
return vec![]
}
let all_zeros = BitVec::<u8, bitvec::order::Lsb0>::repeat(false, expected_bits);
let signing_context = SigningContext { parent_hash, session_index };
for unchecked_bitfield in unchecked_bitfields {
// Find and skip invalid bitfields.
if unchecked_bitfield.unchecked_payload().0.len() != expected_bits {
log::trace!(
target: LOG_TARGET,
"bad bitfield length: {} != {:?}",
unchecked_bitfield.unchecked_payload().0.len(),
expected_bits,
);
continue
}
if unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() !=
all_zeros
{
log::trace!(
target: LOG_TARGET,
"bitfield contains disputed cores: {:?}",
unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone()
);
continue
}
let validator_index = unchecked_bitfield.unchecked_validator_index();
if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) {
log::trace!(
target: LOG_TARGET,
"bitfield validator index is not greater than last: !({:?} < {})",
last_index.as_ref().map(|x| x.0),
validator_index.0
);
continue
}
if unchecked_bitfield.unchecked_validator_index().0 as usize >= validators.len() {
log::trace!(
target: LOG_TARGET,
"bitfield validator index is out of bounds: {} >= {}",
validator_index.0,
validators.len(),
);
continue
}
let validator_public = &validators[validator_index.0 as usize];
// Validate bitfield signature.
if let Ok(signed_bitfield) =
unchecked_bitfield.try_into_checked(&signing_context, validator_public)
{
bitfields.push(signed_bitfield);
METRICS.on_valid_bitfield_signature();
log::warn!(target: LOG_TARGET, "Invalid bitfield signature");
METRICS.on_invalid_bitfield_signature();
};
last_index = Some(validator_index);
}
bitfields
}
/// Performs various filtering on the backed candidates inherent data.
/// Must maintain the invariant that the returned candidate collection contains the candidates
/// sorted in dependency order for each para. When doing any filtering, we must therefore drop any
/// subsequent candidates after the filtered one.
///
Tsvetomir Dimitrov
committed
/// Filter out:
/// 1. any candidates which don't form a chain with the other candidates of the paraid (even if they
/// do form a chain but are not in the right order).
/// 2. any candidates that have a concluded invalid dispute or who are descendants of a concluded
/// invalid candidate.
/// 3. any unscheduled candidates, as well as candidates whose paraid has multiple cores assigned
Andrei Sandu
committed
/// but have no injected core index.
/// 4. all backing votes from disabled validators
/// 5. any candidates that end up with less than `effective_minimum_backing_votes` backing votes
/// Returns the scheduled
/// backed candidates which passed filtering, mapped by para id and in the right dependency order.
fn sanitize_backed_candidates<T: crate::inclusion::Config>(
backed_candidates: Vec<BackedCandidate<T::Hash>>,
Tsvetomir Dimitrov
committed
allowed_relay_parents: &AllowedRelayParentsTracker<T::Hash, BlockNumberFor<T>>,
concluded_invalid_with_descendants: BTreeSet<CandidateHash>,
Andrei Sandu
committed
scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>>,
core_index_enabled: bool,
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
) -> BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>> {
// Map the candidates to the right paraids, while making sure that the order between candidates
// of the same para is preserved.
let mut candidates_per_para: BTreeMap<ParaId, Vec<_>> = BTreeMap::new();
for candidate in backed_candidates {
candidates_per_para
.entry(candidate.descriptor().para_id)
.or_default()
.push(candidate);
}
// Check that candidates pertaining to the same para form a chain. Drop the ones that
// don't, along with the rest of candidates which follow them in the input vector.
filter_unchained_candidates::<T>(&mut candidates_per_para, allowed_relay_parents);
// Remove any candidates that were concluded invalid or who are descendants of concluded invalid
// candidates (along with their descendants).
retain_candidates::<T, _, _>(&mut candidates_per_para, |_, candidate| {
let keep = !concluded_invalid_with_descendants.contains(&candidate.candidate().hash());
if !keep {
log::debug!(
target: LOG_TARGET,
"Found backed candidate {:?} which was concluded invalid or is a descendant of a concluded invalid candidate, for paraid {:?}.",
candidate.candidate().hash(),
candidate.descriptor().para_id
);
}
keep
// Map candidates to scheduled cores. Filter out any unscheduled candidates along with their
// descendants.
Andrei Sandu
committed
let mut backed_candidates_with_core = map_candidates_to_cores::<T>(
&allowed_relay_parents,
scheduled,
core_index_enabled,
candidates_per_para,
Andrei Sandu
committed
);