grandpa: fix creation of justification with equivocating precommits in commit (#11302)

* grandpa: fix creation of justification ancestry

we would reject commits that have precommits targeting blocks lower than the
commit target. when there is an equivocation (or if it the commit is not
minimal) it is possible to have such precommits and we should assume that they
are the round base.

* grandpa: bump to 0.16.0

* grandpa: add test for justification with equivocation

* grandpa: fix failing test
This commit is contained in:
André Silva
2022-06-14 11:41:11 +01:00
committed by GitHub
parent 525fc8ebc3
commit 06cf8ad076
9 changed files with 125 additions and 20 deletions
@@ -42,9 +42,9 @@ use crate::{AuthorityList, Commit, Error};
/// nodes, and are used by syncing nodes to prove authority set handoffs.
#[derive(Clone, Encode, Decode, PartialEq, Eq, Debug)]
pub struct GrandpaJustification<Block: BlockT> {
round: u64,
pub(crate) round: u64,
pub(crate) commit: Commit<Block>,
votes_ancestries: Vec<Block::Header>,
pub(crate) votes_ancestries: Vec<Block::Header>,
}
impl<Block: BlockT> GrandpaJustification<Block> {
@@ -66,16 +66,33 @@ impl<Block: BlockT> GrandpaJustification<Block> {
Err(Error::Client(ClientError::BadJustification(msg)))
};
// we pick the precommit for the lowest block as the base that
// should serve as the root block for populating ancestry (i.e.
// collect all headers from all precommit blocks to the base)
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.clone(), 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 == commit.target_hash {
if current_hash == base_hash {
break
}
match client.header(BlockId::Hash(current_hash))? {
Some(current_header) => {
if *current_header.number() <= commit.target_number {
// NOTE: this should never happen as we pick the lowest block
// as base and only traverse backwards from the other blocks
// in the commit. but better be safe to avoid an unbound loop.
if *current_header.number() <= base_number {
return error()
}
@@ -83,6 +100,7 @@ impl<Block: BlockT> GrandpaJustification<Block> {
if votes_ancestries_hashes.insert(current_hash) {
votes_ancestries.push(current_header);
}
current_hash = parent_hash;
},
_ => return error(),
@@ -142,13 +160,30 @@ impl<Block: BlockT> GrandpaJustification<Block> {
let ancestry_chain = AncestryChain::<Block>::new(&self.votes_ancestries);
match finality_grandpa::validate_commit(&self.commit, voters, &ancestry_chain) {
Ok(ref result) if result.ghost().is_some() => {},
Ok(ref result) if result.is_valid() => {},
_ => {
let msg = "invalid commit in grandpa justification".to_string();
return Err(ClientError::BadJustification(msg))
},
}
// we pick the precommit for the lowest block as the base that
// should serve as the root block for populating ancestry (i.e.
// collect all headers from all precommit blocks to the base)
let base_hash = self
.commit
.precommits
.iter()
.map(|signed| &signed.precommit)
.min_by_key(|precommit| precommit.target_number)
.map(|precommit| precommit.target_hash.clone())
.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.commit.precommits.iter() {
@@ -165,11 +200,11 @@ impl<Block: BlockT> GrandpaJustification<Block> {
))
}
if self.commit.target_hash == signed.precommit.target_hash {
if base_hash == signed.precommit.target_hash {
continue
}
match ancestry_chain.ancestry(self.commit.target_hash, signed.precommit.target_hash) {
match ancestry_chain.ancestry(base_hash, signed.precommit.target_hash) {
Ok(route) => {
// ancestry starts from parent hash but the precommit target hash has been
// visited