1use crate::{
25 configuration,
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 + scheduler::Config + initializer::Config + pallet_babe::Config
118 {
119 type WeightInfo: WeightInfo;
121 }
122
123 #[pallet::error]
124 pub enum Error<T> {
125 TooManyInclusionInherents,
127 InvalidParentHeader,
130 InherentDataFilteredDuringExecution,
133 UnscheduledCandidate,
135 }
136
137 #[pallet::storage]
144 pub(crate) type Included<T> = StorageValue<_, ()>;
145
146 #[pallet::storage]
148 pub type OnChainVotes<T: Config> = StorageValue<_, ScrapedOnChainVotes<T::Hash>>;
149
150 pub(crate) fn set_scrapable_on_chain_disputes<T: Config>(
152 session: SessionIndex,
153 checked_disputes: CheckedMultiDisputeStatementSet,
154 ) {
155 crate::paras_inherent::OnChainVotes::<T>::mutate(move |value| {
156 let disputes =
157 checked_disputes.into_iter().map(DisputeStatementSet::from).collect::<Vec<_>>();
158 let backing_validators_per_candidate = match value.take() {
159 Some(v) => v.backing_validators_per_candidate,
160 None => Vec::new(),
161 };
162 *value = Some(ScrapedOnChainVotes::<T::Hash> {
163 backing_validators_per_candidate,
164 disputes,
165 session,
166 });
167 })
168 }
169
170 pub(crate) fn set_scrapable_on_chain_backings<T: Config>(
172 session: SessionIndex,
173 backing_validators_per_candidate: Vec<(
174 CandidateReceipt<T::Hash>,
175 Vec<(ValidatorIndex, ValidityAttestation)>,
176 )>,
177 ) {
178 crate::paras_inherent::OnChainVotes::<T>::mutate(move |value| {
179 let disputes = match value.take() {
180 Some(v) => v.disputes,
181 None => MultiDisputeStatementSet::default(),
182 };
183 *value = Some(ScrapedOnChainVotes::<T::Hash> {
184 backing_validators_per_candidate,
185 disputes,
186 session,
187 });
188 })
189 }
190
191 #[pallet::hooks]
192 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
193 fn on_initialize(_: BlockNumberFor<T>) -> Weight {
194 T::DbWeight::get().reads_writes(1, 1) }
196
197 fn on_finalize(_: BlockNumberFor<T>) {
198 if Included::<T>::take().is_none() {
199 panic!("ParachainInherent was not executed in this block. This is a bug. Please report this at https://github.com/paritytech/polkadot-sdk/issues.");
200 }
201 }
202 }
203
204 #[pallet::inherent]
205 impl<T: Config> ProvideInherent for Pallet<T> {
206 type Call = Call<T>;
207 type Error = MakeFatalError<()>;
208 const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER;
209
210 fn create_inherent(data: &InherentData) -> Option<Self::Call> {
211 let inherent_data = Self::create_inherent_inner(data)?;
212
213 Some(Call::enter { data: inherent_data })
214 }
215
216 fn is_inherent(call: &Self::Call) -> bool {
217 matches!(call, Call::enter { .. })
218 }
219 }
220
221 #[pallet::call]
222 impl<T: Config> Pallet<T> {
223 #[pallet::call_index(0)]
225 #[pallet::weight((
226 paras_inherent_total_weight::<T>(
227 data.backed_candidates.as_slice(),
228 &data.bitfields,
229 &data.disputes,
230 ),
231 DispatchClass::Mandatory,
232 ))]
233 pub fn enter(
234 origin: OriginFor<T>,
235 data: ParachainsInherentData<HeaderFor<T>>,
236 ) -> DispatchResultWithPostInfo {
237 ensure_none(origin)?;
238
239 ensure!(!Included::<T>::exists(), Error::<T>::TooManyInclusionInherents);
240 Included::<T>::set(Some(()));
241 let initial_data = data.clone();
242
243 Self::process_inherent_data(data).and_then(|(processed, post_info)| {
244 ensure!(initial_data == processed, Error::<T>::InherentDataFilteredDuringExecution);
245 Ok(post_info)
246 })
247 }
248 }
249}
250
251impl<T: Config> Pallet<T> {
252 fn create_inherent_inner(data: &InherentData) -> Option<ParachainsInherentData<HeaderFor<T>>> {
256 let parachains_inherent_data = match data.get_data(&Self::INHERENT_IDENTIFIER) {
257 Ok(Some(d)) => d,
258 Ok(None) => return None,
259 Err(_) => {
260 log::warn!(target: LOG_TARGET, "ParachainsInherentData failed to decode");
261 return None;
262 },
263 };
264 match Self::process_inherent_data(parachains_inherent_data) {
265 Ok((processed, _)) => Some(processed),
266 Err(err) => {
267 log::warn!(target: LOG_TARGET, "Processing inherent data failed: {:?}", err);
268 None
269 },
270 }
271 }
272
273 fn process_inherent_data(
281 data: ParachainsInherentData<HeaderFor<T>>,
282 ) -> Result<(ParachainsInherentData<HeaderFor<T>>, PostDispatchInfo), DispatchErrorWithPostInfo>
283 {
284 #[cfg(feature = "runtime-metrics")]
285 sp_io::init_tracing();
286
287 let ParachainsInherentData {
288 mut bitfields,
289 mut backed_candidates,
290 parent_header,
291 mut disputes,
292 } = data;
293
294 log::debug!(
295 target: LOG_TARGET,
296 "[process_inherent_data] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}",
297 bitfields.len(),
298 backed_candidates.len(),
299 disputes.len()
300 );
301
302 let parent_hash = frame_system::Pallet::<T>::parent_hash();
303
304 ensure!(
305 parent_header.hash().as_ref() == parent_hash.as_ref(),
306 Error::<T>::InvalidParentHeader,
307 );
308
309 let now = frame_system::Pallet::<T>::block_number();
310 let config = configuration::ActiveConfig::<T>::get();
311
312 let current_session = shared::CurrentSessionIndex::<T>::get();
313
314 {
316 let parent_number = now.saturating_sub(One::one());
317 let parent_storage_root = *parent_header.state_root();
318
319 shared::Pallet::<T>::new_block(
320 parent_hash,
321 scheduler::Pallet::<T>::claim_queue(),
322 parent_number,
323 config.scheduler_params.lookahead,
324 parent_storage_root,
325 current_session,
326 config.max_relay_parent_session_age,
327 );
328 }
329
330 let candidates_weight = backed_candidates_weight::<T>(&backed_candidates);
331 let bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
332 let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
333
334 let weight_before_filtering = candidates_weight + bitfields_weight + disputes_weight;
336
337 METRICS.on_before_filter(weight_before_filtering.ref_time());
338 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());
339 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());
340
341 let expected_bits = scheduler::Pallet::<T>::num_availability_cores();
342 let validator_public = shared::ActiveValidatorKeys::<T>::get();
343
344 let max_block_weight = {
351 let dispatch_class = DispatchClass::Mandatory;
352 let max_block_weight_full = <T as frame_system::Config>::BlockWeights::get();
353 log::debug!(target: LOG_TARGET, "Max block weight: {}", max_block_weight_full.max_block);
354 let max_weight = max_block_weight_full
357 .per_class
358 .get(dispatch_class)
359 .max_total
360 .unwrap_or(max_block_weight_full.max_block);
361 log::debug!(target: LOG_TARGET, "Used max block time weight: {}", max_weight);
362
363 let max_block_size_full = <T as frame_system::Config>::BlockLength::get();
364 let max_block_size = max_block_size_full.max.get(dispatch_class);
365 log::debug!(target: LOG_TARGET, "Used max block size: {}", max_block_size);
366
367 max_weight.set_proof_size(*max_block_size as u64)
369 };
370 log::debug!(target: LOG_TARGET, "Used max block weight: {}", max_block_weight);
371
372 let entropy = compute_entropy::<T>(parent_hash);
373 let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into());
374
375 if let Err(()) = T::DisputesHandler::deduplicate_and_sort_dispute_data(&mut disputes) {
377 log::debug!(target: LOG_TARGET, "Found duplicate statement sets, retaining the first");
378 }
379
380 let post_conclusion_acceptance_period = config.dispute_post_conclusion_acceptance_period;
381
382 let dispute_statement_set_valid = move |set: DisputeStatementSet| {
383 T::DisputesHandler::filter_dispute_data(set, post_conclusion_acceptance_period)
384 };
385
386 let (checked_disputes_sets, checked_disputes_sets_consumed_weight) =
389 limit_and_sanitize_disputes::<T, _>(
390 disputes,
391 dispute_statement_set_valid,
392 max_block_weight,
393 );
394
395 let mut all_weight_after = {
396 let non_disputes_weight = apply_weight_limit::<T>(
399 &mut backed_candidates,
400 &mut bitfields,
401 max_block_weight.saturating_sub(checked_disputes_sets_consumed_weight),
402 &mut rng,
403 );
404
405 let all_weight_after =
406 non_disputes_weight.saturating_add(checked_disputes_sets_consumed_weight);
407
408 METRICS.on_after_filter(all_weight_after.ref_time());
409 log::debug!(
410 target: LOG_TARGET,
411 "[process_inherent_data] after filter: bitfields.len(): {}, backed_candidates.len(): {}, checked_disputes_sets.len() {}",
412 bitfields.len(),
413 backed_candidates.len(),
414 checked_disputes_sets.len()
415 );
416 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());
417 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());
418
419 if all_weight_after.any_gt(max_block_weight) {
420 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());
421 }
422 all_weight_after
423 };
424
425 if let Err(e) =
432 T::DisputesHandler::process_checked_multi_dispute_data(&checked_disputes_sets)
433 {
434 log::warn!(target: LOG_TARGET, "MultiDisputesData failed to update: {:?}", e);
435 };
436 METRICS.on_disputes_imported(checked_disputes_sets.len() as u64);
437
438 set_scrapable_on_chain_disputes::<T>(current_session, checked_disputes_sets.clone());
439
440 if T::DisputesHandler::is_frozen() {
441 METRICS.on_relay_chain_freeze();
443
444 let disputes = checked_disputes_sets
445 .into_iter()
446 .map(|checked| checked.into())
447 .collect::<Vec<_>>();
448 let processed = ParachainsInherentData {
449 bitfields: Vec::new(),
450 backed_candidates: Vec::new(),
451 disputes,
452 parent_header,
453 };
454
455 return Ok((processed, Some(checked_disputes_sets_consumed_weight).into()));
457 }
458
459 let current_concluded_invalid_disputes = checked_disputes_sets
467 .iter()
468 .map(AsRef::as_ref)
469 .filter(|dss| dss.session == current_session)
470 .map(|dss| (dss.session, dss.candidate_hash))
471 .filter(|(session, candidate)| {
472 <T>::DisputesHandler::concluded_invalid(*session, *candidate)
473 })
474 .map(|(_session, candidate)| candidate)
475 .collect::<BTreeSet<CandidateHash>>();
476
477 let (freed_disputed, concluded_invalid_hashes): (Vec<CoreIndex>, BTreeSet<CandidateHash>) =
479 inclusion::Pallet::<T>::free_disputed(¤t_concluded_invalid_disputes)
480 .into_iter()
481 .unzip();
482
483 let disputed_bitfield = create_disputed_bitfield(expected_bits, freed_disputed.iter());
488
489 let bitfields = sanitize_bitfields::<T>(
490 bitfields,
491 disputed_bitfield,
492 expected_bits,
493 parent_hash,
494 current_session,
495 &validator_public[..],
496 );
497 METRICS.on_bitfields_processed(bitfields.len() as u64);
498
499 let (enact_weight, freed_concluded) =
502 inclusion::Pallet::<T>::update_pending_availability_and_get_freed_cores(
503 &validator_public[..],
504 bitfields.clone(),
505 );
506 all_weight_after.saturating_accrue(enact_weight);
507 log::debug!(
508 target: LOG_TARGET,
509 "Enacting weight: {}, all weight: {}",
510 enact_weight.ref_time(),
511 all_weight_after.ref_time(),
512 );
513
514 if all_weight_after.any_gt(max_block_weight) {
519 log::warn!(
520 target: LOG_TARGET,
521 "Overweight para inherent data after enacting the candidates {:?}: {} > {}",
522 parent_hash,
523 all_weight_after,
524 max_block_weight,
525 );
526 }
527
528 for (_, candidate_hash) in &freed_concluded {
530 T::DisputesHandler::note_included(current_session, *candidate_hash, now);
531 }
532
533 METRICS.on_candidates_included(freed_concluded.len() as u64);
534
535 let freed_timeout = if scheduler::Pallet::<T>::availability_timeout_check_required() {
537 inclusion::Pallet::<T>::free_timedout()
538 } else {
539 Vec::new()
540 };
541
542 if !freed_timeout.is_empty() {
543 log::debug!(target: LOG_TARGET, "Evicted timed out cores: {:?}", freed_timeout);
544 }
545
546 let (candidate_receipt_with_backing_validator_indices, backed_candidates_with_core) =
548 Self::back_candidates(concluded_invalid_hashes, backed_candidates)?;
549
550 set_scrapable_on_chain_backings::<T>(
551 current_session,
552 candidate_receipt_with_backing_validator_indices,
553 );
554
555 let disputes = checked_disputes_sets
556 .into_iter()
557 .map(|checked| checked.into())
558 .collect::<Vec<_>>();
559
560 let bitfields = bitfields.into_iter().map(|v| v.into_unchecked()).collect();
561
562 let count = backed_candidates_with_core.len();
563 let processed = ParachainsInherentData {
564 bitfields,
565 backed_candidates: backed_candidates_with_core.into_iter().fold(
566 Vec::with_capacity(count),
567 |mut acc, (_id, candidates)| {
568 acc.extend(candidates.into_iter().map(|(c, _)| c));
569 acc
570 },
571 ),
572 disputes,
573 parent_header,
574 };
575 Ok((processed, Some(all_weight_after).into()))
576 }
577
578 fn back_candidates(
579 concluded_invalid_hashes: BTreeSet<CandidateHash>,
580 backed_candidates: Vec<BackedCandidate<T::Hash>>,
581 ) -> Result<
582 (
583 Vec<(CandidateReceipt<T::Hash>, Vec<(ValidatorIndex, ValidityAttestation)>)>,
584 BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>>,
585 ),
586 DispatchErrorWithPostInfo,
587 > {
588 let allowed_scheduling_parents = shared::AllowedSchedulingParents::<T>::get();
589
590 let upcoming_new_session = initializer::Pallet::<T>::upcoming_session_change();
591
592 METRICS.on_candidates_processed_total(backed_candidates.len() as u64);
593
594 let occupied_cores: BTreeSet<_> =
595 inclusion::Pallet::<T>::get_occupied_cores().map(|(core, _)| core).collect();
596
597 let mut eligible: BTreeMap<ParaId, BTreeSet<CoreIndex>> = BTreeMap::new();
598
599 let is_blocked = |core_idx| occupied_cores.contains(&core_idx) || upcoming_new_session;
600 let scheduled = scheduler::Pallet::<T>::advance_claim_queue(is_blocked);
601 let total_eligible_cores = scheduled.len();
602
603 for (core_idx, para_id) in scheduled {
604 eligible.entry(para_id).or_default().insert(core_idx);
605 }
606
607 let node_features = configuration::ActiveConfig::<T>::get().node_features;
608 let v3_enabled = FeatureIndex::CandidateReceiptV3.is_set(&node_features);
609
610 let backed_candidates_with_core = sanitize_backed_candidates::<T>(
611 backed_candidates,
612 &allowed_scheduling_parents,
613 concluded_invalid_hashes,
614 eligible,
615 v3_enabled,
616 );
617 let count = count_backed_candidates(&backed_candidates_with_core);
618
619 ensure!(count <= total_eligible_cores, Error::<T>::UnscheduledCandidate);
620
621 METRICS.on_candidates_sanitized(count as u64);
622
623 let candidate_receipt_with_backing_validator_indices =
625 inclusion::Pallet::<T>::process_candidates(
626 &allowed_scheduling_parents,
627 &backed_candidates_with_core,
628 scheduler::Pallet::<T>::group_validators,
629 )?;
630
631 Ok((candidate_receipt_with_backing_validator_indices, backed_candidates_with_core))
632 }
633}
634
635pub(super) fn create_disputed_bitfield<'a, I>(
637 expected_bits: usize,
638 freed_cores: I,
639) -> DisputedBitfield
640where
641 I: 'a + IntoIterator<Item = &'a CoreIndex>,
642{
643 let mut bitvec = BitVec::repeat(false, expected_bits);
644 for core_idx in freed_cores {
645 let core_idx = core_idx.0 as usize;
646 if core_idx < expected_bits {
647 bitvec.set(core_idx, true);
648 }
649 }
650 DisputedBitfield::from(bitvec)
651}
652
653fn random_sel<X, F: Fn(&X) -> Weight>(
661 rng: &mut rand_chacha::ChaChaRng,
662 selectables: &[X],
663 mut preferred_indices: Vec<usize>,
664 weight_fn: F,
665 weight_limit: Weight,
666) -> (Weight, Vec<usize>) {
667 if selectables.is_empty() {
668 return (Weight::zero(), Vec::new());
669 }
670 let mut indices = (0..selectables.len())
672 .into_iter()
673 .filter(|idx| !preferred_indices.contains(idx))
674 .collect::<Vec<_>>();
675 let mut picked_indices = Vec::with_capacity(selectables.len().saturating_sub(1));
676
677 let mut weight_acc = Weight::zero();
678
679 preferred_indices.shuffle(rng);
680 for preferred_idx in preferred_indices {
681 if let Some(item) = selectables.get(preferred_idx) {
683 let updated = weight_acc.saturating_add(weight_fn(item));
684 if updated.any_gt(weight_limit) {
685 continue;
686 }
687 weight_acc = updated;
688 picked_indices.push(preferred_idx);
689 }
690 }
691
692 indices.shuffle(rng);
693 for idx in indices {
694 let item = &selectables[idx];
695 let updated = weight_acc.saturating_add(weight_fn(item));
696
697 if updated.any_gt(weight_limit) {
698 continue;
699 }
700 weight_acc = updated;
701
702 picked_indices.push(idx);
703 }
704
705 picked_indices.sort_unstable();
709 (weight_acc, picked_indices)
710}
711
712pub(crate) fn apply_weight_limit<T: Config + inclusion::Config>(
730 candidates: &mut Vec<BackedCandidate<<T>::Hash>>,
731 bitfields: &mut UncheckedSignedAvailabilityBitfields,
732 max_consumable_weight: Weight,
733 rng: &mut rand_chacha::ChaChaRng,
734) -> Weight {
735 let total_candidates_weight = backed_candidates_weight::<T>(candidates.as_slice());
736
737 let total_bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
738
739 let total = total_bitfields_weight.saturating_add(total_candidates_weight);
740
741 if max_consumable_weight.all_gte(total) {
743 return total;
744 }
745
746 let mut chained_candidates: Vec<Vec<_>> = Vec::new();
751 let mut current_para_id = None;
752
753 for candidate in core::mem::take(candidates).into_iter() {
754 let candidate_para_id = candidate.descriptor().para_id();
755 if Some(candidate_para_id) == current_para_id {
756 let chain = chained_candidates
757 .last_mut()
758 .expect("if the current_para_id is Some, then vec is not empty; qed");
759 chain.push(candidate);
760 } else {
761 current_para_id = Some(candidate_para_id);
762 chained_candidates.push(vec![candidate]);
763 }
764 }
765
766 let preferred_chain_indices = chained_candidates
774 .iter()
775 .enumerate()
776 .filter_map(|(idx, candidates)| {
777 if candidates
779 .iter()
780 .any(|candidate| candidate.candidate().commitments.new_validation_code.is_some())
781 {
782 Some(idx)
783 } else {
784 None
785 }
786 })
787 .collect::<Vec<usize>>();
788
789 if let Some(max_consumable_by_candidates) =
792 max_consumable_weight.checked_sub(&total_bitfields_weight)
793 {
794 let (acc_candidate_weight, chained_indices) =
795 random_sel::<Vec<BackedCandidate<<T as frame_system::Config>::Hash>>, _>(
796 rng,
797 &chained_candidates,
798 preferred_chain_indices,
799 |candidates| backed_candidates_weight::<T>(&candidates),
800 max_consumable_by_candidates,
801 );
802 log::debug!(target: LOG_TARGET, "Indices Candidates: {:?}, size: {}", chained_indices, candidates.len());
803 chained_candidates
804 .indexed_retain(|idx, _backed_candidates| chained_indices.binary_search(&idx).is_ok());
805 let total_consumed = acc_candidate_weight.saturating_add(total_bitfields_weight);
808
809 *candidates = chained_candidates.into_iter().flatten().collect::<Vec<_>>();
810
811 return total_consumed;
812 }
813
814 candidates.clear();
815
816 let (total_consumed, indices) = random_sel::<UncheckedSignedAvailabilityBitfield, _>(
819 rng,
820 &bitfields,
821 vec![],
822 |bitfield| signed_bitfield_weight::<T>(&bitfield),
823 max_consumable_weight,
824 );
825 log::debug!(target: LOG_TARGET, "Indices Bitfields: {:?}, size: {}", indices, bitfields.len());
826
827 bitfields.indexed_retain(|idx, _bitfield| indices.binary_search(&idx).is_ok());
828
829 total_consumed
830}
831
832pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
844 unchecked_bitfields: UncheckedSignedAvailabilityBitfields,
845 disputed_bitfield: DisputedBitfield,
846 expected_bits: usize,
847 parent_hash: T::Hash,
848 session_index: SessionIndex,
849 validators: &[ValidatorId],
850) -> SignedAvailabilityBitfields {
851 let mut bitfields = Vec::with_capacity(unchecked_bitfields.len());
852
853 let mut last_index: Option<ValidatorIndex> = None;
854
855 if disputed_bitfield.0.len() != expected_bits {
856 log::error!(target: LOG_TARGET, "BUG: disputed_bitfield != expected_bits");
859 return vec![];
860 }
861
862 let all_zeros = BitVec::<u8, bitvec::order::Lsb0>::repeat(false, expected_bits);
863 let signing_context = SigningContext { parent_hash, session_index };
864 for unchecked_bitfield in unchecked_bitfields {
865 if unchecked_bitfield.unchecked_payload().0.len() != expected_bits {
867 log::trace!(
868 target: LOG_TARGET,
869 "bad bitfield length: {} != {:?}",
870 unchecked_bitfield.unchecked_payload().0.len(),
871 expected_bits,
872 );
873 continue;
874 }
875
876 if unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() !=
877 all_zeros
878 {
879 log::trace!(
880 target: LOG_TARGET,
881 "bitfield contains disputed cores: {:?}",
882 unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone()
883 );
884 continue;
885 }
886
887 let validator_index = unchecked_bitfield.unchecked_validator_index();
888
889 if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) {
890 log::trace!(
891 target: LOG_TARGET,
892 "bitfield validator index is not greater than last: !({:?} < {})",
893 last_index.as_ref().map(|x| x.0),
894 validator_index.0
895 );
896 continue;
897 }
898
899 if unchecked_bitfield.unchecked_validator_index().0 as usize >= validators.len() {
900 log::trace!(
901 target: LOG_TARGET,
902 "bitfield validator index is out of bounds: {} >= {}",
903 validator_index.0,
904 validators.len(),
905 );
906 continue;
907 }
908
909 let validator_public = &validators[validator_index.0 as usize];
910
911 if let Ok(signed_bitfield) =
913 unchecked_bitfield.try_into_checked(&signing_context, validator_public)
914 {
915 bitfields.push(signed_bitfield);
916 METRICS.on_valid_bitfield_signature();
917 } else {
918 log::warn!(target: LOG_TARGET, "Invalid bitfield signature");
919 METRICS.on_invalid_bitfield_signature();
920 };
921
922 last_index = Some(validator_index);
923 }
924 bitfields
925}
926
927fn check_descriptor_version_and_signals<T: crate::inclusion::Config>(
947 candidate: &BackedCandidate<T::Hash>,
948 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
949 v3_enabled: bool,
950) -> bool {
951 let current_session_index = shared::CurrentSessionIndex::<T>::get();
952 let descriptor_version = candidate.descriptor().version();
953
954 if descriptor_version == CandidateDescriptorVersion::Unknown {
955 log::debug!(
956 target: LOG_TARGET,
957 "Candidate with unknown descriptor version. Dropping candidate {:?} for paraid {:?}.",
958 candidate.candidate().hash(),
959 candidate.descriptor().para_id()
960 );
961 return false;
962 }
963
964 if let Err(reason) = candidate.descriptor().check_version_acceptance(v3_enabled) {
966 log::debug!(
967 target: LOG_TARGET,
968 "{}. Dropping candidate {:?} for paraid {:?}.",
969 reason,
970 candidate.candidate().hash(),
971 candidate.descriptor().para_id()
972 );
973 return false;
974 }
975
976 let relay_parent = candidate.descriptor().relay_parent();
979
980 let session_index = candidate.descriptor().session_index().unwrap_or(current_session_index);
981
982 if shared::Pallet::<T>::get_relay_parent_info(session_index, relay_parent).is_none() {
983 log::debug!(
984 target: LOG_TARGET,
985 "Relay parent {:?} for candidate {:?} is not in the allowed relay parents of session {}.",
986 relay_parent,
987 candidate.candidate().hash(),
988 session_index,
989 );
990 return false;
991 };
992
993 let scheduling_parent = candidate.descriptor().scheduling_parent();
1002 let Some((sp_info, _)) = allowed_scheduling_parents.acquire_info(scheduling_parent) else {
1003 log::debug!(
1004 target: LOG_TARGET,
1005 "Scheduling parent {:?} for candidate {:?} is not in the allowed scheduling parents.",
1006 scheduling_parent,
1007 candidate.candidate().hash(),
1008 );
1009 return false;
1010 };
1011
1012 if let Err(err) = candidate.candidate().parse_ump_signals(&sp_info.claim_queue) {
1016 log::debug!(
1017 target: LOG_TARGET,
1018 "UMP signal check failed: {:?}. Dropping candidate {:?} for paraid {:?}.",
1019 err,
1020 candidate.candidate().hash(),
1021 candidate.descriptor().para_id()
1022 );
1023 return false;
1024 }
1025
1026 if descriptor_version == CandidateDescriptorVersion::V1 {
1027 return true;
1029 }
1030
1031 let Some(scheduling_session) = candidate.descriptor().scheduling_session() else {
1035 log::debug!(
1036 target: LOG_TARGET,
1037 "Invalid V2/V3 candidate receipt {:?} for paraid {:?}, missing scheduling session.",
1038 candidate.candidate().hash(),
1039 candidate.descriptor().para_id(),
1040 );
1041 return false;
1042 };
1043
1044 if scheduling_session != current_session_index {
1046 log::debug!(
1047 target: LOG_TARGET,
1048 "Dropping candidate receipt {:?} for paraid {:?}, invalid scheduling session {}, current session {}",
1049 candidate.candidate().hash(),
1050 candidate.descriptor().para_id(),
1051 scheduling_session,
1052 current_session_index
1053 );
1054 return false;
1055 }
1056
1057 true
1058}
1059
1060fn sanitize_backed_candidates<T: crate::inclusion::Config>(
1103 backed_candidates: Vec<BackedCandidate<T::Hash>>,
1104 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1105 concluded_invalid_with_descendants: BTreeSet<CandidateHash>,
1106 scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>>,
1107 v3_enabled: bool,
1108) -> BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>> {
1109 let mut candidates_per_para: BTreeMap<ParaId, Vec<_>> = BTreeMap::new();
1112
1113 for candidate in backed_candidates {
1114 if !check_descriptor_version_and_signals::<T>(
1115 &candidate,
1116 allowed_scheduling_parents,
1117 v3_enabled,
1118 ) {
1119 continue;
1120 }
1121
1122 candidates_per_para
1123 .entry(candidate.descriptor().para_id())
1124 .or_default()
1125 .push(candidate);
1126 }
1127
1128 filter_unchained_candidates::<T>(&mut candidates_per_para);
1131
1132 retain_candidates::<T, _, _>(&mut candidates_per_para, |_, candidate| {
1135 let keep = !concluded_invalid_with_descendants.contains(&candidate.candidate().hash());
1136
1137 if !keep {
1138 log::debug!(
1139 target: LOG_TARGET,
1140 "Found backed candidate {:?} which was concluded invalid or is a descendant of a concluded invalid candidate, for paraid {:?}.",
1141 candidate.candidate().hash(),
1142 candidate.descriptor().para_id()
1143 );
1144 }
1145 keep
1146 });
1147
1148 let mut backed_candidates_with_core =
1151 map_candidates_to_cores::<T>(&allowed_scheduling_parents, scheduled, candidates_per_para);
1152
1153 filter_backed_statements_from_disabled_validators::<T>(
1157 &mut backed_candidates_with_core,
1158 &allowed_scheduling_parents,
1159 );
1160
1161 backed_candidates_with_core
1162}
1163
1164fn count_backed_candidates<B>(backed_candidates: &BTreeMap<ParaId, Vec<B>>) -> usize {
1165 backed_candidates.values().map(|c| c.len()).sum()
1166}
1167
1168fn compute_entropy<T: Config>(parent_hash: T::Hash) -> [u8; 32] {
1173 const CANDIDATE_SEED_SUBJECT: [u8; 32] = *b"candidate-seed-selection-subject";
1174 let vrf_random = ParentBlockRandomness::<T>::random(&CANDIDATE_SEED_SUBJECT[..]).0;
1179 let mut entropy: [u8; 32] = CANDIDATE_SEED_SUBJECT;
1180 if let Some(vrf_random) = vrf_random {
1181 entropy.as_mut().copy_from_slice(vrf_random.as_ref());
1182 } else {
1183 log::warn!(target: LOG_TARGET, "ParentBlockRandomness did not provide entropy");
1186 entropy.as_mut().copy_from_slice(parent_hash.as_ref());
1187 }
1188 entropy
1189}
1190
1191fn limit_and_sanitize_disputes<
1206 T: Config,
1207 CheckValidityFn: FnMut(DisputeStatementSet) -> Option<CheckedDisputeStatementSet>,
1208>(
1209 disputes: MultiDisputeStatementSet,
1210 mut dispute_statement_set_valid: CheckValidityFn,
1211 max_consumable_weight: Weight,
1212) -> (Vec<CheckedDisputeStatementSet>, Weight) {
1213 let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
1215
1216 if disputes_weight.any_gt(max_consumable_weight) {
1217 log::debug!(target: LOG_TARGET, "Above max consumable weight: {}/{}", disputes_weight, max_consumable_weight);
1218 let mut checked_acc = Vec::<CheckedDisputeStatementSet>::with_capacity(disputes.len());
1219
1220 let mut weight_acc = Weight::zero();
1222
1223 disputes.into_iter().for_each(|dss| {
1225 let dispute_weight = dispute_statement_set_weight::<T, &DisputeStatementSet>(&dss);
1226 let updated = weight_acc.saturating_add(dispute_weight);
1227 if max_consumable_weight.all_gte(updated) {
1228 weight_acc = updated;
1230 if let Some(checked) = dispute_statement_set_valid(dss) {
1231 checked_acc.push(checked);
1232 }
1233 }
1234 });
1235
1236 (checked_acc, weight_acc)
1237 } else {
1238 let checked = disputes
1240 .into_iter()
1241 .filter_map(|dss| dispute_statement_set_valid(dss))
1242 .collect::<Vec<CheckedDisputeStatementSet>>();
1243 let checked_disputes_weight = checked_multi_dispute_statement_sets_weight::<T>(&checked);
1245 (checked, checked_disputes_weight)
1246 }
1247}
1248
1249fn retain_candidates<
1252 T: inclusion::Config + paras::Config + inclusion::Config,
1253 F: FnMut(ParaId, &mut C) -> bool,
1254 C,
1255>(
1256 candidates_per_para: &mut BTreeMap<ParaId, Vec<C>>,
1257 mut pred: F,
1258) {
1259 for (para_id, candidates) in candidates_per_para.iter_mut() {
1260 let mut latest_valid_idx = None;
1261
1262 for (idx, candidate) in candidates.iter_mut().enumerate() {
1263 if pred(*para_id, candidate) {
1264 latest_valid_idx = Some(idx);
1266 } else {
1267 break;
1268 }
1269 }
1270
1271 if let Some(latest_valid_idx) = latest_valid_idx {
1272 candidates.truncate(latest_valid_idx + 1);
1273 } else {
1274 candidates.clear();
1275 }
1276 }
1277
1278 candidates_per_para.retain(|_, c| !c.is_empty());
1279}
1280
1281fn filter_backed_statements_from_disabled_validators<
1284 T: shared::Config + scheduler::Config + inclusion::Config,
1285>(
1286 backed_candidates_with_core: &mut BTreeMap<
1287 ParaId,
1288 Vec<(BackedCandidate<<T as frame_system::Config>::Hash>, CoreIndex)>,
1289 >,
1290 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1291) {
1292 let disabled_validators =
1293 BTreeSet::<_>::from_iter(shared::Pallet::<T>::disabled_validators().into_iter());
1294
1295 if disabled_validators.is_empty() {
1296 return;
1298 }
1299
1300 let minimum_backing_votes = configuration::ActiveConfig::<T>::get().minimum_backing_votes;
1301
1302 retain_candidates::<T, _, _>(backed_candidates_with_core, |para_id, (bc, core_idx)| {
1307 let (validator_indices, maybe_injected_core_index) = bc.validator_indices_and_core_index();
1309 let mut validator_indices = BitVec::<_>::from(validator_indices);
1310
1311 let scheduling_parent_block_number = match allowed_scheduling_parents
1314 .acquire_info(bc.descriptor().scheduling_parent())
1315 {
1316 Some((_, block_num)) => block_num,
1317 None => {
1318 log::debug!(
1319 target: LOG_TARGET,
1320 "Scheduling parent {:?} for candidate is not in the allowed scheduling parents. Dropping the candidate.",
1321 bc.descriptor().scheduling_parent()
1322 );
1323 return false;
1324 },
1325 };
1326
1327 let group_idx = match scheduler::Pallet::<T>::group_assigned_to_core(
1329 *core_idx,
1330 scheduling_parent_block_number + One::one(),
1331 ) {
1332 Some(group_idx) => group_idx,
1333 None => {
1334 log::debug!(target: LOG_TARGET, "Can't get the group index for core idx {:?}. Dropping the candidate.", core_idx);
1335 return false;
1336 },
1337 };
1338
1339 let validator_group = match scheduler::Pallet::<T>::group_validators(group_idx) {
1341 Some(validator_group) => validator_group,
1342 None => {
1343 log::debug!(target: LOG_TARGET, "Can't get the validators from group {:?}. Dropping the candidate.", group_idx);
1344 return false;
1345 },
1346 };
1347
1348 let disabled_indices = BitVec::<u8, bitvec::order::Lsb0>::from_iter(
1350 validator_group.iter().map(|idx| disabled_validators.contains(idx)),
1351 );
1352 let indices_to_drop = disabled_indices.clone() & &validator_indices;
1355
1356 for idx in indices_to_drop.iter_ones().rev() {
1358 let mapped_idx = validator_indices[..idx].count_ones();
1365 bc.validity_votes_mut().remove(mapped_idx);
1366 }
1367
1368 validator_indices &= !disabled_indices;
1370 bc.set_validator_indices_and_core_index(validator_indices, maybe_injected_core_index);
1372
1373 if bc.validity_votes().len() <
1377 effective_minimum_backing_votes(validator_group.len(), minimum_backing_votes)
1378 {
1379 log::debug!(
1380 target: LOG_TARGET,
1381 "Dropping candidate {:?} of paraid {:?} because it was left with too few backing votes after votes from disabled validators were filtered.",
1382 bc.candidate().hash(),
1383 para_id
1384 );
1385
1386 return false;
1387 }
1388
1389 true
1390 });
1391}
1392
1393fn filter_unchained_candidates<T: inclusion::Config + paras::Config + inclusion::Config>(
1398 candidates: &mut BTreeMap<ParaId, Vec<BackedCandidate<T::Hash>>>,
1399) {
1400 let mut para_latest_context: BTreeMap<ParaId, (HeadData, BlockNumberFor<T>)> = BTreeMap::new();
1401 for para_id in candidates.keys() {
1402 let Some(latest_head_data) = inclusion::Pallet::<T>::para_latest_head_data(¶_id) else {
1403 defensive!("Latest included head data for paraid {:?} is None", para_id);
1404 continue;
1405 };
1406 let Some(latest_relay_parent) = inclusion::Pallet::<T>::para_most_recent_context(¶_id)
1407 else {
1408 defensive!("Latest relay parent for paraid {:?} is None", para_id);
1409 continue;
1410 };
1411 para_latest_context.insert(*para_id, (latest_head_data, latest_relay_parent));
1412 }
1413
1414 let mut para_visited_candidates: BTreeMap<ParaId, BTreeSet<CandidateHash>> = BTreeMap::new();
1415
1416 retain_candidates::<T, _, _>(candidates, |para_id, candidate| {
1417 let Some((latest_head_data, latest_relay_parent)) = para_latest_context.get(¶_id)
1418 else {
1419 return false;
1420 };
1421 let candidate_hash = candidate.candidate().hash();
1422
1423 let visited_candidates =
1424 para_visited_candidates.entry(para_id).or_insert_with(|| BTreeSet::new());
1425 if visited_candidates.contains(&candidate_hash) {
1426 log::debug!(
1427 target: LOG_TARGET,
1428 "Found duplicate candidates for paraid {:?}. Dropping the candidates with hash {:?}",
1429 para_id,
1430 candidate_hash
1431 );
1432
1433 return false;
1435 } else {
1436 visited_candidates.insert(candidate_hash);
1437 }
1438
1439 let check_ctx = CandidateCheckContext::<T>::new(Some(*latest_relay_parent));
1440
1441 match check_ctx.verify_backed_candidate(candidate.candidate(), latest_head_data.clone()) {
1442 Ok(relay_parent_block_number) => {
1443 para_latest_context.insert(
1444 para_id,
1445 (
1446 candidate.candidate().commitments.head_data.clone(),
1447 relay_parent_block_number,
1448 ),
1449 );
1450 true
1451 },
1452 Err(err) => {
1453 log::debug!(
1454 target: LOG_TARGET,
1455 "Backed candidate verification for candidate {:?} of paraid {:?} failed with {:?}",
1456 candidate_hash,
1457 para_id,
1458 err
1459 );
1460 false
1461 },
1462 }
1463 });
1464}
1465
1466fn map_candidates_to_cores<T: configuration::Config + scheduler::Config + inclusion::Config>(
1474 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1475 mut scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>>,
1476 candidates: BTreeMap<ParaId, Vec<BackedCandidate<T::Hash>>>,
1477) -> BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>> {
1478 let mut backed_candidates_with_core = BTreeMap::new();
1479
1480 for (para_id, backed_candidates) in candidates.into_iter() {
1481 if backed_candidates.len() == 0 {
1482 defensive!("Backed candidates for paraid {} is empty.", para_id);
1483 continue;
1484 }
1485
1486 let Some(scheduled_cores) = scheduled.get_mut(¶_id) else {
1487 log::debug!(
1488 target: LOG_TARGET,
1489 "Paraid: {:?} has no entry in scheduled cores but {} candidates were supplied.",
1490 para_id,
1491 backed_candidates.len()
1492 );
1493 continue;
1494 };
1495
1496 if scheduled_cores.len() == 0 {
1498 log::debug!(
1499 target: LOG_TARGET,
1500 "Paraid: {:?} has no scheduled cores but {} candidates were supplied.",
1501 para_id,
1502 backed_candidates.len()
1503 );
1504 continue;
1505 }
1506
1507 let mut temp_backed_candidates = Vec::with_capacity(scheduled_cores.len());
1509
1510 for candidate in backed_candidates {
1511 if scheduled_cores.len() == 0 {
1512 log::debug!(
1515 target: LOG_TARGET,
1516 "Found enough candidates for paraid: {:?}.",
1517 candidate.descriptor().para_id()
1518 );
1519 break;
1520 }
1521
1522 if let Some(core_index) = get_core_index::<T>(allowed_scheduling_parents, &candidate) {
1523 if scheduled_cores.remove(&core_index) {
1524 temp_backed_candidates.push((candidate, core_index));
1525 } else {
1526 log::debug!(
1530 target: LOG_TARGET,
1531 "Found a backed candidate {:?} with core index {}, which is not scheduled for paraid {:?}.",
1532 candidate.candidate().hash(),
1533 core_index.0,
1534 candidate.descriptor().para_id()
1535 );
1536
1537 break;
1538 }
1539 } else {
1540 log::debug!(
1545 target: LOG_TARGET,
1546 "Found a backed candidate {:?} without core index information for para {:?}, dropping",
1547 candidate.candidate().hash(),
1548 candidate.descriptor().para_id()
1549 );
1550
1551 break;
1552 }
1553 }
1554
1555 if !temp_backed_candidates.is_empty() {
1556 backed_candidates_with_core
1557 .entry(para_id)
1558 .or_insert_with(|| vec![])
1559 .extend(temp_backed_candidates);
1560 }
1561 }
1562
1563 backed_candidates_with_core
1564}
1565
1566fn get_core_index<T: configuration::Config + scheduler::Config + inclusion::Config>(
1568 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1569 candidate: &BackedCandidate<T::Hash>,
1570) -> Option<CoreIndex> {
1571 candidate
1572 .candidate()
1573 .descriptor
1574 .core_index()
1575 .or_else(|| get_injected_core_index::<T>(allowed_scheduling_parents, &candidate))
1576}
1577
1578fn get_injected_core_index<T: configuration::Config + scheduler::Config + inclusion::Config>(
1579 allowed_scheduling_parents: &AllowedSchedulingParentsTracker<T::Hash, BlockNumberFor<T>>,
1580 candidate: &BackedCandidate<T::Hash>,
1581) -> Option<CoreIndex> {
1582 let (validator_indices, Some(core_idx)) = candidate.validator_indices_and_core_index() else {
1586 return None;
1587 };
1588
1589 let scheduling_parent_block_number =
1590 match allowed_scheduling_parents.acquire_info(candidate.descriptor().scheduling_parent()) {
1591 Some((_, block_num)) => block_num,
1592 None => {
1593 log::debug!(
1594 target: LOG_TARGET,
1595 "Scheduling parent {:?} for candidate {:?} is not in the allowed scheduling parents.",
1596 candidate.descriptor().scheduling_parent(),
1597 candidate.candidate().hash(),
1598 );
1599 return None;
1600 },
1601 };
1602
1603 let group_idx = match scheduler::Pallet::<T>::group_assigned_to_core(
1605 core_idx,
1606 scheduling_parent_block_number + One::one(),
1607 ) {
1608 Some(group_idx) => group_idx,
1609 None => {
1610 log::debug!(
1611 target: LOG_TARGET,
1612 "Can't get the group index for core idx {:?}.",
1613 core_idx,
1614 );
1615 return None;
1616 },
1617 };
1618
1619 let group_validators = match scheduler::Pallet::<T>::group_validators(group_idx) {
1620 Some(validators) => validators,
1621 None => return None,
1622 };
1623
1624 if group_validators.len() == validator_indices.len() {
1625 Some(core_idx)
1626 } else {
1627 log::debug!(
1628 target: LOG_TARGET,
1629 "Expected validator_indices count different than the real one: {}, {} for candidate {:?}",
1630 group_validators.len(),
1631 validator_indices.len(),
1632 candidate.candidate().hash()
1633 );
1634
1635 None
1636 }
1637}