// Copyright (C) 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, AuthoritySet}; use bp_runtime::HeaderId; use finality_grandpa::voter_set::VoterSet; use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, SetId}; use sp_runtime::{traits::Header as HeaderT, RuntimeDebug}; 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 { /// Could not convert `AuthorityList` to `VoterSet`. InvalidAuthorityList, /// 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, } /// The context needed for validating GRANDPA finality proofs. #[derive(RuntimeDebug)] pub struct JustificationVerificationContext { /// The authority set used to verify the justification. pub voter_set: VoterSet, /// The ID of the authority set used to verify the justification. pub authority_set_id: SetId, } impl TryFrom for JustificationVerificationContext { type Error = Error; fn try_from(authority_set: AuthoritySet) -> Result { let voter_set = VoterSet::new(authority_set.authorities).ok_or(Error::InvalidAuthorityList)?; Ok(JustificationVerificationContext { voter_set, authority_set_id: authority_set.set_id }) } } 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), context: &JustificationVerificationContext, 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 = context.voter_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 context.voter_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, context.authority_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(()) } }