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:
Age Manning
2020-07-07 10:13:16 +10:00
committed by GitHub
parent 5977c00edb
commit 5bc8fea2e0
26 changed files with 1339 additions and 934 deletions

View File

@@ -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");

View File

@@ -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,
}

View File

@@ -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),

View File

@@ -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());
}
}

View 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);
}
}