Integrate litep2p into Polkadot SDK (#2944)

[litep2p](https://github.com/altonen/litep2p) is a libp2p-compatible P2P
networking library. It supports all of the features of `rust-libp2p`
that are currently being utilized by Polkadot SDK.

Compared to `rust-libp2p`, `litep2p` has a quite different architecture
which is why the new `litep2p` network backend is only able to use a
little of the existing code in `sc-network`. The design has been mainly
influenced by how we'd wish to structure our networking-related code in
Polkadot SDK: independent higher-levels protocols directly communicating
with the network over links that support bidirectional backpressure. A
good example would be `NotificationHandle`/`RequestResponseHandle`
abstractions which allow, e.g., `SyncingEngine` to directly communicate
with peers to announce/request blocks.

I've tried running `polkadot --network-backend litep2p` with a few
different peer configurations and there is a noticeable reduction in
networking CPU usage. For high load (`--out-peers 200`), networking CPU
usage goes down from ~110% to ~30% (80 pp) and for normal load
(`--out-peers 40`), the usage goes down from ~55% to ~18% (37 pp).

These should not be taken as final numbers because:

a) there are still some low-hanging optimization fruits, such as
enabling [receive window
auto-tuning](https://github.com/libp2p/rust-yamux/pull/176), integrating
`Peerset` more closely with `litep2p` or improving memory usage of the
WebSocket transport
b) fixing bugs/instabilities that incorrectly cause `litep2p` to do less
work will increase the networking CPU usage
c) verification in a more diverse set of tests/conditions is needed

Nevertheless, these numbers should give an early estimate for CPU usage
of the new networking backend.

This PR consists of three separate changes:
* introduce a generic `PeerId` (wrapper around `Multihash`) so that we
don't have use `NetworkService::PeerId` in every part of the code that
uses a `PeerId`
* introduce `NetworkBackend` trait, implement it for the libp2p network
stack and make Polkadot SDK generic over `NetworkBackend`
  * implement `NetworkBackend` for litep2p

