Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Parachains double vote handler initial implementation. #840

Merged
merged 27 commits into from
Mar 22, 2020
Merged

Parachains double vote handler initial implementation. #840

merged 27 commits into from
Mar 22, 2020

Conversation

montekki
Copy link
Contributor

A runtime handler to slash validators for double votes.

Fixes #760

@montekki montekki added the A3-in_progress Pull request is in progress. No review needed at this stage. label Feb 12, 2020
None => return Err("Not in validator set".into()),
};

let offender = match T::FullIdentificationOf::convert(validators[offender_idx].clone()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, this will only work for validators who are in the current set. In the future we probably have to accept another argument to the DoubleVoteReport that we can use with session::historical::Module to extract the FullIdentification.

(Statement::Valid(hash), Statement::Candidate(candidate)) |
(Statement::Invalid(hash), Statement::Candidate(candidate))
if candidate.hash() == hash => {
T::ReportDoubleVote::report_offence(vec![], offence);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the plan to determine reporters further on?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also - a side PR on Substrate for report_offence to return a value if the report should be ignored would be helpful in practice, this is when pallet-offences triages and finds that all reports are duplicate.

The reason I want that is so we can reject duplicate offence reports. It should not be possible for a single equivocation to allow huge spam on the chain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it should be a signed extrinsic and the reporer's Id is ensure_signed result?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bears some investigation. My understanding was that the signed extrinsic bears a fee (which we don't want). Maybe setting weight to 0 is the correct approach, though. The other thing you should check before using signed extrinsics is that returning an error means that the extrinsic cannot be included in the block. That's for the same anti-spam reason.

}

