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,
Entry::{Occupied, Vacant},
},
btree_set::BTreeSet,
},
prelude::*,
vec,
vec::Vec,
};
type SignedPrecommit<Header> = finality_grandpa::SignedPrecommit<
<Header as HeaderT>::Hash,
<Header as HeaderT>::Number,
AuthoritySignature,
AuthorityId,
>;
#[derive(RuntimeDebug)]
pub struct AncestryChain<Header: HeaderT> {
base: HeaderId<Header::Hash, Header::Number>,
parents: BTreeMap<Header::Hash, Header::Hash>,
unvisited: BTreeSet<Header::Hash>,
}
impl<Header: HeaderT> AncestryChain<Header> {
pub fn new(
justification: &GrandpaJustification<Header>,
) -> (AncestryChain<Header>, Vec<usize>) {
let mut parents = BTreeMap::new();
let mut unvisited = BTreeSet::new();
let mut ignored_idxs = Vec::new();
for (idx, ancestor) in justification.votes_ancestries.iter().enumerate() {
let hash = ancestor.hash();
match parents.entry(hash) {
Occupied(_) => {
ignored_idxs.push(idx);
},
Vacant(entry) => {
entry.insert(*ancestor.parent_hash());
unvisited.insert(hash);
},
}
}
(AncestryChain { base: justification.commit_target_id(), parents, unvisited }, ignored_idxs)
}
pub fn parent_hash_of(&self, hash: &Header::Hash) -> Option<&Header::Hash> {
self.parents.get(hash)
}
pub fn ancestry(
&self,
precommit_target_hash: &Header::Hash,
precommit_target_number: &Header::Number,
) -> Option<Vec<Header::Hash>> {
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.parent_hash_of(¤t_hash) {
Some(parent_hash) => {
let is_visited_before = self.unvisited.get(¤t_hash).is_none();
if is_visited_before {
return Some(route)
}
route.push(current_hash);
*parent_hash
},
None => return None,
};
}
Some(route)
}
fn mark_route_as_visited(&mut self, route: Vec<Header::Hash>) {
for hash in route {
self.unvisited.remove(&hash);
}
}
fn is_fully_visited(&self) -> bool {
self.unvisited.is_empty()
}
}
#[derive(Eq, RuntimeDebug, PartialEq)]
pub enum Error {
InvalidAuthorityList,
InvalidJustificationTarget,
DuplicateVotesAncestries,
Precommit(PrecommitError),
TooLowCumulativeWeight,
RedundantVotesAncestries,
}
#[derive(Eq, RuntimeDebug, PartialEq)]
pub enum PrecommitError {
RedundantAuthorityVote,
UnknownAuthorityVote,
DuplicateAuthorityVote,
InvalidAuthoritySignature,
UnrelatedAncestryVote,
}
#[derive(RuntimeDebug)]
pub struct JustificationVerificationContext {
pub voter_set: VoterSet<AuthorityId>,
pub authority_set_id: SetId,
}
impl TryFrom<AuthoritySet> for JustificationVerificationContext {
type Error = Error;
fn try_from(authority_set: AuthoritySet) -> Result<Self, Self::Error> {
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,
}
trait JustificationVerifier<Header: HeaderT> {
fn process_duplicate_votes_ancestries(
&mut self,
duplicate_votes_ancestries: Vec<usize>,
) -> Result<(), Error>;
fn process_redundant_vote(
&mut self,
precommit_idx: usize,
) -> Result<IterationFlow, PrecommitError>;
fn process_known_authority_vote(
&mut self,
precommit_idx: usize,
signed: &SignedPrecommit<Header>,
) -> Result<IterationFlow, PrecommitError>;
fn process_unknown_authority_vote(
&mut self,
precommit_idx: usize,
) -> Result<(), PrecommitError>;
fn process_unrelated_ancestry_vote(
&mut self,
precommit_idx: usize,
) -> Result<IterationFlow, PrecommitError>;
fn process_invalid_signature_vote(
&mut self,
precommit_idx: usize,
) -> Result<(), PrecommitError>;
fn process_valid_vote(&mut self, signed: &SignedPrecommit<Header>);
fn process_redundant_votes_ancestries(
&mut self,
redundant_votes_ancestries: BTreeSet<Header::Hash>,
) -> Result<(), Error>;
fn verify_justification(
&mut self,
finalized_target: (Header::Hash, Header::Number),
context: &JustificationVerificationContext,
justification: &GrandpaJustification<Header>,
) -> Result<(), Error> {
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, ignored_idxs) = AncestryChain::new(justification);
let mut signature_buffer = Vec::new();
let mut cumulative_weight = 0u64;
if !ignored_idxs.is_empty() {
self.process_duplicate_votes_ancestries(ignored_idxs)?;
}
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
}
}
let authority_info = match context.voter_set.get(&signed.id) {
Some(authority_info) => {
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
},
};
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
}
}
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
}
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());
}
}
if cumulative_weight < threshold {
return Err(Error::TooLowCumulativeWeight)
}
if !chain.is_fully_visited() {
self.process_redundant_votes_ancestries(chain.unvisited)?;
}
Ok(())
}
}