The new library should be considered experimental which is why
`rust-libp2p` will remain as the default option for the time being. This
PR currently depends on the master branch of `litep2p` but I'll cut a
new release for the library once all review comments have been
addresses.

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: Dmitry Markin <dmitry@markin.tech>
Co-authored-by: Alexandru Vasile <60601340+lexnv@users.noreply.github.com>
Co-authored-by: Alexandru Vasile <alexandru.vasile@parity.io>
This commit is contained in:
Aaro Altonen
2024-04-08 19:44:13 +03:00
committed by GitHub
parent 9543d31474
commit 80616f6d03
181 changed files with 11055 additions and 1862 deletions
+211 -135
View File
@@ -21,28 +21,165 @@
//! Traits defined by `sc-network`.
use crate::{
config::MultiaddrWithPeerId,
error,
config::{IncomingRequest, MultiaddrWithPeerId, NotificationHandshake, Params, SetConfig},
error::{self, Error},
event::Event,
network_state::NetworkState,
request_responses::{IfDisconnected, RequestFailure},
service::signature::Signature,
service::{metrics::NotificationMetrics, signature::Signature, PeerStoreProvider},
types::ProtocolName,
ReputationChange,
Multiaddr, ReputationChange,
};
use futures::{channel::oneshot, Stream};
use libp2p::{Multiaddr, PeerId};
use prometheus_endpoint::Registry;
use sc_network_common::role::ObservedRole;
use sc_client_api::BlockBackend;
use sc_network_common::{role::ObservedRole, ExHashT};
use sc_network_types::PeerId;
use sp_runtime::traits::Block as BlockT;
use std::{collections::HashSet, fmt::Debug, future::Future, pin::Pin, sync::Arc};
use std::{collections::HashSet, fmt::Debug, future::Future, pin::Pin, sync::Arc, time::Duration};
pub use libp2p::{identity::SigningError, kad::record::Key as KademliaKey};
/// Supertrait defining the services provided by [`NetworkBackend`] service handle.
pub trait NetworkService:
NetworkSigner
+ NetworkDHTProvider
+ NetworkStatusProvider
+ NetworkPeers
+ NetworkEventStream
+ NetworkStateInfo
+ NetworkRequest
+ Send
+ Sync
+ 'static
{
}
impl<T> NetworkService for T where
T: NetworkSigner
+ NetworkDHTProvider
+ NetworkStatusProvider
+ NetworkPeers
+ NetworkEventStream
+ NetworkStateInfo
+ NetworkRequest
+ Send
+ Sync
+ 'static
{
}
/// Trait defining the required functionality from a notification protocol configuration.
pub trait NotificationConfig: Debug {
/// Get access to the `SetConfig` of the notification protocol.
fn set_config(&self) -> &SetConfig;
/// Get protocol name.
fn protocol_name(&self) -> &ProtocolName;
}
/// Trait defining the required functionality from a request-response protocol configuration.
pub trait RequestResponseConfig: Debug {
/// Get protocol name.
fn protocol_name(&self) -> &ProtocolName;
}
/// Trait defining required functionality from `PeerStore`.
#[async_trait::async_trait]
pub trait PeerStore {
/// Get handle to `PeerStore`.
fn handle(&self) -> Arc<dyn PeerStoreProvider>;
/// Start running `PeerStore` event loop.
async fn run(self);
}
/// Networking backend.
#[async_trait::async_trait]
pub trait NetworkBackend<B: BlockT + 'static, H: ExHashT>: Send + 'static {
/// Type representing notification protocol-related configuration.
type NotificationProtocolConfig: NotificationConfig;
/// Type representing request-response protocol-related configuration.
type RequestResponseProtocolConfig: RequestResponseConfig;
/// Type implementing `NetworkService` for the networking backend.
///
/// `NetworkService` allows other subsystems of the blockchain to interact with `sc-network`
/// using `NetworkService`.
type NetworkService<Block, Hash>: NetworkService + Clone;
/// Type implementing [`PeerStore`].
type PeerStore: PeerStore;
/// Bitswap config.
type BitswapConfig;
/// Create new `NetworkBackend`.
fn new(params: Params<B, H, Self>) -> Result<Self, Error>
where
Self: Sized;
/// Get handle to `NetworkService` of the `NetworkBackend`.
fn network_service(&self) -> Arc<dyn NetworkService>;
/// Create [`PeerStore`].
fn peer_store(bootnodes: Vec<PeerId>) -> Self::PeerStore;
/// Register metrics that are used by the notification protocols.
fn register_notification_metrics(registry: Option<&Registry>) -> NotificationMetrics;
/// Create Bitswap server.
fn bitswap_server(
client: Arc<dyn BlockBackend<B> + Send + Sync>,
) -> (Pin<Box<dyn Future<Output = ()> + Send>>, Self::BitswapConfig);
/// Create notification protocol configuration and an associated `NotificationService`
/// for the protocol.
fn notification_config(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_notification_size: u64,
handshake: Option<NotificationHandshake>,
set_config: SetConfig,
metrics: NotificationMetrics,
peerstore_handle: Arc<dyn PeerStoreProvider>,
) -> (Self::NotificationProtocolConfig, Box<dyn NotificationService>);
/// Create request-response protocol configuration.
fn request_response_config(
protocol_name: ProtocolName,
fallback_names: Vec<ProtocolName>,
max_request_size: u64,
max_response_size: u64,
request_timeout: Duration,
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
) -> Self::RequestResponseProtocolConfig;
/// Start [`NetworkBackend`] event loop.
async fn run(mut self);
}
/// Signer with network identity
pub trait NetworkSigner {
/// Signs the message with the `KeyPair` that defines the local [`PeerId`].
fn sign_with_local_identity(&self, msg: impl AsRef<[u8]>) -> Result<Signature, SigningError>;
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError>;
/// Verify signature using peer's public key.
///
/// `public_key` must be Protobuf-encoded ed25519 public key.
///
/// Returns `Err(())` if public cannot be parsed into a valid ed25519 public key.
fn verify(
&self,
peer_id: sc_network_types::PeerId,
public_key: &Vec<u8>,
signature: &Vec<u8>,
message: &Vec<u8>,
) -> Result<bool, String>;
}
impl<T> NetworkSigner for Arc<T>
@@ -50,9 +187,19 @@ where
T: ?Sized,
T: NetworkSigner,
{
fn sign_with_local_identity(&self, msg: impl AsRef<[u8]>) -> Result<Signature, SigningError> {
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError> {
T::sign_with_local_identity(self, msg)
}
fn verify(
&self,
peer_id: sc_network_types::PeerId,
public_key: &Vec<u8>,
signature: &Vec<u8>,
message: &Vec<u8>,
) -> Result<bool, String> {
T::verify(self, peer_id, public_key, signature, message)
}
}
/// Provides access to the networking DHT.
@@ -117,6 +264,11 @@ pub trait NetworkStatusProvider {
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn status(&self) -> Result<NetworkStatus, ()>;
/// Get the network state.
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn network_state(&self) -> Result<NetworkState, ()>;
}
// Manual implementation to avoid extra boxing here
@@ -134,9 +286,20 @@ where
{
T::status(self)
}
fn network_state<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<NetworkState, ()>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
T::network_state(self)
}
}
/// Provides low-level API for manipulating network peers.
#[async_trait::async_trait]
pub trait NetworkPeers {
/// Set authorized peers.
///
@@ -237,9 +400,15 @@ pub trait NetworkPeers {
/// decoded into a role, the role queried from `PeerStore` and if the role is not stored
/// there either, `None` is returned and the peer should be discarded.
fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole>;
/// Get the list of reserved peers.
///
/// Returns an error if the `NetworkWorker` is no longer running.
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()>;
}
// Manual implementation to avoid extra boxing here
#[async_trait::async_trait]
impl<T> NetworkPeers for Arc<T>
where
T: ?Sized,
@@ -316,6 +485,16 @@ where
fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
T::peer_role(self, peer_id, handshake)
}
fn reserved_peers<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<PeerId>, ()>> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
T::reserved_peers(self)
}
}
/// Provides access to network-level event stream.
@@ -389,15 +568,15 @@ pub trait NotificationSender: Send + Sync + 'static {
-> Result<Box<dyn NotificationSenderReady + '_>, NotificationSenderError>;
}
/// Error returned by [`NetworkNotification::notification_sender`].
/// Error returned by the notification sink.
#[derive(Debug, thiserror::Error)]
pub enum NotificationSenderError {
/// The notification receiver has been closed, usually because the underlying connection
/// closed.
///
/// Some of the notifications most recently sent may not have been received. However,
/// the peer may still be connected and a new `NotificationSender` for the same
/// protocol obtained from [`NetworkNotification::notification_sender`].
/// the peer may still be connected and a new notification sink for the same
/// protocol obtained from [`NotificationService::message_sink()`].
#[error("The notification receiver has been closed")]
Closed,
/// Protocol name hasn't been registered.
@@ -405,127 +584,6 @@ pub enum NotificationSenderError {
BadProtocol,
}
/// Provides ability to send network notifications.
pub trait NetworkNotification {
/// Appends a notification to the buffer of pending outgoing notifications with the given peer.
/// Has no effect if the notifications channel with this protocol name is not open.
///
/// If the buffer of pending outgoing notifications with that peer is full, the notification
/// is silently dropped and the connection to the remote will start being shut down. This
/// happens if you call this method at a higher rate than the rate at which the peer processes
/// these notifications, or if the available network bandwidth is too low.
///
/// For this reason, this method is considered soft-deprecated. You are encouraged to use
/// [`NetworkNotification::notification_sender`] instead.
///
/// > **Note**: The reason why this is a no-op in the situation where we have no channel is
/// > that we don't guarantee message delivery anyway. Networking issues can cause
/// > connections to drop at any time, and higher-level logic shouldn't differentiate
/// > between the remote voluntarily closing a substream or a network error
/// > preventing the message from being delivered.
///
/// The protocol must have been registered with
/// `crate::config::NetworkConfiguration::notifications_protocols`.
fn write_notification(&self, target: PeerId, protocol: ProtocolName, message: Vec<u8>);
/// Obtains a [`NotificationSender`] for a connected peer, if it exists.
///
/// A `NotificationSender` is scoped to a particular connection to the peer that holds
/// a receiver. With a `NotificationSender` at hand, sending a notification is done in two
/// steps:
///
/// 1. [`NotificationSender::ready`] is used to wait for the sender to become ready
/// for another notification, yielding a [`NotificationSenderReady`] token.
/// 2. [`NotificationSenderReady::send`] enqueues the notification for sending. This operation
/// can only fail if the underlying notification substream or connection has suddenly closed.
///
/// An error is returned by [`NotificationSenderReady::send`] if there exists no open
/// notifications substream with that combination of peer and protocol, or if the remote
/// has asked to close the notifications substream. If that happens, it is guaranteed that an
/// [`Event::NotificationStreamClosed`] has been generated on the stream returned by
/// [`NetworkEventStream::event_stream`].
///
/// If the remote requests to close the notifications substream, all notifications successfully
/// enqueued using [`NotificationSenderReady::send`] will finish being sent out before the
/// substream actually gets closed, but attempting to enqueue more notifications will now
/// return an error. It is however possible for the entire connection to be abruptly closed,
/// in which case enqueued notifications will be lost.
///
/// The protocol must have been registered with
/// `crate::config::NetworkConfiguration::notifications_protocols`.
///
/// # Usage
///
/// This method returns a struct that allows waiting until there is space available in the
/// buffer of messages towards the given peer. If the peer processes notifications at a slower
/// rate than we send them, this buffer will quickly fill up.
///
/// As such, you should never do something like this:
///
/// ```ignore
/// // Do NOT do this
/// for peer in peers {
/// if let Ok(n) = network.notification_sender(peer, ...) {
/// if let Ok(s) = n.ready().await {
/// let _ = s.send(...);
/// }
/// }
/// }
/// ```
///
/// Doing so would slow down all peers to the rate of the slowest one. A malicious or
/// malfunctioning peer could intentionally process notifications at a very slow rate.
///
/// Instead, you are encouraged to maintain your own buffer of notifications on top of the one
/// maintained by `sc-network`, and use `notification_sender` to progressively send out
/// elements from your buffer. If this additional buffer is full (which will happen at some
/// point if the peer is too slow to process notifications), appropriate measures can be taken,
/// such as removing non-critical notifications from the buffer or disconnecting the peer
/// using [`NetworkPeers::disconnect_peer`].
///
///
/// Notifications Per-peer buffer
/// broadcast +-------> of notifications +--> `notification_sender` +--> Internet
/// ^ (not covered by
/// | sc-network)
/// +
/// Notifications should be dropped
/// if buffer is full
///
///
/// See also the `sc-network-gossip` crate for a higher-level way to send notifications.
fn notification_sender(
&self,
target: PeerId,
protocol: ProtocolName,
) -> Result<Box<dyn NotificationSender>, NotificationSenderError>;
/// Set handshake for the notification protocol.
fn set_notification_handshake(&self, protocol: ProtocolName, handshake: Vec<u8>);
}
impl<T> NetworkNotification for Arc<T>
where
T: ?Sized,
T: NetworkNotification,
{
fn write_notification(&self, target: PeerId, protocol: ProtocolName, message: Vec<u8>) {
T::write_notification(self, target, protocol, message)
}
fn notification_sender(
&self,
target: PeerId,
protocol: ProtocolName,
) -> Result<Box<dyn NotificationSender>, NotificationSenderError> {
T::notification_sender(self, target, protocol)
}
fn set_notification_handshake(&self, protocol: ProtocolName, handshake: Vec<u8>) {
T::set_notification_handshake(self, protocol, handshake)
}
}
/// Provides ability to send network requests.
#[async_trait::async_trait]
pub trait NetworkRequest {
@@ -662,6 +720,15 @@ pub enum Direction {
Outbound,
}
impl From<litep2p::protocol::notification::Direction> for Direction {
fn from(direction: litep2p::protocol::notification::Direction) -> Self {
match direction {
litep2p::protocol::notification::Direction::Inbound => Direction::Inbound,
litep2p::protocol::notification::Direction::Outbound => Direction::Outbound,
}
}
}
impl Direction {
/// Is the direction inbound.
pub fn is_inbound(&self) -> bool {
@@ -771,13 +838,13 @@ pub trait NotificationService: Debug + Send {
async fn close_substream(&mut self, peer: PeerId) -> Result<(), ()>;
/// Send synchronous `notification` to `peer`.
fn send_sync_notification(&self, peer: &PeerId, notification: Vec<u8>);
fn send_sync_notification(&mut self, peer: &PeerId, notification: Vec<u8>);
/// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
///
/// Returns an error if the peer doesn't exist.
async fn send_async_notification(
&self,
&mut self,
peer: &PeerId,
notification: Vec<u8>,
) -> Result<(), error::Error>;
@@ -827,3 +894,12 @@ pub trait MessageSink: Send + Sync {
/// Returns an error if the peer does not exist.
async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), error::Error>;
}
/// Trait defining the behavior of a bandwidth sink.
pub trait BandwidthSink: Send + Sync {
/// Get the number of bytes received.
fn total_inbound(&self) -> u64;
/// Get the number of bytes sent.
fn total_outbound(&self) -> u64;
}