From 7670b481278c478b3aa0763fc0ca6d3ee50bacb5 Mon Sep 17 00:00:00 2001 From: Serban Iorga Date: Mon, 24 Jul 2023 16:26:23 +0300 Subject: [PATCH] GRANDPA justifications: equivocation detection primitives (#2295) (#2297) * GRANDPA justifications: equivocation detection primitives - made the justification verification logic more customizable - added support for parsing multiple justifications and extracting equivocations - split the logic into multiple files - split the errors into multiple enums --- .../header-chain/src/justification.rs | 444 ------------------ .../header-chain/src/justification/mod.rs | 127 +++++ .../verification/equivocation.rs | 179 +++++++ .../src/justification/verification/mod.rs | 279 +++++++++++ .../justification/verification/optimizer.rs | 132 ++++++ .../src/justification/verification/strict.rs | 106 +++++ .../tests/implementation_match.rs | 27 +- .../tests/justification/equivocation.rs | 124 +++++ .../optimizer.rs} | 178 +------ .../tests/justification/strict.rs | 196 ++++++++ .../primitives/header-chain/tests/tests.rs | 7 + 11 files changed, 1163 insertions(+), 636 deletions(-) delete mode 100644 bridges/primitives/header-chain/src/justification.rs create mode 100644 bridges/primitives/header-chain/src/justification/mod.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/equivocation.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/mod.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/optimizer.rs create mode 100644 bridges/primitives/header-chain/src/justification/verification/strict.rs create mode 100644 bridges/primitives/header-chain/tests/justification/equivocation.rs rename bridges/primitives/header-chain/tests/{justification.rs => justification/optimizer.rs} (55%) create mode 100644 bridges/primitives/header-chain/tests/justification/strict.rs create mode 100644 bridges/primitives/header-chain/tests/tests.rs diff --git a/bridges/primitives/header-chain/src/justification.rs b/bridges/primitives/header-chain/src/justification.rs deleted file mode 100644 index 714546a42ef2d..0000000000000 --- a/bridges/primitives/header-chain/src/justification.rs +++ /dev/null @@ -1,444 +0,0 @@ -// Copyright 2019-2021 Parity Technologies (UK) Ltd. -// This file is part of Parity Bridges Common. - -// Parity Bridges Common is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity Bridges Common is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity Bridges Common. If not, see . - -//! Pallet for checking GRANDPA Finality Proofs. -//! -//! Adapted copy of substrate/client/finality-grandpa/src/justification.rs. If origin -//! will ever be moved to the sp_consensus_grandpa, we should reuse that implementation. - -use crate::ChainWithGrandpa; - -use bp_runtime::{BlockNumberOf, Chain, HashOf, HeaderId}; -use codec::{Decode, Encode, MaxEncodedLen}; -use finality_grandpa::voter_set::VoterSet; -use frame_support::{RuntimeDebug, RuntimeDebugNoBound}; -use scale_info::TypeInfo; -use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, SetId}; -use sp_runtime::{traits::Header as HeaderT, SaturatedConversion}; -use sp_std::{ - collections::{btree_map::BTreeMap, btree_set::BTreeSet}, - prelude::*, -}; - -/// A GRANDPA Justification is a proof that a given header was finalized -/// at a certain height and with a certain set of authorities. -/// -/// This particular proof is used to prove that headers on a bridged chain -/// (so not our chain) have been finalized correctly. -#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound)] -pub struct GrandpaJustification { - /// The round (voting period) this justification is valid for. - pub round: u64, - /// The set of votes for the chain which is to be finalized. - pub commit: - finality_grandpa::Commit, - /// A proof that the chain of blocks in the commit are related to each other. - pub votes_ancestries: Vec
, -} - -impl GrandpaJustification { - /// Returns reasonable size of justification using constants from the provided chain. - /// - /// An imprecise analogue of `MaxEncodedLen` implementation. We don't use it for - /// any precise calculations - that's just an estimation. - pub fn max_reasonable_size(required_precommits: u32) -> u32 - where - C: Chain + ChainWithGrandpa, - { - // we don't need precise results here - just estimations, so some details - // are removed from computations (e.g. bytes required to encode vector length) - - // structures in `finality_grandpa` crate are not implementing `MaxEncodedLength`, so - // here's our estimation for the `finality_grandpa::Commit` struct size - // - // precommit is: hash + number - // signed precommit is: precommit + signature (64b) + authority id - // commit is: hash + number + vec of signed precommits - let signed_precommit_size: u32 = BlockNumberOf::::max_encoded_len() - .saturating_add(HashOf::::max_encoded_len().saturated_into()) - .saturating_add(64) - .saturating_add(AuthorityId::max_encoded_len().saturated_into()) - .saturated_into(); - let max_expected_signed_commit_size = signed_precommit_size - .saturating_mul(required_precommits) - .saturating_add(BlockNumberOf::::max_encoded_len().saturated_into()) - .saturating_add(HashOf::::max_encoded_len().saturated_into()); - - // justification is a signed GRANDPA commit, `votes_ancestries` vector and round number - let max_expected_votes_ancestries_size = C::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY - .saturating_mul(C::AVERAGE_HEADER_SIZE_IN_JUSTIFICATION); - - 8u32.saturating_add(max_expected_signed_commit_size) - .saturating_add(max_expected_votes_ancestries_size) - } - - pub fn commit_target_id(&self) -> HeaderId { - HeaderId(self.commit.target_number, self.commit.target_hash) - } -} - -impl crate::FinalityProof for GrandpaJustification { - fn target_header_number(&self) -> H::Number { - self.commit.target_number - } -} - -/// Justification verification error. -#[derive(Eq, RuntimeDebug, PartialEq)] -pub enum Error { - /// Failed to decode justification. - JustificationDecode, - /// Justification is finalizing unexpected header. - InvalidJustificationTarget, - /// Justification contains redundant votes. - RedundantVotesInJustification, - /// Justification contains unknown authority precommit. - UnknownAuthorityVote, - /// Justification contains duplicate authority precommit. - DuplicateAuthorityVote, - /// The authority has provided an invalid signature. - InvalidAuthoritySignature, - /// The justification contains precommit for header that is not a descendant of the commit - /// header. - UnrelatedAncestryVote, - /// The cumulative weight of all votes in the justification is not enough to justify commit - /// header finalization. - TooLowCumulativeWeight, - /// The justification contains extra (unused) headers in its `votes_ancestries` field. - RedundantVotesAncestries, -} - -/// Given GRANDPA authorities set size, return number of valid authorities votes that the -/// justification must have to be valid. -/// -/// This function assumes that all authorities have the same vote weight. -pub fn required_justification_precommits(authorities_set_length: u32) -> u32 { - authorities_set_length - authorities_set_length.saturating_sub(1) / 3 -} - -/// Decode justification target. -pub fn decode_justification_target( - raw_justification: &[u8], -) -> Result<(Header::Hash, Header::Number), Error> { - GrandpaJustification::
::decode(&mut &*raw_justification) - .map(|justification| (justification.commit.target_hash, justification.commit.target_number)) - .map_err(|_| Error::JustificationDecode) -} - -/// Verify and optimize given justification by removing unknown and duplicate votes. -pub fn verify_and_optimize_justification( - finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, - justification: &mut GrandpaJustification
, -) -> Result<(), Error> { - let mut optimizer = OptimizationCallbacks { - extra_precommits: vec![], - redundant_votes_ancestries: Default::default(), - }; - verify_justification_with_callbacks( - finalized_target, - authorities_set_id, - authorities_set, - justification, - &mut optimizer, - )?; - optimizer.optimize(justification); - - Ok(()) -} - -/// Verify that justification, that is generated by given authority set, finalizes given header. -pub fn verify_justification( - finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, - justification: &GrandpaJustification
, -) -> Result<(), Error> { - verify_justification_with_callbacks( - finalized_target, - authorities_set_id, - authorities_set, - justification, - &mut StrictVerificationCallbacks, - ) -} - -/// Verification callbacks. -trait VerificationCallbacks { - /// Called when we see a precommit from unknown authority. - fn on_unkown_authority(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit with duplicate vote from known authority. - fn on_duplicate_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit with an invalid signature. - fn on_invalid_authority_signature(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit after we've collected enough votes from authorities. - fn on_redundant_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when we see a precommit that is not a descendant of the commit target. - fn on_unrelated_ancestry_vote(&mut self, precommit_idx: usize) -> Result<(), Error>; - /// Called when there are redundant headers in the votes ancestries. - fn on_redundant_votes_ancestries( - &mut self, - redundant_votes_ancestries: BTreeSet, - ) -> Result<(), Error>; -} - -/// Verification callbacks that reject all unknown, duplicate or redundant votes. -struct StrictVerificationCallbacks; - -impl VerificationCallbacks
for StrictVerificationCallbacks { - fn on_unkown_authority(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::UnknownAuthorityVote) - } - - fn on_duplicate_authority_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::DuplicateAuthorityVote) - } - - fn on_invalid_authority_signature(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::InvalidAuthoritySignature) - } - - fn on_redundant_authority_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::RedundantVotesInJustification) - } - - fn on_unrelated_ancestry_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> { - Err(Error::UnrelatedAncestryVote) - } - - fn on_redundant_votes_ancestries( - &mut self, - _redundant_votes_ancestries: BTreeSet, - ) -> Result<(), Error> { - Err(Error::RedundantVotesAncestries) - } -} - -/// Verification callbacks for justification optimization. -struct OptimizationCallbacks { - extra_precommits: Vec, - redundant_votes_ancestries: BTreeSet, -} - -impl OptimizationCallbacks
{ - fn optimize(self, justification: &mut GrandpaJustification
) { - for invalid_precommit_idx in self.extra_precommits.into_iter().rev() { - justification.commit.precommits.remove(invalid_precommit_idx); - } - if !self.redundant_votes_ancestries.is_empty() { - justification - .votes_ancestries - .retain(|header| !self.redundant_votes_ancestries.contains(&header.hash())) - } - } -} - -impl VerificationCallbacks
for OptimizationCallbacks
{ - fn on_unkown_authority(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_duplicate_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_invalid_authority_signature(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_redundant_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_unrelated_ancestry_vote(&mut self, precommit_idx: usize) -> Result<(), Error> { - self.extra_precommits.push(precommit_idx); - Ok(()) - } - - fn on_redundant_votes_ancestries( - &mut self, - redundant_votes_ancestries: BTreeSet, - ) -> Result<(), Error> { - self.redundant_votes_ancestries = redundant_votes_ancestries; - Ok(()) - } -} - -/// Verify that justification, that is generated by given authority set, finalizes given header. -fn verify_justification_with_callbacks>( - finalized_target: (Header::Hash, Header::Number), - authorities_set_id: SetId, - authorities_set: &VoterSet, - justification: &GrandpaJustification
, - callbacks: &mut C, -) -> Result<(), Error> { - // ensure that it is justification for the expected header - if (justification.commit.target_hash, justification.commit.target_number) != finalized_target { - return Err(Error::InvalidJustificationTarget) - } - - let threshold = authorities_set.threshold().get(); - let mut chain = AncestryChain::new(justification); - let mut signature_buffer = Vec::new(); - let mut votes = BTreeSet::new(); - let mut cumulative_weight = 0u64; - - for (precommit_idx, signed) in justification.commit.precommits.iter().enumerate() { - // if we have collected enough precommits, we probabably want to fail/remove extra - // precommits - if cumulative_weight >= threshold { - callbacks.on_redundant_authority_vote(precommit_idx)?; - continue - } - - // authority must be in the set - let authority_info = match authorities_set.get(&signed.id) { - Some(authority_info) => authority_info, - None => { - callbacks.on_unkown_authority(precommit_idx)?; - continue - }, - }; - - // check if authority has already voted in the same round. - // - // there's a lot of code in `validate_commit` and `import_precommit` functions inside - // `finality-grandpa` crate (mostly related to reporting equivocations). But the only thing - // that we care about is that only first vote from the authority is accepted - if votes.contains(&signed.id) { - callbacks.on_duplicate_authority_vote(precommit_idx)?; - continue - } - - // all precommits must be descendants of the target block - let route = - match chain.ancestry(&signed.precommit.target_hash, &signed.precommit.target_number) { - Some(route) => route, - None => { - callbacks.on_unrelated_ancestry_vote(precommit_idx)?; - continue - }, - }; - - // verify authority signature - if !sp_consensus_grandpa::check_message_signature_with_buffer( - &finality_grandpa::Message::Precommit(signed.precommit.clone()), - &signed.id, - &signed.signature, - justification.round, - authorities_set_id, - &mut signature_buffer, - ) { - callbacks.on_invalid_authority_signature(precommit_idx)?; - continue - } - - // now we can count the vote since we know that it is valid - votes.insert(signed.id.clone()); - chain.mark_route_as_visited(route); - cumulative_weight = cumulative_weight.saturating_add(authority_info.weight().get()); - } - - // check that the cumulative weight of validators that voted for the justification target (or - // one of its descendents) is larger than the required threshold. - if cumulative_weight < threshold { - return Err(Error::TooLowCumulativeWeight) - } - - // check that there are no extra headers in the justification - if !chain.is_fully_visited() { - callbacks.on_redundant_votes_ancestries(chain.unvisited)?; - } - - Ok(()) -} - -/// Votes ancestries with useful methods. -#[derive(RuntimeDebug)] -pub struct AncestryChain { - /// We expect all forks in the ancestry chain to be descendants of base. - base: HeaderId, - /// Header hash => parent header hash mapping. - pub parents: BTreeMap, - /// Hashes of headers that were not visited by `ancestry()`. - pub unvisited: BTreeSet, -} - -impl AncestryChain
{ - /// Create new ancestry chain. - pub fn new(justification: &GrandpaJustification
) -> AncestryChain
{ - let mut parents = BTreeMap::new(); - let mut unvisited = BTreeSet::new(); - for ancestor in &justification.votes_ancestries { - let hash = ancestor.hash(); - let parent_hash = *ancestor.parent_hash(); - parents.insert(hash, parent_hash); - unvisited.insert(hash); - } - AncestryChain { base: justification.commit_target_id(), parents, unvisited } - } - - /// Returns a route if the precommit target block is a descendant of the `base` block. - pub fn ancestry( - &self, - precommit_target_hash: &Header::Hash, - precommit_target_number: &Header::Number, - ) -> Option> { - if precommit_target_number < &self.base.number() { - return None - } - - let mut route = vec![]; - let mut current_hash = *precommit_target_hash; - loop { - if current_hash == self.base.hash() { - break - } - - current_hash = match self.parents.get(¤t_hash) { - Some(parent_hash) => { - let is_visited_before = self.unvisited.get(¤t_hash).is_none(); - if is_visited_before { - // If the current header has been visited in a previous call, it is a - // descendent of `base` (we assume that the previous call was successful). - return Some(route) - } - route.push(current_hash); - - *parent_hash - }, - None => return None, - }; - } - - Some(route) - } - - fn mark_route_as_visited(&mut self, route: Vec) { - for hash in route { - self.unvisited.remove(&hash); - } - } - - fn is_fully_visited(&self) -> bool { - self.unvisited.is_empty() - } -} diff --git a/bridges/primitives/header-chain/src/justification/mod.rs b/bridges/primitives/header-chain/src/justification/mod.rs new file mode 100644 index 0000000000000..17be6cfd79611 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/mod.rs @@ -0,0 +1,127 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for checking GRANDPA Finality Proofs. +//! +//! Adapted copy of substrate/client/finality-grandpa/src/justification.rs. If origin +//! will ever be moved to the sp_consensus_grandpa, we should reuse that implementation. + +mod verification; + +use crate::ChainWithGrandpa; +pub use verification::{ + equivocation::{EquivocationsCollector, Error as EquivocationsCollectorError}, + optimizer::verify_and_optimize_justification, + strict::verify_justification, + AncestryChain, Error as JustificationVerificationError, PrecommitError, +}; + +use bp_runtime::{BlockNumberOf, Chain, HashOf, HeaderId}; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{RuntimeDebug, RuntimeDebugNoBound}; +use scale_info::TypeInfo; +use sp_consensus_grandpa::{AuthorityId, AuthoritySignature}; +use sp_runtime::{traits::Header as HeaderT, SaturatedConversion}; +use sp_std::prelude::*; + +/// A GRANDPA Justification is a proof that a given header was finalized +/// at a certain height and with a certain set of authorities. +/// +/// This particular proof is used to prove that headers on a bridged chain +/// (so not our chain) have been finalized correctly. +#[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebugNoBound)] +pub struct GrandpaJustification { + /// The round (voting period) this justification is valid for. + pub round: u64, + /// The set of votes for the chain which is to be finalized. + pub commit: + finality_grandpa::Commit, + /// A proof that the chain of blocks in the commit are related to each other. + pub votes_ancestries: Vec
, +} + +impl GrandpaJustification { + /// Returns reasonable size of justification using constants from the provided chain. + /// + /// An imprecise analogue of `MaxEncodedLen` implementation. We don't use it for + /// any precise calculations - that's just an estimation. + pub fn max_reasonable_size(required_precommits: u32) -> u32 + where + C: Chain + ChainWithGrandpa, + { + // we don't need precise results here - just estimations, so some details + // are removed from computations (e.g. bytes required to encode vector length) + + // structures in `finality_grandpa` crate are not implementing `MaxEncodedLength`, so + // here's our estimation for the `finality_grandpa::Commit` struct size + // + // precommit is: hash + number + // signed precommit is: precommit + signature (64b) + authority id + // commit is: hash + number + vec of signed precommits + let signed_precommit_size: u32 = BlockNumberOf::::max_encoded_len() + .saturating_add(HashOf::::max_encoded_len().saturated_into()) + .saturating_add(64) + .saturating_add(AuthorityId::max_encoded_len().saturated_into()) + .saturated_into(); + let max_expected_signed_commit_size = signed_precommit_size + .saturating_mul(required_precommits) + .saturating_add(BlockNumberOf::::max_encoded_len().saturated_into()) + .saturating_add(HashOf::::max_encoded_len().saturated_into()); + + let max_expected_votes_ancestries_size = C::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY + .saturating_mul(C::AVERAGE_HEADER_SIZE_IN_JUSTIFICATION); + + // justification is round number (u64=8b), a signed GRANDPA commit and the + // `votes_ancestries` vector + 8u32.saturating_add(max_expected_signed_commit_size) + .saturating_add(max_expected_votes_ancestries_size) + } + + /// Return identifier of header that this justification claims to finalize. + pub fn commit_target_id(&self) -> HeaderId { + HeaderId(self.commit.target_number, self.commit.target_hash) + } +} + +impl crate::FinalityProof for GrandpaJustification { + fn target_header_number(&self) -> H::Number { + self.commit.target_number + } +} + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum Error { + /// Failed to decode justification. + JustificationDecode, +} + +/// Given GRANDPA authorities set size, return number of valid authorities votes that the +/// justification must have to be valid. +/// +/// This function assumes that all authorities have the same vote weight. +pub fn required_justification_precommits(authorities_set_length: u32) -> u32 { + authorities_set_length - authorities_set_length.saturating_sub(1) / 3 +} + +/// Decode justification target. +pub fn decode_justification_target( + raw_justification: &[u8], +) -> Result<(Header::Hash, Header::Number), Error> { + GrandpaJustification::
::decode(&mut &*raw_justification) + .map(|justification| (justification.commit.target_hash, justification.commit.target_number)) + .map_err(|_| Error::JustificationDecode) +} diff --git a/bridges/primitives/header-chain/src/justification/verification/equivocation.rs b/bridges/primitives/header-chain/src/justification/verification/equivocation.rs new file mode 100644 index 0000000000000..0ade3736c2274 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/equivocation.rs @@ -0,0 +1,179 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for extracting equivocations from multiple GRANDPA Finality Proofs. + +use crate::justification::{ + verification::{ + Error as JustificationVerificationError, JustificationVerifier, PrecommitError, + SignedPrecommit, + }, + GrandpaJustification, +}; + +use crate::justification::verification::IterationFlow; +use finality_grandpa::voter_set::VoterSet; +use frame_support::RuntimeDebug; +use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, EquivocationProof, Precommit, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + prelude::*, +}; + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum Error { + /// Justification is targeting unexpected round. + InvalidRound, + /// Justification verification error. + JustificationVerification(JustificationVerificationError), +} + +enum AuthorityVotes { + SingleVote(SignedPrecommit
), + Equivocation( + finality_grandpa::Equivocation, AuthoritySignature>, + ), +} + +/// Structure that can extract equivocations from multiple GRANDPA justifications. +pub struct EquivocationsCollector<'a, Header: HeaderT> { + round: u64, + authorities_set_id: SetId, + authorities_set: &'a VoterSet, + + votes: BTreeMap>, +} + +impl<'a, Header: HeaderT> EquivocationsCollector<'a, Header> { + /// Create a new instance of `EquivocationsCollector`. + pub fn new( + authorities_set_id: SetId, + authorities_set: &'a VoterSet, + base_justification: &GrandpaJustification
, + ) -> Result { + let mut checker = Self { + round: base_justification.round, + authorities_set_id, + authorities_set, + votes: BTreeMap::new(), + }; + + checker.parse_justification(base_justification)?; + Ok(checker) + } + + /// Parse an additional justification for equivocations. + pub fn parse_justification( + &mut self, + justification: &GrandpaJustification
, + ) -> Result<(), Error> { + // The justification should target the same round as the base justification. + if self.round != justification.round { + return Err(Error::InvalidRound) + } + + self.verify_justification( + (justification.commit.target_hash, justification.commit.target_number), + self.authorities_set_id, + self.authorities_set, + justification, + ) + .map_err(Error::JustificationVerification) + } + + /// Extract the equivocation proofs that have been collected. + pub fn into_equivocation_proofs(self) -> Vec> { + let mut equivocations = vec![]; + for (_authority, vote) in self.votes { + if let AuthorityVotes::Equivocation(equivocation) = vote { + equivocations.push(EquivocationProof::new( + self.authorities_set_id, + sp_consensus_grandpa::Equivocation::Precommit(equivocation), + )); + } + } + + equivocations + } +} + +impl<'a, Header: HeaderT> JustificationVerifier
for EquivocationsCollector<'a, Header> { + fn process_redundant_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Ok(IterationFlow::Run) + } + + fn process_known_authority_vote( + &mut self, + _precommit_idx: usize, + _signed: &SignedPrecommit
, + ) -> Result { + Ok(IterationFlow::Run) + } + + fn process_unknown_authority_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Ok(()) + } + + fn process_unrelated_ancestry_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Ok(IterationFlow::Run) + } + + fn process_invalid_signature_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Ok(()) + } + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
) { + match self.votes.get_mut(&signed.id) { + Some(vote) => match vote { + AuthorityVotes::SingleVote(first_vote) => { + if first_vote.precommit != signed.precommit { + *vote = AuthorityVotes::Equivocation(finality_grandpa::Equivocation { + round_number: self.round, + identity: signed.id.clone(), + first: (first_vote.precommit.clone(), first_vote.signature.clone()), + second: (signed.precommit.clone(), signed.signature.clone()), + }); + } + }, + AuthorityVotes::Equivocation(_) => {}, + }, + None => { + self.votes.insert(signed.id.clone(), AuthorityVotes::SingleVote(signed.clone())); + }, + } + } + + fn process_redundant_votes_ancestries( + &mut self, + _redundant_votes_ancestries: BTreeSet, + ) -> Result<(), JustificationVerificationError> { + Ok(()) + } +} diff --git a/bridges/primitives/header-chain/src/justification/verification/mod.rs b/bridges/primitives/header-chain/src/justification/verification/mod.rs new file mode 100644 index 0000000000000..7cec1f14e966e --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/mod.rs @@ -0,0 +1,279 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for checking GRANDPA Finality Proofs. + +pub mod equivocation; +pub mod optimizer; +pub mod strict; + +use crate::justification::GrandpaJustification; + +use bp_runtime::HeaderId; +use finality_grandpa::voter_set::VoterSet; +use frame_support::RuntimeDebug; +use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + prelude::*, +}; + +type SignedPrecommit
= finality_grandpa::SignedPrecommit< +
::Hash, +
::Number, + AuthoritySignature, + AuthorityId, +>; + +/// Votes ancestries with useful methods. +#[derive(RuntimeDebug)] +pub struct AncestryChain { + /// We expect all forks in the ancestry chain to be descendants of base. + base: HeaderId, + /// Header hash => parent header hash mapping. + pub parents: BTreeMap, + /// Hashes of headers that were not visited by `ancestry()`. + pub unvisited: BTreeSet, +} + +impl AncestryChain
{ + /// Create new ancestry chain. + pub fn new(justification: &GrandpaJustification
) -> AncestryChain
{ + let mut parents = BTreeMap::new(); + let mut unvisited = BTreeSet::new(); + for ancestor in &justification.votes_ancestries { + let hash = ancestor.hash(); + let parent_hash = *ancestor.parent_hash(); + parents.insert(hash, parent_hash); + unvisited.insert(hash); + } + AncestryChain { base: justification.commit_target_id(), parents, unvisited } + } + + /// Returns a route if the precommit target block is a descendant of the `base` block. + pub fn ancestry( + &self, + precommit_target_hash: &Header::Hash, + precommit_target_number: &Header::Number, + ) -> Option> { + if precommit_target_number < &self.base.number() { + return None + } + + let mut route = vec![]; + let mut current_hash = *precommit_target_hash; + loop { + if current_hash == self.base.hash() { + break + } + + current_hash = match self.parents.get(¤t_hash) { + Some(parent_hash) => { + let is_visited_before = self.unvisited.get(¤t_hash).is_none(); + if is_visited_before { + // If the current header has been visited in a previous call, it is a + // descendent of `base` (we assume that the previous call was successful). + return Some(route) + } + route.push(current_hash); + + *parent_hash + }, + None => return None, + }; + } + + Some(route) + } + + fn mark_route_as_visited(&mut self, route: Vec) { + for hash in route { + self.unvisited.remove(&hash); + } + } + + fn is_fully_visited(&self) -> bool { + self.unvisited.is_empty() + } +} + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum Error { + /// Justification is finalizing unexpected header. + InvalidJustificationTarget, + /// Error validating a precommit + Precommit(PrecommitError), + /// The cumulative weight of all votes in the justification is not enough to justify commit + /// header finalization. + TooLowCumulativeWeight, + /// The justification contains extra (unused) headers in its `votes_ancestries` field. + RedundantVotesAncestries, +} + +/// Justification verification error. +#[derive(Eq, RuntimeDebug, PartialEq)] +pub enum PrecommitError { + /// Justification contains redundant votes. + RedundantAuthorityVote, + /// Justification contains unknown authority precommit. + UnknownAuthorityVote, + /// Justification contains duplicate authority precommit. + DuplicateAuthorityVote, + /// The authority has provided an invalid signature. + InvalidAuthoritySignature, + /// The justification contains precommit for header that is not a descendant of the commit + /// header. + UnrelatedAncestryVote, +} + +enum IterationFlow { + Run, + Skip, +} + +/// Verification callbacks. +trait JustificationVerifier { + fn process_redundant_vote( + &mut self, + precommit_idx: usize, + ) -> Result; + + fn process_known_authority_vote( + &mut self, + precommit_idx: usize, + signed: &SignedPrecommit
, + ) -> Result; + + fn process_unknown_authority_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError>; + + fn process_unrelated_ancestry_vote( + &mut self, + precommit_idx: usize, + ) -> Result; + + fn process_invalid_signature_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError>; + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
); + + /// Called when there are redundant headers in the votes ancestries. + fn process_redundant_votes_ancestries( + &mut self, + redundant_votes_ancestries: BTreeSet, + ) -> Result<(), Error>; + + fn verify_justification( + &mut self, + finalized_target: (Header::Hash, Header::Number), + authorities_set_id: SetId, + authorities_set: &VoterSet, + justification: &GrandpaJustification
, + ) -> Result<(), Error> { + // ensure that it is justification for the expected header + if (justification.commit.target_hash, justification.commit.target_number) != + finalized_target + { + return Err(Error::InvalidJustificationTarget) + } + + let threshold = authorities_set.threshold().get(); + let mut chain = AncestryChain::new(justification); + let mut signature_buffer = Vec::new(); + let mut cumulative_weight = 0u64; + + for (precommit_idx, signed) in justification.commit.precommits.iter().enumerate() { + if cumulative_weight >= threshold { + let action = + self.process_redundant_vote(precommit_idx).map_err(Error::Precommit)?; + if matches!(action, IterationFlow::Skip) { + continue + } + } + + // authority must be in the set + let authority_info = match authorities_set.get(&signed.id) { + Some(authority_info) => { + // The implementer may want to do extra checks here. + // For example to see if the authority has already voted in the same round. + let action = self + .process_known_authority_vote(precommit_idx, signed) + .map_err(Error::Precommit)?; + if matches!(action, IterationFlow::Skip) { + continue + } + + authority_info + }, + None => { + self.process_unknown_authority_vote(precommit_idx).map_err(Error::Precommit)?; + continue + }, + }; + + // all precommits must be descendants of the target block + let maybe_route = + chain.ancestry(&signed.precommit.target_hash, &signed.precommit.target_number); + if maybe_route.is_none() { + let action = self + .process_unrelated_ancestry_vote(precommit_idx) + .map_err(Error::Precommit)?; + if matches!(action, IterationFlow::Skip) { + continue + } + } + + // verify authority signature + if !sp_consensus_grandpa::check_message_signature_with_buffer( + &finality_grandpa::Message::Precommit(signed.precommit.clone()), + &signed.id, + &signed.signature, + justification.round, + authorities_set_id, + &mut signature_buffer, + ) { + self.process_invalid_signature_vote(precommit_idx).map_err(Error::Precommit)?; + continue + } + + // now we can count the vote since we know that it is valid + self.process_valid_vote(signed); + if let Some(route) = maybe_route { + chain.mark_route_as_visited(route); + cumulative_weight = cumulative_weight.saturating_add(authority_info.weight().get()); + } + } + + // check that the cumulative weight of validators that voted for the justification target + // (or one of its descendents) is larger than the required threshold. + if cumulative_weight < threshold { + return Err(Error::TooLowCumulativeWeight) + } + + // check that there are no extra headers in the justification + if !chain.is_fully_visited() { + self.process_redundant_votes_ancestries(chain.unvisited)?; + } + + Ok(()) + } +} diff --git a/bridges/primitives/header-chain/src/justification/verification/optimizer.rs b/bridges/primitives/header-chain/src/justification/verification/optimizer.rs new file mode 100644 index 0000000000000..4cc6778ff511e --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/optimizer.rs @@ -0,0 +1,132 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for optimizing GRANDPA Finality Proofs. + +use crate::justification::{ + verification::{Error, JustificationVerifier, PrecommitError}, + GrandpaJustification, +}; + +use crate::justification::verification::{IterationFlow, SignedPrecommit}; +use finality_grandpa::voter_set::VoterSet; +use sp_consensus_grandpa::{AuthorityId, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::{collections::btree_set::BTreeSet, prelude::*}; + +// Verification callbacks for justification optimization. +struct JustificationOptimizer { + votes: BTreeSet, + + extra_precommits: Vec, + redundant_votes_ancestries: BTreeSet, +} + +impl JustificationOptimizer
{ + fn optimize(self, justification: &mut GrandpaJustification
) { + for invalid_precommit_idx in self.extra_precommits.into_iter().rev() { + justification.commit.precommits.remove(invalid_precommit_idx); + } + if !self.redundant_votes_ancestries.is_empty() { + justification + .votes_ancestries + .retain(|header| !self.redundant_votes_ancestries.contains(&header.hash())) + } + } +} + +impl JustificationVerifier
for JustificationOptimizer
{ + fn process_redundant_vote( + &mut self, + precommit_idx: usize, + ) -> Result { + self.extra_precommits.push(precommit_idx); + Ok(IterationFlow::Skip) + } + + fn process_known_authority_vote( + &mut self, + precommit_idx: usize, + signed: &SignedPrecommit
, + ) -> Result { + // Skip duplicate votes + if self.votes.contains(&signed.id) { + self.extra_precommits.push(precommit_idx); + return Ok(IterationFlow::Skip) + } + + Ok(IterationFlow::Run) + } + + fn process_unknown_authority_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError> { + self.extra_precommits.push(precommit_idx); + Ok(()) + } + + fn process_unrelated_ancestry_vote( + &mut self, + precommit_idx: usize, + ) -> Result { + self.extra_precommits.push(precommit_idx); + Ok(IterationFlow::Skip) + } + + fn process_invalid_signature_vote( + &mut self, + precommit_idx: usize, + ) -> Result<(), PrecommitError> { + self.extra_precommits.push(precommit_idx); + Ok(()) + } + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
) { + self.votes.insert(signed.id.clone()); + } + + fn process_redundant_votes_ancestries( + &mut self, + redundant_votes_ancestries: BTreeSet, + ) -> Result<(), Error> { + self.redundant_votes_ancestries = redundant_votes_ancestries; + Ok(()) + } +} + +/// Verify and optimize given justification by removing unknown and duplicate votes. +pub fn verify_and_optimize_justification( + finalized_target: (Header::Hash, Header::Number), + authorities_set_id: SetId, + authorities_set: &VoterSet, + justification: &mut GrandpaJustification
, +) -> Result<(), Error> { + let mut optimizer = JustificationOptimizer { + votes: BTreeSet::new(), + extra_precommits: vec![], + redundant_votes_ancestries: Default::default(), + }; + optimizer.verify_justification( + finalized_target, + authorities_set_id, + authorities_set, + justification, + )?; + optimizer.optimize(justification); + + Ok(()) +} diff --git a/bridges/primitives/header-chain/src/justification/verification/strict.rs b/bridges/primitives/header-chain/src/justification/verification/strict.rs new file mode 100644 index 0000000000000..da936c2358277 --- /dev/null +++ b/bridges/primitives/header-chain/src/justification/verification/strict.rs @@ -0,0 +1,106 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Logic for checking if GRANDPA Finality Proofs are valid and optimal. + +use crate::justification::{ + verification::{Error, JustificationVerifier, PrecommitError}, + GrandpaJustification, +}; + +use crate::justification::verification::{IterationFlow, SignedPrecommit}; +use finality_grandpa::voter_set::VoterSet; +use sp_consensus_grandpa::{AuthorityId, SetId}; +use sp_runtime::traits::Header as HeaderT; +use sp_std::collections::btree_set::BTreeSet; + +/// Verification callbacks that reject all unknown, duplicate or redundant votes. +struct StrictJustificationVerifier { + votes: BTreeSet, +} + +impl JustificationVerifier
for StrictJustificationVerifier { + fn process_redundant_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Err(PrecommitError::RedundantAuthorityVote) + } + + fn process_known_authority_vote( + &mut self, + _precommit_idx: usize, + signed: &SignedPrecommit
, + ) -> Result { + if self.votes.contains(&signed.id) { + // There's a lot of code in `validate_commit` and `import_precommit` functions + // inside `finality-grandpa` crate (mostly related to reporting equivocations). + // But the only thing that we care about is that only first vote from the + // authority is accepted + return Err(PrecommitError::DuplicateAuthorityVote) + } + + Ok(IterationFlow::Run) + } + + fn process_unknown_authority_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Err(PrecommitError::UnknownAuthorityVote) + } + + fn process_unrelated_ancestry_vote( + &mut self, + _precommit_idx: usize, + ) -> Result { + Err(PrecommitError::UnrelatedAncestryVote) + } + + fn process_invalid_signature_vote( + &mut self, + _precommit_idx: usize, + ) -> Result<(), PrecommitError> { + Err(PrecommitError::InvalidAuthoritySignature) + } + + fn process_valid_vote(&mut self, signed: &SignedPrecommit
) { + self.votes.insert(signed.id.clone()); + } + + fn process_redundant_votes_ancestries( + &mut self, + _redundant_votes_ancestries: BTreeSet, + ) -> Result<(), Error> { + Err(Error::RedundantVotesAncestries) + } +} + +/// Verify that justification, that is generated by given authority set, finalizes given header. +pub fn verify_justification( + finalized_target: (Header::Hash, Header::Number), + authorities_set_id: SetId, + authorities_set: &VoterSet, + justification: &GrandpaJustification
, +) -> Result<(), Error> { + let mut verifier = StrictJustificationVerifier { votes: BTreeSet::new() }; + verifier.verify_justification( + finalized_target, + authorities_set_id, + authorities_set, + justification, + ) +} diff --git a/bridges/primitives/header-chain/tests/implementation_match.rs b/bridges/primitives/header-chain/tests/implementation_match.rs index d5e42e214976b..c4cd7f5f5b26e 100644 --- a/bridges/primitives/header-chain/tests/implementation_match.rs +++ b/bridges/primitives/header-chain/tests/implementation_match.rs @@ -21,7 +21,9 @@ //! Some of tests in this module may partially duplicate tests from `justification.rs`, //! but their purpose is different. -use bp_header_chain::justification::{verify_justification, Error, GrandpaJustification}; +use bp_header_chain::justification::{ + verify_justification, GrandpaJustification, JustificationVerificationError, PrecommitError, +}; use bp_test_utils::{ header_id, make_justification_for_header, signed_precommit, test_header, Account, JustificationGeneratorParams, ALICE, BOB, CHARLIE, DAVE, EVE, FERDIE, TEST_GRANDPA_SET_ID, @@ -85,13 +87,6 @@ fn minimal_accounts_set() -> Vec<(Account, AuthorityWeight)> { vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1), (DAVE, 1)] } -/// Get a minimal subset of GRANDPA authorities that have enough cumulative vote weight to justify a -/// header finality. -pub fn minimal_voter_set() -> VoterSet { - VoterSet::new(minimal_accounts_set().iter().map(|(id, w)| (AuthorityId::from(*id), *w))) - .unwrap() -} - /// Make a valid GRANDPA justification with sensible defaults. pub fn make_default_justification(header: &TestHeader) -> GrandpaJustification { make_justification_for_header(JustificationGeneratorParams { @@ -124,7 +119,7 @@ fn same_result_when_precommit_target_has_lower_number_than_commit_target() { &full_voter_set(), &justification, ), - Err(Error::UnrelatedAncestryVote), + Err(JustificationVerificationError::Precommit(PrecommitError::UnrelatedAncestryVote)), ); // original implementation returns `Ok(validation_result)` @@ -157,7 +152,7 @@ fn same_result_when_precommit_target_is_not_descendant_of_commit_target() { &full_voter_set(), &justification, ), - Err(Error::UnrelatedAncestryVote), + Err(JustificationVerificationError::Precommit(PrecommitError::UnrelatedAncestryVote)), ); // original implementation returns `Ok(validation_result)` @@ -191,7 +186,7 @@ fn same_result_when_there_are_not_enough_cumulative_weight_to_finalize_commit_ta &full_voter_set(), &justification, ), - Err(Error::TooLowCumulativeWeight), + Err(JustificationVerificationError::TooLowCumulativeWeight), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == false`. @@ -229,7 +224,7 @@ fn different_result_when_justification_contains_duplicate_vote() { &full_voter_set(), &justification, ), - Err(Error::DuplicateAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -270,7 +265,7 @@ fn different_results_when_authority_equivocates_once_in_a_round() { &full_voter_set(), &justification, ), - Err(Error::DuplicateAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -323,7 +318,7 @@ fn different_results_when_authority_equivocates_twice_in_a_round() { &full_voter_set(), &justification, ), - Err(Error::DuplicateAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::DuplicateAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -362,7 +357,7 @@ fn different_results_when_there_are_more_than_enough_votes() { &full_voter_set(), &justification, ), - Err(Error::RedundantVotesInJustification), + Err(JustificationVerificationError::Precommit(PrecommitError::RedundantAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. @@ -403,7 +398,7 @@ fn different_results_when_there_is_a_vote_of_unknown_authority() { &full_voter_set(), &justification, ), - Err(Error::UnknownAuthorityVote), + Err(JustificationVerificationError::Precommit(PrecommitError::UnknownAuthorityVote)), ); // original implementation returns `Ok(validation_result)` // with `validation_result.is_valid() == true`. diff --git a/bridges/primitives/header-chain/tests/justification/equivocation.rs b/bridges/primitives/header-chain/tests/justification/equivocation.rs new file mode 100644 index 0000000000000..072d5668edeb4 --- /dev/null +++ b/bridges/primitives/header-chain/tests/justification/equivocation.rs @@ -0,0 +1,124 @@ +// Copyright 2020-2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Tests for Grandpa equivocations collector code. + +use bp_header_chain::justification::EquivocationsCollector; +use bp_test_utils::*; +use finality_grandpa::Precommit; +use sp_consensus_grandpa::EquivocationProof; + +type TestHeader = sp_runtime::testing::Header; + +#[test] +fn duplicate_votes_are_not_considered_equivocations() { + let voter_set = voter_set(); + let base_justification = make_default_justification::(&test_header(1)); + + let mut collector = + EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap(); + collector.parse_justification(&base_justification.clone()).unwrap(); + + assert_eq!(collector.into_equivocation_proofs().len(), 0); +} + +#[test] +fn equivocations_are_detected_in_base_justification_redundant_votes() { + let voter_set = voter_set(); + let mut base_justification = make_default_justification::(&test_header(1)); + + let first_vote = base_justification.commit.precommits[0].clone(); + let equivocation = signed_precommit::( + &ALICE, + header_id::(1), + base_justification.round, + TEST_GRANDPA_SET_ID, + ); + base_justification.commit.precommits.push(equivocation.clone()); + + let collector = + EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap(); + + assert_eq!( + collector.into_equivocation_proofs(), + vec![EquivocationProof::new( + 1, + sp_consensus_grandpa::Equivocation::Precommit(finality_grandpa::Equivocation { + round_number: 1, + identity: ALICE.into(), + first: ( + Precommit { + target_hash: first_vote.precommit.target_hash, + target_number: first_vote.precommit.target_number + }, + first_vote.signature + ), + second: ( + Precommit { + target_hash: equivocation.precommit.target_hash, + target_number: equivocation.precommit.target_number + }, + equivocation.signature + ) + }) + )] + ); +} + +#[test] +fn equivocations_are_detected_in_extra_justification_redundant_votes() { + let voter_set = voter_set(); + let base_justification = make_default_justification::(&test_header(1)); + let first_vote = base_justification.commit.precommits[0].clone(); + + let mut extra_justification = base_justification.clone(); + let equivocation = signed_precommit::( + &ALICE, + header_id::(1), + base_justification.round, + TEST_GRANDPA_SET_ID, + ); + extra_justification.commit.precommits.push(equivocation.clone()); + + let mut collector = + EquivocationsCollector::new(TEST_GRANDPA_SET_ID, &voter_set, &base_justification).unwrap(); + collector.parse_justification(&extra_justification).unwrap(); + + assert_eq!( + collector.into_equivocation_proofs(), + vec![EquivocationProof::new( + 1, + sp_consensus_grandpa::Equivocation::Precommit(finality_grandpa::Equivocation { + round_number: 1, + identity: ALICE.into(), + first: ( + Precommit { + target_hash: first_vote.precommit.target_hash, + target_number: first_vote.precommit.target_number + }, + first_vote.signature + ), + second: ( + Precommit { + target_hash: equivocation.precommit.target_hash, + target_number: equivocation.precommit.target_number + }, + equivocation.signature + ) + }) + )] + ); +} diff --git a/bridges/primitives/header-chain/tests/justification.rs b/bridges/primitives/header-chain/tests/justification/optimizer.rs similarity index 55% rename from bridges/primitives/header-chain/tests/justification.rs rename to bridges/primitives/header-chain/tests/justification/optimizer.rs index 26ed67fa65f61..8d1ba5ac6facb 100644 --- a/bridges/primitives/header-chain/tests/justification.rs +++ b/bridges/primitives/header-chain/tests/justification/optimizer.rs @@ -14,189 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . -//! Tests for Grandpa Justification code. +//! Tests for Grandpa Justification optimizer code. -use bp_header_chain::justification::{ - required_justification_precommits, verify_and_optimize_justification, verify_justification, - Error, -}; +use bp_header_chain::justification::verify_and_optimize_justification; use bp_test_utils::*; use finality_grandpa::SignedPrecommit; use sp_consensus_grandpa::AuthoritySignature; type TestHeader = sp_runtime::testing::Header; -#[test] -fn valid_justification_accepted() { - let authorities = vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)]; - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: authorities.clone(), - ancestors: 7, - forks: 3, - }; - - let justification = make_justification_for_header::(params.clone()); - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Ok(()), - ); - - assert_eq!(justification.commit.precommits.len(), authorities.len()); - assert_eq!(justification.votes_ancestries.len(), params.ancestors as usize); -} - -#[test] -fn valid_justification_accepted_with_single_fork() { - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)], - ancestors: 5, - forks: 1, - }; - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &make_justification_for_header::(params) - ), - Ok(()), - ); -} - -#[test] -fn valid_justification_accepted_with_arbitrary_number_of_authorities() { - use finality_grandpa::voter_set::VoterSet; - use sp_consensus_grandpa::AuthorityId; - - let n = 15; - let required_signatures = required_justification_precommits(n as _); - let authorities = accounts(n).iter().map(|k| (*k, 1)).collect::>(); - - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: authorities.clone().into_iter().take(required_signatures as _).collect(), - ancestors: n.into(), - forks: required_signatures, - }; - - let authorities = authorities - .iter() - .map(|(id, w)| (AuthorityId::from(*id), *w)) - .collect::>(); - let voter_set = VoterSet::new(authorities).unwrap(); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set, - &make_justification_for_header::(params) - ), - Ok(()), - ); -} - -#[test] -fn justification_with_invalid_target_rejected() { - assert_eq!( - verify_justification::( - header_id::(2), - TEST_GRANDPA_SET_ID, - &voter_set(), - &make_default_justification::(&test_header(1)), - ), - Err(Error::InvalidJustificationTarget), - ); -} - -#[test] -fn justification_with_invalid_commit_rejected() { - let mut justification = make_default_justification::(&test_header(1)); - justification.commit.precommits.clear(); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Err(Error::TooLowCumulativeWeight), - ); -} - -#[test] -fn justification_with_invalid_authority_signature_rejected() { - let mut justification = make_default_justification::(&test_header(1)); - justification.commit.precommits[0].signature = - sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Err(Error::InvalidAuthoritySignature), - ); -} - -#[test] -fn justification_with_invalid_precommit_ancestry() { - let mut justification = make_default_justification::(&test_header(1)); - justification.votes_ancestries.push(test_header(10)); - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &justification, - ), - Err(Error::RedundantVotesAncestries), - ); -} - -#[test] -fn justification_is_invalid_if_we_dont_meet_threshold() { - // Need at least three authorities to sign off or else the voter set threshold can't be reached - let authorities = vec![(ALICE, 1), (BOB, 1)]; - - let params = JustificationGeneratorParams { - header: test_header(1), - round: TEST_GRANDPA_ROUND, - set_id: TEST_GRANDPA_SET_ID, - authorities: authorities.clone(), - ancestors: 2 * authorities.len() as u32, - forks: 2, - }; - - assert_eq!( - verify_justification::( - header_id::(1), - TEST_GRANDPA_SET_ID, - &voter_set(), - &make_justification_for_header::(params) - ), - Err(Error::TooLowCumulativeWeight), - ); -} - #[test] fn optimizer_does_noting_with_minimal_justification() { let mut justification = make_default_justification::(&test_header(1)); diff --git a/bridges/primitives/header-chain/tests/justification/strict.rs b/bridges/primitives/header-chain/tests/justification/strict.rs new file mode 100644 index 0000000000000..bf8fa5c9f4579 --- /dev/null +++ b/bridges/primitives/header-chain/tests/justification/strict.rs @@ -0,0 +1,196 @@ +// Copyright 2020-2021 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Tests for Grandpa strict justification verifier code. + +use bp_header_chain::justification::{ + required_justification_precommits, verify_justification, JustificationVerificationError, + PrecommitError, +}; +use bp_test_utils::*; + +type TestHeader = sp_runtime::testing::Header; + +#[test] +fn valid_justification_accepted() { + let authorities = vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)]; + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: authorities.clone(), + ancestors: 7, + forks: 3, + }; + + let justification = make_justification_for_header::(params.clone()); + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Ok(()), + ); + + assert_eq!(justification.commit.precommits.len(), authorities.len()); + assert_eq!(justification.votes_ancestries.len(), params.ancestors as usize); +} + +#[test] +fn valid_justification_accepted_with_single_fork() { + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: vec![(ALICE, 1), (BOB, 1), (CHARLIE, 1)], + ancestors: 5, + forks: 1, + }; + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &make_justification_for_header::(params) + ), + Ok(()), + ); +} + +#[test] +fn valid_justification_accepted_with_arbitrary_number_of_authorities() { + use finality_grandpa::voter_set::VoterSet; + use sp_consensus_grandpa::AuthorityId; + + let n = 15; + let required_signatures = required_justification_precommits(n as _); + let authorities = accounts(n).iter().map(|k| (*k, 1)).collect::>(); + + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: authorities.clone().into_iter().take(required_signatures as _).collect(), + ancestors: n.into(), + forks: required_signatures, + }; + + let authorities = authorities + .iter() + .map(|(id, w)| (AuthorityId::from(*id), *w)) + .collect::>(); + let voter_set = VoterSet::new(authorities).unwrap(); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set, + &make_justification_for_header::(params) + ), + Ok(()), + ); +} + +#[test] +fn justification_with_invalid_target_rejected() { + assert_eq!( + verify_justification::( + header_id::(2), + TEST_GRANDPA_SET_ID, + &voter_set(), + &make_default_justification::(&test_header(1)), + ), + Err(JustificationVerificationError::InvalidJustificationTarget), + ); +} + +#[test] +fn justification_with_invalid_commit_rejected() { + let mut justification = make_default_justification::(&test_header(1)); + justification.commit.precommits.clear(); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Err(JustificationVerificationError::TooLowCumulativeWeight), + ); +} + +#[test] +fn justification_with_invalid_authority_signature_rejected() { + let mut justification = make_default_justification::(&test_header(1)); + justification.commit.precommits[0].signature = + sp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Err(JustificationVerificationError::Precommit(PrecommitError::InvalidAuthoritySignature)), + ); +} + +#[test] +fn justification_with_invalid_precommit_ancestry() { + let mut justification = make_default_justification::(&test_header(1)); + justification.votes_ancestries.push(test_header(10)); + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &justification, + ), + Err(JustificationVerificationError::RedundantVotesAncestries), + ); +} + +#[test] +fn justification_is_invalid_if_we_dont_meet_threshold() { + // Need at least three authorities to sign off or else the voter set threshold can't be reached + let authorities = vec![(ALICE, 1), (BOB, 1)]; + + let params = JustificationGeneratorParams { + header: test_header(1), + round: TEST_GRANDPA_ROUND, + set_id: TEST_GRANDPA_SET_ID, + authorities: authorities.clone(), + ancestors: 2 * authorities.len() as u32, + forks: 2, + }; + + assert_eq!( + verify_justification::( + header_id::(1), + TEST_GRANDPA_SET_ID, + &voter_set(), + &make_justification_for_header::(params) + ), + Err(JustificationVerificationError::TooLowCumulativeWeight), + ); +} diff --git a/bridges/primitives/header-chain/tests/tests.rs b/bridges/primitives/header-chain/tests/tests.rs new file mode 100644 index 0000000000000..7c525a9303adc --- /dev/null +++ b/bridges/primitives/header-chain/tests/tests.rs @@ -0,0 +1,7 @@ +mod justification { + mod equivocation; + mod optimizer; + mod strict; +} + +mod implementation_match;