// Copyright 2018-2019 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see .
//! Peer Set Manager (PSM). Contains the strategy for choosing which nodes the network should be
//! connected to.
mod slots;
use std::collections::VecDeque;
use futures::{prelude::*, sync::mpsc, try_ready};
use libp2p::PeerId;
use linked_hash_map::LinkedHashMap;
use log::trace;
use lru_cache::LruCache;
use slots::{SlotType, SlotState, Slots};
use serde_json::json;
const PEERSET_SCORES_CACHE_SIZE: usize = 1000;
const DISCOVERED_NODES_LIMIT: u32 = 1000;
#[derive(Debug)]
struct PeersetData {
/// List of nodes that we know exist, but we are not connected to.
/// Elements in this list must never be in `out_slots` or `in_slots`.
discovered: Slots,
/// If true, we only accept reserved nodes.
reserved_only: bool,
/// Node slots for outgoing connections.
out_slots: Slots,
/// Node slots for incoming connections.
in_slots: Slots,
/// List of node scores.
scores: LruCache,
}
#[derive(Debug)]
enum Action {
AddReservedPeer(PeerId),
RemoveReservedPeer(PeerId),
SetReservedOnly(bool),
ReportPeer(PeerId, i32),
}
/// Shared handle to the peer set manager (PSM). Distributed around the code.
#[derive(Debug, Clone)]
pub struct PeersetHandle {
tx: mpsc::UnboundedSender,
}
impl PeersetHandle {
/// Adds a new reserved peer. The peerset will make an effort to always remain connected to
/// this peer.
///
/// Has no effect if the node was already a reserved peer.
///
/// > **Note**: Keep in mind that the networking has to know an address for this node,
/// > otherwise it will not be able to connect to it.
pub fn add_reserved_peer(&self, peer_id: PeerId) {
let _ = self.tx.unbounded_send(Action::AddReservedPeer(peer_id));
}
/// Remove a previously-added reserved peer.
///
/// Has no effect if the node was not a reserved peer.
pub fn remove_reserved_peer(&self, peer_id: PeerId) {
let _ = self.tx.unbounded_send(Action::RemoveReservedPeer(peer_id));
}
/// Sets whether or not the peerset only has connections .
pub fn set_reserved_only(&self, reserved: bool) {
let _ = self.tx.unbounded_send(Action::SetReservedOnly(reserved));
}
/// Reports an adjustment to the reputation of the given peer.
pub fn report_peer(&self, peer_id: PeerId, score_diff: i32) {
let _ = self.tx.unbounded_send(Action::ReportPeer(peer_id, score_diff));
}
}
/// Message that can be sent by the peer set manager (PSM).
#[derive(Debug, PartialEq)]
pub enum Message {
/// Request to open a connection to the given peer. From the point of view of the PSM, we are
/// immediately connected.
Connect(PeerId),
/// Drop the connection to the given peer, or cancel the connection attempt after a `Connect`.
Drop(PeerId),
/// Equivalent to `Connect` for the peer corresponding to this incoming index.
Accept(IncomingIndex),
/// Equivalent to `Drop` for the peer corresponding to this incoming index.
Reject(IncomingIndex),
}
/// Opaque identifier for an incoming connection. Allocated by the network.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct IncomingIndex(pub u64);
impl From for IncomingIndex {
fn from(val: u64) -> IncomingIndex {
IncomingIndex(val)
}
}
/// Configuration to pass when creating the peer set manager.
#[derive(Debug)]
pub struct PeersetConfig {
/// Maximum number of ingoing links to peers.
pub in_peers: u32,
/// Maximum number of outgoing links to peers.
pub out_peers: u32,
/// List of bootstrap nodes to initialize the peer with.
///
/// > **Note**: Keep in mind that the networking has to know an address for these nodes,
/// > otherwise it will not be able to connect to them.
pub bootnodes: Vec,
/// If true, we only accept reserved nodes.
pub reserved_only: bool,
/// List of nodes that we should always be connected to.
///
/// > **Note**: Keep in mind that the networking has to know an address for these nodes,
/// > otherwise it will not be able to connect to them.
pub reserved_nodes: Vec,
}
/// Side of the peer set manager owned by the network. In other words, the "receiving" side.
///
/// Implements the `Stream` trait and can be polled for messages. The `Stream` never ends and never
/// errors.
#[derive(Debug)]
pub struct Peerset {
data: PeersetData,
rx: mpsc::UnboundedReceiver,
message_queue: VecDeque,
}
impl Peerset {
/// Builds a new peerset from the given configuration.
pub fn from_config(config: PeersetConfig) -> (Peerset, PeersetHandle) {
let (tx, rx) = mpsc::unbounded();
let data = PeersetData {
discovered: Slots::new(DISCOVERED_NODES_LIMIT),
reserved_only: config.reserved_only,
out_slots: Slots::new(config.out_peers),
in_slots: Slots::new(config.in_peers),
scores: LruCache::new(PEERSET_SCORES_CACHE_SIZE),
};
let handle = PeersetHandle {
tx,
};
let mut peerset = Peerset {
data,
rx,
message_queue: VecDeque::new(),
};
for peer_id in config.reserved_nodes {
peerset.data.discovered.add_peer(peer_id, SlotType::Reserved);
}
for peer_id in config.bootnodes {
peerset.data.discovered.add_peer(peer_id, SlotType::Common);
}
peerset.alloc_slots();
(peerset, handle)
}
fn on_add_reserved_peer(&mut self, peer_id: PeerId) {
// Nothing more to do if we're already connected.
if self.data.in_slots.contains(&peer_id) {
self.data.in_slots.mark_reserved(&peer_id);
return;
}
match self.data.out_slots.add_peer(peer_id, SlotType::Reserved) {
SlotState::Added(peer_id) => {
// reserved node may have been previously stored as normal node in discovered list
self.data.discovered.remove_peer(&peer_id);
// notify that connection has been made
trace!(target: "peerset", "Connecting to new reserved peer {}", peer_id);
self.message_queue.push_back(Message::Connect(peer_id));
return;
},
SlotState::Swaped { removed, added } => {
// reserved node may have been previously stored as normal node in discovered list
self.data.discovered.remove_peer(&added);
// let's add the peer we disconnected from to the discovered list again
self.data.discovered.add_peer(removed.clone(), SlotType::Common);
// swap connections
trace!(target: "peerset", "Connecting to new reserved peer {}, dropping {}", added, removed);
self.message_queue.push_back(Message::Drop(removed));
self.message_queue.push_back(Message::Connect(added));
}
SlotState::AlreadyExists(_) | SlotState::Upgraded(_) => {
return;
}
SlotState::MaxCapacity(peer_id) => {
self.data.discovered.add_peer(peer_id, SlotType::Reserved);
return;
}
}
}
fn on_remove_reserved_peer(&mut self, peer_id: PeerId) {
self.data.in_slots.mark_not_reserved(&peer_id);
self.data.out_slots.mark_not_reserved(&peer_id);
self.data.discovered.mark_not_reserved(&peer_id);
if self.data.reserved_only {
if self.data.in_slots.remove_peer(&peer_id) || self.data.out_slots.remove_peer(&peer_id) {
// insert peer back into discovered list
self.data.discovered.add_peer(peer_id.clone(), SlotType::Common);
self.message_queue.push_back(Message::Drop(peer_id));
// call alloc_slots again, cause we may have some reserved peers in discovered list
// waiting for the slot that was just cleared
self.alloc_slots();
}
}
}
fn on_set_reserved_only(&mut self, reserved_only: bool) {
// Disconnect non-reserved nodes.
self.data.reserved_only = reserved_only;
if self.data.reserved_only {
for peer_id in self.data.in_slots.clear_common_slots().into_iter().chain(self.data.out_slots.clear_common_slots().into_iter()) {
// insert peer back into discovered list
self.data.discovered.add_peer(peer_id.clone(), SlotType::Common);
self.message_queue.push_back(Message::Drop(peer_id));
}
} else {
self.alloc_slots();
}
}
fn on_report_peer(&mut self, peer_id: PeerId, score_diff: i32) {
let score = match self.data.scores.get_mut(&peer_id) {
Some(score) => {
*score = score.saturating_add(score_diff);
*score
},
None => {
self.data.scores.insert(peer_id.clone(), score_diff);
score_diff
}
};
if score < 0 {
// peer will be removed from `in_slots` or `out_slots` in `on_dropped` method
if self.data.in_slots.contains(&peer_id) || self.data.out_slots.contains(&peer_id) {
self.data.in_slots.remove_peer(&peer_id);
self.data.out_slots.remove_peer(&peer_id);
self.message_queue.push_back(Message::Drop(peer_id));
}
}
}
fn alloc_slots(&mut self) {
while let Some((peer_id, slot_type)) = self.data.discovered.pop_most_important_peer(self.data.reserved_only) {
match self.data.out_slots.add_peer(peer_id, slot_type) {
SlotState::Added(peer_id) => {
trace!(target: "peerset", "Connecting to new peer {}", peer_id);
self.message_queue.push_back(Message::Connect(peer_id));
},
SlotState::Swaped { removed, added } => {
// insert peer back into discovered list
trace!(target: "peerset", "Connecting to new peer {}, dropping {}", added, removed);
self.data.discovered.add_peer(removed.clone(), SlotType::Common);
self.message_queue.push_back(Message::Drop(removed));
self.message_queue.push_back(Message::Connect(added));
}
SlotState::Upgraded(_) | SlotState::AlreadyExists(_) => {
// TODO: we should never reach this point
},
SlotState::MaxCapacity(peer_id) => {
self.data.discovered.add_peer(peer_id, slot_type);
break;
},
}
}
}
/// Indicate that we received an incoming connection. Must be answered either with
/// a corresponding `Accept` or `Reject`, except if we were already connected to this peer.
///
/// Note that this mechanism is orthogonal to `Connect`/`Drop`. Accepting an incoming
/// connection implicitely means `Accept`, but incoming connections aren't cancelled by
/// `dropped`.
///
/// Because of concurrency issues, it is acceptable to call `incoming` with a `PeerId` the
/// peerset is already connected to, in which case it must not answer.
pub fn incoming(&mut self, peer_id: PeerId, index: IncomingIndex) {
trace!(
target: "peerset",
"Incoming {:?}\nin_slots={:?}\nout_slots={:?}",
peer_id, self.data.in_slots, self.data.out_slots
);
// if `reserved_only` is set, but this peer is not a part of our discovered list,
// a) it is not reserved, so we reject the connection
// b) we are already connected to it, so we reject the connection
if self.data.reserved_only && !self.data.discovered.is_reserved(&peer_id) {
self.message_queue.push_back(Message::Reject(index));
return;
}
// check if we are already connected to this peer
if self.data.out_slots.contains(&peer_id) {
// we are already connected. in this case we do not answer
return;
}
let slot_type = if self.data.reserved_only {
SlotType::Reserved
} else {
SlotType::Common
};
match self.data.in_slots.add_peer(peer_id, slot_type) {
SlotState::Added(peer_id) => {
// reserved node may have been previously stored as normal node in discovered list
self.data.discovered.remove_peer(&peer_id);
self.message_queue.push_back(Message::Accept(index));
return;
},
SlotState::Swaped { removed, added } => {
// reserved node may have been previously stored as normal node in discovered list
self.data.discovered.remove_peer(&added);
// swap connections.
self.message_queue.push_back(Message::Drop(removed));
self.message_queue.push_back(Message::Accept(index));
},
SlotState::AlreadyExists(_) | SlotState::Upgraded(_) => {
// we are already connected. in this case we do not answer
return;
},
SlotState::MaxCapacity(peer_id) => {
self.data.discovered.add_peer(peer_id, slot_type);
self.message_queue.push_back(Message::Reject(index));
return;
},
}
}
/// Indicate that we dropped an active connection with a peer, or that we failed to connect.
///
/// Must only be called after the PSM has either generated a `Connect` message with this
/// `PeerId`, or accepted an incoming connection with this `PeerId`.
pub fn dropped(&mut self, peer_id: PeerId) {
trace!(
target: "peerset",
"Dropping {:?}\nin_slots={:?}\nout_slots={:?}",
peer_id, self.data.in_slots, self.data.out_slots
);
// Automatically connect back if reserved.
if self.data.in_slots.is_reserved(&peer_id) || self.data.out_slots.is_reserved(&peer_id) {
self.message_queue.push_back(Message::Connect(peer_id));
return;
}
// Otherwise, free the slot.
self.data.in_slots.remove_peer(&peer_id);
self.data.out_slots.remove_peer(&peer_id);
// Note: in this dummy implementation we consider that peers never expire. As soon as we
// are disconnected from a peer, we try again.
self.data.discovered.add_peer(peer_id, SlotType::Common);
self.alloc_slots();
}
/// Adds discovered peer ids to the PSM.
///
/// > **Note**: There is no equivalent "expired" message, meaning that it is the responsibility
/// > of the PSM to remove `PeerId`s that fail to dial too often.
pub fn discovered>(&mut self, peer_ids: I) {
for peer_id in peer_ids {
if !self.data.in_slots.contains(&peer_id) && !self.data.out_slots.contains(&peer_id) && !self.data.discovered.contains(&peer_id) {
trace!(target: "peerset", "Discovered new peer: {:?}", peer_id);
self.data.discovered.add_peer(peer_id, SlotType::Common);
} else {
trace!(target: "peerset", "Discovered known peer: {:?}", peer_id);
}
}
self.alloc_slots();
}
/// Produces a JSON object containing the state of the peerset manager, for debugging purposes.
pub fn debug_info(&self) -> serde_json::Value {
json!({
"data": {
// add scores
"discovered": self.data.discovered.debug_info(),
"reserved_only": self.data.reserved_only,
"out_slots": self.data.out_slots.debug_info(),
"in_slots": self.data.in_slots.debug_info()
},
"message_queue": self.message_queue.len(),
})
}
}
impl Stream for Peerset {
type Item = Message;
type Error = ();
fn poll(&mut self) -> Poll