mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-14 02:12:33 +00:00
Activate peer scoring (#1284)
* Initial score structure * Peer manager update * Updates to dialing * Correct tests * Correct typos and remove unused function * Integrate scoring into the network crate * Clean warnings * Formatting * Shift core functionality into the behaviour * Temp commit * Shift disconnections into the behaviour * Temp commit * Update libp2p and gossipsub * Remove gossipsub lru cache * Correct merge conflicts * Modify handler and correct tests * Update enr network globals on socket update * Apply clippy lints * Add new prysm fingerprint * More clippy fixes
This commit is contained in:
@@ -131,6 +131,20 @@ fn client_from_agent_version(agent_version: &str) -> (ClientKind, String, String
|
||||
let unknown = String::from("unknown");
|
||||
(kind, unknown.clone(), unknown)
|
||||
}
|
||||
Some("Prysm") => {
|
||||
let kind = ClientKind::Prysm;
|
||||
let mut version = String::from("unknown");
|
||||
let mut os_version = version.clone();
|
||||
if agent_split.next().is_some() {
|
||||
if let Some(agent_version) = agent_split.next() {
|
||||
version = agent_version.into();
|
||||
if let Some(agent_os_version) = agent_split.next() {
|
||||
os_version = agent_os_version.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
(kind, version, os_version)
|
||||
}
|
||||
Some("nim-libp2p") => {
|
||||
let kind = ClientKind::Nimbus;
|
||||
let mut version = String::from("unknown");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pub use self::peerdb::*;
|
||||
use crate::discovery::{Discovery, DiscoveryEvent};
|
||||
use crate::rpc::{MetaData, Protocol, RPCError, RPCResponseErrorCode};
|
||||
use crate::rpc::{GoodbyeReason, MetaData, Protocol, RPCError, RPCResponseErrorCode};
|
||||
use crate::{error, metrics};
|
||||
use crate::{Enr, EnrExt, NetworkConfig, NetworkGlobals, PeerId};
|
||||
use futures::prelude::*;
|
||||
@@ -10,7 +10,7 @@ use futures::Stream;
|
||||
use hashset_delay::HashSetDelay;
|
||||
use libp2p::core::multiaddr::Protocol as MProtocol;
|
||||
use libp2p::identify::IdentifyInfo;
|
||||
use slog::{crit, debug, error};
|
||||
use slog::{crit, debug, error, warn};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
@@ -27,12 +27,11 @@ pub mod client;
|
||||
mod peer_info;
|
||||
mod peer_sync_status;
|
||||
mod peerdb;
|
||||
pub(crate) mod score;
|
||||
|
||||
pub use peer_info::{PeerConnectionStatus::*, PeerInfo};
|
||||
pub use peer_sync_status::{PeerSyncStatus, SyncInfo};
|
||||
/// The minimum reputation before a peer is disconnected.
|
||||
// Most likely this needs tweaking.
|
||||
const _MIN_REP_BEFORE_BAN: Rep = 10;
|
||||
use score::{PeerAction, ScoreState};
|
||||
/// The time in seconds between re-status's peers.
|
||||
const STATUS_INTERVAL: u64 = 300;
|
||||
/// The time in seconds between PING events. We do not send a ping if the other peer as PING'd us within
|
||||
@@ -63,44 +62,6 @@ pub struct PeerManager<TSpec: EthSpec> {
|
||||
log: slog::Logger,
|
||||
}
|
||||
|
||||
/// A collection of actions a peer can perform which will adjust its reputation.
|
||||
/// Each variant has an associated reputation change.
|
||||
// To easily assess the behaviour of reputation changes the number of variants should stay low, and
|
||||
// somewhat generic.
|
||||
pub enum PeerAction {
|
||||
/// We should not communicate more with this peer.
|
||||
/// This action will cause the peer to get banned.
|
||||
Fatal,
|
||||
/// An error occurred with this peer but it is not necessarily malicious.
|
||||
/// We have high tolerance for this actions: several occurrences are needed for a peer to get
|
||||
/// kicked.
|
||||
/// NOTE: ~15 occurrences will get the peer banned
|
||||
HighToleranceError,
|
||||
/// An error occurred with this peer but it is not necessarily malicious.
|
||||
/// We have high tolerance for this actions: several occurrences are needed for a peer to get
|
||||
/// kicked.
|
||||
/// NOTE: ~10 occurrences will get the peer banned
|
||||
MidToleranceError,
|
||||
/// This peer's action is not malicious but will not be tolerated. A few occurrences will cause
|
||||
/// the peer to get kicked.
|
||||
/// NOTE: ~5 occurrences will get the peer banned
|
||||
LowToleranceError,
|
||||
/// Received an expected message.
|
||||
_ValidMessage,
|
||||
}
|
||||
|
||||
impl PeerAction {
|
||||
fn rep_change(&self) -> RepChange {
|
||||
match self {
|
||||
PeerAction::Fatal => RepChange::worst(),
|
||||
PeerAction::LowToleranceError => RepChange::bad(60),
|
||||
PeerAction::MidToleranceError => RepChange::bad(25),
|
||||
PeerAction::HighToleranceError => RepChange::bad(15),
|
||||
PeerAction::_ValidMessage => RepChange::good(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The events that the `PeerManager` outputs (requests).
|
||||
pub enum PeerManagerEvent {
|
||||
/// Dial a PeerId.
|
||||
@@ -114,7 +75,7 @@ pub enum PeerManagerEvent {
|
||||
/// Request METADATA from a peer.
|
||||
MetaData(PeerId),
|
||||
/// The peer should be disconnected.
|
||||
DisconnectPeer(PeerId),
|
||||
DisconnectPeer(PeerId, GoodbyeReason),
|
||||
}
|
||||
|
||||
impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
@@ -147,6 +108,89 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
|
||||
/* Public accessible functions */
|
||||
|
||||
/// Attempts to connect to a peer.
|
||||
///
|
||||
/// Returns true if the peer was accepted into the database.
|
||||
pub fn dial_peer(&mut self, peer_id: &PeerId) -> bool {
|
||||
self.events.push(PeerManagerEvent::Dial(peer_id.clone()));
|
||||
self.connect_peer(peer_id, ConnectingType::Dialing)
|
||||
}
|
||||
|
||||
/// The application layer wants to disconnect from a peer for a particular reason.
|
||||
///
|
||||
/// All instant disconnections are fatal and we ban the associated peer.
|
||||
///
|
||||
/// This will send a goodbye and disconnect the peer if it is connected or dialing.
|
||||
pub fn goodbye_peer(&mut self, peer_id: &PeerId, reason: GoodbyeReason) {
|
||||
// get the peer info
|
||||
if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) {
|
||||
debug!(self.log, "Sending goodbye to peer"; "peer_id" => peer_id.to_string(), "reason" => reason.to_string(), "score" => info.score.to_string());
|
||||
// Goodbye's are fatal
|
||||
info.score.apply_peer_action(PeerAction::Fatal);
|
||||
if info.connection_status.is_connected_or_dialing() {
|
||||
self.events
|
||||
.push(PeerManagerEvent::DisconnectPeer(peer_id.clone(), reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports a peer for some action.
|
||||
///
|
||||
/// If the peer doesn't exist, log a warning and insert defaults.
|
||||
pub fn report_peer(&mut self, peer_id: &PeerId, action: PeerAction) {
|
||||
// TODO: Remove duplicate code - This is duplicated in the update_peer_scores()
|
||||
// function.
|
||||
|
||||
// Variables to update the PeerDb if required.
|
||||
let mut ban_peer = None;
|
||||
let mut unban_peer = None;
|
||||
|
||||
if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) {
|
||||
let previous_state = info.score.state();
|
||||
info.score.apply_peer_action(action);
|
||||
if previous_state != info.score.state() {
|
||||
match info.score.state() {
|
||||
ScoreState::Ban => {
|
||||
debug!(self.log, "Peer has been banned"; "peer_id" => peer_id.to_string(), "score" => info.score.to_string());
|
||||
ban_peer = Some(peer_id.clone());
|
||||
if info.connection_status.is_connected_or_dialing() {
|
||||
self.events.push(PeerManagerEvent::DisconnectPeer(
|
||||
peer_id.clone(),
|
||||
GoodbyeReason::BadScore,
|
||||
));
|
||||
}
|
||||
}
|
||||
ScoreState::Disconnect => {
|
||||
debug!(self.log, "Peer transitioned to disconnect state"; "peer_id" => peer_id.to_string(), "score" => info.score.to_string(), "past_state" => previous_state.to_string());
|
||||
// disconnect the peer if it's currently connected or dialing
|
||||
unban_peer = Some(peer_id.clone());
|
||||
if info.connection_status.is_connected_or_dialing() {
|
||||
self.events.push(PeerManagerEvent::DisconnectPeer(
|
||||
peer_id.clone(),
|
||||
GoodbyeReason::BadScore,
|
||||
));
|
||||
}
|
||||
// TODO: Update the peer manager to inform that the peer is disconnecting.
|
||||
}
|
||||
ScoreState::Healthy => {
|
||||
debug!(self.log, "Peer transitioned to healthy state"; "peer_id" => peer_id.to_string(), "score" => info.score.to_string(), "past_state" => previous_state.to_string());
|
||||
// unban the peer if it was previously banned.
|
||||
unban_peer = Some(peer_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the PeerDB state.
|
||||
if let Some(peer_id) = ban_peer.take() {
|
||||
self.network_globals.peers.write().ban(&peer_id);
|
||||
} else {
|
||||
if let Some(peer_id) = unban_peer.take() {
|
||||
self.network_globals.peers.write().unban(&peer_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Discovery Requests */
|
||||
|
||||
/// Provides a reference to the underlying discovery service.
|
||||
@@ -178,9 +222,12 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
self.status_peers.insert(peer_id.clone());
|
||||
}
|
||||
|
||||
/* Notifications from the Swarm */
|
||||
|
||||
/// Updates the state of the peer as disconnected.
|
||||
///
|
||||
/// This is also called when dialing a peer fails.
|
||||
pub fn notify_disconnect(&mut self, peer_id: &PeerId) {
|
||||
//self.update_reputations();
|
||||
self.network_globals.peers.write().disconnect(peer_id);
|
||||
|
||||
// remove the ping and status timer for the peer
|
||||
@@ -193,6 +240,17 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
);
|
||||
}
|
||||
|
||||
/// A dial attempt has failed.
|
||||
///
|
||||
/// NOTE: It can be the case that we are dialing a peer and during the dialing process the peer
|
||||
/// connects and the dial attempt later fails. To handle this, we only update the peer_db if
|
||||
/// the peer is not already connected.
|
||||
pub fn notify_dial_failure(&mut self, peer_id: &PeerId) {
|
||||
if !self.network_globals.peers.read().is_connected(peer_id) {
|
||||
self.notify_disconnect(peer_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a peer as connected as long as their reputation allows it
|
||||
/// Informs if the peer was accepted
|
||||
pub fn connect_ingoing(&mut self, peer_id: &PeerId) -> bool {
|
||||
@@ -205,28 +263,17 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
self.connect_peer(peer_id, ConnectingType::OutgoingConnected)
|
||||
}
|
||||
|
||||
/// Updates the database informing that a peer is being dialed.
|
||||
pub fn dialing_peer(&mut self, peer_id: &PeerId) -> bool {
|
||||
self.connect_peer(peer_id, ConnectingType::Dialing)
|
||||
}
|
||||
|
||||
/// Updates the database informing that a peer is being disconnected.
|
||||
pub fn _disconnecting_peer(&mut self, _peer_id: &PeerId) -> bool {
|
||||
// TODO: implement
|
||||
true
|
||||
}
|
||||
|
||||
/// Reports a peer for some action.
|
||||
/// Reports if a peer is banned or not.
|
||||
///
|
||||
/// If the peer doesn't exist, log a warning and insert defaults.
|
||||
pub fn report_peer(&mut self, peer_id: &PeerId, action: PeerAction) {
|
||||
//TODO: Check these. There are double disconnects for example
|
||||
// self.update_reputations();
|
||||
self.network_globals
|
||||
.peers
|
||||
.write()
|
||||
.add_reputation(peer_id, action.rep_change());
|
||||
// self.update_reputations();
|
||||
/// This is used to determine if we should accept incoming connections.
|
||||
pub fn is_banned(&self, peer_id: &PeerId) -> bool {
|
||||
self.network_globals.peers.read().is_banned(peer_id)
|
||||
}
|
||||
|
||||
/// Updates `PeerInfo` with `identify` information.
|
||||
@@ -239,9 +286,12 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An error has occured in the RPC.
|
||||
///
|
||||
/// This adjusts a peer's score based on the error.
|
||||
pub fn handle_rpc_error(&mut self, peer_id: &PeerId, protocol: Protocol, err: &RPCError) {
|
||||
let client = self.network_globals.client(peer_id);
|
||||
debug!(self.log, "RPCError"; "protocol" => protocol.to_string(), "err" => err.to_string(), "client" => client.to_string());
|
||||
warn!(self.log, "RPC Error"; "protocol" => protocol.to_string(), "err" => err.to_string(), "client" => client.to_string());
|
||||
|
||||
// Map this error to a `PeerAction` (if any)
|
||||
let peer_action = match err {
|
||||
@@ -423,6 +473,8 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
/// multiaddr here, however this could relate to duplicate PeerId's etc. If the lookup
|
||||
/// proves resource constraining, we should switch to multiaddr dialling here.
|
||||
fn peers_discovered(&mut self, peers: &[Enr], min_ttl: Option<Instant>) {
|
||||
let mut to_dial_peers = Vec::new();
|
||||
|
||||
for enr in peers {
|
||||
let peer_id = enr.peer_id();
|
||||
|
||||
@@ -433,9 +485,8 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
.peers
|
||||
.read()
|
||||
.is_connected_or_dialing(&peer_id)
|
||||
&& !self.network_globals.peers.read().peer_banned(&peer_id)
|
||||
&& !self.network_globals.peers.read().is_banned(&peer_id)
|
||||
{
|
||||
debug!(self.log, "Dialing discovered peer"; "peer_id"=> peer_id.to_string());
|
||||
// TODO: Update output
|
||||
// This should be updated with the peer dialing. In fact created once the peer is
|
||||
// dialed
|
||||
@@ -445,9 +496,13 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
.write()
|
||||
.update_min_ttl(&peer_id, min_ttl);
|
||||
}
|
||||
self.events.push(PeerManagerEvent::Dial(peer_id));
|
||||
to_dial_peers.push(peer_id);
|
||||
}
|
||||
}
|
||||
for peer_id in to_dial_peers {
|
||||
debug!(self.log, "Dialing discovered peer"; "peer_id"=> peer_id.to_string());
|
||||
self.dial_peer(&peer_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a peer as connected. The `ingoing` parameter determines if the peer is being
|
||||
@@ -465,9 +520,7 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
let mut peerdb = self.network_globals.peers.write();
|
||||
if peerdb.connection_status(peer_id).map(|c| c.is_banned()) == Some(true) {
|
||||
// don't connect if the peer is banned
|
||||
// TODO: Handle this case. If peer is banned this shouldn't be reached. It will put
|
||||
// our connection/disconnection out of sync with libp2p
|
||||
// return false;
|
||||
slog::crit!(self.log, "Connection has been allowed to a banned peer"; "peer_id" => peer_id.to_string());
|
||||
}
|
||||
|
||||
match connection {
|
||||
@@ -491,36 +544,22 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Notifies the peer manager that this peer is being dialed.
|
||||
pub fn _dialing_peer(&mut self, peer_id: &PeerId) {
|
||||
self.network_globals.peers.write().dialing_peer(peer_id);
|
||||
}
|
||||
|
||||
/// Updates the reputation of known peers according to their connection
|
||||
/// Updates the scores of known peers according to their connection
|
||||
/// status and the time that has passed.
|
||||
///
|
||||
/// **Disconnected peers** get a 1rep hit every hour they stay disconnected.
|
||||
/// **Banned peers** get a 1rep gain for every hour to slowly allow them back again.
|
||||
///
|
||||
/// A banned(disconnected) peer that gets its rep above(below) MIN_REP_BEFORE_BAN is
|
||||
/// now considered a disconnected(banned) peer.
|
||||
// TODO: Implement when reputation is added.
|
||||
fn _update_reputations(&mut self) {
|
||||
/*
|
||||
// avoid locking the peerdb too often
|
||||
// TODO: call this on a timer
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
// Check for peers that get banned, unbanned and that should be disconnected
|
||||
let mut ban_queue = Vec::new();
|
||||
let mut unban_queue = Vec::new();
|
||||
|
||||
/// NOTE: This is experimental and will likely be adjusted
|
||||
fn update_peer_scores(&mut self) {
|
||||
/* Check how long have peers been in this state and update their reputations if needed */
|
||||
let mut pdb = self.network_globals.peers.write();
|
||||
|
||||
for (id, info) in pdb._peers_mut() {
|
||||
// Update reputations
|
||||
let mut to_ban_peers = Vec::new();
|
||||
let mut to_unban_peers = Vec::new();
|
||||
|
||||
for (peer_id, info) in pdb.peers_mut() {
|
||||
let previous_state = info.score.state();
|
||||
// Update scores
|
||||
info.score.update();
|
||||
|
||||
/* TODO: Implement logic about connection lifetimes
|
||||
match info.connection_status {
|
||||
Connected { .. } => {
|
||||
// Connected peers gain reputation by sending useful messages
|
||||
@@ -570,21 +609,49 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
// This peer gets unbanned
|
||||
unban_queue.push(id.clone());
|
||||
}
|
||||
*/
|
||||
|
||||
// handle score transitions
|
||||
if previous_state != info.score.state() {
|
||||
match info.score.state() {
|
||||
ScoreState::Ban => {
|
||||
debug!(self.log, "Peer has been banned"; "peer_id" => peer_id.to_string(), "score" => info.score.to_string());
|
||||
to_ban_peers.push(peer_id.clone());
|
||||
if info.connection_status.is_connected_or_dialing() {
|
||||
self.events.push(PeerManagerEvent::DisconnectPeer(
|
||||
peer_id.clone(),
|
||||
GoodbyeReason::BadScore,
|
||||
));
|
||||
}
|
||||
}
|
||||
ScoreState::Disconnect => {
|
||||
debug!(self.log, "Peer transitioned to disconnect state"; "peer_id" => peer_id.to_string(), "score" => info.score.to_string(), "past_state" => previous_state.to_string());
|
||||
// disconnect the peer if it's currently connected or dialing
|
||||
to_unban_peers.push(peer_id.clone());
|
||||
if info.connection_status.is_connected_or_dialing() {
|
||||
self.events.push(PeerManagerEvent::DisconnectPeer(
|
||||
peer_id.clone(),
|
||||
GoodbyeReason::BadScore,
|
||||
));
|
||||
}
|
||||
// TODO: Update peer manager to report that it's disconnecting.
|
||||
}
|
||||
ScoreState::Healthy => {
|
||||
debug!(self.log, "Peer transitioned to healthy state"; "peer_id" => peer_id.to_string(), "score" => info.score.to_string(), "past_state" => previous_state.to_string());
|
||||
// unban the peer if it was previously banned.
|
||||
to_unban_peers.push(peer_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in ban_queue {
|
||||
pdb.ban(&id);
|
||||
|
||||
self.events
|
||||
.push(PeerManagerEvent::DisconnectPeer(id.clone()));
|
||||
// process banning peers
|
||||
for peer_id in to_ban_peers {
|
||||
pdb.ban(&peer_id);
|
||||
}
|
||||
|
||||
for id in unban_queue {
|
||||
pdb.disconnect(&id);
|
||||
// process unbanning peers
|
||||
for peer_id in to_unban_peers {
|
||||
pdb.unban(&peer_id);
|
||||
}
|
||||
|
||||
self._last_updated = Instant::now();
|
||||
*/
|
||||
}
|
||||
|
||||
/// The Peer manager's heartbeat maintains the peer count and maintains peer reputations.
|
||||
@@ -605,7 +672,8 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
|
||||
// TODO: If we have too many peers, remove peers that are not required for subnet
|
||||
// validation.
|
||||
|
||||
// TODO: Perform peer reputation maintenance here
|
||||
// Updates peer's scores.
|
||||
self.update_peer_scores();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +704,7 @@ impl<TSpec: EthSpec> Stream for PeerManager<TSpec> {
|
||||
self.events.push(PeerManagerEvent::Ping(peer_id));
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
error!(self.log, "Failed to check for peers to ping"; "error" => format!("{}",e))
|
||||
error!(self.log, "Failed to check for peers to ping"; "error" => e.to_string())
|
||||
}
|
||||
Poll::Ready(None) | Poll::Pending => break,
|
||||
}
|
||||
@@ -649,7 +717,7 @@ impl<TSpec: EthSpec> Stream for PeerManager<TSpec> {
|
||||
self.events.push(PeerManagerEvent::Status(peer_id))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
error!(self.log, "Failed to check for peers to ping"; "error" => format!("{}",e))
|
||||
error!(self.log, "Failed to check for peers to ping"; "error" => e.to_string())
|
||||
}
|
||||
Poll::Ready(None) | Poll::Pending => break,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::client::Client;
|
||||
use super::peerdb::{Rep, DEFAULT_REPUTATION};
|
||||
use super::score::Score;
|
||||
use super::PeerSyncStatus;
|
||||
use crate::rpc::MetaData;
|
||||
use crate::Multiaddr;
|
||||
@@ -18,7 +18,7 @@ pub struct PeerInfo<T: EthSpec> {
|
||||
/// The connection status of the peer
|
||||
_status: PeerStatus,
|
||||
/// The peers reputation
|
||||
pub reputation: Rep,
|
||||
pub score: Score,
|
||||
/// Client managing this peer
|
||||
pub client: Client,
|
||||
/// Connection status of this peer
|
||||
@@ -41,7 +41,7 @@ impl<TSpec: EthSpec> Default for PeerInfo<TSpec> {
|
||||
fn default() -> PeerInfo<TSpec> {
|
||||
PeerInfo {
|
||||
_status: Default::default(),
|
||||
reputation: DEFAULT_REPUTATION,
|
||||
score: Score::default(),
|
||||
client: Client::default(),
|
||||
connection_status: Default::default(),
|
||||
listening_addresses: vec![],
|
||||
@@ -146,7 +146,7 @@ impl Default for PeerConnectionStatus {
|
||||
}
|
||||
|
||||
impl PeerConnectionStatus {
|
||||
/// Checks if the status is connected
|
||||
/// Checks if the status is connected.
|
||||
pub fn is_connected(&self) -> bool {
|
||||
match self {
|
||||
PeerConnectionStatus::Connected { .. } => true,
|
||||
@@ -154,7 +154,7 @@ impl PeerConnectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the status is connected
|
||||
/// Checks if the status is connected.
|
||||
pub fn is_dialing(&self) -> bool {
|
||||
match self {
|
||||
PeerConnectionStatus::Dialing { .. } => true,
|
||||
@@ -162,7 +162,12 @@ impl PeerConnectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the status is banned
|
||||
/// The peer is either connected or in the process of being dialed.
|
||||
pub fn is_connected_or_dialing(&self) -> bool {
|
||||
self.is_connected() || self.is_dialing()
|
||||
}
|
||||
|
||||
/// Checks if the status is banned.
|
||||
pub fn is_banned(&self) -> bool {
|
||||
match self {
|
||||
PeerConnectionStatus::Banned { .. } => true,
|
||||
@@ -170,7 +175,7 @@ impl PeerConnectionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the status is disconnected
|
||||
/// Checks if the status is disconnected.
|
||||
pub fn is_disconnected(&self) -> bool {
|
||||
match self {
|
||||
Disconnected { .. } => true,
|
||||
@@ -214,6 +219,13 @@ impl PeerConnectionStatus {
|
||||
};
|
||||
}
|
||||
|
||||
/// The score system has unbanned the peer. Update the connection status
|
||||
pub fn unban(&mut self) {
|
||||
if let PeerConnectionStatus::Banned { since } = self {
|
||||
*self = PeerConnectionStatus::Disconnected { since: *since }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connections(&self) -> (u8, u8) {
|
||||
match self {
|
||||
Connected { n_in, n_out } => (*n_in, *n_out),
|
||||
|
||||
@@ -1,74 +1,47 @@
|
||||
use super::peer_info::{PeerConnectionStatus, PeerInfo};
|
||||
use super::peer_sync_status::PeerSyncStatus;
|
||||
use super::score::Score;
|
||||
use crate::rpc::methods::MetaData;
|
||||
use crate::PeerId;
|
||||
use slog::{crit, debug, trace, warn};
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use types::{EthSpec, SubnetId};
|
||||
|
||||
/// A peer's reputation (perceived potential usefulness)
|
||||
pub type Rep = u8;
|
||||
|
||||
/// Reputation change (positive or negative)
|
||||
pub struct RepChange {
|
||||
is_good: bool,
|
||||
diff: Rep,
|
||||
}
|
||||
|
||||
/// Max number of disconnected nodes to remember
|
||||
const MAX_DC_PEERS: usize = 30;
|
||||
|
||||
/// The default starting reputation for an unknown peer.
|
||||
pub const DEFAULT_REPUTATION: Rep = 50;
|
||||
/// Max number of disconnected nodes to remember.
|
||||
const MAX_DC_PEERS: usize = 100;
|
||||
/// The maximum number of banned nodes to remember.
|
||||
const MAX_BANNED_PEERS: usize = 300;
|
||||
|
||||
/// Storage of known peers, their reputation and information
|
||||
pub struct PeerDB<TSpec: EthSpec> {
|
||||
/// The collection of known connected peers, their status and reputation
|
||||
peers: HashMap<PeerId, PeerInfo<TSpec>>,
|
||||
/// Tracking of number of disconnected nodes
|
||||
n_dc: usize,
|
||||
/// The number of disconnected nodes in the database.
|
||||
disconnected_peers: usize,
|
||||
/// The number of banned peers in the database.
|
||||
banned_peers: usize,
|
||||
/// PeerDB's logger
|
||||
log: slog::Logger,
|
||||
}
|
||||
|
||||
impl RepChange {
|
||||
pub fn good(diff: Rep) -> Self {
|
||||
RepChange {
|
||||
is_good: true,
|
||||
diff,
|
||||
}
|
||||
}
|
||||
pub fn bad(diff: Rep) -> Self {
|
||||
RepChange {
|
||||
is_good: false,
|
||||
diff,
|
||||
}
|
||||
}
|
||||
pub const fn worst() -> Self {
|
||||
RepChange {
|
||||
is_good: false,
|
||||
diff: Rep::max_value(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
pub fn new(log: &slog::Logger) -> Self {
|
||||
Self {
|
||||
log: log.clone(),
|
||||
n_dc: 0,
|
||||
disconnected_peers: 0,
|
||||
banned_peers: 0,
|
||||
peers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/* Getters */
|
||||
|
||||
/// Gives the reputation of a peer, or DEFAULT_REPUTATION if it is unknown.
|
||||
pub fn reputation(&self, peer_id: &PeerId) -> Rep {
|
||||
/// Gives the score of a peer, or default score if it is unknown.
|
||||
pub fn score(&self, peer_id: &PeerId) -> Score {
|
||||
self.peers
|
||||
.get(peer_id)
|
||||
.map_or(DEFAULT_REPUTATION, |info| info.reputation)
|
||||
.map_or(Score::default(), |info| info.score)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all peers in the db.
|
||||
@@ -77,7 +50,7 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
}
|
||||
|
||||
/// Returns an iterator over all peers in the db.
|
||||
pub(super) fn _peers_mut(&mut self) -> impl Iterator<Item = (&PeerId, &mut PeerInfo<TSpec>)> {
|
||||
pub(super) fn peers_mut(&mut self) -> impl Iterator<Item = (&PeerId, &mut PeerInfo<TSpec>)> {
|
||||
self.peers.iter_mut()
|
||||
}
|
||||
|
||||
@@ -97,8 +70,25 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
self.peers.get_mut(peer_id)
|
||||
}
|
||||
|
||||
/// Returns if the peer is already connected.
|
||||
pub fn is_connected(&self, peer_id: &PeerId) -> bool {
|
||||
if let Some(PeerConnectionStatus::Connected { .. }) = self.connection_status(peer_id) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// If we are connected or currently dialing the peer returns true.
|
||||
pub fn is_connected_or_dialing(&self, peer_id: &PeerId) -> bool {
|
||||
match self.connection_status(peer_id) {
|
||||
Some(PeerConnectionStatus::Connected { .. })
|
||||
| Some(PeerConnectionStatus::Dialing { .. }) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// Returns true if the peer is synced at least to our current head.
|
||||
pub fn peer_synced(&self, peer_id: &PeerId) -> bool {
|
||||
pub fn is_synced(&self, peer_id: &PeerId) -> bool {
|
||||
match self.peers.get(peer_id).map(|info| &info.sync_status) {
|
||||
Some(PeerSyncStatus::Synced { .. }) => true,
|
||||
Some(_) => false,
|
||||
@@ -107,7 +97,7 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
}
|
||||
|
||||
/// Returns true if the Peer is banned.
|
||||
pub fn peer_banned(&self, peer_id: &PeerId) -> bool {
|
||||
pub fn is_banned(&self, peer_id: &PeerId) -> bool {
|
||||
match self.peers.get(peer_id).map(|info| &info.connection_status) {
|
||||
Some(status) => status.is_banned(),
|
||||
None => false,
|
||||
@@ -179,7 +169,7 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
}
|
||||
|
||||
/// Returns a vector containing peers (their ids and info), sorted by
|
||||
/// reputation from highest to lowest, and filtered using `is_status`
|
||||
/// score from highest to lowest, and filtered using `is_status`
|
||||
pub fn best_peers_by_status<F>(&self, is_status: F) -> Vec<(&PeerId, &PeerInfo<TSpec>)>
|
||||
where
|
||||
F: Fn(&PeerConnectionStatus) -> bool,
|
||||
@@ -189,8 +179,8 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
.iter()
|
||||
.filter(|(_, info)| is_status(&info.connection_status))
|
||||
.collect::<Vec<_>>();
|
||||
by_status.sort_by_key(|(_, info)| Rep::max_value() - info.reputation);
|
||||
by_status
|
||||
by_status.sort_by_key(|(_, info)| info.score);
|
||||
by_status.into_iter().rev().collect()
|
||||
}
|
||||
|
||||
/// Returns the peer with highest reputation that satisfies `is_status`
|
||||
@@ -201,7 +191,7 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
self.peers
|
||||
.iter()
|
||||
.filter(|(_, info)| is_status(&info.connection_status))
|
||||
.max_by_key(|(_, info)| info.reputation)
|
||||
.max_by_key(|(_, info)| info.score)
|
||||
.map(|(id, _)| id)
|
||||
}
|
||||
|
||||
@@ -211,24 +201,6 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
.map(|info| info.connection_status.clone())
|
||||
}
|
||||
|
||||
/// Returns if the peer is already connected.
|
||||
pub fn is_connected(&self, peer_id: &PeerId) -> bool {
|
||||
if let Some(PeerConnectionStatus::Connected { .. }) = self.connection_status(peer_id) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// If we are connected or currently dialing the peer returns true.
|
||||
pub fn is_connected_or_dialing(&self, peer_id: &PeerId) -> bool {
|
||||
match self.connection_status(peer_id) {
|
||||
Some(PeerConnectionStatus::Connected { .. })
|
||||
| Some(PeerConnectionStatus::Dialing { .. }) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/* Setters */
|
||||
|
||||
/// A peer is being dialed.
|
||||
@@ -236,7 +208,10 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
let info = self.peers.entry(peer_id.clone()).or_default();
|
||||
|
||||
if info.connection_status.is_disconnected() {
|
||||
self.n_dc = self.n_dc.saturating_sub(1);
|
||||
self.disconnected_peers = self.disconnected_peers.saturating_sub(1);
|
||||
}
|
||||
if info.connection_status.is_banned() {
|
||||
self.banned_peers = self.banned_peers.saturating_sub(1);
|
||||
}
|
||||
info.connection_status = PeerConnectionStatus::Dialing {
|
||||
since: Instant::now(),
|
||||
@@ -284,7 +259,10 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
let info = self.peers.entry(peer_id.clone()).or_default();
|
||||
|
||||
if info.connection_status.is_disconnected() {
|
||||
self.n_dc = self.n_dc.saturating_sub(1);
|
||||
self.disconnected_peers = self.disconnected_peers.saturating_sub(1);
|
||||
}
|
||||
if info.connection_status.is_banned() {
|
||||
self.banned_peers = self.banned_peers.saturating_sub(1);
|
||||
}
|
||||
info.connection_status.connect_ingoing();
|
||||
}
|
||||
@@ -294,7 +272,10 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
let info = self.peers.entry(peer_id.clone()).or_default();
|
||||
|
||||
if info.connection_status.is_disconnected() {
|
||||
self.n_dc = self.n_dc.saturating_sub(1);
|
||||
self.disconnected_peers = self.disconnected_peers.saturating_sub(1);
|
||||
}
|
||||
if info.connection_status.is_banned() {
|
||||
self.banned_peers = self.banned_peers.saturating_sub(1);
|
||||
}
|
||||
info.connection_status.connect_outgoing();
|
||||
}
|
||||
@@ -309,40 +290,93 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
});
|
||||
if !info.connection_status.is_disconnected() && !info.connection_status.is_banned() {
|
||||
info.connection_status.disconnect();
|
||||
self.n_dc += 1;
|
||||
self.disconnected_peers += 1;
|
||||
}
|
||||
self.shrink_to_fit();
|
||||
}
|
||||
|
||||
/// Drops the peers with the lowest reputation so that the number of
|
||||
/// disconnected peers is less than MAX_DC_PEERS
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
// for caution, but the difference should never be > 1
|
||||
while self.n_dc > MAX_DC_PEERS {
|
||||
let to_drop = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(_, info)| info.connection_status.is_disconnected())
|
||||
.min_by_key(|(_, info)| info.reputation)
|
||||
.map(|(id, _)| id.clone())
|
||||
.unwrap(); // should be safe since n_dc > MAX_DC_PEERS > 0
|
||||
self.peers.remove(&to_drop);
|
||||
self.n_dc = self.n_dc.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a peer as banned
|
||||
/// Marks a peer as banned.
|
||||
pub fn ban(&mut self, peer_id: &PeerId) {
|
||||
let log_ref = &self.log;
|
||||
let info = self.peers.entry(peer_id.clone()).or_insert_with(|| {
|
||||
warn!(log_ref, "Banning unknown peer";
|
||||
"peer_id" => peer_id.to_string());
|
||||
"peer_id" => peer_id.to_string());
|
||||
PeerInfo::default()
|
||||
});
|
||||
|
||||
if info.connection_status.is_disconnected() {
|
||||
self.n_dc = self.n_dc.saturating_sub(1);
|
||||
self.disconnected_peers = self.disconnected_peers.saturating_sub(1);
|
||||
}
|
||||
if !info.connection_status.is_banned() {
|
||||
info.connection_status.ban();
|
||||
self.banned_peers += 1;
|
||||
}
|
||||
self.shrink_to_fit();
|
||||
}
|
||||
|
||||
/// Unbans a peer.
|
||||
pub fn unban(&mut self, peer_id: &PeerId) {
|
||||
let log_ref = &self.log;
|
||||
let info = self.peers.entry(peer_id.clone()).or_insert_with(|| {
|
||||
warn!(log_ref, "UnBanning unknown peer";
|
||||
"peer_id" => peer_id.to_string());
|
||||
PeerInfo::default()
|
||||
});
|
||||
|
||||
if info.connection_status.is_banned() {
|
||||
info.connection_status.unban();
|
||||
self.banned_peers = self.banned_peers.saturating_sub(1);
|
||||
}
|
||||
self.shrink_to_fit();
|
||||
}
|
||||
|
||||
/// Removes banned and disconnected peers from the DB if we have reached any of our limits.
|
||||
/// Drops the peers with the lowest reputation so that the number of
|
||||
/// disconnected peers is less than MAX_DC_PEERS
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
// Remove excess baned peers
|
||||
while self.banned_peers > MAX_BANNED_PEERS {
|
||||
if let Some(to_drop) = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(_, info)| info.connection_status.is_banned())
|
||||
.min_by(|(_, info_a), (_, info_b)| {
|
||||
info_a
|
||||
.score
|
||||
.partial_cmp(&info_b.score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.map(|(id, _)| id.clone())
|
||||
{
|
||||
debug!(self.log, "Removing old banned peer"; "peer_id" => to_drop.to_string());
|
||||
self.peers.remove(&to_drop);
|
||||
}
|
||||
// If there is no minimum, this is a coding error. For safety we decrease
|
||||
// the count to avoid a potential infinite loop.
|
||||
self.banned_peers = self.banned_peers.saturating_sub(1);
|
||||
}
|
||||
|
||||
// Remove excess disconnected peers
|
||||
while self.disconnected_peers > MAX_DC_PEERS {
|
||||
if let Some(to_drop) = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter(|(_, info)| info.connection_status.is_disconnected())
|
||||
.min_by(|(_, info_a), (_, info_b)| {
|
||||
info_a
|
||||
.score
|
||||
.partial_cmp(&info_b.score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.map(|(id, _)| id.clone())
|
||||
{
|
||||
debug!(self.log, "Removing old disconnected peer"; "peer_id" => to_drop.to_string());
|
||||
self.peers.remove(&to_drop);
|
||||
}
|
||||
// If there is no minimum, this is a coding error. For safety we decrease
|
||||
// the count to avoid a potential infinite loop.
|
||||
self.disconnected_peers = self.disconnected_peers.saturating_sub(1);
|
||||
}
|
||||
info.connection_status.ban();
|
||||
}
|
||||
|
||||
/// Add the meta data of a peer.
|
||||
@@ -354,16 +388,6 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the reputation of peer.
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn set_reputation(&mut self, peer_id: &PeerId, rep: Rep) {
|
||||
if let Some(peer_info) = self.peers.get_mut(peer_id) {
|
||||
peer_info.reputation = rep;
|
||||
} else {
|
||||
crit!(self.log, "Tried to modify reputation for an unknown peer"; "peer_id" => peer_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the syncing status of a peer.
|
||||
pub fn set_sync_status(&mut self, peer_id: &PeerId, sync_status: PeerSyncStatus) {
|
||||
if let Some(peer_info) = self.peers.get_mut(peer_id) {
|
||||
@@ -372,26 +396,6 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
|
||||
crit!(self.log, "Tried to the sync status for an unknown peer"; "peer_id" => peer_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds to a peer's reputation by `change`. If the reputation exceeds Rep's
|
||||
/// upper (lower) bounds, it stays at the maximum (minimum) value.
|
||||
pub(super) fn add_reputation(&mut self, peer_id: &PeerId, change: RepChange) {
|
||||
let log_ref = &self.log;
|
||||
let info = match self.peers.entry(peer_id.clone()) {
|
||||
Entry::Vacant(_) => {
|
||||
warn!(log_ref, "Peer is unknown, no reputation change made";
|
||||
"peer_id" => peer_id.to_string());
|
||||
return;
|
||||
}
|
||||
Entry::Occupied(e) => e.into_mut(),
|
||||
};
|
||||
|
||||
info.reputation = if change.is_good {
|
||||
info.reputation.saturating_add(change.diff)
|
||||
} else {
|
||||
info.reputation.saturating_sub(change.diff)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -413,6 +417,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_score<TSpec: EthSpec>(db: &mut PeerDB<TSpec>, peer_id: &PeerId, score: f64) {
|
||||
if let Some(info) = db.peer_info_mut(peer_id) {
|
||||
info.score.add(score);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_db() -> PeerDB<M> {
|
||||
let log = build_log(slog::Level::Debug, false);
|
||||
PeerDB::new(&log)
|
||||
@@ -437,9 +447,9 @@ mod tests {
|
||||
// this is the only peer
|
||||
assert_eq!(pdb.peers().count(), 1);
|
||||
// the peer has the default reputation
|
||||
assert_eq!(pdb.reputation(&random_peer), DEFAULT_REPUTATION);
|
||||
assert_eq!(pdb.score(&random_peer).score(), Score::default().score());
|
||||
// it should be connected, and therefore not counted as disconnected
|
||||
assert_eq!(pdb.n_dc, 0);
|
||||
assert_eq!(pdb.disconnected_peers, 0);
|
||||
assert!(peer_info.unwrap().connection_status.is_connected());
|
||||
assert_eq!(
|
||||
peer_info.unwrap().connection_status.connections(),
|
||||
@@ -447,50 +457,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_reputation() {
|
||||
let mut pdb = get_db();
|
||||
let random_peer = PeerId::random();
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
|
||||
let mut rep = Rep::min_value();
|
||||
pdb.set_reputation(&random_peer, rep);
|
||||
assert_eq!(pdb.reputation(&random_peer), rep);
|
||||
|
||||
rep = Rep::max_value();
|
||||
pdb.set_reputation(&random_peer, rep);
|
||||
assert_eq!(pdb.reputation(&random_peer), rep);
|
||||
|
||||
rep = Rep::max_value() / 100;
|
||||
pdb.set_reputation(&random_peer, rep);
|
||||
assert_eq!(pdb.reputation(&random_peer), rep);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reputation_change() {
|
||||
let mut pdb = get_db();
|
||||
|
||||
// 0 change does not change de reputation
|
||||
let random_peer = PeerId::random();
|
||||
let change = RepChange::good(0);
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
pdb.add_reputation(&random_peer, change);
|
||||
assert_eq!(pdb.reputation(&random_peer), DEFAULT_REPUTATION);
|
||||
|
||||
// overflowing change is capped
|
||||
let random_peer = PeerId::random();
|
||||
let change = RepChange::worst();
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
pdb.add_reputation(&random_peer, change);
|
||||
assert_eq!(pdb.reputation(&random_peer), Rep::min_value());
|
||||
|
||||
let random_peer = PeerId::random();
|
||||
let change = RepChange::good(Rep::max_value());
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
pdb.add_reputation(&random_peer, change);
|
||||
assert_eq!(pdb.reputation(&random_peer), Rep::max_value());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_are_bounded() {
|
||||
let mut pdb = get_db();
|
||||
@@ -499,13 +465,30 @@ mod tests {
|
||||
let p = PeerId::random();
|
||||
pdb.connect_ingoing(&p);
|
||||
}
|
||||
assert_eq!(pdb.n_dc, 0);
|
||||
assert_eq!(pdb.disconnected_peers, 0);
|
||||
|
||||
for p in pdb.connected_peer_ids().cloned().collect::<Vec<_>>() {
|
||||
pdb.disconnect(&p);
|
||||
}
|
||||
|
||||
assert_eq!(pdb.n_dc, MAX_DC_PEERS);
|
||||
assert_eq!(pdb.disconnected_peers, MAX_DC_PEERS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_banned_are_bounded() {
|
||||
let mut pdb = get_db();
|
||||
|
||||
for _ in 0..MAX_BANNED_PEERS + 1 {
|
||||
let p = PeerId::random();
|
||||
pdb.connect_ingoing(&p);
|
||||
}
|
||||
assert_eq!(pdb.banned_peers, 0);
|
||||
|
||||
for p in pdb.connected_peer_ids().cloned().collect::<Vec<_>>() {
|
||||
pdb.ban(&p);
|
||||
}
|
||||
|
||||
assert_eq!(pdb.banned_peers, MAX_BANNED_PEERS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -518,14 +501,16 @@ mod tests {
|
||||
pdb.connect_ingoing(&p0);
|
||||
pdb.connect_ingoing(&p1);
|
||||
pdb.connect_ingoing(&p2);
|
||||
pdb.set_reputation(&p0, 70);
|
||||
pdb.set_reputation(&p1, 100);
|
||||
pdb.set_reputation(&p2, 50);
|
||||
add_score(&mut pdb, &p0, 70.0);
|
||||
add_score(&mut pdb, &p1, 100.0);
|
||||
add_score(&mut pdb, &p2, 50.0);
|
||||
|
||||
let best_peers = pdb.best_peers_by_status(PeerConnectionStatus::is_connected);
|
||||
assert!(vec![&p1, &p0, &p2]
|
||||
.into_iter()
|
||||
.eq(best_peers.into_iter().map(|p| p.0)));
|
||||
let best_peers: Vec<&PeerId> = pdb
|
||||
.best_peers_by_status(PeerConnectionStatus::is_connected)
|
||||
.iter()
|
||||
.map(|p| p.0)
|
||||
.collect();
|
||||
assert_eq!(vec![&p1, &p0, &p2], best_peers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -538,15 +523,15 @@ mod tests {
|
||||
pdb.connect_ingoing(&p0);
|
||||
pdb.connect_ingoing(&p1);
|
||||
pdb.connect_ingoing(&p2);
|
||||
pdb.set_reputation(&p0, 70);
|
||||
pdb.set_reputation(&p1, 100);
|
||||
pdb.set_reputation(&p2, 50);
|
||||
add_score(&mut pdb, &p0, 70.0);
|
||||
add_score(&mut pdb, &p1, 100.0);
|
||||
add_score(&mut pdb, &p2, 50.0);
|
||||
|
||||
let the_best = pdb.best_by_status(PeerConnectionStatus::is_connected);
|
||||
assert!(the_best.is_some());
|
||||
// Consistency check
|
||||
let best_peers = pdb.best_peers_by_status(PeerConnectionStatus::is_connected);
|
||||
assert_eq!(the_best, best_peers.into_iter().map(|p| p.0).next());
|
||||
assert_eq!(the_best, best_peers.iter().next().map(|p| p.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -556,26 +541,86 @@ mod tests {
|
||||
let random_peer = PeerId::random();
|
||||
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
|
||||
pdb.connect_outgoing(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
|
||||
pdb.ban(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.n_dc, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
dbg!("1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_ban_consistency() {
|
||||
let mut pdb = get_db();
|
||||
|
||||
let random_peer = PeerId::random();
|
||||
let random_peer1 = PeerId::random();
|
||||
let random_peer2 = PeerId::random();
|
||||
let random_peer3 = PeerId::random();
|
||||
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
pdb.connect_ingoing(&random_peer1);
|
||||
pdb.connect_ingoing(&random_peer2);
|
||||
pdb.connect_ingoing(&random_peer3);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
pdb.disconnect(&random_peer1);
|
||||
pdb.ban(&random_peer2);
|
||||
pdb.connect_ingoing(&random_peer3);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
pdb.ban(&random_peer1);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
|
||||
pdb.connect_outgoing(&random_peer2);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
pdb.ban(&random_peer3);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
|
||||
pdb.ban(&random_peer3);
|
||||
pdb.connect_ingoing(&random_peer1);
|
||||
pdb.disconnect(&random_peer2);
|
||||
pdb.ban(&random_peer3);
|
||||
pdb.connect_ingoing(&random_peer);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
|
||||
pdb.disconnect(&random_peer);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
assert_eq!(pdb.banned_peers, pdb.banned_peers().count());
|
||||
pdb.ban(&random_peer);
|
||||
assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count());
|
||||
}
|
||||
}
|
||||
|
||||
251
beacon_node/eth2_libp2p/src/peer_manager/score.rs
Normal file
251
beacon_node/eth2_libp2p/src/peer_manager/score.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
//! This contains the scoring logic for peers.
|
||||
//!
|
||||
//! A peer's score is a rational number in the range [-100, 100].
|
||||
//!
|
||||
//! As the logic develops this documentation will advance.
|
||||
//!
|
||||
//! The scoring algorithms are currently experimental.
|
||||
use serde::Serialize;
|
||||
use std::time::Instant;
|
||||
|
||||
lazy_static! {
|
||||
static ref HALFLIFE_DECAY: f64 = -2.0f64.ln() / SCORE_HALFLIFE;
|
||||
}
|
||||
|
||||
/// The default score for new peers.
|
||||
pub(crate) const DEFAULT_SCORE: f64 = 0.0;
|
||||
/// The minimum reputation before a peer is disconnected.
|
||||
const MIN_SCORE_BEFORE_DISCONNECT: f64 = -20.0;
|
||||
/// The minimum reputation before a peer is banned.
|
||||
const MIN_SCORE_BEFORE_BAN: f64 = -50.0;
|
||||
/// The maximum score a peer can obtain.
|
||||
const MAX_SCORE: f64 = 100.0;
|
||||
/// The minimum score a peer can obtain.
|
||||
const MIN_SCORE: f64 = -100.0;
|
||||
/// The halflife of a peer's score. I.e the number of seconds it takes for the score to decay to half its value.
|
||||
const SCORE_HALFLIFE: f64 = 600.0;
|
||||
/// The number of seconds we ban a peer for before their score begins to decay.
|
||||
const BANNED_BEFORE_DECAY: u64 = 1800;
|
||||
|
||||
/// A collection of actions a peer can perform which will adjust its score.
|
||||
/// Each variant has an associated score change.
|
||||
// To easily assess the behaviour of scores changes the number of variants should stay low, and
|
||||
// somewhat generic.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PeerAction {
|
||||
/// We should not communicate more with this peer.
|
||||
/// This action will cause the peer to get banned.
|
||||
Fatal,
|
||||
/// This peer's action is not malicious but will not be tolerated. A few occurrences will cause
|
||||
/// the peer to get kicked.
|
||||
/// NOTE: ~5 occurrences will get the peer banned
|
||||
LowToleranceError,
|
||||
/// An error occurred with this peer but it is not necessarily malicious.
|
||||
/// We have high tolerance for this actions: several occurrences are needed for a peer to get
|
||||
/// kicked.
|
||||
/// NOTE: ~10 occurrences will get the peer banned
|
||||
MidToleranceError,
|
||||
/// An error occurred with this peer but it is not necessarily malicious.
|
||||
/// We have high tolerance for this actions: several occurrences are needed for a peer to get
|
||||
/// kicked.
|
||||
/// NOTE: ~15 occurrences will get the peer banned
|
||||
HighToleranceError,
|
||||
/// Received an expected message.
|
||||
_ValidMessage,
|
||||
}
|
||||
|
||||
/// The expected state of the peer given the peer's score.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum ScoreState {
|
||||
/// We are content with the peers performance. We permit connections and messages.
|
||||
Healthy,
|
||||
/// The peer should be disconnected. We allow re-connections if the peer is persistent.
|
||||
Disconnect,
|
||||
/// The peer is banned. We disallow new connections until it's score has decayed into a
|
||||
/// tolerable threshold.
|
||||
Ban,
|
||||
}
|
||||
|
||||
/// A peer's score (perceived potential usefulness).
|
||||
///
|
||||
/// This simplistic version consists of a global score per peer which decays to 0 over time. The
|
||||
/// decay rate applies equally to positive and negative scores.
|
||||
#[derive(Copy, PartialEq, Clone, Debug, Serialize)]
|
||||
pub struct Score {
|
||||
/// The global score.
|
||||
// NOTE: In the future we may separate this into sub-scores involving the RPC, Gossipsub and
|
||||
// lighthouse.
|
||||
score: f64,
|
||||
/// The time the score was last updated to perform time-based adjustments such as score-decay.
|
||||
#[serde(skip)]
|
||||
last_updated: Instant,
|
||||
}
|
||||
|
||||
impl Default for Score {
|
||||
fn default() -> Self {
|
||||
Score {
|
||||
score: DEFAULT_SCORE,
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Score {}
|
||||
|
||||
impl PartialOrd for Score {
|
||||
fn partial_cmp(&self, other: &Score) -> Option<std::cmp::Ordering> {
|
||||
self.score
|
||||
.partial_cmp(&other.score)
|
||||
.or_else(|| self.last_updated.partial_cmp(&other.last_updated))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Score {
|
||||
fn cmp(&self, other: &Score) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other)
|
||||
.unwrap_or_else(|| std::cmp::Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Score {
|
||||
fn from(f: f64) -> Self {
|
||||
Score {
|
||||
score: f,
|
||||
last_updated: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Score {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:.2}", self.score)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeerAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PeerAction::Fatal => write!(f, "Fatal"),
|
||||
PeerAction::LowToleranceError => write!(f, "Low Tolerance Error"),
|
||||
PeerAction::MidToleranceError => write!(f, "Mid Tolerance Error"),
|
||||
PeerAction::HighToleranceError => write!(f, "High Tolerance Error"),
|
||||
PeerAction::_ValidMessage => write!(f, "Valid Message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ScoreState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ScoreState::Healthy => write!(f, "Healthy"),
|
||||
ScoreState::Ban => write!(f, "Ban"),
|
||||
ScoreState::Disconnect => write!(f, "Disconnect"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Score {
|
||||
/// Access to the underlying score.
|
||||
pub fn score(&self) -> f64 {
|
||||
self.score
|
||||
}
|
||||
|
||||
/// Modifies the score based on a peer's action.
|
||||
pub fn apply_peer_action(&mut self, peer_action: PeerAction) {
|
||||
match peer_action {
|
||||
PeerAction::Fatal => self.score = MIN_SCORE, // The worst possible score
|
||||
PeerAction::LowToleranceError => self.add(-10.0),
|
||||
PeerAction::MidToleranceError => self.add(-5.0),
|
||||
PeerAction::HighToleranceError => self.add(-1.0),
|
||||
PeerAction::_ValidMessage => self.add(0.1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected state of the peer given it's score.
|
||||
pub(crate) fn state(&self) -> ScoreState {
|
||||
match self.score {
|
||||
x if x <= MIN_SCORE_BEFORE_BAN => ScoreState::Ban,
|
||||
x if x <= MIN_SCORE_BEFORE_DISCONNECT => ScoreState::Disconnect,
|
||||
_ => ScoreState::Healthy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an f64 to the score abiding by the limits.
|
||||
pub fn add(&mut self, score: f64) {
|
||||
let mut new_score = self.score + score;
|
||||
if new_score > MAX_SCORE {
|
||||
new_score = MAX_SCORE;
|
||||
}
|
||||
if new_score < MIN_SCORE {
|
||||
new_score = MIN_SCORE;
|
||||
}
|
||||
|
||||
self.score = new_score;
|
||||
}
|
||||
|
||||
/// Applies time-based logic such as decay rates to the score.
|
||||
/// This function should be called periodically.
|
||||
pub fn update(&mut self) {
|
||||
// Apply decay logic
|
||||
//
|
||||
// There is two distinct decay processes. One for banned peers and one for all others. If
|
||||
// the score is below the banning threshold and the duration since it was last update is
|
||||
// shorter than the banning threshold, we do nothing.
|
||||
let now = Instant::now();
|
||||
if self.score <= MIN_SCORE_BEFORE_BAN
|
||||
&& now
|
||||
.checked_duration_since(self.last_updated)
|
||||
.map(|d| d.as_secs())
|
||||
<= Some(BANNED_BEFORE_DECAY)
|
||||
{
|
||||
// The peer is banned and still within the ban timeout. Do not update it's score.
|
||||
return;
|
||||
}
|
||||
|
||||
// Decay the current score
|
||||
// Using exponential decay based on a constant half life.
|
||||
if let Some(secs_since_update) = now
|
||||
.checked_duration_since(self.last_updated)
|
||||
.map(|d| d.as_secs())
|
||||
{
|
||||
// e^(-ln(2)/HL*t)
|
||||
let decay_factor = (*HALFLIFE_DECAY * secs_since_update as f64).exp();
|
||||
self.score *= decay_factor;
|
||||
self.last_updated = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reputation_change() {
|
||||
let mut score = Score::default();
|
||||
|
||||
// 0 change does not change de reputation
|
||||
//
|
||||
let change = 0.0;
|
||||
score.add(change);
|
||||
assert_eq!(score.score(), DEFAULT_SCORE);
|
||||
|
||||
// underflowing change is capped
|
||||
let mut score = Score::default();
|
||||
let change = MIN_SCORE - 50.0;
|
||||
score.add(change);
|
||||
assert_eq!(score.score(), MIN_SCORE);
|
||||
|
||||
// overflowing change is capped
|
||||
let mut score = Score::default();
|
||||
let change = MAX_SCORE + 50.0;
|
||||
score.add(change);
|
||||
assert_eq!(score.score(), MAX_SCORE);
|
||||
|
||||
// Score adjusts
|
||||
let mut score = Score::default();
|
||||
let change = 1.32;
|
||||
score.add(change);
|
||||
assert_eq!(score.score(), DEFAULT_SCORE + change);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user