pre-checking: Reject failed PVFs (#6492)

* pre-checking: Reject failed PVFs

* paras: immediately reject any PVF that cannot reach a supermajority

* Make the `quorum` reject condition a bit more clear semantically

* Add comment

* Update implementer's guide

* Update a link

Not related to the rest of the PR, but I randomly noticed and fixed this.

* Update runtime/parachains/src/paras/tests.rs

Co-authored-by: s0me0ne-unkn0wn <48632512+s0me0ne-unkn0wn@users.noreply.github.com>

* Remove unneeded loop

* Log PVF retries using `info!`

* Change retry logs to `warn!` and add preparation failure log

* Log PVF execution failure

* Clarify why we reject failed PVFs

* Fix PVF reject runtime benchmarks

Co-authored-by: s0me0ne-unkn0wn <48632512+s0me0ne-unkn0wn@users.noreply.github.com>
This commit is contained in:
Marcin S
2023-01-12 04:24:42 -05:00
committed by GitHub
parent ae74d33a93
commit 2efa3bab98
11 changed files with 126 additions and 74 deletions
@@ -170,7 +170,8 @@ where
}
/// Generates a list of votes combined with signatures for the active validator set. The number of
/// votes is equal to the minimum number of votes required to reach the supermajority.
/// votes is equal to the minimum number of votes required to reach the threshold for either accept
/// or reject.
fn generate_statements<T>(
vote_outcome: VoteOutcome,
) -> impl Iterator<Item = (PvfCheckStatement, ValidatorSignature)>
@@ -179,7 +180,11 @@ where
{
let validators = ParasShared::<T>::active_validator_keys();
let required_votes = primitives::supermajority_threshold(validators.len());
let accept_threshold = primitives::supermajority_threshold(validators.len());
let required_votes = match vote_outcome {
VoteOutcome::Accept => accept_threshold,
VoteOutcome::Reject => validators.len() - accept_threshold,
};
(0..required_votes).map(move |validator_index| {
let stmt = PvfCheckStatement {
accept: vote_outcome == VoteOutcome::Accept,
+18 -15
View File
@@ -73,13 +73,14 @@
//!
//! # PVF Pre-checking
//!
//! As was mentioned above, a brand new validation code should go through a process of approval.
//! As part of this process, validators from the active set will take the validation code and
//! check if it is malicious. Once they did that and have their judgement, either accept or reject,
//! they issue a statement in a form of an unsigned extrinsic. This extrinsic is processed by this
//! pallet. Once supermajority is gained for accept, then the process that initiated the check
//! is resumed (as mentioned before this can be either upgrading of validation code or onboarding).
//! If supermajority is gained for reject, then the process is canceled.
//! As was mentioned above, a brand new validation code should go through a process of approval. As
//! part of this process, validators from the active set will take the validation code and check if
//! it is malicious. Once they did that and have their judgement, either accept or reject, they
//! issue a statement in a form of an unsigned extrinsic. This extrinsic is processed by this
//! pallet. Once supermajority is gained for accept, then the process that initiated the check is
//! resumed (as mentioned before this can be either upgrading of validation code or onboarding). If
//! getting a supermajority becomes impossible (>1/3 of validators have already voted against), then
//! we reject.
//!
//! Below is a state diagram that depicts states of a single PVF pre-checking vote.
//!
@@ -92,8 +93,8 @@
//! │ │ │
//! │ ┌───────┐
//! │ │ │
//! └─▶│ init │────supermajority ┌──────────┐
//! │ │ against │ │
//! └─▶│ init │──── >1/3 against ┌──────────┐
//! │ │ │ │
//! └───────┘ └──────────▶│ rejected │
//! ▲ │ │ │
//! │ │ session └──────────┘
@@ -452,12 +453,14 @@ impl<BlockNumber> PvfCheckActiveVoteState<BlockNumber> {
/// Returns `None` if the quorum is not reached, or the direction of the decision.
fn quorum(&self, n_validators: usize) -> Option<PvfCheckOutcome> {
let q_threshold = primitives::supermajority_threshold(n_validators);
// NOTE: counting the reject votes is deliberately placed first. This is to err on the safe.
if self.votes_reject.count_ones() >= q_threshold {
Some(PvfCheckOutcome::Rejected)
} else if self.votes_accept.count_ones() >= q_threshold {
let accept_threshold = primitives::supermajority_threshold(n_validators);
// At this threshold, a supermajority is no longer possible, so we reject.
let reject_threshold = n_validators - accept_threshold;
if self.votes_accept.count_ones() >= accept_threshold {
Some(PvfCheckOutcome::Accepted)
} else if self.votes_reject.count_ones() > reject_threshold {
Some(PvfCheckOutcome::Rejected)
} else {
None
}
@@ -1011,7 +1014,7 @@ pub mod pallet {
}
if let Some(outcome) = active_vote.quorum(validators.len()) {
// The supermajority quorum has been achieved.
// The quorum has been achieved.
//
// Remove the PVF vote from the active map and finalize the PVF checking according
// to the outcome.
+18 -9
View File
@@ -1249,15 +1249,24 @@ fn pvf_check_upgrade_reject() {
Paras::schedule_code_upgrade(a, new_code.clone(), RELAY_PARENT, &Configuration::config());
check_code_is_stored(&new_code);
// Supermajority of validators vote against `new_code`. PVF should be rejected.
IntoIterator::into_iter([0, 1, 2, 3])
.map(|i| PvfCheckStatement {
accept: false,
subject: new_code.hash(),
session_index: EXPECTED_SESSION,
validator_index: i.into(),
})
.for_each(sign_and_include_pvf_check_statement);
// 1/3 of validators vote against `new_code`. PVF should not be rejected yet.
sign_and_include_pvf_check_statement(PvfCheckStatement {
accept: false,
subject: new_code.hash(),
session_index: EXPECTED_SESSION,
validator_index: 0.into(),
});
// Verify that the new code is not yet discarded.
check_code_is_stored(&new_code);
// >1/3 of validators vote against `new_code`. PVF should be rejected.
sign_and_include_pvf_check_statement(PvfCheckStatement {
accept: false,
subject: new_code.hash(),
session_index: EXPECTED_SESSION,
validator_index: 1.into(),
});
// Verify that the new code is discarded.
check_code_is_not_stored(&new_code);