Skip to content

Commit ab4de94

Browse files
authored
Grandpa justifications: Avoid duplicate vote ancestries (#2634) (#2635)
* Grandpa justifications: Avoid duplicate vote ancestries
1 parent 465562a commit ab4de94

7 files changed

Lines changed: 114 additions & 16 deletions

File tree

primitives/header-chain/src/justification/verification/equivocation.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ impl<'a, Header: HeaderT> EquivocationsCollector<'a, Header> {
101101
}
102102

103103
impl<'a, Header: HeaderT> JustificationVerifier<Header> for EquivocationsCollector<'a, Header> {
104+
fn process_duplicate_votes_ancestries(
105+
&mut self,
106+
_duplicate_votes_ancestries: Vec<usize>,
107+
) -> Result<(), JustificationVerificationError> {
108+
Ok(())
109+
}
110+
104111
fn process_redundant_vote(
105112
&mut self,
106113
_precommit_idx: usize,

primitives/header-chain/src/justification/verification/mod.rs

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ use finality_grandpa::voter_set::VoterSet;
2727
use sp_consensus_grandpa::{AuthorityId, AuthoritySignature, SetId};
2828
use sp_runtime::{traits::Header as HeaderT, RuntimeDebug};
2929
use sp_std::{
30-
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
30+
collections::{
31+
btree_map::{
32+
BTreeMap,
33+
Entry::{Occupied, Vacant},
34+
},
35+
btree_set::BTreeSet,
36+
},
3137
prelude::*,
3238
};
3339

@@ -44,23 +50,40 @@ pub struct AncestryChain<Header: HeaderT> {
4450
/// We expect all forks in the ancestry chain to be descendants of base.
4551
base: HeaderId<Header::Hash, Header::Number>,
4652
/// Header hash => parent header hash mapping.
47-
pub parents: BTreeMap<Header::Hash, Header::Hash>,
53+
parents: BTreeMap<Header::Hash, Header::Hash>,
4854
/// Hashes of headers that were not visited by `ancestry()`.
49-
pub unvisited: BTreeSet<Header::Hash>,
55+
unvisited: BTreeSet<Header::Hash>,
5056
}
5157

5258
impl<Header: HeaderT> AncestryChain<Header> {
53-
/// Create new ancestry chain.
54-
pub fn new(justification: &GrandpaJustification<Header>) -> AncestryChain<Header> {
59+
/// Creates a new instance of `AncestryChain` starting from a `GrandpaJustification`.
60+
///
61+
/// Returns the `AncestryChain` and a `Vec` containing the `votes_ancestries` entries
62+
/// that were ignored when creating it, because they are duplicates.
63+
pub fn new(
64+
justification: &GrandpaJustification<Header>,
65+
) -> (AncestryChain<Header>, Vec<usize>) {
5566
let mut parents = BTreeMap::new();
5667
let mut unvisited = BTreeSet::new();
57-
for ancestor in &justification.votes_ancestries {
68+
let mut ignored_idxs = Vec::new();
69+
for (idx, ancestor) in justification.votes_ancestries.iter().enumerate() {
5870
let hash = ancestor.hash();
59-
let parent_hash = *ancestor.parent_hash();
60-
parents.insert(hash, parent_hash);
61-
unvisited.insert(hash);
71+
match parents.entry(hash) {
72+
Occupied(_) => {
73+
ignored_idxs.push(idx);
74+
},
75+
Vacant(entry) => {
76+
entry.insert(*ancestor.parent_hash());
77+
unvisited.insert(hash);
78+
},
79+
}
6280
}
63-
AncestryChain { base: justification.commit_target_id(), parents, unvisited }
81+
(AncestryChain { base: justification.commit_target_id(), parents, unvisited }, ignored_idxs)
82+
}
83+
84+
/// Returns the hash of a block's parent if the block is present in the ancestry.
85+
pub fn parent_hash_of(&self, hash: &Header::Hash) -> Option<&Header::Hash> {
86+
self.parents.get(hash)
6487
}
6588

6689
/// Returns a route if the precommit target block is a descendant of the `base` block.
@@ -80,7 +103,7 @@ impl<Header: HeaderT> AncestryChain<Header> {
80103
break
81104
}
82105

83-
current_hash = match self.parents.get(&current_hash) {
106+
current_hash = match self.parent_hash_of(&current_hash) {
84107
Some(parent_hash) => {
85108
let is_visited_before = self.unvisited.get(&current_hash).is_none();
86109
if is_visited_before {
@@ -117,6 +140,8 @@ pub enum Error {
117140
InvalidAuthorityList,
118141
/// Justification is finalizing unexpected header.
119142
InvalidJustificationTarget,
143+
/// The justification contains duplicate headers in its `votes_ancestries` field.
144+
DuplicateVotesAncestries,
120145
/// Error validating a precommit
121146
Precommit(PrecommitError),
122147
/// The cumulative weight of all votes in the justification is not enough to justify commit
@@ -168,6 +193,12 @@ enum IterationFlow {
168193

169194
/// Verification callbacks.
170195
trait JustificationVerifier<Header: HeaderT> {
196+
/// Called when there are duplicate headers in the votes ancestries.
197+
fn process_duplicate_votes_ancestries(
198+
&mut self,
199+
duplicate_votes_ancestries: Vec<usize>,
200+
) -> Result<(), Error>;
201+
171202
fn process_redundant_vote(
172203
&mut self,
173204
precommit_idx: usize,
@@ -216,10 +247,14 @@ trait JustificationVerifier<Header: HeaderT> {
216247
}
217248

218249
let threshold = context.voter_set.threshold().get();
219-
let mut chain = AncestryChain::new(justification);
250+
let (mut chain, ignored_idxs) = AncestryChain::new(justification);
220251
let mut signature_buffer = Vec::new();
221252
let mut cumulative_weight = 0u64;
222253

254+
if !ignored_idxs.is_empty() {
255+
self.process_duplicate_votes_ancestries(ignored_idxs)?;
256+
}
257+
223258
for (precommit_idx, signed) in justification.commit.precommits.iter().enumerate() {
224259
if cumulative_weight >= threshold {
225260
let action =

primitives/header-chain/src/justification/verification/optimizer.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct JustificationOptimizer<Header: HeaderT> {
3333
votes: BTreeSet<AuthorityId>,
3434

3535
extra_precommits: Vec<usize>,
36+
duplicate_votes_ancestries_idxs: Vec<usize>,
3637
redundant_votes_ancestries: BTreeSet<Header::Hash>,
3738
}
3839

@@ -41,6 +42,11 @@ impl<Header: HeaderT> JustificationOptimizer<Header> {
4142
for invalid_precommit_idx in self.extra_precommits.into_iter().rev() {
4243
justification.commit.precommits.remove(invalid_precommit_idx);
4344
}
45+
if !self.duplicate_votes_ancestries_idxs.is_empty() {
46+
for idx in self.duplicate_votes_ancestries_idxs.iter().rev() {
47+
justification.votes_ancestries.swap_remove(*idx);
48+
}
49+
}
4450
if !self.redundant_votes_ancestries.is_empty() {
4551
justification
4652
.votes_ancestries
@@ -50,6 +56,14 @@ impl<Header: HeaderT> JustificationOptimizer<Header> {
5056
}
5157

5258
impl<Header: HeaderT> JustificationVerifier<Header> for JustificationOptimizer<Header> {
59+
fn process_duplicate_votes_ancestries(
60+
&mut self,
61+
duplicate_votes_ancestries: Vec<usize>,
62+
) -> Result<(), Error> {
63+
self.duplicate_votes_ancestries_idxs = duplicate_votes_ancestries.to_vec();
64+
Ok(())
65+
}
66+
5367
fn process_redundant_vote(
5468
&mut self,
5569
precommit_idx: usize,
@@ -118,6 +132,7 @@ pub fn verify_and_optimize_justification<Header: HeaderT>(
118132
let mut optimizer = JustificationOptimizer {
119133
votes: BTreeSet::new(),
120134
extra_precommits: vec![],
135+
duplicate_votes_ancestries_idxs: vec![],
121136
redundant_votes_ancestries: Default::default(),
122137
};
123138
optimizer.verify_justification(finalized_target, context, justification)?;

primitives/header-chain/src/justification/verification/strict.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,21 @@ use crate::justification::verification::{
2626
};
2727
use sp_consensus_grandpa::AuthorityId;
2828
use sp_runtime::traits::Header as HeaderT;
29-
use sp_std::collections::btree_set::BTreeSet;
29+
use sp_std::{collections::btree_set::BTreeSet, vec::Vec};
3030

3131
/// Verification callbacks that reject all unknown, duplicate or redundant votes.
3232
struct StrictJustificationVerifier {
3333
votes: BTreeSet<AuthorityId>,
3434
}
3535

3636
impl<Header: HeaderT> JustificationVerifier<Header> for StrictJustificationVerifier {
37+
fn process_duplicate_votes_ancestries(
38+
&mut self,
39+
_duplicate_votes_ancestries: Vec<usize>,
40+
) -> Result<(), Error> {
41+
Err(Error::DuplicateVotesAncestries)
42+
}
43+
3744
fn process_redundant_vote(
3845
&mut self,
3946
_precommit_idx: usize,

primitives/header-chain/tests/implementation_match.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ struct AncestryChain(bp_header_chain::justification::AncestryChain<TestHeader>);
4242

4343
impl AncestryChain {
4444
fn new(justification: &GrandpaJustification<TestHeader>) -> Self {
45-
Self(bp_header_chain::justification::AncestryChain::new(justification))
45+
Self(bp_header_chain::justification::AncestryChain::new(justification).0)
4646
}
4747
}
4848

@@ -58,7 +58,7 @@ impl finality_grandpa::Chain<TestHash, TestNumber> for AncestryChain {
5858
if current_hash == base {
5959
break
6060
}
61-
match self.0.parents.get(&current_hash) {
61+
match self.0.parent_hash_of(&current_hash) {
6262
Some(parent_hash) => {
6363
current_hash = *parent_hash;
6464
route.push(current_hash);

primitives/header-chain/tests/justification/optimizer.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,26 @@ fn unrelated_ancestry_votes_are_removed_by_optimizer() {
158158
assert_eq!(num_precommits_before - 1, num_precommits_after);
159159
}
160160

161+
#[test]
162+
fn duplicate_votes_ancestries_are_removed_by_optimizer() {
163+
let mut justification = make_default_justification::<TestHeader>(&test_header(1));
164+
let optimized_votes_ancestries = justification.votes_ancestries.clone();
165+
justification.votes_ancestries = justification
166+
.votes_ancestries
167+
.into_iter()
168+
.flat_map(|item| std::iter::repeat(item).take(3))
169+
.collect();
170+
171+
verify_and_optimize_justification::<TestHeader>(
172+
header_id::<TestHeader>(1),
173+
&verification_context(TEST_GRANDPA_SET_ID),
174+
&mut justification,
175+
)
176+
.unwrap();
177+
178+
assert_eq!(justification.votes_ancestries, optimized_votes_ancestries);
179+
}
180+
161181
#[test]
162182
fn redundant_votes_ancestries_are_removed_by_optimizer() {
163183
let mut justification = make_default_justification::<TestHeader>(&test_header(1));

primitives/header-chain/tests/justification/strict.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,21 @@ fn justification_with_invalid_authority_signature_rejected() {
149149
}
150150

151151
#[test]
152-
fn justification_with_invalid_precommit_ancestry() {
152+
fn justification_with_duplicate_votes_ancestry() {
153+
let mut justification = make_default_justification::<TestHeader>(&test_header(1));
154+
justification.votes_ancestries.push(justification.votes_ancestries[0].clone());
155+
156+
assert_eq!(
157+
verify_justification::<TestHeader>(
158+
header_id::<TestHeader>(1),
159+
&verification_context(TEST_GRANDPA_SET_ID),
160+
&justification,
161+
),
162+
Err(JustificationVerificationError::DuplicateVotesAncestries),
163+
);
164+
}
165+
#[test]
166+
fn justification_with_redundant_votes_ancestry() {
153167
let mut justification = make_default_justification::<TestHeader>(&test_header(1));
154168
justification.votes_ancestries.push(test_header(10));
155169

0 commit comments

Comments
 (0)