Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Commit 94941a8

Browse files
Punish peers for duplicate GRANDPA neighbor messages (#12462)
* Decrease peer reputation for duplicate GRANDPA neighbor messages. * Fix comparison * Fix update_peer_state() validity condition * Add negative test * Rework update_peer_state() validity condition, add tests * update_peer_state() validity condition: invert comparison * Split InvalidViewChange and DuplicateNeighborMessage misbehaviors * Enforce rate-limiting of duplicate GRANDPA neighbor packets * Update client/finality-grandpa/src/communication/gossip.rs Co-authored-by: André Silva <[email protected]> * Make rolling clock back in a test safer Co-authored-by: André Silva <[email protected]>
1 parent b324e51 commit 94941a8

File tree

3 files changed

+124
-52
lines changed

3 files changed

+124
-52
lines changed

client/finality-grandpa/src/communication/gossip.rs

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
//! impolite to send messages about r+1 or later. "future-round" messages can
3636
//! be dropped and ignored.
3737
//!
38-
//! It is impolite to send a neighbor packet which moves backwards in protocol state.
38+
//! It is impolite to send a neighbor packet which moves backwards or does not progress
39+
//! protocol state.
3940
//!
4041
//! This is beneficial if it conveys some progress in the protocol state of the peer.
4142
//!
@@ -97,7 +98,7 @@ use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnbound
9798
use sp_finality_grandpa::AuthorityId;
9899
use sp_runtime::traits::{Block as BlockT, NumberFor, Zero};
99100

100-
use super::{benefit, cost, Round, SetId};
101+
use super::{benefit, cost, Round, SetId, NEIGHBOR_REBROADCAST_PERIOD};
101102
use crate::{environment, CatchUp, CompactCommit, SignedMessage};
102103

103104
use std::{
@@ -148,14 +149,15 @@ enum Consider {
148149
/// A view of protocol state.
149150
#[derive(Debug)]
150151
struct View<N> {
151-
round: Round, // the current round we are at.
152-
set_id: SetId, // the current voter set id.
153-
last_commit: Option<N>, // commit-finalized block height, if any.
152+
round: Round, // the current round we are at.
153+
set_id: SetId, // the current voter set id.
154+
last_commit: Option<N>, // commit-finalized block height, if any.
155+
last_update: Option<Instant>, // last time we heard from peer, used for spamming detection.
154156
}
155157

156158
impl<N> Default for View<N> {
157159
fn default() -> Self {
158-
View { round: Round(1), set_id: SetId(0), last_commit: None }
160+
View { round: Round(1), set_id: SetId(0), last_commit: None, last_update: None }
159161
}
160162
}
161163

@@ -225,7 +227,12 @@ impl<N> LocalView<N> {
225227
/// Converts the local view to a `View` discarding round and set id
226228
/// information about the last commit.
227229
fn as_view(&self) -> View<&N> {
228-
View { round: self.round, set_id: self.set_id, last_commit: self.last_commit_height() }
230+
View {
231+
round: self.round,
232+
set_id: self.set_id,
233+
last_commit: self.last_commit_height(),
234+
last_update: None,
235+
}
229236
}
230237

231238
/// Update the set ID. implies a reset to round 1.
@@ -417,6 +424,8 @@ pub(super) struct FullCatchUpMessage<Block: BlockT> {
417424
pub(super) enum Misbehavior {
418425
// invalid neighbor message, considering the last one.
419426
InvalidViewChange,
427+
// duplicate neighbor message.
428+
DuplicateNeighborMessage,
420429
// could not decode neighbor message. bytes-length of the packet.
421430
UndecodablePacket(i32),
422431
// Bad catch up message (invalid signatures).
@@ -438,6 +447,7 @@ impl Misbehavior {
438447

439448
match *self {
440449
InvalidViewChange => cost::INVALID_VIEW_CHANGE,
450+
DuplicateNeighborMessage => cost::DUPLICATE_NEIGHBOR_MESSAGE,
441451
UndecodablePacket(bytes) => ReputationChange::new(
442452
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
443453
"Grandpa: Bad packet",
@@ -488,20 +498,22 @@ struct Peers<N> {
488498
second_stage_peers: HashSet<PeerId>,
489499
/// The randomly picked set of `LUCKY_PEERS` light clients we'll gossip commit messages to.
490500
lucky_light_peers: HashSet<PeerId>,
501+
/// Neighbor packet rebroadcast period --- we reduce the reputation of peers sending duplicate
502+
/// packets too often.
503+
neighbor_rebroadcast_period: Duration,
491504
}
492505

493-
impl<N> Default for Peers<N> {
494-
fn default() -> Self {
506+
impl<N: Ord> Peers<N> {
507+
fn new(neighbor_rebroadcast_period: Duration) -> Self {
495508
Peers {
496509
inner: Default::default(),
497510
first_stage_peers: Default::default(),
498511
second_stage_peers: Default::default(),
499512
lucky_light_peers: Default::default(),
513+
neighbor_rebroadcast_period,
500514
}
501515
}
502-
}
503516

504-
impl<N: Ord> Peers<N> {
505517
fn new_peer(&mut self, who: PeerId, role: ObservedRole) {
506518
match role {
507519
ObservedRole::Authority if self.first_stage_peers.len() < LUCKY_PEERS => {
@@ -547,10 +559,23 @@ impl<N: Ord> Peers<N> {
547559
return Err(Misbehavior::InvalidViewChange)
548560
}
549561

562+
let now = Instant::now();
563+
let duplicate_packet = (update.set_id, update.round, Some(&update.commit_finalized_height)) ==
564+
(peer.view.set_id, peer.view.round, peer.view.last_commit.as_ref());
565+
566+
if duplicate_packet {
567+
if let Some(last_update) = peer.view.last_update {
568+
if now < last_update + self.neighbor_rebroadcast_period / 2 {
569+
return Err(Misbehavior::DuplicateNeighborMessage)
570+
}
571+
}
572+
}
573+
550574
peer.view = View {
551575
round: update.round,
552576
set_id: update.set_id,
553577
last_commit: Some(update.commit_finalized_height),
578+
last_update: Some(now),
554579
};
555580

556581
trace!(target: "afg", "Peer {} updated view. Now at {:?}, {:?}",
@@ -748,7 +773,7 @@ impl<Block: BlockT> Inner<Block> {
748773

749774
Inner {
750775
local_view: None,
751-
peers: Peers::default(),
776+
peers: Peers::new(NEIGHBOR_REBROADCAST_PERIOD),
752777
live_topics: KeepTopics::new(),
753778
next_rebroadcast: Instant::now() + REBROADCAST_AFTER,
754779
authorities: Vec::new(),
@@ -758,13 +783,16 @@ impl<Block: BlockT> Inner<Block> {
758783
}
759784
}
760785

761-
/// Note a round in the current set has started.
786+
/// Note a round in the current set has started. Does nothing if the last
787+
/// call to the function was with the same `round`.
762788
fn note_round(&mut self, round: Round) -> MaybeMessage<Block> {
763789
{
764790
let local_view = match self.local_view {
765791
None => return None,
766792
Some(ref mut v) =>
767793
if v.round == round {
794+
// Do not send neighbor packets out if `round` has not changed ---
795+
// such behavior is punishable.
768796
return None
769797
} else {
770798
v
@@ -803,6 +831,8 @@ impl<Block: BlockT> Inner<Block> {
803831
);
804832
self.authorities = authorities;
805833
}
834+
// Do not send neighbor packets out if the `set_id` has not changed ---
835+
// such behavior is punishable.
806836
return None
807837
} else {
808838
v
@@ -816,7 +846,9 @@ impl<Block: BlockT> Inner<Block> {
816846
self.multicast_neighbor_packet()
817847
}
818848

819-
/// Note that we've imported a commit finalizing a given block.
849+
/// Note that we've imported a commit finalizing a given block. Does nothing if the last
850+
/// call to the function was with the same or higher `finalized` number.
851+
/// `set_id` & `round` are the ones the commit message is from.
820852
fn note_commit_finalized(
821853
&mut self,
822854
round: Round,
@@ -1357,6 +1389,8 @@ impl<Block: BlockT> GossipValidator<Block> {
13571389
}
13581390

13591391
/// Note that we've imported a commit finalizing a given block.
1392+
/// `set_id` & `round` are the ones the commit message is from and not necessarily
1393+
/// the latest set ID & round started.
13601394
pub(super) fn note_commit_finalized<F>(
13611395
&self,
13621396
round: Round,
@@ -1647,11 +1681,12 @@ pub(super) struct PeerReport {
16471681

16481682
#[cfg(test)]
16491683
mod tests {
1650-
use super::{environment::SharedVoterSetState, *};
1684+
use super::{super::NEIGHBOR_REBROADCAST_PERIOD, environment::SharedVoterSetState, *};
16511685
use crate::communication;
16521686
use sc_network::config::Role;
16531687
use sc_network_gossip::Validator as GossipValidatorT;
16541688
use sp_core::{crypto::UncheckedFrom, H256};
1689+
use std::time::Instant;
16551690
use substrate_test_runtime_client::runtime::{Block, Header};
16561691

16571692
// some random config (not really needed)
@@ -1684,7 +1719,12 @@ mod tests {
16841719

16851720
#[test]
16861721
fn view_vote_rules() {
1687-
let view = View { round: Round(100), set_id: SetId(1), last_commit: Some(1000u64) };
1722+
let view = View {
1723+
round: Round(100),
1724+
set_id: SetId(1),
1725+
last_commit: Some(1000u64),
1726+
last_update: None,
1727+
};
16881728

16891729
assert_eq!(view.consider_vote(Round(98), SetId(1)), Consider::RejectPast);
16901730
assert_eq!(view.consider_vote(Round(1), SetId(0)), Consider::RejectPast);
@@ -1701,7 +1741,12 @@ mod tests {
17011741

17021742
#[test]
17031743
fn view_global_message_rules() {
1704-
let view = View { round: Round(100), set_id: SetId(2), last_commit: Some(1000u64) };
1744+
let view = View {
1745+
round: Round(100),
1746+
set_id: SetId(2),
1747+
last_commit: Some(1000u64),
1748+
last_update: None,
1749+
};
17051750

17061751
assert_eq!(view.consider_global(SetId(3), 1), Consider::RejectFuture);
17071752
assert_eq!(view.consider_global(SetId(3), 1000), Consider::RejectFuture);
@@ -1719,7 +1764,7 @@ mod tests {
17191764

17201765
#[test]
17211766
fn unknown_peer_cannot_be_updated() {
1722-
let mut peers = Peers::default();
1767+
let mut peers = Peers::new(NEIGHBOR_REBROADCAST_PERIOD);
17231768
let id = PeerId::random();
17241769

17251770
let update =
@@ -1750,27 +1795,35 @@ mod tests {
17501795
let update4 =
17511796
NeighborPacket { round: Round(3), set_id: SetId(11), commit_finalized_height: 80 };
17521797

1753-
let mut peers = Peers::default();
1798+
// Use shorter rebroadcast period to safely roll the clock back in the last test
1799+
// and don't hit the system boot time on systems with unsigned time.
1800+
const SHORT_NEIGHBOR_REBROADCAST_PERIOD: Duration = Duration::from_secs(1);
1801+
let mut peers = Peers::new(SHORT_NEIGHBOR_REBROADCAST_PERIOD);
17541802
let id = PeerId::random();
17551803

17561804
peers.new_peer(id, ObservedRole::Authority);
17571805

1758-
let mut check_update = move |update: NeighborPacket<_>| {
1806+
let check_update = |peers: &mut Peers<_>, update: NeighborPacket<_>| {
17591807
let view = peers.update_peer_state(&id, update.clone()).unwrap().unwrap();
17601808
assert_eq!(view.round, update.round);
17611809
assert_eq!(view.set_id, update.set_id);
17621810
assert_eq!(view.last_commit, Some(update.commit_finalized_height));
17631811
};
17641812

1765-
check_update(update1);
1766-
check_update(update2);
1767-
check_update(update3);
1768-
check_update(update4);
1813+
check_update(&mut peers, update1);
1814+
check_update(&mut peers, update2);
1815+
check_update(&mut peers, update3);
1816+
check_update(&mut peers, update4.clone());
1817+
1818+
// Allow duplicate neighbor packets if enough time has passed.
1819+
peers.inner.get_mut(&id).unwrap().view.last_update =
1820+
Some(Instant::now() - SHORT_NEIGHBOR_REBROADCAST_PERIOD);
1821+
check_update(&mut peers, update4);
17691822
}
17701823

17711824
#[test]
17721825
fn invalid_view_change() {
1773-
let mut peers = Peers::default();
1826+
let mut peers = Peers::new(NEIGHBOR_REBROADCAST_PERIOD);
17741827

17751828
let id = PeerId::random();
17761829
peers.new_peer(id, ObservedRole::Authority);
@@ -1783,29 +1836,41 @@ mod tests {
17831836
.unwrap()
17841837
.unwrap();
17851838

1786-
let mut check_update = move |update: NeighborPacket<_>| {
1839+
let mut check_update = move |update: NeighborPacket<_>, misbehavior| {
17871840
let err = peers.update_peer_state(&id, update.clone()).unwrap_err();
1788-
assert_eq!(err, Misbehavior::InvalidViewChange);
1841+
assert_eq!(err, misbehavior);
17891842
};
17901843

17911844
// round moves backwards.
1792-
check_update(NeighborPacket {
1793-
round: Round(9),
1794-
set_id: SetId(10),
1795-
commit_finalized_height: 10,
1796-
});
1797-
// commit finalized height moves backwards.
1798-
check_update(NeighborPacket {
1799-
round: Round(10),
1800-
set_id: SetId(10),
1801-
commit_finalized_height: 9,
1802-
});
1845+
check_update(
1846+
NeighborPacket { round: Round(9), set_id: SetId(10), commit_finalized_height: 10 },
1847+
Misbehavior::InvalidViewChange,
1848+
);
18031849
// set ID moves backwards.
1804-
check_update(NeighborPacket {
1805-
round: Round(10),
1806-
set_id: SetId(9),
1807-
commit_finalized_height: 10,
1808-
});
1850+
check_update(
1851+
NeighborPacket { round: Round(10), set_id: SetId(9), commit_finalized_height: 10 },
1852+
Misbehavior::InvalidViewChange,
1853+
);
1854+
// commit finalized height moves backwards.
1855+
check_update(
1856+
NeighborPacket { round: Round(10), set_id: SetId(10), commit_finalized_height: 9 },
1857+
Misbehavior::InvalidViewChange,
1858+
);
1859+
// duplicate packet without grace period.
1860+
check_update(
1861+
NeighborPacket { round: Round(10), set_id: SetId(10), commit_finalized_height: 10 },
1862+
Misbehavior::DuplicateNeighborMessage,
1863+
);
1864+
// commit finalized height moves backwards while round moves forward.
1865+
check_update(
1866+
NeighborPacket { round: Round(11), set_id: SetId(10), commit_finalized_height: 9 },
1867+
Misbehavior::InvalidViewChange,
1868+
);
1869+
// commit finalized height moves backwards while set ID moves forward.
1870+
check_update(
1871+
NeighborPacket { round: Round(10), set_id: SetId(11), commit_finalized_height: 9 },
1872+
Misbehavior::InvalidViewChange,
1873+
);
18091874
}
18101875

18111876
#[test]

client/finality-grandpa/src/communication/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use std::{
3737
pin::Pin,
3838
sync::Arc,
3939
task::{Context, Poll},
40+
time::Duration,
4041
};
4142

4243
use finality_grandpa::{
@@ -68,6 +69,9 @@ mod periodic;
6869
#[cfg(test)]
6970
pub(crate) mod tests;
7071

72+
// How often to rebroadcast neighbor packets, in cases where no new packets are created.
73+
pub(crate) const NEIGHBOR_REBROADCAST_PERIOD: Duration = Duration::from_secs(2 * 60);
74+
7175
pub mod grandpa_protocol_name {
7276
use sc_chain_spec::ChainSpec;
7377
use sc_network_common::protocol::ProtocolName;
@@ -103,6 +107,8 @@ mod cost {
103107
pub(super) const UNKNOWN_VOTER: Rep = Rep::new(-150, "Grandpa: Unknown voter");
104108

105109
pub(super) const INVALID_VIEW_CHANGE: Rep = Rep::new(-500, "Grandpa: Invalid view change");
110+
pub(super) const DUPLICATE_NEIGHBOR_MESSAGE: Rep =
111+
Rep::new(-500, "Grandpa: Duplicate neighbor message without grace period");
106112
pub(super) const PER_UNDECODABLE_BYTE: i32 = -5;
107113
pub(super) const PER_SIGNATURE_CHECKED: i32 = -25;
108114
pub(super) const PER_BLOCK_LOADED: i32 = -10;
@@ -279,7 +285,7 @@ impl<B: BlockT, N: Network<B>> NetworkBridge<B, N> {
279285
}
280286

281287
let (neighbor_packet_worker, neighbor_packet_sender) =
282-
periodic::NeighborPacketWorker::new();
288+
periodic::NeighborPacketWorker::new(NEIGHBOR_REBROADCAST_PERIOD);
283289

284290
NetworkBridge {
285291
service,

0 commit comments

Comments
 (0)