use std::{
collections::{HashMap, HashSet},
marker::PhantomData,
sync::Arc,
};
use codec::{Decode, DecodeAll, Encode};
use finality_grandpa::{voter_set::VoterSet, Error as GrandpaError};
use sp_blockchain::{Error as ClientError, HeaderBackend};
use sp_consensus_grandpa::AuthorityId;
use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use crate::{AuthorityList, Commit, Error};
#[derive(Clone, Encode, Decode, PartialEq, Eq, Debug)]
pub struct GrandpaJustification<Block: BlockT> {
pub justification: sp_consensus_grandpa::GrandpaJustification<Block::Header>,
_block: PhantomData<Block>,
}
impl<Block: BlockT> From<sp_consensus_grandpa::GrandpaJustification<Block::Header>>
for GrandpaJustification<Block>
{
fn from(justification: sp_consensus_grandpa::GrandpaJustification<Block::Header>) -> Self {
Self { justification, _block: Default::default() }
}
}
impl<Block: BlockT> Into<sp_consensus_grandpa::GrandpaJustification<Block::Header>>
for GrandpaJustification<Block>
{
fn into(self) -> sp_consensus_grandpa::GrandpaJustification<Block::Header> {
self.justification
}
}
impl<Block: BlockT> GrandpaJustification<Block> {
pub fn from_commit<C>(
client: &Arc<C>,
round: u64,
commit: Commit<Block::Header>,
) -> Result<Self, Error>
where
C: HeaderBackend<Block>,
{
let mut votes_ancestries_hashes = HashSet::new();
let mut votes_ancestries = Vec::new();
let error = || {
let msg = "invalid precommits for target commit".to_string();
Err(Error::Client(ClientError::BadJustification(msg)))
};
let (base_hash, base_number) = match commit
.precommits
.iter()
.map(|signed| &signed.precommit)
.min_by_key(|precommit| precommit.target_number)
.map(|precommit| (precommit.target_hash, precommit.target_number))
{
None => return error(),
Some(base) => base,
};
for signed in commit.precommits.iter() {
let mut current_hash = signed.precommit.target_hash;
loop {
if current_hash == base_hash {
break
}
match client.header(current_hash)? {
Some(current_header) => {
if *current_header.number() <= base_number {
return error()
}
let parent_hash = *current_header.parent_hash();
if votes_ancestries_hashes.insert(current_hash) {
votes_ancestries.push(current_header);
}
current_hash = parent_hash;
},
_ => return error(),
}
}
}
Ok(sp_consensus_grandpa::GrandpaJustification { round, commit, votes_ancestries }.into())
}
pub fn decode_and_verify_finalizes(
encoded: &[u8],
finalized_target: (Block::Hash, NumberFor<Block>),
set_id: u64,
voters: &VoterSet<AuthorityId>,
) -> Result<Self, ClientError>
where
NumberFor<Block>: finality_grandpa::BlockNumberOps,
{
let justification = GrandpaJustification::<Block>::decode_all(&mut &*encoded)
.map_err(|_| ClientError::JustificationDecode)?;
if (
justification.justification.commit.target_hash,
justification.justification.commit.target_number,
) != finalized_target
{
let msg = "invalid commit target in grandpa justification".to_string();
Err(ClientError::BadJustification(msg))
} else {
justification.verify_with_voter_set(set_id, voters).map(|_| justification)
}
}
pub fn verify(&self, set_id: u64, authorities: &AuthorityList) -> Result<(), ClientError>
where
NumberFor<Block>: finality_grandpa::BlockNumberOps,
{
let voters = VoterSet::new(authorities.iter().cloned())
.ok_or(ClientError::Consensus(sp_consensus::Error::InvalidAuthoritiesSet))?;
self.verify_with_voter_set(set_id, &voters)
}
pub(crate) fn verify_with_voter_set(
&self,
set_id: u64,
voters: &VoterSet<AuthorityId>,
) -> Result<(), ClientError>
where
NumberFor<Block>: finality_grandpa::BlockNumberOps,
{
use finality_grandpa::Chain;
let ancestry_chain = AncestryChain::<Block>::new(&self.justification.votes_ancestries);
match finality_grandpa::validate_commit(&self.justification.commit, voters, &ancestry_chain)
{
Ok(ref result) if result.is_valid() => {},
_ => {
let msg = "invalid commit in grandpa justification".to_string();
return Err(ClientError::BadJustification(msg))
},
}
let base_hash = self
.justification
.commit
.precommits
.iter()
.map(|signed| &signed.precommit)
.min_by_key(|precommit| precommit.target_number)
.map(|precommit| precommit.target_hash)
.expect(
"can only fail if precommits is empty; \
commit has been validated above; \
valid commits must include precommits; \
qed.",
);
let mut buf = Vec::new();
let mut visited_hashes = HashSet::new();
for signed in self.justification.commit.precommits.iter() {
if !sp_consensus_grandpa::check_message_signature_with_buffer(
&finality_grandpa::Message::Precommit(signed.precommit.clone()),
&signed.id,
&signed.signature,
self.justification.round,
set_id,
&mut buf,
) {
return Err(ClientError::BadJustification(
"invalid signature for precommit in grandpa justification".to_string(),
))
}
if base_hash == signed.precommit.target_hash {
continue
}
match ancestry_chain.ancestry(base_hash, signed.precommit.target_hash) {
Ok(route) => {
visited_hashes.insert(signed.precommit.target_hash);
for hash in route {
visited_hashes.insert(hash);
}
},
_ =>
return Err(ClientError::BadJustification(
"invalid precommit ancestry proof in grandpa justification".to_string(),
)),
}
}
let ancestry_hashes: HashSet<_> = self
.justification
.votes_ancestries
.iter()
.map(|h: &Block::Header| h.hash())
.collect();
if visited_hashes != ancestry_hashes {
return Err(ClientError::BadJustification(
"invalid precommit ancestries in grandpa justification with unused headers"
.to_string(),
))
}
Ok(())
}
pub fn target(&self) -> (NumberFor<Block>, Block::Hash) {
(self.justification.commit.target_number, self.justification.commit.target_hash)
}
}
struct AncestryChain<Block: BlockT> {
ancestry: HashMap<Block::Hash, Block::Header>,
}
impl<Block: BlockT> AncestryChain<Block> {
fn new(ancestry: &[Block::Header]) -> AncestryChain<Block> {
let ancestry: HashMap<_, _> =
ancestry.iter().cloned().map(|h: Block::Header| (h.hash(), h)).collect();
AncestryChain { ancestry }
}
}
impl<Block: BlockT> finality_grandpa::Chain<Block::Hash, NumberFor<Block>> for AncestryChain<Block>
where
NumberFor<Block>: finality_grandpa::BlockNumberOps,
{
fn ancestry(
&self,
base: Block::Hash,
block: Block::Hash,
) -> Result<Vec<Block::Hash>, GrandpaError> {
let mut route = Vec::new();
let mut current_hash = block;
loop {
if current_hash == base {
break
}
match self.ancestry.get(¤t_hash) {
Some(current_header) => {
current_hash = *current_header.parent_hash();
route.push(current_hash);
},
_ => return Err(GrandpaError::NotDescendent),
}
}
route.pop(); Ok(route)
}
}