Filter votes from disabled validators in BackedCandidates in process_inherent_data (#2889)

Backport of https://github.com/paritytech/polkadot-sdk/pull/1863 to
master

Extend candidate sanitation in paras_inherent by removing backing votes
from disabled validators. Check
https://github.com/paritytech/polkadot-sdk/issues/1592 for more details.

This change is related to the disabling strategy implementation
(https://github.com/paritytech/polkadot-sdk/pull/2226).

---------

Co-authored-by: ordian <noreply@reusable.software>
Co-authored-by: ordian <write@reusable.software>
Co-authored-by: Maciej <maciej.zyszkiewicz@parity.io>
This commit is contained in:
Tsvetomir Dimitrov
2024-01-18 09:33:58 +02:00
committed by GitHub
parent f574868822
commit f8954093b4
21 changed files with 506 additions and 64 deletions
@@ -60,3 +60,35 @@ processing it, so the processed inherent data is simply dropped.
This also means that the `enter` function keeps data around for no good reason. This seems acceptable though as the size
of a block is rather limited. Nevertheless if we ever wanted to optimize this we can easily implement an inherent
collector that has two implementations, where one clones and stores the data and the other just passes it on.
## Sanitization
`ParasInherent` with the entry point of `create_inherent` sanitizes the input data, while the `enter` entry point
enforces already sanitized input data. If unsanitized data is provided the module generates an error.
Disputes are included in the block with a priority for a security reasons. It's important to include as many dispute
votes onchain as possible so that disputes conclude faster and the offenders are punished. However if there are too many
disputes to include in a block the dispute set is trimmed so that it respects max block weight.
Dispute data is first deduplicated and sorted by block number (older first) and dispute location (local then remote).
Concluded and ancient (disputes initiated before the post conclusion acceptance period) disputes are filtered out.
Votes with invalid signatures or from unknown validators (not found in the active set for the current session) are also
filtered out.
All dispute statements are included in the order described in the previous paragraph until the available block weight is
exhausted. After the dispute data is included all remaining weight is filled in with candidates and availability
bitfields. Bitfields are included with priority, then candidates containing code updates and finally any backed
candidates. If there is not enough weight for all backed candidates they are trimmed by random selection. Disputes are
processed in three separate functions - `deduplicate_and_sort_dispute_data`, `filter_dispute_data` and
`limit_and_sanitize_disputes`.
Availability bitfields are also sanitized by dropping malformed ones, containing disputed cores or bad signatures. Refer
to `sanitize_bitfields` function for implementation details.
Backed candidates sanitization removes malformed ones, candidates which have got concluded invalid disputes against them
or candidates produced by unassigned cores. Furthermore any backing votes from disabled validators for a candidate are
dropped. This is part of the validator disabling strategy. After filtering the statements from disabled validators a
backed candidate may end up with votes count less than `minimum_backing_votes` (a parameter from `HostConfiguiration`).
In this case the whole candidate is dropped otherwise it will be rejected by `process_candidates` from pallet inclusion.
All checks related to backed candidates are implemented in `sanitize_backed_candidates` and
`filter_backed_statements_from_disabled_validators`.
@@ -746,7 +746,9 @@ mod tests {
type AssignCoretime = ();
}
impl parachains_shared::Config for Test {}
impl parachains_shared::Config for Test {
type DisabledValidators = ();
}
parameter_types! {
pub const LeasePeriod: BlockNumber = 3;
@@ -197,7 +197,9 @@ impl configuration::Config for Test {
type WeightInfo = configuration::TestWeightInfo;
}
impl shared::Config for Test {}
impl shared::Config for Test {
type DisabledValidators = ();
}
impl origin::Config for Test {}
@@ -799,7 +799,9 @@ mod tests {
type MaxFreezes = ConstU32<1>;
}
impl shared::Config for Test {}
impl shared::Config for Test {
type DisabledValidators = ();
}
impl origin::Config for Test {}
+26 -1
View File
@@ -194,7 +194,22 @@ impl crate::configuration::Config for Test {
type WeightInfo = crate::configuration::TestWeightInfo;
}
impl crate::shared::Config for Test {}
pub struct MockDisabledValidators {}
impl frame_support::traits::DisabledValidators for MockDisabledValidators {
/// Returns true if the given validator is disabled.
fn is_disabled(index: u32) -> bool {
disabled_validators().iter().any(|v| *v == index)
}
/// Returns a hardcoded list (`DISABLED_VALIDATORS`) of disabled validators
fn disabled_validators() -> Vec<u32> {
disabled_validators()
}
}
impl crate::shared::Config for Test {
type DisabledValidators = MockDisabledValidators;
}
impl origin::Config for Test {}
@@ -564,6 +579,8 @@ thread_local! {
pub static AVAILABILITY_REWARDS: RefCell<HashMap<ValidatorIndex, usize>>
= RefCell::new(HashMap::new());
pub static DISABLED_VALIDATORS: RefCell<Vec<u32>> = RefCell::new(vec![]);
}
pub fn backing_rewards() -> HashMap<ValidatorIndex, usize> {
@@ -574,6 +591,10 @@ pub fn availability_rewards() -> HashMap<ValidatorIndex, usize> {
AVAILABILITY_REWARDS.with(|r| r.borrow().clone())
}
pub fn disabled_validators() -> Vec<u32> {
DISABLED_VALIDATORS.with(|r| r.borrow().clone())
}
parameter_types! {
pub static Processed: Vec<(ParaId, UpwardMessage)> = vec![];
}
@@ -713,3 +734,7 @@ pub(crate) fn deregister_parachain(id: ParaId) {
pub(crate) fn try_deregister_parachain(id: ParaId) -> crate::DispatchResult {
frame_support::storage::transactional::with_storage_layer(|| Paras::schedule_para_cleanup(id))
}
pub(crate) fn set_disabled_validators(disabled: Vec<u32>) {
DISABLED_VALIDATORS.with(|d| *d.borrow_mut() = disabled)
}
@@ -30,7 +30,8 @@ use crate::{
metrics::METRICS,
paras,
scheduler::{self, FreedReason},
shared, ParaId,
shared::{self, AllowedRelayParentsTracker},
ParaId,
};
use bitvec::prelude::BitVec;
use frame_support::{
@@ -42,8 +43,8 @@ use frame_support::{
use frame_system::pallet_prelude::*;
use pallet_babe::{self, ParentBlockRandomness};
use primitives::{
BackedCandidate, CandidateHash, CandidateReceipt, CheckedDisputeStatementSet,
CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet,
effective_minimum_backing_votes, BackedCandidate, CandidateHash, CandidateReceipt,
CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex, DisputeStatementSet,
InherentData as ParachainsInherentData, MultiDisputeStatementSet, ScrapedOnChainVotes,
SessionIndex, SignedAvailabilityBitfields, SigningContext, UncheckedSignedAvailabilityBitfield,
UncheckedSignedAvailabilityBitfields, ValidatorId, ValidatorIndex, ValidityAttestation,
@@ -142,6 +143,8 @@ pub mod pallet {
DisputeStatementsUnsortedOrDuplicates,
/// A dispute statement was invalid.
DisputeInvalid,
/// A candidate was backed by a disabled validator
BackedByDisabled,
}
/// Whether the paras inherent was included within this block.
@@ -378,6 +381,7 @@ impl<T: Config> Pallet<T> {
let bitfields_weight = signed_bitfields_weight::<T>(&bitfields);
let disputes_weight = multi_dispute_statement_sets_weight::<T>(&disputes);
// Weight before filtering/sanitization
let all_weight_before = candidates_weight + bitfields_weight + disputes_weight;
METRICS.on_before_filter(all_weight_before.ref_time());
@@ -587,17 +591,19 @@ impl<T: Config> Pallet<T> {
METRICS.on_candidates_processed_total(backed_candidates.len() as u64);
let backed_candidates = sanitize_backed_candidates::<T, _>(
backed_candidates,
|candidate_idx: usize,
backed_candidate: &BackedCandidate<<T as frame_system::Config>::Hash>|
-> bool {
let para_id = backed_candidate.descriptor().para_id;
let prev_context = <paras::Pallet<T>>::para_most_recent_context(para_id);
let check_ctx = CandidateCheckContext::<T>::new(prev_context);
let SanitizedBackedCandidates { backed_candidates, votes_from_disabled_were_dropped } =
sanitize_backed_candidates::<T, _>(
backed_candidates,
&allowed_relay_parents,
|candidate_idx: usize,
backed_candidate: &BackedCandidate<<T as frame_system::Config>::Hash>|
-> bool {
let para_id = backed_candidate.descriptor().para_id;
let prev_context = <paras::Pallet<T>>::para_most_recent_context(para_id);
let check_ctx = CandidateCheckContext::<T>::new(prev_context);
// never include a concluded-invalid candidate
current_concluded_invalid_disputes.contains(&backed_candidate.hash()) ||
// never include a concluded-invalid candidate
current_concluded_invalid_disputes.contains(&backed_candidate.hash()) ||
// Instead of checking the candidates with code upgrades twice
// move the checking up here and skip it in the training wheels fallback.
// That way we avoid possible duplicate checks while assuring all
@@ -607,12 +613,19 @@ impl<T: Config> Pallet<T> {
check_ctx
.verify_backed_candidate(&allowed_relay_parents, candidate_idx, backed_candidate)
.is_err()
},
&scheduled,
);
},
&scheduled,
);
METRICS.on_candidates_sanitized(backed_candidates.len() as u64);
// In `Enter` context (invoked during execution) there should be no backing votes from
// disabled validators because they should have been filtered out during inherent data
// preparation (`ProvideInherent` context). Abort in such cases.
if context == ProcessInherentDataContext::Enter {
ensure!(!votes_from_disabled_were_dropped, Error::<T>::BackedByDisabled);
}
// Process backed candidates according to scheduled cores.
let inclusion::ProcessedCandidates::<<HeaderFor<T> as HeaderT>::Hash> {
core_indices: occupied,
@@ -900,7 +913,19 @@ pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
bitfields
}
/// Filter out any candidates that have a concluded invalid dispute.
// Result from `sanitize_backed_candidates`
#[derive(Debug, PartialEq)]
struct SanitizedBackedCandidates<Hash> {
// Sanitized backed candidates. The `Vec` is sorted according to the occupied core index.
backed_candidates: Vec<BackedCandidate<Hash>>,
// Set to true if any votes from disabled validators were dropped from the input.
votes_from_disabled_were_dropped: bool,
}
/// Filter out:
/// 1. any candidates that have a concluded invalid dispute
/// 2. all backing votes from disabled validators
/// 3. any candidates that end up with less than `effective_minimum_backing_votes` backing votes
///
/// `scheduled` follows the same naming scheme as provided in the
/// guide: Currently `free` but might become `occupied`.
@@ -910,15 +935,17 @@ pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
/// `candidate_has_concluded_invalid_dispute` must return `true` if the candidate
/// is disputed, false otherwise. The passed `usize` is the candidate index.
///
/// The returned `Vec` is sorted according to the occupied core index.
/// Returns struct `SanitizedBackedCandidates` where `backed_candidates` are sorted according to the
/// occupied core index.
fn sanitize_backed_candidates<
T: crate::inclusion::Config,
F: FnMut(usize, &BackedCandidate<T::Hash>) -> bool,
>(
mut backed_candidates: Vec<BackedCandidate<T::Hash>>,
allowed_relay_parents: &AllowedRelayParentsTracker<T::Hash, BlockNumberFor<T>>,
mut candidate_has_concluded_invalid_dispute_or_is_invalid: F,
scheduled: &BTreeMap<ParaId, CoreIndex>,
) -> Vec<BackedCandidate<T::Hash>> {
) -> SanitizedBackedCandidates<T::Hash> {
// Remove any candidates that were concluded invalid.
// This does not assume sorting.
backed_candidates.indexed_retain(move |candidate_idx, backed_candidate| {
@@ -936,6 +963,13 @@ fn sanitize_backed_candidates<
scheduled.get(&desc.para_id).is_some()
});
// Filter out backing statements from disabled validators
let dropped_disabled = filter_backed_statements_from_disabled_validators::<T>(
&mut backed_candidates,
&allowed_relay_parents,
scheduled,
);
// Sort the `Vec` last, once there is a guarantee that these
// `BackedCandidates` references the expected relay chain parent,
// but more importantly are scheduled for a free core.
@@ -946,7 +980,10 @@ fn sanitize_backed_candidates<
scheduled[&x.descriptor().para_id].cmp(&scheduled[&y.descriptor().para_id])
});
backed_candidates
SanitizedBackedCandidates {
backed_candidates,
votes_from_disabled_were_dropped: dropped_disabled,
}
}
/// Derive entropy from babe provided per block randomness.
@@ -1029,3 +1066,105 @@ fn limit_and_sanitize_disputes<
(checked, checked_disputes_weight)
}
}
// Filters statements from disabled validators in `BackedCandidate`, non-scheduled candidates and
// few more sanity checks. Returns `true` if at least one statement is removed and `false`
// otherwise.
fn filter_backed_statements_from_disabled_validators<T: shared::Config + scheduler::Config>(
backed_candidates: &mut Vec<BackedCandidate<<T as frame_system::Config>::Hash>>,
allowed_relay_parents: &AllowedRelayParentsTracker<T::Hash, BlockNumberFor<T>>,
scheduled: &BTreeMap<ParaId, CoreIndex>,
) -> bool {
let disabled_validators =
BTreeSet::<_>::from_iter(shared::Pallet::<T>::disabled_validators().into_iter());
if disabled_validators.is_empty() {
// No disabled validators - nothing to do
return false
}
let backed_len_before = backed_candidates.len();
// Flag which will be returned. Set to `true` if at least one vote is filtered.
let mut filtered = false;
let minimum_backing_votes = configuration::Pallet::<T>::config().minimum_backing_votes;
// Process all backed candidates. `validator_indices` in `BackedCandidates` are indices within
// the validator group assigned to the parachain. To obtain this group we need:
// 1. Core index assigned to the parachain which has produced the candidate
// 2. The relay chain block number of the candidate
backed_candidates.retain_mut(|bc| {
// Get `core_idx` assigned to the `para_id` of the candidate
let core_idx = match scheduled.get(&bc.descriptor().para_id) {
Some(core_idx) => *core_idx,
None => {
log::debug!(target: LOG_TARGET, "Can't get core idx of a backed candidate for para id {:?}. Dropping the candidate.", bc.descriptor().para_id);
return false
}
};
// Get relay parent block number of the candidate. We need this to get the group index assigned to this core at this block number
let relay_parent_block_number = match allowed_relay_parents
.acquire_info(bc.descriptor().relay_parent, None) {
Some((_, block_num)) => block_num,
None => {
log::debug!(target: LOG_TARGET, "Relay parent {:?} for candidate is not in the allowed relay parents. Dropping the candidate.", bc.descriptor().relay_parent);
return false
}
};
// Get the group index for the core
let group_idx = match <scheduler::Pallet<T>>::group_assigned_to_core(
core_idx,
relay_parent_block_number + One::one(),
) {
Some(group_idx) => group_idx,
None => {
log::debug!(target: LOG_TARGET, "Can't get the group index for core idx {:?}. Dropping the candidate.", core_idx);
return false
},
};
// And finally get the validator group for this group index
let validator_group = match <scheduler::Pallet<T>>::group_validators(group_idx) {
Some(validator_group) => validator_group,
None => {
log::debug!(target: LOG_TARGET, "Can't get the validators from group {:?}. Dropping the candidate.", group_idx);
return false
}
};
// Bitmask with the disabled indices within the validator group
let disabled_indices = BitVec::<u8, bitvec::order::Lsb0>::from_iter(validator_group.iter().map(|idx| disabled_validators.contains(idx)));
// The indices of statements from disabled validators in `BackedCandidate`. We have to drop these.
let indices_to_drop = disabled_indices.clone() & &bc.validator_indices;
// Apply the bitmask to drop the disabled validator from `validator_indices`
bc.validator_indices &= !disabled_indices;
// Remove the corresponding votes from `validity_votes`
for idx in indices_to_drop.iter_ones().rev() {
bc.validity_votes.remove(idx);
}
// If at least one statement was dropped we need to return `true`
if indices_to_drop.count_ones() > 0 {
filtered = true;
}
// By filtering votes we might render the candidate invalid and cause a failure in
// [`process_candidates`]. To avoid this we have to perform a sanity check here. If there
// are not enough backing votes after filtering we will remove the whole candidate.
if bc.validity_votes.len() < effective_minimum_backing_votes(
validator_group.len(),
minimum_backing_votes
) {
return false
}
true
});
// Also return `true` if a whole candidate was dropped from the set
filtered || backed_len_before != backed_candidates.len()
}
@@ -1227,6 +1227,12 @@ mod sanitizers {
}
mod candidates {
use crate::{
mock::set_disabled_validators,
scheduler::{common::Assignment, ParasEntry},
};
use sp_std::collections::vec_deque::VecDeque;
use super::*;
// Backed candidates and scheduled parachains used for `sanitize_backed_candidates` testing
@@ -1235,10 +1241,20 @@ mod sanitizers {
scheduled_paras: BTreeMap<primitives::Id, CoreIndex>,
}
// Generate test data for the candidates test
// Generate test data for the candidates and assert that the evnironment is set as expected
// (check the comments for details)
fn get_test_data() -> TestData {
const RELAY_PARENT_NUM: u32 = 3;
// Add the relay parent to `shared` pallet. Otherwise some code (e.g. filtering backing
// votes) won't behave correctly
shared::Pallet::<Test>::add_allowed_relay_parent(
default_header().hash(),
Default::default(),
RELAY_PARENT_NUM,
1,
);
let header = default_header();
let relay_parent = header.hash();
let session_index = SessionIndex::from(0_u32);
@@ -1252,6 +1268,7 @@ mod sanitizers {
keyring::Sr25519Keyring::Bob,
keyring::Sr25519Keyring::Charlie,
keyring::Sr25519Keyring::Dave,
keyring::Sr25519Keyring::Eve,
];
for validator in validators.iter() {
Keystore::sr25519_generate_new(
@@ -1262,11 +1279,42 @@ mod sanitizers {
.unwrap();
}
// Set active validators in `shared` pallet
let validator_ids =
validators.iter().map(|v| v.public().into()).collect::<Vec<ValidatorId>>();
shared::Pallet::<Test>::set_active_validators_ascending(validator_ids);
// Two scheduled parachains - ParaId(1) on CoreIndex(0) and ParaId(2) on CoreIndex(1)
let scheduled = (0_usize..2)
.into_iter()
.map(|idx| (ParaId::from(1_u32 + idx as u32), CoreIndex::from(idx as u32)))
.collect::<BTreeMap<_, _>>();
// Set the validator groups in `scheduler`
scheduler::Pallet::<Test>::set_validator_groups(vec![
vec![ValidatorIndex(0), ValidatorIndex(1)],
vec![ValidatorIndex(2), ValidatorIndex(3)],
]);
// Update scheduler's claimqueue with the parachains
scheduler::Pallet::<Test>::set_claimqueue(BTreeMap::from([
(
CoreIndex::from(0),
VecDeque::from([ParasEntry::new(
Assignment::Pool { para_id: 1.into(), core_index: CoreIndex(1) },
RELAY_PARENT_NUM,
)]),
),
(
CoreIndex::from(1),
VecDeque::from([ParasEntry::new(
Assignment::Pool { para_id: 2.into(), core_index: CoreIndex(1) },
RELAY_PARENT_NUM,
)]),
),
]));
// Callback used for backing candidates
let group_validators = |group_index: GroupIndex| {
match group_index {
group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]),
@@ -1276,6 +1324,7 @@ mod sanitizers {
.map(|m| m.into_iter().map(ValidatorIndex).collect::<Vec<_>>())
};
// Two backed candidates from each parachain
let backed_candidates = (0_usize..2)
.into_iter()
.map(|idx0| {
@@ -1304,6 +1353,22 @@ mod sanitizers {
})
.collect::<Vec<_>>();
// State sanity checks
assert_eq!(
<scheduler::Pallet<Test>>::scheduled_paras().collect::<Vec<_>>(),
vec![(CoreIndex(0), ParaId::from(1)), (CoreIndex(1), ParaId::from(2))]
);
assert_eq!(
shared::Pallet::<Test>::active_validator_indices(),
vec![
ValidatorIndex(0),
ValidatorIndex(1),
ValidatorIndex(2),
ValidatorIndex(3),
ValidatorIndex(4)
]
);
TestData { backed_candidates, scheduled_paras: scheduled }
}
@@ -1318,10 +1383,14 @@ mod sanitizers {
assert_eq!(
sanitize_backed_candidates::<Test, _>(
backed_candidates.clone(),
&<shared::Pallet<Test>>::allowed_relay_parents(),
has_concluded_invalid,
&scheduled
),
backed_candidates
SanitizedBackedCandidates {
backed_candidates,
votes_from_disabled_were_dropped: false
}
);
{}
@@ -1337,12 +1406,18 @@ mod sanitizers {
let has_concluded_invalid =
|_idx: usize, _backed_candidate: &BackedCandidate| -> bool { false };
assert!(sanitize_backed_candidates::<Test, _>(
let SanitizedBackedCandidates {
backed_candidates: sanitized_backed_candidates,
votes_from_disabled_were_dropped,
} = sanitize_backed_candidates::<Test, _>(
backed_candidates.clone(),
&<shared::Pallet<Test>>::allowed_relay_parents(),
has_concluded_invalid,
&scheduled
)
.is_empty());
&scheduled,
);
assert!(sanitized_backed_candidates.is_empty());
assert!(!votes_from_disabled_were_dropped);
});
}
@@ -1364,15 +1439,113 @@ mod sanitizers {
};
let has_concluded_invalid =
|_idx: usize, candidate: &BackedCandidate| set.contains(&candidate.hash());
assert_eq!(
sanitize_backed_candidates::<Test, _>(
backed_candidates.clone(),
has_concluded_invalid,
&scheduled
)
.len(),
backed_candidates.len() / 2
let SanitizedBackedCandidates {
backed_candidates: sanitized_backed_candidates,
votes_from_disabled_were_dropped,
} = sanitize_backed_candidates::<Test, _>(
backed_candidates.clone(),
&<shared::Pallet<Test>>::allowed_relay_parents(),
has_concluded_invalid,
&scheduled,
);
assert_eq!(sanitized_backed_candidates.len(), backed_candidates.len() / 2);
assert!(!votes_from_disabled_were_dropped);
});
}
#[test]
fn disabled_non_signing_validator_doesnt_get_filtered() {
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
let TestData { mut backed_candidates, scheduled_paras } = get_test_data();
// Disable Eve
set_disabled_validators(vec![4]);
let before = backed_candidates.clone();
// Eve is disabled but no backing statement is signed by it so nothing should be
// filtered
assert!(!filter_backed_statements_from_disabled_validators::<Test>(
&mut backed_candidates,
&<shared::Pallet<Test>>::allowed_relay_parents(),
&scheduled_paras
));
assert_eq!(backed_candidates, before);
});
}
#[test]
fn drop_statements_from_disabled_without_dropping_candidate() {
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
let TestData { mut backed_candidates, scheduled_paras } = get_test_data();
// Disable Alice
set_disabled_validators(vec![0]);
// Update `minimum_backing_votes` in HostConfig. We want `minimum_backing_votes` set
// to one so that the candidate will have enough backing votes even after dropping
// Alice's one.
let mut hc = configuration::Pallet::<Test>::config();
hc.minimum_backing_votes = 1;
configuration::Pallet::<Test>::force_set_active_config(hc);
// Verify the initial state is as expected
assert_eq!(backed_candidates.get(0).unwrap().validity_votes.len(), 2);
assert_eq!(
backed_candidates.get(0).unwrap().validator_indices.get(0).unwrap(),
true
);
assert_eq!(
backed_candidates.get(0).unwrap().validator_indices.get(1).unwrap(),
true
);
let untouched = backed_candidates.get(1).unwrap().clone();
assert!(filter_backed_statements_from_disabled_validators::<Test>(
&mut backed_candidates,
&<shared::Pallet<Test>>::allowed_relay_parents(),
&scheduled_paras
));
// there should still be two backed candidates
assert_eq!(backed_candidates.len(), 2);
// but the first one should have only one validity vote
assert_eq!(backed_candidates.get(0).unwrap().validity_votes.len(), 1);
// Validator 0 vote should be dropped, validator 1 - retained
assert_eq!(
backed_candidates.get(0).unwrap().validator_indices.get(0).unwrap(),
false
);
assert_eq!(
backed_candidates.get(0).unwrap().validator_indices.get(1).unwrap(),
true
);
// the second candidate shouldn't be modified
assert_eq!(*backed_candidates.get(1).unwrap(), untouched);
});
}
#[test]
fn drop_candidate_if_all_statements_are_from_disabled() {
new_test_ext(MockGenesisConfig::default()).execute_with(|| {
let TestData { mut backed_candidates, scheduled_paras } = get_test_data();
// Disable Alice and Bob
set_disabled_validators(vec![0, 1]);
// Verify the initial state is as expected
assert_eq!(backed_candidates.get(0).unwrap().validity_votes.len(), 2);
let untouched = backed_candidates.get(1).unwrap().clone();
assert!(filter_backed_statements_from_disabled_validators::<Test>(
&mut backed_candidates,
&<shared::Pallet<Test>>::allowed_relay_parents(),
&scheduled_paras
));
assert_eq!(backed_candidates.len(), 1);
assert_eq!(*backed_candidates.get(0).unwrap(), untouched);
});
}
}
@@ -21,29 +21,16 @@ use primitives::{
vstaging::{ApprovalVotingParams, NodeFeatures},
ValidatorIndex,
};
use sp_std::{collections::btree_map::BTreeMap, prelude::Vec};
use sp_std::prelude::Vec;
/// Implementation for `DisabledValidators`
// CAVEAT: this should only be called on the node side
// as it might produce incorrect results on session boundaries
pub fn disabled_validators<T>() -> Vec<ValidatorIndex>
where
T: pallet_session::Config + shared::Config,
T: shared::Config,
{
let shuffled_indices = <shared::Pallet<T>>::active_validator_indices();
// mapping from raw validator index to `ValidatorIndex`
// this computation is the same within a session, but should be cheap
let reverse_index = shuffled_indices
.iter()
.enumerate()
.map(|(i, v)| (v.0, ValidatorIndex(i as u32)))
.collect::<BTreeMap<u32, ValidatorIndex>>();
// we might have disabled validators who are not parachain validators
<pallet_session::Pallet<T>>::disabled_validators()
.iter()
.filter_map(|v| reverse_index.get(v).cloned())
.collect()
<shared::Pallet<T>>::disabled_validators()
}
/// Returns the current state of the node features.
@@ -691,4 +691,9 @@ impl<T: Config> Pallet<T> {
pub(crate) fn set_validator_groups(validator_groups: Vec<Vec<ValidatorIndex>>) {
ValidatorGroups::<T>::set(validator_groups);
}
#[cfg(test)]
pub(crate) fn set_claimqueue(claimqueue: BTreeMap<CoreIndex, VecDeque<ParasEntryType<T>>>) {
ClaimQueue::<T>::set(claimqueue);
}
}
+39 -3
View File
@@ -19,11 +19,14 @@
//! To avoid cyclic dependencies, it is important that this pallet is not
//! dependent on any of the other pallets.
use frame_support::pallet_prelude::*;
use frame_support::{pallet_prelude::*, traits::DisabledValidators};
use frame_system::pallet_prelude::BlockNumberFor;
use primitives::{SessionIndex, ValidatorId, ValidatorIndex};
use sp_runtime::traits::AtLeast32BitUnsigned;
use sp_std::{collections::vec_deque::VecDeque, vec::Vec};
use sp_std::{
collections::{btree_map::BTreeMap, vec_deque::VecDeque},
vec::Vec,
};
use rand::{seq::SliceRandom, SeedableRng};
use rand_chacha::ChaCha20Rng;
@@ -129,7 +132,9 @@ pub mod pallet {
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {}
pub trait Config: frame_system::Config {
type DisabledValidators: frame_support::traits::DisabledValidators;
}
/// The current session index.
#[pallet::storage]
@@ -216,6 +221,25 @@ impl<T: Config> Pallet<T> {
Self::session_index().saturating_add(SESSION_DELAY)
}
/// Fetches disabled validators list from session pallet.
/// CAVEAT: this might produce incorrect results on session boundaries
pub fn disabled_validators() -> Vec<ValidatorIndex> {
let shuffled_indices = Pallet::<T>::active_validator_indices();
// mapping from raw validator index to `ValidatorIndex`
// this computation is the same within a session, but should be cheap
let reverse_index = shuffled_indices
.iter()
.enumerate()
.map(|(i, v)| (v.0, ValidatorIndex(i as u32)))
.collect::<BTreeMap<u32, ValidatorIndex>>();
// we might have disabled validators who are not parachain validators
T::DisabledValidators::disabled_validators()
.iter()
.filter_map(|v| reverse_index.get(v).cloned())
.collect()
}
/// Test function for setting the current session index.
#[cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
pub fn set_session_index(index: SessionIndex) {
@@ -239,4 +263,16 @@ impl<T: Config> Pallet<T> {
ActiveValidatorIndices::<T>::set(indices);
ActiveValidatorKeys::<T>::set(keys);
}
#[cfg(test)]
pub(crate) fn add_allowed_relay_parent(
relay_parent: T::Hash,
state_root: T::Hash,
number: BlockNumberFor<T>,
max_ancestry_len: u32,
) {
AllowedRelayParents::<T>::mutate(|tracker| {
tracker.update(relay_parent, state_root, number, max_ancestry_len)
})
}
}
+3 -1
View File
@@ -909,7 +909,9 @@ impl parachains_configuration::Config for Runtime {
type WeightInfo = weights::runtime_parachains_configuration::WeightInfo<Runtime>;
}
impl parachains_shared::Config for Runtime {}
impl parachains_shared::Config for Runtime {
type DisabledValidators = Session;
}
impl parachains_session_info::Config for Runtime {
type ValidatorSet = Historical;
+3 -1
View File
@@ -478,7 +478,9 @@ impl parachains_configuration::Config for Runtime {
type WeightInfo = parachains_configuration::TestWeightInfo;
}
impl parachains_shared::Config for Runtime {}
impl parachains_shared::Config for Runtime {
type DisabledValidators = Session;
}
impl parachains_inclusion::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
+3 -1
View File
@@ -1117,7 +1117,9 @@ impl parachains_configuration::Config for Runtime {
type WeightInfo = weights::runtime_parachains_configuration::WeightInfo<Runtime>;
}
impl parachains_shared::Config for Runtime {}
impl parachains_shared::Config for Runtime {
type DisabledValidators = Session;
}
impl parachains_session_info::Config for Runtime {
type ValidatorSet = Historical;
+3 -1
View File
@@ -128,7 +128,9 @@ impl pallet_balances::Config for Runtime {
type MaxFreezes = ConstU32<0>;
}
impl shared::Config for Runtime {}
impl shared::Config for Runtime {
type DisabledValidators = ();
}
impl configuration::Config for Runtime {
type WeightInfo = configuration::TestWeightInfo;
@@ -120,7 +120,9 @@ impl pallet_uniques::Config for Runtime {
type Helper = ();
}
impl shared::Config for Runtime {}
impl shared::Config for Runtime {
type DisabledValidators = ();
}
impl configuration::Config for Runtime {
type WeightInfo = configuration::TestWeightInfo;
@@ -100,7 +100,9 @@ impl pallet_balances::Config for Runtime {
type MaxFreezes = ConstU32<0>;
}
impl shared::Config for Runtime {}
impl shared::Config for Runtime {
type DisabledValidators = ();
}
impl configuration::Config for Runtime {
type WeightInfo = configuration::TestWeightInfo;
+10
View File
@@ -0,0 +1,10 @@
title: Filter backing votes from disabled validators in paras_inherent
doc:
- audience: Runtime User
description: |
paras_inherent drops any backing votes from disabled validators on block import and asserts
that no votes from disabled validators are included in a block during execution
crates:
- name: polkadot-runtime-parachains
+4
View File
@@ -96,6 +96,10 @@ impl DisabledValidators for MockDisabledValidators {
fn is_disabled(index: AuthorityIndex) -> bool {
DisabledValidatorTestValue::get().binary_search(&index).is_ok()
}
fn disabled_validators() -> Vec<u32> {
DisabledValidatorTestValue::get()
}
}
impl pallet_aura::Config for Test {
@@ -99,7 +99,9 @@ impl pallet_balances::Config for Runtime {
type RuntimeFreezeReason = RuntimeFreezeReason;
}
impl shared::Config for Runtime {}
impl shared::Config for Runtime {
type DisabledValidators = ();
}
impl configuration::Config for Runtime {
type WeightInfo = configuration::TestWeightInfo;
+4
View File
@@ -918,6 +918,10 @@ impl<T: Config> frame_support::traits::DisabledValidators for Pallet<T> {
fn is_disabled(index: u32) -> bool {
<Pallet<T>>::disabled_validators().binary_search(&index).is_ok()
}
fn disabled_validators() -> Vec<u32> {
<Pallet<T>>::disabled_validators()
}
}
/// Wraps the author-scraping logic for consensus engines that can recover
@@ -251,10 +251,17 @@ pub trait ValidatorRegistration<ValidatorId> {
pub trait DisabledValidators {
/// Returns true if the given validator is disabled.
fn is_disabled(index: u32) -> bool;
/// Returns all disabled validators
fn disabled_validators() -> Vec<u32>;
}
impl DisabledValidators for () {
fn is_disabled(_index: u32) -> bool {
false
}
fn disabled_validators() -> Vec<u32> {
vec![]
}
}