-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Parachains double vote handler initial implementation. #840
Parachains double vote handler initial implementation. #840
Conversation
runtime/common/src/parachains.rs
Outdated
None => return Err("Not in validator set".into()), | ||
}; | ||
|
||
let offender = match T::FullIdentificationOf::convert(validators[offender_idx].clone()) { |
There was a problem hiding this comment.
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
.
runtime/common/src/parachains.rs
Outdated
(Statement::Valid(hash), Statement::Candidate(candidate)) | | ||
(Statement::Invalid(hash), Statement::Candidate(candidate)) | ||
if candidate.hash() == hash => { | ||
T::ReportDoubleVote::report_offence(vec![], offence); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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%.
runtime/common/src/parachains.rs
Outdated
/// Identity of the double-voter. | ||
pub identity: ValidatorId, | ||
/// First vote of the double-vote. | ||
pub first: (Statement, ValidityAttestation), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
runtime/common/src/parachains.rs
Outdated
let first = self.first.clone(); | ||
let second = self.second.clone(); | ||
|
||
Self::verify_vote(first, &parent_hash, self.identity.clone())?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
runtime/common/src/parachains.rs
Outdated
|
||
let (payload, sig) = match attestation { | ||
ValidityAttestation::Implicit(sig) => { | ||
let payload = localized_payload(statement, parent_hash); |
There was a problem hiding this comment.
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.
runtime/common/src/parachains.rs
Outdated
let e = TransactionValidityError::from( | ||
InvalidTransaction::Custom(DoubleVoteValidityError::NotDoubleVote as u8) | ||
); | ||
match (&report.first.0, &report.second.0) { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 Ok
ing the SignedExtension
. Otherwise useless duplicates will be included for free. That is a spam vector that should be tested against.
runtime/common/src/parachains.rs
Outdated
); | ||
report.verify(parent_hash).map_err(|_| e)?; | ||
|
||
let e = TransactionValidityError::from( |
There was a problem hiding this comment.
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.
runtime/common/src/parachains.rs
Outdated
@@ -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() { |
There was a problem hiding this comment.
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.
runtime/common/src/parachains.rs
Outdated
} | ||
}; | ||
|
||
match sig.verify(&payload[..], &authority) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be an if
.
runtime/common/src/parachains.rs
Outdated
(payload, sig) | ||
} | ||
ValidityAttestation::Explicit(sig) => { | ||
let payload = localized_payload(statement, parent_hash); |
There was a problem hiding this comment.
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).
runtime/common/src/parachains.rs
Outdated
) -> DispatchResult { | ||
let reporter = ensure_signed(origin)?; | ||
|
||
// The following code duplicates the logic in `ValidateDoubeVoteReports::validate` |
There was a problem hiding this comment.
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?
runtime/common/src/parachains.rs
Outdated
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
runtime/common/src/parachains.rs
Outdated
|
||
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
runtime/common/src/parachains.rs
Outdated
/// The mapping from parent block hashes to session indexes. | ||
/// | ||
/// Used for double vote report validation. | ||
/// This is not pruned at the moment. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
runtime/common/src/parachains.rs
Outdated
// 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(), |
There was a problem hiding this comment.
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
There was a problem hiding this 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.
@rphmeier I've added an attempt at pruning, the idea is to ask |
@rphmeier can this be merged now? |
This is definitely too aggressive but I will not block on it. Will file an issue with my full thoughts
Yup happy to merge it as soon as ready |
* slightly changed relay loop initialization * git mv * clippy * more clippy * loop_run -> run_loop * review and clippy * clippy
A runtime handler to slash validators for double votes.
Fixes #760