Skip to content

Commit f2d4c48

Browse files
tdimitrovordianordianOverkillus
authored
Filter votes from disabled validators in BackedCandidates in process_inherent_data (#1863)
Fixes #1592 Please refer to the issue for details. --------- Co-authored-by: ordian <[email protected]> Co-authored-by: ordian <[email protected]> Co-authored-by: Maciej <[email protected]>
1 parent 5e8b367 commit f2d4c48

File tree

20 files changed

+491
-64
lines changed

20 files changed

+491
-64
lines changed

polkadot/roadmap/implementers-guide/src/runtime/parainherent.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,34 @@ processing it, so the processed inherent data is simply dropped.
6060
This also means that the `enter` function keeps data around for no good reason. This seems acceptable though as the size
6161
of a block is rather limited. Nevertheless if we ever wanted to optimize this we can easily implement an inherent
6262
collector that has two implementations, where one clones and stores the data and the other just passes it on.
63+
64+
## Data sanitization
65+
`ParasInherent` with the entry point of `create_inherent` sanitizes the input data, while the `enter` entry point
66+
enforces already sanitized input data. If unsanitized data is provided the module generates an error.
67+
68+
Disputes are included in the block with a priority for a security reasons. It's important to include as many dispute
69+
votes onchain as possible so that disputes conclude faster and the offenders are punished. However if there are too many
70+
disputes to include in a block the dispute set is trimmed so that it respects max block weight.
71+
72+
Dispute data is first deduplicated and sorted by block number (older first) and dispute location (local then remote).
73+
Concluded and ancient (disputes initiated before the post conclusion acceptance period) disputes are filtered out.
74+
Votes with invalid signatures or from unknown validators (not found in the active set for the current session) are also
75+
filtered out.
76+
77+
All dispute statements are included in the order described in the previous paragraph until the available block weight is
78+
exhausted. After the dispute data is included all remaining weight is filled in with candidates and availability
79+
bitfields. Bitfields are included with priority, then candidates containing code updates and finally any backed
80+
candidates. If there is not enough weight for all backed candidates they are trimmed by random selection. Disputes are
81+
processed in three separate functions - `deduplicate_and_sort_dispute_data`, `filter_dispute_data` and
82+
`limit_and_sanitize_disputes`.
83+
84+
Availability bitfields are also sanitized by dropping malformed ones, containing disputed cores or bad signatures. Refer
85+
to `sanitize_bitfields` function for implementation details.
86+
87+
Backed candidates sanitization removes malformed ones, candidates which have got concluded invalid disputes against them
88+
or candidates produced by unassigned cores. Furthermore any backing votes from disabled validators for a candidate are
89+
dropped. This is part of the validator disabling strategy. After filtering the statements from disabled validators a
90+
backed candidate may end up with votes count less than `minimum_backing_votes` (a parameter from `HostConfiguiration`).
91+
In this case the whole candidate is dropped otherwise it will be rejected by `process_candidates` from pallet inclusion.
92+
All checks related to backed candidates are implemented in `sanitize_backed_candidates` and
93+
`filter_backed_statements_from_disabled_validators`.

polkadot/runtime/common/src/assigned_slots/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,9 @@ mod tests {
745745
type OnNewHead = ();
746746
}
747747

748-
impl parachains_shared::Config for Test {}
748+
impl parachains_shared::Config for Test {
749+
type DisabledValidators = ();
750+
}
749751

750752
parameter_types! {
751753
pub const LeasePeriod: BlockNumber = 3;

polkadot/runtime/common/src/integration_tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ impl configuration::Config for Test {
197197
type WeightInfo = configuration::TestWeightInfo;
198198
}
199199

200-
impl shared::Config for Test {}
200+
impl shared::Config for Test {
201+
type DisabledValidators = ();
202+
}
201203

202204
impl origin::Config for Test {}
203205

polkadot/runtime/common/src/paras_registrar/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,9 @@ mod tests {
799799
type MaxFreezes = ConstU32<1>;
800800
}
801801

802-
impl shared::Config for Test {}
802+
impl shared::Config for Test {
803+
type DisabledValidators = ();
804+
}
803805

804806
impl origin::Config for Test {}
805807

polkadot/runtime/parachains/src/mock.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,22 @@ impl crate::configuration::Config for Test {
184184
type WeightInfo = crate::configuration::TestWeightInfo;
185185
}
186186

187-
impl crate::shared::Config for Test {}
187+
pub struct MockDisabledValidators {}
188+
impl frame_support::traits::DisabledValidators for MockDisabledValidators {
189+
/// Returns true if the given validator is disabled.
190+
fn is_disabled(index: u32) -> bool {
191+
disabled_validators().iter().any(|v| *v == index)
192+
}
193+
194+
/// Returns a hardcoded list (`DISABLED_VALIDATORS`) of disabled validators
195+
fn disabled_validators() -> Vec<u32> {
196+
disabled_validators()
197+
}
198+
}
199+
200+
impl crate::shared::Config for Test {
201+
type DisabledValidators = MockDisabledValidators;
202+
}
188203

189204
impl origin::Config for Test {}
190205

@@ -432,6 +447,8 @@ thread_local! {
432447

433448
pub static AVAILABILITY_REWARDS: RefCell<HashMap<ValidatorIndex, usize>>
434449
= RefCell::new(HashMap::new());
450+
451+
pub static DISABLED_VALIDATORS: RefCell<Vec<u32>> = RefCell::new(vec![]);
435452
}
436453

437454
pub fn backing_rewards() -> HashMap<ValidatorIndex, usize> {
@@ -442,6 +459,10 @@ pub fn availability_rewards() -> HashMap<ValidatorIndex, usize> {
442459
AVAILABILITY_REWARDS.with(|r| r.borrow().clone())
443460
}
444461

462+
pub fn disabled_validators() -> Vec<u32> {
463+
DISABLED_VALIDATORS.with(|r| r.borrow().clone())
464+
}
465+
445466
parameter_types! {
446467
pub static Processed: Vec<(ParaId, UpwardMessage)> = vec![];
447468
}
@@ -581,3 +602,7 @@ pub(crate) fn deregister_parachain(id: ParaId) {
581602
pub(crate) fn try_deregister_parachain(id: ParaId) -> crate::DispatchResult {
582603
frame_support::storage::transactional::with_storage_layer(|| Paras::schedule_para_cleanup(id))
583604
}
605+
606+
pub(crate) fn set_disabled_validators(disabled: Vec<u32>) {
607+
DISABLED_VALIDATORS.with(|d| *d.borrow_mut() = disabled)
608+
}

polkadot/runtime/parachains/src/paras_inherent/mod.rs

Lines changed: 160 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ use crate::{
3030
metrics::METRICS,
3131
paras,
3232
scheduler::{self, FreedReason},
33-
shared, ParaId,
33+
shared::{self, AllowedRelayParentsTracker},
34+
ParaId,
3435
};
3536
use bitvec::prelude::BitVec;
3637
use frame_support::{
@@ -42,8 +43,8 @@ use frame_support::{
4243
use frame_system::pallet_prelude::*;
4344
use pallet_babe::{self, ParentBlockRandomness};
4445
use primitives::{
45-
BackedCandidate, CandidateHash, CandidateReceipt, CheckedDisputeStatementSet,
46-
CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet,
46+
effective_minimum_backing_votes, BackedCandidate, CandidateHash, CandidateReceipt,
47+
CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet,
4748
InherentData as ParachainsInherentData, MultiDisputeStatementSet, ScrapedOnChainVotes,
4849
SessionIndex, SignedAvailabilityBitfields, SigningContext, UncheckedSignedAvailabilityBitfield,
4950
UncheckedSignedAvailabilityBitfields, ValidatorId, ValidatorIndex, ValidityAttestation,
@@ -142,6 +143,8 @@ pub mod pallet {
142143
DisputeStatementsUnsortedOrDuplicates,
143144
/// A dispute statement was invalid.
144145
DisputeInvalid,
146+
/// A candidate was backed by a disabled validator
147+
BackedByDisabled,
145148
}
146149

147150
/// Whether the paras inherent was included within this block.
@@ -378,6 +381,7 @@ impl<T: Config> Pallet<T> {
378381
let bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
379382
let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
380383

384+
// Weight before filtering/sanitization
381385
let all_weight_before = candidates_weight + bitfields_weight + disputes_weight;
382386

383387
METRICS.on_before_filter(all_weight_before.ref_time());
@@ -587,17 +591,19 @@ impl<T: Config> Pallet<T> {
587591

588592
METRICS.on_candidates_processed_total(backed_candidates.len() as u64);
589593

590-
let backed_candidates = sanitize_backed_candidates::<T, _>(
591-
backed_candidates,
592-
|candidate_idx: usize,
593-
backed_candidate: &BackedCandidate<<T as frame_system::Config>::Hash>|
594-
-> bool {
595-
let para_id = backed_candidate.descriptor().para_id;
596-
let prev_context = <paras::Pallet<T>>::para_most_recent_context(para_id);
597-
let check_ctx = CandidateCheckContext::<T>::new(prev_context);
598-
599-
// never include a concluded-invalid candidate
600-
current_concluded_invalid_disputes.contains(&backed_candidate.hash()) ||
594+
let SanitizedBackedCandidates { backed_candidates, votes_from_disabled_were_dropped } =
595+
sanitize_backed_candidates::<T, _>(
596+
backed_candidates,
597+
&allowed_relay_parents,
598+
|candidate_idx: usize,
599+
backed_candidate: &BackedCandidate<<T as frame_system::Config>::Hash>|
600+
-> bool {
601+
let para_id = backed_candidate.descriptor().para_id;
602+
let prev_context = <paras::Pallet<T>>::para_most_recent_context(para_id);
603+
let check_ctx = CandidateCheckContext::<T>::new(prev_context);
604+
605+
// never include a concluded-invalid candidate
606+
current_concluded_invalid_disputes.contains(&backed_candidate.hash()) ||
601607
// Instead of checking the candidates with code upgrades twice
602608
// move the checking up here and skip it in the training wheels fallback.
603609
// That way we avoid possible duplicate checks while assuring all
@@ -607,12 +613,19 @@ impl<T: Config> Pallet<T> {
607613
check_ctx
608614
.verify_backed_candidate(&allowed_relay_parents, candidate_idx, backed_candidate)
609615
.is_err()
610-
},
611-
&scheduled,
612-
);
616+
},
617+
&scheduled,
618+
);
613619

614620
METRICS.on_candidates_sanitized(backed_candidates.len() as u64);
615621

622+
// In `Enter` context (invoked during execution) there should be no backing votes from
623+
// disabled validators because they should have been filtered out during inherent data
624+
// preparation (`ProvideInherent` context). Abort in such cases.
625+
if context == ProcessInherentDataContext::Enter {
626+
ensure!(!votes_from_disabled_were_dropped, Error::<T>::BackedByDisabled);
627+
}
628+
616629
// Process backed candidates according to scheduled cores.
617630
let inclusion::ProcessedCandidates::<<HeaderFor<T> as HeaderT>::Hash> {
618631
core_indices: occupied,
@@ -900,7 +913,19 @@ pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
900913
bitfields
901914
}
902915

903-
/// Filter out any candidates that have a concluded invalid dispute.
916+
// Result from `sanitize_backed_candidates`
917+
#[derive(Debug, PartialEq)]
918+
struct SanitizedBackedCandidates<Hash> {
919+
// Sanitized backed candidates. The `Vec` is sorted according to the occupied core index.
920+
backed_candidates: Vec<BackedCandidate<Hash>>,
921+
// Set to true if any votes from disabled validators were dropped from the input.
922+
votes_from_disabled_were_dropped: bool,
923+
}
924+
925+
/// Filter out:
926+
/// 1. any candidates that have a concluded invalid dispute
927+
/// 2. all backing votes from disabled validators
928+
/// 3. any candidates that end up with less than `effective_minimum_backing_votes` backing votes
904929
///
905930
/// `scheduled` follows the same naming scheme as provided in the
906931
/// guide: Currently `free` but might become `occupied`.
@@ -910,15 +935,17 @@ pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
910935
/// `candidate_has_concluded_invalid_dispute` must return `true` if the candidate
911936
/// is disputed, false otherwise. The passed `usize` is the candidate index.
912937
///
913-
/// The returned `Vec` is sorted according to the occupied core index.
938+
/// Returns struct `SanitizedBackedCandidates` where `backed_candidates` are sorted according to the
939+
/// occupied core index.
914940
fn sanitize_backed_candidates<
915941
T: crate::inclusion::Config,
916942
F: FnMut(usize, &BackedCandidate<T::Hash>) -> bool,
917943
>(
918944
mut backed_candidates: Vec<BackedCandidate<T::Hash>>,
945+
allowed_relay_parents: &AllowedRelayParentsTracker<T::Hash, BlockNumberFor<T>>,
919946
mut candidate_has_concluded_invalid_dispute_or_is_invalid: F,
920947
scheduled: &BTreeMap<ParaId, CoreIndex>,
921-
) -> Vec<BackedCandidate<T::Hash>> {
948+
) -> SanitizedBackedCandidates<T::Hash> {
922949
// Remove any candidates that were concluded invalid.
923950
// This does not assume sorting.
924951
backed_candidates.indexed_retain(move |candidate_idx, backed_candidate| {
@@ -936,6 +963,13 @@ fn sanitize_backed_candidates<
936963
scheduled.get(&desc.para_id).is_some()
937964
});
938965

966+
// Filter out backing statements from disabled validators
967+
let dropped_disabled = filter_backed_statements_from_disabled_validators::<T>(
968+
&mut backed_candidates,
969+
&allowed_relay_parents,
970+
scheduled,
971+
);
972+
939973
// Sort the `Vec` last, once there is a guarantee that these
940974
// `BackedCandidates` references the expected relay chain parent,
941975
// but more importantly are scheduled for a free core.
@@ -946,7 +980,10 @@ fn sanitize_backed_candidates<
946980
scheduled[&x.descriptor().para_id].cmp(&scheduled[&y.descriptor().para_id])
947981
});
948982

949-
backed_candidates
983+
SanitizedBackedCandidates {
984+
backed_candidates,
985+
votes_from_disabled_were_dropped: dropped_disabled,
986+
}
950987
}
951988

952989
/// Derive entropy from babe provided per block randomness.
@@ -1029,3 +1066,105 @@ fn limit_and_sanitize_disputes<
10291066
(checked, checked_disputes_weight)
10301067
}
10311068
}
1069+
1070+
// Filters statements from disabled validators in `BackedCandidate`, non-scheduled candidates and
1071+
// few more sanity checks. Returns `true` if at least one statement is removed and `false`
1072+
// otherwise.
1073+
fn filter_backed_statements_from_disabled_validators<T: shared::Config + scheduler::Config>(
1074+
backed_candidates: &mut Vec<BackedCandidate<<T as frame_system::Config>::Hash>>,
1075+
allowed_relay_parents: &AllowedRelayParentsTracker<T::Hash, BlockNumberFor<T>>,
1076+
scheduled: &BTreeMap<ParaId, CoreIndex>,
1077+
) -> bool {
1078+
let disabled_validators =
1079+
BTreeSet::<_>::from_iter(shared::Pallet::<T>::disabled_validators().into_iter());
1080+
1081+
if disabled_validators.is_empty() {
1082+
// No disabled validators - nothing to do
1083+
return false
1084+
}
1085+
1086+
let backed_len_before = backed_candidates.len();
1087+
1088+
// Flag which will be returned. Set to `true` if at least one vote is filtered.
1089+
let mut filtered = false;
1090+
1091+
let minimum_backing_votes = configuration::Pallet::<T>::config().minimum_backing_votes;
1092+
1093+
// Process all backed candidates. `validator_indices` in `BackedCandidates` are indices within
1094+
// the validator group assigned to the parachain. To obtain this group we need:
1095+
// 1. Core index assigned to the parachain which has produced the candidate
1096+
// 2. The relay chain block number of the candidate
1097+
backed_candidates.retain_mut(|bc| {
1098+
// Get `core_idx` assigned to the `para_id` of the candidate
1099+
let core_idx = match scheduled.get(&bc.descriptor().para_id) {
1100+
Some(core_idx) => *core_idx,
1101+
None => {
1102+
log::debug!(target: LOG_TARGET, "Can't get core idx of a backed candidate for para id {:?}. Dropping the candidate.", bc.descriptor().para_id);
1103+
return false
1104+
}
1105+
};
1106+
1107+
// Get relay parent block number of the candidate. We need this to get the group index assigned to this core at this block number
1108+
let relay_parent_block_number = match allowed_relay_parents
1109+
.acquire_info(bc.descriptor().relay_parent, None) {
1110+
Some((_, block_num)) => block_num,
1111+
None => {
1112+
log::debug!(target: LOG_TARGET, "Relay parent {:?} for candidate is not in the allowed relay parents. Dropping the candidate.", bc.descriptor().relay_parent);
1113+
return false
1114+
}
1115+
};
1116+
1117+
// Get the group index for the core
1118+
let group_idx = match <scheduler::Pallet<T>>::group_assigned_to_core(
1119+
core_idx,
1120+
relay_parent_block_number + One::one(),
1121+
) {
1122+
Some(group_idx) => group_idx,
1123+
None => {
1124+
log::debug!(target: LOG_TARGET, "Can't get the group index for core idx {:?}. Dropping the candidate.", core_idx);
1125+
return false
1126+
},
1127+
};
1128+
1129+
// And finally get the validator group for this group index
1130+
let validator_group = match <scheduler::Pallet<T>>::group_validators(group_idx) {
1131+
Some(validator_group) => validator_group,
1132+
None => {
1133+
log::debug!(target: LOG_TARGET, "Can't get the validators from group {:?}. Dropping the candidate.", group_idx);
1134+
return false
1135+
}
1136+
};
1137+
1138+
// Bitmask with the disabled indices within the validator group
1139+
let disabled_indices = BitVec::<u8, bitvec::order::Lsb0>::from_iter(validator_group.iter().map(|idx| disabled_validators.contains(idx)));
1140+
// The indices of statements from disabled validators in `BackedCandidate`. We have to drop these.
1141+
let indices_to_drop = disabled_indices.clone() & &bc.validator_indices;
1142+
// Apply the bitmask to drop the disabled validator from `validator_indices`
1143+
bc.validator_indices &= !disabled_indices;
1144+
// Remove the corresponding votes from `validity_votes`
1145+
for idx in indices_to_drop.iter_ones().rev() {
1146+
bc.validity_votes.remove(idx);
1147+
}
1148+
1149+
// If at least one statement was dropped we need to return `true`
1150+
if indices_to_drop.count_ones() > 0 {
1151+
filtered = true;
1152+
}
1153+
1154+
// By filtering votes we might render the candidate invalid and cause a failure in
1155+
// [`process_candidates`]. To avoid this we have to perform a sanity check here. If there
1156+
// are not enough backing votes after filtering we will remove the whole candidate.
1157+
if bc.validity_votes.len() < effective_minimum_backing_votes(
1158+
validator_group.len(),
1159+
minimum_backing_votes
1160+
1161+
) {
1162+
return false
1163+
}
1164+
1165+
true
1166+
});
1167+
1168+
// Also return `true` if a whole candidate was dropped from the set
1169+
filtered || backed_len_before != backed_candidates.len()
1170+
}

0 commit comments

Comments
 (0)