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;