1use crate::{
25 configuration, coretime,
26 disputes::DisputesHandler,
27 inclusion::{self, CandidateCheckContext},
28 initializer,
29 metrics::METRICS,
30 paras, scheduler,
31 shared::{self, AllowedSchedulingParentsTracker},
32 ParaId,
33};
34use alloc::{
35 collections::{btree_map::BTreeMap, btree_set::BTreeSet},
36 vec,
37 vec::Vec,
38};
39use bitvec::prelude::BitVec;
40use core::result::Result;
41use frame_support::{
42 defensive,
43 dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo},
44 inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent},
45 pallet_prelude::*,
46 traits::Randomness,
47};
48
49use frame_system::pallet_prelude::*;
50use pallet_babe::{self, ParentBlockRandomness};
51use polkadot_primitives::{
52 effective_minimum_backing_votes, node_features::FeatureIndex, BackedCandidate,
53 CandidateDescriptorVersion, CandidateHash, CandidateReceiptV2 as CandidateReceipt,
54 CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet,
55 HeadData, InherentData as ParachainsInherentData, MultiDisputeStatementSet,
56 ScrapedOnChainVotes, SessionIndex, SignedAvailabilityBitfields, SigningContext,
57 UncheckedSignedAvailabilityBitfield, UncheckedSignedAvailabilityBitfields, ValidatorId,
58 ValidatorIndex, ValidityAttestation, PARACHAINS_INHERENT_IDENTIFIER,
59};
60use rand::{seq::SliceRandom, SeedableRng};
61use scale_info::TypeInfo;
62use sp_runtime::traits::{Header as HeaderT, One, Saturating};
63
64mod misc;
65mod weights;
66
67use self::weights::checked_multi_dispute_statement_sets_weight;
68pub use self::{
69 misc::{IndexedRetain, IsSortedBy},
70 weights::{
71 backed_candidate_weight, backed_candidates_weight, dispute_statement_set_weight,
72 multi_dispute_statement_sets_weight, paras_inherent_total_weight, signed_bitfield_weight,
73 signed_bitfields_weight, TestWeightInfo, WeightInfo,
74 },
75};
76
77#[cfg(feature = "runtime-benchmarks")]
78mod benchmarking;
79
80#[cfg(test)]
81mod tests;
82
83const LOG_TARGET: &str = "runtime::inclusion-inherent";
84
85#[derive(Default, PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo)]
88pub(crate) struct DisputedBitfield(pub(crate) BitVec<u8, bitvec::order::Lsb0>);
89
90impl From<BitVec<u8, bitvec::order::Lsb0>> for DisputedBitfield {
91 fn from(inner: BitVec<u8, bitvec::order::Lsb0>) -> Self {
92 Self(inner)
93 }
94}
95
96#[cfg(test)]
97impl DisputedBitfield {
98 pub fn zeros(n: usize) -> Self {
100 Self::from(BitVec::<u8, bitvec::order::Lsb0>::repeat(false, n))
101 }
102}
103
104pub use pallet::*;
105
106#[frame_support::pallet]
107pub mod pallet {
108 use super::*;
109
110 #[pallet::pallet]
111 #[pallet::without_storage_info]
112 pub struct Pallet<T>(_);
113
114 #[pallet::config]
115 #[pallet::disable_frame_system_supertrait_check]
116 pub trait Config:
117 inclusion::Config
118 + scheduler::Config
119 + initializer::Config
120 + coretime::Config
121 + pallet_babe::Config
122 {
123 type WeightInfo: WeightInfo;
125 }
126
127 #[pallet::error]
128 pub enum Error<T> {
129 TooManyInclusionInherents,
131 InvalidParentHeader,
134 InherentDataFilteredDuringExecution,
137 UnscheduledCandidate,
139 }
140
141 #[pallet::storage]
148 pub(crate) type Included<T> = StorageValue<_, ()>;
149
150 #[pallet::storage]
152 pub type OnChainVotes<T: Config> = StorageValue<_, ScrapedOnChainVotes<T::Hash>>;
153
154 pub(crate) fn set_scrapable_on_chain_disputes<T: Config>(
156 session: SessionIndex,
157 checked_disputes: CheckedMultiDisputeStatementSet,
158 ) {
159 crate::paras_inherent::OnChainVotes::<T>::mutate(move |value| {
160 let disputes =
161 checked_disputes.into_iter().map(DisputeStatementSet::from).collect::<Vec<_>>();
162 let backing_validators_per_candidate = match value.take() {
163 Some(v) => v.backing_validators_per_candidate,
164 None => Vec::new(),
165 };
166 *value = Some(ScrapedOnChainVotes::<T::Hash> {
167 backing_validators_per_candidate,
168 disputes,
169 session,
170 });
171 })
172 }
173
174 pub(crate) fn set_scrapable_on_chain_backings<T: Config>(
176 session: SessionIndex,
177 backing_validators_per_candidate: Vec<(
178 CandidateReceipt<T::Hash>,
179 Vec<(ValidatorIndex, ValidityAttestation)>,
180 )>,
181 ) {
182 crate::paras_inherent::OnChainVotes::<T>::mutate(move |value| {
183 let disputes = match value.take() {
184 Some(v) => v.disputes,
185 None => MultiDisputeStatementSet::default(),
186 };
187 *value = Some(ScrapedOnChainVotes::<T::Hash> {
188 backing_validators_per_candidate,
189 disputes,
190 session,
191 });
192 })
193 }
194
195 #[pallet::hooks]
196 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
197 fn on_initialize(_: BlockNumberFor<T>) -> Weight {
198 T::DbWeight::get().reads_writes(1, 1) }
200
201 fn on_finalize(_: BlockNumberFor<T>) {
202 if Included::<T>::take().is_none() {
203 panic!("ParachainInherent was not executed in this block. This is a bug. Please report this at https://github.com/paritytech/polkadot-sdk/issues.");
204 }
205 }
206 }
207
208 #[pallet::inherent]
209 impl<T: Config> ProvideInherent for Pallet<T> {
210 type Call = Call<T>;
211 type Error = MakeFatalError<()>;
212 const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER;
213
214 fn create_inherent(data: &InherentData) -> Option<Self::Call> {
215 let inherent_data = Self::create_inherent_inner(data)?;
216
217 Some(Call::enter { data: inherent_data })
218 }
219
220 fn is_inherent(call: &Self::Call) -> bool {
221 matches!(call, Call::enter { .. })
222 }
223 }
224
225 #[pallet::call]
226 impl<T: Config> Pallet<T> {
227 #[pallet::call_index(0)]
229 #[pallet::weight((
230 paras_inherent_total_weight::<T>(
231 data.backed_candidates.as_slice(),
232 &data.bitfields,
233 &data.disputes,
234 ),
235 DispatchClass::Mandatory,
236 ))]
237 pub fn enter(
238 origin: OriginFor<T>,
239 data: ParachainsInherentData<HeaderFor<T>>,
240 ) -> DispatchResultWithPostInfo {
241 ensure_none(origin)?;
242
243 ensure!(!Included::<T>::exists(), Error::<T>::TooManyInclusionInherents);
244 Included::<T>::set(Some(()));
245 let initial_data = data.clone();
246
247 Self::process_inherent_data(data).and_then(|(processed, post_info)| {
248 ensure!(initial_data == processed, Error::<T>::InherentDataFilteredDuringExecution);
249 Ok(post_info)
250 })
251 }
252 }
253}
254
255impl<T: Config> Pallet<T> {
256 fn create_inherent_inner(data: &InherentData) -> Option<ParachainsInherentData<HeaderFor<T>>> {
260 let parachains_inherent_data = match data.get_data(&Self::INHERENT_IDENTIFIER) {
261 Ok(Some(d)) => d,
262 Ok(None) => return None,
263 Err(_) => {
264 log::warn!(target: LOG_TARGET, "ParachainsInherentData failed to decode");
265 return None;
266 },
267 };
268 match Self::process_inherent_data(parachains_inherent_data) {
269 Ok((processed, _)) => Some(processed),
270 Err(err) => {
271 log::warn!(target: LOG_TARGET, "Processing inherent data failed: {:?}", err);
272 None
273 },
274 }
275 }
276
277 fn process_inherent_data(
285 data: ParachainsInherentData<HeaderFor<T>>,
286 ) -> Result<(ParachainsInherentData<HeaderFor<T>>, PostDispatchInfo), DispatchErrorWithPostInfo>
287 {
288 #[cfg(feature = "runtime-metrics")]
289 sp_io::init_tracing();
290
291 let ParachainsInherentData {
292 mut bitfields,
293 mut backed_candidates,
294 parent_header,
295 mut disputes,
296 } = data;
297
298 log::debug!(
299 target: LOG_TARGET,
300 "[process_inherent_data] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}",
301 bitfields.len(),
302 backed_candidates.len(),
303 disputes.len()
304 );
305
306 let parent_hash = frame_system::Pallet::<T>::parent_hash();
307
308 ensure!(
309 parent_header.hash().as_ref() == parent_hash.as_ref(),
310 Error::<T>::InvalidParentHeader,
311 );
312
313 let now = frame_system::Pallet::<T>::block_number();
314 let config = configuration::ActiveConfig::<T>::get();
315
316 let current_session = shared::CurrentSessionIndex::<T>::get();
317
318 {
320 let parent_number = now.saturating_sub(One::one());
321 let parent_storage_root = *parent_header.state_root();
322
323 shared::Pallet::<T>::new_block(
324 parent_hash,
325 scheduler::Pallet::<T>::claim_queue(),
326 parent_number,
327 config.scheduler_params.lookahead,
328 parent_storage_root,
329 current_session,
330 );
331 }
332
333 let candidates_weight = backed_candidates_weight::<T>(&backed_candidates);
334 let bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
335 let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
336
337 let weight_before_filtering = candidates_weight + bitfields_weight + disputes_weight;
339
340 METRICS.on_before_filter(weight_before_filtering.ref_time());
341 log::debug!(target: LOG_TARGET, "Size before filter: {}, candidates + bitfields: {}, disputes: {}", weight_before_filtering.proof_size(), candidates_weight.proof_size() + bitfields_weight.proof_size(), disputes_weight.proof_size());
342 log::debug!(target: LOG_TARGET, "Time weight before filter: {}, candidates + bitfields: {}, disputes: {}", weight_before_filtering.ref_time(), candidates_weight.ref_time() + bitfields_weight.ref_time(), disputes_weight.ref_time());
343
344 let expected_bits = scheduler::Pallet::<T>::num_availability_cores();
345 let validator_public = shared::ActiveValidatorKeys::<T>::get();
346
347 let max_block_weight = {
354 let dispatch_class = DispatchClass::Mandatory;
355 let max_block_weight_full = <T as frame_system::Config>::BlockWeights::get();
356 log::debug!(target: LOG_TARGET, "Max block weight: {}", max_block_weight_full.max_block);
357 let max_weight = max_block_weight_full
360 .per_class
361 .get(dispatch_class)
362 .max_total
363 .unwrap_or(max_block_weight_full.max_block);
364 log::debug!(target: LOG_TARGET, "Used max block time weight: {}", max_weight);
365
366 let max_block_size_full = <T as frame_system::Config>::BlockLength::get();
367 let max_block_size = max_block_size_full.max.get(dispatch_class);
368 log::debug!(target: LOG_TARGET, "Used max block size: {}", max_block_size);
369
370 max_weight.set_proof_size(*max_block_size as u64)
372 };
373 log::debug!(target: LOG_TARGET, "Used max block weight: {}", max_block_weight);
374
375 let entropy = compute_entropy::<T>(parent_hash);
376 let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into());
377
378 if let Err(()) = T::DisputesHandler::deduplicate_and_sort_dispute_data(&mut disputes) {
380 log::debug!(target: LOG_TARGET, "Found duplicate statement sets, retaining the first");
381 }
382
383 let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period;
384
385 let dispute_statement_set_valid = move |set: DisputeStatementSet| {
386 T::DisputesHandler::filter_dispute_data(set, post_conclusion_acceptance_period)
387 };
388
389 let (checked_disputes_sets, checked_disputes_sets_consumed_weight) =
392 limit_and_sanitize_disputes::<T, _>(
393 disputes,
394 dispute_statement_set_valid,
395 max_block_weight,
396 );
397
398 let mut all_weight_after = {
399 let non_disputes_weight = apply_weight_limit::<T>(
402 &mut backed_candidates,
403 &mut bitfields,
404 max_block_weight.saturating_sub(checked_disputes_sets_consumed_weight),
405 &mut rng,
406 );
407
408 let all_weight_after =
409 non_disputes_weight.saturating_add(checked_disputes_sets_consumed_weight);
410
411 METRICS.on_after_filter(all_weight_after.ref_time());
412 log::debug!(
413 target: LOG_TARGET,
414 "[process_inherent_data] after filter: bitfields.len(): {}, backed_candidates.len(): {}, checked_disputes_sets.len() {}",
415 bitfields.len(),
416 backed_candidates.len(),
417 checked_disputes_sets.len()
418 );
419 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());
420 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());
421
422 if all_weight_after.any_gt(max_block_weight) {
423 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());
424 }
425 all_weight_after
426 };
427
428 if let Err(e) =
435 T::DisputesHandler::process_checked_multi_dispute_data(&checked_disputes_sets)
436 {
437 log::warn!(target: LOG_TARGET, "MultiDisputesData failed to update: {:?}", e);
438 };
439 METRICS.on_disputes_imported(checked_disputes_sets.len() as u64);
440
441 set_scrapable_on_chain_disputes::<T>(current_session, checked_disputes_sets.clone());
442
443 if T::DisputesHandler::is_frozen() {
444 METRICS.on_relay_chain_freeze();
446
447 let disputes = checked_disputes_sets
448 .into_iter()
449 .map(|checked| checked.into())
450 .collect::<Vec<_>>();
451 let processed = ParachainsInherentData {
452 bitfields: Vec::new(),
453 backed_candidates: Vec::new(),
454 disputes,
455 parent_header,
456 };
457
458 return Ok((processed, Some(checked_disputes_sets_consumed_weight).into()));
460 }
461
462 let current_concluded_invalid_disputes = checked_disputes_sets
470 .iter()
471 .map(AsRef::as_ref)
472 .filter(|dss| dss.session == current_session)
473 .map(|dss| (dss.session, dss.candidate_hash))
474 .filter(|(session, candidate)| {
475 <T>::DisputesHandler::concluded_invalid(*session, *candidate)
476 })
477 .map(|(_session, candidate)| candidate)
478 .collect::<BTreeSet<CandidateHash>>();
479
480 let (freed_disputed, concluded_invalid_hashes): (Vec<CoreIndex>, BTreeSet<CandidateHash>) =
482 inclusion::Pallet::<T>::free_disputed(¤t_concluded_invalid_disputes)
483 .into_iter()
484 .unzip();
485
486 let disputed_bitfield = create_disputed_bitfield(expected_bits, freed_disputed.iter());
491
492 let bitfields = sanitize_bitfields::<T>(
493 bitfields,
494 disputed_bitfield,
495 expected_bits,
496 parent_hash,
497 current_session,
498 &validator_public[..],
499 );
500 METRICS.on_bitfields_processed(bitfields.len() as u64);
501
502 let (enact_weight, freed_concluded) =
505 inclusion::Pallet::<T>::update_pending_availability_and_get_freed_cores(
506 &validator_public[..],
507 bitfields.clone(),
508 );
509 all_weight_after.saturating_accrue(enact_weight);
510 log::debug!(
511 target: LOG_TARGET,
512 "Enacting weight: {}, all weight: {}",
513 enact_weight.ref_time(),
514 all_weight_after.ref_time(),
515 );
516
517 if all_weight_after.any_gt(max_block_weight) {
522 log::warn!(
523 target: LOG_TARGET,
524 "Overweight para inherent data after enacting the candidates {:?}: {} > {}",
525 parent_hash,
526 all_weight_after,
527 max_block_weight,
528 );
529 }
530
531 for (_, candidate_hash) in &freed_concluded {
533 T::DisputesHandler::note_included(current_session, *candidate_hash, now);
534 }
535
536 METRICS.on_candidates_included(freed_concluded.len() as u64);
537
538 let freed_timeout = if scheduler::Pallet::<T>::availability_timeout_check_required() {
540 inclusion::Pallet::<T>::free_timedout()
541 } else {
542 Vec::new()
543 };
544
545 if !freed_timeout.is_empty() {
546 log::debug!(target: LOG_TARGET, "Evicted timed out cores: {:?}", freed_timeout);
547 }
548
549 let (candidate_receipt_with_backing_validator_indices, backed_candidates_with_core) =
551 Self::back_candidates(concluded_invalid_hashes, backed_candidates)?;
552
553 set_scrapable_on_chain_backings::<T>(
554 current_session,
555 candidate_receipt_with_backing_validator_indices,
556 );
557
558 let disputes = checked_disputes_sets
559 .into_iter()
560 .map(|checked| checked.into())
561 .collect::<Vec<_>>();
562
563 let bitfields = bitfields.into_iter().map(|v| v.into_unchecked()).collect();
564
565 let count = backed_candidates_with_core.len();
566 let processed = ParachainsInherentData {
567 bitfields,
568 backed_candidates: backed_candidates_with_core.into_iter().fold(
569 Vec::with_capacity(count),
570 |mut acc, (_id, candidates)| {
571 acc.extend(candidates.into_iter().map(|(c, _)| c));
572 acc
573 },
574 ),
575 disputes,
576 parent_header,
577 };
578 Ok((processed, Some(all_weight_after).into()))
579 }
580
581 fn back_candidates(
582 concluded_invalid_hashes: BTreeSet<CandidateHash>,
583 backed_candidates: Vec<BackedCandidate<T::Hash>>,
584 ) -> Result<
585 (
586 Vec<(CandidateReceipt<T::Hash>, Vec<(ValidatorIndex, ValidityAttestation)>)>,
587 BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>>,
588 ),
589 DispatchErrorWithPostInfo,
590 > {
591 let allowed_scheduling_parents = shared::AllowedSchedulingParents::<T>::get();
592
593 let upcoming_new_session = initializer::Pallet::<T>::upcoming_session_change();
594
595 METRICS.on_candidates_processed_total(backed_candidates.len() as u64);
596
597 let occupied_cores: BTreeSet<_> =
598 inclusion::Pallet::<T>::get_occupied_cores().map(|(core, _)| core).collect();
599
600 let mut eligible: BTreeMap<ParaId, BTreeSet<CoreIndex>> = BTreeMap::new();
601
602 let is_blocked = |core_idx| occupied_cores.contains(&core_idx) || upcoming_new_session;
603 let scheduled = scheduler::Pallet::<T>::advance_claim_queue(is_blocked);
604 let total_eligible_cores = scheduled.len();
605
606 for (core_idx, para_id) in scheduled {
607 eligible.entry(para_id).or_default().insert(core_idx);
608 }
609
610 let node_features = configuration::ActiveConfig::<T>::get().node_features;
611 let v3_enabled = FeatureIndex::CandidateReceiptV3.is_set(&node_features);
612
613 let backed_candidates_with_core = sanitize_backed_candidates::<T>(
614 backed_candidates,
615 &allowed_scheduling_parents,
616 concluded_invalid_hashes,
617 eligible,
618 v3_enabled,
619 );
620 let count = count_backed_candidates(&backed_candidates_with_core);
621
622 ensure!(count <= total_eligible_cores, Error::<T>::UnscheduledCandidate);
623
624 METRICS.on_candidates_sanitized(count as u64);
625
626 let candidate_receipt_with_backing_validator_indices =
628 inclusion::Pallet::<T>::process_candidates(
629 &allowed_scheduling_parents,
630 &backed_candidates_with_core,
631 scheduler::Pallet::<T>::group_validators,
632 )?;
633
634 Ok((candidate_receipt_with_backing_validator_indices, backed_candidates_with_core))
635 }
636}
637
638pub(super) fn create_disputed_bitfield<'a, I>(
640 expected_bits: usize,
641 freed_cores: I,
642) -> DisputedBitfield
643where
644 I: 'a + IntoIterator<Item = &'a CoreIndex>,
645{
646 let mut bitvec = BitVec::repeat(false, expected_bits);
647 for core_idx in freed_cores {
648 let core_idx = core_idx.0 as usize;
649 if core_idx < expected_bits {
650 bitvec.set(core_idx, true);
651 }
652 }
653 DisputedBitfield::from(bitvec)
654}
655
656fn random_sel<X, F: Fn(&X) -> Weight>(
664 rng: &mut rand_chacha::ChaChaRng,
665 selectables: &[X],
666 mut preferred_indices: Vec<usize>,
667 weight_fn: F,
668 weight_limit: Weight,
669) -> (Weight, Vec<usize>) {
670 if selectables.is_empty() {
671 return (Weight::zero(), Vec::new());
672 }
673 let mut indices = (0..selectables.len())
675 .into_iter()
676 .filter(|idx| !preferred_indices.contains(idx))
677 .collect::<Vec<_>>();
678 let mut picked_indices = Vec::with_capacity(selectables.len().saturating_sub(1));
679
680 let mut weight_acc = Weight::zero();
681
682 preferred_indices.shuffle(rng);
683 for preferred_idx in preferred_indices {
684 if let Some(item) = selectables.get(preferred_idx) {
686 let updated = weight_acc.saturating_add(weight_fn(item));
687 if updated.any_gt(weight_limit) {
688 continue;
689 }
690 weight_acc = updated;
691 picked_indices.push(preferred_idx);
692 }
693 }
694
695 indices.shuffle(rng);
696 for idx in indices {
697 let item = &selectables[idx];
698 let updated = weight_acc.saturating_add(weight_fn(item));
699
700 if updated.any_gt(weight_limit) {
701 continue;
702 }
703 weight_acc = updated;
704
705 picked_indices.push(idx);
706 }
707
708 picked_indices.sort_unstable();
712 (weight_acc, picked_indices)
713}
714
715pub(crate) fn apply_weight_limit<T: Config + inclusion::Config>(
733 candidates: &mut Vec<BackedCandidate<<T>::Hash>>,
734 bitfields: &mut UncheckedSignedAvailabilityBitfields,
735 max_consumable_weight: Weight,
736 rng: &mut rand_chacha::ChaChaRng,
737) -> Weight {
738 let total_candidates_weight = backed_candidates_weight::<T>(candidates.as_slice());
739
740 let total_bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
741
742 let total = total_bitfields_weight.saturating_add(total_candidates_weight);
743
744 if max_consumable_weight.all_gte(total) {
746 return total;
747 }
748
749 let mut chained_candidates: Vec<Vec<_>> = Vec::new();
754 let mut current_para_id = None;
755
756 for candidate in core::mem::take(candidates).into_iter() {
757 let candidate_para_id = candidate.descriptor().para_id();
758 if Some(candidate_para_id) == current_para_id {
759 let chain = chained_candidates
760 .last_mut()
761 .expect("if the current_para_id is Some, then vec is not empty; qed");
762 chain.push(candidate);
763 } else {
764 current_para_id = Some(candidate_para_id);
765 chained_candidates.push(vec![candidate]);
766 }
767 }
768
769 let preferred_chain_indices = chained_candidates
777 .iter()
778 .enumerate()
779 .filter_map(|(idx, candidates)| {
780 if candidates
782 .iter()
783 .any(|candidate| candidate.candidate().commitments.new_validation_code.is_some())
784 {
785 Some(idx)
786 } else {
787 None
788 }
789 })
790 .collect::<Vec<usize>>();
791
792 if let Some(max_consumable_by_candidates) =
795 max_consumable_weight.checked_sub(&total_bitfields_weight)
796 {
797 let (acc_candidate_weight, chained_indices) =
798 random_sel::<Vec<BackedCandidate<<T as frame_system::Config>::Hash>>, _>(
799 rng,
800 &chained_candidates,
801 preferred_chain_indices,
802 |candidates| backed_candidates_weight::<T>(&candidates),
803 max_consumable_by_candidates,
804 );
805 log::debug!(target: LOG_TARGET, "Indices Candidates: {:?}, size: {}", chained_indices, candidates.len());
806 chained_candidates
807 .indexed_retain(|idx, _backed_candidates| chained_indices.binary_search(&idx).is_ok());
808 let total_consumed = acc_candidate_weight.saturating_add(total_bitfields_weight);
811
812 *candidates = chained_candidates.into_iter().flatten().collect::<Vec<_>>();
813
814 return total_consumed;
815 }
816
817 candidates.clear();
818
819 let (total_consumed, indices) = random_sel::<UncheckedSignedAvailabilityBitfield, _>(
822 rng,
823 &bitfields,
824 vec![],
825 |bitfield| signed_bitfield_weight::<T>(&bitfield),
826 max_consumable_weight,
827 );
828 log::debug!(target: LOG_TARGET, "Indices Bitfields: {:?}, size: {}", indices, bitfields.len());
829
830 bitfields.indexed_retain(|idx, _bitfield| indices.binary_search(&idx).is_ok());
831
832 total_consumed
833}
834
835pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
847 unchecked_bitfields: UncheckedSignedAvailabilityBitfields,
848 disputed_bitfield: DisputedBitfield,
849 expected_bits: usize,
850 parent_hash: T::Hash,
851 session_index: SessionIndex,
852 validators: &[ValidatorId],
853) -> SignedAvailabilityBitfields {
854 let mut bitfields = Vec::with_capacity(unchecked_bitfields.len());
855
856 let mut last_index: Option<ValidatorIndex> = None;
857
858 if disputed_bitfield.0.len() != expected_bits {
859 log::error!(target: LOG_TARGET, "BUG: disputed_bitfield != expected_bits");
862 return vec![];
863 }
864
865 let all_zeros = BitVec::<u8, bitvec::order::Lsb0>::repeat(false, expected_bits);
866 let signing_context = SigningContext { parent_hash, session_index };
867 for unchecked_bitfield in unchecked_bitfields {
868 if unchecked_bitfield.unchecked_payload().0.len() != expected_bits {
870 log::trace!(
871 target: LOG_TARGET,
872 "bad bitfield length: {} != {:?}",
873 unchecked_bitfield.unchecked_payload().0.len(),
874 expected_bits,
875 );
876 continue;
877 }
878
879 if unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() !=
880 all_zeros
881 {
882 log::trace!(
883 target: LOG_TARGET,
884 "bitfield contains disputed cores: {:?}",
885 unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone()
886 );
887 continue;
888 }
889
890 let validator_index = unchecked_bitfield.unchecked_validator_index();
891
892 if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) {
893 log::trace!(
894 target: LOG_TARGET,
895 "bitfield validator index is not greater than last: !({:?} < {})",
896 last_index.as_ref().map(|x| x.0),
897 validator_index.0
898 );
899 continue;
900 }
901
902 if unchecked_bitfield.unchecked_validator_index().0 as usize >= validators.len() {
903 log::trace!(
904 target: LOG_TARGET,
905 "bitfield validator index is out of bounds: {} >= {}",
906 validator_index.0,
907 validators.len(),
908 );
909 continue;
910 }
911
912 let validator_public = &validators[validator_index.0 as usize];
913
914 if let Ok(signed_bitfield) =
916 unchecked_bitfield.try_into_checked(&signing_context, validator_public)
917 {
918 bitfields.push(signed_bitfield);
919 METRICS.on_valid_bitfield_signature();
920 } else {
921 log::warn!(target: LOG_TARGET, "Invalid bitfield signature");
922 METRICS.on_invalid_bitfield_signature();
923 };
924
925 last_index = Some(validator_index);
926 }
927 bitfields
928}
929
930fn check_descriptor_version_and_signals<T: crate::inclusion::Config>(
950 candidate: &BackedCandidate<T::Hash>,
951 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
952 v3_enabled: bool,
953) -> bool {
954 let current_session_index = shared::CurrentSessionIndex::<T>::get();
955 let descriptor_version = candidate.descriptor().version();
956
957 if matches!(descriptor_version, CandidateDescriptorVersion::Unknown(_)) {
958 log::debug!(
959 target: LOG_TARGET,
960 "Candidate with unknown descriptor version. Dropping candidate {:?} for paraid {:?}.",
961 candidate.candidate().hash(),
962 candidate.descriptor().para_id()
963 );
964 return false;
965 }
966
967 if let Err(reason) = candidate.descriptor().check_version_acceptance(v3_enabled) {
969 log::debug!(
970 target: LOG_TARGET,
971 "{}. Dropping candidate {:?} for paraid {:?}.",
972 reason,
973 candidate.candidate().hash(),
974 candidate.descriptor().para_id()
975 );
976 return false;
977 }
978
979 let relay_parent = candidate.descriptor().relay_parent();
982
983 let session_index = candidate.descriptor().session_index().unwrap_or(current_session_index);
984
985 if shared::Pallet::<T>::get_relay_parent_info(session_index, relay_parent).is_none() {
986 log::debug!(
987 target: LOG_TARGET,
988 "Relay parent {:?} for candidate {:?} is not in the allowed relay parents of session {}.",
989 relay_parent,
990 candidate.candidate().hash(),
991 session_index,
992 );
993 return false;
994 };
995
996 let scheduling_parent = candidate.descriptor().scheduling_parent();
1005 let Some((sp_info, _)) = allowed_scheduling_parents.acquire_info(scheduling_parent) else {
1006 log::debug!(
1007 target: LOG_TARGET,
1008 "Scheduling parent {:?} for candidate {:?} is not in the allowed scheduling parents.",
1009 scheduling_parent,
1010 candidate.candidate().hash(),
1011 );
1012 return false;
1013 };
1014
1015 if let Err(err) = candidate.candidate().parse_ump_signals(&sp_info.claim_queue) {
1019 log::debug!(
1020 target: LOG_TARGET,
1021 "UMP signal check failed: {:?}. Dropping candidate {:?} for paraid {:?}.",
1022 err,
1023 candidate.candidate().hash(),
1024 candidate.descriptor().para_id()
1025 );
1026 return false;
1027 }
1028
1029 if descriptor_version == CandidateDescriptorVersion::V1 {
1030 return true;
1032 }
1033
1034 let Some(scheduling_session) = candidate.descriptor().scheduling_session() else {
1038 log::debug!(
1039 target: LOG_TARGET,
1040 "Invalid V2/V3 candidate receipt {:?} for paraid {:?}, missing scheduling session.",
1041 candidate.candidate().hash(),
1042 candidate.descriptor().para_id(),
1043 );
1044 return false;
1045 };
1046
1047 if scheduling_session != current_session_index {
1049 log::debug!(
1050 target: LOG_TARGET,
1051 "Dropping candidate receipt {:?} for paraid {:?}, invalid scheduling session {}, current session {}",
1052 candidate.candidate().hash(),
1053 candidate.descriptor().para_id(),
1054 scheduling_session,
1055 current_session_index
1056 );
1057 return false;
1058 }
1059
1060 true
1061}
1062
1063fn sanitize_backed_candidates<T: crate::inclusion::Config>(
1106 backed_candidates: Vec<BackedCandidate<T::Hash>>,
1107 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1108 concluded_invalid_with_descendants: BTreeSet<CandidateHash>,
1109 scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>>,
1110 v3_enabled: bool,
1111) -> BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>> {
1112 let mut candidates_per_para: BTreeMap<ParaId, Vec<_>> = BTreeMap::new();
1115
1116 for candidate in backed_candidates {
1117 if !check_descriptor_version_and_signals::<T>(
1118 &candidate,
1119 allowed_scheduling_parents,
1120 v3_enabled,
1121 ) {
1122 continue;
1123 }
1124
1125 candidates_per_para
1126 .entry(candidate.descriptor().para_id())
1127 .or_default()
1128 .push(candidate);
1129 }
1130
1131 filter_unchained_candidates::<T>(&mut candidates_per_para);
1134
1135 retain_candidates::<T, _, _>(&mut candidates_per_para, |_, candidate| {
1138 let keep = !concluded_invalid_with_descendants.contains(&candidate.candidate().hash());
1139
1140 if !keep {
1141 log::debug!(
1142 target: LOG_TARGET,
1143 "Found backed candidate {:?} which was concluded invalid or is a descendant of a concluded invalid candidate, for paraid {:?}.",
1144 candidate.candidate().hash(),
1145 candidate.descriptor().para_id()
1146 );
1147 }
1148 keep
1149 });
1150
1151 let mut backed_candidates_with_core =
1154 map_candidates_to_cores::<T>(&allowed_scheduling_parents, scheduled, candidates_per_para);
1155
1156 filter_backed_statements_from_disabled_validators::<T>(
1160 &mut backed_candidates_with_core,
1161 &allowed_scheduling_parents,
1162 );
1163
1164 backed_candidates_with_core
1165}
1166
1167fn count_backed_candidates<B>(backed_candidates: &BTreeMap<ParaId, Vec<B>>) -> usize {
1168 backed_candidates.values().map(|c| c.len()).sum()
1169}
1170
1171fn compute_entropy<T: Config>(parent_hash: T::Hash) -> [u8; 32] {
1176 const CANDIDATE_SEED_SUBJECT: [u8; 32] = *b"candidate-seed-selection-subject";
1177 let vrf_random = ParentBlockRandomness::<T>::random(&CANDIDATE_SEED_SUBJECT[..]).0;
1182 let mut entropy: [u8; 32] = CANDIDATE_SEED_SUBJECT;
1183 if let Some(vrf_random) = vrf_random {
1184 entropy.as_mut().copy_from_slice(vrf_random.as_ref());
1185 } else {
1186 log::warn!(target: LOG_TARGET, "ParentBlockRandomness did not provide entropy");
1189 entropy.as_mut().copy_from_slice(parent_hash.as_ref());
1190 }
1191 entropy
1192}
1193
1194fn limit_and_sanitize_disputes<
1209 T: Config,
1210 CheckValidityFn: FnMut(DisputeStatementSet) -> Option<CheckedDisputeStatementSet>,
1211>(
1212 disputes: MultiDisputeStatementSet,
1213 mut dispute_statement_set_valid: CheckValidityFn,
1214 max_consumable_weight: Weight,
1215) -> (Vec<CheckedDisputeStatementSet>, Weight) {
1216 let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
1218
1219 if disputes_weight.any_gt(max_consumable_weight) {
1220 log::debug!(target: LOG_TARGET, "Above max consumable weight: {}/{}", disputes_weight, max_consumable_weight);
1221 let mut checked_acc = Vec::<CheckedDisputeStatementSet>::with_capacity(disputes.len());
1222
1223 let mut weight_acc = Weight::zero();
1225
1226 disputes.into_iter().for_each(|dss| {
1228 let dispute_weight = dispute_statement_set_weight::<T, &DisputeStatementSet>(&dss);
1229 let updated = weight_acc.saturating_add(dispute_weight);
1230 if max_consumable_weight.all_gte(updated) {
1231 weight_acc = updated;
1233 if let Some(checked) = dispute_statement_set_valid(dss) {
1234 checked_acc.push(checked);
1235 }
1236 }
1237 });
1238
1239 (checked_acc, weight_acc)
1240 } else {
1241 let checked = disputes
1243 .into_iter()
1244 .filter_map(|dss| dispute_statement_set_valid(dss))
1245 .collect::<Vec<CheckedDisputeStatementSet>>();
1246 let checked_disputes_weight = checked_multi_dispute_statement_sets_weight::<T>(&checked);
1248 (checked, checked_disputes_weight)
1249 }
1250}
1251
1252fn retain_candidates<
1255 T: inclusion::Config + paras::Config + inclusion::Config,
1256 F: FnMut(ParaId, &mut C) -> bool,
1257 C,
1258>(
1259 candidates_per_para: &mut BTreeMap<ParaId, Vec<C>>,
1260 mut pred: F,
1261) {
1262 for (para_id, candidates) in candidates_per_para.iter_mut() {
1263 let mut latest_valid_idx = None;
1264
1265 for (idx, candidate) in candidates.iter_mut().enumerate() {
1266 if pred(*para_id, candidate) {
1267 latest_valid_idx = Some(idx);
1269 } else {
1270 break;
1271 }
1272 }
1273
1274 if let Some(latest_valid_idx) = latest_valid_idx {
1275 candidates.truncate(latest_valid_idx + 1);
1276 } else {
1277 candidates.clear();
1278 }
1279 }
1280
1281 candidates_per_para.retain(|_, c| !c.is_empty());
1282}
1283
1284fn filter_backed_statements_from_disabled_validators<
1287 T: shared::Config + scheduler::Config + inclusion::Config,
1288>(
1289 backed_candidates_with_core: &mut BTreeMap<
1290 ParaId,
1291 Vec<(BackedCandidate<<T as frame_system::Config>::Hash>, CoreIndex)>,
1292 >,
1293 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1294) {
1295 let disabled_validators =
1296 BTreeSet::<_>::from_iter(shared::Pallet::<T>::disabled_validators().into_iter());
1297
1298 if disabled_validators.is_empty() {
1299 return;
1301 }
1302
1303 let minimum_backing_votes = configuration::ActiveConfig::<T>::get().minimum_backing_votes;
1304
1305 retain_candidates::<T, _, _>(backed_candidates_with_core, |para_id, (bc, core_idx)| {
1310 let (validator_indices, maybe_injected_core_index) = bc.validator_indices_and_core_index();
1312 let mut validator_indices = BitVec::<_>::from(validator_indices);
1313
1314 let scheduling_parent_block_number = match allowed_scheduling_parents
1317 .acquire_info(bc.descriptor().scheduling_parent())
1318 {
1319 Some((_, block_num)) => block_num,
1320 None => {
1321 log::debug!(
1322 target: LOG_TARGET,
1323 "Scheduling parent {:?} for candidate is not in the allowed scheduling parents. Dropping the candidate.",
1324 bc.descriptor().scheduling_parent()
1325 );
1326 return false;
1327 },
1328 };
1329
1330 let group_idx = match scheduler::Pallet::<T>::group_assigned_to_core(
1332 *core_idx,
1333 scheduling_parent_block_number + One::one(),
1334 ) {
1335 Some(group_idx) => group_idx,
1336 None => {
1337 log::debug!(target: LOG_TARGET, "Can't get the group index for core idx {:?}. Dropping the candidate.", core_idx);
1338 return false;
1339 },
1340 };
1341
1342 let validator_group = match scheduler::Pallet::<T>::group_validators(group_idx) {
1344 Some(validator_group) => validator_group,
1345 None => {
1346 log::debug!(target: LOG_TARGET, "Can't get the validators from group {:?}. Dropping the candidate.", group_idx);
1347 return false;
1348 },
1349 };
1350
1351 let disabled_indices = BitVec::<u8, bitvec::order::Lsb0>::from_iter(
1353 validator_group.iter().map(|idx| disabled_validators.contains(idx)),
1354 );
1355 let indices_to_drop = disabled_indices.clone() & &validator_indices;
1358
1359 for idx in indices_to_drop.iter_ones().rev() {
1361 let mapped_idx = validator_indices[..idx].count_ones();
1368 bc.validity_votes_mut().remove(mapped_idx);
1369 }
1370
1371 validator_indices &= !disabled_indices;
1373 bc.set_validator_indices_and_core_index(validator_indices, maybe_injected_core_index);
1375
1376 if bc.validity_votes().len() <
1380 effective_minimum_backing_votes(validator_group.len(), minimum_backing_votes)
1381 {
1382 log::debug!(
1383 target: LOG_TARGET,
1384 "Dropping candidate {:?} of paraid {:?} because it was left with too few backing votes after votes from disabled validators were filtered.",
1385 bc.candidate().hash(),
1386 para_id
1387 );
1388
1389 return false;
1390 }
1391
1392 true
1393 });
1394}
1395
1396fn filter_unchained_candidates<T: inclusion::Config + paras::Config + inclusion::Config>(
1401 candidates: &mut BTreeMap<ParaId, Vec<BackedCandidate<T::Hash>>>,
1402) {
1403 let mut para_latest_context: BTreeMap<ParaId, (HeadData, BlockNumberFor<T>)> = BTreeMap::new();
1404 for para_id in candidates.keys() {
1405 let Some(latest_head_data) = inclusion::Pallet::<T>::para_latest_head_data(¶_id) else {
1406 defensive!("Latest included head data for paraid {:?} is None", para_id);
1407 continue;
1408 };
1409 let Some(latest_relay_parent) = inclusion::Pallet::<T>::para_most_recent_context(¶_id)
1410 else {
1411 defensive!("Latest relay parent for paraid {:?} is None", para_id);
1412 continue;
1413 };
1414 para_latest_context.insert(*para_id, (latest_head_data, latest_relay_parent));
1415 }
1416
1417 let mut para_visited_candidates: BTreeMap<ParaId, BTreeSet<CandidateHash>> = BTreeMap::new();
1418
1419 retain_candidates::<T, _, _>(candidates, |para_id, candidate| {
1420 let Some((latest_head_data, latest_relay_parent)) = para_latest_context.get(¶_id)
1421 else {
1422 return false;
1423 };
1424 let candidate_hash = candidate.candidate().hash();
1425
1426 let visited_candidates =
1427 para_visited_candidates.entry(para_id).or_insert_with(|| BTreeSet::new());
1428 if visited_candidates.contains(&candidate_hash) {
1429 log::debug!(
1430 target: LOG_TARGET,
1431 "Found duplicate candidates for paraid {:?}. Dropping the candidates with hash {:?}",
1432 para_id,
1433 candidate_hash
1434 );
1435
1436 return false;
1438 } else {
1439 visited_candidates.insert(candidate_hash);
1440 }
1441
1442 let check_ctx = CandidateCheckContext::<T>::new(Some(*latest_relay_parent));
1443
1444 match check_ctx.verify_backed_candidate(candidate.candidate(), latest_head_data.clone()) {
1445 Ok(relay_parent_block_number) => {
1446 para_latest_context.insert(
1447 para_id,
1448 (
1449 candidate.candidate().commitments.head_data.clone(),
1450 relay_parent_block_number,
1451 ),
1452 );
1453 true
1454 },
1455 Err(err) => {
1456 log::debug!(
1457 target: LOG_TARGET,
1458 "Backed candidate verification for candidate {:?} of paraid {:?} failed with {:?}",
1459 candidate_hash,
1460 para_id,
1461 err
1462 );
1463 false
1464 },
1465 }
1466 });
1467}
1468
1469fn map_candidates_to_cores<T: configuration::Config + scheduler::Config + inclusion::Config>(
1477 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1478 mut scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>>,
1479 candidates: BTreeMap<ParaId, Vec<BackedCandidate<T::Hash>>>,
1480) -> BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>> {
1481 let mut backed_candidates_with_core = BTreeMap::new();
1482
1483 for (para_id, backed_candidates) in candidates.into_iter() {
1484 if backed_candidates.len() == 0 {
1485 defensive!("Backed candidates for paraid {} is empty.", para_id);
1486 continue;
1487 }
1488
1489 let Some(scheduled_cores) = scheduled.get_mut(¶_id) else {
1490 log::debug!(
1491 target: LOG_TARGET,
1492 "Paraid: {:?} has no entry in scheduled cores but {} candidates were supplied.",
1493 para_id,
1494 backed_candidates.len()
1495 );
1496 continue;
1497 };
1498
1499 if scheduled_cores.len() == 0 {
1501 log::debug!(
1502 target: LOG_TARGET,
1503 "Paraid: {:?} has no scheduled cores but {} candidates were supplied.",
1504 para_id,
1505 backed_candidates.len()
1506 );
1507 continue;
1508 }
1509
1510 let mut temp_backed_candidates = Vec::with_capacity(scheduled_cores.len());
1512
1513 for candidate in backed_candidates {
1514 if scheduled_cores.len() == 0 {
1515 log::debug!(
1518 target: LOG_TARGET,
1519 "Found enough candidates for paraid: {:?}.",
1520 candidate.descriptor().para_id()
1521 );
1522 break;
1523 }
1524
1525 if let Some(core_index) = get_core_index::<T>(allowed_scheduling_parents, &candidate) {
1526 if scheduled_cores.remove(&core_index) {
1527 temp_backed_candidates.push((candidate, core_index));
1528 } else {
1529 log::debug!(
1533 target: LOG_TARGET,
1534 "Found a backed candidate {:?} with core index {}, which is not scheduled for paraid {:?}.",
1535 candidate.candidate().hash(),
1536 core_index.0,
1537 candidate.descriptor().para_id()
1538 );
1539
1540 break;
1541 }
1542 } else {
1543 log::debug!(
1548 target: LOG_TARGET,
1549 "Found a backed candidate {:?} without core index information for para {:?}, dropping",
1550 candidate.candidate().hash(),
1551 candidate.descriptor().para_id()
1552 );
1553
1554 break;
1555 }
1556 }
1557
1558 if !temp_backed_candidates.is_empty() {
1559 backed_candidates_with_core
1560 .entry(para_id)
1561 .or_insert_with(|| vec![])
1562 .extend(temp_backed_candidates);
1563 }
1564 }
1565
1566 backed_candidates_with_core
1567}
1568
1569fn get_core_index<T: configuration::Config + scheduler::Config + inclusion::Config>(
1571 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1572 candidate: &BackedCandidate<T::Hash>,
1573) -> Option<CoreIndex> {
1574 candidate
1575 .candidate()
1576 .descriptor
1577 .core_index()
1578 .or_else(|| get_injected_core_index::<T>(allowed_scheduling_parents, &candidate))
1579}
1580
1581fn get_injected_core_index<T: configuration::Config + scheduler::Config + inclusion::Config>(
1582 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1583 candidate: &BackedCandidate<T::Hash>,
1584) -> Option<CoreIndex> {
1585 let (validator_indices, Some(core_idx)) = candidate.validator_indices_and_core_index() else {
1589 return None;
1590 };
1591
1592 let scheduling_parent_block_number =
1593 match allowed_scheduling_parents.acquire_info(candidate.descriptor().scheduling_parent()) {
1594 Some((_, block_num)) => block_num,
1595 None => {
1596 log::debug!(
1597 target: LOG_TARGET,
1598 "Scheduling parent {:?} for candidate {:?} is not in the allowed scheduling parents.",
1599 candidate.descriptor().scheduling_parent(),
1600 candidate.candidate().hash(),
1601 );
1602 return None;
1603 },
1604 };
1605
1606 let group_idx = match scheduler::Pallet::<T>::group_assigned_to_core(
1608 core_idx,
1609 scheduling_parent_block_number + One::one(),
1610 ) {
1611 Some(group_idx) => group_idx,
1612 None => {
1613 log::debug!(
1614 target: LOG_TARGET,
1615 "Can't get the group index for core idx {:?}.",
1616 core_idx,
1617 );
1618 return None;
1619 },
1620 };
1621
1622 let group_validators = match scheduler::Pallet::<T>::group_validators(group_idx) {
1623 Some(validators) => validators,
1624 None => return None,
1625 };
1626
1627 if group_validators.len() == validator_indices.len() {
1628 Some(core_idx)
1629 } else {
1630 log::debug!(
1631 target: LOG_TARGET,
1632 "Expected validator_indices count different than the real one: {}, {} for candidate {:?}",
1633 group_validators.len(),
1634 validator_indices.len(),
1635 candidate.candidate().hash()
1636 );
1637
1638 None
1639 }
1640}