fn time_slot(&self) -> Self::TimeSlot {
self.session_index
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since reports are deduplicated based on time slot, we might want to (for correctness) use the relay parent hash as the time slot instead of the session. But it doesn't make that much difference, since we slash 100%.

@montekki montekki marked this pull request as ready for review February 20, 2020 14:28
@montekki montekki added A0-please_review Pull request needs code review. and removed A3-in_progress Pull request is in progress. No review needed at this stage. labels Feb 20, 2020
@montekki montekki requested review from bkchr and rphmeier February 20, 2020 14:28
/// Identity of the double-voter.
pub identity: ValidatorId,
/// First vote of the double-vote.
pub first: (Statement, ValidityAttestation),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that Statement, ValidityAttestation pairs make sense. A ValidityAttestation, CandidateHash pair would make sense, or a Statement, Signature pair.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statement, Signature makes most sense, since ValidityAttestation does not allow for Invalid attestations.

let first = self.first.clone();
let second = self.second.clone();

Self::verify_vote(first, &parent_hash, self.identity.clone())?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should check that the first statement != the second.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also a test that a double-vote report with both times the same report fails would be good!


let (payload, sig) = match attestation {
ValidityAttestation::Implicit(sig) => {
let payload = localized_payload(statement, parent_hash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. a ValidityAttestation::Implicit corresponds directly to a Statement::Candidate. and a ValidityAttestation::Explicit corresponds to a Statement::Valid. However, the statement isn't checked against that type, so the code doesn't make sense. In this case, a Statement, Signature pair would make most sense.

let e = TransactionValidityError::from(
InvalidTransaction::Custom(DoubleVoteValidityError::NotDoubleVote as u8)
);
match (&report.first.0, &report.second.0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now that all pairs are checked here, but IMO the logic should not be so spread out. Probably should be checked in report.verify.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking on this a bit further - I think we also need to check against duplicate reports (report_offence & friends) before Oking the SignedExtension. Otherwise useless duplicates will be included for free. That is a spam vector that should be tested against.

);
report.verify(parent_hash).map_err(|_| e)?;

let e = TransactionValidityError::from(
Copy link
Contributor

@rphmeier rphmeier Feb 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pre-emptive let e = ... pattern and then return Err(e) is kind of weird compared to return Err(...). Mostly because e is rebound several times and exists in a scope where it is not relevant. Strikes me as a bit disorganized and makes the code harder to follow.

@@ -1746,4 +2063,273 @@ mod tests {
let hashed_null_node = <NodeCodec<Blake2Hasher> as trie_db::NodeCodec>::hashed_null_node();
assert_eq!(hashed_null_node, EMPTY_TRIE_ROOT.into())
}

#[test]
fn double_vote_works() {
Copy link
Contributor

@rphmeier rphmeier Feb 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer to split the many tests here into multiple functions.

}
};

match sig.verify(&payload[..], &authority) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an if.

(payload, sig)
}
ValidityAttestation::Explicit(sig) => {
let payload = localized_payload(statement, parent_hash);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved out of the match (the same code exists in the other branch).

) -> DispatchResult {
let reporter = ensure_signed(origin)?;

// The following code duplicates the logic in `ValidateDoubeVoteReports::validate`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't it a function that is called by both?

let parent_hash = <system::Module<T>>::block_hash(<system::Module<T>>::block_number());

let authorities = Self::authorities();
let offender_idx = match authorities.iter().position(|a| *a == report.identity) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let offender_idx = match authorities.iter().position(|a| *a == report.identity) {
let offender_idx = authorities.iter().position(|a| *a == report.identity).ok_or_else(|| "Not in validator set")?;

Much shorter.


/// Ensure that double vote reports are only processed if valid.
#[derive(Encode, Decode, Clone, Eq, PartialEq)]
pub struct ValidateDoubleVoteReports<T: Trait + Send + Sync>(rstd::marker::PhantomData<T>) where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub struct ValidateDoubleVoteReports<T: Trait + Send + Sync>(rstd::marker::PhantomData<T>) where
pub struct ValidateDoubleVoteReports<T>(rstd::marker::PhantomData<T>);

Trait bounds, if not required, should not be added to the definition.

@montekki montekki added A3-in_progress Pull request is in progress. No review needed at this stage. and removed A0-please_review Pull request needs code review. labels Feb 29, 2020
@montekki montekki added A0-please_review Pull request needs code review. and removed A3-in_progress Pull request is in progress. No review needed at this stage. labels Mar 17, 2020
/// The mapping from parent block hashes to session indexes.
///
/// Used for double vote report validation.
/// This is not pruned at the moment.
Copy link
Contributor

@rphmeier rphmeier Mar 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any thoughts on how it might be pruned in the future? I'd imagine we can get an API from staking/session that notifies us every time a session is removed from a bonding window (likely batches of sessions as the bonding window shrinks by era). It makes sense to account for such an API when setting up the storage now, even though it will not currently be triggered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about implementing a custome type T::SessionInterface (not it is Self) that would proxy calls to the already existing implementation, it looks like we need prune_historical_up_to. My understanding is that this will be called on every era start pruning all data up to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not that easy: implementing a custom SessionInterface on Runtime is conflicting with implementation in substrate. I will read further into the code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to be responsible for the call to prune_historical_up_to, but we would want to receive notifications when that is called, and for which sessions.

Co-Authored-By: Robert Habermeier <rphmeier@gmail.com>
// this here to get the full identification of the offender.
let offender = T::KeyOwnerProofSystem::check_proof(
(PARACHAIN_KEY_TYPE_ID, report.identity.encode()),
report.proof.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clone is probably unnecessary here. i'd prefer to avoid the clone as the FullIdentification will be heavy

Copy link
Contributor

@rphmeier rphmeier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General approach & tests look good. Would still like to get more clarity on how we can prevent the ParentToSessionIndex map from blowing up and make sure the schema allows for pruning that in the future.

@montekki
Copy link
Contributor Author

@rphmeier I've added an attempt at pruning, the idea is to ask Staking::current_era() in on_initialize and keep it until the era changes. Upon the era change the remove_all is called for the ParentToSessionIndex mapping. What do you think?

@gavofyork gavofyork added A8-looksgood and removed A0-please_review Pull request needs code review. labels Mar 21, 2020
@gavofyork
Copy link
Member

@rphmeier can this be merged now?

@rphmeier
Copy link
Contributor

rphmeier commented Mar 22, 2020

@rphmeier I've added an attempt at pruning, the idea is to ask Staking::current_era() in on_initialize and keep it until the era changes. Upon the era change the remove_all is called for the ParentToSessionIndex mapping. What do you think?

This is definitely too aggressive but I will not block on it. Will file an issue with my full thoughts

@gavofyork

@rphmeier can this be merged now?

Yup happy to merge it as soon as ready

@montekki montekki merged commit b361171 into paritytech:master Mar 22, 2020
tomusdrw pushed a commit that referenced this pull request Mar 26, 2021
* slightly changed relay loop initialization

* git mv

* clippy

* more clippy

* loop_run -> run_loop

* review and clippy

* clippy
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Double-vote Slash Handler
5 participants