forked from paritytech/polkadot-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GRANDPA justifications: equivocation detection primitives (paritytech…
…#2295) (paritytech#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
- Loading branch information
Showing
11 changed files
with
1,163 additions
and
636 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
127 changes: 127 additions & 0 deletions
127
bridges/primitives/header-chain/src/justification/mod.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <http://www.gnu.org/licenses/>. | ||
|
||
//! 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<Header: HeaderT> { | ||
/// 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<Header::Hash, Header::Number, AuthoritySignature, AuthorityId>, | ||
/// A proof that the chain of blocks in the commit are related to each other. | ||
pub votes_ancestries: Vec<Header>, | ||
} | ||
|
||
impl<H: HeaderT> GrandpaJustification<H> { | ||
/// 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<C>(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::<C>::max_encoded_len() | ||
.saturating_add(HashOf::<C>::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::<C>::max_encoded_len().saturated_into()) | ||
.saturating_add(HashOf::<C>::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<H::Hash, H::Number> { | ||
HeaderId(self.commit.target_number, self.commit.target_hash) | ||
} | ||
} | ||
|
||
impl<H: HeaderT> crate::FinalityProof<H::Number> for GrandpaJustification<H> { | ||
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<Header: HeaderT>( | ||
raw_justification: &[u8], | ||
) -> Result<(Header::Hash, Header::Number), Error> { | ||
GrandpaJustification::<Header>::decode(&mut &*raw_justification) | ||
.map(|justification| (justification.commit.target_hash, justification.commit.target_number)) | ||
.map_err(|_| Error::JustificationDecode) | ||
} |
179 changes: 179 additions & 0 deletions
179
bridges/primitives/header-chain/src/justification/verification/equivocation.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <http://www.gnu.org/licenses/>. | ||
|
||
//! 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<Header: HeaderT> { | ||
SingleVote(SignedPrecommit<Header>), | ||
Equivocation( | ||
finality_grandpa::Equivocation<AuthorityId, Precommit<Header>, 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<AuthorityId>, | ||
|
||
votes: BTreeMap<AuthorityId, AuthorityVotes<Header>>, | ||
} | ||
|
||
impl<'a, Header: HeaderT> EquivocationsCollector<'a, Header> { | ||
/// Create a new instance of `EquivocationsCollector`. | ||
pub fn new( | ||
authorities_set_id: SetId, | ||
authorities_set: &'a VoterSet<AuthorityId>, | ||
base_justification: &GrandpaJustification<Header>, | ||
) -> Result<Self, Error> { | ||
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<Header>, | ||
) -> 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<EquivocationProof<Header::Hash, Header::Number>> { | ||
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<Header> for EquivocationsCollector<'a, Header> { | ||
fn process_redundant_vote( | ||
&mut self, | ||
_precommit_idx: usize, | ||
) -> Result<IterationFlow, PrecommitError> { | ||
Ok(IterationFlow::Run) | ||
} | ||
|
||
fn process_known_authority_vote( | ||
&mut self, | ||
_precommit_idx: usize, | ||
_signed: &SignedPrecommit<Header>, | ||
) -> Result<IterationFlow, PrecommitError> { | ||
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<IterationFlow, PrecommitError> { | ||
Ok(IterationFlow::Run) | ||
} | ||
|
||
fn process_invalid_signature_vote( | ||
&mut self, | ||
_precommit_idx: usize, | ||
) -> Result<(), PrecommitError> { | ||
Ok(()) | ||
} | ||
|
||
fn process_valid_vote(&mut self, signed: &SignedPrecommit<Header>) { | ||
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<Header::Hash>, | ||
) -> Result<(), JustificationVerificationError> { | ||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.