feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
discovery::{DiscoveryBehaviour, DiscoveryConfig, DiscoveryOut},
|
||||
event::DhtEvent,
|
||||
peer_info,
|
||||
peer_store::PeerStoreProvider,
|
||||
protocol::{CustomMessageOutcome, NotificationsSink, Protocol},
|
||||
protocol_controller::SetId,
|
||||
request_responses::{self, IfDisconnected, ProtocolConfig, RequestFailure},
|
||||
service::traits::Direction,
|
||||
types::ProtocolName,
|
||||
ReputationChange,
|
||||
};
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use libp2p::{
|
||||
connection_limits::ConnectionLimits,
|
||||
core::Multiaddr,
|
||||
identify::Info as IdentifyInfo,
|
||||
identity::PublicKey,
|
||||
kad::{Record, RecordKey},
|
||||
swarm::NetworkBehaviour,
|
||||
PeerId, StreamProtocol,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub use crate::request_responses::{InboundFailure, OutboundFailure, ResponseFailure};
|
||||
|
||||
/// General behaviour of the network. Combines all protocols together.
|
||||
#[derive(NetworkBehaviour)]
|
||||
#[behaviour(to_swarm = "BehaviourOut")]
|
||||
pub struct Behaviour<B: BlockT> {
|
||||
/// Connection limits.
|
||||
connection_limits: libp2p::connection_limits::Behaviour,
|
||||
/// All the bizinikiwi-specific protocols.
|
||||
bizinikiwi: Protocol<B>,
|
||||
/// Periodically pings and identifies the nodes we are connected to, and store information in a
|
||||
/// cache.
|
||||
peer_info: peer_info::PeerInfoBehaviour,
|
||||
/// Discovers nodes of the network.
|
||||
discovery: DiscoveryBehaviour,
|
||||
/// Generic request-response protocols.
|
||||
request_responses: request_responses::RequestResponsesBehaviour,
|
||||
}
|
||||
|
||||
/// Event generated by `Behaviour`.
|
||||
#[derive(Debug)]
|
||||
pub enum BehaviourOut {
|
||||
/// Started a random iterative Kademlia discovery query.
|
||||
RandomKademliaStarted,
|
||||
|
||||
/// We have received a request from a peer and answered it.
|
||||
///
|
||||
/// This event is generated for statistics purposes.
|
||||
InboundRequest {
|
||||
/// Protocol name of the request.
|
||||
protocol: ProtocolName,
|
||||
/// If `Ok`, contains the time elapsed between when we received the request and when we
|
||||
/// sent back the response. If `Err`, the error that happened.
|
||||
result: Result<Duration, ResponseFailure>,
|
||||
},
|
||||
|
||||
/// A request has succeeded or failed.
|
||||
///
|
||||
/// This event is generated for statistics purposes.
|
||||
RequestFinished {
|
||||
/// Name of the protocol in question.
|
||||
protocol: ProtocolName,
|
||||
/// Duration the request took.
|
||||
duration: Duration,
|
||||
/// Result of the request.
|
||||
result: Result<(), RequestFailure>,
|
||||
},
|
||||
|
||||
/// A request protocol handler issued reputation changes for the given peer.
|
||||
ReputationChanges { peer: PeerId, changes: Vec<ReputationChange> },
|
||||
|
||||
/// Opened a substream with the given node with the given notifications protocol.
|
||||
///
|
||||
/// The protocol is always one of the notification protocols that have been registered.
|
||||
NotificationStreamOpened {
|
||||
/// Node we opened the substream with.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
/// Direction of the stream.
|
||||
direction: Direction,
|
||||
/// If the negotiation didn't use the main name of the protocol (the one in
|
||||
/// `notifications_protocol`), then this field contains which name has actually been
|
||||
/// used.
|
||||
/// See also [`crate::Event::NotificationStreamOpened`].
|
||||
negotiated_fallback: Option<ProtocolName>,
|
||||
/// Object that permits sending notifications to the peer.
|
||||
notifications_sink: NotificationsSink,
|
||||
/// Received handshake.
|
||||
received_handshake: Vec<u8>,
|
||||
},
|
||||
|
||||
/// The [`NotificationsSink`] object used to send notifications with the given peer must be
|
||||
/// replaced with a new one.
|
||||
///
|
||||
/// This event is typically emitted when a transport-level connection is closed and we fall
|
||||
/// back to a secondary connection.
|
||||
NotificationStreamReplaced {
|
||||
/// Id of the peer we are connected to.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
/// Replacement for the previous [`NotificationsSink`].
|
||||
notifications_sink: NotificationsSink,
|
||||
},
|
||||
|
||||
/// Closed a substream with the given node. Always matches a corresponding previous
|
||||
/// `NotificationStreamOpened` message.
|
||||
NotificationStreamClosed {
|
||||
/// Node we closed the substream with.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
},
|
||||
|
||||
/// Received one or more messages from the given node using the given protocol.
|
||||
NotificationsReceived {
|
||||
/// Node we received the message from.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
/// Concerned protocol and associated message.
|
||||
notification: Vec<u8>,
|
||||
},
|
||||
|
||||
/// We have obtained identity information from a peer, including the addresses it is listening
|
||||
/// on.
|
||||
PeerIdentify {
|
||||
/// Id of the peer that has been identified.
|
||||
peer_id: PeerId,
|
||||
/// Information about the peer.
|
||||
info: IdentifyInfo,
|
||||
},
|
||||
|
||||
/// We have learned about the existence of a node on the default set.
|
||||
Discovered(PeerId),
|
||||
|
||||
/// Events generated by a DHT as a response to get_value or put_value requests with the
|
||||
/// request duration. Or events generated by the DHT as a consequnce of receiving a record
|
||||
/// to store from peers.
|
||||
Dht(DhtEvent, Option<Duration>),
|
||||
|
||||
/// Ignored event generated by lower layers.
|
||||
None,
|
||||
}
|
||||
|
||||
impl<B: BlockT> Behaviour<B> {
|
||||
/// Builds a new `Behaviour`.
|
||||
pub fn new(
|
||||
bizinikiwi: Protocol<B>,
|
||||
user_agent: String,
|
||||
local_public_key: PublicKey,
|
||||
disco_config: DiscoveryConfig,
|
||||
request_response_protocols: Vec<ProtocolConfig>,
|
||||
peer_store_handle: Arc<dyn PeerStoreProvider>,
|
||||
external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
|
||||
public_addresses: Vec<Multiaddr>,
|
||||
connection_limits: ConnectionLimits,
|
||||
) -> Result<Self, request_responses::RegisterError> {
|
||||
Ok(Self {
|
||||
bizinikiwi,
|
||||
peer_info: peer_info::PeerInfoBehaviour::new(
|
||||
user_agent,
|
||||
local_public_key,
|
||||
external_addresses,
|
||||
public_addresses,
|
||||
),
|
||||
discovery: disco_config.finish(),
|
||||
request_responses: request_responses::RequestResponsesBehaviour::new(
|
||||
request_response_protocols.into_iter(),
|
||||
peer_store_handle,
|
||||
)?,
|
||||
connection_limits: libp2p::connection_limits::Behaviour::new(connection_limits),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the list of nodes that we know exist in the network.
|
||||
pub fn known_peers(&mut self) -> HashSet<PeerId> {
|
||||
self.discovery.known_peers()
|
||||
}
|
||||
|
||||
/// Adds a hard-coded address for the given peer, that never expires.
|
||||
pub fn add_known_address(&mut self, peer_id: PeerId, addr: Multiaddr) {
|
||||
self.discovery.add_known_address(peer_id, addr)
|
||||
}
|
||||
|
||||
/// Returns the number of nodes in each Kademlia kbucket.
|
||||
///
|
||||
/// Identifies kbuckets by the base 2 logarithm of their lower bound.
|
||||
pub fn num_entries_per_kbucket(&mut self) -> Option<Vec<(u32, usize)>> {
|
||||
self.discovery.num_entries_per_kbucket()
|
||||
}
|
||||
|
||||
/// Returns the number of records in the Kademlia record stores.
|
||||
pub fn num_kademlia_records(&mut self) -> Option<usize> {
|
||||
self.discovery.num_kademlia_records()
|
||||
}
|
||||
|
||||
/// Returns the total size in bytes of all the records in the Kademlia record stores.
|
||||
pub fn kademlia_records_total_size(&mut self) -> Option<usize> {
|
||||
self.discovery.kademlia_records_total_size()
|
||||
}
|
||||
|
||||
/// Borrows `self` and returns a struct giving access to the information about a node.
|
||||
///
|
||||
/// Returns `None` if we don't know anything about this node. Always returns `Some` for nodes
|
||||
/// we're connected to, meaning that if `None` is returned then we're not connected to that
|
||||
/// node.
|
||||
pub fn node(&self, peer_id: &PeerId) -> Option<peer_info::Node<'_>> {
|
||||
self.peer_info.node(peer_id)
|
||||
}
|
||||
|
||||
/// Initiates sending a request.
|
||||
pub fn send_request(
|
||||
&mut self,
|
||||
target: &PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
pending_response: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
) {
|
||||
self.request_responses.send_request(
|
||||
target,
|
||||
protocol,
|
||||
request,
|
||||
fallback_request,
|
||||
pending_response,
|
||||
connect,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a shared reference to the user protocol.
|
||||
pub fn user_protocol(&self) -> &Protocol<B> {
|
||||
&self.bizinikiwi
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the user protocol.
|
||||
pub fn user_protocol_mut(&mut self) -> &mut Protocol<B> {
|
||||
&mut self.bizinikiwi
|
||||
}
|
||||
|
||||
/// Add a self-reported address of a remote peer to the k-buckets of the supported
|
||||
/// DHTs (`supported_protocols`).
|
||||
pub fn add_self_reported_address_to_dht(
|
||||
&mut self,
|
||||
peer_id: &PeerId,
|
||||
supported_protocols: &[StreamProtocol],
|
||||
addr: Multiaddr,
|
||||
) {
|
||||
self.discovery.add_self_reported_address(peer_id, supported_protocols, addr);
|
||||
}
|
||||
|
||||
/// Start finding closest peerst to the target `PeerId`. Will later produce either a
|
||||
/// `ClosestPeersFound` or `ClosestPeersNotFound` event.
|
||||
pub fn find_closest_peers(&mut self, target: PeerId) {
|
||||
self.discovery.find_closest_peers(target);
|
||||
}
|
||||
|
||||
/// Start querying a record from the DHT. Will later produce either a `ValueFound` or a
|
||||
/// `ValueNotFound` event.
|
||||
pub fn get_value(&mut self, key: RecordKey) {
|
||||
self.discovery.get_value(key);
|
||||
}
|
||||
|
||||
/// Starts putting a record into DHT. Will later produce either a `ValuePut` or a
|
||||
/// `ValuePutFailed` event.
|
||||
pub fn put_value(&mut self, key: RecordKey, value: Vec<u8>) {
|
||||
self.discovery.put_value(key, value);
|
||||
}
|
||||
|
||||
/// Puts a record into DHT, on the provided Peers
|
||||
pub fn put_record_to(
|
||||
&mut self,
|
||||
record: Record,
|
||||
peers: HashSet<pezsc_network_types::PeerId>,
|
||||
update_local_storage: bool,
|
||||
) {
|
||||
self.discovery.put_record_to(record, peers, update_local_storage);
|
||||
}
|
||||
|
||||
/// Stores value in DHT
|
||||
pub fn store_record(
|
||||
&mut self,
|
||||
record_key: RecordKey,
|
||||
record_value: Vec<u8>,
|
||||
publisher: Option<PeerId>,
|
||||
expires: Option<Instant>,
|
||||
) {
|
||||
self.discovery.store_record(record_key, record_value, publisher, expires);
|
||||
}
|
||||
|
||||
/// Start providing `key` on the DHT.
|
||||
pub fn start_providing(&mut self, key: RecordKey) {
|
||||
self.discovery.start_providing(key)
|
||||
}
|
||||
|
||||
/// Stop providing `key` on the DHT.
|
||||
pub fn stop_providing(&mut self, key: &RecordKey) {
|
||||
self.discovery.stop_providing(key)
|
||||
}
|
||||
|
||||
/// Start searching for providers on the DHT. Will later produce either a `ProvidersFound`
|
||||
/// or `ProvidersNotFound` event.
|
||||
pub fn get_providers(&mut self, key: RecordKey) {
|
||||
self.discovery.get_providers(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CustomMessageOutcome> for BehaviourOut {
|
||||
fn from(event: CustomMessageOutcome) -> Self {
|
||||
match event {
|
||||
CustomMessageOutcome::NotificationStreamOpened {
|
||||
remote,
|
||||
set_id,
|
||||
direction,
|
||||
negotiated_fallback,
|
||||
received_handshake,
|
||||
notifications_sink,
|
||||
} => BehaviourOut::NotificationStreamOpened {
|
||||
remote,
|
||||
set_id,
|
||||
direction,
|
||||
negotiated_fallback,
|
||||
received_handshake,
|
||||
notifications_sink,
|
||||
},
|
||||
CustomMessageOutcome::NotificationStreamReplaced {
|
||||
remote,
|
||||
set_id,
|
||||
notifications_sink,
|
||||
} => BehaviourOut::NotificationStreamReplaced { remote, set_id, notifications_sink },
|
||||
CustomMessageOutcome::NotificationStreamClosed { remote, set_id } =>
|
||||
BehaviourOut::NotificationStreamClosed { remote, set_id },
|
||||
CustomMessageOutcome::NotificationsReceived { remote, set_id, notification } =>
|
||||
BehaviourOut::NotificationsReceived { remote, set_id, notification },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<request_responses::Event> for BehaviourOut {
|
||||
fn from(event: request_responses::Event) -> Self {
|
||||
match event {
|
||||
request_responses::Event::InboundRequest { protocol, result, .. } =>
|
||||
BehaviourOut::InboundRequest { protocol, result },
|
||||
request_responses::Event::RequestFinished { protocol, duration, result, .. } =>
|
||||
BehaviourOut::RequestFinished { protocol, duration, result },
|
||||
request_responses::Event::ReputationChanges { peer, changes } =>
|
||||
BehaviourOut::ReputationChanges { peer, changes },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<peer_info::PeerInfoEvent> for BehaviourOut {
|
||||
fn from(event: peer_info::PeerInfoEvent) -> Self {
|
||||
let peer_info::PeerInfoEvent::Identified { peer_id, info } = event;
|
||||
BehaviourOut::PeerIdentify { peer_id, info }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiscoveryOut> for BehaviourOut {
|
||||
fn from(event: DiscoveryOut) -> Self {
|
||||
match event {
|
||||
DiscoveryOut::UnroutablePeer(_peer_id) => {
|
||||
// Obtaining and reporting listen addresses for unroutable peers back
|
||||
// to Kademlia is handled by the `Identify` protocol, part of the
|
||||
// `PeerInfoBehaviour`. See the `From<peer_info::PeerInfoEvent>`
|
||||
// implementation.
|
||||
BehaviourOut::None
|
||||
},
|
||||
DiscoveryOut::Discovered(peer_id) => BehaviourOut::Discovered(peer_id),
|
||||
DiscoveryOut::ClosestPeersFound(target, peers, duration) => BehaviourOut::Dht(
|
||||
DhtEvent::ClosestPeersFound(
|
||||
target.into(),
|
||||
peers
|
||||
.into_iter()
|
||||
.map(|(p, addrs)| (p.into(), addrs.into_iter().map(Into::into).collect()))
|
||||
.collect(),
|
||||
),
|
||||
Some(duration),
|
||||
),
|
||||
DiscoveryOut::ClosestPeersNotFound(target, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::ClosestPeersNotFound(target.into()), Some(duration)),
|
||||
DiscoveryOut::ValueFound(results, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::ValueFound(results.into()), Some(duration)),
|
||||
DiscoveryOut::ValueNotFound(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::ValueNotFound(key.into()), Some(duration)),
|
||||
DiscoveryOut::ValuePut(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::ValuePut(key.into()), Some(duration)),
|
||||
DiscoveryOut::PutRecordRequest(record_key, record_value, publisher, expires) =>
|
||||
BehaviourOut::Dht(
|
||||
DhtEvent::PutRecordRequest(record_key.into(), record_value, publisher, expires),
|
||||
None,
|
||||
),
|
||||
DiscoveryOut::ValuePutFailed(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::ValuePutFailed(key.into()), Some(duration)),
|
||||
DiscoveryOut::StartedProviding(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::StartedProviding(key.into()), Some(duration)),
|
||||
DiscoveryOut::StartProvidingFailed(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::StartProvidingFailed(key.into()), Some(duration)),
|
||||
DiscoveryOut::ProvidersFound(key, providers, duration) => BehaviourOut::Dht(
|
||||
DhtEvent::ProvidersFound(
|
||||
key.into(),
|
||||
providers.into_iter().map(Into::into).collect(),
|
||||
),
|
||||
Some(duration),
|
||||
),
|
||||
DiscoveryOut::NoMoreProviders(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::NoMoreProviders(key.into()), Some(duration)),
|
||||
DiscoveryOut::ProvidersNotFound(key, duration) =>
|
||||
BehaviourOut::Dht(DhtEvent::ProvidersNotFound(key.into()), Some(duration)),
|
||||
DiscoveryOut::RandomKademliaStarted => BehaviourOut::RandomKademliaStarted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<void::Void> for BehaviourOut {
|
||||
fn from(e: void::Void) -> Self {
|
||||
void::unreachable(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Bizinikiwi.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// Bizinikiwi 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.
|
||||
|
||||
// Bizinikiwi 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 Bizinikiwi. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Bitswap server for Bizinikiwi.
|
||||
//!
|
||||
//! Allows querying transactions by hash over standard bitswap protocol
|
||||
//! Only supports bitswap 1.2.0.
|
||||
//! CID is expected to reference 256-bit Blake2b transaction hash.
|
||||
|
||||
use crate::{
|
||||
request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
|
||||
types::ProtocolName,
|
||||
MAX_RESPONSE_SIZE,
|
||||
};
|
||||
|
||||
use cid::{self, Version};
|
||||
use futures::StreamExt;
|
||||
use log::{debug, error, trace};
|
||||
use prost::Message;
|
||||
use pezsc_client_api::BlockBackend;
|
||||
use pezsc_network_types::PeerId;
|
||||
use schema::bitswap::{
|
||||
message::{wantlist::WantType, Block as MessageBlock, BlockPresence, BlockPresenceType},
|
||||
Message as BitswapMessage,
|
||||
};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
use std::{io, sync::Arc, time::Duration};
|
||||
use unsigned_varint::encode as varint_encode;
|
||||
|
||||
mod schema;
|
||||
|
||||
const LOG_TARGET: &str = "bitswap";
|
||||
|
||||
// Undocumented, but according to JS the bitswap messages have a max size of 512*1024 bytes
|
||||
// https://github.com/ipfs/js-ipfs-bitswap/blob/
|
||||
// d8f80408aadab94c962f6b88f343eb9f39fa0fcc/src/decision-engine/index.js#L16
|
||||
// We set it to the same value as max bizinikiwi protocol message
|
||||
const MAX_PACKET_SIZE: u64 = MAX_RESPONSE_SIZE;
|
||||
|
||||
/// Max number of queued responses before denying requests.
|
||||
const MAX_REQUEST_QUEUE: usize = 20;
|
||||
|
||||
/// Max number of blocks per wantlist
|
||||
const MAX_WANTED_BLOCKS: usize = 16;
|
||||
|
||||
/// Bitswap protocol name
|
||||
const PROTOCOL_NAME: &'static str = "/ipfs/bitswap/1.2.0";
|
||||
|
||||
/// Prefix represents all metadata of a CID, without the actual content.
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
struct Prefix {
|
||||
/// The version of CID.
|
||||
pub version: Version,
|
||||
/// The codec of CID.
|
||||
pub codec: u64,
|
||||
/// The multihash type of CID.
|
||||
pub mh_type: u64,
|
||||
/// The multihash length of CID.
|
||||
pub mh_len: u8,
|
||||
}
|
||||
|
||||
impl Prefix {
|
||||
/// Convert the prefix to encoded bytes.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut res = Vec::with_capacity(4);
|
||||
let mut buf = varint_encode::u64_buffer();
|
||||
let version = varint_encode::u64(self.version.into(), &mut buf);
|
||||
res.extend_from_slice(version);
|
||||
let mut buf = varint_encode::u64_buffer();
|
||||
let codec = varint_encode::u64(self.codec, &mut buf);
|
||||
res.extend_from_slice(codec);
|
||||
let mut buf = varint_encode::u64_buffer();
|
||||
let mh_type = varint_encode::u64(self.mh_type, &mut buf);
|
||||
res.extend_from_slice(mh_type);
|
||||
let mut buf = varint_encode::u64_buffer();
|
||||
let mh_len = varint_encode::u64(self.mh_len as u64, &mut buf);
|
||||
res.extend_from_slice(mh_len);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Bitswap request handler
|
||||
pub struct BitswapRequestHandler<B> {
|
||||
client: Arc<dyn BlockBackend<B> + Send + Sync>,
|
||||
request_receiver: async_channel::Receiver<IncomingRequest>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> BitswapRequestHandler<B> {
|
||||
/// Create a new [`BitswapRequestHandler`].
|
||||
pub fn new(client: Arc<dyn BlockBackend<B> + Send + Sync>) -> (Self, ProtocolConfig) {
|
||||
let (tx, request_receiver) = async_channel::bounded(MAX_REQUEST_QUEUE);
|
||||
|
||||
let config = ProtocolConfig {
|
||||
name: ProtocolName::from(PROTOCOL_NAME),
|
||||
fallback_names: vec![],
|
||||
max_request_size: MAX_PACKET_SIZE,
|
||||
max_response_size: MAX_PACKET_SIZE,
|
||||
request_timeout: Duration::from_secs(15),
|
||||
inbound_queue: Some(tx),
|
||||
};
|
||||
|
||||
(Self { client, request_receiver }, config)
|
||||
}
|
||||
|
||||
/// Run [`BitswapRequestHandler`].
|
||||
pub async fn run(mut self) {
|
||||
while let Some(request) = self.request_receiver.next().await {
|
||||
let IncomingRequest { peer, payload, pending_response } = request;
|
||||
|
||||
match self.handle_message(&peer, &payload) {
|
||||
Ok(response) => {
|
||||
let response = OutgoingResponse {
|
||||
result: Ok(response),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
};
|
||||
|
||||
match pending_response.send(response) {
|
||||
Ok(()) => {
|
||||
trace!(target: LOG_TARGET, "Handled bitswap request from {peer}.",)
|
||||
},
|
||||
Err(_) => debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to handle light client request from {peer}: {}",
|
||||
BitswapError::SendResponse,
|
||||
),
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!(target: LOG_TARGET, "Failed to process request from {peer}: {err}");
|
||||
|
||||
// TODO: adjust reputation?
|
||||
|
||||
let response = OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes: vec![],
|
||||
sent_feedback: None,
|
||||
};
|
||||
|
||||
if pending_response.send(response).is_err() {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Failed to handle bitswap request from {peer}: {}",
|
||||
BitswapError::SendResponse,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle received Bitswap request
|
||||
fn handle_message(
|
||||
&mut self,
|
||||
peer: &PeerId,
|
||||
payload: &Vec<u8>,
|
||||
) -> Result<Vec<u8>, BitswapError> {
|
||||
let request = schema::bitswap::Message::decode(&payload[..])?;
|
||||
|
||||
trace!(target: LOG_TARGET, "Received request: {:?} from {}", request, peer);
|
||||
|
||||
let mut response = BitswapMessage::default();
|
||||
|
||||
let wantlist = match request.wantlist {
|
||||
Some(wantlist) => wantlist,
|
||||
None => {
|
||||
debug!(target: LOG_TARGET, "Unexpected bitswap message from {}", peer);
|
||||
return Err(BitswapError::InvalidWantList);
|
||||
},
|
||||
};
|
||||
|
||||
if wantlist.entries.len() > MAX_WANTED_BLOCKS {
|
||||
trace!(target: LOG_TARGET, "Ignored request: too many entries");
|
||||
return Err(BitswapError::TooManyEntries);
|
||||
}
|
||||
|
||||
for entry in wantlist.entries {
|
||||
let cid = match cid::Cid::read_bytes(entry.block.as_slice()) {
|
||||
Ok(cid) => cid,
|
||||
Err(e) => {
|
||||
trace!(target: LOG_TARGET, "Bad CID {:?}: {:?}", entry.block, e);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
if cid.version() != cid::Version::V1 ||
|
||||
cid.hash().code() != u64::from(cid::multihash::Code::Blake2b256) ||
|
||||
cid.hash().size() != 32
|
||||
{
|
||||
debug!(target: LOG_TARGET, "Ignoring unsupported CID {}: {}", peer, cid);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut hash = B::Hash::default();
|
||||
hash.as_mut().copy_from_slice(&cid.hash().digest()[0..32]);
|
||||
let transaction = match self.client.indexed_transaction(hash) {
|
||||
Ok(ex) => ex,
|
||||
Err(e) => {
|
||||
error!(target: LOG_TARGET, "Error retrieving transaction {}: {}", hash, e);
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
match transaction {
|
||||
Some(transaction) => {
|
||||
trace!(target: LOG_TARGET, "Found CID {:?}, hash {:?}", cid, hash);
|
||||
|
||||
if entry.want_type == WantType::Block as i32 {
|
||||
let prefix = Prefix {
|
||||
version: cid.version(),
|
||||
codec: cid.codec(),
|
||||
mh_type: cid.hash().code(),
|
||||
mh_len: cid.hash().size(),
|
||||
};
|
||||
response
|
||||
.payload
|
||||
.push(MessageBlock { prefix: prefix.to_bytes(), data: transaction });
|
||||
} else {
|
||||
response.block_presences.push(BlockPresence {
|
||||
r#type: BlockPresenceType::Have as i32,
|
||||
cid: cid.to_bytes(),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
trace!(target: LOG_TARGET, "Missing CID {:?}, hash {:?}", cid, hash);
|
||||
|
||||
if entry.send_dont_have {
|
||||
response.block_presences.push(BlockPresence {
|
||||
r#type: BlockPresenceType::DontHave as i32,
|
||||
cid: cid.to_bytes(),
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response.encode_to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
/// Bitswap protocol error.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BitswapError {
|
||||
/// Protobuf decoding error.
|
||||
#[error("Failed to decode request: {0}.")]
|
||||
DecodeProto(#[from] prost::DecodeError),
|
||||
|
||||
/// Protobuf encoding error.
|
||||
#[error("Failed to encode response: {0}.")]
|
||||
EncodeProto(#[from] prost::EncodeError),
|
||||
|
||||
/// Client backend error.
|
||||
#[error(transparent)]
|
||||
Client(#[from] pezsp_blockchain::Error),
|
||||
|
||||
/// Error parsing CID
|
||||
#[error(transparent)]
|
||||
BadCid(#[from] cid::Error),
|
||||
|
||||
/// Packet read error.
|
||||
#[error(transparent)]
|
||||
Read(#[from] io::Error),
|
||||
|
||||
/// Error sending response.
|
||||
#[error("Failed to send response.")]
|
||||
SendResponse,
|
||||
|
||||
/// Message doesn't have a WANT list.
|
||||
#[error("Invalid WANT list.")]
|
||||
InvalidWantList,
|
||||
|
||||
/// Too many blocks requested.
|
||||
#[error("Too many block entries in the request.")]
|
||||
TooManyEntries,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::channel::oneshot;
|
||||
use pezsc_block_builder::BlockBuilderBuilder;
|
||||
use schema::bitswap::{
|
||||
message::{wantlist::Entry, Wantlist},
|
||||
Message as BitswapMessage,
|
||||
};
|
||||
use pezsp_consensus::BlockOrigin;
|
||||
use pezsp_runtime::codec::Encode;
|
||||
use bizinikiwi_test_runtime::ExtrinsicBuilder;
|
||||
use bizinikiwi_test_runtime_client::{self, prelude::*, TestClientBuilder};
|
||||
|
||||
#[tokio::test]
|
||||
async fn undecodable_message() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
|
||||
|
||||
tokio::spawn(async move { bitswap.run().await });
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
config
|
||||
.inbound_queue
|
||||
.unwrap()
|
||||
.send(IncomingRequest {
|
||||
peer: PeerId::random(),
|
||||
payload: vec![0x13, 0x37, 0x13, 0x38],
|
||||
pending_response: tx,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
|
||||
assert_eq!(result, Err(()));
|
||||
assert_eq!(reputation_changes, Vec::new());
|
||||
assert!(sent_feedback.is_none());
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_want_list() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
let (bitswap, mut config) = BitswapRequestHandler::new(Arc::new(client));
|
||||
|
||||
tokio::spawn(async move { bitswap.run().await });
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
config
|
||||
.inbound_queue
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(IncomingRequest {
|
||||
peer: PeerId::random(),
|
||||
payload: BitswapMessage { wantlist: None, ..Default::default() }.encode_to_vec(),
|
||||
pending_response: tx,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
|
||||
assert_eq!(result, Err(()));
|
||||
assert_eq!(reputation_changes, Vec::new());
|
||||
assert!(sent_feedback.is_none());
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// Empty WANT list should not cause an error
|
||||
let (tx, rx) = oneshot::channel();
|
||||
config
|
||||
.inbound_queue
|
||||
.unwrap()
|
||||
.send(IncomingRequest {
|
||||
peer: PeerId::random(),
|
||||
payload: BitswapMessage {
|
||||
wantlist: Some(Default::default()),
|
||||
..Default::default()
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pending_response: tx,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
|
||||
assert_eq!(result, Ok(BitswapMessage::default().encode_to_vec()));
|
||||
assert_eq!(reputation_changes, Vec::new());
|
||||
assert!(sent_feedback.is_none());
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn too_long_want_list() {
|
||||
let client = bizinikiwi_test_runtime_client::new();
|
||||
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
|
||||
|
||||
tokio::spawn(async move { bitswap.run().await });
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
config
|
||||
.inbound_queue
|
||||
.unwrap()
|
||||
.send(IncomingRequest {
|
||||
peer: PeerId::random(),
|
||||
payload: BitswapMessage {
|
||||
wantlist: Some(Wantlist {
|
||||
entries: (0..MAX_WANTED_BLOCKS + 1)
|
||||
.map(|_| Entry::default())
|
||||
.collect::<Vec<_>>(),
|
||||
full: false,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pending_response: tx,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
|
||||
assert_eq!(result, Err(()));
|
||||
assert_eq!(reputation_changes, Vec::new());
|
||||
assert!(sent_feedback.is_none());
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_not_found() {
|
||||
let client = TestClientBuilder::with_tx_storage(u32::MAX).build();
|
||||
|
||||
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
|
||||
tokio::spawn(async move { bitswap.run().await });
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
config
|
||||
.inbound_queue
|
||||
.unwrap()
|
||||
.send(IncomingRequest {
|
||||
peer: PeerId::random(),
|
||||
payload: BitswapMessage {
|
||||
wantlist: Some(Wantlist {
|
||||
entries: vec![Entry {
|
||||
block: cid::Cid::new_v1(
|
||||
0x70,
|
||||
cid::multihash::Multihash::wrap(
|
||||
u64::from(cid::multihash::Code::Blake2b256),
|
||||
&[0u8; 32],
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.to_bytes(),
|
||||
..Default::default()
|
||||
}],
|
||||
full: false,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pending_response: tx,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
|
||||
assert_eq!(result, Ok(vec![]));
|
||||
assert_eq!(reputation_changes, Vec::new());
|
||||
assert!(sent_feedback.is_none());
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_found() {
|
||||
let client = TestClientBuilder::with_tx_storage(u32::MAX).build();
|
||||
let mut block_builder = BlockBuilderBuilder::new(&client)
|
||||
.on_parent_block(client.chain_info().genesis_hash)
|
||||
.with_parent_block_number(0)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// encoded extrinsic: [161, .. , 2, 6, 16, 19, 55, 19, 56]
|
||||
let ext = ExtrinsicBuilder::new_indexed_call(vec![0x13, 0x37, 0x13, 0x38]).build();
|
||||
let pattern_index = ext.encoded_size() - 4;
|
||||
|
||||
block_builder.push(ext.clone()).unwrap();
|
||||
let block = block_builder.build().unwrap().block;
|
||||
|
||||
client.import(BlockOrigin::File, block).await.unwrap();
|
||||
|
||||
let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
|
||||
|
||||
tokio::spawn(async move { bitswap.run().await });
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
config
|
||||
.inbound_queue
|
||||
.unwrap()
|
||||
.send(IncomingRequest {
|
||||
peer: PeerId::random(),
|
||||
payload: BitswapMessage {
|
||||
wantlist: Some(Wantlist {
|
||||
entries: vec![Entry {
|
||||
block: cid::Cid::new_v1(
|
||||
0x70,
|
||||
cid::multihash::Multihash::wrap(
|
||||
u64::from(cid::multihash::Code::Blake2b256),
|
||||
&pezsp_crypto_hashing::blake2_256(&ext.encode()[pattern_index..]),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.to_bytes(),
|
||||
..Default::default()
|
||||
}],
|
||||
full: false,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.encode_to_vec(),
|
||||
pending_response: tx,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
|
||||
assert_eq!(reputation_changes, Vec::new());
|
||||
assert!(sent_feedback.is_none());
|
||||
|
||||
let response =
|
||||
schema::bitswap::Message::decode(&result.expect("fetch to succeed")[..]).unwrap();
|
||||
assert_eq!(response.payload[0].data, vec![0x13, 0x37, 0x13, 0x38]);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Include sources generated from protobuf definitions.
|
||||
|
||||
pub(crate) mod bitswap {
|
||||
include!(concat!(env!("OUT_DIR"), "/bitswap.message.rs"));
|
||||
}
|
||||
@@ -0,0 +1,997 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Configuration of the networking layer.
|
||||
//!
|
||||
//! The [`Params`] struct is the struct that must be passed in order to initialize the networking.
|
||||
//! See the documentation of [`Params`].
|
||||
|
||||
pub use crate::{
|
||||
discovery::DEFAULT_KADEMLIA_REPLICATION_FACTOR,
|
||||
peer_store::PeerStoreProvider,
|
||||
protocol::{notification_service, NotificationsSink, ProtocolHandlePair},
|
||||
request_responses::{
|
||||
IncomingRequest, OutgoingResponse, ProtocolConfig as RequestResponseConfig,
|
||||
},
|
||||
service::{
|
||||
metrics::NotificationMetrics,
|
||||
traits::{NotificationConfig, NotificationService, PeerStore},
|
||||
},
|
||||
types::ProtocolName,
|
||||
};
|
||||
|
||||
pub use pezsc_network_types::{build_multiaddr, ed25519};
|
||||
use pezsc_network_types::{
|
||||
multiaddr::{self, Multiaddr},
|
||||
PeerId,
|
||||
};
|
||||
|
||||
use crate::service::{ensure_addresses_consistent_with_transport, traits::NetworkBackend};
|
||||
use codec::Encode;
|
||||
use prometheus_endpoint::Registry;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub use pezsc_network_common::{
|
||||
role::{Role, Roles},
|
||||
sync::SyncMode,
|
||||
ExHashT,
|
||||
};
|
||||
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt, fs,
|
||||
future::Future,
|
||||
io::{self, Write},
|
||||
iter,
|
||||
net::Ipv4Addr,
|
||||
num::NonZeroUsize,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
str::{self, FromStr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Default timeout for idle connections of 10 seconds is good enough for most networks.
|
||||
/// It doesn't make sense to expose it as a CLI parameter on individual nodes, but customizations
|
||||
/// are possible in custom nodes through [`NetworkConfiguration`].
|
||||
pub const DEFAULT_IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Maximum number of locally kept Kademlia provider keys.
|
||||
///
|
||||
/// 10000 keys is enough for a testnet with fast runtime (1-minute epoch) and 13 teyrchains.
|
||||
pub const KADEMLIA_MAX_PROVIDER_KEYS: usize = 10000;
|
||||
|
||||
/// Time to keep Kademlia content provider records.
|
||||
///
|
||||
/// 10 h is enough time to keep the teyrchain bootnode record for two 4-hour epochs.
|
||||
pub const KADEMLIA_PROVIDER_RECORD_TTL: Duration = Duration::from_secs(10 * 3600);
|
||||
|
||||
/// Interval of republishing Kademlia provider records.
|
||||
///
|
||||
/// 3.5 h means we refresh next epoch provider record 30 minutes before next 4-hour epoch comes.
|
||||
pub const KADEMLIA_PROVIDER_REPUBLISH_INTERVAL: Duration = Duration::from_secs(12600);
|
||||
|
||||
/// Protocol name prefix, transmitted on the wire for legacy protocol names.
|
||||
/// I.e., `dot` in `/hez/sync/2`. Should be unique for each chain. Always UTF-8.
|
||||
/// Deprecated in favour of genesis hash & fork ID based protocol names.
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ProtocolId(smallvec::SmallVec<[u8; 6]>);
|
||||
|
||||
impl<'a> From<&'a str> for ProtocolId {
|
||||
fn from(bytes: &'a str) -> ProtocolId {
|
||||
Self(bytes.as_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ProtocolId {
|
||||
fn as_ref(&self) -> &str {
|
||||
str::from_utf8(&self.0[..])
|
||||
.expect("the only way to build a ProtocolId is through a UTF-8 String; qed")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ProtocolId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Debug::fmt(self.as_ref(), f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a string address and splits it into Multiaddress and PeerId, if
|
||||
/// valid.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
|
||||
/// use pezsc_network::config::parse_str_addr;
|
||||
/// let (peer_id, addr) = parse_str_addr(
|
||||
/// "/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV"
|
||||
/// ).unwrap();
|
||||
/// assert_eq!(peer_id, "QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV".parse::<PeerId>().unwrap().into());
|
||||
/// assert_eq!(addr, "/ip4/198.51.100.19/tcp/30333".parse::<Multiaddr>().unwrap());
|
||||
/// ```
|
||||
pub fn parse_str_addr(addr_str: &str) -> Result<(PeerId, Multiaddr), ParseErr> {
|
||||
let addr: Multiaddr = addr_str.parse()?;
|
||||
parse_addr(addr)
|
||||
}
|
||||
|
||||
/// Splits a Multiaddress into a Multiaddress and PeerId.
|
||||
pub fn parse_addr(mut addr: Multiaddr) -> Result<(PeerId, Multiaddr), ParseErr> {
|
||||
let multihash = match addr.pop() {
|
||||
Some(multiaddr::Protocol::P2p(multihash)) => multihash,
|
||||
_ => return Err(ParseErr::PeerIdMissing),
|
||||
};
|
||||
let peer_id = PeerId::from_multihash(multihash).map_err(|_| ParseErr::InvalidPeerId)?;
|
||||
|
||||
Ok((peer_id, addr))
|
||||
}
|
||||
|
||||
/// Address of a node, including its identity.
|
||||
///
|
||||
/// This struct represents a decoded version of a multiaddress that ends with `/p2p/<peerid>`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
|
||||
/// use pezsc_network::config::MultiaddrWithPeerId;
|
||||
/// let addr: MultiaddrWithPeerId =
|
||||
/// "/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV".parse().unwrap();
|
||||
/// assert_eq!(addr.peer_id.to_base58(), "QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV");
|
||||
/// assert_eq!(addr.multiaddr.to_string(), "/ip4/198.51.100.19/tcp/30333");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub struct MultiaddrWithPeerId {
|
||||
/// Address of the node.
|
||||
pub multiaddr: Multiaddr,
|
||||
/// Its identity.
|
||||
pub peer_id: PeerId,
|
||||
}
|
||||
|
||||
impl MultiaddrWithPeerId {
|
||||
/// Concatenates the multiaddress and peer ID into one multiaddress containing both.
|
||||
pub fn concat(&self) -> Multiaddr {
|
||||
let proto = multiaddr::Protocol::P2p(From::from(self.peer_id));
|
||||
self.multiaddr.clone().with(proto)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MultiaddrWithPeerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.concat(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MultiaddrWithPeerId {
|
||||
type Err = ParseErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (peer_id, multiaddr) = parse_str_addr(s)?;
|
||||
Ok(Self { peer_id, multiaddr })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MultiaddrWithPeerId> for String {
|
||||
fn from(ma: MultiaddrWithPeerId) -> String {
|
||||
format!("{}", ma)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for MultiaddrWithPeerId {
|
||||
type Error = ParseErr;
|
||||
fn try_from(string: String) -> Result<Self, Self::Error> {
|
||||
string.parse()
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can be generated by `parse_str_addr`.
|
||||
#[derive(Debug)]
|
||||
pub enum ParseErr {
|
||||
/// Error while parsing the multiaddress.
|
||||
MultiaddrParse(multiaddr::ParseError),
|
||||
/// Multihash of the peer ID is invalid.
|
||||
InvalidPeerId,
|
||||
/// The peer ID is missing from the address.
|
||||
PeerIdMissing,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MultiaddrParse(err) => write!(f, "{}", err),
|
||||
Self::InvalidPeerId => write!(f, "Peer id at the end of the address is invalid"),
|
||||
Self::PeerIdMissing => write!(f, "Peer id is missing from the address"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseErr {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::MultiaddrParse(err) => Some(err),
|
||||
Self::InvalidPeerId => None,
|
||||
Self::PeerIdMissing => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<multiaddr::ParseError> for ParseErr {
|
||||
fn from(err: multiaddr::ParseError) -> ParseErr {
|
||||
Self::MultiaddrParse(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom handshake for the notification protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationHandshake(Vec<u8>);
|
||||
|
||||
impl NotificationHandshake {
|
||||
/// Create new `NotificationHandshake` from an object that implements `Encode`
|
||||
pub fn new<H: Encode>(handshake: H) -> Self {
|
||||
Self(handshake.encode())
|
||||
}
|
||||
|
||||
/// Create new `NotificationHandshake` from raw bytes
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NotificationHandshake {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the transport layer.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TransportConfig {
|
||||
/// Normal transport mode.
|
||||
Normal {
|
||||
/// If true, the network will use mDNS to discover other libp2p nodes on the local network
|
||||
/// and connect to them if they support the same chain.
|
||||
enable_mdns: bool,
|
||||
|
||||
/// If true, allow connecting to private IPv4/IPv6 addresses (as defined in
|
||||
/// [RFC1918](https://tools.ietf.org/html/rfc1918)). Irrelevant for addresses that have
|
||||
/// been passed in `::pezsc_network::config::NetworkConfiguration::boot_nodes`.
|
||||
allow_private_ip: bool,
|
||||
},
|
||||
|
||||
/// Only allow connections within the same process.
|
||||
/// Only addresses of the form `/memory/...` will be supported.
|
||||
MemoryOnly,
|
||||
}
|
||||
|
||||
/// The policy for connections to non-reserved peers.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum NonReservedPeerMode {
|
||||
/// Accept them. This is the default.
|
||||
Accept,
|
||||
/// Deny them.
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl NonReservedPeerMode {
|
||||
/// Attempt to parse the peer mode from a string.
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"accept" => Some(Self::Accept),
|
||||
"deny" => Some(Self::Deny),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If we are in "reserved-only" peer mode.
|
||||
pub fn is_reserved_only(&self) -> bool {
|
||||
matches!(self, NonReservedPeerMode::Deny)
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration of a node's secret key, describing the type of key
|
||||
/// and how it is obtained. A node's identity keypair is the result of
|
||||
/// the evaluation of the node key configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NodeKeyConfig {
|
||||
/// A Ed25519 secret key configuration.
|
||||
Ed25519(Secret<ed25519::SecretKey>),
|
||||
}
|
||||
|
||||
impl Default for NodeKeyConfig {
|
||||
fn default() -> NodeKeyConfig {
|
||||
Self::Ed25519(Secret::New)
|
||||
}
|
||||
}
|
||||
|
||||
/// The options for obtaining a Ed25519 secret key.
|
||||
pub type Ed25519Secret = Secret<ed25519::SecretKey>;
|
||||
|
||||
/// The configuration options for obtaining a secret key `K`.
|
||||
#[derive(Clone)]
|
||||
pub enum Secret<K> {
|
||||
/// Use the given secret key `K`.
|
||||
Input(K),
|
||||
/// Read the secret key from a file. If the file does not exist,
|
||||
/// it is created with a newly generated secret key `K`. The format
|
||||
/// of the file is determined by `K`:
|
||||
///
|
||||
/// * `ed25519::SecretKey`: An unencoded 32 bytes Ed25519 secret key.
|
||||
File(PathBuf),
|
||||
/// Always generate a new secret key `K`.
|
||||
New,
|
||||
}
|
||||
|
||||
impl<K> fmt::Debug for Secret<K> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Input(_) => f.debug_tuple("Secret::Input").finish(),
|
||||
Self::File(path) => f.debug_tuple("Secret::File").field(path).finish(),
|
||||
Self::New => f.debug_tuple("Secret::New").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeKeyConfig {
|
||||
/// Evaluate a `NodeKeyConfig` to obtain an identity `Keypair`:
|
||||
///
|
||||
/// * If the secret is configured as input, the corresponding keypair is returned.
|
||||
///
|
||||
/// * If the secret is configured as a file, it is read from that file, if it exists. Otherwise
|
||||
/// a new secret is generated and stored. In either case, the keypair obtained from the
|
||||
/// secret is returned.
|
||||
///
|
||||
/// * If the secret is configured to be new, it is generated and the corresponding keypair is
|
||||
/// returned.
|
||||
pub fn into_keypair(self) -> io::Result<ed25519::Keypair> {
|
||||
use NodeKeyConfig::*;
|
||||
match self {
|
||||
Ed25519(Secret::New) => Ok(ed25519::Keypair::generate()),
|
||||
|
||||
Ed25519(Secret::Input(k)) => Ok(ed25519::Keypair::from(k).into()),
|
||||
|
||||
Ed25519(Secret::File(f)) => get_secret(
|
||||
f,
|
||||
|mut b| match String::from_utf8(b.to_vec()).ok().and_then(|s| {
|
||||
if s.len() == 64 {
|
||||
array_bytes::hex2bytes(&s).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
Some(s) => ed25519::SecretKey::try_from_bytes(s),
|
||||
_ => ed25519::SecretKey::try_from_bytes(&mut b),
|
||||
},
|
||||
ed25519::SecretKey::generate,
|
||||
|b| b.as_ref().to_vec(),
|
||||
)
|
||||
.map(ed25519::Keypair::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a secret key from a file, if it exists, or generate a
|
||||
/// new secret key and write it to that file. In either case,
|
||||
/// the secret key is returned.
|
||||
fn get_secret<P, F, G, E, W, K>(file: P, parse: F, generate: G, serialize: W) -> io::Result<K>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
F: for<'r> FnOnce(&'r mut [u8]) -> Result<K, E>,
|
||||
G: FnOnce() -> K,
|
||||
E: Error + Send + Sync + 'static,
|
||||
W: Fn(&K) -> Vec<u8>,
|
||||
{
|
||||
std::fs::read(&file)
|
||||
.and_then(|mut sk_bytes| {
|
||||
parse(&mut sk_bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
})
|
||||
.or_else(|e| {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
file.as_ref().parent().map_or(Ok(()), fs::create_dir_all)?;
|
||||
let sk = generate();
|
||||
let mut sk_vec = serialize(&sk);
|
||||
write_secret_file(file, &sk_vec)?;
|
||||
sk_vec.zeroize();
|
||||
Ok(sk)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Write secret bytes to a file.
|
||||
fn write_secret_file<P>(path: P, sk_bytes: &[u8]) -> io::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut file = open_secret_file(&path)?;
|
||||
file.write_all(sk_bytes)
|
||||
}
|
||||
|
||||
/// Opens a file containing a secret key in write mode.
|
||||
#[cfg(unix)]
|
||||
fn open_secret_file<P>(path: P) -> io::Result<fs::File>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path)
|
||||
}
|
||||
|
||||
/// Opens a file containing a secret key in write mode.
|
||||
#[cfg(not(unix))]
|
||||
fn open_secret_file<P>(path: P) -> Result<fs::File, io::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fs::OpenOptions::new().write(true).create_new(true).open(path)
|
||||
}
|
||||
|
||||
/// Configuration for a set of nodes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SetConfig {
|
||||
/// Maximum allowed number of incoming substreams related to this set.
|
||||
pub in_peers: u32,
|
||||
|
||||
/// Number of outgoing substreams related to this set that we're trying to maintain.
|
||||
pub out_peers: u32,
|
||||
|
||||
/// List of reserved node addresses.
|
||||
pub reserved_nodes: Vec<MultiaddrWithPeerId>,
|
||||
|
||||
/// Whether nodes that aren't in [`SetConfig::reserved_nodes`] are accepted or automatically
|
||||
/// refused.
|
||||
pub non_reserved_mode: NonReservedPeerMode,
|
||||
}
|
||||
|
||||
impl Default for SetConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
in_peers: 25,
|
||||
out_peers: 75,
|
||||
reserved_nodes: Vec::new(),
|
||||
non_reserved_mode: NonReservedPeerMode::Accept,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to [`SetConfig`] for sets that aren't the default set.
|
||||
///
|
||||
/// > **Note**: As new fields might be added in the future, please consider using the `new` method
|
||||
/// > and modifiers instead of creating this struct manually.
|
||||
#[derive(Debug)]
|
||||
pub struct NonDefaultSetConfig {
|
||||
/// Name of the notifications protocols of this set. A substream on this set will be
|
||||
/// considered established once this protocol is open.
|
||||
///
|
||||
/// > **Note**: This field isn't present for the default set, as this is handled internally
|
||||
/// > by the networking code.
|
||||
protocol_name: ProtocolName,
|
||||
|
||||
/// If the remote reports that it doesn't support the protocol indicated in the
|
||||
/// `notifications_protocol` field, then each of these fallback names will be tried one by
|
||||
/// one.
|
||||
///
|
||||
/// If a fallback is used, it will be reported in
|
||||
/// `pezsc_network::protocol::event::Event::NotificationStreamOpened::negotiated_fallback`
|
||||
fallback_names: Vec<ProtocolName>,
|
||||
|
||||
/// Handshake of the protocol
|
||||
///
|
||||
/// NOTE: Currently custom handshakes are not fully supported. See issue #5685 for more
|
||||
/// details. This field is temporarily used to allow moving the hardcoded block announcement
|
||||
/// protocol out of `protocol.rs`.
|
||||
handshake: Option<NotificationHandshake>,
|
||||
|
||||
/// Maximum allowed size of single notifications.
|
||||
max_notification_size: u64,
|
||||
|
||||
/// Base configuration.
|
||||
set_config: SetConfig,
|
||||
|
||||
/// Notification handle.
|
||||
///
|
||||
/// Notification handle is created during `NonDefaultSetConfig` creation and its other half,
|
||||
/// `Box<dyn NotificationService>` is given to the protocol created the config and
|
||||
/// `ProtocolHandle` is given to `Notifications` when it initializes itself. This handle allows
|
||||
/// `Notifications ` to communicate with the protocol directly without relaying events through
|
||||
/// `sc-network.`
|
||||
protocol_handle_pair: ProtocolHandlePair,
|
||||
}
|
||||
|
||||
impl NonDefaultSetConfig {
|
||||
/// Creates a new [`NonDefaultSetConfig`]. Zero slots and accepts only reserved nodes.
|
||||
/// Also returns an object which allows the protocol to communicate with `Notifications`.
|
||||
pub fn new(
|
||||
protocol_name: ProtocolName,
|
||||
fallback_names: Vec<ProtocolName>,
|
||||
max_notification_size: u64,
|
||||
handshake: Option<NotificationHandshake>,
|
||||
set_config: SetConfig,
|
||||
) -> (Self, Box<dyn NotificationService>) {
|
||||
let (protocol_handle_pair, notification_service) =
|
||||
notification_service(protocol_name.clone());
|
||||
(
|
||||
Self {
|
||||
protocol_name,
|
||||
max_notification_size,
|
||||
fallback_names,
|
||||
handshake,
|
||||
set_config,
|
||||
protocol_handle_pair,
|
||||
},
|
||||
notification_service,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get reference to protocol name.
|
||||
pub fn protocol_name(&self) -> &ProtocolName {
|
||||
&self.protocol_name
|
||||
}
|
||||
|
||||
/// Get reference to fallback protocol names.
|
||||
pub fn fallback_names(&self) -> impl Iterator<Item = &ProtocolName> {
|
||||
self.fallback_names.iter()
|
||||
}
|
||||
|
||||
/// Get reference to handshake.
|
||||
pub fn handshake(&self) -> &Option<NotificationHandshake> {
|
||||
&self.handshake
|
||||
}
|
||||
|
||||
/// Get maximum notification size.
|
||||
pub fn max_notification_size(&self) -> u64 {
|
||||
self.max_notification_size
|
||||
}
|
||||
|
||||
/// Get reference to `SetConfig`.
|
||||
pub fn set_config(&self) -> &SetConfig {
|
||||
&self.set_config
|
||||
}
|
||||
|
||||
/// Take `ProtocolHandlePair` from `NonDefaultSetConfig`
|
||||
pub fn take_protocol_handle(self) -> ProtocolHandlePair {
|
||||
self.protocol_handle_pair
|
||||
}
|
||||
|
||||
/// Modifies the configuration to allow non-reserved nodes.
|
||||
pub fn allow_non_reserved(&mut self, in_peers: u32, out_peers: u32) {
|
||||
self.set_config.in_peers = in_peers;
|
||||
self.set_config.out_peers = out_peers;
|
||||
self.set_config.non_reserved_mode = NonReservedPeerMode::Accept;
|
||||
}
|
||||
|
||||
/// Add a node to the list of reserved nodes.
|
||||
pub fn add_reserved(&mut self, peer: MultiaddrWithPeerId) {
|
||||
self.set_config.reserved_nodes.push(peer);
|
||||
}
|
||||
|
||||
/// Add a list of protocol names used for backward compatibility.
|
||||
///
|
||||
/// See the explanations in [`NonDefaultSetConfig::fallback_names`].
|
||||
pub fn add_fallback_names(&mut self, fallback_names: Vec<ProtocolName>) {
|
||||
self.fallback_names.extend(fallback_names);
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationConfig for NonDefaultSetConfig {
|
||||
fn set_config(&self) -> &SetConfig {
|
||||
&self.set_config
|
||||
}
|
||||
|
||||
/// Get reference to protocol name.
|
||||
fn protocol_name(&self) -> &ProtocolName {
|
||||
&self.protocol_name
|
||||
}
|
||||
}
|
||||
|
||||
/// Network service configuration.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NetworkConfiguration {
|
||||
/// Directory path to store network-specific configuration. None means nothing will be saved.
|
||||
pub net_config_path: Option<PathBuf>,
|
||||
|
||||
/// Multiaddresses to listen for incoming connections.
|
||||
pub listen_addresses: Vec<Multiaddr>,
|
||||
|
||||
/// Multiaddresses to advertise. Detected automatically if empty.
|
||||
pub public_addresses: Vec<Multiaddr>,
|
||||
|
||||
/// List of initial node addresses
|
||||
pub boot_nodes: Vec<MultiaddrWithPeerId>,
|
||||
|
||||
/// The node key configuration, which determines the node's network identity keypair.
|
||||
pub node_key: NodeKeyConfig,
|
||||
|
||||
/// Configuration for the default set of nodes used for block syncing and transactions.
|
||||
pub default_peers_set: SetConfig,
|
||||
|
||||
/// Number of substreams to reserve for full nodes for block syncing and transactions.
|
||||
/// Any other slot will be dedicated to light nodes.
|
||||
///
|
||||
/// This value is implicitly capped to `default_set.out_peers + default_set.in_peers`.
|
||||
pub default_peers_set_num_full: u32,
|
||||
|
||||
/// Client identifier. Sent over the wire for debugging purposes.
|
||||
pub client_version: String,
|
||||
|
||||
/// Name of the node. Sent over the wire for debugging purposes.
|
||||
pub node_name: String,
|
||||
|
||||
/// Configuration for the transport layer.
|
||||
pub transport: TransportConfig,
|
||||
|
||||
/// Idle connection timeout.
|
||||
///
|
||||
/// Set by default to [`DEFAULT_IDLE_CONNECTION_TIMEOUT`].
|
||||
pub idle_connection_timeout: Duration,
|
||||
|
||||
/// Maximum number of peers to ask the same blocks in parallel.
|
||||
pub max_parallel_downloads: u32,
|
||||
|
||||
/// Maximum number of blocks per request.
|
||||
pub max_blocks_per_request: u32,
|
||||
|
||||
/// Number of peers that need to be connected before warp sync is started.
|
||||
pub min_peers_to_start_warp_sync: Option<usize>,
|
||||
|
||||
/// Initial syncing mode.
|
||||
pub sync_mode: SyncMode,
|
||||
|
||||
/// True if Kademlia random discovery should be enabled.
|
||||
///
|
||||
/// If true, the node will automatically randomly walk the DHT in order to find new peers.
|
||||
pub enable_dht_random_walk: bool,
|
||||
|
||||
/// Should we insert non-global addresses into the DHT?
|
||||
pub allow_non_globals_in_dht: bool,
|
||||
|
||||
/// Require iterative Kademlia DHT queries to use disjoint paths for increased resiliency in
|
||||
/// the presence of potentially adversarial nodes.
|
||||
pub kademlia_disjoint_query_paths: bool,
|
||||
|
||||
/// Kademlia replication factor determines to how many closest peers a record is replicated to.
|
||||
///
|
||||
/// Discovery mechanism requires successful replication to all
|
||||
/// `kademlia_replication_factor` peers to consider record successfully put.
|
||||
pub kademlia_replication_factor: NonZeroUsize,
|
||||
|
||||
/// Enable serving block data over IPFS bitswap.
|
||||
pub ipfs_server: bool,
|
||||
|
||||
/// Networking backend used for P2P communication.
|
||||
pub network_backend: NetworkBackendType,
|
||||
}
|
||||
|
||||
impl NetworkConfiguration {
|
||||
/// Create new default configuration
|
||||
pub fn new<SN: Into<String>, SV: Into<String>>(
|
||||
node_name: SN,
|
||||
client_version: SV,
|
||||
node_key: NodeKeyConfig,
|
||||
net_config_path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
let default_peers_set = SetConfig::default();
|
||||
Self {
|
||||
net_config_path,
|
||||
listen_addresses: Vec::new(),
|
||||
public_addresses: Vec::new(),
|
||||
boot_nodes: Vec::new(),
|
||||
node_key,
|
||||
default_peers_set_num_full: default_peers_set.in_peers + default_peers_set.out_peers,
|
||||
default_peers_set,
|
||||
client_version: client_version.into(),
|
||||
node_name: node_name.into(),
|
||||
transport: TransportConfig::Normal { enable_mdns: false, allow_private_ip: true },
|
||||
idle_connection_timeout: DEFAULT_IDLE_CONNECTION_TIMEOUT,
|
||||
max_parallel_downloads: 5,
|
||||
max_blocks_per_request: 64,
|
||||
min_peers_to_start_warp_sync: None,
|
||||
sync_mode: SyncMode::Full,
|
||||
enable_dht_random_walk: true,
|
||||
allow_non_globals_in_dht: false,
|
||||
kademlia_disjoint_query_paths: false,
|
||||
kademlia_replication_factor: NonZeroUsize::new(DEFAULT_KADEMLIA_REPLICATION_FACTOR)
|
||||
.expect("value is a constant; constant is non-zero; qed."),
|
||||
ipfs_server: false,
|
||||
network_backend: NetworkBackendType::Litep2p,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new default configuration for localhost-only connection with random port (useful for
|
||||
/// testing)
|
||||
pub fn new_local() -> NetworkConfiguration {
|
||||
let mut config =
|
||||
NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
|
||||
|
||||
config.listen_addresses =
|
||||
vec![iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
|
||||
.chain(iter::once(multiaddr::Protocol::Tcp(0)))
|
||||
.collect()];
|
||||
|
||||
config.allow_non_globals_in_dht = true;
|
||||
config
|
||||
}
|
||||
|
||||
/// Create new default configuration for localhost-only connection with random port (useful for
|
||||
/// testing)
|
||||
pub fn new_memory() -> NetworkConfiguration {
|
||||
let mut config =
|
||||
NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
|
||||
|
||||
config.listen_addresses =
|
||||
vec![iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
|
||||
.chain(iter::once(multiaddr::Protocol::Tcp(0)))
|
||||
.collect()];
|
||||
|
||||
config.allow_non_globals_in_dht = true;
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Network initialization parameters.
|
||||
pub struct Params<Block: BlockT, H: ExHashT, N: NetworkBackend<Block, H>> {
|
||||
/// Assigned role for our node (full, light, ...).
|
||||
pub role: Role,
|
||||
|
||||
/// How to spawn background tasks.
|
||||
pub executor: Box<dyn Fn(Pin<Box<dyn Future<Output = ()> + Send>>) + Send + Sync>,
|
||||
|
||||
/// Network layer configuration.
|
||||
pub network_config: FullNetworkConfiguration<Block, H, N>,
|
||||
|
||||
/// Legacy name of the protocol to use on the wire. Should be different for each chain.
|
||||
pub protocol_id: ProtocolId,
|
||||
|
||||
/// Genesis hash of the chain
|
||||
pub genesis_hash: Block::Hash,
|
||||
|
||||
/// Fork ID to distinguish protocols of different hard forks. Part of the standard protocol
|
||||
/// name on the wire.
|
||||
pub fork_id: Option<String>,
|
||||
|
||||
/// Registry for recording prometheus metrics to.
|
||||
pub metrics_registry: Option<Registry>,
|
||||
|
||||
/// Block announce protocol configuration
|
||||
pub block_announce_config: N::NotificationProtocolConfig,
|
||||
|
||||
/// Bitswap configuration, if the server has been enabled.
|
||||
pub bitswap_config: Option<N::BitswapConfig>,
|
||||
|
||||
/// Notification metrics.
|
||||
pub notification_metrics: NotificationMetrics,
|
||||
}
|
||||
|
||||
/// Full network configuration.
|
||||
pub struct FullNetworkConfiguration<B: BlockT + 'static, H: ExHashT, N: NetworkBackend<B, H>> {
|
||||
/// Installed notification protocols.
|
||||
pub(crate) notification_protocols: Vec<N::NotificationProtocolConfig>,
|
||||
|
||||
/// List of request-response protocols that the node supports.
|
||||
pub(crate) request_response_protocols: Vec<N::RequestResponseProtocolConfig>,
|
||||
|
||||
/// Network configuration.
|
||||
pub network_config: NetworkConfiguration,
|
||||
|
||||
/// [`PeerStore`](crate::peer_store::PeerStore),
|
||||
peer_store: Option<N::PeerStore>,
|
||||
|
||||
/// Handle to [`PeerStore`](crate::peer_store::PeerStore).
|
||||
peer_store_handle: Arc<dyn PeerStoreProvider>,
|
||||
|
||||
/// Registry for recording prometheus metrics to.
|
||||
pub metrics_registry: Option<Registry>,
|
||||
}
|
||||
|
||||
impl<B: BlockT + 'static, H: ExHashT, N: NetworkBackend<B, H>> FullNetworkConfiguration<B, H, N> {
|
||||
/// Create new [`FullNetworkConfiguration`].
|
||||
pub fn new(network_config: &NetworkConfiguration, metrics_registry: Option<Registry>) -> Self {
|
||||
let bootnodes = network_config.boot_nodes.iter().map(|bootnode| bootnode.peer_id).collect();
|
||||
let peer_store = N::peer_store(bootnodes, metrics_registry.clone());
|
||||
let peer_store_handle = peer_store.handle();
|
||||
|
||||
Self {
|
||||
peer_store: Some(peer_store),
|
||||
peer_store_handle,
|
||||
notification_protocols: Vec::new(),
|
||||
request_response_protocols: Vec::new(),
|
||||
network_config: network_config.clone(),
|
||||
metrics_registry,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a notification protocol.
|
||||
pub fn add_notification_protocol(&mut self, config: N::NotificationProtocolConfig) {
|
||||
self.notification_protocols.push(config);
|
||||
}
|
||||
|
||||
/// Get reference to installed notification protocols.
|
||||
pub fn notification_protocols(&self) -> &Vec<N::NotificationProtocolConfig> {
|
||||
&self.notification_protocols
|
||||
}
|
||||
|
||||
/// Add a request-response protocol.
|
||||
pub fn add_request_response_protocol(&mut self, config: N::RequestResponseProtocolConfig) {
|
||||
self.request_response_protocols.push(config);
|
||||
}
|
||||
|
||||
/// Get handle to [`PeerStore`].
|
||||
pub fn peer_store_handle(&self) -> Arc<dyn PeerStoreProvider> {
|
||||
Arc::clone(&self.peer_store_handle)
|
||||
}
|
||||
|
||||
/// Take [`PeerStore`].
|
||||
///
|
||||
/// `PeerStore` is created when `FullNetworkConfig` is initialized so that `PeerStoreHandle`s
|
||||
/// can be passed onto notification protocols. `PeerStore` itself should be started only once
|
||||
/// and since technically it's not a libp2p task, it should be started with `SpawnHandle` in
|
||||
/// `builder.rs` instead of using the libp2p/litep2p executor in the networking backend. This
|
||||
/// function consumes `PeerStore` and starts its event loop in the appropriate place.
|
||||
pub fn take_peer_store(&mut self) -> N::PeerStore {
|
||||
self.peer_store
|
||||
.take()
|
||||
.expect("`PeerStore` can only be taken once when it's started; qed")
|
||||
}
|
||||
|
||||
/// Verify addresses are consistent with enabled transports.
|
||||
pub fn sanity_check_addresses(&self) -> Result<(), crate::error::Error> {
|
||||
ensure_addresses_consistent_with_transport(
|
||||
self.network_config.listen_addresses.iter(),
|
||||
&self.network_config.transport,
|
||||
)?;
|
||||
ensure_addresses_consistent_with_transport(
|
||||
self.network_config.boot_nodes.iter().map(|x| &x.multiaddr),
|
||||
&self.network_config.transport,
|
||||
)?;
|
||||
ensure_addresses_consistent_with_transport(
|
||||
self.network_config
|
||||
.default_peers_set
|
||||
.reserved_nodes
|
||||
.iter()
|
||||
.map(|x| &x.multiaddr),
|
||||
&self.network_config.transport,
|
||||
)?;
|
||||
|
||||
for notification_protocol in &self.notification_protocols {
|
||||
ensure_addresses_consistent_with_transport(
|
||||
notification_protocol.set_config().reserved_nodes.iter().map(|x| &x.multiaddr),
|
||||
&self.network_config.transport,
|
||||
)?;
|
||||
}
|
||||
ensure_addresses_consistent_with_transport(
|
||||
self.network_config.public_addresses.iter(),
|
||||
&self.network_config.transport,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check for duplicate bootnodes.
|
||||
pub fn sanity_check_bootnodes(&self) -> Result<(), crate::error::Error> {
|
||||
self.network_config.boot_nodes.iter().try_for_each(|bootnode| {
|
||||
if let Some(other) = self
|
||||
.network_config
|
||||
.boot_nodes
|
||||
.iter()
|
||||
.filter(|o| o.multiaddr == bootnode.multiaddr)
|
||||
.find(|o| o.peer_id != bootnode.peer_id)
|
||||
{
|
||||
Err(crate::error::Error::DuplicateBootnode {
|
||||
address: bootnode.multiaddr.clone().into(),
|
||||
first_id: bootnode.peer_id.into(),
|
||||
second_id: other.peer_id.into(),
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Collect all reserved nodes and bootnodes addresses.
|
||||
pub fn known_addresses(&self) -> Vec<(PeerId, Multiaddr)> {
|
||||
let mut addresses: Vec<_> = self
|
||||
.network_config
|
||||
.default_peers_set
|
||||
.reserved_nodes
|
||||
.iter()
|
||||
.map(|reserved| (reserved.peer_id, reserved.multiaddr.clone()))
|
||||
.chain(self.notification_protocols.iter().flat_map(|protocol| {
|
||||
protocol
|
||||
.set_config()
|
||||
.reserved_nodes
|
||||
.iter()
|
||||
.map(|reserved| (reserved.peer_id, reserved.multiaddr.clone()))
|
||||
}))
|
||||
.chain(
|
||||
self.network_config
|
||||
.boot_nodes
|
||||
.iter()
|
||||
.map(|bootnode| (bootnode.peer_id, bootnode.multiaddr.clone())),
|
||||
)
|
||||
.collect();
|
||||
|
||||
// Remove possible duplicates.
|
||||
addresses.sort();
|
||||
addresses.dedup();
|
||||
|
||||
addresses
|
||||
}
|
||||
}
|
||||
|
||||
/// Network backend type.
|
||||
#[derive(Debug, Clone, Default, Copy)]
|
||||
pub enum NetworkBackendType {
|
||||
/// Use litep2p for P2P networking.
|
||||
///
|
||||
/// This is the preferred option for Bizinikiwi-based chains.
|
||||
#[default]
|
||||
Litep2p,
|
||||
|
||||
/// Use libp2p for P2P networking.
|
||||
///
|
||||
/// The libp2p is still used for compatibility reasons until the
|
||||
/// ecosystem switches entirely to litep2p. The backend will enter
|
||||
/// a "best-effort" maintenance mode, where only critical issues will
|
||||
/// get fixed. If you are unsure, please use `NetworkBackendType::Litep2p`.
|
||||
Libp2p,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn tempdir_with_prefix(prefix: &str) -> TempDir {
|
||||
tempfile::Builder::new().prefix(prefix).tempdir().unwrap()
|
||||
}
|
||||
|
||||
fn secret_bytes(kp: ed25519::Keypair) -> Vec<u8> {
|
||||
kp.secret().to_bytes().into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_secret_file() {
|
||||
let tmp = tempdir_with_prefix("x");
|
||||
std::fs::remove_dir(tmp.path()).unwrap(); // should be recreated
|
||||
let file = tmp.path().join("x").to_path_buf();
|
||||
let kp1 = NodeKeyConfig::Ed25519(Secret::File(file.clone())).into_keypair().unwrap();
|
||||
let kp2 = NodeKeyConfig::Ed25519(Secret::File(file.clone())).into_keypair().unwrap();
|
||||
assert!(file.is_file() && secret_bytes(kp1) == secret_bytes(kp2))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_secret_input() {
|
||||
let sk = ed25519::SecretKey::generate();
|
||||
let kp1 = NodeKeyConfig::Ed25519(Secret::Input(sk.clone())).into_keypair().unwrap();
|
||||
let kp2 = NodeKeyConfig::Ed25519(Secret::Input(sk)).into_keypair().unwrap();
|
||||
assert!(secret_bytes(kp1) == secret_bytes(kp2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_secret_new() {
|
||||
let kp1 = NodeKeyConfig::Ed25519(Secret::New).into_keypair().unwrap();
|
||||
let kp2 = NodeKeyConfig::Ed25519(Secret::New).into_keypair().unwrap();
|
||||
assert!(secret_bytes(kp1) != secret_bytes(kp2));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Bizinikiwi network possible errors.
|
||||
|
||||
use crate::{config::TransportConfig, types::ProtocolName};
|
||||
|
||||
use pezsc_network_types::{multiaddr::Multiaddr, PeerId};
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Result type alias for the network.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Error type for the network.
|
||||
#[derive(thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Io error
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Client error
|
||||
#[error(transparent)]
|
||||
Client(#[from] Box<pezsp_blockchain::Error>),
|
||||
/// The same bootnode (based on address) is registered with two different peer ids.
|
||||
#[error(
|
||||
"The same bootnode (`{address}`) is registered with two different peer ids: `{first_id}` and `{second_id}`"
|
||||
)]
|
||||
DuplicateBootnode {
|
||||
/// The address of the bootnode.
|
||||
address: Multiaddr,
|
||||
/// The first peer id that was found for the bootnode.
|
||||
first_id: PeerId,
|
||||
/// The second peer id that was found for the bootnode.
|
||||
second_id: PeerId,
|
||||
},
|
||||
/// Prometheus metrics error.
|
||||
#[error(transparent)]
|
||||
Prometheus(#[from] prometheus_endpoint::PrometheusError),
|
||||
/// The network addresses are invalid because they don't match the transport.
|
||||
#[error(
|
||||
"The following addresses are invalid because they don't match the transport: {addresses:?}"
|
||||
)]
|
||||
AddressesForAnotherTransport {
|
||||
/// Transport used.
|
||||
transport: TransportConfig,
|
||||
/// The invalid addresses.
|
||||
addresses: Vec<Multiaddr>,
|
||||
},
|
||||
/// The same request-response protocol has been registered multiple times.
|
||||
#[error("Request-response protocol registered multiple times: {protocol}")]
|
||||
DuplicateRequestResponseProtocol {
|
||||
/// Name of the protocol registered multiple times.
|
||||
protocol: ProtocolName,
|
||||
},
|
||||
/// Peer does not exist.
|
||||
#[error("Peer `{0}` does not exist.")]
|
||||
PeerDoesntExist(PeerId),
|
||||
/// Channel closed.
|
||||
#[error("Channel closed")]
|
||||
ChannelClosed,
|
||||
/// Connection closed.
|
||||
#[error("Connection closed")]
|
||||
ConnectionClosed,
|
||||
/// Litep2p error.
|
||||
#[error("Litep2p error: `{0}`")]
|
||||
Litep2p(litep2p::Error),
|
||||
}
|
||||
|
||||
// Make `Debug` use the `Display` implementation.
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Network event types. These are not the part of the protocol, but rather
|
||||
//! events that happen on the network like DHT get/put results received.
|
||||
|
||||
use crate::types::ProtocolName;
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use pezsc_network_common::role::ObservedRole;
|
||||
use pezsc_network_types::{
|
||||
kad::{Key, PeerRecord},
|
||||
multiaddr::Multiaddr,
|
||||
PeerId,
|
||||
};
|
||||
|
||||
/// Events generated by DHT as a response to get_value and put_value requests.
|
||||
#[derive(Debug, Clone)]
|
||||
#[must_use]
|
||||
pub enum DhtEvent {
|
||||
/// Found closest peers to the target `PeerId`. With libp2p also delivers a partial result
|
||||
/// in case the query timed out, because it can contain the target peer's address.
|
||||
ClosestPeersFound(PeerId, Vec<(PeerId, Vec<Multiaddr>)>),
|
||||
|
||||
/// Closest peers to the target `PeerId` has not been found.
|
||||
ClosestPeersNotFound(PeerId),
|
||||
|
||||
/// The value was found.
|
||||
ValueFound(PeerRecord),
|
||||
|
||||
/// The requested record has not been found in the DHT.
|
||||
ValueNotFound(Key),
|
||||
|
||||
/// The record has been successfully inserted into the DHT.
|
||||
ValuePut(Key),
|
||||
|
||||
/// An error has occurred while putting a record into the DHT.
|
||||
ValuePutFailed(Key),
|
||||
|
||||
/// Successfully started providing the given key.
|
||||
StartedProviding(Key),
|
||||
|
||||
/// An error occured while registering as a content provider on the DHT.
|
||||
StartProvidingFailed(Key),
|
||||
|
||||
/// The DHT received a put record request.
|
||||
PutRecordRequest(Key, Vec<u8>, Option<PeerId>, Option<std::time::Instant>),
|
||||
|
||||
/// The providers for [`Key`] were found. Multiple such events can be generated per provider
|
||||
/// discovery request.
|
||||
ProvidersFound(Key, Vec<PeerId>),
|
||||
|
||||
/// `GET_PROVIDERS` query finished and won't yield any more providers.
|
||||
NoMoreProviders(Key),
|
||||
|
||||
/// `GET_PROVIDERS` query failed and no providers for [`Key`] were found. libp2p also emits
|
||||
/// this event after already yielding some results via [`DhtEvent::ProvidersFound`].
|
||||
ProvidersNotFound(Key),
|
||||
}
|
||||
|
||||
/// Type for events generated by networking layer.
|
||||
#[derive(Debug, Clone)]
|
||||
#[must_use]
|
||||
pub enum Event {
|
||||
/// Event generated by a DHT.
|
||||
Dht(DhtEvent),
|
||||
|
||||
/// Opened a substream with the given node with the given notifications protocol.
|
||||
///
|
||||
/// The protocol is always one of the notification protocols that have been registered.
|
||||
NotificationStreamOpened {
|
||||
/// Node we opened the substream with.
|
||||
remote: PeerId,
|
||||
/// The concerned protocol. Each protocol uses a different substream.
|
||||
/// This is always equal to the value of
|
||||
/// `pezsc_network::config::NonDefaultSetConfig::notifications_protocol` of one of the
|
||||
/// configured sets.
|
||||
protocol: ProtocolName,
|
||||
/// If the negotiation didn't use the main name of the protocol (the one in
|
||||
/// `notifications_protocol`), then this field contains which name has actually been
|
||||
/// used.
|
||||
/// Always contains a value equal to the value in
|
||||
/// `pezsc_network::config::NonDefaultSetConfig::fallback_names`.
|
||||
negotiated_fallback: Option<ProtocolName>,
|
||||
/// Role of the remote.
|
||||
role: ObservedRole,
|
||||
/// Received handshake.
|
||||
received_handshake: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Closed a substream with the given node. Always matches a corresponding previous
|
||||
/// `NotificationStreamOpened` message.
|
||||
NotificationStreamClosed {
|
||||
/// Node we closed the substream with.
|
||||
remote: PeerId,
|
||||
/// The concerned protocol. Each protocol uses a different substream.
|
||||
protocol: ProtocolName,
|
||||
},
|
||||
|
||||
/// Received one or more messages from the given node using the given protocol.
|
||||
NotificationsReceived {
|
||||
/// Node we received the message from.
|
||||
remote: PeerId,
|
||||
/// Concerned protocol and associated message.
|
||||
messages: Vec<(ProtocolName, Bytes)>,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#![warn(unused_extern_crates)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! Bizinikiwi-specific P2P networking.
|
||||
//!
|
||||
//! **Important**: This crate is unstable and the API and usage may change.
|
||||
//!
|
||||
//! # Node identities and addresses
|
||||
//!
|
||||
//! In a decentralized network, each node possesses a network private key and a network public key.
|
||||
//! In Bizinikiwi, the keys are based on the ed25519 curve.
|
||||
//!
|
||||
//! From a node's public key, we can derive its *identity*. In Bizinikiwi and libp2p, a node's
|
||||
//! identity is represented with the [`PeerId`] struct. All network communications between nodes on
|
||||
//! the network use encryption derived from both sides's keys, which means that **identities cannot
|
||||
//! be faked**.
|
||||
//!
|
||||
//! A node's identity uniquely identifies a machine on the network. If you start two or more
|
||||
//! clients using the same network key, large interferences will happen.
|
||||
//!
|
||||
//! # Bizinikiwi's network protocol
|
||||
//!
|
||||
//! Bizinikiwi's networking protocol is based upon libp2p. It is at the moment not possible and not
|
||||
//! planned to permit using something else than the libp2p network stack and the rust-libp2p
|
||||
//! library. However the libp2p framework is very flexible and the rust-libp2p library could be
|
||||
//! extended to support a wider range of protocols than what is offered by libp2p.
|
||||
//!
|
||||
//! ## Discovery mechanisms
|
||||
//!
|
||||
//! In order for our node to join a peer-to-peer network, it has to know a list of nodes that are
|
||||
//! part of said network. This includes nodes identities and their address (how to reach them).
|
||||
//! Building such a list is called the **discovery** mechanism. There are three mechanisms that
|
||||
//! Bizinikiwi uses:
|
||||
//!
|
||||
//! - Bootstrap nodes. These are hard-coded node identities and addresses passed alongside with
|
||||
//! the network configuration.
|
||||
//! - mDNS. We perform a UDP broadcast on the local network. Nodes that listen may respond with
|
||||
//! their identity. More info [here](https://github.com/libp2p/specs/blob/master/discovery/mdns.md).
|
||||
//! mDNS can be disabled in the network configuration.
|
||||
//! - Kademlia random walk. Once connected, we perform random Kademlia `FIND_NODE` requests on the
|
||||
//! configured Kademlia DHTs (one per configured chain protocol) in order for nodes to propagate to
|
||||
//! us their view of the network. More information about Kademlia can be found [on
|
||||
//! Wikipedia](https://en.wikipedia.org/wiki/Kademlia).
|
||||
//!
|
||||
//! ## Connection establishment
|
||||
//!
|
||||
//! When node Alice knows node Bob's identity and address, it can establish a connection with Bob.
|
||||
//! All connections must always use encryption and multiplexing. While some node addresses (eg.
|
||||
//! addresses using `/quic`) already imply which encryption and/or multiplexing to use, for others
|
||||
//! the **multistream-select** protocol is used in order to negotiate an encryption layer and/or a
|
||||
//! multiplexing layer.
|
||||
//!
|
||||
//! The connection establishment mechanism is called the **transport**.
|
||||
//!
|
||||
//! As of the writing of this documentation, the following base-layer protocols are supported by
|
||||
//! Bizinikiwi:
|
||||
//!
|
||||
//! - TCP/IP for addresses of the form `/ip4/1.2.3.4/tcp/5`. Once the TCP connection is open, an
|
||||
//! encryption and a multiplexing layer are negotiated on top.
|
||||
//! - WebSockets for addresses of the form `/ip4/1.2.3.4/tcp/5/ws`. A TCP/IP connection is open and
|
||||
//! the WebSockets protocol is negotiated on top. Communications then happen inside WebSockets data
|
||||
//! frames. Encryption and multiplexing are additionally negotiated again inside this channel.
|
||||
//! - DNS for addresses of the form `/dns/example.com/tcp/5` or `/dns/example.com/tcp/5/ws`. A
|
||||
//! node's address can contain a domain name.
|
||||
//! - (All of the above using IPv6 instead of IPv4.)
|
||||
//!
|
||||
//! On top of the base-layer protocol, the [Noise](https://noiseprotocol.org/) protocol is
|
||||
//! negotiated and applied. The exact handshake protocol is experimental and is subject to change.
|
||||
//!
|
||||
//! The following multiplexing protocols are supported:
|
||||
//!
|
||||
//! - [Yamux](https://github.com/hashicorp/yamux/blob/master/spec.md).
|
||||
//!
|
||||
//! ## Substreams
|
||||
//!
|
||||
//! Once a connection has been established and uses multiplexing, substreams can be opened. When
|
||||
//! a substream is open, the **multistream-select** protocol is used to negotiate which protocol
|
||||
//! to use on that given substream.
|
||||
//!
|
||||
//! Protocols that are specific to a certain chain have a `<protocol-id>` in their name. This
|
||||
//! "protocol ID" is defined in the chain specifications. For example, the protocol ID of Pezkuwi
|
||||
//! is "hez". In the protocol names below, `<protocol-id>` must be replaced with the corresponding
|
||||
//! protocol ID.
|
||||
//!
|
||||
//! > **Note**: It is possible for the same connection to be used for multiple chains. For example,
|
||||
//! > one can use both the `/hez/sync/2` and `/sub/sync/2` protocols on the same
|
||||
//! > connection, provided that the remote supports them.
|
||||
//!
|
||||
//! Bizinikiwi uses the following standard libp2p protocols:
|
||||
//!
|
||||
//! - **`/ipfs/ping/1.0.0`**. We periodically open an ephemeral substream in order to ping the
|
||||
//! remote and check whether the connection is still alive. Failure for the remote to reply leads
|
||||
//! to a disconnection.
|
||||
//! - **[`/ipfs/id/1.0.0`](https://github.com/libp2p/specs/tree/master/identify)**. We
|
||||
//! periodically open an ephemeral substream in order to ask information from the remote.
|
||||
//! - **[`/<protocol_id>/kad`](https://github.com/libp2p/specs/pull/108)**. We periodically open
|
||||
//! ephemeral substreams for Kademlia random walk queries. Each Kademlia query is done in a
|
||||
//! separate substream.
|
||||
//!
|
||||
//! Additionally, Bizinikiwi uses the following non-libp2p-standard protocols:
|
||||
//!
|
||||
//! - **`/bizinikiwi/<protocol-id>/<version>`** (where `<protocol-id>` must be replaced with the
|
||||
//! protocol ID of the targeted chain, and `<version>` is a number between 2 and 6). For each
|
||||
//! connection we optionally keep an additional substream for all Bizinikiwi-based communications
|
||||
//! alive. This protocol is considered legacy, and is progressively being replaced with
|
||||
//! alternatives. This is designated as "The legacy Bizinikiwi substream" in this documentation. See
|
||||
//! below for more details.
|
||||
//! - **`/<protocol-id>/sync/2`** is a request-response protocol (see below) that lets one perform
|
||||
//! requests for information about blocks. Each request is the encoding of a `BlockRequest` and
|
||||
//! each response is the encoding of a `BlockResponse`, as defined in the `api.v1.proto` file in
|
||||
//! this source tree.
|
||||
//! - **`/<protocol-id>/light/2`** is a request-response protocol (see below) that lets one perform
|
||||
//! light-client-related requests for information about the state. Each request is the encoding of
|
||||
//! a `light::Request` and each response is the encoding of a `light::Response`, as defined in the
|
||||
//! `light.v1.proto` file in this source tree.
|
||||
//! - **`/<protocol-id>/transactions/1`** is a notifications protocol (see below) where
|
||||
//! transactions are pushed to other nodes. The handshake is empty on both sides. The message
|
||||
//! format is a SCALE-encoded list of transactions, where each transaction is an opaque list of
|
||||
//! bytes.
|
||||
//! - **`/<protocol-id>/block-announces/1`** is a notifications protocol (see below) where
|
||||
//! block announces are pushed to other nodes. The handshake is empty on both sides. The message
|
||||
//! format is a SCALE-encoded tuple containing a block header followed with an opaque list of
|
||||
//! bytes containing some data associated with this block announcement, e.g. a candidate message.
|
||||
//! - Notifications protocols that are registered using
|
||||
//! `NetworkConfiguration::notifications_protocols`. For example: `/paritytech/grandpa/1`. See
|
||||
//! below for more information.
|
||||
//!
|
||||
//! ## The legacy Bizinikiwi substream
|
||||
//!
|
||||
//! Bizinikiwi uses a component named the **peerset manager (PSM)**. Through the discovery
|
||||
//! mechanism, the PSM is aware of the nodes that are part of the network and decides which nodes
|
||||
//! we should perform Bizinikiwi-based communications with. For these nodes, we open a connection
|
||||
//! if necessary and open a unique substream for Bizinikiwi-based communications. If the PSM decides
|
||||
//! that we should disconnect a node, then that substream is closed.
|
||||
//!
|
||||
//! For more information about the PSM, see the *sc-peerset* crate.
|
||||
//!
|
||||
//! Note that at the moment there is no mechanism in place to solve the issues that arise where the
|
||||
//! two sides of a connection open the unique substream simultaneously. In order to not run into
|
||||
//! issues, only the dialer of a connection is allowed to open the unique substream. When the
|
||||
//! substream is closed, the entire connection is closed as well. This is a bug that will be
|
||||
//! resolved by deprecating the protocol entirely.
|
||||
//!
|
||||
//! Within the unique Bizinikiwi substream, messages encoded using
|
||||
//! [*parity-scale-codec*](https://github.com/paritytech/parity-scale-codec) are exchanged.
|
||||
//! The detail of theses messages is not totally in place, but they can be found in the
|
||||
//! `message.rs` file.
|
||||
//!
|
||||
//! Once the substream is open, the first step is an exchange of a *status* message from both
|
||||
//! sides, containing information such as the chain root hash, head of chain, and so on.
|
||||
//!
|
||||
//! Communications within this substream include:
|
||||
//!
|
||||
//! - Syncing. Blocks are announced and requested from other nodes.
|
||||
//! - Light-client requests. When a light client requires information, a random node we have a
|
||||
//! substream open with is chosen, and the information is requested from it.
|
||||
//! - Gossiping. Used for example by grandpa.
|
||||
//!
|
||||
//! ## Request-response protocols
|
||||
//!
|
||||
//! A so-called request-response protocol is defined as follow:
|
||||
//!
|
||||
//! - When a substream is opened, the opening side sends a message whose content is
|
||||
//! protocol-specific. The message must be prefixed with an
|
||||
//! [LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. After the
|
||||
//! message has been sent, the writing side is closed.
|
||||
//! - The remote sends back the response prefixed with a LEB128-encoded length, and closes its
|
||||
//! side as well.
|
||||
//!
|
||||
//! Each request is performed in a new separate substream.
|
||||
//!
|
||||
//! ## Notifications protocols
|
||||
//!
|
||||
//! A so-called notifications protocol is defined as follow:
|
||||
//!
|
||||
//! - When a substream is opened, the opening side sends a handshake message whose content is
|
||||
//! protocol-specific. The handshake message must be prefixed with an
|
||||
//! [LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. The
|
||||
//! handshake message can be of length 0, in which case the sender has to send a single `0`.
|
||||
//! - The receiver then either immediately closes the substream, or answers with its own
|
||||
//! LEB128-prefixed protocol-specific handshake response. The message can be of length 0, in which
|
||||
//! case a single `0` has to be sent back.
|
||||
//! - Once the handshake has completed, the notifications protocol is unidirectional. Only the
|
||||
//! node which initiated the substream can push notifications. If the remote wants to send
|
||||
//! notifications as well, it has to open its own undirectional substream.
|
||||
//! - Each notification must be prefixed with an LEB128-encoded length. The encoding of the
|
||||
//! messages is specific to each protocol.
|
||||
//! - Either party can signal that it doesn't want a notifications substream anymore by closing
|
||||
//! its writing side. The other party should respond by closing its own writing side soon after.
|
||||
//!
|
||||
//! The API of `sc-network` allows one to register user-defined notification protocols.
|
||||
//! `sc-network` automatically tries to open a substream towards each node for which the legacy
|
||||
//! Substream substream is open. The handshake is then performed automatically.
|
||||
//!
|
||||
//! For example, the `sc-consensus-grandpa` crate registers the `/paritytech/grandpa/1`
|
||||
//! notifications protocol.
|
||||
//!
|
||||
//! At the moment, for backwards-compatibility, notification protocols are tied to the legacy
|
||||
//! Bizinikiwi substream. Additionally, the handshake message is hardcoded to be a single 8-bits
|
||||
//! integer representing the role of the node:
|
||||
//!
|
||||
//! - 1 for a full node.
|
||||
//! - 2 for a light node.
|
||||
//! - 4 for an authority.
|
||||
//!
|
||||
//! In the future, though, these restrictions will be removed.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Using the `sc-network` crate is done through the [`NetworkWorker`] struct. Create this
|
||||
//! struct by passing a [`config::Params`], then poll it as if it was a `Future`. You can extract an
|
||||
//! `Arc<NetworkService>` from the `NetworkWorker`, which can be shared amongst multiple places
|
||||
//! in order to give orders to the networking.
|
||||
//!
|
||||
//! See the [`config`] module for more information about how to configure the networking.
|
||||
//!
|
||||
//! After the `NetworkWorker` has been created, the important things to do are:
|
||||
//!
|
||||
//! - Calling `NetworkWorker::poll` in order to advance the network. This can be done by
|
||||
//! dispatching a background task with the [`NetworkWorker`].
|
||||
//! - Calling `on_block_import` whenever a block is added to the client.
|
||||
//! - Calling `on_block_finalized` whenever a block is finalized.
|
||||
//! - Calling `trigger_repropagate` when a transaction is added to the pool.
|
||||
//!
|
||||
//! More precise usage details are still being worked on and will likely change in the future.
|
||||
|
||||
mod behaviour;
|
||||
mod bitswap;
|
||||
mod litep2p;
|
||||
mod protocol;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
pub mod config;
|
||||
pub mod discovery;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod network_state;
|
||||
pub mod peer_info;
|
||||
pub mod peer_store;
|
||||
pub mod protocol_controller;
|
||||
pub mod request_responses;
|
||||
pub mod service;
|
||||
pub mod transport;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
pub use crate::litep2p::Litep2pNetworkBackend;
|
||||
pub use event::{DhtEvent, Event};
|
||||
#[doc(inline)]
|
||||
pub use request_responses::{Config, IfDisconnected, RequestFailure};
|
||||
pub use pezsc_network_common::{
|
||||
role::{ObservedRole, Roles},
|
||||
types::ReputationChange,
|
||||
};
|
||||
pub use pezsc_network_types::{
|
||||
multiaddr::{self, Multiaddr},
|
||||
PeerId,
|
||||
};
|
||||
pub use service::{
|
||||
metrics::NotificationMetrics,
|
||||
signature::Signature,
|
||||
traits::{
|
||||
KademliaKey, MessageSink, NetworkBackend, NetworkBlock, NetworkDHTProvider,
|
||||
NetworkEventStream, NetworkPeers, NetworkRequest, NetworkSigner, NetworkStateInfo,
|
||||
NetworkStatus, NetworkStatusProvider, NetworkSyncForkRequest, NotificationConfig,
|
||||
NotificationSender as NotificationSenderT, NotificationSenderError,
|
||||
NotificationSenderReady, NotificationService,
|
||||
},
|
||||
DecodingError, Keypair, NetworkService, NetworkWorker, NotificationSender, OutboundFailure,
|
||||
PublicKey,
|
||||
};
|
||||
pub use types::ProtocolName;
|
||||
|
||||
/// Log target for `sc-network`.
|
||||
const LOG_TARGET: &str = "sub-libp2p";
|
||||
|
||||
/// The maximum allowed number of established connections per peer.
|
||||
///
|
||||
/// Typically, and by design of the network behaviours in this crate,
|
||||
/// there is a single established connection per peer. However, to
|
||||
/// avoid unnecessary and nondeterministic connection closure in
|
||||
/// case of (possibly repeated) simultaneous dialing attempts between
|
||||
/// two peers, the per-peer connection limit is not set to 1 but 2.
|
||||
const MAX_CONNECTIONS_PER_PEER: usize = 2;
|
||||
|
||||
/// The maximum number of concurrent established connections that were incoming.
|
||||
const MAX_CONNECTIONS_ESTABLISHED_INCOMING: u32 = 10_000;
|
||||
|
||||
/// Maximum response size limit.
|
||||
pub const MAX_RESPONSE_SIZE: u64 = 16 * 1024 * 1024;
|
||||
@@ -0,0 +1,962 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! libp2p-related discovery code for litep2p backend.
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
NetworkConfiguration, ProtocolId, KADEMLIA_MAX_PROVIDER_KEYS, KADEMLIA_PROVIDER_RECORD_TTL,
|
||||
KADEMLIA_PROVIDER_REPUBLISH_INTERVAL,
|
||||
},
|
||||
peer_store::PeerStoreProvider,
|
||||
};
|
||||
|
||||
use array_bytes::bytes2hex;
|
||||
use futures::{FutureExt, Stream};
|
||||
use futures_timer::Delay;
|
||||
use ip_network::IpNetwork;
|
||||
use litep2p::{
|
||||
protocol::{
|
||||
libp2p::{
|
||||
identify::{Config as IdentifyConfig, IdentifyEvent},
|
||||
kademlia::{
|
||||
Config as KademliaConfig, ConfigBuilder as KademliaConfigBuilder, ContentProvider,
|
||||
IncomingRecordValidationMode, KademliaEvent, KademliaHandle, PeerRecord, QueryId,
|
||||
Quorum, Record, RecordKey,
|
||||
},
|
||||
ping::{Config as PingConfig, PingEvent},
|
||||
},
|
||||
mdns::{Config as MdnsConfig, MdnsEvent},
|
||||
},
|
||||
types::multiaddr::{Multiaddr, Protocol},
|
||||
PeerId, ProtocolName,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use pezsc_network_types::kad::Key as KademliaKey;
|
||||
use schnellru::{ByLength, LruMap};
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
iter,
|
||||
num::NonZeroUsize,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::discovery";
|
||||
|
||||
/// Kademlia query interval.
|
||||
const KADEMLIA_QUERY_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
/// mDNS query interval.
|
||||
const MDNS_QUERY_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// The minimum number of peers we expect an answer before we terminate the request.
|
||||
const GET_RECORD_REDUNDANCY_FACTOR: usize = 4;
|
||||
|
||||
/// The maximum number of tracked external addresses we allow.
|
||||
const MAX_EXTERNAL_ADDRESSES: u32 = 32;
|
||||
|
||||
/// Number of times observed address is received from different peers before it is confirmed as
|
||||
/// external.
|
||||
const MIN_ADDRESS_CONFIRMATIONS: usize = 3;
|
||||
|
||||
/// Quorum threshold to interpret `PUT_VALUE` & `ADD_PROVIDER` as successful.
|
||||
///
|
||||
/// As opposed to libp2p, litep2p does not finish the query as soon as the required number of
|
||||
/// peers have reached. Instead, it tries to put the record to all target peers (typically 20) and
|
||||
/// uses the quorum setting only to determine the success of the query.
|
||||
///
|
||||
/// We set the threshold to 50% of the target peers to account for unreachable peers. The actual
|
||||
/// number of stored records may be higher.
|
||||
const QUORUM_THRESHOLD: NonZeroUsize = NonZeroUsize::new(10).expect("10 > 0; qed");
|
||||
|
||||
/// Discovery events.
|
||||
#[derive(Debug)]
|
||||
pub enum DiscoveryEvent {
|
||||
/// Ping RTT measured for peer.
|
||||
Ping {
|
||||
/// Remote peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Ping round-trip time.
|
||||
rtt: Duration,
|
||||
},
|
||||
|
||||
/// Peer identified over `/ipfs/identify/1.0.0` protocol.
|
||||
Identified {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Listen addresses.
|
||||
listen_addresses: Vec<Multiaddr>,
|
||||
|
||||
/// Supported protocols.
|
||||
supported_protocols: HashSet<ProtocolName>,
|
||||
},
|
||||
|
||||
/// One or more addresses discovered.
|
||||
///
|
||||
/// This event is emitted when a new peer is discovered over mDNS.
|
||||
Discovered {
|
||||
/// Discovered addresses.
|
||||
addresses: Vec<Multiaddr>,
|
||||
},
|
||||
|
||||
/// Routing table has been updated.
|
||||
RoutingTableUpdate {
|
||||
/// Peers that were added to routing table.
|
||||
peers: HashSet<PeerId>,
|
||||
},
|
||||
|
||||
/// New external address discovered.
|
||||
ExternalAddressDiscovered {
|
||||
/// Discovered address.
|
||||
address: Multiaddr,
|
||||
},
|
||||
|
||||
/// The external address has expired.
|
||||
///
|
||||
/// This happens when the internal buffers exceed the maximum number of external addresses,
|
||||
/// and this address is the oldest one.
|
||||
ExternalAddressExpired {
|
||||
/// Expired address.
|
||||
address: Multiaddr,
|
||||
},
|
||||
|
||||
/// `FIND_NODE` query succeeded.
|
||||
FindNodeSuccess {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
|
||||
/// Target.
|
||||
target: PeerId,
|
||||
|
||||
/// Found peers.
|
||||
peers: Vec<(PeerId, Vec<Multiaddr>)>,
|
||||
},
|
||||
|
||||
/// `GetRecord` query succeeded.
|
||||
GetRecordSuccess {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
},
|
||||
|
||||
/// Record was found from the DHT.
|
||||
GetRecordPartialResult {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
|
||||
/// Record.
|
||||
record: PeerRecord,
|
||||
},
|
||||
|
||||
/// Record was successfully stored on the DHT.
|
||||
PutRecordSuccess {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
},
|
||||
|
||||
/// Providers were successfully retrieved.
|
||||
GetProvidersSuccess {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
/// Found providers sorted by distance to provided key.
|
||||
providers: Vec<ContentProvider>,
|
||||
},
|
||||
|
||||
/// Provider was successfully published.
|
||||
AddProviderSuccess {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
/// Provided key.
|
||||
provided_key: RecordKey,
|
||||
},
|
||||
|
||||
/// Query failed.
|
||||
QueryFailed {
|
||||
/// Query ID.
|
||||
query_id: QueryId,
|
||||
},
|
||||
|
||||
/// Incoming record to store.
|
||||
IncomingRecord {
|
||||
/// Record.
|
||||
record: Record,
|
||||
},
|
||||
|
||||
/// Started a random Kademlia query.
|
||||
RandomKademliaStarted,
|
||||
}
|
||||
|
||||
/// Discovery.
|
||||
pub struct Discovery {
|
||||
/// Local peer ID.
|
||||
local_peer_id: litep2p::PeerId,
|
||||
|
||||
/// Ping event stream.
|
||||
ping_event_stream: Box<dyn Stream<Item = PingEvent> + Send + Unpin>,
|
||||
|
||||
/// Identify event stream.
|
||||
identify_event_stream: Box<dyn Stream<Item = IdentifyEvent> + Send + Unpin>,
|
||||
|
||||
/// mDNS event stream, if enabled.
|
||||
mdns_event_stream: Option<Box<dyn Stream<Item = MdnsEvent> + Send + Unpin>>,
|
||||
|
||||
/// Kademlia handle.
|
||||
kademlia_handle: KademliaHandle,
|
||||
|
||||
/// `Peerstore` handle.
|
||||
_peerstore_handle: Arc<dyn PeerStoreProvider>,
|
||||
|
||||
/// Next Kademlia query for a random peer ID.
|
||||
///
|
||||
/// If `None`, there is currently a query pending.
|
||||
next_kad_query: Option<Delay>,
|
||||
|
||||
/// Active `FIND_NODE` query if it exists.
|
||||
random_walk_query_id: Option<QueryId>,
|
||||
|
||||
/// Pending events.
|
||||
pending_events: VecDeque<DiscoveryEvent>,
|
||||
|
||||
/// Allow non-global addresses in the DHT.
|
||||
allow_non_global_addresses: bool,
|
||||
|
||||
/// Protocols supported by the local node.
|
||||
local_protocols: HashSet<ProtocolName>,
|
||||
|
||||
/// Public addresses.
|
||||
public_addresses: HashSet<Multiaddr>,
|
||||
|
||||
/// Listen addresses.
|
||||
listen_addresses: Arc<RwLock<HashSet<Multiaddr>>>,
|
||||
|
||||
/// External address confirmations.
|
||||
address_confirmations: LruMap<Multiaddr, HashSet<PeerId>>,
|
||||
|
||||
/// Delay to next `FIND_NODE` query.
|
||||
duration_to_next_find_query: Duration,
|
||||
}
|
||||
|
||||
/// Legacy (fallback) Kademlia protocol name based on `protocol_id`.
|
||||
fn legacy_kademlia_protocol_name(id: &ProtocolId) -> ProtocolName {
|
||||
ProtocolName::from(format!("/{}/kad", id.as_ref()))
|
||||
}
|
||||
|
||||
/// Kademlia protocol name based on `genesis_hash` and `fork_id`.
|
||||
fn kademlia_protocol_name<Hash: AsRef<[u8]>>(
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
) -> ProtocolName {
|
||||
let genesis_hash_hex = bytes2hex("", genesis_hash.as_ref());
|
||||
let protocol = if let Some(fork_id) = fork_id {
|
||||
format!("/{}/{}/kad", genesis_hash_hex, fork_id)
|
||||
} else {
|
||||
format!("/{}/kad", genesis_hash_hex)
|
||||
};
|
||||
|
||||
ProtocolName::from(protocol)
|
||||
}
|
||||
|
||||
impl Discovery {
|
||||
/// Create new [`Discovery`].
|
||||
///
|
||||
/// Enables `/ipfs/ping/1.0.0` and `/ipfs/identify/1.0.0` by default and starts
|
||||
/// the mDNS peer discovery if it was enabled.
|
||||
pub fn new<Hash: AsRef<[u8]> + Clone>(
|
||||
local_peer_id: litep2p::PeerId,
|
||||
config: &NetworkConfiguration,
|
||||
genesis_hash: Hash,
|
||||
fork_id: Option<&str>,
|
||||
protocol_id: &ProtocolId,
|
||||
known_peers: HashMap<PeerId, Vec<Multiaddr>>,
|
||||
listen_addresses: Arc<RwLock<HashSet<Multiaddr>>>,
|
||||
_peerstore_handle: Arc<dyn PeerStoreProvider>,
|
||||
) -> (Self, PingConfig, IdentifyConfig, KademliaConfig, Option<MdnsConfig>) {
|
||||
let (ping_config, ping_event_stream) = PingConfig::default();
|
||||
let user_agent = format!("{} ({}) (litep2p)", config.client_version, config.node_name);
|
||||
|
||||
let (identify_config, identify_event_stream) =
|
||||
IdentifyConfig::new("/bizinikiwi/1.0".to_string(), Some(user_agent));
|
||||
|
||||
let (mdns_config, mdns_event_stream) = match config.transport {
|
||||
crate::config::TransportConfig::Normal { enable_mdns, .. } => match enable_mdns {
|
||||
true => {
|
||||
let (mdns_config, mdns_event_stream) = MdnsConfig::new(MDNS_QUERY_INTERVAL);
|
||||
(Some(mdns_config), Some(mdns_event_stream))
|
||||
},
|
||||
false => (None, None),
|
||||
},
|
||||
_ => panic!("memory transport not supported"),
|
||||
};
|
||||
|
||||
let (kademlia_config, kademlia_handle) = {
|
||||
let protocol_names = vec![
|
||||
kademlia_protocol_name(genesis_hash.clone(), fork_id),
|
||||
legacy_kademlia_protocol_name(protocol_id),
|
||||
];
|
||||
|
||||
KademliaConfigBuilder::new()
|
||||
.with_known_peers(known_peers)
|
||||
.with_protocol_names(protocol_names)
|
||||
.with_incoming_records_validation_mode(IncomingRecordValidationMode::Manual)
|
||||
.with_provider_record_ttl(KADEMLIA_PROVIDER_RECORD_TTL)
|
||||
.with_provider_refresh_interval(KADEMLIA_PROVIDER_REPUBLISH_INTERVAL)
|
||||
.with_max_provider_keys(KADEMLIA_MAX_PROVIDER_KEYS)
|
||||
.build()
|
||||
};
|
||||
|
||||
(
|
||||
Self {
|
||||
local_peer_id,
|
||||
ping_event_stream,
|
||||
identify_event_stream,
|
||||
mdns_event_stream,
|
||||
kademlia_handle,
|
||||
_peerstore_handle,
|
||||
listen_addresses,
|
||||
random_walk_query_id: None,
|
||||
pending_events: VecDeque::new(),
|
||||
duration_to_next_find_query: Duration::from_secs(1),
|
||||
address_confirmations: LruMap::new(ByLength::new(MAX_EXTERNAL_ADDRESSES)),
|
||||
allow_non_global_addresses: config.allow_non_globals_in_dht,
|
||||
public_addresses: config.public_addresses.iter().cloned().map(Into::into).collect(),
|
||||
next_kad_query: Some(Delay::new(KADEMLIA_QUERY_INTERVAL)),
|
||||
local_protocols: HashSet::from_iter([kademlia_protocol_name(
|
||||
genesis_hash,
|
||||
fork_id,
|
||||
)]),
|
||||
},
|
||||
ping_config,
|
||||
identify_config,
|
||||
kademlia_config,
|
||||
mdns_config,
|
||||
)
|
||||
}
|
||||
|
||||
/// Add known peer to `Kademlia`.
|
||||
#[allow(unused)]
|
||||
pub async fn add_known_peer(&mut self, peer: PeerId, addresses: Vec<Multiaddr>) {
|
||||
self.kademlia_handle.add_known_peer(peer, addresses).await;
|
||||
}
|
||||
|
||||
/// Add self-reported addresses to routing table if `peer` supports
|
||||
/// at least one of the locally supported DHT protocol.
|
||||
pub async fn add_self_reported_address(
|
||||
&mut self,
|
||||
peer: PeerId,
|
||||
supported_protocols: HashSet<ProtocolName>,
|
||||
addresses: Vec<Multiaddr>,
|
||||
) {
|
||||
if self.local_protocols.is_disjoint(&supported_protocols) {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Ignoring self-reported address of peer {peer} as remote node is not part of the \
|
||||
Kademlia DHT supported by the local node.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let addresses = addresses
|
||||
.into_iter()
|
||||
.filter_map(|address| {
|
||||
if !self.allow_non_global_addresses && !Discovery::can_add_to_dht(&address) {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"ignoring self-reported non-global address {address} from {peer}."
|
||||
);
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(address)
|
||||
})
|
||||
.collect();
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"add self-reported addresses for {peer:?}: {addresses:?}",
|
||||
);
|
||||
|
||||
self.kademlia_handle.add_known_peer(peer, addresses).await;
|
||||
}
|
||||
|
||||
/// Start Kademlia `FIND_NODE` query for `target`.
|
||||
pub async fn find_node(&mut self, target: PeerId) -> QueryId {
|
||||
self.kademlia_handle.find_node(target).await
|
||||
}
|
||||
|
||||
/// Start Kademlia `GET_VALUE` query for `key`.
|
||||
pub async fn get_value(&mut self, key: KademliaKey) -> QueryId {
|
||||
self.kademlia_handle
|
||||
.get_record(
|
||||
RecordKey::new(&key.to_vec()),
|
||||
Quorum::N(NonZeroUsize::new(GET_RECORD_REDUNDANCY_FACTOR).unwrap()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Publish value on the DHT using Kademlia `PUT_VALUE`.
|
||||
pub async fn put_value(&mut self, key: KademliaKey, value: Vec<u8>) -> QueryId {
|
||||
self.kademlia_handle
|
||||
.put_record(
|
||||
Record::new(RecordKey::new(&key.to_vec()), value),
|
||||
Quorum::N(QUORUM_THRESHOLD),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Put record to given peers.
|
||||
pub async fn put_value_to_peers(
|
||||
&mut self,
|
||||
record: Record,
|
||||
peers: Vec<pezsc_network_types::PeerId>,
|
||||
update_local_storage: bool,
|
||||
) -> QueryId {
|
||||
self.kademlia_handle
|
||||
.put_record_to_peers(
|
||||
record,
|
||||
peers.into_iter().map(|peer| peer.into()).collect(),
|
||||
update_local_storage,
|
||||
// These are the peers that just returned the record to us in authority-discovery,
|
||||
// so we assume they are all reachable.
|
||||
Quorum::All,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Store record in the local DHT store.
|
||||
pub async fn store_record(
|
||||
&mut self,
|
||||
key: KademliaKey,
|
||||
value: Vec<u8>,
|
||||
publisher: Option<pezsc_network_types::PeerId>,
|
||||
expires: Option<Instant>,
|
||||
) {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Storing DHT record with key {key:?}, originally published by {publisher:?}, \
|
||||
expires {expires:?}.",
|
||||
);
|
||||
|
||||
self.kademlia_handle
|
||||
.store_record(Record {
|
||||
key: RecordKey::new(&key.to_vec()),
|
||||
value,
|
||||
publisher: publisher.map(Into::into),
|
||||
expires,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Start providing `key`.
|
||||
pub async fn start_providing(&mut self, key: KademliaKey) -> QueryId {
|
||||
self.kademlia_handle
|
||||
.start_providing(key.into(), Quorum::N(QUORUM_THRESHOLD))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Stop providing `key`.
|
||||
pub async fn stop_providing(&mut self, key: KademliaKey) {
|
||||
self.kademlia_handle.stop_providing(key.into()).await;
|
||||
}
|
||||
|
||||
/// Get providers for `key`.
|
||||
pub async fn get_providers(&mut self, key: KademliaKey) -> QueryId {
|
||||
self.kademlia_handle.get_providers(key.into()).await
|
||||
}
|
||||
|
||||
/// Check if the observed address is a known address.
|
||||
fn is_known_address(known: &Multiaddr, observed: &Multiaddr) -> bool {
|
||||
let mut known = known.iter();
|
||||
let mut observed = observed.iter();
|
||||
|
||||
loop {
|
||||
match (known.next(), observed.next()) {
|
||||
(None, None) => return true,
|
||||
(None, Some(Protocol::P2p(_))) => return true,
|
||||
(Some(Protocol::P2p(_)), None) => return true,
|
||||
(known, observed) if known != observed => return false,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Can `address` be added to DHT.
|
||||
fn can_add_to_dht(address: &Multiaddr) -> bool {
|
||||
let ip = match address.iter().next() {
|
||||
Some(Protocol::Ip4(ip)) => IpNetwork::from(ip),
|
||||
Some(Protocol::Ip6(ip)) => IpNetwork::from(ip),
|
||||
Some(Protocol::Dns(_)) | Some(Protocol::Dns4(_)) | Some(Protocol::Dns6(_)) =>
|
||||
return true,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
ip.is_global()
|
||||
}
|
||||
|
||||
/// Check if `address` can be considered a new external address.
|
||||
///
|
||||
/// If this address replaces an older address, the expired address is returned.
|
||||
fn is_new_external_address(
|
||||
&mut self,
|
||||
address: &Multiaddr,
|
||||
peer: PeerId,
|
||||
) -> (bool, Option<Multiaddr>) {
|
||||
log::trace!(target: LOG_TARGET, "verify new external address: {address}");
|
||||
|
||||
if !self.allow_non_global_addresses && !Discovery::can_add_to_dht(&address) {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"ignoring externally reported non-global address {address} from {peer}."
|
||||
);
|
||||
|
||||
return (false, None);
|
||||
}
|
||||
|
||||
// is the address one of our known addresses
|
||||
if self
|
||||
.listen_addresses
|
||||
.read()
|
||||
.iter()
|
||||
.chain(self.public_addresses.iter())
|
||||
.any(|known_address| Discovery::is_known_address(&known_address, &address))
|
||||
{
|
||||
return (true, None);
|
||||
}
|
||||
|
||||
match self.address_confirmations.get(address) {
|
||||
Some(confirmations) => {
|
||||
confirmations.insert(peer);
|
||||
|
||||
if confirmations.len() >= MIN_ADDRESS_CONFIRMATIONS {
|
||||
return (true, None);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let oldest = (self.address_confirmations.len() >=
|
||||
self.address_confirmations.limiter().max_length() as usize)
|
||||
.then(|| {
|
||||
self.address_confirmations.pop_oldest().map(|(address, peers)| {
|
||||
if peers.len() >= MIN_ADDRESS_CONFIRMATIONS {
|
||||
return Some(address);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.flatten();
|
||||
|
||||
self.address_confirmations.insert(address.clone(), iter::once(peer).collect());
|
||||
|
||||
return (false, oldest);
|
||||
},
|
||||
}
|
||||
|
||||
(false, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Discovery {
|
||||
type Item = DiscoveryEvent;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = Pin::into_inner(self);
|
||||
|
||||
if let Some(event) = this.pending_events.pop_front() {
|
||||
return Poll::Ready(Some(event));
|
||||
}
|
||||
|
||||
if let Some(mut delay) = this.next_kad_query.take() {
|
||||
match delay.poll_unpin(cx) {
|
||||
Poll::Pending => {
|
||||
this.next_kad_query = Some(delay);
|
||||
},
|
||||
Poll::Ready(()) => {
|
||||
let peer = PeerId::random();
|
||||
|
||||
log::trace!(target: LOG_TARGET, "start next kademlia query for {peer:?}");
|
||||
|
||||
match this.kademlia_handle.try_find_node(peer) {
|
||||
Ok(query_id) => {
|
||||
this.random_walk_query_id = Some(query_id);
|
||||
return Poll::Ready(Some(DiscoveryEvent::RandomKademliaStarted));
|
||||
},
|
||||
Err(()) => {
|
||||
this.duration_to_next_find_query = cmp::min(
|
||||
this.duration_to_next_find_query * 2,
|
||||
Duration::from_secs(60),
|
||||
);
|
||||
this.next_kad_query =
|
||||
Some(Delay::new(this.duration_to_next_find_query));
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
match Pin::new(&mut this.kademlia_handle).poll_next(cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(KademliaEvent::FindNodeSuccess { query_id, peers, .. }))
|
||||
if Some(query_id) == this.random_walk_query_id =>
|
||||
{
|
||||
// the addresses are already inserted into the DHT and in `TransportManager` so
|
||||
// there is no need to add them again. The found peers must be registered to
|
||||
// `Peerstore` so other protocols are aware of them through `Peerset`.
|
||||
log::trace!(target: LOG_TARGET, "dht random walk yielded {} peers", peers.len());
|
||||
|
||||
this.next_kad_query = Some(Delay::new(KADEMLIA_QUERY_INTERVAL));
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::RoutingTableUpdate {
|
||||
peers: peers.into_iter().map(|(peer, _)| peer).collect(),
|
||||
}));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::FindNodeSuccess { query_id, target, peers })) => {
|
||||
log::trace!(target: LOG_TARGET, "find node query yielded {} peers", peers.len());
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::FindNodeSuccess {
|
||||
query_id,
|
||||
target,
|
||||
peers,
|
||||
}));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::RoutingTableUpdate { peers })) => {
|
||||
log::trace!(target: LOG_TARGET, "routing table update, discovered {} peers", peers.len());
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::RoutingTableUpdate {
|
||||
peers: peers.into_iter().collect(),
|
||||
}));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::GetRecordSuccess { query_id })) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"`GET_RECORD` succeeded for {query_id:?}",
|
||||
);
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::GetRecordSuccess { query_id }));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::GetRecordPartialResult { query_id, record })) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"`GET_RECORD` intermediary succeeded for {query_id:?}: {record:?}",
|
||||
);
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::GetRecordPartialResult {
|
||||
query_id,
|
||||
record,
|
||||
}));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::PutRecordSuccess { query_id, key: _ })) =>
|
||||
return Poll::Ready(Some(DiscoveryEvent::PutRecordSuccess { query_id })),
|
||||
Poll::Ready(Some(KademliaEvent::QueryFailed { query_id })) => {
|
||||
match this.random_walk_query_id == Some(query_id) {
|
||||
true => {
|
||||
this.random_walk_query_id = None;
|
||||
this.duration_to_next_find_query =
|
||||
cmp::min(this.duration_to_next_find_query * 2, Duration::from_secs(60));
|
||||
this.next_kad_query = Some(Delay::new(this.duration_to_next_find_query));
|
||||
},
|
||||
false => return Poll::Ready(Some(DiscoveryEvent::QueryFailed { query_id })),
|
||||
}
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::IncomingRecord { record })) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"incoming `PUT_RECORD` request with key {:?} from publisher {:?}",
|
||||
record.key,
|
||||
record.publisher,
|
||||
);
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::IncomingRecord { record }));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::GetProvidersSuccess {
|
||||
provided_key,
|
||||
providers,
|
||||
query_id,
|
||||
})) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"`GET_PROVIDERS` for {query_id:?} with {provided_key:?} yielded {providers:?}",
|
||||
);
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::GetProvidersSuccess {
|
||||
query_id,
|
||||
providers,
|
||||
}));
|
||||
},
|
||||
Poll::Ready(Some(KademliaEvent::AddProviderSuccess { query_id, provided_key })) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"`ADD_PROVIDER` for {query_id:?} with {provided_key:?} succeeded",
|
||||
);
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::AddProviderSuccess {
|
||||
query_id,
|
||||
provided_key,
|
||||
}));
|
||||
},
|
||||
// We do not validate incoming providers.
|
||||
Poll::Ready(Some(KademliaEvent::IncomingProvider { .. })) => {},
|
||||
}
|
||||
|
||||
match Pin::new(&mut this.identify_event_stream).poll_next(cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(IdentifyEvent::PeerIdentified {
|
||||
peer,
|
||||
listen_addresses,
|
||||
supported_protocols,
|
||||
observed_address,
|
||||
..
|
||||
})) => {
|
||||
let observed_address =
|
||||
if let Some(Protocol::P2p(peer_id)) = observed_address.iter().last() {
|
||||
if peer_id != *this.local_peer_id.as_ref() {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Discovered external address for a peer that is not us: {observed_address}",
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(observed_address)
|
||||
}
|
||||
} else {
|
||||
Some(observed_address.with(Protocol::P2p(this.local_peer_id.into())))
|
||||
};
|
||||
|
||||
// Ensure that an external address with a different peer ID does not have
|
||||
// side effects of evicting other external addresses via `ExternalAddressExpired`.
|
||||
if let Some(observed_address) = observed_address {
|
||||
let (is_new, expired_address) =
|
||||
this.is_new_external_address(&observed_address, peer);
|
||||
|
||||
if let Some(expired_address) = expired_address {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Removing expired external address expired={expired_address} is_new={is_new} observed={observed_address}",
|
||||
);
|
||||
|
||||
this.pending_events.push_back(DiscoveryEvent::ExternalAddressExpired {
|
||||
address: expired_address,
|
||||
});
|
||||
}
|
||||
|
||||
if is_new {
|
||||
this.pending_events.push_back(DiscoveryEvent::ExternalAddressDiscovered {
|
||||
address: observed_address.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Poll::Ready(Some(DiscoveryEvent::Identified {
|
||||
peer,
|
||||
listen_addresses,
|
||||
supported_protocols,
|
||||
}));
|
||||
},
|
||||
}
|
||||
|
||||
match Pin::new(&mut this.ping_event_stream).poll_next(cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(PingEvent::Ping { peer, ping })) =>
|
||||
return Poll::Ready(Some(DiscoveryEvent::Ping { peer, rtt: ping })),
|
||||
}
|
||||
|
||||
if let Some(ref mut mdns_event_stream) = &mut this.mdns_event_stream {
|
||||
match Pin::new(mdns_event_stream).poll_next(cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(None) => return Poll::Ready(None),
|
||||
Poll::Ready(Some(MdnsEvent::Discovered(addresses))) =>
|
||||
return Poll::Ready(Some(DiscoveryEvent::Discovered { addresses })),
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::atomic::AtomicU32;
|
||||
|
||||
use crate::{
|
||||
config::ProtocolId,
|
||||
peer_store::{PeerStore, PeerStoreProvider},
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_tracing::tracing_subscriber;
|
||||
|
||||
use litep2p::{
|
||||
config::ConfigBuilder as Litep2pConfigBuilder, transport::tcp::config::Config as TcpConfig,
|
||||
Litep2p,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn litep2p_discovery_works() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.try_init();
|
||||
|
||||
let mut known_peers = HashMap::new();
|
||||
let genesis_hash = H256::from_low_u64_be(1);
|
||||
let fork_id = Some("test-fork-id");
|
||||
let protocol_id = ProtocolId::from("hez");
|
||||
|
||||
// Build backends such that the first peer is known to all other peers.
|
||||
let backends = (0..10)
|
||||
.map(|i| {
|
||||
let keypair = litep2p::crypto::ed25519::Keypair::generate();
|
||||
let peer_id: PeerId = keypair.public().to_peer_id().into();
|
||||
|
||||
let listen_addresses = Arc::new(RwLock::new(HashSet::new()));
|
||||
|
||||
let peer_store = PeerStore::new(vec![], None);
|
||||
let peer_store_handle: Arc<dyn PeerStoreProvider> = Arc::new(peer_store.handle());
|
||||
|
||||
let (discovery, ping_config, identify_config, kademlia_config, _mdns) =
|
||||
Discovery::new(
|
||||
peer_id,
|
||||
&NetworkConfiguration::new_local(),
|
||||
genesis_hash,
|
||||
fork_id,
|
||||
&protocol_id,
|
||||
known_peers.clone(),
|
||||
listen_addresses.clone(),
|
||||
peer_store_handle,
|
||||
);
|
||||
|
||||
let config = Litep2pConfigBuilder::new()
|
||||
.with_keypair(keypair)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec!["/ip6/::1/tcp/0".parse().unwrap()],
|
||||
..Default::default()
|
||||
})
|
||||
.with_libp2p_ping(ping_config)
|
||||
.with_libp2p_identify(identify_config)
|
||||
.with_libp2p_kademlia(kademlia_config)
|
||||
.build();
|
||||
|
||||
let mut litep2p = Litep2p::new(config).unwrap();
|
||||
|
||||
let addresses = litep2p.listen_addresses().cloned().collect::<Vec<_>>();
|
||||
// Propagate addresses to discovery.
|
||||
addresses.iter().for_each(|address| {
|
||||
listen_addresses.write().insert(address.clone());
|
||||
});
|
||||
|
||||
// Except the first peer, all other peers know the first peer addresses.
|
||||
if i == 0 {
|
||||
log::info!(target: LOG_TARGET, "First peer is {peer_id:?} with addresses {addresses:?}");
|
||||
known_peers.insert(peer_id, addresses.clone());
|
||||
} else {
|
||||
let (peer, addresses) = known_peers.iter().next().unwrap();
|
||||
|
||||
let result = litep2p.add_known_address(*peer, addresses.into_iter().cloned());
|
||||
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?}: Adding known peer {peer:?} with addresses {addresses:?} result={result:?}");
|
||||
|
||||
}
|
||||
|
||||
(peer_id, litep2p, discovery)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total_peers = backends.len() as u32;
|
||||
let remaining_peers =
|
||||
backends.iter().map(|(peer_id, _, _)| *peer_id).collect::<HashSet<_>>();
|
||||
|
||||
let first_peer = *known_peers.iter().next().unwrap().0;
|
||||
|
||||
// Each backend must discover the whole network.
|
||||
let mut futures = FuturesUnordered::new();
|
||||
let num_finished = Arc::new(AtomicU32::new(0));
|
||||
|
||||
for (peer_id, mut litep2p, mut discovery) in backends {
|
||||
// Remove the local peer id from the set.
|
||||
let mut remaining_peers = remaining_peers.clone();
|
||||
remaining_peers.remove(&peer_id);
|
||||
|
||||
let num_finished = num_finished.clone();
|
||||
|
||||
let future = async move {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} starting loop");
|
||||
|
||||
if peer_id != first_peer {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} dialing {first_peer:?}");
|
||||
litep2p.dial(&first_peer).await.unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
// We need to keep the network alive until all peers are discovered.
|
||||
if num_finished.load(std::sync::atomic::Ordering::Relaxed) == total_peers {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} all peers discovered");
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
// Drive litep2p backend forward.
|
||||
event = litep2p.next_event() => {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} Litep2p event: {event:?}");
|
||||
},
|
||||
|
||||
// Detect discovery events.
|
||||
event = discovery.next() => {
|
||||
match event.unwrap() {
|
||||
// We have discovered the peer via kademlia and established
|
||||
// a connection on the identify protocol.
|
||||
DiscoveryEvent::Identified { peer, .. } => {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} Peer {peer} identified");
|
||||
|
||||
remaining_peers.remove(&peer);
|
||||
|
||||
if remaining_peers.is_empty() {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} All peers discovered");
|
||||
|
||||
num_finished.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
||||
}
|
||||
},
|
||||
|
||||
event => {
|
||||
log::info!(target: LOG_TARGET, "{peer_id:?} Discovery event: {event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
futures.push(future);
|
||||
}
|
||||
|
||||
// Futures will exit when all peers are discovered.
|
||||
tokio::time::timeout(Duration::from_secs(60), futures.next())
|
||||
.await
|
||||
.expect("All peers should finish within 60 seconds");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,481 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `Peerstore` implementation for `litep2p`.
|
||||
//!
|
||||
//! `Peerstore` is responsible for storing information about remote peers
|
||||
//! such as their addresses, reputations, supported protocols etc.
|
||||
|
||||
use crate::{
|
||||
peer_store::{PeerStoreProvider, ProtocolHandle},
|
||||
service::{metrics::PeerStoreMetrics, traits::PeerStore},
|
||||
ObservedRole, ReputationChange,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use prometheus_endpoint::Registry;
|
||||
use wasm_timer::Delay;
|
||||
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::peerstore";
|
||||
|
||||
/// We don't accept nodes whose reputation is under this value.
|
||||
pub const BANNED_THRESHOLD: i32 = 71 * (i32::MIN / 100);
|
||||
|
||||
/// Relative decrement of a reputation value that is applied every second. I.e., for inverse
|
||||
/// decrement of 200 we decrease absolute value of the reputation by 1/200.
|
||||
///
|
||||
/// This corresponds to a factor of `k = 0.995`, where k = 1 - 1 / INVERSE_DECREMENT.
|
||||
///
|
||||
/// It takes ~ `ln(0.5) / ln(k)` seconds to reduce the reputation by half, or 138.63 seconds for the
|
||||
/// values above.
|
||||
///
|
||||
/// In this setup:
|
||||
/// - `i32::MAX` becomes 0 in exactly 3544 seconds, or approximately 59 minutes
|
||||
/// - `i32::MIN` escapes the banned threshold in 69 seconds
|
||||
const INVERSE_DECREMENT: i32 = 200;
|
||||
|
||||
/// Amount of time between the moment we last updated the [`PeerStore`] entry and the moment we
|
||||
/// remove it, once the reputation value reaches 0.
|
||||
const FORGET_AFTER: Duration = Duration::from_secs(3600);
|
||||
|
||||
/// Peer information.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PeerInfo {
|
||||
/// Reputation of the peer.
|
||||
reputation: i32,
|
||||
|
||||
/// Instant when the peer was last updated.
|
||||
last_updated: Instant,
|
||||
|
||||
/// Role of the peer, if known.
|
||||
role: Option<ObservedRole>,
|
||||
}
|
||||
|
||||
impl Default for PeerInfo {
|
||||
fn default() -> Self {
|
||||
Self { reputation: 0i32, last_updated: Instant::now(), role: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl PeerInfo {
|
||||
fn is_banned(&self) -> bool {
|
||||
self.reputation < BANNED_THRESHOLD
|
||||
}
|
||||
|
||||
fn add_reputation(&mut self, increment: i32) {
|
||||
self.reputation = self.reputation.saturating_add(increment);
|
||||
self.bump_last_updated();
|
||||
}
|
||||
|
||||
fn decay_reputation(&mut self, seconds_passed: u64) {
|
||||
// Note that decaying the reputation value happens "on its own",
|
||||
// so we don't do `bump_last_updated()`.
|
||||
for _ in 0..seconds_passed {
|
||||
let mut diff = self.reputation / INVERSE_DECREMENT;
|
||||
if diff == 0 && self.reputation < 0 {
|
||||
diff = -1;
|
||||
} else if diff == 0 && self.reputation > 0 {
|
||||
diff = 1;
|
||||
}
|
||||
|
||||
self.reputation = self.reputation.saturating_sub(diff);
|
||||
|
||||
if self.reputation == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bump_last_updated(&mut self) {
|
||||
self.last_updated = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PeerstoreHandleInner {
|
||||
peers: HashMap<PeerId, PeerInfo>,
|
||||
protocols: Vec<Arc<dyn ProtocolHandle>>,
|
||||
metrics: Option<PeerStoreMetrics>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PeerstoreHandle(Arc<Mutex<PeerstoreHandleInner>>);
|
||||
|
||||
impl PeerstoreHandle {
|
||||
/// Constructs a new [`PeerstoreHandle`].
|
||||
fn new(
|
||||
peers: HashMap<PeerId, PeerInfo>,
|
||||
protocols: Vec<Arc<dyn ProtocolHandle>>,
|
||||
metrics: Option<PeerStoreMetrics>,
|
||||
) -> Self {
|
||||
Self(Arc::new(Mutex::new(PeerstoreHandleInner { peers, protocols, metrics })))
|
||||
}
|
||||
|
||||
/// Add known peer to [`Peerstore`].
|
||||
pub fn add_known_peer(&self, peer: PeerId) {
|
||||
self.0
|
||||
.lock()
|
||||
.peers
|
||||
.insert(peer, PeerInfo { reputation: 0i32, last_updated: Instant::now(), role: None });
|
||||
}
|
||||
|
||||
pub fn peer_count(&self) -> usize {
|
||||
self.0.lock().peers.len()
|
||||
}
|
||||
|
||||
fn progress_time(&self, seconds_passed: u64) {
|
||||
if seconds_passed == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lock = self.0.lock();
|
||||
|
||||
// Drive reputation values towards 0.
|
||||
lock.peers
|
||||
.iter_mut()
|
||||
.for_each(|(_, info)| info.decay_reputation(seconds_passed));
|
||||
|
||||
// Retain only entries with non-zero reputation values or not expired ones.
|
||||
let now = Instant::now();
|
||||
let mut num_banned_peers = 0;
|
||||
lock.peers.retain(|_, info| {
|
||||
if info.is_banned() {
|
||||
num_banned_peers += 1;
|
||||
}
|
||||
info.reputation != 0 || info.last_updated + FORGET_AFTER > now
|
||||
});
|
||||
|
||||
if let Some(metrics) = &lock.metrics {
|
||||
metrics.num_discovered.set(lock.peers.len() as u64);
|
||||
metrics.num_banned_peers.set(num_banned_peers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PeerStoreProvider for PeerstoreHandle {
|
||||
fn is_banned(&self, peer: &PeerId) -> bool {
|
||||
self.0.lock().peers.get(peer).map_or(false, |info| info.is_banned())
|
||||
}
|
||||
|
||||
/// Register a protocol handle to disconnect peers whose reputation drops below the threshold.
|
||||
fn register_protocol(&self, protocol_handle: Arc<dyn ProtocolHandle>) {
|
||||
self.0.lock().protocols.push(protocol_handle);
|
||||
}
|
||||
|
||||
/// Report peer disconnection for reputation adjustment.
|
||||
fn report_disconnect(&self, _peer: PeerId) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Adjust peer reputation.
|
||||
fn report_peer(&self, peer_id: PeerId, change: ReputationChange) {
|
||||
let mut lock = self.0.lock();
|
||||
let peer_info = lock.peers.entry(peer_id).or_default();
|
||||
let was_banned = peer_info.is_banned();
|
||||
peer_info.add_reputation(change.value);
|
||||
let peer_reputation = peer_info.reputation;
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Report {}: {:+} to {}. Reason: {}.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_reputation,
|
||||
change.reason,
|
||||
);
|
||||
|
||||
if !peer_info.is_banned() {
|
||||
if was_banned {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Peer {} is now unbanned: {:+} to {}. Reason: {}.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_reputation,
|
||||
change.reason,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Peer is currently banned, disconnect it from all protocols.
|
||||
lock.protocols.iter().for_each(|handle| handle.disconnect_peer(peer_id.into()));
|
||||
|
||||
// The peer is banned for the first time.
|
||||
if !was_banned {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Report {}: {:+} to {}. Reason: {}. Banned, disconnecting.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_reputation,
|
||||
change.reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The peer was already banned and it got another negative report.
|
||||
// This may happen during a batch report.
|
||||
if change.value < 0 {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Report {}: {:+} to {}. Reason: {}. Misbehaved during the ban threshold.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_reputation,
|
||||
change.reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set peer role.
|
||||
fn set_peer_role(&self, peer: &PeerId, role: ObservedRole) {
|
||||
self.0.lock().peers.entry(*peer).or_default().role = Some(role);
|
||||
}
|
||||
|
||||
/// Get peer reputation.
|
||||
fn peer_reputation(&self, peer: &PeerId) -> i32 {
|
||||
self.0.lock().peers.get(peer).map_or(0i32, |info| info.reputation)
|
||||
}
|
||||
|
||||
/// Get peer role, if available.
|
||||
fn peer_role(&self, peer: &PeerId) -> Option<ObservedRole> {
|
||||
self.0.lock().peers.get(peer).and_then(|info| info.role)
|
||||
}
|
||||
|
||||
/// Get candidates with highest reputations for initiating outgoing connections.
|
||||
fn outgoing_candidates(&self, count: usize, ignored: HashSet<PeerId>) -> Vec<PeerId> {
|
||||
let handle = self.0.lock();
|
||||
|
||||
let mut candidates = handle
|
||||
.peers
|
||||
.iter()
|
||||
.filter_map(|(peer, info)| {
|
||||
(!ignored.contains(&peer) && !info.is_banned()).then_some((*peer, info.reputation))
|
||||
})
|
||||
.collect::<Vec<(PeerId, _)>>();
|
||||
candidates.sort_by(|(_, a), (_, b)| b.cmp(a));
|
||||
candidates
|
||||
.into_iter()
|
||||
.take(count)
|
||||
.map(|(peer, _score)| peer)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Add known peer.
|
||||
fn add_known_peer(&self, peer: PeerId) {
|
||||
self.0.lock().peers.entry(peer).or_default().last_updated = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// `Peerstore` handle for testing.
|
||||
///
|
||||
/// This instance of `Peerstore` is not shared between protocols.
|
||||
#[cfg(test)]
|
||||
pub fn peerstore_handle_test() -> PeerstoreHandle {
|
||||
PeerstoreHandle(Arc::new(Mutex::new(Default::default())))
|
||||
}
|
||||
|
||||
/// Peerstore implementation.
|
||||
pub struct Peerstore {
|
||||
/// Handle to `Peerstore`.
|
||||
peerstore_handle: PeerstoreHandle,
|
||||
}
|
||||
|
||||
impl Peerstore {
|
||||
/// Create new [`Peerstore`].
|
||||
pub fn new(bootnodes: Vec<PeerId>, metrics_registry: Option<Registry>) -> Self {
|
||||
let metrics = if let Some(registry) = &metrics_registry {
|
||||
PeerStoreMetrics::register(registry)
|
||||
.map_err(|err| {
|
||||
log::error!(target: LOG_TARGET, "Failed to register peer store metrics: {}", err);
|
||||
err
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let peerstore_handle = PeerstoreHandle::new(
|
||||
bootnodes.iter().map(|peer_id| (*peer_id, PeerInfo::default())).collect(),
|
||||
Vec::new(),
|
||||
metrics,
|
||||
);
|
||||
|
||||
Self { peerstore_handle }
|
||||
}
|
||||
|
||||
/// Get mutable reference to the underlying [`PeerstoreHandle`].
|
||||
pub fn handle(&mut self) -> &mut PeerstoreHandle {
|
||||
&mut self.peerstore_handle
|
||||
}
|
||||
|
||||
/// Add known peer to [`Peerstore`].
|
||||
pub fn add_known_peer(&mut self, peer: PeerId) {
|
||||
self.peerstore_handle.add_known_peer(peer);
|
||||
}
|
||||
|
||||
/// Start [`Peerstore`] event loop.
|
||||
async fn run(self) {
|
||||
let started = Instant::now();
|
||||
let mut latest_time_update = started;
|
||||
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
// We basically do `(now - self.latest_update).as_secs()`, except that by the way we do
|
||||
// it we know that we're not going to miss seconds because of rounding to integers.
|
||||
let seconds_passed = {
|
||||
let elapsed_latest = latest_time_update - started;
|
||||
let elapsed_now = now - started;
|
||||
latest_time_update = now;
|
||||
elapsed_now.as_secs() - elapsed_latest.as_secs()
|
||||
};
|
||||
|
||||
self.peerstore_handle.progress_time(seconds_passed);
|
||||
let _ = Delay::new(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerStore for Peerstore {
|
||||
/// Get handle to `PeerStore`.
|
||||
fn handle(&self) -> Arc<dyn PeerStoreProvider> {
|
||||
Arc::new(self.peerstore_handle.clone())
|
||||
}
|
||||
|
||||
/// Start running `PeerStore` event loop.
|
||||
async fn run(self) {
|
||||
self.run().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PeerInfo, PeerStoreProvider, Peerstore};
|
||||
|
||||
#[test]
|
||||
fn decaying_zero_reputation_yields_zero() {
|
||||
let mut peer_info = PeerInfo::default();
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
|
||||
peer_info.decay_reputation(1);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
|
||||
peer_info.decay_reputation(100_000);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_positive_reputation_decreases_it() {
|
||||
const INITIAL_REPUTATION: i32 = 100;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(1);
|
||||
assert!(peer_info.reputation >= 0);
|
||||
assert!(peer_info.reputation < INITIAL_REPUTATION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_negative_reputation_increases_it() {
|
||||
const INITIAL_REPUTATION: i32 = -100;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(1);
|
||||
assert!(peer_info.reputation <= 0);
|
||||
assert!(peer_info.reputation > INITIAL_REPUTATION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_max_reputation_finally_yields_zero() {
|
||||
const INITIAL_REPUTATION: i32 = i32::MAX;
|
||||
const SECONDS: u64 = 3544;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert!(peer_info.reputation > 0);
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_min_reputation_finally_yields_zero() {
|
||||
const INITIAL_REPUTATION: i32 = i32::MIN;
|
||||
const SECONDS: u64 = 3544;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert!(peer_info.reputation < 0);
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_banned_peers() {
|
||||
let peer_a = pezsc_network_types::PeerId::random();
|
||||
let peer_b = pezsc_network_types::PeerId::random();
|
||||
let peer_c = pezsc_network_types::PeerId::random();
|
||||
|
||||
let metrics_registry = prometheus_endpoint::Registry::new();
|
||||
let mut peerstore = Peerstore::new(
|
||||
vec![peer_a, peer_b, peer_c].into_iter().map(Into::into).collect(),
|
||||
Some(metrics_registry),
|
||||
);
|
||||
let metrics = peerstore.peerstore_handle.0.lock().metrics.as_ref().unwrap().clone();
|
||||
let handle = peerstore.handle();
|
||||
|
||||
// Check initial state. Advance time to propagate peers.
|
||||
handle.progress_time(1);
|
||||
assert_eq!(metrics.num_discovered.get(), 3);
|
||||
assert_eq!(metrics.num_banned_peers.get(), 0);
|
||||
|
||||
// Report 2 peers with a negative reputation.
|
||||
handle.report_peer(
|
||||
peer_a,
|
||||
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
|
||||
);
|
||||
handle.report_peer(
|
||||
peer_b,
|
||||
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
|
||||
);
|
||||
|
||||
// Advance time to propagate peers.
|
||||
handle.progress_time(1);
|
||||
assert_eq!(metrics.num_discovered.get(), 3);
|
||||
assert_eq!(metrics.num_banned_peers.get(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `NetworkService` implementation for `litep2p`.
|
||||
|
||||
use crate::{
|
||||
config::MultiaddrWithPeerId,
|
||||
litep2p::shim::{
|
||||
notification::{config::ProtocolControlHandle, peerset::PeersetCommand},
|
||||
request_response::OutboundRequest,
|
||||
},
|
||||
network_state::NetworkState,
|
||||
peer_store::PeerStoreProvider,
|
||||
service::out_events,
|
||||
Event, IfDisconnected, NetworkDHTProvider, NetworkEventStream, NetworkPeers, NetworkRequest,
|
||||
NetworkSigner, NetworkStateInfo, NetworkStatus, NetworkStatusProvider, OutboundFailure,
|
||||
ProtocolName, RequestFailure, Signature,
|
||||
};
|
||||
|
||||
use codec::DecodeAll;
|
||||
use futures::{channel::oneshot, stream::BoxStream};
|
||||
use libp2p::identity::SigningError;
|
||||
use litep2p::{
|
||||
addresses::PublicAddresses, crypto::ed25519::Keypair,
|
||||
types::multiaddr::Multiaddr as LiteP2pMultiaddr,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use pezsc_network_types::kad::{Key as KademliaKey, Record};
|
||||
|
||||
use pezsc_network_common::{
|
||||
role::{ObservedRole, Roles},
|
||||
types::ReputationChange,
|
||||
};
|
||||
use pezsc_network_types::{
|
||||
multiaddr::{Multiaddr, Protocol},
|
||||
PeerId,
|
||||
};
|
||||
use pezsc_utils::mpsc::TracingUnboundedSender;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{atomic::Ordering, Arc},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p";
|
||||
|
||||
/// Commands sent by [`Litep2pNetworkService`] to
|
||||
/// [`Litep2pNetworkBackend`](super::Litep2pNetworkBackend).
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkServiceCommand {
|
||||
/// Find peers closest to `target` in the DHT.
|
||||
FindClosestPeers {
|
||||
/// Target peer ID.
|
||||
target: PeerId,
|
||||
},
|
||||
|
||||
/// Get value from DHT.
|
||||
GetValue {
|
||||
/// Record key.
|
||||
key: KademliaKey,
|
||||
},
|
||||
|
||||
/// Put value to DHT.
|
||||
PutValue {
|
||||
/// Record key.
|
||||
key: KademliaKey,
|
||||
|
||||
/// Record value.
|
||||
value: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Put value to DHT.
|
||||
PutValueTo {
|
||||
/// Record.
|
||||
record: Record,
|
||||
/// Peers we want to put the record.
|
||||
peers: Vec<pezsc_network_types::PeerId>,
|
||||
/// If we should update the local storage or not.
|
||||
update_local_storage: bool,
|
||||
},
|
||||
/// Store record in the local DHT store.
|
||||
StoreRecord {
|
||||
/// Record key.
|
||||
key: KademliaKey,
|
||||
|
||||
/// Record value.
|
||||
value: Vec<u8>,
|
||||
|
||||
/// Original publisher of the record.
|
||||
publisher: Option<PeerId>,
|
||||
|
||||
/// Record expiration time as measured by a local, monothonic clock.
|
||||
expires: Option<Instant>,
|
||||
},
|
||||
|
||||
/// Start providing `key`.
|
||||
StartProviding { key: KademliaKey },
|
||||
|
||||
/// Stop providing `key`.
|
||||
StopProviding { key: KademliaKey },
|
||||
|
||||
/// Get providers for `key`.
|
||||
GetProviders { key: KademliaKey },
|
||||
|
||||
/// Query network status.
|
||||
Status {
|
||||
/// `oneshot::Sender` for sending the status.
|
||||
tx: oneshot::Sender<NetworkStatus>,
|
||||
},
|
||||
|
||||
/// Add `peers` to `protocol`'s reserved set.
|
||||
AddPeersToReservedSet {
|
||||
/// Protocol.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Reserved peers.
|
||||
peers: HashSet<Multiaddr>,
|
||||
},
|
||||
|
||||
/// Add known address for peer.
|
||||
AddKnownAddress {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Address.
|
||||
address: Multiaddr,
|
||||
},
|
||||
|
||||
/// Set reserved peers for `protocol`.
|
||||
SetReservedPeers {
|
||||
/// Protocol.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Reserved peers.
|
||||
peers: HashSet<Multiaddr>,
|
||||
},
|
||||
|
||||
/// Disconnect peer from protocol.
|
||||
DisconnectPeer {
|
||||
/// Protocol.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
},
|
||||
|
||||
/// Set protocol to reserved only (true/false) mode.
|
||||
SetReservedOnly {
|
||||
/// Protocol.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Reserved only?
|
||||
reserved_only: bool,
|
||||
},
|
||||
|
||||
/// Remove reserved peers from protocol.
|
||||
RemoveReservedPeers {
|
||||
/// Protocol.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Peers to remove from the reserved set.
|
||||
peers: HashSet<PeerId>,
|
||||
},
|
||||
|
||||
/// Create event stream for DHT events.
|
||||
EventStream {
|
||||
/// Sender for the events.
|
||||
tx: out_events::Sender,
|
||||
},
|
||||
}
|
||||
|
||||
/// `NetworkService` implementation for `litep2p`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Litep2pNetworkService {
|
||||
/// Local peer ID.
|
||||
local_peer_id: litep2p::PeerId,
|
||||
|
||||
/// The `KeyPair` that defines the `PeerId` of the local node.
|
||||
keypair: Keypair,
|
||||
|
||||
/// TX channel for sending commands to [`Litep2pNetworkBackend`](super::Litep2pNetworkBackend).
|
||||
cmd_tx: TracingUnboundedSender<NetworkServiceCommand>,
|
||||
|
||||
/// Handle to `PeerStore`.
|
||||
peer_store_handle: Arc<dyn PeerStoreProvider>,
|
||||
|
||||
/// Peerset handles.
|
||||
peerset_handles: HashMap<ProtocolName, ProtocolControlHandle>,
|
||||
|
||||
/// Name for the block announce protocol.
|
||||
block_announce_protocol: ProtocolName,
|
||||
|
||||
/// Installed request-response protocols.
|
||||
request_response_protocols: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
|
||||
|
||||
/// Listen addresses.
|
||||
listen_addresses: Arc<RwLock<HashSet<LiteP2pMultiaddr>>>,
|
||||
|
||||
/// External addresses.
|
||||
external_addresses: PublicAddresses,
|
||||
}
|
||||
|
||||
impl Litep2pNetworkService {
|
||||
/// Create new [`Litep2pNetworkService`].
|
||||
pub fn new(
|
||||
local_peer_id: litep2p::PeerId,
|
||||
keypair: Keypair,
|
||||
cmd_tx: TracingUnboundedSender<NetworkServiceCommand>,
|
||||
peer_store_handle: Arc<dyn PeerStoreProvider>,
|
||||
peerset_handles: HashMap<ProtocolName, ProtocolControlHandle>,
|
||||
block_announce_protocol: ProtocolName,
|
||||
request_response_protocols: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
|
||||
listen_addresses: Arc<RwLock<HashSet<LiteP2pMultiaddr>>>,
|
||||
external_addresses: PublicAddresses,
|
||||
) -> Self {
|
||||
Self {
|
||||
local_peer_id,
|
||||
keypair,
|
||||
cmd_tx,
|
||||
peer_store_handle,
|
||||
peerset_handles,
|
||||
block_announce_protocol,
|
||||
request_response_protocols,
|
||||
listen_addresses,
|
||||
external_addresses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkSigner for Litep2pNetworkService {
|
||||
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError> {
|
||||
let public_key = self.keypair.public();
|
||||
let bytes = self.keypair.sign(msg.as_ref());
|
||||
|
||||
Ok(Signature {
|
||||
public_key: crate::service::signature::PublicKey::Litep2p(
|
||||
litep2p::crypto::PublicKey::Ed25519(public_key),
|
||||
),
|
||||
bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn verify(
|
||||
&self,
|
||||
peer: PeerId,
|
||||
public_key: &Vec<u8>,
|
||||
signature: &Vec<u8>,
|
||||
message: &Vec<u8>,
|
||||
) -> Result<bool, String> {
|
||||
let identity = litep2p::PeerId::from_public_key_protobuf(&public_key);
|
||||
let public_key = litep2p::crypto::RemotePublicKey::from_protobuf_encoding(&public_key)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let peer: litep2p::PeerId = peer.into();
|
||||
|
||||
Ok(peer == identity && public_key.verify(message, signature))
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkDHTProvider for Litep2pNetworkService {
|
||||
fn find_closest_peers(&self, target: PeerId) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::FindClosestPeers { target });
|
||||
}
|
||||
|
||||
fn get_value(&self, key: &KademliaKey) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::GetValue { key: key.clone() });
|
||||
}
|
||||
|
||||
fn put_value(&self, key: KademliaKey, value: Vec<u8>) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::PutValue { key, value });
|
||||
}
|
||||
|
||||
fn put_record_to(&self, record: Record, peers: HashSet<PeerId>, update_local_storage: bool) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::PutValueTo {
|
||||
record: Record {
|
||||
key: record.key.to_vec().into(),
|
||||
value: record.value,
|
||||
publisher: record.publisher.map(|peer_id| {
|
||||
let peer_id: pezsc_network_types::PeerId = peer_id.into();
|
||||
peer_id.into()
|
||||
}),
|
||||
expires: record.expires,
|
||||
},
|
||||
peers: peers.into_iter().collect(),
|
||||
update_local_storage,
|
||||
});
|
||||
}
|
||||
|
||||
fn store_record(
|
||||
&self,
|
||||
key: KademliaKey,
|
||||
value: Vec<u8>,
|
||||
publisher: Option<PeerId>,
|
||||
expires: Option<Instant>,
|
||||
) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::StoreRecord {
|
||||
key,
|
||||
value,
|
||||
publisher,
|
||||
expires,
|
||||
});
|
||||
}
|
||||
|
||||
fn start_providing(&self, key: KademliaKey) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::StartProviding { key });
|
||||
}
|
||||
|
||||
fn stop_providing(&self, key: KademliaKey) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::StopProviding { key });
|
||||
}
|
||||
|
||||
fn get_providers(&self, key: KademliaKey) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::GetProviders { key });
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkStatusProvider for Litep2pNetworkService {
|
||||
async fn status(&self) -> Result<NetworkStatus, ()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.cmd_tx
|
||||
.unbounded_send(NetworkServiceCommand::Status { tx })
|
||||
.map_err(|_| ())?;
|
||||
|
||||
rx.await.map_err(|_| ())
|
||||
}
|
||||
|
||||
async fn network_state(&self) -> Result<NetworkState, ()> {
|
||||
Ok(NetworkState {
|
||||
peer_id: self.local_peer_id.to_base58(),
|
||||
listened_addresses: self
|
||||
.listen_addresses
|
||||
.read()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|a| Multiaddr::from(a).into())
|
||||
.collect(),
|
||||
external_addresses: self
|
||||
.external_addresses
|
||||
.get_addresses()
|
||||
.into_iter()
|
||||
.map(|a| Multiaddr::from(a).into())
|
||||
.collect(),
|
||||
connected_peers: HashMap::new(),
|
||||
not_connected_peers: HashMap::new(),
|
||||
// TODO: Check what info we can include here.
|
||||
// Issue reference: https://github.com/pezkuwichain/kurdistan-sdk/issues/15.
|
||||
peerset: serde_json::json!(
|
||||
"Unimplemented. See https://github.com/pezkuwichain/kurdistan-sdk/issues/15."
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Manual implementation to avoid extra boxing here
|
||||
// TODO: functions modifying peerset state could be modified to call peerset directly if the
|
||||
// `Multiaddr` only contains a `PeerId`
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkPeers for Litep2pNetworkService {
|
||||
fn set_authorized_peers(&self, peers: HashSet<PeerId>) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedPeers {
|
||||
protocol: self.block_announce_protocol.clone(),
|
||||
peers: peers
|
||||
.into_iter()
|
||||
.map(|peer| Multiaddr::empty().with(Protocol::P2p(peer.into())))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
fn set_authorized_only(&self, reserved_only: bool) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedOnly {
|
||||
protocol: self.block_announce_protocol.clone(),
|
||||
reserved_only,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_known_address(&self, peer: PeerId, address: Multiaddr) {
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.unbounded_send(NetworkServiceCommand::AddKnownAddress { peer, address });
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
|
||||
self.peer_store_handle.peer_reputation(peer_id)
|
||||
}
|
||||
|
||||
fn report_peer(&self, peer: PeerId, cost_benefit: ReputationChange) {
|
||||
self.peer_store_handle.report_peer(peer, cost_benefit);
|
||||
}
|
||||
|
||||
fn disconnect_peer(&self, peer: PeerId, protocol: ProtocolName) {
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.unbounded_send(NetworkServiceCommand::DisconnectPeer { protocol, peer });
|
||||
}
|
||||
|
||||
fn accept_unreserved_peers(&self) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedOnly {
|
||||
protocol: self.block_announce_protocol.clone(),
|
||||
reserved_only: false,
|
||||
});
|
||||
}
|
||||
|
||||
fn deny_unreserved_peers(&self) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::SetReservedOnly {
|
||||
protocol: self.block_announce_protocol.clone(),
|
||||
reserved_only: true,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::AddPeersToReservedSet {
|
||||
protocol: self.block_announce_protocol.clone(),
|
||||
peers: HashSet::from_iter([peer.concat().into()]),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_reserved_peer(&self, peer: PeerId) {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::RemoveReservedPeers {
|
||||
protocol: self.block_announce_protocol.clone(),
|
||||
peers: HashSet::from_iter([peer]),
|
||||
});
|
||||
}
|
||||
|
||||
fn set_reserved_peers(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.unbounded_send(NetworkServiceCommand::SetReservedPeers { protocol, peers });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_peers_to_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.unbounded_send(NetworkServiceCommand::AddPeersToReservedSet { protocol, peers });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_peers_from_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: Vec<PeerId>,
|
||||
) -> Result<(), String> {
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::RemoveReservedPeers {
|
||||
protocol,
|
||||
peers: peers.into_iter().map(From::from).collect(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_num_connected(&self) -> usize {
|
||||
self.peerset_handles
|
||||
.get(&self.block_announce_protocol)
|
||||
.map_or(0usize, |handle| handle.connected_peers.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
fn peer_role(&self, peer: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
|
||||
match Roles::decode_all(&mut &handshake[..]) {
|
||||
Ok(role) => Some(role.into()),
|
||||
Err(_) => {
|
||||
log::debug!(target: LOG_TARGET, "handshake doesn't contain peer role: {handshake:?}");
|
||||
self.peer_store_handle.peer_role(&(peer.into()))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the list of reserved peers.
|
||||
///
|
||||
/// Returns an error if the `NetworkWorker` is no longer running.
|
||||
async fn reserved_peers(&self) -> Result<Vec<PeerId>, ()> {
|
||||
let Some(handle) = self.peerset_handles.get(&self.block_announce_protocol) else {
|
||||
return Err(());
|
||||
};
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
handle
|
||||
.tx
|
||||
.unbounded_send(PeersetCommand::GetReservedPeers { tx })
|
||||
.map_err(|_| ())?;
|
||||
|
||||
// the channel can only be closed if `Peerset` no longer exists
|
||||
rx.await.map_err(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkEventStream for Litep2pNetworkService {
|
||||
fn event_stream(&self, stream_name: &'static str) -> BoxStream<'static, Event> {
|
||||
let (tx, rx) = out_events::channel(stream_name, 100_000);
|
||||
let _ = self.cmd_tx.unbounded_send(NetworkServiceCommand::EventStream { tx });
|
||||
Box::pin(rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkStateInfo for Litep2pNetworkService {
|
||||
fn external_addresses(&self) -> Vec<Multiaddr> {
|
||||
self.external_addresses.get_addresses().into_iter().map(Into::into).collect()
|
||||
}
|
||||
|
||||
fn listen_addresses(&self) -> Vec<Multiaddr> {
|
||||
self.listen_addresses.read().iter().cloned().map(Into::into).collect()
|
||||
}
|
||||
|
||||
fn local_peer_id(&self) -> PeerId {
|
||||
self.local_peer_id.into()
|
||||
}
|
||||
}
|
||||
|
||||
// Manual implementation to avoid extra boxing here
|
||||
#[async_trait::async_trait]
|
||||
impl NetworkRequest for Litep2pNetworkService {
|
||||
async fn request(
|
||||
&self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
connect: IfDisconnected,
|
||||
) -> Result<(Vec<u8>, ProtocolName), RequestFailure> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
self.start_request(target, protocol, request, fallback_request, tx, connect);
|
||||
|
||||
match rx.await {
|
||||
Ok(v) => v,
|
||||
// The channel can only be closed if the network worker no longer exists. If the
|
||||
// network worker no longer exists, then all connections to `target` are necessarily
|
||||
// closed, and we legitimately report this situation as a "ConnectionClosed".
|
||||
Err(_) => Err(RequestFailure::Network(OutboundFailure::ConnectionClosed)),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_request(
|
||||
&self,
|
||||
peer: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
sender: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
) {
|
||||
match self.request_response_protocols.get(&protocol) {
|
||||
Some(tx) => {
|
||||
let _ = tx.unbounded_send(OutboundRequest::new(
|
||||
peer,
|
||||
request,
|
||||
sender,
|
||||
fallback_request,
|
||||
connect,
|
||||
));
|
||||
},
|
||||
None => log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{protocol} doesn't exist, cannot send request to {peer:?}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shim for litep2p's Bitswap implementation to make it work with `sc-network`.
|
||||
|
||||
use futures::StreamExt;
|
||||
use litep2p::protocol::libp2p::bitswap::{
|
||||
BitswapEvent, BitswapHandle, BlockPresenceType, Config, ResponseType, WantType,
|
||||
};
|
||||
|
||||
use pezsc_client_api::BlockBackend;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::bitswap";
|
||||
|
||||
pub struct BitswapServer<Block: BlockT> {
|
||||
/// Bitswap handle.
|
||||
handle: BitswapHandle,
|
||||
|
||||
/// Blockchain client.
|
||||
client: Arc<dyn BlockBackend<Block> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> BitswapServer<Block> {
|
||||
/// Create new [`BitswapServer`].
|
||||
pub fn new(
|
||||
client: Arc<dyn BlockBackend<Block> + Send + Sync>,
|
||||
) -> (Pin<Box<dyn Future<Output = ()> + Send>>, Config) {
|
||||
let (config, handle) = Config::new();
|
||||
let bitswap = Self { client, handle };
|
||||
|
||||
(Box::pin(async move { bitswap.run().await }), config)
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
log::debug!(target: LOG_TARGET, "starting bitswap server");
|
||||
|
||||
while let Some(event) = self.handle.next().await {
|
||||
match event {
|
||||
BitswapEvent::Request { peer, cids } => {
|
||||
log::debug!(target: LOG_TARGET, "handle bitswap request from {peer:?} for {cids:?}");
|
||||
|
||||
let response: Vec<ResponseType> = cids
|
||||
.into_iter()
|
||||
.map(|(cid, want_type)| {
|
||||
let mut hash = Block::Hash::default();
|
||||
hash.as_mut().copy_from_slice(&cid.hash().digest()[0..32]);
|
||||
let transaction = match self.client.indexed_transaction(hash) {
|
||||
Ok(ex) => ex,
|
||||
Err(error) => {
|
||||
log::error!(target: LOG_TARGET, "error retrieving transaction {hash}: {error}");
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
match transaction {
|
||||
Some(transaction) => {
|
||||
log::trace!(target: LOG_TARGET, "found cid {cid:?}, hash {hash:?}");
|
||||
|
||||
match want_type {
|
||||
WantType::Block =>
|
||||
ResponseType::Block { cid, block: transaction },
|
||||
_ => ResponseType::Presence {
|
||||
cid,
|
||||
presence: BlockPresenceType::Have,
|
||||
},
|
||||
}
|
||||
},
|
||||
None => {
|
||||
log::trace!(target: LOG_TARGET, "missing cid {cid:?}, hash {hash:?}");
|
||||
|
||||
ResponseType::Presence {
|
||||
cid,
|
||||
presence: BlockPresenceType::DontHave,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.handle.send_response(peer, response).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shims for fitting `litep2p` APIs to `sc-network` APIs.
|
||||
|
||||
pub(crate) mod bitswap;
|
||||
pub(crate) mod notification;
|
||||
pub(crate) mod request_response;
|
||||
@@ -0,0 +1,168 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `litep2p` notification protocol configuration.
|
||||
|
||||
use crate::{
|
||||
config::{MultiaddrWithPeerId, NonReservedPeerMode, NotificationHandshake, SetConfig},
|
||||
litep2p::shim::notification::{
|
||||
peerset::{Peerset, PeersetCommand},
|
||||
NotificationProtocol,
|
||||
},
|
||||
peer_store::PeerStoreProvider,
|
||||
service::{metrics::NotificationMetrics, traits::NotificationConfig},
|
||||
NotificationService, ProtocolName,
|
||||
};
|
||||
|
||||
use litep2p::protocol::notification::{Config, ConfigBuilder};
|
||||
|
||||
use pezsc_utils::mpsc::TracingUnboundedSender;
|
||||
|
||||
use std::sync::{atomic::AtomicUsize, Arc};
|
||||
|
||||
/// Handle for controlling the notification protocol.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProtocolControlHandle {
|
||||
/// TX channel for sending commands to `Peerset` of the notification protocol.
|
||||
pub tx: TracingUnboundedSender<PeersetCommand>,
|
||||
|
||||
/// Peers currently connected to this protocol.
|
||||
pub connected_peers: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl ProtocolControlHandle {
|
||||
/// Create new [`ProtocolControlHandle`].
|
||||
pub fn new(
|
||||
tx: TracingUnboundedSender<PeersetCommand>,
|
||||
connected_peers: Arc<AtomicUsize>,
|
||||
) -> Self {
|
||||
Self { tx, connected_peers }
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the notification protocol.
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationProtocolConfig {
|
||||
/// Name of the notifications protocols of this set. A substream on this set will be
|
||||
/// considered established once this protocol is open.
|
||||
pub protocol_name: ProtocolName,
|
||||
|
||||
/// Maximum allowed size of single notifications.
|
||||
max_notification_size: usize,
|
||||
|
||||
/// Base configuration.
|
||||
set_config: SetConfig,
|
||||
|
||||
/// `litep2p` notification config.
|
||||
pub config: Config,
|
||||
|
||||
/// Handle for controlling the notification protocol.
|
||||
pub handle: ProtocolControlHandle,
|
||||
}
|
||||
|
||||
impl NotificationProtocolConfig {
|
||||
// Create new [`NotificationProtocolConfig`].
|
||||
pub fn new(
|
||||
protocol_name: ProtocolName,
|
||||
fallback_names: Vec<ProtocolName>,
|
||||
max_notification_size: usize,
|
||||
handshake: Option<NotificationHandshake>,
|
||||
set_config: SetConfig,
|
||||
metrics: NotificationMetrics,
|
||||
peerstore_handle: Arc<dyn PeerStoreProvider>,
|
||||
) -> (Self, Box<dyn NotificationService>) {
|
||||
// create `Peerset`/`Peerstore` handle for the protocol
|
||||
let connected_peers = Arc::new(Default::default());
|
||||
let (peerset, peerset_tx) = Peerset::new(
|
||||
protocol_name.clone(),
|
||||
set_config.out_peers as usize,
|
||||
set_config.in_peers as usize,
|
||||
set_config.non_reserved_mode == NonReservedPeerMode::Deny,
|
||||
set_config.reserved_nodes.iter().map(|address| address.peer_id).collect(),
|
||||
Arc::clone(&connected_peers),
|
||||
peerstore_handle,
|
||||
);
|
||||
|
||||
// create `litep2p` notification protocol configuration for the protocol
|
||||
//
|
||||
// NOTE: currently only dummy value is given as the handshake as protocols (apart from
|
||||
// syncing) are not configuring their own handshake and instead default to role being the
|
||||
// handshake. As the time of writing this, most protocols are not aware of the role and
|
||||
// that should be refactored in the future.
|
||||
let (config, handle) = ConfigBuilder::new(protocol_name.clone().into())
|
||||
.with_handshake(handshake.map_or(vec![1], |handshake| (*handshake).to_vec()))
|
||||
.with_max_size(max_notification_size as usize)
|
||||
.with_auto_accept_inbound(true)
|
||||
.with_fallback_names(fallback_names.into_iter().map(From::from).collect())
|
||||
.build();
|
||||
|
||||
// initialize the actual object implementing `NotificationService` and combine the
|
||||
// `litep2p::NotificationHandle` with `Peerset` to implement a full and independent
|
||||
// notification protocol runner
|
||||
let protocol = NotificationProtocol::new(protocol_name.clone(), handle, peerset, metrics);
|
||||
|
||||
(
|
||||
Self {
|
||||
protocol_name,
|
||||
max_notification_size,
|
||||
set_config,
|
||||
config,
|
||||
handle: ProtocolControlHandle::new(peerset_tx, connected_peers),
|
||||
},
|
||||
Box::new(protocol),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get reference to protocol name.
|
||||
pub fn protocol_name(&self) -> &ProtocolName {
|
||||
&self.protocol_name
|
||||
}
|
||||
|
||||
/// Get reference to `SetConfig`.
|
||||
pub fn set_config(&self) -> &SetConfig {
|
||||
&self.set_config
|
||||
}
|
||||
|
||||
/// Modifies the configuration to allow non-reserved nodes.
|
||||
pub fn allow_non_reserved(&mut self, in_peers: u32, out_peers: u32) {
|
||||
self.set_config.in_peers = in_peers;
|
||||
self.set_config.out_peers = out_peers;
|
||||
self.set_config.non_reserved_mode = NonReservedPeerMode::Accept;
|
||||
}
|
||||
|
||||
/// Add a node to the list of reserved nodes.
|
||||
pub fn add_reserved(&mut self, peer: MultiaddrWithPeerId) {
|
||||
self.set_config.reserved_nodes.push(peer);
|
||||
}
|
||||
|
||||
/// Get maximum notification size.
|
||||
pub fn max_notification_size(&self) -> usize {
|
||||
self.max_notification_size
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationConfig for NotificationProtocolConfig {
|
||||
fn set_config(&self) -> &SetConfig {
|
||||
&self.set_config
|
||||
}
|
||||
|
||||
/// Get reference to protocol name.
|
||||
fn protocol_name(&self) -> &ProtocolName {
|
||||
&self.protocol_name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shim for `litep2p::NotificationHandle` to combine `Peerset`-like behavior
|
||||
//! with `NotificationService`.
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
litep2p::shim::notification::peerset::{OpenResult, Peerset, PeersetNotificationCommand},
|
||||
service::{
|
||||
metrics::NotificationMetrics,
|
||||
traits::{NotificationEvent as BizinikiwiNotificationEvent, ValidationResult},
|
||||
},
|
||||
MessageSink, NotificationService, ProtocolName,
|
||||
};
|
||||
|
||||
use futures::{future::BoxFuture, stream::FuturesUnordered, StreamExt};
|
||||
use litep2p::protocol::notification::{
|
||||
NotificationEvent, NotificationHandle, NotificationSink,
|
||||
ValidationResult as Litep2pValidationResult,
|
||||
};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
use std::{collections::HashSet, fmt};
|
||||
|
||||
pub mod config;
|
||||
pub mod peerset;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::notification";
|
||||
|
||||
/// Wrapper over `litep2p`'s notification sink.
|
||||
pub struct Litep2pMessageSink {
|
||||
/// Protocol.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Remote peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Notification sink.
|
||||
sink: NotificationSink,
|
||||
|
||||
/// Notification metrics.
|
||||
metrics: NotificationMetrics,
|
||||
}
|
||||
|
||||
impl Litep2pMessageSink {
|
||||
/// Create new [`Litep2pMessageSink`].
|
||||
fn new(
|
||||
peer: PeerId,
|
||||
protocol: ProtocolName,
|
||||
sink: NotificationSink,
|
||||
metrics: NotificationMetrics,
|
||||
) -> Self {
|
||||
Self { protocol, peer, sink, metrics }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MessageSink for Litep2pMessageSink {
|
||||
/// Send synchronous `notification` to the peer associated with this [`MessageSink`].
|
||||
fn send_sync_notification(&self, notification: Vec<u8>) {
|
||||
let size = notification.len();
|
||||
|
||||
match self.sink.send_sync_notification(notification) {
|
||||
Ok(_) => self.metrics.register_notification_sent(&self.protocol, size),
|
||||
Err(error) => log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: failed to send sync notification to {:?}: {error:?}",
|
||||
self.protocol,
|
||||
self.peer,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an asynchronous `notification` to to the peer associated with this [`MessageSink`],
|
||||
/// allowing sender to exercise backpressure.
|
||||
///
|
||||
/// Returns an error if the peer does not exist.
|
||||
async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), Error> {
|
||||
let size = notification.len();
|
||||
|
||||
match self.sink.send_async_notification(notification).await {
|
||||
Ok(_) => {
|
||||
self.metrics.register_notification_sent(&self.protocol, size);
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: failed to send async notification to {:?}: {error:?}",
|
||||
self.protocol,
|
||||
self.peer,
|
||||
);
|
||||
|
||||
Err(Error::Litep2p(error))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification protocol implementation.
|
||||
pub struct NotificationProtocol {
|
||||
/// Protocol name.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// `litep2p` notification handle.
|
||||
handle: NotificationHandle,
|
||||
|
||||
/// Peerset for the notification protocol.
|
||||
///
|
||||
/// Listens to peering-related events and either opens or closes substreams to remote peers.
|
||||
peerset: Peerset,
|
||||
|
||||
/// Pending validations for inbound substreams.
|
||||
pending_validations: FuturesUnordered<
|
||||
BoxFuture<'static, (PeerId, Result<ValidationResult, oneshot::error::RecvError>)>,
|
||||
>,
|
||||
|
||||
/// Pending cancels.
|
||||
pending_cancels: HashSet<litep2p::PeerId>,
|
||||
|
||||
/// Notification metrics.
|
||||
metrics: NotificationMetrics,
|
||||
}
|
||||
|
||||
impl fmt::Debug for NotificationProtocol {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NotificationProtocol")
|
||||
.field("protocol", &self.protocol)
|
||||
.field("handle", &self.handle)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationProtocol {
|
||||
/// Create new [`NotificationProtocol`].
|
||||
pub fn new(
|
||||
protocol: ProtocolName,
|
||||
handle: NotificationHandle,
|
||||
peerset: Peerset,
|
||||
metrics: NotificationMetrics,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol,
|
||||
handle,
|
||||
peerset,
|
||||
metrics,
|
||||
pending_cancels: HashSet::new(),
|
||||
pending_validations: FuturesUnordered::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle `Peerset` command.
|
||||
async fn on_peerset_command(&mut self, command: PeersetNotificationCommand) {
|
||||
if !command.open_peers.is_empty() {
|
||||
log::trace!(target: LOG_TARGET, "{}: open substreams to {:?}", self.protocol, command.open_peers);
|
||||
let _ = self
|
||||
.handle
|
||||
.open_substream_batch(command.open_peers.into_iter().map(From::from))
|
||||
.await;
|
||||
}
|
||||
|
||||
if !command.close_peers.is_empty() {
|
||||
log::trace!(target: LOG_TARGET, "{}: close substreams to {:?}", self.protocol, command.close_peers);
|
||||
self.handle
|
||||
.close_substream_batch(command.close_peers.into_iter().map(From::from))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NotificationService for NotificationProtocol {
|
||||
async fn open_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
async fn close_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn send_sync_notification(&mut self, peer: &PeerId, notification: Vec<u8>) {
|
||||
let size = notification.len();
|
||||
|
||||
if let Ok(_) = self.handle.send_sync_notification(peer.into(), notification) {
|
||||
self.metrics.register_notification_sent(&self.protocol, size);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_async_notification(
|
||||
&mut self,
|
||||
peer: &PeerId,
|
||||
notification: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let size = notification.len();
|
||||
|
||||
match self.handle.send_async_notification(peer.into(), notification).await {
|
||||
Ok(_) => {
|
||||
self.metrics.register_notification_sent(&self.protocol, size);
|
||||
Ok(())
|
||||
},
|
||||
Err(_) => Err(Error::ChannelClosed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set handshake for the notification protocol replacing the old handshake.
|
||||
async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
|
||||
self.handle.set_handshake(handshake);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set handshake for the notification protocol replacing the old handshake.
|
||||
///
|
||||
/// For `litep2p` this is identical to `NotificationService::set_handshake()` since `litep2p`
|
||||
/// allows updating the handshake synchronously.
|
||||
fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
|
||||
self.handle.set_handshake(handshake);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Make a copy of the object so it can be shared between protocol components
|
||||
/// who wish to have access to the same underlying notification protocol.
|
||||
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()> {
|
||||
unimplemented!("clonable `NotificationService` not supported by `litep2p`");
|
||||
}
|
||||
|
||||
/// Get protocol name of the `NotificationService`.
|
||||
fn protocol(&self) -> &ProtocolName {
|
||||
&self.protocol
|
||||
}
|
||||
|
||||
/// Get message sink of the peer.
|
||||
fn message_sink(&self, peer: &PeerId) -> Option<Box<dyn MessageSink>> {
|
||||
self.handle.notification_sink(peer.into()).map(|sink| {
|
||||
let sink: Box<dyn MessageSink> = Box::new(Litep2pMessageSink::new(
|
||||
*peer,
|
||||
self.protocol.clone(),
|
||||
sink,
|
||||
self.metrics.clone(),
|
||||
));
|
||||
sink
|
||||
})
|
||||
}
|
||||
|
||||
/// Get next event from the `Notifications` event stream.
|
||||
async fn next_event(&mut self) -> Option<BizinikiwiNotificationEvent> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
event = self.handle.next() => match event? {
|
||||
NotificationEvent::ValidateSubstream { peer, handshake, .. } => {
|
||||
if let ValidationResult::Reject = self.peerset.report_inbound_substream(peer.into()) {
|
||||
self.handle.send_validation_result(peer, Litep2pValidationResult::Reject);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.pending_validations.push(Box::pin(async move { (peer.into(), rx.await) }));
|
||||
|
||||
log::trace!(target: LOG_TARGET, "{}: validate substream for {peer:?}", self.protocol);
|
||||
|
||||
return Some(BizinikiwiNotificationEvent::ValidateInboundSubstream {
|
||||
peer: peer.into(),
|
||||
handshake,
|
||||
result_tx: tx,
|
||||
});
|
||||
}
|
||||
NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
fallback,
|
||||
handshake,
|
||||
direction,
|
||||
..
|
||||
} => {
|
||||
self.metrics.register_substream_opened(&self.protocol);
|
||||
|
||||
match self.peerset.report_substream_opened(peer.into(), direction.into()) {
|
||||
OpenResult::Reject => {
|
||||
let _ = self.handle.close_substream_batch(vec![peer].into_iter().map(From::from)).await;
|
||||
self.pending_cancels.insert(peer);
|
||||
|
||||
continue
|
||||
}
|
||||
OpenResult::Accept { direction } => {
|
||||
log::trace!(target: LOG_TARGET, "{}: substream opened for {peer:?}", self.protocol);
|
||||
|
||||
return Some(BizinikiwiNotificationEvent::NotificationStreamOpened {
|
||||
peer: peer.into(),
|
||||
handshake,
|
||||
direction,
|
||||
negotiated_fallback: fallback.map(From::from),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
NotificationEvent::NotificationStreamClosed {
|
||||
peer,
|
||||
} => {
|
||||
log::trace!(target: LOG_TARGET, "{}: substream closed for {peer:?}", self.protocol);
|
||||
|
||||
self.metrics.register_substream_closed(&self.protocol);
|
||||
self.peerset.report_substream_closed(peer.into());
|
||||
|
||||
if self.pending_cancels.remove(&peer) {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"{}: substream closed to canceled peer ({peer:?})",
|
||||
self.protocol
|
||||
);
|
||||
continue
|
||||
}
|
||||
|
||||
return Some(BizinikiwiNotificationEvent::NotificationStreamClosed { peer: peer.into() })
|
||||
}
|
||||
NotificationEvent::NotificationStreamOpenFailure {
|
||||
peer,
|
||||
error,
|
||||
} => {
|
||||
log::trace!(target: LOG_TARGET, "{}: open failure for {peer:?}", self.protocol);
|
||||
self.peerset.report_substream_open_failure(peer.into(), error);
|
||||
}
|
||||
NotificationEvent::NotificationReceived {
|
||||
peer,
|
||||
notification,
|
||||
} => {
|
||||
self.metrics.register_notification_received(&self.protocol, notification.len());
|
||||
|
||||
if !self.pending_cancels.contains(&peer) {
|
||||
return Some(BizinikiwiNotificationEvent::NotificationReceived {
|
||||
peer: peer.into(),
|
||||
notification: notification.to_vec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
result = self.pending_validations.next(), if !self.pending_validations.is_empty() => {
|
||||
let (peer, result) = result?;
|
||||
let validation_result = match result {
|
||||
Ok(ValidationResult::Accept) => Litep2pValidationResult::Accept,
|
||||
_ => {
|
||||
self.peerset.report_substream_rejected(peer);
|
||||
Litep2pValidationResult::Reject
|
||||
}
|
||||
};
|
||||
|
||||
self.handle.send_validation_result(peer.into(), validation_result);
|
||||
}
|
||||
command = self.peerset.next() => self.on_peerset_command(command?).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,383 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Fuzz test emulates network events and peer connection handling by `Peerset`
|
||||
//! and `PeerStore` to discover possible inconsistencies in peer management.
|
||||
|
||||
use crate::{
|
||||
litep2p::{
|
||||
peerstore::Peerstore,
|
||||
shim::notification::peerset::{OpenResult, Peerset, PeersetCommand},
|
||||
},
|
||||
service::traits::{Direction, PeerStore, ValidationResult},
|
||||
ProtocolName,
|
||||
};
|
||||
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use litep2p::protocol::notification::NotificationError;
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform, WeightedIndex},
|
||||
seq::IteratorRandom,
|
||||
};
|
||||
|
||||
use pezsc_network_common::types::ReputationChange;
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(debug_assertions)]
|
||||
async fn run() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
for _ in 0..50 {
|
||||
test_once().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn test_once() {
|
||||
// PRNG to use.
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// peers that the peerset knows about.
|
||||
let mut known_peers = HashSet::<PeerId>::new();
|
||||
|
||||
// peers that we have reserved. Always a subset of `known_peers`.
|
||||
let mut reserved_peers = HashSet::<PeerId>::new();
|
||||
|
||||
// reserved only mode
|
||||
let mut reserved_only = Uniform::new_inclusive(0, 10).sample(&mut rng) == 0;
|
||||
|
||||
// Bootnodes for `PeerStore` initialization.
|
||||
let bootnodes = (0..Uniform::new_inclusive(0, 4).sample(&mut rng))
|
||||
.map(|_| {
|
||||
let id = PeerId::random();
|
||||
known_peers.insert(id);
|
||||
id
|
||||
})
|
||||
.collect();
|
||||
|
||||
let peerstore = Peerstore::new(bootnodes, None);
|
||||
let peer_store_handle = peerstore.handle();
|
||||
|
||||
let (mut peerset, to_peerset) = Peerset::new(
|
||||
ProtocolName::from("/notif/1"),
|
||||
Uniform::new_inclusive(0, 25).sample(&mut rng),
|
||||
Uniform::new_inclusive(0, 25).sample(&mut rng),
|
||||
reserved_only,
|
||||
(0..Uniform::new_inclusive(0, 2).sample(&mut rng))
|
||||
.map(|_| {
|
||||
let id = PeerId::random();
|
||||
known_peers.insert(id);
|
||||
reserved_peers.insert(id);
|
||||
id
|
||||
})
|
||||
.collect(),
|
||||
Default::default(),
|
||||
Arc::clone(&peer_store_handle),
|
||||
);
|
||||
|
||||
tokio::spawn(peerstore.run());
|
||||
|
||||
// opening substreams
|
||||
let mut opening = HashMap::<PeerId, Direction>::new();
|
||||
|
||||
// open substreams
|
||||
let mut open = HashMap::<PeerId, Direction>::new();
|
||||
|
||||
// closing substreams
|
||||
let mut closing = HashSet::<PeerId>::new();
|
||||
|
||||
// closed substreams
|
||||
let mut closed = HashSet::<PeerId>::new();
|
||||
|
||||
// perform a certain number of actions while checking that the state is consistent.
|
||||
//
|
||||
// if we reach the end of the loop, the run has succeeded
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
// PRNG to use in `spawn_blocking` context.
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for _ in 0..2500 {
|
||||
// each of these weights corresponds to an action that we may perform
|
||||
let action_weights =
|
||||
[300, 110, 110, 110, 110, 90, 70, 30, 110, 110, 110, 110, 20, 110, 50, 110];
|
||||
|
||||
match WeightedIndex::new(&action_weights).unwrap().sample(&mut rng) {
|
||||
0 => match peerset.next().now_or_never() {
|
||||
Some(Some(command)) => {
|
||||
// open substreams to `peers`
|
||||
for peer in command.open_peers {
|
||||
opening.insert(peer, Direction::Outbound);
|
||||
closed.remove(&peer);
|
||||
|
||||
assert!(!closing.contains(&peer));
|
||||
assert!(!open.contains_key(&peer));
|
||||
}
|
||||
|
||||
// close substreams to `peers`
|
||||
for peer in command.close_peers {
|
||||
assert!(closing.insert(peer));
|
||||
assert!(open.remove(&peer).is_some());
|
||||
assert!(!opening.contains_key(&peer));
|
||||
}
|
||||
},
|
||||
Some(None) => panic!("peerset exited"),
|
||||
None => {},
|
||||
},
|
||||
// get inbound connection from an unknown peer
|
||||
1 => {
|
||||
let new_peer = PeerId::random();
|
||||
peer_store_handle.add_known_peer(new_peer);
|
||||
|
||||
match peerset.report_inbound_substream(new_peer) {
|
||||
ValidationResult::Accept => {
|
||||
opening.insert(new_peer, Direction::Inbound);
|
||||
},
|
||||
ValidationResult::Reject => {},
|
||||
}
|
||||
},
|
||||
// substream opened successfully
|
||||
//
|
||||
// remove peer from `opening` (which contains its direction), report the open
|
||||
// substream to `Peerset` and move peer state to `open`.
|
||||
//
|
||||
// if the substream was canceled while it was opening, move peer to `closing`
|
||||
2 =>
|
||||
if let Some(peer) = opening.keys().choose(&mut rng).copied() {
|
||||
let direction = opening.remove(&peer).unwrap();
|
||||
match peerset.report_substream_opened(peer, direction) {
|
||||
OpenResult::Accept { .. } => {
|
||||
assert!(open.insert(peer, direction).is_none());
|
||||
},
|
||||
OpenResult::Reject => {
|
||||
assert!(closing.insert(peer));
|
||||
},
|
||||
}
|
||||
},
|
||||
// substream failed to open
|
||||
3 =>
|
||||
if let Some(peer) = opening.keys().choose(&mut rng).copied() {
|
||||
let _ = opening.remove(&peer).unwrap();
|
||||
peerset.report_substream_open_failure(peer, NotificationError::Rejected);
|
||||
},
|
||||
// substream was closed by remote peer
|
||||
4 =>
|
||||
if let Some(peer) = open.keys().choose(&mut rng).copied() {
|
||||
let _ = open.remove(&peer).unwrap();
|
||||
peerset.report_substream_closed(peer);
|
||||
assert!(closed.insert(peer));
|
||||
},
|
||||
// substream was closed by local node
|
||||
5 =>
|
||||
if let Some(peer) = closing.iter().choose(&mut rng).copied() {
|
||||
assert!(closing.remove(&peer));
|
||||
assert!(closed.insert(peer));
|
||||
peerset.report_substream_closed(peer);
|
||||
},
|
||||
// random connected peer was disconnected by the protocol
|
||||
6 =>
|
||||
if let Some(peer) = open.keys().choose(&mut rng).copied() {
|
||||
to_peerset.unbounded_send(PeersetCommand::DisconnectPeer { peer }).unwrap();
|
||||
},
|
||||
// ban random peer
|
||||
7 =>
|
||||
if let Some(peer) = known_peers.iter().choose(&mut rng).copied() {
|
||||
peer_store_handle.report_peer(peer, ReputationChange::new_fatal(""));
|
||||
},
|
||||
// inbound substream is received for a peer that was considered
|
||||
// outbound
|
||||
8 => {
|
||||
let outbound_peers = opening
|
||||
.iter()
|
||||
.filter_map(|(peer, direction)| {
|
||||
std::matches!(direction, Direction::Outbound).then_some(*peer)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
if let Some(peer) = outbound_peers.iter().choose(&mut rng).copied() {
|
||||
match peerset.report_inbound_substream(peer) {
|
||||
ValidationResult::Accept => {
|
||||
opening.insert(peer, Direction::Inbound);
|
||||
},
|
||||
ValidationResult::Reject => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
// set reserved peers
|
||||
//
|
||||
// choose peers from all available sets (open, opening, closing, closed) + some new
|
||||
// peers
|
||||
9 => {
|
||||
let num_open = Uniform::new_inclusive(0, open.len()).sample(&mut rng);
|
||||
let num_opening = Uniform::new_inclusive(0, opening.len()).sample(&mut rng);
|
||||
let num_closing = Uniform::new_inclusive(0, closing.len()).sample(&mut rng);
|
||||
let num_closed = Uniform::new_inclusive(0, closed.len()).sample(&mut rng);
|
||||
|
||||
let peers = open
|
||||
.keys()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_open)
|
||||
.into_iter()
|
||||
.chain(
|
||||
opening
|
||||
.keys()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_opening)
|
||||
.into_iter(),
|
||||
)
|
||||
.chain(
|
||||
closing
|
||||
.iter()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_closing)
|
||||
.into_iter(),
|
||||
)
|
||||
.chain(
|
||||
closed
|
||||
.iter()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_closed)
|
||||
.into_iter(),
|
||||
)
|
||||
.chain((0..5).map(|_| {
|
||||
let peer = PeerId::random();
|
||||
known_peers.insert(peer);
|
||||
peer_store_handle.add_known_peer(peer);
|
||||
peer
|
||||
}))
|
||||
.filter(|peer| !reserved_peers.contains(peer))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
reserved_peers.extend(peers.clone().into_iter());
|
||||
to_peerset.unbounded_send(PeersetCommand::SetReservedPeers { peers }).unwrap();
|
||||
},
|
||||
// add reserved peers
|
||||
10 => {
|
||||
let num_open = Uniform::new_inclusive(0, open.len()).sample(&mut rng);
|
||||
let num_opening = Uniform::new_inclusive(0, opening.len()).sample(&mut rng);
|
||||
let num_closing = Uniform::new_inclusive(0, closing.len()).sample(&mut rng);
|
||||
let num_closed = Uniform::new_inclusive(0, closed.len()).sample(&mut rng);
|
||||
|
||||
let peers = open
|
||||
.keys()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_open)
|
||||
.into_iter()
|
||||
.chain(
|
||||
opening
|
||||
.keys()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_opening)
|
||||
.into_iter(),
|
||||
)
|
||||
.chain(
|
||||
closing
|
||||
.iter()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_closing)
|
||||
.into_iter(),
|
||||
)
|
||||
.chain(
|
||||
closed
|
||||
.iter()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_closed)
|
||||
.into_iter(),
|
||||
)
|
||||
.chain((0..5).map(|_| {
|
||||
let peer = PeerId::random();
|
||||
known_peers.insert(peer);
|
||||
peer_store_handle.add_known_peer(peer);
|
||||
peer
|
||||
}))
|
||||
.filter(|peer| !reserved_peers.contains(peer))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
reserved_peers.extend(peers.clone().into_iter());
|
||||
to_peerset.unbounded_send(PeersetCommand::AddReservedPeers { peers }).unwrap();
|
||||
},
|
||||
// remove reserved peers
|
||||
11 => {
|
||||
let num_to_remove =
|
||||
Uniform::new_inclusive(0, reserved_peers.len()).sample(&mut rng);
|
||||
let peers = reserved_peers
|
||||
.iter()
|
||||
.copied()
|
||||
.choose_multiple(&mut rng, num_to_remove)
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
peers.iter().for_each(|peer| {
|
||||
assert!(reserved_peers.remove(peer));
|
||||
});
|
||||
|
||||
to_peerset
|
||||
.unbounded_send(PeersetCommand::RemoveReservedPeers { peers })
|
||||
.unwrap();
|
||||
},
|
||||
// set reserved only
|
||||
12 => {
|
||||
reserved_only = !reserved_only;
|
||||
|
||||
let _ = to_peerset
|
||||
.unbounded_send(PeersetCommand::SetReservedOnly { reserved_only });
|
||||
},
|
||||
//
|
||||
// discover a new node.
|
||||
13 => {
|
||||
let new_peer = PeerId::random();
|
||||
known_peers.insert(new_peer);
|
||||
peer_store_handle.add_known_peer(new_peer);
|
||||
},
|
||||
// protocol rejected a substream that was accepted by `Peerset`
|
||||
14 => {
|
||||
let inbound_peers = opening
|
||||
.iter()
|
||||
.filter_map(|(peer, direction)| {
|
||||
std::matches!(direction, Direction::Inbound).then_some(*peer)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
if let Some(peer) = inbound_peers.iter().choose(&mut rng).copied() {
|
||||
peerset.report_substream_rejected(peer);
|
||||
opening.remove(&peer);
|
||||
}
|
||||
},
|
||||
// inbound substream received for a peer in `closed`
|
||||
15 =>
|
||||
if let Some(peer) = closed.iter().choose(&mut rng).copied() {
|
||||
match peerset.report_inbound_substream(peer) {
|
||||
ValidationResult::Accept => {
|
||||
assert!(closed.remove(&peer));
|
||||
opening.insert(peer, Direction::Inbound);
|
||||
},
|
||||
ValidationResult::Reject => {},
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#[cfg(test)]
|
||||
mod fuzz;
|
||||
#[cfg(test)]
|
||||
mod peerset;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Metrics for [`RequestResponseProtocol`](super::RequestResponseProtocol).
|
||||
|
||||
use crate::{service::metrics::Metrics, types::ProtocolName};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// Request-response metrics.
|
||||
pub struct RequestResponseMetrics {
|
||||
/// Metrics.
|
||||
metrics: Option<Metrics>,
|
||||
|
||||
/// Protocol name.
|
||||
protocol: ProtocolName,
|
||||
}
|
||||
|
||||
impl RequestResponseMetrics {
|
||||
pub fn new(metrics: Option<Metrics>, protocol: ProtocolName) -> Self {
|
||||
Self { metrics, protocol }
|
||||
}
|
||||
|
||||
/// Register inbound request failure to Prometheus
|
||||
pub fn register_inbound_request_failure(&self, reason: &str) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.requests_in_failure_total
|
||||
.with_label_values(&[&self.protocol, reason])
|
||||
.inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register inbound request success to Prometheus
|
||||
pub fn register_inbound_request_success(&self, serve_time: Duration) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.requests_in_success_total
|
||||
.with_label_values(&[&self.protocol])
|
||||
.observe(serve_time.as_secs_f64());
|
||||
}
|
||||
}
|
||||
|
||||
/// Register inbound request failure to Prometheus
|
||||
pub fn register_outbound_request_failure(&self, reason: &str) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.requests_out_failure_total
|
||||
.with_label_values(&[&self.protocol, reason])
|
||||
.inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register inbound request success to Prometheus
|
||||
pub fn register_outbound_request_success(&self, duration: Duration) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.requests_out_success_total
|
||||
.with_label_values(&[&self.protocol])
|
||||
.observe(duration.as_secs_f64());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Shim for litep2p's request-response implementation to make it work with `pezsc_network`'s
|
||||
//! request-response API.
|
||||
|
||||
use crate::{
|
||||
litep2p::shim::request_response::metrics::RequestResponseMetrics,
|
||||
peer_store::PeerStoreProvider,
|
||||
request_responses::{IncomingRequest, OutgoingResponse},
|
||||
service::{metrics::Metrics, traits::RequestResponseConfig as RequestResponseConfigT},
|
||||
IfDisconnected, OutboundFailure, ProtocolName, RequestFailure,
|
||||
};
|
||||
|
||||
use futures::{channel::oneshot, future::BoxFuture, stream::FuturesUnordered, StreamExt};
|
||||
use litep2p::{
|
||||
error::{ImmediateDialError, NegotiationError, SubstreamError},
|
||||
protocol::request_response::{
|
||||
DialOptions, RejectReason, RequestResponseError, RequestResponseEvent,
|
||||
RequestResponseHandle,
|
||||
},
|
||||
types::RequestId,
|
||||
};
|
||||
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsc_utils::mpsc::{TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod metrics;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::request-response";
|
||||
|
||||
/// Type containing information related to an outbound request.
|
||||
#[derive(Debug)]
|
||||
pub struct OutboundRequest {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Request.
|
||||
request: Vec<u8>,
|
||||
|
||||
/// Fallback request, if provided.
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
|
||||
/// `oneshot::Sender` for sending the received response, or failure.
|
||||
sender: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
|
||||
/// What should the node do if `peer` is disconnected.
|
||||
dial_behavior: IfDisconnected,
|
||||
}
|
||||
|
||||
impl OutboundRequest {
|
||||
/// Create new [`OutboundRequest`].
|
||||
pub fn new(
|
||||
peer: PeerId,
|
||||
request: Vec<u8>,
|
||||
sender: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
dial_behavior: IfDisconnected,
|
||||
) -> Self {
|
||||
OutboundRequest { peer, request, sender, fallback_request, dial_behavior }
|
||||
}
|
||||
}
|
||||
|
||||
/// Pending request.
|
||||
struct PendingRequest {
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
started: Instant,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
}
|
||||
|
||||
impl PendingRequest {
|
||||
/// Create new [`PendingRequest`].
|
||||
fn new(
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
started: Instant,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
) -> Self {
|
||||
Self { tx, started, fallback_request }
|
||||
}
|
||||
}
|
||||
|
||||
/// Request-response protocol configuration.
|
||||
///
|
||||
/// See [`RequestResponseConfiguration`](crate::request_response::ProtocolConfig) for more details.
|
||||
#[derive(Debug)]
|
||||
pub struct RequestResponseConfig {
|
||||
/// Name of the protocol on the wire. Should be something like `/foo/bar`.
|
||||
pub protocol_name: ProtocolName,
|
||||
|
||||
/// Fallback on the wire protocol names to support.
|
||||
pub fallback_names: Vec<ProtocolName>,
|
||||
|
||||
/// Maximum allowed size, in bytes, of a request.
|
||||
pub max_request_size: u64,
|
||||
|
||||
/// Maximum allowed size, in bytes, of a response.
|
||||
pub max_response_size: u64,
|
||||
|
||||
/// Duration after which emitted requests are considered timed out.
|
||||
pub request_timeout: Duration,
|
||||
|
||||
/// Channel on which the networking service will send incoming requests.
|
||||
pub inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
|
||||
}
|
||||
|
||||
impl RequestResponseConfig {
|
||||
/// Create new [`RequestResponseConfig`].
|
||||
pub(crate) fn new(
|
||||
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 {
|
||||
Self {
|
||||
protocol_name,
|
||||
fallback_names,
|
||||
max_request_size,
|
||||
max_response_size,
|
||||
request_timeout,
|
||||
inbound_queue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestResponseConfigT for RequestResponseConfig {
|
||||
fn protocol_name(&self) -> &ProtocolName {
|
||||
&self.protocol_name
|
||||
}
|
||||
}
|
||||
|
||||
/// Request-response protocol.
|
||||
///
|
||||
/// This is slightly different from the `RequestResponsesBehaviour` in that it is protocol-specific,
|
||||
/// meaning there is an instance of `RequestResponseProtocol` for each installed request-response
|
||||
/// protocol and that instance deals only with the requests and responses of that protocol, nothing
|
||||
/// else. It also differs from the other implementation by combining both inbound and outbound
|
||||
/// requests under one instance so all request-response-related behavior of any given protocol is
|
||||
/// handled through one instance of `RequestResponseProtocol`.
|
||||
pub struct RequestResponseProtocol {
|
||||
/// Protocol name.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Handle to request-response protocol.
|
||||
handle: RequestResponseHandle,
|
||||
|
||||
/// Inbound queue for sending received requests to protocol implementation in Pezkuwi SDK.
|
||||
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
|
||||
|
||||
/// Handle to `Peerstore`.
|
||||
peerstore_handle: Arc<dyn PeerStoreProvider>,
|
||||
|
||||
/// Pending responses.
|
||||
pending_inbound_responses: HashMap<RequestId, PendingRequest>,
|
||||
|
||||
/// Pending outbound responses.
|
||||
pending_outbound_responses: FuturesUnordered<
|
||||
BoxFuture<'static, (litep2p::PeerId, RequestId, Result<OutgoingResponse, ()>, Instant)>,
|
||||
>,
|
||||
|
||||
/// RX channel for receiving info for outbound requests.
|
||||
request_rx: TracingUnboundedReceiver<OutboundRequest>,
|
||||
|
||||
/// Map of supported request-response protocols which are used to support fallback requests.
|
||||
///
|
||||
/// If negotiation for the main protocol fails and the request was sent with a fallback,
|
||||
/// [`RequestResponseProtocol`] queries this map and sends the request that protocol for
|
||||
/// processing.
|
||||
request_tx: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
|
||||
|
||||
/// Metrics, if enabled.
|
||||
metrics: RequestResponseMetrics,
|
||||
}
|
||||
|
||||
impl RequestResponseProtocol {
|
||||
/// Create new [`RequestResponseProtocol`].
|
||||
pub fn new(
|
||||
protocol: ProtocolName,
|
||||
handle: RequestResponseHandle,
|
||||
peerstore_handle: Arc<dyn PeerStoreProvider>,
|
||||
inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
|
||||
request_rx: TracingUnboundedReceiver<OutboundRequest>,
|
||||
request_tx: HashMap<ProtocolName, TracingUnboundedSender<OutboundRequest>>,
|
||||
metrics: Option<Metrics>,
|
||||
) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
request_rx,
|
||||
request_tx,
|
||||
inbound_queue,
|
||||
peerstore_handle,
|
||||
protocol: protocol.clone(),
|
||||
pending_inbound_responses: HashMap::new(),
|
||||
pending_outbound_responses: FuturesUnordered::new(),
|
||||
metrics: RequestResponseMetrics::new(metrics, protocol),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send `request` to `peer`.
|
||||
async fn on_send_request(
|
||||
&mut self,
|
||||
peer: PeerId,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
) {
|
||||
let dial_options = match connect {
|
||||
IfDisconnected::TryConnect => DialOptions::Dial,
|
||||
IfDisconnected::ImmediateError => DialOptions::Reject,
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: send request to {:?} (fallback {:?}) (dial options: {:?})",
|
||||
self.protocol,
|
||||
peer,
|
||||
fallback_request,
|
||||
dial_options,
|
||||
);
|
||||
|
||||
match self.handle.try_send_request(peer.into(), request, dial_options) {
|
||||
Ok(request_id) => {
|
||||
self.pending_inbound_responses
|
||||
.insert(request_id, PendingRequest::new(tx, Instant::now(), fallback_request));
|
||||
},
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{}: failed to send request to {peer:?}: {error:?}",
|
||||
self.protocol,
|
||||
);
|
||||
|
||||
let _ = tx.send(Err(RequestFailure::Refused));
|
||||
self.metrics.register_inbound_request_failure(error.to_string().as_ref());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle inbound request from `peer`
|
||||
///
|
||||
/// If the protocol is configured outbound only, reject the request immediately.
|
||||
fn on_inbound_request(
|
||||
&mut self,
|
||||
peer: litep2p::PeerId,
|
||||
fallback: Option<litep2p::ProtocolName>,
|
||||
request_id: RequestId,
|
||||
request: Vec<u8>,
|
||||
) {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: request received from {peer:?} ({fallback:?} {request_id:?}), request size {:?}",
|
||||
self.protocol,
|
||||
request.len(),
|
||||
);
|
||||
|
||||
let Some(inbound_queue) = &self.inbound_queue else {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: rejecting inbound request from {peer:?}, protocol configured as outbound only",
|
||||
self.protocol,
|
||||
);
|
||||
|
||||
self.handle.reject_request(request_id);
|
||||
return;
|
||||
};
|
||||
|
||||
if self.peerstore_handle.is_banned(&peer.into()) {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: rejecting inbound request from banned {peer:?} ({request_id:?})",
|
||||
self.protocol,
|
||||
);
|
||||
|
||||
self.handle.reject_request(request_id);
|
||||
self.metrics.register_inbound_request_failure("banned-peer");
|
||||
return;
|
||||
}
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
match inbound_queue.try_send(IncomingRequest {
|
||||
peer: peer.into(),
|
||||
payload: request,
|
||||
pending_response: tx,
|
||||
}) {
|
||||
Ok(_) => {
|
||||
self.pending_outbound_responses.push(Box::pin(async move {
|
||||
(peer, request_id, rx.await.map_err(|_| ()), Instant::now())
|
||||
}));
|
||||
},
|
||||
Err(error) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{:?}: dropping request from {peer:?} ({request_id:?}), inbound queue full",
|
||||
self.protocol,
|
||||
);
|
||||
|
||||
self.handle.reject_request(request_id);
|
||||
self.metrics.register_inbound_request_failure(error.to_string().as_ref());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle received inbound response.
|
||||
fn on_inbound_response(
|
||||
&mut self,
|
||||
peer: litep2p::PeerId,
|
||||
request_id: RequestId,
|
||||
_fallback: Option<litep2p::ProtocolName>,
|
||||
response: Vec<u8>,
|
||||
) {
|
||||
match self.pending_inbound_responses.remove(&request_id) {
|
||||
None => log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{:?}: response received for {peer:?} but {request_id:?} doesn't exist",
|
||||
self.protocol,
|
||||
),
|
||||
Some(PendingRequest { tx, started, .. }) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{:?}: response received for {peer:?} ({request_id:?}), response size {:?}",
|
||||
self.protocol,
|
||||
response.len(),
|
||||
);
|
||||
|
||||
let _ = tx.send(Ok((response, self.protocol.clone())));
|
||||
self.metrics.register_outbound_request_success(started.elapsed());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle failed outbound request.
|
||||
fn on_request_failed(
|
||||
&mut self,
|
||||
peer: litep2p::PeerId,
|
||||
request_id: RequestId,
|
||||
error: RequestResponseError,
|
||||
) {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"{:?}: request failed for {peer:?} ({request_id:?}): {error:?}",
|
||||
self.protocol
|
||||
);
|
||||
|
||||
let Some(PendingRequest { tx, fallback_request, .. }) =
|
||||
self.pending_inbound_responses.remove(&request_id)
|
||||
else {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{:?}: request failed for peer {peer:?} but {request_id:?} doesn't exist",
|
||||
self.protocol,
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let status = match error {
|
||||
RequestResponseError::NotConnected =>
|
||||
Some((RequestFailure::NotConnected, "not-connected")),
|
||||
RequestResponseError::Rejected(reason) => {
|
||||
let reason = match reason {
|
||||
RejectReason::ConnectionClosed => "connection-closed",
|
||||
RejectReason::SubstreamClosed => "substream-closed",
|
||||
RejectReason::SubstreamOpenError(substream_error) => match substream_error {
|
||||
SubstreamError::NegotiationError(NegotiationError::Timeout) =>
|
||||
"substream-timeout",
|
||||
_ => "substream-open-error",
|
||||
},
|
||||
RejectReason::DialFailed(None) => "dial-failed",
|
||||
RejectReason::DialFailed(Some(ImmediateDialError::AlreadyConnected)) =>
|
||||
"dial-already-connected",
|
||||
RejectReason::DialFailed(Some(ImmediateDialError::PeerIdMissing)) =>
|
||||
"dial-peerid-missing",
|
||||
RejectReason::DialFailed(Some(ImmediateDialError::TriedToDialSelf)) =>
|
||||
"dial-tried-to-dial-self",
|
||||
RejectReason::DialFailed(Some(ImmediateDialError::NoAddressAvailable)) =>
|
||||
"dial-no-address-available",
|
||||
RejectReason::DialFailed(Some(ImmediateDialError::TaskClosed)) =>
|
||||
"dial-task-closed",
|
||||
RejectReason::DialFailed(Some(ImmediateDialError::ChannelClogged)) =>
|
||||
"dial-channel-clogged",
|
||||
};
|
||||
|
||||
Some((RequestFailure::Refused, reason))
|
||||
},
|
||||
RequestResponseError::Timeout =>
|
||||
Some((RequestFailure::Network(OutboundFailure::Timeout), "timeout")),
|
||||
RequestResponseError::Canceled => {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"{}: request canceled by local node to {peer:?} ({request_id:?})",
|
||||
self.protocol,
|
||||
);
|
||||
None
|
||||
},
|
||||
RequestResponseError::TooLargePayload => {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{}: tried to send too large request to {peer:?} ({request_id:?})",
|
||||
self.protocol,
|
||||
);
|
||||
Some((RequestFailure::Refused, "payload-too-large"))
|
||||
},
|
||||
RequestResponseError::UnsupportedProtocol => match fallback_request {
|
||||
Some((request, protocol)) => match self.request_tx.get(&protocol) {
|
||||
Some(sender) => {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"{}: failed to negotiate protocol with {:?}. Trying the fallback protocol ({})",
|
||||
self.protocol,
|
||||
peer,
|
||||
protocol,
|
||||
);
|
||||
|
||||
let outbound_request = OutboundRequest::new(
|
||||
peer.into(),
|
||||
request,
|
||||
tx,
|
||||
None,
|
||||
IfDisconnected::ImmediateError,
|
||||
);
|
||||
|
||||
// since remote peer doesn't support the main protocol (`self.protocol`),
|
||||
// try to send the request over a fallback protocol by creating a new
|
||||
// `OutboundRequest` from the original data, now with the fallback request
|
||||
// payload, and send it over to the (fallback) request handler like it was
|
||||
// a normal request.
|
||||
let _ = sender.unbounded_send(outbound_request);
|
||||
|
||||
return;
|
||||
},
|
||||
None => {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"{}: fallback request provided but protocol ({}) doesn't exist (peer {:?})",
|
||||
self.protocol,
|
||||
protocol,
|
||||
peer,
|
||||
);
|
||||
|
||||
Some((RequestFailure::Refused, "invalid-fallback-protocol"))
|
||||
},
|
||||
},
|
||||
None => Some((RequestFailure::Refused, "unsupported-protocol")),
|
||||
},
|
||||
};
|
||||
|
||||
if let Some((error, reason)) = status {
|
||||
self.metrics.register_outbound_request_failure(reason);
|
||||
let _ = tx.send(Err(error));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle outbound response.
|
||||
fn on_outbound_response(
|
||||
&mut self,
|
||||
peer: litep2p::PeerId,
|
||||
request_id: RequestId,
|
||||
response: OutgoingResponse,
|
||||
started: Instant,
|
||||
) {
|
||||
let OutgoingResponse { result, reputation_changes, sent_feedback } = response;
|
||||
|
||||
for change in reputation_changes {
|
||||
log::trace!(target: LOG_TARGET, "{}: report {peer:?}: {change:?}", self.protocol);
|
||||
self.peerstore_handle.report_peer(peer.into(), change);
|
||||
}
|
||||
|
||||
match result {
|
||||
Err(()) => {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"{}: response rejected ({request_id:?}) for {peer:?}",
|
||||
self.protocol,
|
||||
);
|
||||
|
||||
self.handle.reject_request(request_id);
|
||||
self.metrics.register_inbound_request_failure("rejected");
|
||||
},
|
||||
Ok(response) => {
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: send response ({request_id:?}) to {peer:?}, response size {}",
|
||||
self.protocol,
|
||||
response.len(),
|
||||
);
|
||||
|
||||
match sent_feedback {
|
||||
None => self.handle.send_response(request_id, response),
|
||||
Some(feedback) =>
|
||||
self.handle.send_response_with_feedback(request_id, response, feedback),
|
||||
}
|
||||
|
||||
self.metrics.register_inbound_request_success(started.elapsed());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Start running event loop of the request-response protocol.
|
||||
pub async fn run(mut self) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = self.handle.next() => match event {
|
||||
None => return,
|
||||
Some(RequestResponseEvent::RequestReceived {
|
||||
peer,
|
||||
fallback,
|
||||
request_id,
|
||||
request,
|
||||
}) => self.on_inbound_request(peer, fallback, request_id, request),
|
||||
Some(RequestResponseEvent::ResponseReceived { peer, request_id, fallback, response }) => {
|
||||
self.on_inbound_response(peer, request_id, fallback, response);
|
||||
},
|
||||
Some(RequestResponseEvent::RequestFailed { peer, request_id, error }) => {
|
||||
self.on_request_failed(peer, request_id, error);
|
||||
},
|
||||
},
|
||||
event = self.pending_outbound_responses.next(), if !self.pending_outbound_responses.is_empty() => match event {
|
||||
None => return,
|
||||
Some((peer, request_id, Err(()), _)) => {
|
||||
log::debug!(target: LOG_TARGET, "{}: reject request ({request_id:?}) from {peer:?}", self.protocol);
|
||||
|
||||
self.handle.reject_request(request_id);
|
||||
self.metrics.register_inbound_request_failure("rejected");
|
||||
}
|
||||
Some((peer, request_id, Ok(response), started)) => {
|
||||
self.on_outbound_response(peer, request_id, response, started);
|
||||
}
|
||||
},
|
||||
event = self.request_rx.next() => match event {
|
||||
None => return,
|
||||
Some(outbound_request) => {
|
||||
let OutboundRequest { peer, request, sender, dial_behavior, fallback_request } = outbound_request;
|
||||
|
||||
self.on_send_request(peer, request, fallback_request, sender, dial_behavior).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,906 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
litep2p::{
|
||||
peerstore::peerstore_handle_test,
|
||||
shim::request_response::{OutboundRequest, RequestResponseProtocol},
|
||||
},
|
||||
request_responses::{IfDisconnected, IncomingRequest, OutgoingResponse},
|
||||
ProtocolName, RequestFailure,
|
||||
};
|
||||
|
||||
use futures::{channel::oneshot, StreamExt};
|
||||
use litep2p::{
|
||||
config::ConfigBuilder as Litep2pConfigBuilder,
|
||||
protocol::request_response::{
|
||||
ConfigBuilder, DialOptions, RequestResponseError, RequestResponseEvent,
|
||||
RequestResponseHandle,
|
||||
},
|
||||
transport::tcp::config::Config as TcpConfig,
|
||||
Litep2p, Litep2pEvent,
|
||||
};
|
||||
|
||||
use pezsc_network_types::PeerId;
|
||||
use pezsc_utils::mpsc::tracing_unbounded;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc, task::Poll};
|
||||
|
||||
/// Create `litep2p` for testing.
|
||||
async fn make_litep2p() -> (Litep2p, RequestResponseHandle) {
|
||||
let (config, handle) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
(
|
||||
Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap(),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
// connect two `litep2p` instances together
|
||||
async fn connect_peers(litep2p1: &mut Litep2p, litep2p2: &mut Litep2p) {
|
||||
let address = litep2p2.listen_addresses().next().unwrap().clone();
|
||||
litep2p1.dial_address(address).await.unwrap();
|
||||
|
||||
let mut litep2p1_connected = false;
|
||||
let mut litep2p2_connected = false;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = litep2p1.next_event() => match event.unwrap() {
|
||||
Litep2pEvent::ConnectionEstablished { .. } => {
|
||||
litep2p1_connected = true;
|
||||
}
|
||||
_ => {},
|
||||
},
|
||||
event = litep2p2.next_event() => match event.unwrap() {
|
||||
Litep2pEvent::ConnectionEstablished { .. } => {
|
||||
litep2p2_connected = true;
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
if litep2p1_connected && litep2p2_connected {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dial_failure() {
|
||||
let (mut litep2p, handle) = make_litep2p().await;
|
||||
let (tx, _rx) = async_channel::bounded(64);
|
||||
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx.clone())]);
|
||||
|
||||
let protocol = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx),
|
||||
outbound_rx,
|
||||
senders,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p.next_event().await {} });
|
||||
|
||||
let peer = PeerId::random();
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
|
||||
outbound_tx
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer,
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: None,
|
||||
dial_behavior: IfDisconnected::TryConnect,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(std::matches!(result_rx.await, Ok(Err(RequestFailure::Refused))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_request_to_disconnected_peer() {
|
||||
let (mut litep2p, handle) = make_litep2p().await;
|
||||
let (tx, _rx) = async_channel::bounded(64);
|
||||
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx.clone())]);
|
||||
|
||||
let protocol = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx),
|
||||
outbound_rx,
|
||||
senders,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p.next_event().await {} });
|
||||
|
||||
let peer = PeerId::random();
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
|
||||
outbound_tx
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer,
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: None,
|
||||
dial_behavior: IfDisconnected::ImmediateError,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(std::matches!(result_rx.await, Ok(Err(RequestFailure::NotConnected))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_request_to_disconnected_peer_and_dial() {
|
||||
let (mut litep2p1, handle1) = make_litep2p().await;
|
||||
let (mut litep2p2, handle2) = make_litep2p().await;
|
||||
|
||||
let peer1 = *litep2p1.local_peer_id();
|
||||
let peer2 = *litep2p2.local_peer_id();
|
||||
|
||||
litep2p1.add_known_address(
|
||||
peer2,
|
||||
std::iter::once(litep2p2.listen_addresses().next().expect("listen address").clone()),
|
||||
);
|
||||
|
||||
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx1.clone())]);
|
||||
let (tx1, _rx1) = async_channel::bounded(64);
|
||||
|
||||
let protocol1 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx1),
|
||||
outbound_rx1,
|
||||
senders,
|
||||
None,
|
||||
);
|
||||
|
||||
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2)]);
|
||||
let (tx2, rx2) = async_channel::bounded(64);
|
||||
|
||||
let protocol2 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx2),
|
||||
outbound_rx2,
|
||||
senders,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol1.run());
|
||||
tokio::spawn(protocol2.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
let (result_tx, _result_rx) = oneshot::channel();
|
||||
outbound_tx1
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer: peer2.into(),
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: None,
|
||||
dial_behavior: IfDisconnected::TryConnect,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match rx2.recv().await {
|
||||
Ok(IncomingRequest { peer, payload, .. }) => {
|
||||
assert_eq!(peer, Into::<PeerId>::into(peer1));
|
||||
assert_eq!(payload, vec![1, 2, 3, 4]);
|
||||
},
|
||||
Err(error) => panic!("unexpected error: {error:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn too_many_inbound_requests() {
|
||||
let (mut litep2p1, handle1) = make_litep2p().await;
|
||||
let (mut litep2p2, mut handle2) = make_litep2p().await;
|
||||
let peer1 = *litep2p1.local_peer_id();
|
||||
|
||||
connect_peers(&mut litep2p1, &mut litep2p2).await;
|
||||
|
||||
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx)]);
|
||||
let (tx, _rx) = async_channel::bounded(4);
|
||||
|
||||
let protocol = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx),
|
||||
outbound_rx,
|
||||
senders,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
// send 5 request and verify that one of the requests will fail
|
||||
for _ in 0..5 {
|
||||
handle2
|
||||
.send_request(peer1, vec![1, 2, 3, 4], DialOptions::Reject)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// verify that one of the requests is rejected
|
||||
match handle2.next().await {
|
||||
Some(RequestResponseEvent::RequestFailed { peer, error, .. }) => {
|
||||
assert_eq!(peer, peer1);
|
||||
assert_eq!(
|
||||
error,
|
||||
RequestResponseError::Rejected(
|
||||
litep2p::protocol::request_response::RejectReason::SubstreamClosed
|
||||
)
|
||||
);
|
||||
},
|
||||
event => panic!("inavlid event: {event:?}"),
|
||||
}
|
||||
|
||||
// verify that no other events are read from the handle
|
||||
futures::future::poll_fn(|cx| match handle2.poll_next_unpin(cx) {
|
||||
Poll::Pending => Poll::Ready(()),
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feedback_works() {
|
||||
let (mut litep2p1, handle1) = make_litep2p().await;
|
||||
let (mut litep2p2, mut handle2) = make_litep2p().await;
|
||||
|
||||
let peer1 = *litep2p1.local_peer_id();
|
||||
let peer2 = *litep2p2.local_peer_id();
|
||||
|
||||
connect_peers(&mut litep2p1, &mut litep2p2).await;
|
||||
|
||||
let (outbound_tx, outbound_rx) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx)]);
|
||||
let (tx, rx) = async_channel::bounded(4);
|
||||
|
||||
let protocol = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx),
|
||||
outbound_rx,
|
||||
senders,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
let request_id = handle2
|
||||
.send_request(peer1, vec![1, 2, 3, 4], DialOptions::Reject)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rx = match rx.recv().await {
|
||||
Ok(IncomingRequest { peer, payload, pending_response }) => {
|
||||
assert_eq!(peer, peer2.into());
|
||||
assert_eq!(payload, vec![1, 2, 3, 4]);
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
pending_response
|
||||
.send(OutgoingResponse {
|
||||
result: Ok(vec![5, 6, 7, 8]),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: Some(tx),
|
||||
})
|
||||
.unwrap();
|
||||
rx
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
};
|
||||
|
||||
match handle2.next().await {
|
||||
Some(RequestResponseEvent::ResponseReceived {
|
||||
peer,
|
||||
request_id: received_id,
|
||||
response,
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(peer, peer1);
|
||||
assert_eq!(request_id, received_id);
|
||||
assert_eq!(response, vec![5, 6, 7, 8]);
|
||||
assert!(rx.await.is_ok());
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_request_compatible_peers() {
|
||||
// `litep2p1` supports both the new and the old protocol
|
||||
let (mut litep2p1, handle1_1, handle1_2) = {
|
||||
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
(
|
||||
Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config1)
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap(),
|
||||
handle1,
|
||||
handle2,
|
||||
)
|
||||
};
|
||||
|
||||
// `litep2p2` supports only the new protocol
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let mut litep2p2 = Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let peer1 = *litep2p1.local_peer_id();
|
||||
let peer2 = *litep2p2.local_peer_id();
|
||||
|
||||
connect_peers(&mut litep2p1, &mut litep2p2).await;
|
||||
|
||||
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
|
||||
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
|
||||
|
||||
let senders1 = HashMap::from_iter([
|
||||
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
|
||||
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
|
||||
]);
|
||||
|
||||
let (tx1, _rx1) = async_channel::bounded(4);
|
||||
let protocol1 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/2"),
|
||||
handle1_1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx1),
|
||||
outbound_rx1,
|
||||
senders1.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let (tx_fallback, _rx_fallback) = async_channel::bounded(4);
|
||||
let protocol_fallback = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1_2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx_fallback),
|
||||
outbound_rx_fallback,
|
||||
senders1,
|
||||
None,
|
||||
);
|
||||
|
||||
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/2"), outbound_tx2)]);
|
||||
|
||||
let (tx2, rx2) = async_channel::bounded(4);
|
||||
let protocol2 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/2"),
|
||||
handle2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx2),
|
||||
outbound_rx2,
|
||||
senders2,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol1.run());
|
||||
tokio::spawn(protocol2.run());
|
||||
tokio::spawn(protocol_fallback.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
outbound_tx1
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer: peer2.into(),
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: Some((vec![1, 3, 3, 7], ProtocolName::from("/protocol/1"))),
|
||||
dial_behavior: IfDisconnected::ImmediateError,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match rx2.recv().await {
|
||||
Ok(IncomingRequest { peer, payload, pending_response }) => {
|
||||
assert_eq!(peer, peer1.into());
|
||||
assert_eq!(payload, vec![1, 2, 3, 4]);
|
||||
pending_response
|
||||
.send(OutgoingResponse {
|
||||
result: Ok(vec![5, 6, 7, 8]),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
})
|
||||
.unwrap();
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
|
||||
match result_rx.await {
|
||||
Ok(Ok((response, protocol))) => {
|
||||
assert_eq!(response, vec![5, 6, 7, 8]);
|
||||
assert_eq!(protocol, ProtocolName::from("/protocol/2"));
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_request_old_peer_receives() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
// `litep2p1` supports both the new and the old protocol
|
||||
let (mut litep2p1, handle1_1, handle1_2) = {
|
||||
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
(
|
||||
Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config1)
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap(),
|
||||
handle1,
|
||||
handle2,
|
||||
)
|
||||
};
|
||||
|
||||
// `litep2p2` supports only the new protocol
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let mut litep2p2 = Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let peer1 = *litep2p1.local_peer_id();
|
||||
let peer2 = *litep2p2.local_peer_id();
|
||||
|
||||
connect_peers(&mut litep2p1, &mut litep2p2).await;
|
||||
|
||||
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
|
||||
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
|
||||
|
||||
let senders1 = HashMap::from_iter([
|
||||
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
|
||||
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
|
||||
]);
|
||||
|
||||
let (tx1, _rx1) = async_channel::bounded(4);
|
||||
let protocol1 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/2"),
|
||||
handle1_1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx1),
|
||||
outbound_rx1,
|
||||
senders1.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let (tx_fallback, _rx_fallback) = async_channel::bounded(4);
|
||||
let protocol_fallback = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1_2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx_fallback),
|
||||
outbound_rx_fallback,
|
||||
senders1,
|
||||
None,
|
||||
);
|
||||
|
||||
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2)]);
|
||||
|
||||
let (tx2, rx2) = async_channel::bounded(4);
|
||||
let protocol2 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx2),
|
||||
outbound_rx2,
|
||||
senders2,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol1.run());
|
||||
tokio::spawn(protocol2.run());
|
||||
tokio::spawn(protocol_fallback.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
outbound_tx1
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer: peer2.into(),
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: Some((vec![1, 3, 3, 7], ProtocolName::from("/protocol/1"))),
|
||||
dial_behavior: IfDisconnected::ImmediateError,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match rx2.recv().await {
|
||||
Ok(IncomingRequest { peer, payload, pending_response }) => {
|
||||
assert_eq!(peer, peer1.into());
|
||||
assert_eq!(payload, vec![1, 3, 3, 7]);
|
||||
pending_response
|
||||
.send(OutgoingResponse {
|
||||
result: Ok(vec![1, 3, 3, 8]),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
})
|
||||
.unwrap();
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
|
||||
match result_rx.await {
|
||||
Ok(Ok((response, protocol))) => {
|
||||
assert_eq!(response, vec![1, 3, 3, 8]);
|
||||
assert_eq!(protocol, ProtocolName::from("/protocol/1"));
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fallback_request_old_peer_sends() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
// `litep2p1` supports both the new and the old protocol
|
||||
let (mut litep2p1, handle1_1, handle1_2) = {
|
||||
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
(
|
||||
Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config1)
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap(),
|
||||
handle1,
|
||||
handle2,
|
||||
)
|
||||
};
|
||||
|
||||
// `litep2p2` supports only the new protocol
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let mut litep2p2 = Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let peer1 = *litep2p1.local_peer_id();
|
||||
let peer2 = *litep2p2.local_peer_id();
|
||||
|
||||
connect_peers(&mut litep2p1, &mut litep2p2).await;
|
||||
|
||||
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
|
||||
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
|
||||
|
||||
let senders1 = HashMap::from_iter([
|
||||
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
|
||||
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
|
||||
]);
|
||||
|
||||
let (tx1, _rx1) = async_channel::bounded(4);
|
||||
let protocol1 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/2"),
|
||||
handle1_1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx1),
|
||||
outbound_rx1,
|
||||
senders1.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let (tx_fallback, rx_fallback) = async_channel::bounded(4);
|
||||
let protocol_fallback = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1_2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx_fallback),
|
||||
outbound_rx_fallback,
|
||||
senders1,
|
||||
None,
|
||||
);
|
||||
|
||||
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2.clone())]);
|
||||
|
||||
let (tx2, _rx2) = async_channel::bounded(4);
|
||||
let protocol2 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx2),
|
||||
outbound_rx2,
|
||||
senders2,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol1.run());
|
||||
tokio::spawn(protocol2.run());
|
||||
tokio::spawn(protocol_fallback.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
outbound_tx2
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer: peer1.into(),
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: None,
|
||||
dial_behavior: IfDisconnected::ImmediateError,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match rx_fallback.recv().await {
|
||||
Ok(IncomingRequest { peer, payload, pending_response }) => {
|
||||
assert_eq!(peer, peer2.into());
|
||||
assert_eq!(payload, vec![1, 2, 3, 4]);
|
||||
pending_response
|
||||
.send(OutgoingResponse {
|
||||
result: Ok(vec![1, 3, 3, 8]),
|
||||
reputation_changes: Vec::new(),
|
||||
sent_feedback: None,
|
||||
})
|
||||
.unwrap();
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
|
||||
match result_rx.await {
|
||||
Ok(Ok((response, protocol))) => {
|
||||
assert_eq!(response, vec![1, 3, 3, 8]);
|
||||
assert_eq!(protocol, ProtocolName::from("/protocol/1"));
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn old_protocol_supported_but_no_fallback_provided() {
|
||||
pezsp_tracing::try_init_simple();
|
||||
|
||||
// `litep2p1` supports both the new and the old protocol
|
||||
let (mut litep2p1, handle1_1, handle1_2) = {
|
||||
let (config1, handle1) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/2"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
(
|
||||
Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config1)
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap(),
|
||||
handle1,
|
||||
handle2,
|
||||
)
|
||||
};
|
||||
|
||||
// `litep2p2` supports only the old protocol
|
||||
let (config2, handle2) = ConfigBuilder::new(litep2p::ProtocolName::from("/protocol/1"))
|
||||
.with_max_size(1024)
|
||||
.build();
|
||||
|
||||
let mut litep2p2 = Litep2p::new(
|
||||
Litep2pConfigBuilder::new()
|
||||
.with_request_response_protocol(config2)
|
||||
.with_tcp(TcpConfig {
|
||||
listen_addresses: vec![
|
||||
"/ip4/0.0.0.0/tcp/0".parse().unwrap(),
|
||||
"/ip6/::/tcp/0".parse().unwrap(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let peer2 = *litep2p2.local_peer_id();
|
||||
|
||||
connect_peers(&mut litep2p1, &mut litep2p2).await;
|
||||
|
||||
let (outbound_tx1, outbound_rx1) = tracing_unbounded("outbound-request", 1000);
|
||||
let (outbound_tx_fallback, outbound_rx_fallback) = tracing_unbounded("outbound-request", 1000);
|
||||
|
||||
let senders1 = HashMap::from_iter([
|
||||
(ProtocolName::from("/protocol/2"), outbound_tx1.clone()),
|
||||
(ProtocolName::from("/protocol/1"), outbound_tx_fallback),
|
||||
]);
|
||||
|
||||
let (tx1, _rx1) = async_channel::bounded(4);
|
||||
let protocol1 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/2"),
|
||||
handle1_1,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx1),
|
||||
outbound_rx1,
|
||||
senders1.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let (tx_fallback, _rx_fallback) = async_channel::bounded(4);
|
||||
let protocol_fallback = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle1_2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx_fallback),
|
||||
outbound_rx_fallback,
|
||||
senders1,
|
||||
None,
|
||||
);
|
||||
|
||||
let (outbound_tx2, outbound_rx2) = tracing_unbounded("outbound-request", 1000);
|
||||
let senders2 = HashMap::from_iter([(ProtocolName::from("/protocol/1"), outbound_tx2)]);
|
||||
|
||||
let (tx2, _rx2) = async_channel::bounded(4);
|
||||
let protocol2 = RequestResponseProtocol::new(
|
||||
ProtocolName::from("/protocol/1"),
|
||||
handle2,
|
||||
Arc::new(peerstore_handle_test()),
|
||||
Some(tx2),
|
||||
outbound_rx2,
|
||||
senders2,
|
||||
None,
|
||||
);
|
||||
|
||||
tokio::spawn(protocol1.run());
|
||||
tokio::spawn(protocol2.run());
|
||||
tokio::spawn(protocol_fallback.run());
|
||||
tokio::spawn(async move { while let Some(_) = litep2p1.next_event().await {} });
|
||||
tokio::spawn(async move { while let Some(_) = litep2p2.next_event().await {} });
|
||||
|
||||
let (result_tx, result_rx) = oneshot::channel();
|
||||
outbound_tx1
|
||||
.unbounded_send(OutboundRequest {
|
||||
peer: peer2.into(),
|
||||
request: vec![1, 2, 3, 4],
|
||||
sender: result_tx,
|
||||
fallback_request: None,
|
||||
dial_behavior: IfDisconnected::ImmediateError,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match result_rx.await {
|
||||
Ok(Err(error)) => {
|
||||
assert!(std::matches!(error, RequestFailure::Refused));
|
||||
},
|
||||
event => panic!("invalid event: {event:?}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Mocked components for tests.
|
||||
|
||||
use crate::{
|
||||
peer_store::{PeerStoreProvider, ProtocolHandle},
|
||||
ReputationChange,
|
||||
};
|
||||
|
||||
use pezsc_network_common::role::ObservedRole;
|
||||
use pezsc_network_types::PeerId;
|
||||
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
/// No-op `PeerStore`.
|
||||
#[derive(Debug)]
|
||||
pub struct MockPeerStore {}
|
||||
|
||||
impl PeerStoreProvider for MockPeerStore {
|
||||
fn is_banned(&self, _peer_id: &PeerId) -> bool {
|
||||
// Make sure that the peer is not banned.
|
||||
false
|
||||
}
|
||||
|
||||
fn register_protocol(&self, _protocol_handle: Arc<dyn ProtocolHandle>) {
|
||||
// Make sure not to fail.
|
||||
}
|
||||
|
||||
fn report_disconnect(&self, _peer_id: PeerId) {
|
||||
// Make sure not to fail.
|
||||
}
|
||||
|
||||
fn report_peer(&self, _peer_id: PeerId, _change: ReputationChange) {
|
||||
// Make sure not to fail.
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, _peer_id: &PeerId) -> i32 {
|
||||
// Make sure that the peer is not banned.
|
||||
0
|
||||
}
|
||||
|
||||
fn peer_role(&self, _peer_id: &PeerId) -> Option<ObservedRole> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_peer_role(&self, _peer_id: &PeerId, _role: ObservedRole) {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn outgoing_candidates(&self, _count: usize, _ignored: HashSet<PeerId>) -> Vec<PeerId> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn add_known_peer(&self, _peer_id: PeerId) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Information about the networking, for diagnostic purposes.
|
||||
//!
|
||||
//! **Warning**: These APIs are not stable.
|
||||
|
||||
use libp2p::{
|
||||
core::{ConnectedPoint, Endpoint as CoreEndpoint},
|
||||
Multiaddr,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// Returns general information about the networking.
|
||||
///
|
||||
/// Meant for general diagnostic purposes.
|
||||
///
|
||||
/// **Warning**: This API is not stable.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NetworkState {
|
||||
/// PeerId of the local node.
|
||||
pub peer_id: String,
|
||||
/// List of addresses the node is currently listening on.
|
||||
pub listened_addresses: HashSet<Multiaddr>,
|
||||
/// List of addresses the node knows it can be reached as.
|
||||
pub external_addresses: HashSet<Multiaddr>,
|
||||
/// List of node we're connected to.
|
||||
pub connected_peers: HashMap<String, Peer>,
|
||||
/// List of node that we know of but that we're not connected to.
|
||||
pub not_connected_peers: HashMap<String, NotConnectedPeer>,
|
||||
/// State of the peerset manager.
|
||||
pub peerset: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Part of the `NetworkState` struct. Unstable.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Peer {
|
||||
/// How we are connected to the node.
|
||||
pub endpoint: PeerEndpoint,
|
||||
/// Node information, as provided by the node itself. Can be empty if not known yet.
|
||||
pub version_string: Option<String>,
|
||||
/// Latest ping duration with this node.
|
||||
pub latest_ping_time: Option<Duration>,
|
||||
/// List of addresses known for this node.
|
||||
pub known_addresses: HashSet<Multiaddr>,
|
||||
}
|
||||
|
||||
/// Part of the `NetworkState` struct. Unstable.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotConnectedPeer {
|
||||
/// List of addresses known for this node.
|
||||
pub known_addresses: HashSet<Multiaddr>,
|
||||
/// Node information, as provided by the node itself, if we were ever connected to this node.
|
||||
pub version_string: Option<String>,
|
||||
/// Latest ping duration with this node, if we were ever connected to this node.
|
||||
pub latest_ping_time: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Part of the `NetworkState` struct. Unstable.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PeerEndpoint {
|
||||
/// We are dialing the given address.
|
||||
Dialing(Multiaddr, Endpoint),
|
||||
/// We are listening.
|
||||
Listening {
|
||||
/// Local address of the connection.
|
||||
local_addr: Multiaddr,
|
||||
/// Address data is sent back to.
|
||||
send_back_addr: Multiaddr,
|
||||
},
|
||||
}
|
||||
|
||||
/// Part of the `NetworkState` struct. Unstable.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Endpoint {
|
||||
/// The socket comes from a dialer.
|
||||
Dialer,
|
||||
/// The socket comes from a listener.
|
||||
Listener,
|
||||
}
|
||||
|
||||
impl From<ConnectedPoint> for PeerEndpoint {
|
||||
fn from(endpoint: ConnectedPoint) -> Self {
|
||||
match endpoint {
|
||||
ConnectedPoint::Dialer { address, role_override, port_use: _ } =>
|
||||
Self::Dialing(address, role_override.into()),
|
||||
ConnectedPoint::Listener { local_addr, send_back_addr } =>
|
||||
Self::Listening { local_addr, send_back_addr },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreEndpoint> for Endpoint {
|
||||
fn from(endpoint: CoreEndpoint) -> Self {
|
||||
match endpoint {
|
||||
CoreEndpoint::Dialer => Self::Dialer,
|
||||
CoreEndpoint::Listener => Self::Listener,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! [`PeerInfoBehaviour`] is implementation of `NetworkBehaviour` that holds information about peers
|
||||
//! in cache.
|
||||
|
||||
use crate::{utils::interval, LOG_TARGET};
|
||||
use either::Either;
|
||||
|
||||
use fnv::FnvHashMap;
|
||||
use futures::prelude::*;
|
||||
use libp2p::{
|
||||
core::{transport::PortUse, ConnectedPoint, Endpoint},
|
||||
identify::{
|
||||
Behaviour as Identify, Config as IdentifyConfig, Event as IdentifyEvent,
|
||||
Info as IdentifyInfo,
|
||||
},
|
||||
identity::PublicKey,
|
||||
multiaddr::Protocol,
|
||||
ping::{Behaviour as Ping, Config as PingConfig, Event as PingEvent},
|
||||
swarm::{
|
||||
behaviour::{
|
||||
AddressChange, ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm,
|
||||
ListenFailure,
|
||||
},
|
||||
ConnectionDenied, ConnectionHandler, ConnectionHandlerSelect, ConnectionId,
|
||||
NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
|
||||
},
|
||||
Multiaddr, PeerId,
|
||||
};
|
||||
use log::{debug, error, trace, warn};
|
||||
use parking_lot::Mutex;
|
||||
use schnellru::{ByLength, LruMap};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashSet, VecDeque},
|
||||
iter,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Time after we disconnect from a node before we purge its information from the cache.
|
||||
const CACHE_EXPIRE: Duration = Duration::from_secs(10 * 60);
|
||||
/// Interval at which we perform garbage collection on the node info.
|
||||
const GARBAGE_COLLECT_INTERVAL: Duration = Duration::from_secs(2 * 60);
|
||||
/// The maximum number of tracked external addresses we allow.
|
||||
const MAX_EXTERNAL_ADDRESSES: u32 = 32;
|
||||
/// Number of times observed address is received from different peers before it is confirmed as
|
||||
/// external.
|
||||
const MIN_ADDRESS_CONFIRMATIONS: usize = 3;
|
||||
|
||||
/// Implementation of `NetworkBehaviour` that holds information about peers in cache.
|
||||
pub struct PeerInfoBehaviour {
|
||||
/// Periodically ping nodes, and close the connection if it's unresponsive.
|
||||
ping: Ping,
|
||||
/// Periodically identifies the remote and responds to incoming requests.
|
||||
identify: Identify,
|
||||
/// Information that we know about all nodes.
|
||||
nodes_info: FnvHashMap<PeerId, NodeInfo>,
|
||||
/// Interval at which we perform garbage collection in `nodes_info`.
|
||||
garbage_collect: Pin<Box<dyn Stream<Item = ()> + Send>>,
|
||||
/// PeerId of the local node.
|
||||
local_peer_id: PeerId,
|
||||
/// Public addresses supplied by the operator. Never expire.
|
||||
public_addresses: Vec<Multiaddr>,
|
||||
/// Listen addresses. External addresses matching listen addresses never expire.
|
||||
listen_addresses: HashSet<Multiaddr>,
|
||||
/// External address confirmations.
|
||||
address_confirmations: LruMap<Multiaddr, HashSet<PeerId>>,
|
||||
/// Record keeping of external addresses. Data is queried by the `NetworkService`.
|
||||
/// The addresses contain the `/p2p/...` part with local peer ID.
|
||||
external_addresses: ExternalAddresses,
|
||||
/// Pending events to emit to [`Swarm`](libp2p::swarm::Swarm).
|
||||
pending_actions: VecDeque<ToSwarm<PeerInfoEvent, THandlerInEvent<PeerInfoBehaviour>>>,
|
||||
}
|
||||
|
||||
/// Information about a node we're connected to.
|
||||
#[derive(Debug)]
|
||||
struct NodeInfo {
|
||||
/// When we will remove the entry about this node from the list, or `None` if we're connected
|
||||
/// to the node.
|
||||
info_expire: Option<Instant>,
|
||||
/// Non-empty list of connected endpoints, one per connection.
|
||||
endpoints: SmallVec<[ConnectedPoint; crate::MAX_CONNECTIONS_PER_PEER]>,
|
||||
/// Version reported by the remote, or `None` if unknown.
|
||||
client_version: Option<String>,
|
||||
/// Latest ping time with this node.
|
||||
latest_ping: Option<Duration>,
|
||||
}
|
||||
|
||||
impl NodeInfo {
|
||||
fn new(endpoint: ConnectedPoint) -> Self {
|
||||
let mut endpoints = SmallVec::new();
|
||||
endpoints.push(endpoint);
|
||||
Self { info_expire: None, endpoints, client_version: None, latest_ping: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility struct for tracking external addresses. The data is shared with the `NetworkService`.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExternalAddresses {
|
||||
addresses: Arc<Mutex<HashSet<Multiaddr>>>,
|
||||
}
|
||||
|
||||
impl ExternalAddresses {
|
||||
/// Add an external address.
|
||||
pub fn add(&mut self, addr: Multiaddr) -> bool {
|
||||
self.addresses.lock().insert(addr)
|
||||
}
|
||||
|
||||
/// Remove an external address.
|
||||
pub fn remove(&mut self, addr: &Multiaddr) -> bool {
|
||||
self.addresses.lock().remove(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl PeerInfoBehaviour {
|
||||
/// Builds a new `PeerInfoBehaviour`.
|
||||
pub fn new(
|
||||
user_agent: String,
|
||||
local_public_key: PublicKey,
|
||||
external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
|
||||
public_addresses: Vec<Multiaddr>,
|
||||
) -> Self {
|
||||
let identify = {
|
||||
let cfg = IdentifyConfig::new("/bizinikiwi/1.0".to_string(), local_public_key.clone())
|
||||
.with_agent_version(user_agent)
|
||||
// We don't need any peer information cached.
|
||||
.with_cache_size(0);
|
||||
Identify::new(cfg)
|
||||
};
|
||||
|
||||
Self {
|
||||
ping: Ping::new(PingConfig::new()),
|
||||
identify,
|
||||
nodes_info: FnvHashMap::default(),
|
||||
garbage_collect: Box::pin(interval(GARBAGE_COLLECT_INTERVAL)),
|
||||
local_peer_id: local_public_key.to_peer_id(),
|
||||
public_addresses,
|
||||
listen_addresses: HashSet::new(),
|
||||
address_confirmations: LruMap::new(ByLength::new(MAX_EXTERNAL_ADDRESSES)),
|
||||
external_addresses: ExternalAddresses { addresses: external_addresses },
|
||||
pending_actions: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrows `self` and returns a struct giving access to the information about a node.
|
||||
///
|
||||
/// Returns `None` if we don't know anything about this node. Always returns `Some` for nodes
|
||||
/// we're connected to, meaning that if `None` is returned then we're not connected to that
|
||||
/// node.
|
||||
pub fn node(&self, peer_id: &PeerId) -> Option<Node<'_>> {
|
||||
self.nodes_info.get(peer_id).map(Node)
|
||||
}
|
||||
|
||||
/// Inserts a ping time in the cache. Has no effect if we don't have any entry for that node,
|
||||
/// which shouldn't happen.
|
||||
fn handle_ping_report(
|
||||
&mut self,
|
||||
peer_id: &PeerId,
|
||||
ping_time: Duration,
|
||||
connection: ConnectionId,
|
||||
) {
|
||||
trace!(target: LOG_TARGET, "Ping time with {:?} via {:?}: {:?}", peer_id, connection, ping_time);
|
||||
if let Some(entry) = self.nodes_info.get_mut(peer_id) {
|
||||
entry.latest_ping = Some(ping_time);
|
||||
} else {
|
||||
error!(target: LOG_TARGET,
|
||||
"Received ping from node we're not connected to {:?} via {:?}", peer_id, connection);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure address has the `/p2p/...` part with local peer id. Returns `Err` if the address
|
||||
/// already contains a different peer id.
|
||||
fn with_local_peer_id(&self, address: Multiaddr) -> Result<Multiaddr, Multiaddr> {
|
||||
if let Some(Protocol::P2p(peer_id)) = address.iter().last() {
|
||||
if peer_id == self.local_peer_id {
|
||||
Ok(address)
|
||||
} else {
|
||||
Err(address)
|
||||
}
|
||||
} else {
|
||||
Ok(address.with(Protocol::P2p(self.local_peer_id)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts an identify record in the cache & discovers external addresses when multiple
|
||||
/// peers report the same address as observed.
|
||||
fn handle_identify_report(&mut self, peer_id: &PeerId, info: &IdentifyInfo) {
|
||||
trace!(target: LOG_TARGET, "Identified {:?} => {:?}", peer_id, info);
|
||||
if let Some(entry) = self.nodes_info.get_mut(peer_id) {
|
||||
entry.client_version = Some(info.agent_version.clone());
|
||||
} else {
|
||||
error!(target: LOG_TARGET,
|
||||
"Received identify message from node we're not connected to {peer_id:?}");
|
||||
}
|
||||
// Discover external addresses.
|
||||
match self.with_local_peer_id(info.observed_addr.clone()) {
|
||||
Ok(observed_addr) => {
|
||||
let (is_new, expired) = self.is_new_external_address(&observed_addr, *peer_id);
|
||||
if is_new && self.external_addresses.add(observed_addr.clone()) {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Observed address reported by Identify confirmed as external {}",
|
||||
observed_addr,
|
||||
);
|
||||
self.pending_actions.push_back(ToSwarm::ExternalAddrConfirmed(observed_addr));
|
||||
}
|
||||
if let Some(expired) = expired {
|
||||
trace!(target: LOG_TARGET, "Removing replaced external address: {expired}");
|
||||
self.external_addresses.remove(&expired);
|
||||
self.pending_actions.push_back(ToSwarm::ExternalAddrExpired(expired));
|
||||
}
|
||||
},
|
||||
Err(addr) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Identify reported observed address for a peer that is not us: {addr}",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if addresses are equal taking into account they can contain or not contain
|
||||
/// the `/p2p/...` part.
|
||||
fn is_same_address(left: &Multiaddr, right: &Multiaddr) -> bool {
|
||||
let mut left = left.iter();
|
||||
let mut right = right.iter();
|
||||
|
||||
loop {
|
||||
match (left.next(), right.next()) {
|
||||
(None, None) => return true,
|
||||
(None, Some(Protocol::P2p(_))) => return true,
|
||||
(Some(Protocol::P2p(_)), None) => return true,
|
||||
(left, right) if left != right => return false,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if `address` can be considered a new external address.
|
||||
///
|
||||
/// If this address replaces an older address, the expired address is returned.
|
||||
fn is_new_external_address(
|
||||
&mut self,
|
||||
address: &Multiaddr,
|
||||
peer_id: PeerId,
|
||||
) -> (bool, Option<Multiaddr>) {
|
||||
trace!(target: LOG_TARGET, "Verify new external address: {address}");
|
||||
|
||||
// Public and listen addresses don't count towards discovered external addresses
|
||||
// and are always confirmed.
|
||||
// Because they are not kept in the LRU, they are never replaced by discovered
|
||||
// external addresses.
|
||||
if self
|
||||
.listen_addresses
|
||||
.iter()
|
||||
.chain(self.public_addresses.iter())
|
||||
.any(|known_address| PeerInfoBehaviour::is_same_address(&known_address, &address))
|
||||
{
|
||||
return (true, None);
|
||||
}
|
||||
|
||||
match self.address_confirmations.get(address) {
|
||||
Some(confirmations) => {
|
||||
confirmations.insert(peer_id);
|
||||
|
||||
if confirmations.len() >= MIN_ADDRESS_CONFIRMATIONS {
|
||||
return (true, None);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let oldest = (self.address_confirmations.len() >=
|
||||
self.address_confirmations.limiter().max_length() as usize)
|
||||
.then(|| {
|
||||
self.address_confirmations.pop_oldest().map(|(address, peers)| {
|
||||
if peers.len() >= MIN_ADDRESS_CONFIRMATIONS {
|
||||
return Some(address);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.flatten();
|
||||
|
||||
self.address_confirmations
|
||||
.insert(address.clone(), iter::once(peer_id).collect());
|
||||
|
||||
return (false, oldest);
|
||||
},
|
||||
}
|
||||
|
||||
(false, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives access to the information about a node.
|
||||
pub struct Node<'a>(&'a NodeInfo);
|
||||
|
||||
impl<'a> Node<'a> {
|
||||
/// Returns the endpoint of an established connection to the peer.
|
||||
///
|
||||
/// Returns `None` if we are disconnected from the node.
|
||||
pub fn endpoint(&self) -> Option<&'a ConnectedPoint> {
|
||||
self.0.endpoints.get(0)
|
||||
}
|
||||
|
||||
/// Returns the latest version information we know of.
|
||||
pub fn client_version(&self) -> Option<&'a str> {
|
||||
self.0.client_version.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the latest ping time we know of for this node. `None` if we never successfully
|
||||
/// pinged this node.
|
||||
pub fn latest_ping(&self) -> Option<Duration> {
|
||||
self.0.latest_ping
|
||||
}
|
||||
}
|
||||
|
||||
/// Event that can be emitted by the behaviour.
|
||||
#[derive(Debug)]
|
||||
pub enum PeerInfoEvent {
|
||||
/// We have obtained identity information from a peer, including the addresses it is listening
|
||||
/// on.
|
||||
Identified {
|
||||
/// Id of the peer that has been identified.
|
||||
peer_id: PeerId,
|
||||
/// Information about the peer.
|
||||
info: IdentifyInfo,
|
||||
},
|
||||
}
|
||||
|
||||
impl NetworkBehaviour for PeerInfoBehaviour {
|
||||
type ConnectionHandler = ConnectionHandlerSelect<
|
||||
<Ping as NetworkBehaviour>::ConnectionHandler,
|
||||
<Identify as NetworkBehaviour>::ConnectionHandler,
|
||||
>;
|
||||
type ToSwarm = PeerInfoEvent;
|
||||
|
||||
fn handle_pending_inbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
local_addr: &Multiaddr,
|
||||
remote_addr: &Multiaddr,
|
||||
) -> Result<(), ConnectionDenied> {
|
||||
self.ping
|
||||
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)?;
|
||||
self.identify
|
||||
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
|
||||
}
|
||||
|
||||
fn handle_pending_outbound_connection(
|
||||
&mut self,
|
||||
_connection_id: ConnectionId,
|
||||
_maybe_peer: Option<PeerId>,
|
||||
_addresses: &[Multiaddr],
|
||||
_effective_role: Endpoint,
|
||||
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
|
||||
// Only `Discovery::handle_pending_outbound_connection` must be returning addresses to
|
||||
// ensure that we don't return unwanted addresses.
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn handle_established_inbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
local_addr: &Multiaddr,
|
||||
remote_addr: &Multiaddr,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
let ping_handler = self.ping.handle_established_inbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
local_addr,
|
||||
remote_addr,
|
||||
)?;
|
||||
let identify_handler = self.identify.handle_established_inbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
local_addr,
|
||||
remote_addr,
|
||||
)?;
|
||||
Ok(ping_handler.select(identify_handler))
|
||||
}
|
||||
|
||||
fn handle_established_outbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
addr: &Multiaddr,
|
||||
role_override: Endpoint,
|
||||
port_use: PortUse,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
let ping_handler = self.ping.handle_established_outbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
addr,
|
||||
role_override,
|
||||
port_use,
|
||||
)?;
|
||||
let identify_handler = self.identify.handle_established_outbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
addr,
|
||||
role_override,
|
||||
port_use,
|
||||
)?;
|
||||
Ok(ping_handler.select(identify_handler))
|
||||
}
|
||||
|
||||
fn on_swarm_event(&mut self, event: FromSwarm) {
|
||||
match event {
|
||||
FromSwarm::ConnectionEstablished(
|
||||
e @ ConnectionEstablished { peer_id, endpoint, .. },
|
||||
) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ConnectionEstablished(e));
|
||||
self.identify.on_swarm_event(FromSwarm::ConnectionEstablished(e));
|
||||
|
||||
match self.nodes_info.entry(peer_id) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(NodeInfo::new(endpoint.clone()));
|
||||
},
|
||||
Entry::Occupied(e) => {
|
||||
let e = e.into_mut();
|
||||
if e.info_expire.as_ref().map(|exp| *exp < Instant::now()).unwrap_or(false)
|
||||
{
|
||||
e.client_version = None;
|
||||
e.latest_ping = None;
|
||||
}
|
||||
e.info_expire = None;
|
||||
e.endpoints.push(endpoint.clone());
|
||||
},
|
||||
}
|
||||
},
|
||||
FromSwarm::ConnectionClosed(ConnectionClosed {
|
||||
peer_id,
|
||||
connection_id,
|
||||
endpoint,
|
||||
cause,
|
||||
remaining_established,
|
||||
}) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
|
||||
peer_id,
|
||||
connection_id,
|
||||
endpoint,
|
||||
cause,
|
||||
remaining_established,
|
||||
}));
|
||||
self.identify.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
|
||||
peer_id,
|
||||
connection_id,
|
||||
endpoint,
|
||||
cause,
|
||||
remaining_established,
|
||||
}));
|
||||
|
||||
if let Some(entry) = self.nodes_info.get_mut(&peer_id) {
|
||||
if remaining_established == 0 {
|
||||
entry.info_expire = Some(Instant::now() + CACHE_EXPIRE);
|
||||
}
|
||||
entry.endpoints.retain(|ep| ep != endpoint)
|
||||
} else {
|
||||
error!(target: LOG_TARGET,
|
||||
"Unknown connection to {:?} closed: {:?}", peer_id, endpoint);
|
||||
}
|
||||
},
|
||||
FromSwarm::DialFailure(DialFailure { peer_id, error, connection_id }) => {
|
||||
self.ping.on_swarm_event(FromSwarm::DialFailure(DialFailure {
|
||||
peer_id,
|
||||
error,
|
||||
connection_id,
|
||||
}));
|
||||
self.identify.on_swarm_event(FromSwarm::DialFailure(DialFailure {
|
||||
peer_id,
|
||||
error,
|
||||
connection_id,
|
||||
}));
|
||||
},
|
||||
FromSwarm::ListenerClosed(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ListenerClosed(e));
|
||||
self.identify.on_swarm_event(FromSwarm::ListenerClosed(e));
|
||||
},
|
||||
FromSwarm::ListenFailure(ListenFailure {
|
||||
local_addr,
|
||||
send_back_addr,
|
||||
error,
|
||||
connection_id,
|
||||
peer_id,
|
||||
}) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ListenFailure(ListenFailure {
|
||||
local_addr,
|
||||
send_back_addr,
|
||||
error,
|
||||
connection_id,
|
||||
peer_id,
|
||||
}));
|
||||
self.identify.on_swarm_event(FromSwarm::ListenFailure(ListenFailure {
|
||||
local_addr,
|
||||
send_back_addr,
|
||||
error,
|
||||
connection_id,
|
||||
peer_id,
|
||||
}));
|
||||
},
|
||||
FromSwarm::ListenerError(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ListenerError(e));
|
||||
self.identify.on_swarm_event(FromSwarm::ListenerError(e));
|
||||
},
|
||||
FromSwarm::ExternalAddrExpired(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ExternalAddrExpired(e));
|
||||
self.identify.on_swarm_event(FromSwarm::ExternalAddrExpired(e));
|
||||
},
|
||||
FromSwarm::NewListener(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::NewListener(e));
|
||||
self.identify.on_swarm_event(FromSwarm::NewListener(e));
|
||||
},
|
||||
FromSwarm::NewListenAddr(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::NewListenAddr(e));
|
||||
self.identify.on_swarm_event(FromSwarm::NewListenAddr(e));
|
||||
self.listen_addresses.insert(e.addr.clone());
|
||||
},
|
||||
FromSwarm::ExpiredListenAddr(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ExpiredListenAddr(e));
|
||||
self.identify.on_swarm_event(FromSwarm::ExpiredListenAddr(e));
|
||||
self.listen_addresses.remove(e.addr);
|
||||
// Remove matching external address.
|
||||
match self.with_local_peer_id(e.addr.clone()) {
|
||||
Ok(addr) => {
|
||||
self.external_addresses.remove(&addr);
|
||||
self.pending_actions.push_back(ToSwarm::ExternalAddrExpired(addr));
|
||||
},
|
||||
Err(addr) => {
|
||||
warn!(
|
||||
target: LOG_TARGET,
|
||||
"Listen address expired with peer ID that is not us: {addr}",
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
FromSwarm::NewExternalAddrCandidate(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));
|
||||
self.identify.on_swarm_event(FromSwarm::NewExternalAddrCandidate(e));
|
||||
},
|
||||
FromSwarm::ExternalAddrConfirmed(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::ExternalAddrConfirmed(e));
|
||||
self.identify.on_swarm_event(FromSwarm::ExternalAddrConfirmed(e));
|
||||
},
|
||||
FromSwarm::AddressChange(e @ AddressChange { peer_id, old, new, .. }) => {
|
||||
self.ping.on_swarm_event(FromSwarm::AddressChange(e));
|
||||
self.identify.on_swarm_event(FromSwarm::AddressChange(e));
|
||||
|
||||
if let Some(entry) = self.nodes_info.get_mut(&peer_id) {
|
||||
if let Some(endpoint) = entry.endpoints.iter_mut().find(|e| e == &old) {
|
||||
*endpoint = new.clone();
|
||||
} else {
|
||||
error!(target: LOG_TARGET,
|
||||
"Unknown address change for peer {:?} from {:?} to {:?}", peer_id, old, new);
|
||||
}
|
||||
} else {
|
||||
error!(target: LOG_TARGET,
|
||||
"Unknown peer {:?} to change address from {:?} to {:?}", peer_id, old, new);
|
||||
}
|
||||
},
|
||||
FromSwarm::NewExternalAddrOfPeer(e) => {
|
||||
self.ping.on_swarm_event(FromSwarm::NewExternalAddrOfPeer(e));
|
||||
self.identify.on_swarm_event(FromSwarm::NewExternalAddrOfPeer(e));
|
||||
},
|
||||
event => {
|
||||
debug!(target: LOG_TARGET, "New unknown `FromSwarm` libp2p event: {event:?}");
|
||||
self.ping.on_swarm_event(event);
|
||||
self.identify.on_swarm_event(event);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn on_connection_handler_event(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
connection_id: ConnectionId,
|
||||
event: THandlerOutEvent<Self>,
|
||||
) {
|
||||
match event {
|
||||
Either::Left(event) =>
|
||||
self.ping.on_connection_handler_event(peer_id, connection_id, event),
|
||||
Either::Right(event) =>
|
||||
self.identify.on_connection_handler_event(peer_id, connection_id, event),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(&mut self, cx: &mut Context) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
|
||||
if let Some(event) = self.pending_actions.pop_front() {
|
||||
return Poll::Ready(event);
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.ping.poll(cx) {
|
||||
Poll::Pending => break,
|
||||
Poll::Ready(ToSwarm::GenerateEvent(ev)) => {
|
||||
if let PingEvent { peer, result: Ok(rtt), connection } = ev {
|
||||
self.handle_ping_report(&peer, rtt, connection)
|
||||
}
|
||||
},
|
||||
Poll::Ready(event) => {
|
||||
return Poll::Ready(event.map_in(Either::Left).map_out(|_| {
|
||||
unreachable!("`GenerateEvent` is handled in a branch above; qed")
|
||||
}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.identify.poll(cx) {
|
||||
Poll::Pending => break,
|
||||
Poll::Ready(ToSwarm::GenerateEvent(event)) => match event {
|
||||
IdentifyEvent::Received { peer_id, info, .. } => {
|
||||
self.handle_identify_report(&peer_id, &info);
|
||||
let event = PeerInfoEvent::Identified { peer_id, info };
|
||||
return Poll::Ready(ToSwarm::GenerateEvent(event));
|
||||
},
|
||||
IdentifyEvent::Error { connection_id, peer_id, error } => {
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Identification with peer {peer_id:?}({connection_id}) failed => {error}"
|
||||
);
|
||||
},
|
||||
IdentifyEvent::Pushed { .. } => {},
|
||||
IdentifyEvent::Sent { .. } => {},
|
||||
},
|
||||
Poll::Ready(event) => {
|
||||
return Poll::Ready(event.map_in(Either::Right).map_out(|_| {
|
||||
unreachable!("`GenerateEvent` is handled in a branch above; qed")
|
||||
}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
while let Poll::Ready(Some(())) = self.garbage_collect.poll_next_unpin(cx) {
|
||||
self.nodes_info.retain(|_, node| {
|
||||
node.info_expire.as_ref().map(|exp| *exp >= Instant::now()).unwrap_or(true)
|
||||
});
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! [`PeerStore`] manages peer reputations and provides connection candidates to
|
||||
//! [`crate::protocol_controller::ProtocolController`].
|
||||
|
||||
use crate::service::{metrics::PeerStoreMetrics, traits::PeerStore as PeerStoreT};
|
||||
|
||||
use libp2p::PeerId;
|
||||
use log::trace;
|
||||
use parking_lot::Mutex;
|
||||
use partial_sort::PartialSort;
|
||||
use prometheus_endpoint::Registry;
|
||||
use pezsc_network_common::{role::ObservedRole, types::ReputationChange};
|
||||
use std::{
|
||||
cmp::{Ord, Ordering, PartialOrd},
|
||||
collections::{hash_map::Entry, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use wasm_timer::Delay;
|
||||
|
||||
/// Log target for this file.
|
||||
pub const LOG_TARGET: &str = "peerset";
|
||||
|
||||
/// We don't accept nodes whose reputation is under this value.
|
||||
pub const BANNED_THRESHOLD: i32 = 71 * (i32::MIN / 100);
|
||||
/// Reputation change for a node when we get disconnected from it.
|
||||
const DISCONNECT_REPUTATION_CHANGE: i32 = -256;
|
||||
/// Relative decrement of a reputation value that is applied every second. I.e., for inverse
|
||||
/// decrement of 200 we decrease absolute value of the reputation by 1/200.
|
||||
///
|
||||
/// This corresponds to a factor of `k = 0.955`, where k = 1 - 1 / INVERSE_DECREMENT.
|
||||
///
|
||||
/// It takes ~ `ln(0.5) / ln(k)` seconds to reduce the reputation by half, or 138.63 seconds for the
|
||||
/// values above.
|
||||
///
|
||||
/// In this setup:
|
||||
/// - `i32::MAX` becomes 0 in exactly 3544 seconds, or approximately 59 minutes
|
||||
/// - `i32::MIN` becomes 0 in exactly 3544 seconds, or approximately 59 minutes
|
||||
/// - `i32::MIN` escapes the banned threshold in 69 seconds
|
||||
const INVERSE_DECREMENT: i32 = 200;
|
||||
/// Amount of time between the moment we last updated the [`PeerStore`] entry and the moment we
|
||||
/// remove it, once the reputation value reaches 0.
|
||||
const FORGET_AFTER: Duration = Duration::from_secs(3600);
|
||||
|
||||
/// Trait describing the required functionality from a `Peerset` handle.
|
||||
pub trait ProtocolHandle: Debug + Send + Sync {
|
||||
/// Disconnect peer.
|
||||
fn disconnect_peer(&self, peer_id: pezsc_network_types::PeerId);
|
||||
}
|
||||
|
||||
/// Trait providing peer reputation management and connection candidates.
|
||||
pub trait PeerStoreProvider: Debug + Send + Sync {
|
||||
/// Check whether the peer is banned.
|
||||
fn is_banned(&self, peer_id: &pezsc_network_types::PeerId) -> bool;
|
||||
|
||||
/// Register a protocol handle to disconnect peers whose reputation drops below the threshold.
|
||||
fn register_protocol(&self, protocol_handle: Arc<dyn ProtocolHandle>);
|
||||
|
||||
/// Report peer disconnection for reputation adjustment.
|
||||
fn report_disconnect(&self, peer_id: pezsc_network_types::PeerId);
|
||||
|
||||
/// Adjust peer reputation.
|
||||
fn report_peer(&self, peer_id: pezsc_network_types::PeerId, change: ReputationChange);
|
||||
|
||||
/// Set peer role.
|
||||
fn set_peer_role(&self, peer_id: &pezsc_network_types::PeerId, role: ObservedRole);
|
||||
|
||||
/// Get peer reputation.
|
||||
fn peer_reputation(&self, peer_id: &pezsc_network_types::PeerId) -> i32;
|
||||
|
||||
/// Get peer role, if available.
|
||||
fn peer_role(&self, peer_id: &pezsc_network_types::PeerId) -> Option<ObservedRole>;
|
||||
|
||||
/// Get candidates with highest reputations for initiating outgoing connections.
|
||||
fn outgoing_candidates(
|
||||
&self,
|
||||
count: usize,
|
||||
ignored: HashSet<pezsc_network_types::PeerId>,
|
||||
) -> Vec<pezsc_network_types::PeerId>;
|
||||
|
||||
/// Add known peer.
|
||||
fn add_known_peer(&self, peer_id: pezsc_network_types::PeerId);
|
||||
}
|
||||
|
||||
/// Actual implementation of peer reputations and connection candidates provider.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeerStoreHandle {
|
||||
inner: Arc<Mutex<PeerStoreInner>>,
|
||||
}
|
||||
|
||||
impl PeerStoreProvider for PeerStoreHandle {
|
||||
fn is_banned(&self, peer_id: &pezsc_network_types::PeerId) -> bool {
|
||||
self.inner.lock().is_banned(&peer_id.into())
|
||||
}
|
||||
|
||||
fn register_protocol(&self, protocol_handle: Arc<dyn ProtocolHandle>) {
|
||||
self.inner.lock().register_protocol(protocol_handle);
|
||||
}
|
||||
|
||||
fn report_disconnect(&self, peer_id: pezsc_network_types::PeerId) {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.report_disconnect(peer_id.into())
|
||||
}
|
||||
|
||||
fn report_peer(&self, peer_id: pezsc_network_types::PeerId, change: ReputationChange) {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.report_peer(peer_id.into(), change)
|
||||
}
|
||||
|
||||
fn set_peer_role(&self, peer_id: &pezsc_network_types::PeerId, role: ObservedRole) {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.set_peer_role(&peer_id.into(), role)
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, peer_id: &pezsc_network_types::PeerId) -> i32 {
|
||||
self.inner.lock().peer_reputation(&peer_id.into())
|
||||
}
|
||||
|
||||
fn peer_role(&self, peer_id: &pezsc_network_types::PeerId) -> Option<ObservedRole> {
|
||||
self.inner.lock().peer_role(&peer_id.into())
|
||||
}
|
||||
|
||||
fn outgoing_candidates(
|
||||
&self,
|
||||
count: usize,
|
||||
ignored: HashSet<pezsc_network_types::PeerId>,
|
||||
) -> Vec<pezsc_network_types::PeerId> {
|
||||
self.inner
|
||||
.lock()
|
||||
.outgoing_candidates(count, ignored.iter().map(|peer_id| (*peer_id).into()).collect())
|
||||
.iter()
|
||||
.map(|peer_id| peer_id.into())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_known_peer(&self, peer_id: pezsc_network_types::PeerId) {
|
||||
self.inner.lock().add_known_peer(peer_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PeerInfo {
|
||||
/// Reputation of the peer.
|
||||
reputation: i32,
|
||||
|
||||
/// Instant when the peer was last updated.
|
||||
last_updated: Instant,
|
||||
|
||||
/// Role of the peer, if known.
|
||||
role: Option<ObservedRole>,
|
||||
}
|
||||
|
||||
impl Default for PeerInfo {
|
||||
fn default() -> Self {
|
||||
Self { reputation: 0, last_updated: Instant::now(), role: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PeerInfo {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.reputation == other.reputation
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PeerInfo {}
|
||||
|
||||
impl Ord for PeerInfo {
|
||||
// We define reverse order by reputation values.
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.reputation.cmp(&other.reputation).reverse()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PeerInfo {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PeerInfo {
|
||||
fn is_banned(&self) -> bool {
|
||||
self.reputation < BANNED_THRESHOLD
|
||||
}
|
||||
|
||||
fn add_reputation(&mut self, increment: i32) {
|
||||
self.reputation = self.reputation.saturating_add(increment);
|
||||
self.bump_last_updated();
|
||||
}
|
||||
|
||||
fn decay_reputation(&mut self, seconds_passed: u64) {
|
||||
// Note that decaying the reputation value happens "on its own",
|
||||
// so we don't do `bump_last_updated()`.
|
||||
for _ in 0..seconds_passed {
|
||||
let mut diff = self.reputation / INVERSE_DECREMENT;
|
||||
if diff == 0 && self.reputation < 0 {
|
||||
diff = -1;
|
||||
} else if diff == 0 && self.reputation > 0 {
|
||||
diff = 1;
|
||||
}
|
||||
|
||||
self.reputation = self.reputation.saturating_sub(diff);
|
||||
|
||||
if self.reputation == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bump_last_updated(&mut self) {
|
||||
self.last_updated = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PeerStoreInner {
|
||||
peers: HashMap<PeerId, PeerInfo>,
|
||||
protocols: Vec<Arc<dyn ProtocolHandle>>,
|
||||
metrics: Option<PeerStoreMetrics>,
|
||||
}
|
||||
|
||||
impl PeerStoreInner {
|
||||
fn is_banned(&self, peer_id: &PeerId) -> bool {
|
||||
self.peers.get(peer_id).map_or(false, |info| info.is_banned())
|
||||
}
|
||||
|
||||
fn register_protocol(&mut self, protocol_handle: Arc<dyn ProtocolHandle>) {
|
||||
self.protocols.push(protocol_handle);
|
||||
}
|
||||
|
||||
fn report_disconnect(&mut self, peer_id: PeerId) {
|
||||
let peer_info = self.peers.entry(peer_id).or_default();
|
||||
peer_info.add_reputation(DISCONNECT_REPUTATION_CHANGE);
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Peer {} disconnected, reputation: {:+} to {}",
|
||||
peer_id,
|
||||
DISCONNECT_REPUTATION_CHANGE,
|
||||
peer_info.reputation,
|
||||
);
|
||||
}
|
||||
|
||||
fn report_peer(&mut self, peer_id: PeerId, change: ReputationChange) {
|
||||
let peer_info = self.peers.entry(peer_id).or_default();
|
||||
let was_banned = peer_info.is_banned();
|
||||
peer_info.add_reputation(change.value);
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"Report {}: {:+} to {}. Reason: {}.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_info.reputation,
|
||||
change.reason,
|
||||
);
|
||||
|
||||
if !peer_info.is_banned() {
|
||||
if was_banned {
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Peer {} is now unbanned: {:+} to {}. Reason: {}.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_info.reputation,
|
||||
change.reason,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Peer is currently banned, disconnect it from all protocols.
|
||||
self.protocols.iter().for_each(|handle| handle.disconnect_peer(peer_id.into()));
|
||||
|
||||
// The peer is banned for the first time.
|
||||
if !was_banned {
|
||||
log::warn!(
|
||||
target: LOG_TARGET,
|
||||
"Report {}: {:+} to {}. Reason: {}. Banned, disconnecting.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_info.reputation,
|
||||
change.reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The peer was already banned and it got another negative report.
|
||||
// This may happen during a batch report.
|
||||
if change.value < 0 {
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"Report {}: {:+} to {}. Reason: {}. Misbehaved during the ban threshold.",
|
||||
peer_id,
|
||||
change.value,
|
||||
peer_info.reputation,
|
||||
change.reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_peer_role(&mut self, peer_id: &PeerId, role: ObservedRole) {
|
||||
log::trace!(target: LOG_TARGET, "Set {peer_id} role to {role:?}");
|
||||
|
||||
match self.peers.entry(*peer_id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().role = Some(role);
|
||||
},
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(PeerInfo { role: Some(role), ..Default::default() });
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
|
||||
self.peers.get(peer_id).map_or(0, |info| info.reputation)
|
||||
}
|
||||
|
||||
fn peer_role(&self, peer_id: &PeerId) -> Option<ObservedRole> {
|
||||
self.peers.get(peer_id).map_or(None, |info| info.role)
|
||||
}
|
||||
|
||||
fn outgoing_candidates(&self, count: usize, ignored: HashSet<PeerId>) -> Vec<PeerId> {
|
||||
let mut candidates = self
|
||||
.peers
|
||||
.iter()
|
||||
.filter_map(|(peer_id, info)| {
|
||||
(!info.is_banned() && !ignored.contains(peer_id)).then_some((*peer_id, *info))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let count = std::cmp::min(count, candidates.len());
|
||||
candidates.partial_sort(count, |(_, info1), (_, info2)| info1.cmp(info2));
|
||||
candidates.iter().take(count).map(|(peer_id, _)| *peer_id).collect()
|
||||
|
||||
// TODO: keep the peers sorted (in a "bi-multi-map"?) to not repeat sorting every time.
|
||||
}
|
||||
|
||||
fn progress_time(&mut self, seconds_passed: u64) {
|
||||
if seconds_passed == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drive reputation values towards 0.
|
||||
self.peers
|
||||
.iter_mut()
|
||||
.for_each(|(_, info)| info.decay_reputation(seconds_passed));
|
||||
|
||||
// Retain only entries with non-zero reputation values or not expired ones.
|
||||
let now = Instant::now();
|
||||
let mut num_banned_peers: u64 = 0;
|
||||
self.peers.retain(|_, info| {
|
||||
if info.is_banned() {
|
||||
num_banned_peers += 1;
|
||||
}
|
||||
|
||||
info.reputation != 0 || info.last_updated + FORGET_AFTER > now
|
||||
});
|
||||
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.num_discovered.set(self.peers.len() as u64);
|
||||
metrics.num_banned_peers.set(num_banned_peers);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_known_peer(&mut self, peer_id: PeerId) {
|
||||
match self.peers.entry(peer_id) {
|
||||
Entry::Occupied(mut e) => {
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"Trying to add an already known peer {peer_id}, bumping `last_updated`.",
|
||||
);
|
||||
e.get_mut().bump_last_updated();
|
||||
},
|
||||
Entry::Vacant(e) => {
|
||||
trace!(target: LOG_TARGET, "Adding a new known peer {peer_id}.");
|
||||
e.insert(PeerInfo::default());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker part of [`PeerStoreHandle`]
|
||||
#[derive(Debug)]
|
||||
pub struct PeerStore {
|
||||
inner: Arc<Mutex<PeerStoreInner>>,
|
||||
}
|
||||
|
||||
impl PeerStore {
|
||||
/// Create a new peer store from the list of bootnodes.
|
||||
pub fn new(bootnodes: Vec<PeerId>, metrics_registry: Option<Registry>) -> Self {
|
||||
let metrics = if let Some(registry) = &metrics_registry {
|
||||
PeerStoreMetrics::register(registry)
|
||||
.map_err(|err| {
|
||||
log::error!(target: LOG_TARGET, "Failed to register peer set metrics: {}", err);
|
||||
err
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
PeerStore {
|
||||
inner: Arc::new(Mutex::new(PeerStoreInner {
|
||||
peers: bootnodes
|
||||
.into_iter()
|
||||
.map(|peer_id| (peer_id, PeerInfo::default()))
|
||||
.collect(),
|
||||
protocols: Vec::new(),
|
||||
metrics,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get `PeerStoreHandle`.
|
||||
pub fn handle(&self) -> PeerStoreHandle {
|
||||
PeerStoreHandle { inner: self.inner.clone() }
|
||||
}
|
||||
|
||||
/// Drive the `PeerStore`, decaying reputation values over time and removing expired entries.
|
||||
pub async fn run(self) {
|
||||
let started = Instant::now();
|
||||
let mut latest_time_update = started;
|
||||
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
// We basically do `(now - self.latest_update).as_secs()`, except that by the way we do
|
||||
// it we know that we're not going to miss seconds because of rounding to integers.
|
||||
let seconds_passed = {
|
||||
let elapsed_latest = latest_time_update - started;
|
||||
let elapsed_now = now - started;
|
||||
latest_time_update = now;
|
||||
elapsed_now.as_secs() - elapsed_latest.as_secs()
|
||||
};
|
||||
|
||||
self.inner.lock().progress_time(seconds_passed);
|
||||
let _ = Delay::new(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerStoreT for PeerStore {
|
||||
fn handle(&self) -> Arc<dyn PeerStoreProvider> {
|
||||
Arc::new(self.handle())
|
||||
}
|
||||
|
||||
async fn run(self) {
|
||||
self.run().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PeerInfo, PeerStore, PeerStoreProvider};
|
||||
|
||||
#[test]
|
||||
fn decaying_zero_reputation_yields_zero() {
|
||||
let mut peer_info = PeerInfo::default();
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
|
||||
peer_info.decay_reputation(1);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
|
||||
peer_info.decay_reputation(100_000);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_positive_reputation_decreases_it() {
|
||||
const INITIAL_REPUTATION: i32 = 100;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(1);
|
||||
assert!(peer_info.reputation >= 0);
|
||||
assert!(peer_info.reputation < INITIAL_REPUTATION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_negative_reputation_increases_it() {
|
||||
const INITIAL_REPUTATION: i32 = -100;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(1);
|
||||
assert!(peer_info.reputation <= 0);
|
||||
assert!(peer_info.reputation > INITIAL_REPUTATION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_max_reputation_finally_yields_zero() {
|
||||
const INITIAL_REPUTATION: i32 = i32::MAX;
|
||||
const SECONDS: u64 = 3544;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert!(peer_info.reputation > 0);
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decaying_min_reputation_finally_yields_zero() {
|
||||
const INITIAL_REPUTATION: i32 = i32::MIN;
|
||||
const SECONDS: u64 = 3544;
|
||||
|
||||
let mut peer_info = PeerInfo::default();
|
||||
peer_info.reputation = INITIAL_REPUTATION;
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert!(peer_info.reputation < 0);
|
||||
|
||||
peer_info.decay_reputation(SECONDS / 2);
|
||||
assert_eq!(peer_info.reputation, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_banned_peers() {
|
||||
let peer_a = pezsc_network_types::PeerId::random();
|
||||
let peer_b = pezsc_network_types::PeerId::random();
|
||||
let peer_c = pezsc_network_types::PeerId::random();
|
||||
|
||||
let metrics_registry = prometheus_endpoint::Registry::new();
|
||||
let peerstore = PeerStore::new(
|
||||
vec![peer_a, peer_b, peer_c].into_iter().map(Into::into).collect(),
|
||||
Some(metrics_registry),
|
||||
);
|
||||
let metrics = peerstore.inner.lock().metrics.as_ref().unwrap().clone();
|
||||
let handle = peerstore.handle();
|
||||
|
||||
// Check initial state. Advance time to propagate peers.
|
||||
handle.inner.lock().progress_time(1);
|
||||
assert_eq!(metrics.num_discovered.get(), 3);
|
||||
assert_eq!(metrics.num_banned_peers.get(), 0);
|
||||
|
||||
// Report 2 peers with a negative reputation.
|
||||
handle.report_peer(
|
||||
peer_a,
|
||||
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
|
||||
);
|
||||
handle.report_peer(
|
||||
peer_b,
|
||||
pezsc_network_common::types::ReputationChange { value: i32::MIN, reason: "test".into() },
|
||||
);
|
||||
|
||||
// Advance time to propagate banned peers.
|
||||
handle.inner.lock().progress_time(1);
|
||||
assert_eq!(metrics.num_discovered.get(), 3);
|
||||
assert_eq!(metrics.num_banned_peers.get(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
config, error,
|
||||
peer_store::PeerStoreProvider,
|
||||
protocol_controller::{self, SetId},
|
||||
service::{metrics::NotificationMetrics, traits::Direction},
|
||||
types::ProtocolName,
|
||||
};
|
||||
|
||||
use codec::Encode;
|
||||
use libp2p::{
|
||||
core::{transport::PortUse, Endpoint},
|
||||
swarm::{
|
||||
behaviour::FromSwarm, ConnectionDenied, ConnectionId, NetworkBehaviour, THandler,
|
||||
THandlerInEvent, THandlerOutEvent, ToSwarm,
|
||||
},
|
||||
Multiaddr, PeerId,
|
||||
};
|
||||
use log::{debug, warn};
|
||||
|
||||
use codec::DecodeAll;
|
||||
use pezsc_network_common::{role::Roles, types::ReputationChange};
|
||||
use pezsc_utils::mpsc::TracingUnboundedReceiver;
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use std::{collections::HashSet, iter, sync::Arc, task::Poll};
|
||||
|
||||
use notifications::{Notifications, NotificationsOut};
|
||||
|
||||
pub(crate) use notifications::ProtocolHandle;
|
||||
|
||||
pub use notifications::{notification_service, NotificationsSink, ProtocolHandlePair, Ready};
|
||||
|
||||
mod notifications;
|
||||
|
||||
pub mod message;
|
||||
|
||||
// Log target for this file.
|
||||
const LOG_TARGET: &str = "sub-libp2p";
|
||||
|
||||
/// Identifier of the peerset for the block announces protocol.
|
||||
const HARDCODED_PEERSETS_SYNC: SetId = SetId::from(0);
|
||||
|
||||
// Lock must always be taken in order declared here.
|
||||
pub struct Protocol<B: BlockT> {
|
||||
/// Handles opening the unique substream and sending and receiving raw messages.
|
||||
behaviour: Notifications,
|
||||
/// List of notifications protocols that have been registered.
|
||||
notification_protocols: Vec<ProtocolName>,
|
||||
/// Handle to `PeerStore`.
|
||||
peer_store_handle: Arc<dyn PeerStoreProvider>,
|
||||
/// Streams for peers whose handshake couldn't be determined.
|
||||
bad_handshake_streams: HashSet<PeerId>,
|
||||
sync_handle: ProtocolHandle,
|
||||
_marker: std::marker::PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<B: BlockT> Protocol<B> {
|
||||
/// Create a new instance.
|
||||
pub(crate) fn new(
|
||||
roles: Roles,
|
||||
notification_metrics: NotificationMetrics,
|
||||
notification_protocols: Vec<config::NonDefaultSetConfig>,
|
||||
block_announces_protocol: config::NonDefaultSetConfig,
|
||||
peer_store_handle: Arc<dyn PeerStoreProvider>,
|
||||
protocol_controller_handles: Vec<protocol_controller::ProtocolHandle>,
|
||||
from_protocol_controllers: TracingUnboundedReceiver<protocol_controller::Message>,
|
||||
) -> error::Result<(Self, Vec<ProtocolHandle>)> {
|
||||
let (behaviour, notification_protocols, handles) = {
|
||||
let installed_protocols = iter::once(block_announces_protocol.protocol_name().clone())
|
||||
.chain(notification_protocols.iter().map(|p| p.protocol_name().clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// NOTE: Block announcement protocol is still very much hardcoded into
|
||||
// `Protocol`. This protocol must be the first notification protocol given to
|
||||
// `Notifications`
|
||||
let (protocol_configs, mut handles): (Vec<_>, Vec<_>) = iter::once({
|
||||
let config = notifications::ProtocolConfig {
|
||||
name: block_announces_protocol.protocol_name().clone(),
|
||||
fallback_names: block_announces_protocol.fallback_names().cloned().collect(),
|
||||
handshake: block_announces_protocol.handshake().as_ref().unwrap().to_vec(),
|
||||
max_notification_size: block_announces_protocol.max_notification_size(),
|
||||
};
|
||||
|
||||
let (handle, command_stream) =
|
||||
block_announces_protocol.take_protocol_handle().split();
|
||||
|
||||
((config, handle.clone(), command_stream), handle)
|
||||
})
|
||||
.chain(notification_protocols.into_iter().map(|s| {
|
||||
let config = notifications::ProtocolConfig {
|
||||
name: s.protocol_name().clone(),
|
||||
fallback_names: s.fallback_names().cloned().collect(),
|
||||
handshake: s.handshake().as_ref().map_or(roles.encode(), |h| (*h).to_vec()),
|
||||
max_notification_size: s.max_notification_size(),
|
||||
};
|
||||
|
||||
let (handle, command_stream) = s.take_protocol_handle().split();
|
||||
|
||||
((config, handle.clone(), command_stream), handle)
|
||||
}))
|
||||
.unzip();
|
||||
|
||||
handles.iter_mut().for_each(|handle| {
|
||||
handle.set_metrics(notification_metrics.clone());
|
||||
});
|
||||
|
||||
protocol_configs.iter().enumerate().for_each(|(i, (p, _, _))| {
|
||||
debug!(target: LOG_TARGET, "Notifications protocol {:?}: {}", SetId::from(i), p.name);
|
||||
});
|
||||
|
||||
(
|
||||
Notifications::new(
|
||||
protocol_controller_handles,
|
||||
from_protocol_controllers,
|
||||
notification_metrics,
|
||||
protocol_configs.into_iter(),
|
||||
),
|
||||
installed_protocols,
|
||||
handles,
|
||||
)
|
||||
};
|
||||
|
||||
let protocol = Self {
|
||||
behaviour,
|
||||
sync_handle: handles[0].clone(),
|
||||
peer_store_handle,
|
||||
notification_protocols,
|
||||
bad_handshake_streams: HashSet::new(),
|
||||
// TODO: remove when `BlockAnnouncesHandshake` is moved away from `Protocol`
|
||||
_marker: Default::default(),
|
||||
};
|
||||
|
||||
Ok((protocol, handles))
|
||||
}
|
||||
|
||||
pub fn num_sync_peers(&self) -> usize {
|
||||
self.sync_handle.num_peers()
|
||||
}
|
||||
|
||||
/// Returns the list of all the peers we have an open channel to.
|
||||
pub fn open_peers(&self) -> impl Iterator<Item = &PeerId> {
|
||||
self.behaviour.open_peers()
|
||||
}
|
||||
|
||||
/// Disconnects the given peer if we are connected to it.
|
||||
pub fn disconnect_peer(&mut self, peer_id: &PeerId, protocol_name: ProtocolName) {
|
||||
if let Some(position) = self.notification_protocols.iter().position(|p| *p == protocol_name)
|
||||
{
|
||||
self.behaviour.disconnect_peer(peer_id, SetId::from(position));
|
||||
} else {
|
||||
warn!(target: LOG_TARGET, "disconnect_peer() with invalid protocol name")
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if role is available for `peer_id` by attempt to decode the handshake to roles and if
|
||||
/// that fails, check if the role has been registered to `PeerStore`.
|
||||
fn role_available(&self, peer_id: &PeerId, handshake: &Vec<u8>) -> bool {
|
||||
match Roles::decode_all(&mut &handshake[..]) {
|
||||
Ok(_) => true,
|
||||
Err(_) => self.peer_store_handle.peer_role(&((*peer_id).into())).is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of an incoming custom message.
|
||||
#[derive(Debug)]
|
||||
#[must_use]
|
||||
pub enum CustomMessageOutcome {
|
||||
/// Notification protocols have been opened with a remote.
|
||||
NotificationStreamOpened {
|
||||
remote: PeerId,
|
||||
// protocol: ProtocolName,
|
||||
set_id: SetId,
|
||||
/// Direction of the stream.
|
||||
direction: Direction,
|
||||
/// See [`crate::Event::NotificationStreamOpened::negotiated_fallback`].
|
||||
negotiated_fallback: Option<ProtocolName>,
|
||||
/// Received handshake.
|
||||
received_handshake: Vec<u8>,
|
||||
/// Notification sink.
|
||||
notifications_sink: NotificationsSink,
|
||||
},
|
||||
/// The [`NotificationsSink`] of some notification protocols need an update.
|
||||
NotificationStreamReplaced {
|
||||
// Peer ID.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
/// New notification sink.
|
||||
notifications_sink: NotificationsSink,
|
||||
},
|
||||
/// Notification protocols have been closed with a remote.
|
||||
NotificationStreamClosed {
|
||||
// Peer ID.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
},
|
||||
/// Messages have been received on one or more notifications protocols.
|
||||
NotificationsReceived {
|
||||
// Peer ID.
|
||||
remote: PeerId,
|
||||
/// Set ID.
|
||||
set_id: SetId,
|
||||
/// Received notification.
|
||||
notification: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<B: BlockT> NetworkBehaviour for Protocol<B> {
|
||||
type ConnectionHandler = <Notifications as NetworkBehaviour>::ConnectionHandler;
|
||||
type ToSwarm = CustomMessageOutcome;
|
||||
|
||||
fn handle_established_inbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
local_addr: &Multiaddr,
|
||||
remote_addr: &Multiaddr,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
self.behaviour.handle_established_inbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
local_addr,
|
||||
remote_addr,
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_established_outbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
addr: &Multiaddr,
|
||||
role_override: Endpoint,
|
||||
port_use: PortUse,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
self.behaviour.handle_established_outbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
addr,
|
||||
role_override,
|
||||
port_use,
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_pending_outbound_connection(
|
||||
&mut self,
|
||||
_connection_id: ConnectionId,
|
||||
_maybe_peer: Option<PeerId>,
|
||||
_addresses: &[Multiaddr],
|
||||
_effective_role: Endpoint,
|
||||
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
|
||||
// Only `Discovery::handle_pending_outbound_connection` must be returning addresses to
|
||||
// ensure that we don't return unwanted addresses.
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn on_swarm_event(&mut self, event: FromSwarm) {
|
||||
self.behaviour.on_swarm_event(event);
|
||||
}
|
||||
|
||||
fn on_connection_handler_event(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
connection_id: ConnectionId,
|
||||
event: THandlerOutEvent<Self>,
|
||||
) {
|
||||
self.behaviour.on_connection_handler_event(peer_id, connection_id, event);
|
||||
}
|
||||
|
||||
fn poll(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context,
|
||||
) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
|
||||
let event = match self.behaviour.poll(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(ToSwarm::GenerateEvent(ev)) => ev,
|
||||
Poll::Ready(event) => {
|
||||
return Poll::Ready(event.map_out(|_| {
|
||||
unreachable!("`GenerateEvent` is handled in a branch above; qed")
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
let outcome = match event {
|
||||
NotificationsOut::CustomProtocolOpen {
|
||||
peer_id,
|
||||
set_id,
|
||||
direction,
|
||||
received_handshake,
|
||||
notifications_sink,
|
||||
negotiated_fallback,
|
||||
..
|
||||
} =>
|
||||
if set_id == HARDCODED_PEERSETS_SYNC {
|
||||
let _ = self.sync_handle.report_substream_opened(
|
||||
peer_id,
|
||||
direction,
|
||||
received_handshake,
|
||||
negotiated_fallback,
|
||||
notifications_sink,
|
||||
);
|
||||
None
|
||||
} else {
|
||||
match self.role_available(&peer_id, &received_handshake) {
|
||||
true => Some(CustomMessageOutcome::NotificationStreamOpened {
|
||||
remote: peer_id,
|
||||
set_id,
|
||||
direction,
|
||||
negotiated_fallback,
|
||||
received_handshake,
|
||||
notifications_sink,
|
||||
}),
|
||||
false => {
|
||||
self.bad_handshake_streams.insert(peer_id);
|
||||
None
|
||||
},
|
||||
}
|
||||
},
|
||||
NotificationsOut::CustomProtocolReplaced { peer_id, notifications_sink, set_id } =>
|
||||
if set_id == HARDCODED_PEERSETS_SYNC {
|
||||
let _ = self
|
||||
.sync_handle
|
||||
.report_notification_sink_replaced(peer_id, notifications_sink);
|
||||
None
|
||||
} else {
|
||||
(!self.bad_handshake_streams.contains(&peer_id)).then_some(
|
||||
CustomMessageOutcome::NotificationStreamReplaced {
|
||||
remote: peer_id,
|
||||
set_id,
|
||||
notifications_sink,
|
||||
},
|
||||
)
|
||||
},
|
||||
NotificationsOut::CustomProtocolClosed { peer_id, set_id } => {
|
||||
if set_id == HARDCODED_PEERSETS_SYNC {
|
||||
let _ = self.sync_handle.report_substream_closed(peer_id);
|
||||
None
|
||||
} else {
|
||||
(!self.bad_handshake_streams.remove(&peer_id)).then_some(
|
||||
CustomMessageOutcome::NotificationStreamClosed { remote: peer_id, set_id },
|
||||
)
|
||||
}
|
||||
},
|
||||
NotificationsOut::Notification { peer_id, set_id, message } => {
|
||||
if set_id == HARDCODED_PEERSETS_SYNC {
|
||||
let _ = self
|
||||
.sync_handle
|
||||
.report_notification_received(peer_id, message.freeze().into());
|
||||
None
|
||||
} else {
|
||||
(!self.bad_handshake_streams.contains(&peer_id)).then_some(
|
||||
CustomMessageOutcome::NotificationsReceived {
|
||||
remote: peer_id,
|
||||
set_id,
|
||||
notification: message.freeze().into(),
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
NotificationsOut::ProtocolMisbehavior { peer_id, set_id } => {
|
||||
let index: usize = set_id.into();
|
||||
let protocol_name = self.notification_protocols.get(index);
|
||||
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Received unexpected data on outbound notification stream from peer {:?} on protocol {:?}",
|
||||
peer_id,
|
||||
protocol_name
|
||||
);
|
||||
|
||||
self.peer_store_handle.report_peer(
|
||||
peer_id.into(),
|
||||
ReputationChange::new_fatal(
|
||||
"Received unexpected data on outbound notification stream",
|
||||
),
|
||||
);
|
||||
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
match outcome {
|
||||
Some(event) => Poll::Ready(ToSwarm::GenerateEvent(event)),
|
||||
None => {
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Network packet message types. These get serialized and put into the lower level protocol
|
||||
//! payload.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use pezsc_client_api::StorageProof;
|
||||
use pezsc_network_common::message::RequestId;
|
||||
|
||||
/// Remote call response.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
pub struct RemoteCallResponse {
|
||||
/// Id of a request this response was made for.
|
||||
pub id: RequestId,
|
||||
/// Execution proof.
|
||||
pub proof: StorageProof,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote read response.
|
||||
pub struct RemoteReadResponse {
|
||||
/// Id of a request this response was made for.
|
||||
pub id: RequestId,
|
||||
/// Read proof.
|
||||
pub proof: StorageProof,
|
||||
}
|
||||
|
||||
/// Generic types.
|
||||
pub mod generic {
|
||||
use codec::{Decode, Encode, Input};
|
||||
use pezsc_client_api::StorageProof;
|
||||
use pezsc_network_common::{message::RequestId, role::Roles};
|
||||
use pezsp_runtime::ConsensusEngineId;
|
||||
|
||||
/// Consensus is mostly opaque to us
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ConsensusMessage {
|
||||
/// Identifies consensus engine.
|
||||
pub protocol: ConsensusEngineId,
|
||||
/// Message payload.
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Status sent on connection.
|
||||
// TODO https://github.com/pezkuwichain/kurdistan-sdk/issues/24: replace the `Status`
|
||||
// struct with this one, after waiting a few releases beyond `NetworkSpecialization`'s
|
||||
// removal (https://github.com/pezkuwichain/kurdistan-sdk/issues/55)
|
||||
//
|
||||
// and set MIN_VERSION to 6.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CompactStatus<Hash, Number> {
|
||||
/// Protocol version.
|
||||
pub version: u32,
|
||||
/// Minimum supported version.
|
||||
pub min_supported_version: u32,
|
||||
/// Supported roles.
|
||||
pub roles: Roles,
|
||||
/// Best block number.
|
||||
pub best_number: Number,
|
||||
/// Best block hash.
|
||||
pub best_hash: Hash,
|
||||
/// Genesis block hash.
|
||||
pub genesis_hash: Hash,
|
||||
}
|
||||
|
||||
/// Status sent on connection.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Status<Hash, Number> {
|
||||
/// Protocol version.
|
||||
pub version: u32,
|
||||
/// Minimum supported version.
|
||||
pub min_supported_version: u32,
|
||||
/// Supported roles.
|
||||
pub roles: Roles,
|
||||
/// Best block number.
|
||||
pub best_number: Number,
|
||||
/// Best block hash.
|
||||
pub best_hash: Hash,
|
||||
/// Genesis block hash.
|
||||
pub genesis_hash: Hash,
|
||||
/// DEPRECATED. Chain-specific status.
|
||||
pub chain_status: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<Hash: Decode, Number: Decode> Decode for Status<Hash, Number> {
|
||||
fn decode<I: Input>(value: &mut I) -> Result<Self, codec::Error> {
|
||||
const LAST_CHAIN_STATUS_VERSION: u32 = 5;
|
||||
let compact = CompactStatus::decode(value)?;
|
||||
let chain_status = match <Vec<u8>>::decode(value) {
|
||||
Ok(v) => v,
|
||||
Err(e) =>
|
||||
if compact.version <= LAST_CHAIN_STATUS_VERSION {
|
||||
return Err(e);
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
};
|
||||
|
||||
let CompactStatus {
|
||||
version,
|
||||
min_supported_version,
|
||||
roles,
|
||||
best_number,
|
||||
best_hash,
|
||||
genesis_hash,
|
||||
} = compact;
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
min_supported_version,
|
||||
roles,
|
||||
best_number,
|
||||
best_hash,
|
||||
genesis_hash,
|
||||
chain_status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote call request.
|
||||
pub struct RemoteCallRequest<H> {
|
||||
/// Unique request id.
|
||||
pub id: RequestId,
|
||||
/// Block at which to perform call.
|
||||
pub block: H,
|
||||
/// Method name.
|
||||
pub method: String,
|
||||
/// Call data.
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote storage read request.
|
||||
pub struct RemoteReadRequest<H> {
|
||||
/// Unique request id.
|
||||
pub id: RequestId,
|
||||
/// Block at which to perform call.
|
||||
pub block: H,
|
||||
/// Storage key.
|
||||
pub keys: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote storage read child request.
|
||||
pub struct RemoteReadChildRequest<H> {
|
||||
/// Unique request id.
|
||||
pub id: RequestId,
|
||||
/// Block at which to perform call.
|
||||
pub block: H,
|
||||
/// Child Storage key.
|
||||
pub storage_key: Vec<u8>,
|
||||
/// Storage key.
|
||||
pub keys: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote header request.
|
||||
pub struct RemoteHeaderRequest<N> {
|
||||
/// Unique request id.
|
||||
pub id: RequestId,
|
||||
/// Block number to request header for.
|
||||
pub block: N,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote header response.
|
||||
pub struct RemoteHeaderResponse<Header> {
|
||||
/// Id of a request this response was made for.
|
||||
pub id: RequestId,
|
||||
/// Header. None if proof generation has failed (e.g. header is unknown).
|
||||
pub header: Option<Header>,
|
||||
/// Header proof.
|
||||
pub proof: StorageProof,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote changes request.
|
||||
pub struct RemoteChangesRequest<H> {
|
||||
/// Unique request id.
|
||||
pub id: RequestId,
|
||||
/// Hash of the first block of the range (including first) where changes are requested.
|
||||
pub first: H,
|
||||
/// Hash of the last block of the range (including last) where changes are requested.
|
||||
pub last: H,
|
||||
/// Hash of the first block for which the requester has the changes trie root. All other
|
||||
/// affected roots must be proved.
|
||||
pub min: H,
|
||||
/// Hash of the last block that we can use when querying changes.
|
||||
pub max: H,
|
||||
/// Storage child node key which changes are requested.
|
||||
pub storage_key: Option<Vec<u8>>,
|
||||
/// Storage key which changes are requested.
|
||||
pub key: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
|
||||
#[allow(dead_code)]
|
||||
/// Remote changes response.
|
||||
pub struct RemoteChangesResponse<N, H> {
|
||||
/// Id of a request this response was made for.
|
||||
pub id: RequestId,
|
||||
/// Proof has been generated using block with this number as a max block. Should be
|
||||
/// less than or equal to the RemoteChangesRequest::max block number.
|
||||
pub max: N,
|
||||
/// Changes proof.
|
||||
pub proof: Vec<Vec<u8>>,
|
||||
/// Changes tries roots missing on the requester' node.
|
||||
pub roots: Vec<(N, H)>,
|
||||
/// Missing changes tries roots proof.
|
||||
pub roots_proof: StorageProof,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Implementation of libp2p's `NetworkBehaviour` trait that establishes communications and opens
|
||||
//! notifications substreams.
|
||||
|
||||
pub use self::{
|
||||
behaviour::{Notifications, NotificationsOut, ProtocolConfig},
|
||||
handler::{NotificationsSink, Ready},
|
||||
service::{notification_service, ProtocolHandlePair},
|
||||
};
|
||||
|
||||
pub(crate) use self::service::ProtocolHandle;
|
||||
|
||||
mod behaviour;
|
||||
mod handler;
|
||||
mod service;
|
||||
mod tests;
|
||||
mod upgrade;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{service::metrics::NotificationMetrics, types::ProtocolName};
|
||||
|
||||
/// Register opened substream to Prometheus.
|
||||
pub fn register_substream_opened(metrics: &Option<NotificationMetrics>, protocol: &ProtocolName) {
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.register_substream_opened(&protocol);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register closed substream to Prometheus.
|
||||
pub fn register_substream_closed(metrics: &Option<NotificationMetrics>, protocol: &ProtocolName) {
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.register_substream_closed(&protocol);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register sent notification to Prometheus.
|
||||
pub fn register_notification_sent(
|
||||
metrics: &Option<std::sync::Arc<NotificationMetrics>>,
|
||||
protocol: &ProtocolName,
|
||||
size: usize,
|
||||
) {
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.register_notification_sent(protocol, size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register received notification to Prometheus.
|
||||
pub fn register_notification_received(
|
||||
metrics: &Option<NotificationMetrics>,
|
||||
protocol: &ProtocolName,
|
||||
size: usize,
|
||||
) {
|
||||
if let Some(metrics) = metrics {
|
||||
metrics.register_notification_received(protocol, size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Notification service implementation.
|
||||
|
||||
use crate::{
|
||||
error,
|
||||
protocol::notifications::handler::NotificationsSink,
|
||||
service::{
|
||||
metrics::NotificationMetrics,
|
||||
traits::{
|
||||
Direction, MessageSink, NotificationEvent, NotificationService, ValidationResult,
|
||||
},
|
||||
},
|
||||
types::ProtocolName,
|
||||
};
|
||||
|
||||
use futures::{
|
||||
stream::{FuturesUnordered, Stream},
|
||||
StreamExt,
|
||||
};
|
||||
use libp2p::PeerId;
|
||||
use parking_lot::Mutex;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use pezsc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
|
||||
use std::{collections::HashMap, fmt::Debug, sync::Arc};
|
||||
|
||||
pub(crate) mod metrics;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::notification::service";
|
||||
|
||||
/// Default command queue size.
|
||||
const COMMAND_QUEUE_SIZE: usize = 64;
|
||||
|
||||
/// Type representing subscribers of a notification protocol.
|
||||
type Subscribers = Arc<Mutex<Vec<TracingUnboundedSender<InnerNotificationEvent>>>>;
|
||||
|
||||
/// Type representing a distributable message sink.
|
||||
/// Detached message sink must carry the protocol name for registering metrics.
|
||||
///
|
||||
/// See documentation for [`PeerContext`] for more details.
|
||||
type NotificationSink = Arc<Mutex<(NotificationsSink, ProtocolName)>>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MessageSink for NotificationSink {
|
||||
/// Send synchronous `notification` to the peer associated with this [`MessageSink`].
|
||||
fn send_sync_notification(&self, notification: Vec<u8>) {
|
||||
let sink = self.lock();
|
||||
|
||||
metrics::register_notification_sent(sink.0.metrics(), &sink.1, notification.len());
|
||||
sink.0.send_sync_notification(notification);
|
||||
}
|
||||
|
||||
/// Send an asynchronous `notification` to the peer associated with this [`MessageSink`],
|
||||
/// allowing sender to exercise backpressure.
|
||||
///
|
||||
/// Returns an error if the peer does not exist.
|
||||
async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), error::Error> {
|
||||
// notification sink must be cloned because the lock cannot be held across `.await`
|
||||
// this makes the implementation less efficient but not prohibitively so as the same
|
||||
// method is also used by `NetworkService` when sending notifications.
|
||||
let notification_len = notification.len();
|
||||
let sink = self.lock().clone();
|
||||
let permit = sink
|
||||
.0
|
||||
.reserve_notification()
|
||||
.await
|
||||
.map_err(|_| error::Error::ConnectionClosed)?;
|
||||
|
||||
permit.send(notification).map_err(|_| error::Error::ChannelClosed).inspect(|_| {
|
||||
metrics::register_notification_sent(sink.0.metrics(), &sink.1, notification_len);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner notification event to deal with `NotificationsSinks` without exposing that
|
||||
/// implementation detail to [`NotificationService`] consumers.
|
||||
#[derive(Debug)]
|
||||
enum InnerNotificationEvent {
|
||||
/// Validate inbound substream.
|
||||
ValidateInboundSubstream {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Received handshake.
|
||||
handshake: Vec<u8>,
|
||||
|
||||
/// `oneshot::Sender` for sending validation result back to `Notifications`
|
||||
result_tx: oneshot::Sender<ValidationResult>,
|
||||
},
|
||||
|
||||
/// Notification substream open to `peer`.
|
||||
NotificationStreamOpened {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Direction of the substream.
|
||||
direction: Direction,
|
||||
|
||||
/// Received handshake.
|
||||
handshake: Vec<u8>,
|
||||
|
||||
/// Negotiated fallback.
|
||||
negotiated_fallback: Option<ProtocolName>,
|
||||
|
||||
/// Notification sink.
|
||||
sink: NotificationsSink,
|
||||
},
|
||||
|
||||
/// Substream was closed.
|
||||
NotificationStreamClosed {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
},
|
||||
|
||||
/// Notification was received from the substream.
|
||||
NotificationReceived {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Received notification.
|
||||
notification: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Notification sink has been replaced.
|
||||
NotificationSinkReplaced {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Notification sink.
|
||||
sink: NotificationsSink,
|
||||
},
|
||||
}
|
||||
|
||||
/// Notification commands.
|
||||
///
|
||||
/// Sent by the installed protocols to `Notifications` to open/close/modify substreams.
|
||||
#[derive(Debug)]
|
||||
pub enum NotificationCommand {
|
||||
/// Instruct `Notifications` to open a substream to peer.
|
||||
#[allow(unused)]
|
||||
OpenSubstream(PeerId),
|
||||
|
||||
/// Instruct `Notifications` to close the substream to peer.
|
||||
#[allow(unused)]
|
||||
CloseSubstream(PeerId),
|
||||
|
||||
/// Set handshake for the notifications protocol.
|
||||
SetHandshake(Vec<u8>),
|
||||
}
|
||||
|
||||
/// Context assigned to each peer.
|
||||
///
|
||||
/// Contains `NotificationsSink` used by [`NotificationService`] to send notifications
|
||||
/// and an additional, distributable `NotificationsSink` which the protocol may acquire
|
||||
/// if it wishes to send notifications through `NotificationsSink` directly.
|
||||
///
|
||||
/// The distributable `NotificationsSink` is wrapped in an `Arc<Mutex<>>` to allow
|
||||
/// `NotificationsService` to swap the underlying sink in case it's replaced.
|
||||
#[derive(Debug, Clone)]
|
||||
struct PeerContext {
|
||||
/// Sink for sending notifications.
|
||||
sink: NotificationsSink,
|
||||
|
||||
/// Distributable notification sink.
|
||||
shared_sink: NotificationSink,
|
||||
}
|
||||
|
||||
/// Handle that is passed on to the notifications protocol.
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationHandle {
|
||||
/// Protocol name.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// TX channel for sending commands to `Notifications`.
|
||||
tx: mpsc::Sender<NotificationCommand>,
|
||||
|
||||
/// RX channel for receiving events from `Notifications`.
|
||||
rx: TracingUnboundedReceiver<InnerNotificationEvent>,
|
||||
|
||||
/// All subscribers of `NotificationEvent`s.
|
||||
subscribers: Subscribers,
|
||||
|
||||
/// Connected peers.
|
||||
peers: HashMap<PeerId, PeerContext>,
|
||||
}
|
||||
|
||||
impl NotificationHandle {
|
||||
/// Create new [`NotificationHandle`].
|
||||
fn new(
|
||||
protocol: ProtocolName,
|
||||
tx: mpsc::Sender<NotificationCommand>,
|
||||
rx: TracingUnboundedReceiver<InnerNotificationEvent>,
|
||||
subscribers: Arc<Mutex<Vec<TracingUnboundedSender<InnerNotificationEvent>>>>,
|
||||
) -> Self {
|
||||
Self { protocol, tx, rx, subscribers, peers: HashMap::new() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NotificationService for NotificationHandle {
|
||||
/// Instruct `Notifications` to open a new substream for `peer`.
|
||||
async fn open_substream(&mut self, _peer: pezsc_network_types::PeerId) -> Result<(), ()> {
|
||||
todo!("support for opening substreams not implemented yet");
|
||||
}
|
||||
|
||||
/// Instruct `Notifications` to close substream for `peer`.
|
||||
async fn close_substream(&mut self, _peer: pezsc_network_types::PeerId) -> Result<(), ()> {
|
||||
todo!("support for closing substreams not implemented yet, call `NetworkService::disconnect_peer()` instead");
|
||||
}
|
||||
|
||||
/// Send synchronous `notification` to `peer`.
|
||||
fn send_sync_notification(&mut self, peer: &pezsc_network_types::PeerId, notification: Vec<u8>) {
|
||||
if let Some(info) = self.peers.get(&((*peer).into())) {
|
||||
metrics::register_notification_sent(
|
||||
info.sink.metrics(),
|
||||
&self.protocol,
|
||||
notification.len(),
|
||||
);
|
||||
|
||||
let _ = info.sink.send_sync_notification(notification);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
|
||||
async fn send_async_notification(
|
||||
&mut self,
|
||||
peer: &pezsc_network_types::PeerId,
|
||||
notification: Vec<u8>,
|
||||
) -> Result<(), error::Error> {
|
||||
let notification_len = notification.len();
|
||||
let sink = &self
|
||||
.peers
|
||||
.get(&peer.into())
|
||||
.ok_or_else(|| error::Error::PeerDoesntExist((*peer).into()))?
|
||||
.sink;
|
||||
|
||||
sink.reserve_notification()
|
||||
.await
|
||||
.map_err(|_| error::Error::ConnectionClosed)?
|
||||
.send(notification)
|
||||
.map_err(|_| error::Error::ChannelClosed)
|
||||
.inspect(|_| {
|
||||
metrics::register_notification_sent(
|
||||
sink.metrics(),
|
||||
&self.protocol,
|
||||
notification_len,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
/// Set handshake for the notification protocol replacing the old handshake.
|
||||
async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
|
||||
log::trace!(target: LOG_TARGET, "{}: set handshake to {handshake:?}", self.protocol);
|
||||
|
||||
self.tx.send(NotificationCommand::SetHandshake(handshake)).await.map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Non-blocking variant of `set_handshake()` that attempts to update the handshake
|
||||
/// and returns an error if the channel is blocked.
|
||||
///
|
||||
/// Technically the function can return an error if the channel to `Notifications` is closed
|
||||
/// but that doesn't happen under normal operation.
|
||||
fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
|
||||
self.tx.try_send(NotificationCommand::SetHandshake(handshake)).map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Get next event from the `Notifications` event stream.
|
||||
async fn next_event(&mut self) -> Option<NotificationEvent> {
|
||||
loop {
|
||||
match self.rx.next().await? {
|
||||
InnerNotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx } =>
|
||||
return Some(NotificationEvent::ValidateInboundSubstream {
|
||||
peer: peer.into(),
|
||||
handshake,
|
||||
result_tx,
|
||||
}),
|
||||
InnerNotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
handshake,
|
||||
negotiated_fallback,
|
||||
direction,
|
||||
sink,
|
||||
} => {
|
||||
self.peers.insert(
|
||||
peer,
|
||||
PeerContext {
|
||||
sink: sink.clone(),
|
||||
shared_sink: Arc::new(Mutex::new((sink, self.protocol.clone()))),
|
||||
},
|
||||
);
|
||||
return Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer: peer.into(),
|
||||
handshake,
|
||||
direction,
|
||||
negotiated_fallback,
|
||||
});
|
||||
},
|
||||
InnerNotificationEvent::NotificationStreamClosed { peer } => {
|
||||
self.peers.remove(&peer);
|
||||
return Some(NotificationEvent::NotificationStreamClosed { peer: peer.into() });
|
||||
},
|
||||
InnerNotificationEvent::NotificationReceived { peer, notification } =>
|
||||
return Some(NotificationEvent::NotificationReceived {
|
||||
peer: peer.into(),
|
||||
notification,
|
||||
}),
|
||||
InnerNotificationEvent::NotificationSinkReplaced { peer, sink } => {
|
||||
match self.peers.get_mut(&peer) {
|
||||
None => log::error!(
|
||||
"{}: notification sink replaced for {peer} but peer does not exist",
|
||||
self.protocol
|
||||
),
|
||||
Some(context) => {
|
||||
context.sink = sink.clone();
|
||||
*context.shared_sink.lock() = (sink.clone(), self.protocol.clone());
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clone [`NotificationService`]
|
||||
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()> {
|
||||
let mut subscribers = self.subscribers.lock();
|
||||
|
||||
let (event_tx, event_rx) = tracing_unbounded(self.rx.name(), 100_000);
|
||||
subscribers.push(event_tx);
|
||||
|
||||
Ok(Box::new(NotificationHandle {
|
||||
protocol: self.protocol.clone(),
|
||||
tx: self.tx.clone(),
|
||||
rx: event_rx,
|
||||
peers: self.peers.clone(),
|
||||
subscribers: self.subscribers.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get protocol name.
|
||||
fn protocol(&self) -> &ProtocolName {
|
||||
&self.protocol
|
||||
}
|
||||
|
||||
/// Get message sink of the peer.
|
||||
fn message_sink(&self, peer: &pezsc_network_types::PeerId) -> Option<Box<dyn MessageSink>> {
|
||||
match self.peers.get(&peer.into()) {
|
||||
Some(context) => Some(Box::new(context.shared_sink.clone())),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel pair which allows `Notifications` to interact with a protocol.
|
||||
#[derive(Debug)]
|
||||
pub struct ProtocolHandlePair {
|
||||
/// Protocol name.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Subscribers of the notification protocol events.
|
||||
subscribers: Subscribers,
|
||||
|
||||
// Receiver for notification commands received from the protocol implementation.
|
||||
rx: mpsc::Receiver<NotificationCommand>,
|
||||
}
|
||||
|
||||
impl ProtocolHandlePair {
|
||||
/// Create new [`ProtocolHandlePair`].
|
||||
fn new(
|
||||
protocol: ProtocolName,
|
||||
subscribers: Subscribers,
|
||||
rx: mpsc::Receiver<NotificationCommand>,
|
||||
) -> Self {
|
||||
Self { protocol, subscribers, rx }
|
||||
}
|
||||
|
||||
/// Consume `self` and split [`ProtocolHandlePair`] into a handle which allows it to send events
|
||||
/// to the protocol and a stream of commands received from the protocol.
|
||||
pub(crate) fn split(
|
||||
self,
|
||||
) -> (ProtocolHandle, Box<dyn Stream<Item = NotificationCommand> + Send + Unpin>) {
|
||||
(
|
||||
ProtocolHandle::new(self.protocol, self.subscribers),
|
||||
Box::new(ReceiverStream::new(self.rx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle that is passed on to `Notifications` and allows it to directly communicate
|
||||
/// with the protocol.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ProtocolHandle {
|
||||
/// Protocol name.
|
||||
protocol: ProtocolName,
|
||||
|
||||
/// Subscribers of the notification protocol.
|
||||
subscribers: Subscribers,
|
||||
|
||||
/// Number of connected peers.
|
||||
num_peers: usize,
|
||||
|
||||
/// Delegate validation to `Peerset`.
|
||||
delegate_to_peerset: bool,
|
||||
|
||||
/// Prometheus metrics.
|
||||
metrics: Option<NotificationMetrics>,
|
||||
}
|
||||
|
||||
pub(crate) enum ValidationCallResult {
|
||||
WaitForValidation(oneshot::Receiver<ValidationResult>),
|
||||
Delegated,
|
||||
}
|
||||
|
||||
impl ProtocolHandle {
|
||||
/// Create new [`ProtocolHandle`].
|
||||
fn new(protocol: ProtocolName, subscribers: Subscribers) -> Self {
|
||||
Self { protocol, subscribers, num_peers: 0usize, metrics: None, delegate_to_peerset: false }
|
||||
}
|
||||
|
||||
/// Set metrics.
|
||||
pub fn set_metrics(&mut self, metrics: NotificationMetrics) {
|
||||
self.metrics = Some(metrics);
|
||||
}
|
||||
|
||||
/// Delegate validation to `Peerset`.
|
||||
///
|
||||
/// Protocols that do not do any validation themselves and only rely on `Peerset` handling
|
||||
/// validation can disable protocol-side validation entirely by delegating all validation to
|
||||
/// `Peerset`.
|
||||
pub fn delegate_to_peerset(&mut self, delegate: bool) {
|
||||
self.delegate_to_peerset = delegate;
|
||||
}
|
||||
|
||||
/// Report to the protocol that a substream has been opened and it must be validated by the
|
||||
/// protocol.
|
||||
///
|
||||
/// Return `oneshot::Receiver` which allows `Notifications` to poll for the validation result
|
||||
/// from protocol.
|
||||
pub fn report_incoming_substream(
|
||||
&self,
|
||||
peer: PeerId,
|
||||
handshake: Vec<u8>,
|
||||
) -> Result<ValidationCallResult, ()> {
|
||||
let subscribers = self.subscribers.lock();
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: report incoming substream for {peer}, handshake {handshake:?}",
|
||||
self.protocol
|
||||
);
|
||||
|
||||
if self.delegate_to_peerset {
|
||||
return Ok(ValidationCallResult::Delegated);
|
||||
}
|
||||
|
||||
// if there is only one subscriber, `Notifications` can wait directly on the
|
||||
// `oneshot::channel()`'s RX half without indirection
|
||||
if subscribers.len() == 1 {
|
||||
let (result_tx, rx) = oneshot::channel();
|
||||
return subscribers[0]
|
||||
.unbounded_send(InnerNotificationEvent::ValidateInboundSubstream {
|
||||
peer,
|
||||
handshake,
|
||||
result_tx,
|
||||
})
|
||||
.map(|_| ValidationCallResult::WaitForValidation(rx))
|
||||
.map_err(|_| ());
|
||||
}
|
||||
|
||||
// if there are multiple subscribers, create a task which waits for all of the
|
||||
// validations to finish and returns the combined result to `Notifications`
|
||||
let mut results: FuturesUnordered<_> = subscribers
|
||||
.iter()
|
||||
.filter_map(|subscriber| {
|
||||
let (result_tx, rx) = oneshot::channel();
|
||||
|
||||
subscriber
|
||||
.unbounded_send(InnerNotificationEvent::ValidateInboundSubstream {
|
||||
peer,
|
||||
handshake: handshake.clone(),
|
||||
result_tx,
|
||||
})
|
||||
.is_ok()
|
||||
.then_some(rx)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = results.next().await {
|
||||
match event {
|
||||
Err(_) | Ok(ValidationResult::Reject) =>
|
||||
return tx.send(ValidationResult::Reject),
|
||||
Ok(ValidationResult::Accept) => {},
|
||||
}
|
||||
}
|
||||
|
||||
return tx.send(ValidationResult::Accept);
|
||||
});
|
||||
|
||||
Ok(ValidationCallResult::WaitForValidation(rx))
|
||||
}
|
||||
|
||||
/// Report to the protocol that a substream has been opened and that it can now use the handle
|
||||
/// to send notifications to the remote peer.
|
||||
pub fn report_substream_opened(
|
||||
&mut self,
|
||||
peer: PeerId,
|
||||
direction: Direction,
|
||||
handshake: Vec<u8>,
|
||||
negotiated_fallback: Option<ProtocolName>,
|
||||
sink: NotificationsSink,
|
||||
) -> Result<(), ()> {
|
||||
metrics::register_substream_opened(&self.metrics, &self.protocol);
|
||||
|
||||
let mut subscribers = self.subscribers.lock();
|
||||
log::trace!(target: LOG_TARGET, "{}: substream opened for {peer:?}", self.protocol);
|
||||
|
||||
subscribers.retain(|subscriber| {
|
||||
subscriber
|
||||
.unbounded_send(InnerNotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
direction,
|
||||
handshake: handshake.clone(),
|
||||
negotiated_fallback: negotiated_fallback.clone(),
|
||||
sink: sink.clone(),
|
||||
})
|
||||
.is_ok()
|
||||
});
|
||||
self.num_peers += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Substream was closed.
|
||||
pub fn report_substream_closed(&mut self, peer: PeerId) -> Result<(), ()> {
|
||||
metrics::register_substream_closed(&self.metrics, &self.protocol);
|
||||
|
||||
let mut subscribers = self.subscribers.lock();
|
||||
log::trace!(target: LOG_TARGET, "{}: substream closed for {peer:?}", self.protocol);
|
||||
|
||||
subscribers.retain(|subscriber| {
|
||||
subscriber
|
||||
.unbounded_send(InnerNotificationEvent::NotificationStreamClosed { peer })
|
||||
.is_ok()
|
||||
});
|
||||
self.num_peers -= 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notification was received from the substream.
|
||||
pub fn report_notification_received(
|
||||
&mut self,
|
||||
peer: PeerId,
|
||||
notification: Vec<u8>,
|
||||
) -> Result<(), ()> {
|
||||
metrics::register_notification_received(&self.metrics, &self.protocol, notification.len());
|
||||
|
||||
let mut subscribers = self.subscribers.lock();
|
||||
log::trace!(target: LOG_TARGET, "{}: notification received from {peer:?}", self.protocol);
|
||||
|
||||
subscribers.retain(|subscriber| {
|
||||
subscriber
|
||||
.unbounded_send(InnerNotificationEvent::NotificationReceived {
|
||||
peer,
|
||||
notification: notification.clone(),
|
||||
})
|
||||
.is_ok()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notification sink was replaced.
|
||||
pub fn report_notification_sink_replaced(
|
||||
&mut self,
|
||||
peer: PeerId,
|
||||
sink: NotificationsSink,
|
||||
) -> Result<(), ()> {
|
||||
let mut subscribers = self.subscribers.lock();
|
||||
|
||||
log::trace!(
|
||||
target: LOG_TARGET,
|
||||
"{}: notification sink replaced for {peer:?}",
|
||||
self.protocol
|
||||
);
|
||||
|
||||
subscribers.retain(|subscriber| {
|
||||
subscriber
|
||||
.unbounded_send(InnerNotificationEvent::NotificationSinkReplaced {
|
||||
peer,
|
||||
sink: sink.clone(),
|
||||
})
|
||||
.is_ok()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the number of connected peers.
|
||||
pub fn num_peers(&self) -> usize {
|
||||
self.num_peers
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new (protocol, notification) handle pair.
|
||||
///
|
||||
/// Handle pair allows `Notifications` and the protocol to communicate with each other directly.
|
||||
pub fn notification_service(
|
||||
protocol: ProtocolName,
|
||||
) -> (ProtocolHandlePair, Box<dyn NotificationService>) {
|
||||
let (cmd_tx, cmd_rx) = mpsc::channel(COMMAND_QUEUE_SIZE);
|
||||
|
||||
let (event_tx, event_rx) =
|
||||
tracing_unbounded(metric_label_for_protocol(&protocol).leak(), 100_000);
|
||||
let subscribers = Arc::new(Mutex::new(vec![event_tx]));
|
||||
|
||||
(
|
||||
ProtocolHandlePair::new(protocol.clone(), subscribers.clone(), cmd_rx),
|
||||
Box::new(NotificationHandle::new(protocol.clone(), cmd_tx, event_rx, subscribers)),
|
||||
)
|
||||
}
|
||||
|
||||
// Decorates the mpsc-notification-to-protocol metric with the name of the protocol,
|
||||
// to be able to distiguish between different protocols in dashboards.
|
||||
fn metric_label_for_protocol(protocol: &ProtocolName) -> String {
|
||||
let protocol_name = protocol.to_string();
|
||||
let keys = protocol_name.split("/").collect::<Vec<_>>();
|
||||
keys.iter()
|
||||
.rev()
|
||||
.take(2) // Last two tokens give the protocol name and version
|
||||
.fold("mpsc-notification-to-protocol".into(), |acc, val| format!("{}-{}", acc, val))
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
use crate::protocol::notifications::handler::{
|
||||
NotificationsSinkMessage, ASYNC_NOTIFICATIONS_BUFFER_SIZE,
|
||||
};
|
||||
|
||||
use std::future::Future;
|
||||
|
||||
#[tokio::test]
|
||||
async fn validate_and_accept_substream() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (handle, _stream) = proto.split();
|
||||
|
||||
let peer_id = PeerId::random();
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn substream_opened() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, _, _) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
|
||||
let peer_id = PeerId::random();
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_sync_notification() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, _, mut sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
|
||||
assert_eq!(
|
||||
sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] })
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_async_notification() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, mut async_rx, _) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
|
||||
assert_eq!(
|
||||
async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] })
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_sync_notification_to_non_existent_peer() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (_sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (_handle, _stream) = proto.split();
|
||||
let peer = PeerId::random();
|
||||
|
||||
// as per the original implementation, the call doesn't fail
|
||||
notif.send_sync_notification(&peer.into(), vec![1, 3, 3, 7])
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_async_notification_to_non_existent_peer() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (_sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (_handle, _stream) = proto.split();
|
||||
let peer = PeerId::random();
|
||||
|
||||
if let Err(error::Error::PeerDoesntExist(peer_id)) =
|
||||
notif.send_async_notification(&peer.into(), vec![1, 3, 3, 7]).await
|
||||
{
|
||||
assert_eq!(peer, peer_id.into());
|
||||
} else {
|
||||
panic!("invalid error received from `send_async_notification()`");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn receive_notification() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// notification is received
|
||||
handle.report_notification_received(peer_id, vec![1, 3, 3, 8]).unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationReceived { peer, notification }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(notification, vec![1, 3, 3, 8]);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backpressure_works() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, mut async_rx, _) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// fill the message buffer with messages
|
||||
for i in 0..=ASYNC_NOTIFICATIONS_BUFFER_SIZE {
|
||||
assert!(futures::poll!(
|
||||
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, i as u8])
|
||||
)
|
||||
.is_ready());
|
||||
}
|
||||
|
||||
// try to send one more message and verify that the call blocks
|
||||
assert!(futures::poll!(notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]))
|
||||
.is_pending());
|
||||
|
||||
// release one slot from the buffer for new message
|
||||
assert_eq!(
|
||||
async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 0] })
|
||||
);
|
||||
|
||||
// verify that a message can be sent
|
||||
assert!(
|
||||
futures::poll!(notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9])).is_ready()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_disconnects_then_sync_notification_is_sent() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, _, sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// report that a substream has been closed but don't poll `notif` to receive this
|
||||
// information
|
||||
handle.report_substream_closed(peer_id).unwrap();
|
||||
drop(sync_rx);
|
||||
|
||||
// as per documentation, error is not reported but the notification is silently dropped
|
||||
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 7]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_disconnects_then_async_notification_is_sent() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, async_rx, _) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// report that a substream has been closed but don't poll `notif` to receive this
|
||||
// information
|
||||
handle.report_substream_closed(peer_id).unwrap();
|
||||
drop(async_rx);
|
||||
|
||||
// as per documentation, error is not reported but the notification is silently dropped
|
||||
if let Err(error::Error::ConnectionClosed) =
|
||||
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 7]).await
|
||||
{
|
||||
} else {
|
||||
panic!("invalid state after calling `send_async_notification()` on closed connection")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cloned_service_opening_substream_works() {
|
||||
let (proto, mut notif1) = notification_service("/proto/1".into());
|
||||
let (_sink, _async_rx, _) = NotificationsSink::new(PeerId::random());
|
||||
let (handle, _stream) = proto.split();
|
||||
let mut notif2 = notif1.clone().unwrap();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(mut result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
// verify that `notif1` gets the event
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif1.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// verify that because only one listener has thus far send their result, the result is
|
||||
// pending
|
||||
assert!(result_rx.try_recv().is_err());
|
||||
|
||||
// verify that `notif2` also gets the event
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif2.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cloned_service_one_service_rejects_substream() {
|
||||
let (proto, mut notif1) = notification_service("/proto/1".into());
|
||||
let (_sink, _async_rx, _) = NotificationsSink::new(PeerId::random());
|
||||
let (handle, _stream) = proto.split();
|
||||
let mut notif2 = notif1.clone().unwrap();
|
||||
let mut notif3 = notif2.clone().unwrap();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(mut result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
for notif in vec![&mut notif1, &mut notif2] {
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
// `notif3` has not yet sent their validation result
|
||||
assert!(result_rx.try_recv().is_err());
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif3.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Reject).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Reject);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cloned_service_opening_substream_sending_and_receiving_notifications_work() {
|
||||
let (proto, mut notif1) = notification_service("/proto/1".into());
|
||||
let (sink, _, mut sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let mut notif2 = notif1.clone().unwrap();
|
||||
let mut notif3 = notif1.clone().unwrap();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
|
||||
// accept the inbound substream for all services
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that then notification stream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
// receive a notification from peer and verify all services receive it
|
||||
handle.report_notification_received(peer_id, vec![1, 3, 3, 8]).unwrap();
|
||||
|
||||
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
|
||||
if let Some(NotificationEvent::NotificationReceived { peer, notification }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(notification, vec![1, 3, 3, 8]);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
|
||||
for (i, notif) in vec![&mut notif1, &mut notif2, &mut notif3].iter_mut().enumerate() {
|
||||
// send notification from each service and verify peer receives it
|
||||
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, i as u8]);
|
||||
assert_eq!(
|
||||
sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, i as u8] })
|
||||
);
|
||||
}
|
||||
|
||||
// close the substream for peer and verify all services receive the event
|
||||
handle.report_substream_closed(peer_id).unwrap();
|
||||
|
||||
for notif in vec![&mut notif1, &mut notif2, &mut notif3] {
|
||||
if let Some(NotificationEvent::NotificationStreamClosed { peer }) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sending_notifications_using_notifications_sink_works() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, mut async_rx, mut sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// get a copy of the notification sink and send a synchronous notification using.
|
||||
let sink = notif.message_sink(&peer_id.into()).unwrap();
|
||||
sink.send_sync_notification(vec![1, 3, 3, 6]);
|
||||
|
||||
// send an asynchronous notification using the acquired notifications sink.
|
||||
let _ = sink.send_async_notification(vec![1, 3, 3, 7]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 6] }),
|
||||
);
|
||||
assert_eq!(
|
||||
async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 7] }),
|
||||
);
|
||||
|
||||
// send notifications using the stored notification sink as well.
|
||||
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
|
||||
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] }),
|
||||
);
|
||||
assert_eq!(
|
||||
async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] }),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_to_get_notifications_sink_for_non_existent_peer() {
|
||||
let (_proto, notif) = notification_service("/proto/1".into());
|
||||
assert!(notif.message_sink(&pezsc_network_types::PeerId::random()).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notification_sink_replaced() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (sink, mut async_rx, mut sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
let (mut handle, _stream) = proto.split();
|
||||
let peer_id = PeerId::random();
|
||||
|
||||
// validate inbound substream
|
||||
let ValidationCallResult::WaitForValidation(result_rx) =
|
||||
handle.report_incoming_substream(peer_id, vec![1, 3, 3, 7]).unwrap()
|
||||
else {
|
||||
panic!("peerset not enabled");
|
||||
};
|
||||
|
||||
if let Some(NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx }) =
|
||||
notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
let _ = result_tx.send(ValidationResult::Accept).unwrap();
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
|
||||
|
||||
// report that a substream has been opened
|
||||
handle
|
||||
.report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
|
||||
.unwrap();
|
||||
|
||||
if let Some(NotificationEvent::NotificationStreamOpened {
|
||||
peer,
|
||||
negotiated_fallback,
|
||||
handshake,
|
||||
direction,
|
||||
}) = notif.next_event().await
|
||||
{
|
||||
assert_eq!(peer_id, peer.into());
|
||||
assert_eq!(negotiated_fallback, None);
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
assert_eq!(direction, Direction::Inbound);
|
||||
} else {
|
||||
panic!("invalid event received");
|
||||
}
|
||||
|
||||
// get a copy of the notification sink and send a synchronous notification using.
|
||||
let sink = notif.message_sink(&peer_id.into()).unwrap();
|
||||
sink.send_sync_notification(vec![1, 3, 3, 6]);
|
||||
|
||||
// send an asynchronous notification using the acquired notifications sink.
|
||||
let _ = sink.send_async_notification(vec![1, 3, 3, 7]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 6] }),
|
||||
);
|
||||
assert_eq!(
|
||||
async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 7] }),
|
||||
);
|
||||
|
||||
// send notifications using the stored notification sink as well.
|
||||
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
|
||||
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] }),
|
||||
);
|
||||
assert_eq!(
|
||||
async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] }),
|
||||
);
|
||||
|
||||
// the initial connection was closed and `Notifications` switched to secondary connection
|
||||
// and emitted `CustomProtocolReplaced` which informs the local `NotificationService` that
|
||||
// the notification sink was replaced.
|
||||
let (new_sink, mut new_async_rx, mut new_sync_rx) = NotificationsSink::new(PeerId::random());
|
||||
handle.report_notification_sink_replaced(peer_id, new_sink).unwrap();
|
||||
|
||||
// drop the old sinks and poll `notif` once to register the sink replacement
|
||||
drop(sync_rx);
|
||||
drop(async_rx);
|
||||
|
||||
futures::future::poll_fn(|cx| {
|
||||
let _ = std::pin::Pin::new(&mut notif.next_event()).poll(cx);
|
||||
std::task::Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
|
||||
// verify that using the `NotificationService` API automatically results in using the correct
|
||||
// sink
|
||||
notif.send_sync_notification(&peer_id.into(), vec![1, 3, 3, 8]);
|
||||
notif.send_async_notification(&peer_id.into(), vec![1, 3, 3, 9]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
new_sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 8] }),
|
||||
);
|
||||
assert_eq!(
|
||||
new_async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 9] }),
|
||||
);
|
||||
|
||||
// now send two notifications using the acquired message sink and verify that
|
||||
// it's also updated
|
||||
sink.send_sync_notification(vec![1, 3, 3, 6]);
|
||||
|
||||
// send an asynchronous notification using the acquired notifications sink.
|
||||
let _ = sink.send_async_notification(vec![1, 3, 3, 7]).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
new_sync_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 6] }),
|
||||
);
|
||||
assert_eq!(
|
||||
new_async_rx.next().await,
|
||||
Some(NotificationsSinkMessage::Notification { message: vec![1, 3, 3, 7] }),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_handshake() {
|
||||
let (proto, mut notif) = notification_service("/proto/1".into());
|
||||
let (_handle, mut stream) = proto.split();
|
||||
|
||||
assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_ok());
|
||||
|
||||
match stream.next().await {
|
||||
Some(NotificationCommand::SetHandshake(handshake)) => {
|
||||
assert_eq!(handshake, vec![1, 3, 3, 7]);
|
||||
},
|
||||
_ => panic!("invalid event received"),
|
||||
}
|
||||
|
||||
for _ in 0..COMMAND_QUEUE_SIZE {
|
||||
assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_ok());
|
||||
}
|
||||
|
||||
assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_err());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,392 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::{
|
||||
peer_store::PeerStore,
|
||||
protocol::notifications::{Notifications, NotificationsOut, ProtocolConfig},
|
||||
protocol_controller::{ProtoSetConfig, ProtocolController, SetId},
|
||||
service::{
|
||||
metrics::NotificationMetrics,
|
||||
traits::{NotificationEvent, ValidationResult},
|
||||
},
|
||||
};
|
||||
|
||||
use futures::{future::BoxFuture, prelude::*};
|
||||
use libp2p::{
|
||||
core::{
|
||||
transport::{MemoryTransport, PortUse},
|
||||
upgrade, Endpoint,
|
||||
},
|
||||
identity, noise,
|
||||
swarm::{
|
||||
behaviour::FromSwarm, ConnectionDenied, ConnectionId, NetworkBehaviour, Swarm, SwarmEvent,
|
||||
THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
|
||||
},
|
||||
yamux, Multiaddr, PeerId, SwarmBuilder, Transport,
|
||||
};
|
||||
use pezsc_utils::mpsc::tracing_unbounded;
|
||||
use std::{
|
||||
iter,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod conformance;
|
||||
|
||||
/// Builds two nodes that have each other as bootstrap nodes.
|
||||
/// This is to be used only for testing, and a panic will happen if something goes wrong.
|
||||
fn build_nodes() -> (Swarm<CustomProtoWithAddr>, Swarm<CustomProtoWithAddr>) {
|
||||
let mut out = Vec::with_capacity(2);
|
||||
|
||||
let keypairs: Vec<_> = (0..2).map(|_| identity::Keypair::generate_ed25519()).collect();
|
||||
let addrs: Vec<Multiaddr> = (0..2)
|
||||
.map(|_| format!("/memory/{}", rand::random::<u64>()).parse().unwrap())
|
||||
.collect();
|
||||
|
||||
for index in 0..2 {
|
||||
let keypair = keypairs[index].clone();
|
||||
|
||||
let (protocol_handle_pair, mut notif_service) =
|
||||
crate::protocol::notifications::service::notification_service("/foo".into());
|
||||
// The first swarm has the second peer ID present in the peerstore.
|
||||
let peer_store = PeerStore::new(
|
||||
if index == 0 {
|
||||
keypairs.iter().skip(1).map(|keypair| keypair.public().to_peer_id()).collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
let (to_notifications, from_controller) =
|
||||
tracing_unbounded("test_protocol_controller_to_notifications", 10_000);
|
||||
|
||||
let (controller_handle, controller) = ProtocolController::new(
|
||||
SetId::from(0),
|
||||
ProtoSetConfig {
|
||||
in_peers: 25,
|
||||
out_peers: 25,
|
||||
reserved_nodes: Default::default(),
|
||||
reserved_only: false,
|
||||
},
|
||||
to_notifications,
|
||||
Arc::new(peer_store.handle()),
|
||||
);
|
||||
|
||||
let (notif_handle, command_stream) = protocol_handle_pair.split();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if let NotificationEvent::ValidateInboundSubstream { result_tx, .. } =
|
||||
notif_service.next_event().await.unwrap()
|
||||
{
|
||||
result_tx.send(ValidationResult::Accept).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut swarm = SwarmBuilder::with_existing_identity(keypair)
|
||||
.with_tokio()
|
||||
.with_other_transport(|keypair| {
|
||||
MemoryTransport::new()
|
||||
.upgrade(upgrade::Version::V1)
|
||||
.authenticate(noise::Config::new(&keypair).unwrap())
|
||||
.multiplex(yamux::Config::default())
|
||||
.timeout(Duration::from_secs(20))
|
||||
.boxed()
|
||||
})
|
||||
.unwrap()
|
||||
.with_behaviour(|_keypair| CustomProtoWithAddr {
|
||||
inner: Notifications::new(
|
||||
vec![controller_handle],
|
||||
from_controller,
|
||||
NotificationMetrics::new(None),
|
||||
iter::once((
|
||||
ProtocolConfig {
|
||||
name: "/foo".into(),
|
||||
fallback_names: Vec::new(),
|
||||
handshake: Vec::new(),
|
||||
max_notification_size: 1024 * 1024,
|
||||
},
|
||||
notif_handle,
|
||||
command_stream,
|
||||
)),
|
||||
),
|
||||
peer_store_future: peer_store.run().boxed(),
|
||||
protocol_controller_future: controller.run().boxed(),
|
||||
addrs: addrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(n, a)| {
|
||||
if n != index {
|
||||
Some((keypairs[n].public().to_peer_id(), a.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.unwrap()
|
||||
.build();
|
||||
swarm.listen_on(addrs[index].clone()).unwrap();
|
||||
out.push(swarm);
|
||||
}
|
||||
|
||||
// Final output
|
||||
let mut out_iter = out.into_iter();
|
||||
let first = out_iter.next().unwrap();
|
||||
let second = out_iter.next().unwrap();
|
||||
(first, second)
|
||||
}
|
||||
|
||||
/// Wraps around the `CustomBehaviour` network behaviour, and adds hardcoded node addresses to it.
|
||||
struct CustomProtoWithAddr {
|
||||
inner: Notifications,
|
||||
peer_store_future: BoxFuture<'static, ()>,
|
||||
protocol_controller_future: BoxFuture<'static, ()>,
|
||||
addrs: Vec<(PeerId, Multiaddr)>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for CustomProtoWithAddr {
|
||||
type Target = Notifications;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for CustomProtoWithAddr {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBehaviour for CustomProtoWithAddr {
|
||||
type ConnectionHandler = <Notifications as NetworkBehaviour>::ConnectionHandler;
|
||||
type ToSwarm = <Notifications as NetworkBehaviour>::ToSwarm;
|
||||
|
||||
fn handle_pending_inbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
local_addr: &Multiaddr,
|
||||
remote_addr: &Multiaddr,
|
||||
) -> Result<(), ConnectionDenied> {
|
||||
self.inner
|
||||
.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
|
||||
}
|
||||
|
||||
fn handle_pending_outbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
maybe_peer: Option<PeerId>,
|
||||
addresses: &[Multiaddr],
|
||||
effective_role: Endpoint,
|
||||
) -> Result<Vec<Multiaddr>, ConnectionDenied> {
|
||||
let mut list = self.inner.handle_pending_outbound_connection(
|
||||
connection_id,
|
||||
maybe_peer,
|
||||
addresses,
|
||||
effective_role,
|
||||
)?;
|
||||
if let Some(peer_id) = maybe_peer {
|
||||
for (p, a) in self.addrs.iter() {
|
||||
if *p == peer_id {
|
||||
list.push(a.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
fn handle_established_inbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
local_addr: &Multiaddr,
|
||||
remote_addr: &Multiaddr,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
self.inner.handle_established_inbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
local_addr,
|
||||
remote_addr,
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_established_outbound_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
peer: PeerId,
|
||||
addr: &Multiaddr,
|
||||
role_override: Endpoint,
|
||||
port_use: PortUse,
|
||||
) -> Result<THandler<Self>, ConnectionDenied> {
|
||||
self.inner.handle_established_outbound_connection(
|
||||
connection_id,
|
||||
peer,
|
||||
addr,
|
||||
role_override,
|
||||
port_use,
|
||||
)
|
||||
}
|
||||
|
||||
fn on_swarm_event(&mut self, event: FromSwarm) {
|
||||
self.inner.on_swarm_event(event);
|
||||
}
|
||||
|
||||
fn on_connection_handler_event(
|
||||
&mut self,
|
||||
peer_id: PeerId,
|
||||
connection_id: ConnectionId,
|
||||
event: THandlerOutEvent<Self>,
|
||||
) {
|
||||
self.inner.on_connection_handler_event(peer_id, connection_id, event);
|
||||
}
|
||||
|
||||
fn poll(&mut self, cx: &mut Context) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
|
||||
let _ = self.peer_store_future.poll_unpin(cx);
|
||||
let _ = self.protocol_controller_future.poll_unpin(cx);
|
||||
self.inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnect_after_disconnect() {
|
||||
// We connect two nodes together, then force a disconnect (through the API of the `Service`),
|
||||
// check that the disconnect worked, and finally check whether they successfully reconnect.
|
||||
|
||||
let (mut service1, mut service2) = build_nodes();
|
||||
|
||||
// For this test, the services can be in the following states.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum ServiceState {
|
||||
NotConnected,
|
||||
FirstConnec,
|
||||
Disconnected,
|
||||
ConnectedAgain,
|
||||
}
|
||||
let mut service1_state = ServiceState::NotConnected;
|
||||
let mut service2_state = ServiceState::NotConnected;
|
||||
|
||||
loop {
|
||||
// Grab next event from services.
|
||||
let event = {
|
||||
let s1 = service1.select_next_some();
|
||||
let s2 = service2.select_next_some();
|
||||
futures::pin_mut!(s1, s2);
|
||||
match future::select(s1, s2).await {
|
||||
future::Either::Left((ev, _)) => future::Either::Left(ev),
|
||||
future::Either::Right((ev, _)) => future::Either::Right(ev),
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
future::Either::Left(SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen {
|
||||
..
|
||||
})) => match service1_state {
|
||||
ServiceState::NotConnected => {
|
||||
service1_state = ServiceState::FirstConnec;
|
||||
if service2_state == ServiceState::FirstConnec {
|
||||
service1
|
||||
.behaviour_mut()
|
||||
.disconnect_peer(Swarm::local_peer_id(&service2), SetId::from(0));
|
||||
}
|
||||
},
|
||||
ServiceState::Disconnected => service1_state = ServiceState::ConnectedAgain,
|
||||
ServiceState::FirstConnec | ServiceState::ConnectedAgain => panic!(),
|
||||
},
|
||||
future::Either::Left(SwarmEvent::Behaviour(
|
||||
NotificationsOut::CustomProtocolClosed { .. },
|
||||
)) => match service1_state {
|
||||
ServiceState::FirstConnec => service1_state = ServiceState::Disconnected,
|
||||
ServiceState::ConnectedAgain |
|
||||
ServiceState::NotConnected |
|
||||
ServiceState::Disconnected => panic!(),
|
||||
},
|
||||
future::Either::Right(SwarmEvent::Behaviour(
|
||||
NotificationsOut::CustomProtocolOpen { .. },
|
||||
)) => match service2_state {
|
||||
ServiceState::NotConnected => {
|
||||
service2_state = ServiceState::FirstConnec;
|
||||
if service1_state == ServiceState::FirstConnec {
|
||||
service1
|
||||
.behaviour_mut()
|
||||
.disconnect_peer(Swarm::local_peer_id(&service2), SetId::from(0));
|
||||
}
|
||||
},
|
||||
ServiceState::Disconnected => service2_state = ServiceState::ConnectedAgain,
|
||||
ServiceState::FirstConnec | ServiceState::ConnectedAgain => panic!(),
|
||||
},
|
||||
future::Either::Right(SwarmEvent::Behaviour(
|
||||
NotificationsOut::CustomProtocolClosed { .. },
|
||||
)) => match service2_state {
|
||||
ServiceState::FirstConnec => service2_state = ServiceState::Disconnected,
|
||||
ServiceState::ConnectedAgain |
|
||||
ServiceState::NotConnected |
|
||||
ServiceState::Disconnected => panic!(),
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// Due to the bug in `Notifications`, the disconnected node does not always detect that
|
||||
// it was disconnected. The closed inbound substream is tolerated by design, and the
|
||||
// closed outbound substream is not detected until something is sent into it.
|
||||
// See [PR #13396](https://github.com/pezkuwichain/kurdistan-sdk/issues/45).
|
||||
// This happens if the disconnecting node reconnects to it fast enough.
|
||||
// In this case the disconnected node does not transit via `ServiceState::NotConnected`
|
||||
// and stays in `ServiceState::FirstConnec`.
|
||||
// TODO: update this once the fix is finally merged.
|
||||
if service1_state == ServiceState::ConnectedAgain &&
|
||||
service2_state == ServiceState::ConnectedAgain ||
|
||||
service1_state == ServiceState::ConnectedAgain &&
|
||||
service2_state == ServiceState::FirstConnec ||
|
||||
service1_state == ServiceState::FirstConnec &&
|
||||
service2_state == ServiceState::ConnectedAgain
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now that the two services have disconnected and reconnected, wait for 3 seconds and
|
||||
// check whether they're still connected.
|
||||
let mut delay = futures_timer::Delay::new(Duration::from_secs(3));
|
||||
|
||||
loop {
|
||||
// Grab next event from services.
|
||||
let event = {
|
||||
let s1 = service1.select_next_some();
|
||||
let s2 = service2.select_next_some();
|
||||
futures::pin_mut!(s1, s2);
|
||||
match future::select(future::select(s1, s2), &mut delay).await {
|
||||
future::Either::Right(_) => break, // success
|
||||
future::Either::Left((future::Either::Left((ev, _)), _)) => ev,
|
||||
future::Either::Left((future::Either::Right((ev, _)), _)) => ev,
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { .. }) |
|
||||
SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => panic!(),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::notifications::{
|
||||
NotificationsInOpen, NotificationsInSubstreamHandshake, NotificationsOutOpen,
|
||||
};
|
||||
|
||||
pub(crate) use notifications::NotificationsOutError;
|
||||
|
||||
pub use self::{
|
||||
collec::UpgradeCollec,
|
||||
notifications::{
|
||||
NotificationsIn, NotificationsInSubstream, NotificationsOut, NotificationsOutSubstream,
|
||||
},
|
||||
};
|
||||
|
||||
mod collec;
|
||||
mod notifications;
|
||||
@@ -0,0 +1,178 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::prelude::*;
|
||||
use libp2p::core::upgrade::{InboundUpgrade, UpgradeInfo};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
vec,
|
||||
};
|
||||
|
||||
// TODO: move this to libp2p => https://github.com/libp2p/rust-libp2p/issues/1445
|
||||
|
||||
/// Upgrade that combines multiple upgrades of the same type into one. Supports all the protocols
|
||||
/// supported by either sub-upgrade.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpgradeCollec<T>(pub Vec<T>);
|
||||
|
||||
impl<T> From<Vec<T>> for UpgradeCollec<T> {
|
||||
fn from(list: Vec<T>) -> Self {
|
||||
Self(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<T> for UpgradeCollec<T> {
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UpgradeInfo> UpgradeInfo for UpgradeCollec<T> {
|
||||
type Info = ProtoNameWithUsize<T::Info>;
|
||||
type InfoIter = vec::IntoIter<Self::Info>;
|
||||
|
||||
fn protocol_info(&self) -> Self::InfoIter {
|
||||
self.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(n, p)| p.protocol_info().into_iter().map(move |i| ProtoNameWithUsize(i, n)))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C> InboundUpgrade<C> for UpgradeCollec<T>
|
||||
where
|
||||
T: InboundUpgrade<C>,
|
||||
{
|
||||
type Output = (T::Output, usize);
|
||||
type Error = (T::Error, usize);
|
||||
type Future = FutWithUsize<T::Future>;
|
||||
|
||||
fn upgrade_inbound(mut self, sock: C, info: Self::Info) -> Self::Future {
|
||||
let fut = self.0.remove(info.1).upgrade_inbound(sock, info.0);
|
||||
FutWithUsize(fut, info.1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups a `ProtocolName` with a `usize`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProtoNameWithUsize<T>(T, usize);
|
||||
|
||||
impl<T: AsRef<str>> AsRef<str> for ProtoNameWithUsize<T> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent to `fut.map_ok(|v| (v, num)).map_err(|e| (e, num))`, where `fut` and `num` are
|
||||
/// the two fields of this struct.
|
||||
#[pin_project::pin_project]
|
||||
pub struct FutWithUsize<T>(#[pin] T, usize);
|
||||
|
||||
impl<T: Future<Output = Result<O, E>>, O, E> Future for FutWithUsize<T> {
|
||||
type Output = Result<(O, usize), (E, usize)>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match Future::poll(this.0, cx) {
|
||||
Poll::Ready(Ok(v)) => Poll::Ready(Ok((v, *this.1))),
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err((e, *this.1))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::ProtocolName as ProtoName;
|
||||
use libp2p::core::upgrade::UpgradeInfo;
|
||||
|
||||
// TODO: move to mocks
|
||||
mockall::mock! {
|
||||
pub ProtocolUpgrade<T> {}
|
||||
|
||||
impl<T: Clone + AsRef<str>> UpgradeInfo for ProtocolUpgrade<T> {
|
||||
type Info = T;
|
||||
type InfoIter = vec::IntoIter<T>;
|
||||
fn protocol_info(&self) -> vec::IntoIter<T>;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_info() {
|
||||
let upgrades = (1..=3)
|
||||
.map(|i| {
|
||||
let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
|
||||
upgrade.expect_protocol_info().return_once(move || {
|
||||
vec![ProtoNameWithUsize(ProtoName::from(format!("protocol{i}")), i)].into_iter()
|
||||
});
|
||||
upgrade
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let upgrade: UpgradeCollec<_> = upgrades.into_iter().collect::<UpgradeCollec<_>>();
|
||||
let protos = vec![
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol1".to_string()), 1), 0),
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol2".to_string()), 2), 1),
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol3".to_string()), 3), 2),
|
||||
];
|
||||
let upgrades = upgrade.protocol_info().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(upgrades, protos,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_protocol_info() {
|
||||
let mut upgrades = (1..=2)
|
||||
.map(|i| {
|
||||
let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
|
||||
upgrade.expect_protocol_info().return_once(move || {
|
||||
vec![ProtoNameWithUsize(ProtoName::from(format!("protocol{i}")), i)].into_iter()
|
||||
});
|
||||
upgrade
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
upgrades.push({
|
||||
let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
|
||||
upgrade.expect_protocol_info().return_once(move || {
|
||||
vec![
|
||||
ProtoNameWithUsize(ProtoName::from("protocol22".to_string()), 1),
|
||||
ProtoNameWithUsize(ProtoName::from("protocol33".to_string()), 2),
|
||||
ProtoNameWithUsize(ProtoName::from("protocol44".to_string()), 3),
|
||||
]
|
||||
.into_iter()
|
||||
});
|
||||
upgrade
|
||||
});
|
||||
|
||||
let upgrade: UpgradeCollec<_> = upgrades.into_iter().collect::<UpgradeCollec<_>>();
|
||||
let protos = vec![
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol1".to_string()), 1), 0),
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol2".to_string()), 2), 1),
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol22".to_string()), 1), 2),
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol33".to_string()), 2), 2),
|
||||
ProtoNameWithUsize(ProtoNameWithUsize(ProtoName::from("protocol44".to_string()), 3), 2),
|
||||
];
|
||||
let upgrades = upgrade.protocol_info().collect::<Vec<_>>();
|
||||
assert_eq!(upgrades, protos,);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,865 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/// Notifications protocol.
|
||||
///
|
||||
/// The Bizinikiwi notifications protocol consists in the following:
|
||||
///
|
||||
/// - Node A opens a substream to node B and sends a message which contains some
|
||||
/// protocol-specific higher-level logic. This message is prefixed with a variable-length
|
||||
/// integer message length. This message can be empty, in which case `0` is sent.
|
||||
/// - If node B accepts the substream, it sends back a message with the same properties.
|
||||
/// - If instead B refuses the connection (which typically happens because no empty slot is
|
||||
/// available), then it immediately closes the substream without sending back anything.
|
||||
/// - Node A can then send notifications to B, prefixed with a variable-length integer
|
||||
/// indicating the length of the message.
|
||||
/// - Either node A or node B can signal that it doesn't want this notifications substream
|
||||
/// anymore by closing its writing side. The other party should respond by also closing their
|
||||
/// own writing side soon after.
|
||||
///
|
||||
/// Notification substreams are unidirectional. If A opens a substream with B, then B is
|
||||
/// encouraged but not required to open a substream to A as well.
|
||||
use crate::types::ProtocolName;
|
||||
|
||||
use asynchronous_codec::Framed;
|
||||
use bytes::BytesMut;
|
||||
use futures::prelude::*;
|
||||
use libp2p::{
|
||||
core::{InboundUpgrade, OutboundUpgrade, UpgradeInfo},
|
||||
PeerId,
|
||||
};
|
||||
use log::{debug, error, warn};
|
||||
use unsigned_varint::codec::UviBytes;
|
||||
|
||||
use std::{
|
||||
fmt, io, mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
vec,
|
||||
};
|
||||
|
||||
/// Logging target for the file.
|
||||
const LOG_TARGET: &str = "sub-libp2p::notification::upgrade";
|
||||
|
||||
/// Maximum allowed size of the two handshake messages, in bytes.
|
||||
const MAX_HANDSHAKE_SIZE: usize = 1024;
|
||||
|
||||
/// Upgrade that accepts a substream, sends back a status message, then becomes a unidirectional
|
||||
/// stream of messages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationsIn {
|
||||
/// Protocol name to use when negotiating the substream.
|
||||
/// The first one is the main name, while the other ones are fall backs.
|
||||
protocol_names: Vec<ProtocolName>,
|
||||
/// Maximum allowed size for a single notification.
|
||||
max_notification_size: u64,
|
||||
}
|
||||
|
||||
/// Upgrade that opens a substream, waits for the remote to accept by sending back a status
|
||||
/// message, then becomes a unidirectional sink of data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationsOut {
|
||||
/// Protocol name to use when negotiating the substream.
|
||||
/// The first one is the main name, while the other ones are fall backs.
|
||||
protocol_names: Vec<ProtocolName>,
|
||||
/// Message to send when we start the handshake.
|
||||
initial_message: Vec<u8>,
|
||||
/// Maximum allowed size for a single notification.
|
||||
max_notification_size: u64,
|
||||
/// The peerID of the remote.
|
||||
peer_id: PeerId,
|
||||
}
|
||||
|
||||
/// A substream for incoming notification messages.
|
||||
///
|
||||
/// When creating, this struct starts in a state in which we must first send back a handshake
|
||||
/// message to the remote. No message will come before this has been done.
|
||||
#[pin_project::pin_project]
|
||||
pub struct NotificationsInSubstream<TSubstream> {
|
||||
#[pin]
|
||||
socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
|
||||
handshake: NotificationsInSubstreamHandshake,
|
||||
}
|
||||
|
||||
/// State of the handshake sending back process.
|
||||
#[derive(Debug)]
|
||||
pub enum NotificationsInSubstreamHandshake {
|
||||
/// Waiting for the user to give us the handshake message.
|
||||
NotSent,
|
||||
/// User gave us the handshake message. Trying to push it in the socket.
|
||||
PendingSend(Vec<u8>),
|
||||
/// Handshake message was pushed in the socket. Still need to flush.
|
||||
Flush,
|
||||
/// Handshake message successfully sent and flushed.
|
||||
Sent,
|
||||
/// Remote has closed their writing side. We close our own writing side in return.
|
||||
ClosingInResponseToRemote,
|
||||
/// Both our side and the remote have closed their writing side.
|
||||
BothSidesClosed,
|
||||
}
|
||||
|
||||
/// A substream for outgoing notification messages.
|
||||
#[pin_project::pin_project]
|
||||
pub struct NotificationsOutSubstream<TSubstream> {
|
||||
/// Substream where to send messages.
|
||||
#[pin]
|
||||
socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
|
||||
|
||||
/// The remote peer.
|
||||
peer_id: PeerId,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<TSubstream> NotificationsOutSubstream<TSubstream> {
|
||||
pub fn new(socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>) -> Self {
|
||||
Self { socket, peer_id: PeerId::random() }
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationsIn {
|
||||
/// Builds a new potential upgrade.
|
||||
pub fn new(
|
||||
main_protocol_name: impl Into<ProtocolName>,
|
||||
fallback_names: Vec<ProtocolName>,
|
||||
max_notification_size: u64,
|
||||
) -> Self {
|
||||
let mut protocol_names = fallback_names;
|
||||
protocol_names.insert(0, main_protocol_name.into());
|
||||
|
||||
Self { protocol_names, max_notification_size }
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeInfo for NotificationsIn {
|
||||
type Info = ProtocolName;
|
||||
type InfoIter = vec::IntoIter<Self::Info>;
|
||||
|
||||
fn protocol_info(&self) -> Self::InfoIter {
|
||||
self.protocol_names.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSubstream> InboundUpgrade<TSubstream> for NotificationsIn
|
||||
where
|
||||
TSubstream: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
type Output = NotificationsInOpen<TSubstream>;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
type Error = NotificationsHandshakeError;
|
||||
|
||||
fn upgrade_inbound(self, mut socket: TSubstream, _negotiated_name: Self::Info) -> Self::Future {
|
||||
Box::pin(async move {
|
||||
let handshake_len = unsigned_varint::aio::read_usize(&mut socket).await?;
|
||||
if handshake_len > MAX_HANDSHAKE_SIZE {
|
||||
return Err(NotificationsHandshakeError::TooLarge {
|
||||
requested: handshake_len,
|
||||
max: MAX_HANDSHAKE_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
let mut handshake = vec![0u8; handshake_len];
|
||||
if !handshake.is_empty() {
|
||||
socket.read_exact(&mut handshake).await?;
|
||||
}
|
||||
|
||||
let mut codec = UviBytes::default();
|
||||
codec.set_max_len(usize::try_from(self.max_notification_size).unwrap_or(usize::MAX));
|
||||
|
||||
let substream = NotificationsInSubstream {
|
||||
socket: Framed::new(socket, codec),
|
||||
handshake: NotificationsInSubstreamHandshake::NotSent,
|
||||
};
|
||||
|
||||
Ok(NotificationsInOpen { handshake, substream })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Yielded by the [`NotificationsIn`] after a successfully upgrade.
|
||||
pub struct NotificationsInOpen<TSubstream> {
|
||||
/// Handshake sent by the remote.
|
||||
pub handshake: Vec<u8>,
|
||||
/// Implementation of `Stream` that allows receives messages from the substream.
|
||||
pub substream: NotificationsInSubstream<TSubstream>,
|
||||
}
|
||||
|
||||
impl<TSubstream> fmt::Debug for NotificationsInOpen<TSubstream> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NotificationsInOpen")
|
||||
.field("handshake", &self.handshake)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSubstream> NotificationsInSubstream<TSubstream>
|
||||
where
|
||||
TSubstream: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
#[cfg(test)]
|
||||
pub fn new(
|
||||
socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
|
||||
handshake: NotificationsInSubstreamHandshake,
|
||||
) -> Self {
|
||||
Self { socket, handshake }
|
||||
}
|
||||
|
||||
/// Sends the handshake in order to inform the remote that we accept the substream.
|
||||
pub fn send_handshake(&mut self, message: impl Into<Vec<u8>>) {
|
||||
if !matches!(self.handshake, NotificationsInSubstreamHandshake::NotSent) {
|
||||
error!(target: LOG_TARGET, "Tried to send handshake twice");
|
||||
return;
|
||||
}
|
||||
|
||||
self.handshake = NotificationsInSubstreamHandshake::PendingSend(message.into());
|
||||
}
|
||||
|
||||
/// Equivalent to `Stream::poll_next`, except that it only drives the handshake and is
|
||||
/// guaranteed to not generate any notification.
|
||||
pub fn poll_process(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), io::Error>> {
|
||||
let mut this = self.project();
|
||||
|
||||
loop {
|
||||
match mem::replace(this.handshake, NotificationsInSubstreamHandshake::Sent) {
|
||||
NotificationsInSubstreamHandshake::PendingSend(msg) => {
|
||||
match Sink::poll_ready(this.socket.as_mut(), cx) {
|
||||
Poll::Ready(_) => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Flush;
|
||||
match Sink::start_send(this.socket.as_mut(), io::Cursor::new(msg)) {
|
||||
Ok(()) => {},
|
||||
Err(err) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
},
|
||||
Poll::Pending => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::PendingSend(msg);
|
||||
return Poll::Pending;
|
||||
},
|
||||
}
|
||||
},
|
||||
NotificationsInSubstreamHandshake::Flush => {
|
||||
match Sink::poll_flush(this.socket.as_mut(), cx)? {
|
||||
Poll::Ready(()) => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Sent;
|
||||
return Poll::Ready(Ok(()));
|
||||
},
|
||||
Poll::Pending => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Flush;
|
||||
return Poll::Pending;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
st @ NotificationsInSubstreamHandshake::NotSent |
|
||||
st @ NotificationsInSubstreamHandshake::Sent |
|
||||
st @ NotificationsInSubstreamHandshake::ClosingInResponseToRemote |
|
||||
st @ NotificationsInSubstreamHandshake::BothSidesClosed => {
|
||||
*this.handshake = st;
|
||||
return Poll::Ready(Ok(()));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSubstream> Stream for NotificationsInSubstream<TSubstream>
|
||||
where
|
||||
TSubstream: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Item = Result<BytesMut, io::Error>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.project();
|
||||
|
||||
// This `Stream` implementation first tries to send back the handshake if necessary.
|
||||
loop {
|
||||
match mem::replace(this.handshake, NotificationsInSubstreamHandshake::Sent) {
|
||||
NotificationsInSubstreamHandshake::NotSent => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::NotSent;
|
||||
return Poll::Pending;
|
||||
},
|
||||
NotificationsInSubstreamHandshake::PendingSend(msg) => {
|
||||
match Sink::poll_ready(this.socket.as_mut(), cx) {
|
||||
Poll::Ready(_) => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Flush;
|
||||
match Sink::start_send(this.socket.as_mut(), io::Cursor::new(msg)) {
|
||||
Ok(()) => {},
|
||||
Err(err) => return Poll::Ready(Some(Err(err))),
|
||||
}
|
||||
},
|
||||
Poll::Pending => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::PendingSend(msg);
|
||||
return Poll::Pending;
|
||||
},
|
||||
}
|
||||
},
|
||||
NotificationsInSubstreamHandshake::Flush => {
|
||||
match Sink::poll_flush(this.socket.as_mut(), cx)? {
|
||||
Poll::Ready(()) =>
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Sent,
|
||||
Poll::Pending => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Flush;
|
||||
return Poll::Pending;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
NotificationsInSubstreamHandshake::Sent => {
|
||||
match Stream::poll_next(this.socket.as_mut(), cx) {
|
||||
Poll::Ready(None) =>
|
||||
*this.handshake =
|
||||
NotificationsInSubstreamHandshake::ClosingInResponseToRemote,
|
||||
Poll::Ready(Some(msg)) => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Sent;
|
||||
return Poll::Ready(Some(msg));
|
||||
},
|
||||
Poll::Pending => {
|
||||
*this.handshake = NotificationsInSubstreamHandshake::Sent;
|
||||
return Poll::Pending;
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
NotificationsInSubstreamHandshake::ClosingInResponseToRemote =>
|
||||
match Sink::poll_close(this.socket.as_mut(), cx)? {
|
||||
Poll::Ready(()) =>
|
||||
*this.handshake = NotificationsInSubstreamHandshake::BothSidesClosed,
|
||||
Poll::Pending => {
|
||||
*this.handshake =
|
||||
NotificationsInSubstreamHandshake::ClosingInResponseToRemote;
|
||||
return Poll::Pending;
|
||||
},
|
||||
},
|
||||
|
||||
NotificationsInSubstreamHandshake::BothSidesClosed => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationsOut {
|
||||
/// Builds a new potential upgrade.
|
||||
pub fn new(
|
||||
main_protocol_name: impl Into<ProtocolName>,
|
||||
fallback_names: Vec<ProtocolName>,
|
||||
initial_message: impl Into<Vec<u8>>,
|
||||
max_notification_size: u64,
|
||||
peer_id: PeerId,
|
||||
) -> Self {
|
||||
let initial_message = initial_message.into();
|
||||
if initial_message.len() > MAX_HANDSHAKE_SIZE {
|
||||
error!(target: LOG_TARGET, "Outbound networking handshake is above allowed protocol limit");
|
||||
}
|
||||
|
||||
let mut protocol_names = fallback_names;
|
||||
protocol_names.insert(0, main_protocol_name.into());
|
||||
|
||||
Self { protocol_names, initial_message, max_notification_size, peer_id }
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeInfo for NotificationsOut {
|
||||
type Info = ProtocolName;
|
||||
type InfoIter = vec::IntoIter<Self::Info>;
|
||||
|
||||
fn protocol_info(&self) -> Self::InfoIter {
|
||||
self.protocol_names.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSubstream> OutboundUpgrade<TSubstream> for NotificationsOut
|
||||
where
|
||||
TSubstream: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
type Output = NotificationsOutOpen<TSubstream>;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
|
||||
type Error = NotificationsHandshakeError;
|
||||
|
||||
fn upgrade_outbound(self, mut socket: TSubstream, negotiated_name: Self::Info) -> Self::Future {
|
||||
Box::pin(async move {
|
||||
{
|
||||
let mut len_data = unsigned_varint::encode::usize_buffer();
|
||||
let encoded_len =
|
||||
unsigned_varint::encode::usize(self.initial_message.len(), &mut len_data).len();
|
||||
socket.write_all(&len_data[..encoded_len]).await?;
|
||||
}
|
||||
socket.write_all(&self.initial_message).await?;
|
||||
socket.flush().await?;
|
||||
|
||||
// Reading handshake.
|
||||
let handshake_len = unsigned_varint::aio::read_usize(&mut socket).await?;
|
||||
if handshake_len > MAX_HANDSHAKE_SIZE {
|
||||
return Err(NotificationsHandshakeError::TooLarge {
|
||||
requested: handshake_len,
|
||||
max: MAX_HANDSHAKE_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
let mut handshake = vec![0u8; handshake_len];
|
||||
if !handshake.is_empty() {
|
||||
socket.read_exact(&mut handshake).await?;
|
||||
}
|
||||
|
||||
let mut codec = UviBytes::default();
|
||||
codec.set_max_len(usize::try_from(self.max_notification_size).unwrap_or(usize::MAX));
|
||||
|
||||
Ok(NotificationsOutOpen {
|
||||
handshake,
|
||||
negotiated_fallback: if negotiated_name == self.protocol_names[0] {
|
||||
None
|
||||
} else {
|
||||
Some(negotiated_name)
|
||||
},
|
||||
substream: NotificationsOutSubstream {
|
||||
socket: Framed::new(socket, codec),
|
||||
peer_id: self.peer_id,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Yielded by the [`NotificationsOut`] after a successfully upgrade.
|
||||
pub struct NotificationsOutOpen<TSubstream> {
|
||||
/// Handshake returned by the remote.
|
||||
pub handshake: Vec<u8>,
|
||||
/// If the negotiated name is not the "main" protocol name but a fallback, contains the
|
||||
/// name of the negotiated fallback.
|
||||
pub negotiated_fallback: Option<ProtocolName>,
|
||||
/// Implementation of `Sink` that allows sending messages on the substream.
|
||||
pub substream: NotificationsOutSubstream<TSubstream>,
|
||||
}
|
||||
|
||||
impl<TSubstream> fmt::Debug for NotificationsOutOpen<TSubstream> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NotificationsOutOpen")
|
||||
.field("handshake", &self.handshake)
|
||||
.field("negotiated_fallback", &self.negotiated_fallback)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<TSubstream> Sink<Vec<u8>> for NotificationsOutSubstream<TSubstream>
|
||||
where
|
||||
TSubstream: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Error = NotificationsOutError;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
let mut this = self.project();
|
||||
Sink::poll_ready(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
|
||||
}
|
||||
|
||||
fn start_send(self: Pin<&mut Self>, item: Vec<u8>) -> Result<(), Self::Error> {
|
||||
let mut this = self.project();
|
||||
Sink::start_send(this.socket.as_mut(), io::Cursor::new(item))
|
||||
.map_err(NotificationsOutError::Io)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
let mut this = self.project();
|
||||
|
||||
// `Sink::poll_flush` does not expose stream closed error until we write something into
|
||||
// the stream, so the code below makes sure we detect that the substream was closed
|
||||
// even if we don't write anything into it.
|
||||
match Stream::poll_next(this.socket.as_mut(), cx) {
|
||||
Poll::Pending => {},
|
||||
Poll::Ready(Some(result)) => match result {
|
||||
Ok(_) => {
|
||||
debug!(
|
||||
target: "sub-libp2p",
|
||||
"Unexpected incoming data in `NotificationsOutSubstream` peer={:?}",
|
||||
this.peer_id
|
||||
);
|
||||
|
||||
return Poll::Ready(Err(NotificationsOutError::UnexpectedData));
|
||||
},
|
||||
Err(error) => {
|
||||
debug!(
|
||||
target: "sub-libp2p",
|
||||
"Error while reading from `NotificationsOutSubstream` peer={:?} error={error:?}",
|
||||
this.peer_id
|
||||
);
|
||||
|
||||
// The expectation is that the remote has closed the substream.
|
||||
return Poll::Ready(Err(NotificationsOutError::Closed));
|
||||
},
|
||||
},
|
||||
Poll::Ready(None) => return Poll::Ready(Err(NotificationsOutError::Closed)),
|
||||
}
|
||||
|
||||
Sink::poll_flush(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
let mut this = self.project();
|
||||
Sink::poll_close(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error generated by sending on a notifications out substream.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NotificationsHandshakeError {
|
||||
/// I/O error on the substream.
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
/// Initial message or handshake was too large.
|
||||
#[error("Initial message or handshake was too large: {requested}")]
|
||||
TooLarge {
|
||||
/// Size requested by the remote.
|
||||
requested: usize,
|
||||
/// Maximum allowed,
|
||||
max: usize,
|
||||
},
|
||||
|
||||
/// Error while decoding the variable-length integer.
|
||||
#[error(transparent)]
|
||||
VarintDecode(#[from] unsigned_varint::decode::Error),
|
||||
}
|
||||
|
||||
impl From<unsigned_varint::io::ReadError> for NotificationsHandshakeError {
|
||||
fn from(err: unsigned_varint::io::ReadError) -> Self {
|
||||
match err {
|
||||
unsigned_varint::io::ReadError::Io(err) => Self::Io(err),
|
||||
unsigned_varint::io::ReadError::Decode(err) => Self::VarintDecode(err),
|
||||
_ => {
|
||||
warn!("Unrecognized varint decoding error");
|
||||
Self::Io(From::from(io::ErrorKind::InvalidData))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error generated by sending on a notifications out substream.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NotificationsOutError {
|
||||
/// I/O error on the substream.
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
/// The substream was closed.
|
||||
#[error("substream was closed/reset")]
|
||||
Closed,
|
||||
|
||||
/// The remote peer did not comply with the notification spec.
|
||||
///
|
||||
/// This is a terminal error and the peer should be banned immediately.
|
||||
#[error("unexpected data received from the remote peer")]
|
||||
UnexpectedData,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ProtocolName;
|
||||
|
||||
use super::{
|
||||
NotificationsHandshakeError, NotificationsIn, NotificationsInOpen,
|
||||
NotificationsInSubstream, NotificationsOut, NotificationsOutError, NotificationsOutOpen,
|
||||
NotificationsOutSubstream,
|
||||
};
|
||||
use futures::{channel::oneshot, future, prelude::*, SinkExt, StreamExt};
|
||||
use libp2p::{
|
||||
core::{upgrade, InboundUpgrade, OutboundUpgrade, UpgradeInfo},
|
||||
PeerId,
|
||||
};
|
||||
use std::{pin::Pin, task::Poll};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
|
||||
/// Opens a substream to the given address, negotiates the protocol, and returns the substream
|
||||
/// along with the handshake message.
|
||||
async fn dial(
|
||||
addr: std::net::SocketAddr,
|
||||
handshake: impl Into<Vec<u8>>,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<u8>,
|
||||
NotificationsOutSubstream<
|
||||
multistream_select::Negotiated<tokio_util::compat::Compat<TcpStream>>,
|
||||
>,
|
||||
),
|
||||
NotificationsHandshakeError,
|
||||
> {
|
||||
let socket = TcpStream::connect(addr).await.unwrap();
|
||||
let notifs_out = NotificationsOut::new(
|
||||
"/test/proto/1",
|
||||
Vec::new(),
|
||||
handshake,
|
||||
1024 * 1024,
|
||||
PeerId::random(),
|
||||
);
|
||||
let (_, substream) = multistream_select::dialer_select_proto(
|
||||
socket.compat(),
|
||||
notifs_out.protocol_info(),
|
||||
upgrade::Version::V1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let NotificationsOutOpen { handshake, substream, .. } =
|
||||
<NotificationsOut as OutboundUpgrade<_>>::upgrade_outbound(
|
||||
notifs_out,
|
||||
substream,
|
||||
"/test/proto/1".into(),
|
||||
)
|
||||
.await?;
|
||||
Ok((handshake, substream))
|
||||
}
|
||||
|
||||
/// Listens on a localhost, negotiates the protocol, and returns the substream along with the
|
||||
/// handshake message.
|
||||
///
|
||||
/// Also sends the listener address through the given channel.
|
||||
async fn listen_on_localhost(
|
||||
listener_addr_tx: oneshot::Sender<std::net::SocketAddr>,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<u8>,
|
||||
NotificationsInSubstream<
|
||||
multistream_select::Negotiated<tokio_util::compat::Compat<TcpStream>>,
|
||||
>,
|
||||
),
|
||||
NotificationsHandshakeError,
|
||||
> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
listener_addr_tx.send(listener.local_addr().unwrap()).unwrap();
|
||||
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let notifs_in = NotificationsIn::new("/test/proto/1", Vec::new(), 1024 * 1024);
|
||||
let (_, substream) =
|
||||
multistream_select::listener_select_proto(socket.compat(), notifs_in.protocol_info())
|
||||
.await
|
||||
.unwrap();
|
||||
let NotificationsInOpen { handshake, substream, .. } =
|
||||
<NotificationsIn as InboundUpgrade<_>>::upgrade_inbound(
|
||||
notifs_in,
|
||||
substream,
|
||||
"/test/proto/1".into(),
|
||||
)
|
||||
.await?;
|
||||
Ok((handshake, substream))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_works() {
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let (handshake, mut substream) =
|
||||
dial(listener_addr_rx.await.unwrap(), &b"initial message"[..]).await.unwrap();
|
||||
|
||||
assert_eq!(handshake, b"hello world");
|
||||
substream.send(b"test message".to_vec()).await.unwrap();
|
||||
});
|
||||
|
||||
let (handshake, mut substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
|
||||
|
||||
assert_eq!(handshake, b"initial message");
|
||||
substream.send_handshake(&b"hello world"[..]);
|
||||
|
||||
let msg = substream.next().await.unwrap().unwrap();
|
||||
assert_eq!(msg.as_ref(), b"test message");
|
||||
|
||||
client.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_handshake() {
|
||||
// Check that everything still works when the handshake messages are empty.
|
||||
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let (handshake, mut substream) =
|
||||
dial(listener_addr_rx.await.unwrap(), vec![]).await.unwrap();
|
||||
|
||||
assert!(handshake.is_empty());
|
||||
substream.send(Default::default()).await.unwrap();
|
||||
});
|
||||
|
||||
let (handshake, mut substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
|
||||
|
||||
assert!(handshake.is_empty());
|
||||
substream.send_handshake(vec![]);
|
||||
|
||||
let msg = substream.next().await.unwrap().unwrap();
|
||||
assert!(msg.as_ref().is_empty());
|
||||
|
||||
client.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refused() {
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let outcome = dial(listener_addr_rx.await.unwrap(), &b"hello"[..]).await;
|
||||
|
||||
// Despite the protocol negotiation being successfully conducted on the listener
|
||||
// side, we have to receive an error here because the listener didn't send the
|
||||
// handshake.
|
||||
assert!(outcome.is_err());
|
||||
});
|
||||
|
||||
let (handshake, substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
|
||||
assert_eq!(handshake, b"hello");
|
||||
|
||||
// We successfully upgrade to the protocol, but then close the substream.
|
||||
drop(substream);
|
||||
|
||||
client.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn large_initial_message_refused() {
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let ret =
|
||||
dial(listener_addr_rx.await.unwrap(), (0..32768).map(|_| 0).collect::<Vec<_>>())
|
||||
.await;
|
||||
assert!(ret.is_err());
|
||||
});
|
||||
|
||||
let _ret = listen_on_localhost(listener_addr_tx).await;
|
||||
client.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn large_handshake_refused() {
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let ret = dial(listener_addr_rx.await.unwrap(), &b"initial message"[..]).await;
|
||||
assert!(ret.is_err());
|
||||
});
|
||||
|
||||
let (handshake, mut substream) = listen_on_localhost(listener_addr_tx).await.unwrap();
|
||||
assert_eq!(handshake, b"initial message");
|
||||
|
||||
// We check that a handshake that is too large gets refused.
|
||||
substream.send_handshake((0..32768).map(|_| 0).collect::<Vec<_>>());
|
||||
let _ = substream.next().await;
|
||||
|
||||
client.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_handshake_without_polling_for_incoming_data() {
|
||||
const PROTO_NAME: &str = "/test/proto/1";
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let socket = TcpStream::connect(listener_addr_rx.await.unwrap()).await.unwrap();
|
||||
let NotificationsOutOpen { handshake, .. } = OutboundUpgrade::upgrade_outbound(
|
||||
NotificationsOut::new(
|
||||
PROTO_NAME,
|
||||
Vec::new(),
|
||||
&b"initial message"[..],
|
||||
1024 * 1024,
|
||||
PeerId::random(),
|
||||
),
|
||||
socket.compat(),
|
||||
ProtocolName::Static(PROTO_NAME),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(handshake, b"hello world");
|
||||
});
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
listener_addr_tx.send(listener.local_addr().unwrap()).unwrap();
|
||||
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let NotificationsInOpen { handshake, mut substream, .. } = InboundUpgrade::upgrade_inbound(
|
||||
NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
|
||||
socket.compat(),
|
||||
ProtocolName::Static(PROTO_NAME),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(handshake, b"initial message");
|
||||
substream.send_handshake(&b"hello world"[..]);
|
||||
|
||||
// Actually send the handshake.
|
||||
future::poll_fn(|cx| Pin::new(&mut substream).poll_process(cx)).await.unwrap();
|
||||
|
||||
client.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_detect_dropped_out_substream_without_writing_data() {
|
||||
const PROTO_NAME: &str = "/test/proto/1";
|
||||
let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
|
||||
|
||||
let client = tokio::spawn(async move {
|
||||
let socket = TcpStream::connect(listener_addr_rx.await.unwrap()).await.unwrap();
|
||||
let NotificationsOutOpen { handshake, mut substream, .. } =
|
||||
OutboundUpgrade::upgrade_outbound(
|
||||
NotificationsOut::new(
|
||||
PROTO_NAME,
|
||||
Vec::new(),
|
||||
&b"initial message"[..],
|
||||
1024 * 1024,
|
||||
PeerId::random(),
|
||||
),
|
||||
socket.compat(),
|
||||
ProtocolName::Static(PROTO_NAME),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(handshake, b"hello world");
|
||||
|
||||
future::poll_fn(|cx| match Pin::new(&mut substream).poll_flush(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Ok(())) => {
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
},
|
||||
Poll::Ready(Err(e)) => {
|
||||
assert!(matches!(e, NotificationsOutError::Closed));
|
||||
Poll::Ready(())
|
||||
},
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
listener_addr_tx.send(listener.local_addr().unwrap()).unwrap();
|
||||
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let NotificationsInOpen { handshake, mut substream, .. } = InboundUpgrade::upgrade_inbound(
|
||||
NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
|
||||
socket.compat(),
|
||||
ProtocolName::Static(PROTO_NAME),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(handshake, b"initial message");
|
||||
|
||||
// Send the handhsake.
|
||||
substream.send_handshake(&b"hello world"[..]);
|
||||
future::poll_fn(|cx| Pin::new(&mut substream).poll_process(cx)).await.unwrap();
|
||||
|
||||
drop(substream);
|
||||
|
||||
client.await.unwrap();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bitswap.message;
|
||||
|
||||
message Message {
|
||||
message Wantlist {
|
||||
enum WantType {
|
||||
Block = 0;
|
||||
Have = 1;
|
||||
}
|
||||
|
||||
message Entry {
|
||||
bytes block = 1; // the block cid (cidV0 in bitswap 1.0.0, cidV1 in bitswap 1.1.0)
|
||||
int32 priority = 2; // the priority (normalized). default to 1
|
||||
bool cancel = 3; // whether this revokes an entry
|
||||
WantType wantType = 4; // Note: defaults to enum 0, ie Block
|
||||
bool sendDontHave = 5; // Note: defaults to false
|
||||
}
|
||||
|
||||
repeated Entry entries = 1; // a list of wantlist entries
|
||||
bool full = 2; // whether this is the full wantlist. default to false
|
||||
}
|
||||
|
||||
message Block {
|
||||
bytes prefix = 1; // CID prefix (cid version, multicodec and multihash prefix (type + length)
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
enum BlockPresenceType {
|
||||
Have = 0;
|
||||
DontHave = 1;
|
||||
}
|
||||
message BlockPresence {
|
||||
bytes cid = 1;
|
||||
BlockPresenceType type = 2;
|
||||
}
|
||||
|
||||
Wantlist wantlist = 1;
|
||||
repeated bytes blocks = 2; // used to send Blocks in bitswap 1.0.0
|
||||
repeated Block payload = 3; // used to send Blocks in bitswap 1.1.0
|
||||
repeated BlockPresence blockPresences = 4;
|
||||
int32 pendingBytes = 5;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,414 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{service::traits::BandwidthSink, ProtocolName};
|
||||
|
||||
use prometheus_endpoint::{
|
||||
self as prometheus, Counter, CounterVec, Gauge, GaugeVec, HistogramOpts, MetricSource, Opts,
|
||||
PrometheusError, Registry, SourcedCounter, SourcedGauge, U64,
|
||||
};
|
||||
|
||||
use std::{
|
||||
str,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
pub use prometheus_endpoint::{Histogram, HistogramVec};
|
||||
|
||||
/// Registers all networking metrics with the given registry.
|
||||
pub fn register(registry: &Registry, sources: MetricSources) -> Result<Metrics, PrometheusError> {
|
||||
BandwidthCounters::register(registry, sources.bandwidth)?;
|
||||
NumConnectedGauge::register(registry, sources.connected_peers)?;
|
||||
Metrics::register(registry)
|
||||
}
|
||||
|
||||
// Register `sc-network` metrics without bandwidth/connected peer sources.
|
||||
pub fn register_without_sources(registry: &Registry) -> Result<Metrics, PrometheusError> {
|
||||
Metrics::register(registry)
|
||||
}
|
||||
|
||||
/// Predefined metric sources that are fed directly into prometheus.
|
||||
pub struct MetricSources {
|
||||
pub bandwidth: Arc<dyn BandwidthSink>,
|
||||
pub connected_peers: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl MetricSources {
|
||||
pub fn register(
|
||||
registry: &Registry,
|
||||
bandwidth: Arc<dyn BandwidthSink>,
|
||||
connected_peers: Arc<AtomicUsize>,
|
||||
) -> Result<(), PrometheusError> {
|
||||
BandwidthCounters::register(registry, bandwidth)?;
|
||||
NumConnectedGauge::register(registry, connected_peers)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dedicated metrics.
|
||||
#[derive(Clone)]
|
||||
pub struct Metrics {
|
||||
// This list is ordered alphabetically
|
||||
pub connections_closed_total: CounterVec<U64>,
|
||||
pub connections_opened_total: CounterVec<U64>,
|
||||
pub distinct_peers_connections_closed_total: Counter<U64>,
|
||||
pub distinct_peers_connections_opened_total: Counter<U64>,
|
||||
pub incoming_connections_errors_total: CounterVec<U64>,
|
||||
pub incoming_connections_total: Counter<U64>,
|
||||
pub kademlia_query_duration: HistogramVec,
|
||||
pub kademlia_random_queries_total: Counter<U64>,
|
||||
pub kademlia_records_count: Gauge<U64>,
|
||||
pub kademlia_records_sizes_total: Gauge<U64>,
|
||||
pub kbuckets_num_nodes: GaugeVec<U64>,
|
||||
pub listeners_local_addresses: Gauge<U64>,
|
||||
pub listeners_errors_total: Counter<U64>,
|
||||
pub pending_connections: Gauge<U64>,
|
||||
pub pending_connections_errors_total: CounterVec<U64>,
|
||||
pub requests_in_failure_total: CounterVec<U64>,
|
||||
pub requests_in_success_total: HistogramVec,
|
||||
pub requests_out_failure_total: CounterVec<U64>,
|
||||
pub requests_out_success_total: HistogramVec,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
// This list is ordered alphabetically
|
||||
connections_closed_total: prometheus::register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_connections_closed_total",
|
||||
"Total number of connections closed, by direction and reason"
|
||||
),
|
||||
&["direction", "reason"]
|
||||
)?, registry)?,
|
||||
connections_opened_total: prometheus::register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_connections_opened_total",
|
||||
"Total number of connections opened by direction"
|
||||
),
|
||||
&["direction"]
|
||||
)?, registry)?,
|
||||
distinct_peers_connections_closed_total: prometheus::register(Counter::new(
|
||||
"bizinikiwi_sub_libp2p_distinct_peers_connections_closed_total",
|
||||
"Total number of connections closed with distinct peers"
|
||||
)?, registry)?,
|
||||
distinct_peers_connections_opened_total: prometheus::register(Counter::new(
|
||||
"bizinikiwi_sub_libp2p_distinct_peers_connections_opened_total",
|
||||
"Total number of connections opened with distinct peers"
|
||||
)?, registry)?,
|
||||
incoming_connections_errors_total: prometheus::register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_incoming_connections_handshake_errors_total",
|
||||
"Total number of incoming connections that have failed during the \
|
||||
initial handshake"
|
||||
),
|
||||
&["reason"]
|
||||
)?, registry)?,
|
||||
incoming_connections_total: prometheus::register(Counter::new(
|
||||
"bizinikiwi_sub_libp2p_incoming_connections_total",
|
||||
"Total number of incoming connections on the listening sockets"
|
||||
)?, registry)?,
|
||||
kademlia_query_duration: prometheus::register(HistogramVec::new(
|
||||
HistogramOpts {
|
||||
common_opts: Opts::new(
|
||||
"bizinikiwi_sub_libp2p_kademlia_query_duration",
|
||||
"Duration of Kademlia queries per query type"
|
||||
),
|
||||
buckets: prometheus::exponential_buckets(0.5, 2.0, 10)
|
||||
.expect("parameters are always valid values; qed"),
|
||||
},
|
||||
&["type"]
|
||||
)?, registry)?,
|
||||
kademlia_random_queries_total: prometheus::register(Counter::new(
|
||||
"bizinikiwi_sub_libp2p_kademlia_random_queries_total",
|
||||
"Number of random Kademlia queries started",
|
||||
)?, registry)?,
|
||||
kademlia_records_count: prometheus::register(Gauge::new(
|
||||
"bizinikiwi_sub_libp2p_kademlia_records_count",
|
||||
"Number of records in the Kademlia records store",
|
||||
)?, registry)?,
|
||||
kademlia_records_sizes_total: prometheus::register(Gauge::new(
|
||||
"bizinikiwi_sub_libp2p_kademlia_records_sizes_total",
|
||||
"Total size of all the records in the Kademlia records store",
|
||||
)?, registry)?,
|
||||
kbuckets_num_nodes: prometheus::register(GaugeVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_kbuckets_num_nodes",
|
||||
"Number of nodes per kbucket per Kademlia instance"
|
||||
),
|
||||
&["lower_ilog2_bucket_bound"]
|
||||
)?, registry)?,
|
||||
listeners_local_addresses: prometheus::register(Gauge::new(
|
||||
"bizinikiwi_sub_libp2p_listeners_local_addresses",
|
||||
"Number of local addresses we're listening on"
|
||||
)?, registry)?,
|
||||
listeners_errors_total: prometheus::register(Counter::new(
|
||||
"bizinikiwi_sub_libp2p_listeners_errors_total",
|
||||
"Total number of non-fatal errors reported by a listener"
|
||||
)?, registry)?,
|
||||
pending_connections: prometheus::register(Gauge::new(
|
||||
"bizinikiwi_sub_libp2p_pending_connections",
|
||||
"Number of connections in the process of being established",
|
||||
)?, registry)?,
|
||||
pending_connections_errors_total: prometheus::register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_pending_connections_errors_total",
|
||||
"Total number of pending connection errors"
|
||||
),
|
||||
&["reason"]
|
||||
)?, registry)?,
|
||||
requests_in_failure_total: prometheus::register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_requests_in_failure_total",
|
||||
"Total number of incoming requests that the node has failed to answer"
|
||||
),
|
||||
&["protocol", "reason"]
|
||||
)?, registry)?,
|
||||
requests_in_success_total: prometheus::register(HistogramVec::new(
|
||||
HistogramOpts {
|
||||
common_opts: Opts::new(
|
||||
"bizinikiwi_sub_libp2p_requests_in_success_total",
|
||||
"For successful incoming requests, time between receiving the request and \
|
||||
starting to send the response"
|
||||
),
|
||||
buckets: prometheus::exponential_buckets(0.001, 2.0, 16)
|
||||
.expect("parameters are always valid values; qed"),
|
||||
},
|
||||
&["protocol"]
|
||||
)?, registry)?,
|
||||
requests_out_failure_total: prometheus::register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_requests_out_failure_total",
|
||||
"Total number of requests that have failed"
|
||||
),
|
||||
&["protocol", "reason"]
|
||||
)?, registry)?,
|
||||
requests_out_success_total: prometheus::register(HistogramVec::new(
|
||||
HistogramOpts {
|
||||
common_opts: Opts::new(
|
||||
"bizinikiwi_sub_libp2p_requests_out_success_total",
|
||||
"For successful outgoing requests, time between a request's start and finish"
|
||||
),
|
||||
buckets: prometheus::exponential_buckets(0.001, 2.0, 16)
|
||||
.expect("parameters are always valid values; qed"),
|
||||
},
|
||||
&["protocol"]
|
||||
)?, registry)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Peer store metrics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PeerStoreMetrics {
|
||||
pub num_banned_peers: Gauge<U64>,
|
||||
pub num_discovered: Gauge<U64>,
|
||||
}
|
||||
|
||||
impl PeerStoreMetrics {
|
||||
pub fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
num_banned_peers: prometheus::register(
|
||||
Gauge::new(
|
||||
"bizinikiwi_sub_libp2p_peerset_num_banned_peers",
|
||||
"Number of banned peers stored in the peerset manager",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
num_discovered: prometheus::register(
|
||||
Gauge::new(
|
||||
"bizinikiwi_sub_libp2p_peerset_num_discovered",
|
||||
"Number of nodes stored in the peerset manager",
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The bandwidth counter metric.
|
||||
#[derive(Clone)]
|
||||
pub struct BandwidthCounters(Arc<dyn BandwidthSink>);
|
||||
|
||||
impl BandwidthCounters {
|
||||
/// Registers the `BandwidthCounters` metric whose values are
|
||||
/// obtained from the given sinks.
|
||||
fn register(registry: &Registry, sinks: Arc<dyn BandwidthSink>) -> Result<(), PrometheusError> {
|
||||
prometheus::register(
|
||||
SourcedCounter::new(
|
||||
&Opts::new("bizinikiwi_sub_libp2p_network_bytes_total", "Total bandwidth usage")
|
||||
.variable_label("direction"),
|
||||
BandwidthCounters(sinks),
|
||||
)?,
|
||||
registry,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MetricSource for BandwidthCounters {
|
||||
type N = u64;
|
||||
|
||||
fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
|
||||
set(&["in"], self.0.total_inbound());
|
||||
set(&["out"], self.0.total_outbound());
|
||||
}
|
||||
}
|
||||
|
||||
/// The connected peers metric.
|
||||
#[derive(Clone)]
|
||||
pub struct NumConnectedGauge(Arc<AtomicUsize>);
|
||||
|
||||
impl NumConnectedGauge {
|
||||
/// Registers the `MajorSyncingGauge` metric whose value is
|
||||
/// obtained from the given `AtomicUsize`.
|
||||
fn register(registry: &Registry, value: Arc<AtomicUsize>) -> Result<(), PrometheusError> {
|
||||
prometheus::register(
|
||||
SourcedGauge::new(
|
||||
&Opts::new("bizinikiwi_sub_libp2p_peers_count", "Number of connected peers"),
|
||||
NumConnectedGauge(value),
|
||||
)?,
|
||||
registry,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MetricSource for NumConnectedGauge {
|
||||
type N = u64;
|
||||
|
||||
fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
|
||||
set(&[], self.0.load(Ordering::Relaxed) as u64);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification metrics.
|
||||
///
|
||||
/// Wrapper over `Option<InnerNotificationMetrics>` to make metrics reporting code cleaner.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationMetrics {
|
||||
/// Metrics, if enabled.
|
||||
metrics: Option<InnerNotificationMetrics>,
|
||||
}
|
||||
|
||||
impl NotificationMetrics {
|
||||
/// Create new [`NotificationMetrics`].
|
||||
pub fn new(registry: Option<&Registry>) -> NotificationMetrics {
|
||||
let metrics = match registry {
|
||||
Some(registry) => InnerNotificationMetrics::register(registry).ok(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Self { metrics }
|
||||
}
|
||||
|
||||
/// Register opened substream to Prometheus.
|
||||
pub fn register_substream_opened(&self, protocol: &ProtocolName) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.notifications_streams_opened_total.with_label_values(&[&protocol]).inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register closed substream to Prometheus.
|
||||
pub fn register_substream_closed(&self, protocol: &ProtocolName) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.notifications_streams_closed_total
|
||||
.with_label_values(&[&protocol[..]])
|
||||
.inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register sent notification to Prometheus.
|
||||
pub fn register_notification_sent(&self, protocol: &ProtocolName, size: usize) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.notifications_sizes
|
||||
.with_label_values(&["out", protocol])
|
||||
.observe(size as f64);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register received notification to Prometheus.
|
||||
pub fn register_notification_received(&self, protocol: &ProtocolName, size: usize) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics
|
||||
.notifications_sizes
|
||||
.with_label_values(&["in", protocol])
|
||||
.observe(size as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification metrics.
|
||||
#[derive(Debug, Clone)]
|
||||
struct InnerNotificationMetrics {
|
||||
// Total number of opened substreams.
|
||||
pub notifications_streams_opened_total: CounterVec<U64>,
|
||||
|
||||
/// Total number of closed substreams.
|
||||
pub notifications_streams_closed_total: CounterVec<U64>,
|
||||
|
||||
/// In/outbound notification sizes.
|
||||
pub notifications_sizes: HistogramVec,
|
||||
}
|
||||
|
||||
impl InnerNotificationMetrics {
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
notifications_sizes: prometheus::register(
|
||||
HistogramVec::new(
|
||||
HistogramOpts {
|
||||
common_opts: Opts::new(
|
||||
"bizinikiwi_sub_libp2p_notifications_sizes",
|
||||
"Sizes of the notifications send to and received from all nodes",
|
||||
),
|
||||
buckets: prometheus::exponential_buckets(64.0, 4.0, 8)
|
||||
.expect("parameters are always valid values; qed"),
|
||||
},
|
||||
&["direction", "protocol"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
notifications_streams_closed_total: prometheus::register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_notifications_streams_closed_total",
|
||||
"Total number of notification substreams that have been closed",
|
||||
),
|
||||
&["protocol"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
notifications_streams_opened_total: prometheus::register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_notifications_streams_opened_total",
|
||||
"Total number of notification substreams that have been opened",
|
||||
),
|
||||
&["protocol"],
|
||||
)?,
|
||||
registry,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Registering events streams.
|
||||
//!
|
||||
//! This code holds the logic that is used for the network service to inform other parts of
|
||||
//! Bizinikiwi about what is happening.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! - Create an instance of [`OutChannels`].
|
||||
//! - Create channels using the [`channel`] function. The receiving side implements the `Stream`
|
||||
//! trait.
|
||||
//! - You cannot directly send an event on a sender. Instead, you have to call
|
||||
//! [`OutChannels::push`] to put the sender within a [`OutChannels`].
|
||||
//! - Send events by calling [`OutChannels::send`]. Events are cloned for each sender in the
|
||||
//! collection.
|
||||
|
||||
use crate::event::Event;
|
||||
|
||||
use futures::{prelude::*, ready, stream::FusedStream};
|
||||
use log::{debug, error};
|
||||
use prometheus_endpoint::{register, CounterVec, GaugeVec, Opts, PrometheusError, Registry, U64};
|
||||
use std::{
|
||||
backtrace::Backtrace,
|
||||
cell::RefCell,
|
||||
fmt,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/// Log target for this file.
|
||||
pub const LOG_TARGET: &str = "sub-libp2p::out_events";
|
||||
|
||||
/// Creates a new channel that can be associated to a [`OutChannels`].
|
||||
///
|
||||
/// The name is used in Prometheus reports, the queue size threshold is used
|
||||
/// to warn if there are too many unprocessed events in the channel.
|
||||
pub fn channel(name: &'static str, queue_size_warning: usize) -> (Sender, Receiver) {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let tx = Sender {
|
||||
inner: tx,
|
||||
name,
|
||||
queue_size_warning,
|
||||
warning_fired: SenderWarningState::NotFired,
|
||||
creation_backtrace: Backtrace::force_capture(),
|
||||
metrics: None,
|
||||
};
|
||||
let rx = Receiver { inner: rx, name, metrics: None };
|
||||
(tx, rx)
|
||||
}
|
||||
|
||||
/// A state of a sender warning that is used to avoid spamming the logs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SenderWarningState {
|
||||
/// The warning has not been fired yet.
|
||||
NotFired,
|
||||
/// The warning has been fired, and the channel is full
|
||||
FiredFull,
|
||||
/// The warning has been fired and the channel is not full anymore.
|
||||
FiredFree,
|
||||
}
|
||||
|
||||
/// Sending side of a channel.
|
||||
///
|
||||
/// Must be associated with an [`OutChannels`] before anything can be sent on it
|
||||
///
|
||||
/// > **Note**: Contrary to regular channels, this `Sender` is purposefully designed to not
|
||||
/// implement the `Clone` trait e.g. in Order to not complicate the logic keeping the metrics in
|
||||
/// sync on drop. If someone adds a `#[derive(Clone)]` below, it is **wrong**.
|
||||
pub struct Sender {
|
||||
inner: async_channel::Sender<Event>,
|
||||
/// Name to identify the channel (e.g., in Prometheus and logs).
|
||||
name: &'static str,
|
||||
/// Threshold queue size to generate an error message in the logs.
|
||||
queue_size_warning: usize,
|
||||
/// We generate the error message only once to not spam the logs after the first error.
|
||||
/// Subsequently we indicate channel fullness on debug level.
|
||||
warning_fired: SenderWarningState,
|
||||
/// Backtrace of a place where the channel was created.
|
||||
creation_backtrace: Backtrace,
|
||||
/// Clone of [`Receiver::metrics`]. Will be initialized when [`Sender`] is added to
|
||||
/// [`OutChannels`] with `OutChannels::push()`.
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Sender {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_tuple("Sender").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Sender {
|
||||
fn drop(&mut self) {
|
||||
if let Some(metrics) = self.metrics.as_ref() {
|
||||
metrics.num_channels.with_label_values(&[self.name]).dec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Receiving side of a channel.
|
||||
pub struct Receiver {
|
||||
inner: async_channel::Receiver<Event>,
|
||||
name: &'static str,
|
||||
/// Initially contains `None`, and will be set to a value once the corresponding [`Sender`]
|
||||
/// is assigned to an instance of [`OutChannels`].
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl Stream for Receiver {
|
||||
type Item = Event;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Event>> {
|
||||
if let Some(ev) = ready!(Pin::new(&mut self.inner).poll_next(cx)) {
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.event_out(&ev, self.name);
|
||||
}
|
||||
Poll::Ready(Some(ev))
|
||||
} else {
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Receiver {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_tuple("Receiver").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Receiver {
|
||||
fn drop(&mut self) {
|
||||
if !self.inner.is_terminated() {
|
||||
// Empty the list to properly decrease the metrics.
|
||||
while let Some(Some(_)) = self.next().now_or_never() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of senders.
|
||||
pub struct OutChannels {
|
||||
event_streams: Vec<Sender>,
|
||||
/// The metrics we collect. A clone of this is sent to each [`Receiver`] associated with this
|
||||
/// object.
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl OutChannels {
|
||||
/// Creates a new empty collection of senders.
|
||||
pub fn new(registry: Option<&Registry>) -> Result<Self, PrometheusError> {
|
||||
let metrics =
|
||||
if let Some(registry) = registry { Some(Metrics::register(registry)?) } else { None };
|
||||
|
||||
Ok(Self { event_streams: Vec::new(), metrics })
|
||||
}
|
||||
|
||||
/// Adds a new [`Sender`] to the collection.
|
||||
pub fn push(&mut self, mut sender: Sender) {
|
||||
debug_assert!(sender.metrics.is_none());
|
||||
sender.metrics = self.metrics.clone();
|
||||
|
||||
if let Some(metrics) = &self.metrics {
|
||||
metrics.num_channels.with_label_values(&[sender.name]).inc();
|
||||
}
|
||||
|
||||
self.event_streams.push(sender);
|
||||
}
|
||||
|
||||
/// Sends an event.
|
||||
pub fn send(&mut self, event: Event) {
|
||||
self.event_streams.retain_mut(|sender| {
|
||||
let current_pending = sender.inner.len();
|
||||
if current_pending >= sender.queue_size_warning {
|
||||
if sender.warning_fired == SenderWarningState::NotFired {
|
||||
error!(
|
||||
"The number of unprocessed events in channel `{}` exceeded {}.\n\
|
||||
The channel was created at:\n{:}\n
|
||||
The last event was sent from:\n{:}",
|
||||
sender.name,
|
||||
sender.queue_size_warning,
|
||||
sender.creation_backtrace,
|
||||
Backtrace::force_capture(),
|
||||
);
|
||||
} else if sender.warning_fired == SenderWarningState::FiredFree {
|
||||
// We don't want to spam the logs, so we only log on debug level
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Channel `{}` is overflowed again. Number of events: {}",
|
||||
sender.name, current_pending
|
||||
);
|
||||
}
|
||||
sender.warning_fired = SenderWarningState::FiredFull;
|
||||
} else if sender.warning_fired == SenderWarningState::FiredFull &&
|
||||
current_pending < sender.queue_size_warning.wrapping_div(2)
|
||||
{
|
||||
sender.warning_fired = SenderWarningState::FiredFree;
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"Channel `{}` is no longer overflowed. Number of events: {}",
|
||||
sender.name, current_pending
|
||||
);
|
||||
}
|
||||
|
||||
sender.inner.try_send(event.clone()).is_ok()
|
||||
});
|
||||
|
||||
if let Some(metrics) = &self.metrics {
|
||||
for ev in &self.event_streams {
|
||||
metrics.event_in(&event, ev.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for OutChannels {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("OutChannels")
|
||||
.field("num_channels", &self.event_streams.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Metrics {
|
||||
// This list is ordered alphabetically
|
||||
events_total: CounterVec<U64>,
|
||||
notifications_sizes: CounterVec<U64>,
|
||||
num_channels: GaugeVec<U64>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LABEL_BUFFER: RefCell<String> = RefCell::new(String::new());
|
||||
}
|
||||
|
||||
fn format_label(prefix: &str, protocol: &str, callback: impl FnOnce(&str)) {
|
||||
LABEL_BUFFER.with(|label_buffer| {
|
||||
let mut label_buffer = label_buffer.borrow_mut();
|
||||
label_buffer.clear();
|
||||
label_buffer.reserve(prefix.len() + protocol.len() + 2);
|
||||
label_buffer.push_str(prefix);
|
||||
label_buffer.push('"');
|
||||
label_buffer.push_str(protocol);
|
||||
label_buffer.push('"');
|
||||
callback(&label_buffer);
|
||||
});
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn register(registry: &Registry) -> Result<Self, PrometheusError> {
|
||||
Ok(Self {
|
||||
events_total: register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_out_events_events_total",
|
||||
"Number of broadcast network events that have been sent or received across all \
|
||||
channels"
|
||||
),
|
||||
&["event_name", "action", "name"]
|
||||
)?, registry)?,
|
||||
notifications_sizes: register(CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_out_events_notifications_sizes",
|
||||
"Size of notification events that have been sent or received across all \
|
||||
channels"
|
||||
),
|
||||
&["protocol", "action", "name"]
|
||||
)?, registry)?,
|
||||
num_channels: register(GaugeVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_sub_libp2p_out_events_num_channels",
|
||||
"Number of internal active channels that broadcast network events",
|
||||
),
|
||||
&["name"]
|
||||
)?, registry)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn event_in(&self, event: &Event, name: &str) {
|
||||
match event {
|
||||
Event::Dht(_) => {
|
||||
self.events_total.with_label_values(&["dht", "sent", name]).inc();
|
||||
},
|
||||
Event::NotificationStreamOpened { protocol, .. } => {
|
||||
format_label("notif-open-", protocol, |protocol_label| {
|
||||
self.events_total.with_label_values(&[protocol_label, "sent", name]).inc();
|
||||
});
|
||||
},
|
||||
Event::NotificationStreamClosed { protocol, .. } => {
|
||||
format_label("notif-closed-", protocol, |protocol_label| {
|
||||
self.events_total.with_label_values(&[protocol_label, "sent", name]).inc();
|
||||
});
|
||||
},
|
||||
Event::NotificationsReceived { messages, .. } =>
|
||||
for (protocol, message) in messages {
|
||||
format_label("notif-", protocol, |protocol_label| {
|
||||
self.events_total.with_label_values(&[protocol_label, "sent", name]).inc();
|
||||
});
|
||||
self.notifications_sizes
|
||||
.with_label_values(&[protocol, "sent", name])
|
||||
.inc_by(u64::try_from(message.len()).unwrap_or(u64::MAX));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn event_out(&self, event: &Event, name: &str) {
|
||||
match event {
|
||||
Event::Dht(_) => {
|
||||
self.events_total.with_label_values(&["dht", "received", name]).inc();
|
||||
},
|
||||
Event::NotificationStreamOpened { protocol, .. } => {
|
||||
format_label("notif-open-", protocol, |protocol_label| {
|
||||
self.events_total.with_label_values(&[protocol_label, "received", name]).inc();
|
||||
});
|
||||
},
|
||||
Event::NotificationStreamClosed { protocol, .. } => {
|
||||
format_label("notif-closed-", protocol, |protocol_label| {
|
||||
self.events_total.with_label_values(&[protocol_label, "received", name]).inc();
|
||||
});
|
||||
},
|
||||
Event::NotificationsReceived { messages, .. } =>
|
||||
for (protocol, message) in messages {
|
||||
format_label("notif-", protocol, |protocol_label| {
|
||||
self.events_total
|
||||
.with_label_values(&[protocol_label, "received", name])
|
||||
.inc();
|
||||
});
|
||||
self.notifications_sizes
|
||||
.with_label_values(&[protocol, "received", name])
|
||||
.inc_by(u64::try_from(message.len()).unwrap_or(u64::MAX));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
//
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// If you read this, you are very thorough, congratulations.
|
||||
|
||||
//! Signature-related code
|
||||
|
||||
pub use libp2p::identity::SigningError;
|
||||
|
||||
/// Public key.
|
||||
pub enum PublicKey {
|
||||
/// Litep2p public key.
|
||||
Libp2p(libp2p::identity::PublicKey),
|
||||
|
||||
/// Libp2p public key.
|
||||
Litep2p(litep2p::crypto::PublicKey),
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// Protobuf-encode [`PublicKey`].
|
||||
pub fn encode_protobuf(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Libp2p(public) => public.encode_protobuf(),
|
||||
Self::Litep2p(public) => public.to_protobuf_encoding(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get `PeerId` of the [`PublicKey`].
|
||||
pub fn to_peer_id(&self) -> pezsc_network_types::PeerId {
|
||||
match self {
|
||||
Self::Libp2p(public) => public.to_peer_id().into(),
|
||||
Self::Litep2p(public) => public.to_peer_id().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keypair.
|
||||
pub enum Keypair {
|
||||
/// Litep2p keypair.
|
||||
Libp2p(libp2p::identity::Keypair),
|
||||
|
||||
/// Libp2p keypair.
|
||||
Litep2p(litep2p::crypto::ed25519::Keypair),
|
||||
}
|
||||
|
||||
impl Keypair {
|
||||
/// Generate ed25519 keypair.
|
||||
pub fn generate_ed25519() -> Self {
|
||||
Keypair::Litep2p(litep2p::crypto::ed25519::Keypair::generate())
|
||||
}
|
||||
|
||||
/// Get [`Keypair`]'s public key.
|
||||
pub fn public(&self) -> PublicKey {
|
||||
match self {
|
||||
Keypair::Libp2p(keypair) => PublicKey::Libp2p(keypair.public()),
|
||||
Keypair::Litep2p(keypair) => PublicKey::Litep2p(keypair.public().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A result of signing a message with a network identity. Since `PeerId` is potentially a hash of a
|
||||
/// `PublicKey`, you need to reveal the `PublicKey` next to the signature, so the verifier can check
|
||||
/// if the signature was made by the entity that controls a given `PeerId`.
|
||||
pub struct Signature {
|
||||
/// The public key derived from the network identity that signed the message.
|
||||
pub public_key: PublicKey,
|
||||
|
||||
/// The actual signature made for the message signed.
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
/// Create new [`Signature`].
|
||||
pub fn new(public_key: PublicKey, bytes: Vec<u8>) -> Self {
|
||||
Self { public_key, bytes }
|
||||
}
|
||||
|
||||
/// Create a signature for a message with a given network identity.
|
||||
pub fn sign_message(
|
||||
message: impl AsRef<[u8]>,
|
||||
keypair: &Keypair,
|
||||
) -> Result<Self, SigningError> {
|
||||
match keypair {
|
||||
Keypair::Libp2p(keypair) => {
|
||||
let public_key = keypair.public();
|
||||
let bytes = keypair.sign(message.as_ref())?;
|
||||
|
||||
Ok(Signature { public_key: PublicKey::Libp2p(public_key), bytes })
|
||||
},
|
||||
Keypair::Litep2p(keypair) => {
|
||||
let public_key = keypair.public();
|
||||
let bytes = keypair.sign(message.as_ref());
|
||||
|
||||
Ok(Signature { public_key: PublicKey::Litep2p(public_key.into()), bytes })
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,972 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
//
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
//
|
||||
// This program 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.
|
||||
//
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// If you read this, you are very thorough, congratulations.
|
||||
|
||||
//! Traits defined by `sc-network`.
|
||||
|
||||
use crate::{
|
||||
config::{IncomingRequest, MultiaddrWithPeerId, NotificationHandshake, Params, SetConfig},
|
||||
error::{self, Error},
|
||||
event::Event,
|
||||
network_state::NetworkState,
|
||||
request_responses::{IfDisconnected, RequestFailure},
|
||||
service::{metrics::NotificationMetrics, signature::Signature, PeerStoreProvider},
|
||||
types::ProtocolName,
|
||||
ReputationChange,
|
||||
};
|
||||
|
||||
use futures::{channel::oneshot, Stream};
|
||||
use prometheus_endpoint::Registry;
|
||||
|
||||
use pezsc_client_api::BlockBackend;
|
||||
use pezsc_network_common::{role::ObservedRole, ExHashT};
|
||||
pub use pezsc_network_types::{
|
||||
kad::{Key as KademliaKey, Record},
|
||||
multiaddr::Multiaddr,
|
||||
PeerId,
|
||||
};
|
||||
use pezsp_runtime::traits::Block as BlockT;
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub use libp2p::identity::SigningError;
|
||||
|
||||
/// 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>, metrics_registry: Option<Registry>) -> 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: 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: pezsc_network_types::PeerId,
|
||||
public_key: &Vec<u8>,
|
||||
signature: &Vec<u8>,
|
||||
message: &Vec<u8>,
|
||||
) -> Result<bool, String>;
|
||||
}
|
||||
|
||||
impl<T> NetworkSigner for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkSigner,
|
||||
{
|
||||
fn sign_with_local_identity(&self, msg: Vec<u8>) -> Result<Signature, SigningError> {
|
||||
T::sign_with_local_identity(self, msg)
|
||||
}
|
||||
|
||||
fn verify(
|
||||
&self,
|
||||
peer_id: pezsc_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.
|
||||
pub trait NetworkDHTProvider {
|
||||
/// Start finding closest peers to the target.
|
||||
fn find_closest_peers(&self, target: PeerId);
|
||||
|
||||
/// Start getting a value from the DHT.
|
||||
fn get_value(&self, key: &KademliaKey);
|
||||
|
||||
/// Start putting a value in the DHT.
|
||||
fn put_value(&self, key: KademliaKey, value: Vec<u8>);
|
||||
|
||||
/// Start putting the record to `peers`.
|
||||
///
|
||||
/// If `update_local_storage` is true the local storage is udpated as well.
|
||||
fn put_record_to(&self, record: Record, peers: HashSet<PeerId>, update_local_storage: bool);
|
||||
|
||||
/// Store a record in the DHT memory store.
|
||||
fn store_record(
|
||||
&self,
|
||||
key: KademliaKey,
|
||||
value: Vec<u8>,
|
||||
publisher: Option<PeerId>,
|
||||
expires: Option<Instant>,
|
||||
);
|
||||
|
||||
/// Register this node as a provider for `key` on the DHT.
|
||||
fn start_providing(&self, key: KademliaKey);
|
||||
|
||||
/// Deregister this node as a provider for `key` on the DHT.
|
||||
fn stop_providing(&self, key: KademliaKey);
|
||||
|
||||
/// Start getting the list of providers for `key` on the DHT.
|
||||
fn get_providers(&self, key: KademliaKey);
|
||||
}
|
||||
|
||||
impl<T> NetworkDHTProvider for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkDHTProvider,
|
||||
{
|
||||
fn find_closest_peers(&self, target: PeerId) {
|
||||
T::find_closest_peers(self, target)
|
||||
}
|
||||
|
||||
fn get_value(&self, key: &KademliaKey) {
|
||||
T::get_value(self, key)
|
||||
}
|
||||
|
||||
fn put_value(&self, key: KademliaKey, value: Vec<u8>) {
|
||||
T::put_value(self, key, value)
|
||||
}
|
||||
|
||||
fn put_record_to(&self, record: Record, peers: HashSet<PeerId>, update_local_storage: bool) {
|
||||
T::put_record_to(self, record, peers, update_local_storage)
|
||||
}
|
||||
|
||||
fn store_record(
|
||||
&self,
|
||||
key: KademliaKey,
|
||||
value: Vec<u8>,
|
||||
publisher: Option<PeerId>,
|
||||
expires: Option<Instant>,
|
||||
) {
|
||||
T::store_record(self, key, value, publisher, expires)
|
||||
}
|
||||
|
||||
fn start_providing(&self, key: KademliaKey) {
|
||||
T::start_providing(self, key)
|
||||
}
|
||||
|
||||
fn stop_providing(&self, key: KademliaKey) {
|
||||
T::stop_providing(self, key)
|
||||
}
|
||||
|
||||
fn get_providers(&self, key: KademliaKey) {
|
||||
T::get_providers(self, key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an ability to set a fork sync request for a particular block.
|
||||
pub trait NetworkSyncForkRequest<BlockHash, BlockNumber> {
|
||||
/// Notifies the sync service to try and sync the given block from the given
|
||||
/// peers.
|
||||
///
|
||||
/// If the given vector of peers is empty then the underlying implementation
|
||||
/// should make a best effort to fetch the block from any peers it is
|
||||
/// connected to (NOTE: this assumption will change in the future #3629).
|
||||
fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: BlockHash, number: BlockNumber);
|
||||
}
|
||||
|
||||
impl<T, BlockHash, BlockNumber> NetworkSyncForkRequest<BlockHash, BlockNumber> for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkSyncForkRequest<BlockHash, BlockNumber>,
|
||||
{
|
||||
fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: BlockHash, number: BlockNumber) {
|
||||
T::set_sync_fork_request(self, peers, hash, number)
|
||||
}
|
||||
}
|
||||
|
||||
/// Overview status of the network.
|
||||
#[derive(Clone)]
|
||||
pub struct NetworkStatus {
|
||||
/// Total number of connected peers.
|
||||
pub num_connected_peers: usize,
|
||||
/// The total number of bytes received.
|
||||
pub total_bytes_inbound: u64,
|
||||
/// The total number of bytes sent.
|
||||
pub total_bytes_outbound: u64,
|
||||
}
|
||||
|
||||
/// Provides high-level status information about network.
|
||||
#[async_trait::async_trait]
|
||||
pub trait NetworkStatusProvider {
|
||||
/// High-level network status information.
|
||||
///
|
||||
/// 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
|
||||
impl<T> NetworkStatusProvider for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkStatusProvider,
|
||||
{
|
||||
fn status<'life0, 'async_trait>(
|
||||
&'life0 self,
|
||||
) -> Pin<Box<dyn Future<Output = Result<NetworkStatus, ()>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
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.
|
||||
///
|
||||
/// Need a better solution to manage authorized peers, but now just use reserved peers for
|
||||
/// prototyping.
|
||||
fn set_authorized_peers(&self, peers: HashSet<PeerId>);
|
||||
|
||||
/// Set authorized_only flag.
|
||||
///
|
||||
/// Need a better solution to decide authorized_only, but now just use reserved_only flag for
|
||||
/// prototyping.
|
||||
fn set_authorized_only(&self, reserved_only: bool);
|
||||
|
||||
/// Adds an address known to a node.
|
||||
fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr);
|
||||
|
||||
/// Report a given peer as either beneficial (+) or costly (-) according to the
|
||||
/// given scalar.
|
||||
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange);
|
||||
|
||||
/// Get peer reputation.
|
||||
fn peer_reputation(&self, peer_id: &PeerId) -> i32;
|
||||
|
||||
/// Disconnect from a node as soon as possible.
|
||||
///
|
||||
/// This triggers the same effects as if the connection had closed itself spontaneously.
|
||||
fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName);
|
||||
|
||||
/// Connect to unreserved peers and allow unreserved peers to connect for syncing purposes.
|
||||
fn accept_unreserved_peers(&self);
|
||||
|
||||
/// Disconnect from unreserved peers and deny new unreserved peers to connect for syncing
|
||||
/// purposes.
|
||||
fn deny_unreserved_peers(&self);
|
||||
|
||||
/// Adds a `PeerId` and its `Multiaddr` as reserved for a sync protocol (default peer set).
|
||||
///
|
||||
/// Returns an `Err` if the given string is not a valid multiaddress
|
||||
/// or contains an invalid peer ID (which includes the local peer ID).
|
||||
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String>;
|
||||
|
||||
/// Removes a `PeerId` from the list of reserved peers for a sync protocol (default peer set).
|
||||
fn remove_reserved_peer(&self, peer_id: PeerId);
|
||||
|
||||
/// Sets the reserved set of a protocol to the given set of peers.
|
||||
///
|
||||
/// Each `Multiaddr` must end with a `/p2p/` component containing the `PeerId`. It can also
|
||||
/// consist of only `/p2p/<peerid>`.
|
||||
///
|
||||
/// The node will start establishing/accepting connections and substreams to/from peers in this
|
||||
/// set, if it doesn't have any substream open with them yet.
|
||||
///
|
||||
/// Note however, if a call to this function results in less peers on the reserved set, they
|
||||
/// will not necessarily get disconnected (depending on available free slots in the peer set).
|
||||
/// If you want to also disconnect those removed peers, you will have to call
|
||||
/// `remove_from_peers_set` on those in addition to updating the reserved set. You can omit
|
||||
/// this step if the peer set is in reserved only mode.
|
||||
///
|
||||
/// Returns an `Err` if one of the given addresses is invalid or contains an
|
||||
/// invalid peer ID (which includes the local peer ID), or if `protocol` does not
|
||||
/// refer to a known protocol.
|
||||
fn set_reserved_peers(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Add peers to a peer set.
|
||||
///
|
||||
/// Each `Multiaddr` must end with a `/p2p/` component containing the `PeerId`. It can also
|
||||
/// consist of only `/p2p/<peerid>`.
|
||||
///
|
||||
/// Returns an `Err` if one of the given addresses is invalid or contains an
|
||||
/// invalid peer ID (which includes the local peer ID), or if `protocol` does not
|
||||
/// refer to a know protocol.
|
||||
fn add_peers_to_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Remove peers from a peer set.
|
||||
///
|
||||
/// Returns `Err` if `protocol` does not refer to a known protocol.
|
||||
fn remove_peers_from_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: Vec<PeerId>,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Returns the number of peers in the sync peer set we're connected to.
|
||||
fn sync_num_connected(&self) -> usize;
|
||||
|
||||
/// Attempt to get peer role.
|
||||
///
|
||||
/// Right now the peer role is decoded from the received handshake for all protocols
|
||||
/// (`/block-announces/1` has other information as well). If the handshake cannot be
|
||||
/// 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,
|
||||
T: NetworkPeers,
|
||||
{
|
||||
fn set_authorized_peers(&self, peers: HashSet<PeerId>) {
|
||||
T::set_authorized_peers(self, peers)
|
||||
}
|
||||
|
||||
fn set_authorized_only(&self, reserved_only: bool) {
|
||||
T::set_authorized_only(self, reserved_only)
|
||||
}
|
||||
|
||||
fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr) {
|
||||
T::add_known_address(self, peer_id, addr)
|
||||
}
|
||||
|
||||
fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
|
||||
T::report_peer(self, peer_id, cost_benefit)
|
||||
}
|
||||
|
||||
fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
|
||||
T::peer_reputation(self, peer_id)
|
||||
}
|
||||
|
||||
fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName) {
|
||||
T::disconnect_peer(self, peer_id, protocol)
|
||||
}
|
||||
|
||||
fn accept_unreserved_peers(&self) {
|
||||
T::accept_unreserved_peers(self)
|
||||
}
|
||||
|
||||
fn deny_unreserved_peers(&self) {
|
||||
T::deny_unreserved_peers(self)
|
||||
}
|
||||
|
||||
fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
|
||||
T::add_reserved_peer(self, peer)
|
||||
}
|
||||
|
||||
fn remove_reserved_peer(&self, peer_id: PeerId) {
|
||||
T::remove_reserved_peer(self, peer_id)
|
||||
}
|
||||
|
||||
fn set_reserved_peers(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
T::set_reserved_peers(self, protocol, peers)
|
||||
}
|
||||
|
||||
fn add_peers_to_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: HashSet<Multiaddr>,
|
||||
) -> Result<(), String> {
|
||||
T::add_peers_to_reserved_set(self, protocol, peers)
|
||||
}
|
||||
|
||||
fn remove_peers_from_reserved_set(
|
||||
&self,
|
||||
protocol: ProtocolName,
|
||||
peers: Vec<PeerId>,
|
||||
) -> Result<(), String> {
|
||||
T::remove_peers_from_reserved_set(self, protocol, peers)
|
||||
}
|
||||
|
||||
fn sync_num_connected(&self) -> usize {
|
||||
T::sync_num_connected(self)
|
||||
}
|
||||
|
||||
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.
|
||||
pub trait NetworkEventStream {
|
||||
/// Returns a stream containing the events that happen on the network.
|
||||
///
|
||||
/// If this method is called multiple times, the events are duplicated.
|
||||
///
|
||||
/// The stream never ends (unless the `NetworkWorker` gets shut down).
|
||||
///
|
||||
/// The name passed is used to identify the channel in the Prometheus metrics. Note that the
|
||||
/// parameter is a `&'static str`, and not a `String`, in order to avoid accidentally having
|
||||
/// an unbounded set of Prometheus metrics, which would be quite bad in terms of memory
|
||||
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>>;
|
||||
}
|
||||
|
||||
impl<T> NetworkEventStream for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkEventStream,
|
||||
{
|
||||
fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>> {
|
||||
T::event_stream(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for providing information about the local network state
|
||||
pub trait NetworkStateInfo {
|
||||
/// Returns the local external addresses.
|
||||
fn external_addresses(&self) -> Vec<Multiaddr>;
|
||||
|
||||
/// Returns the listening addresses (without trailing `/p2p/` with our `PeerId`).
|
||||
fn listen_addresses(&self) -> Vec<Multiaddr>;
|
||||
|
||||
/// Returns the local Peer ID.
|
||||
fn local_peer_id(&self) -> PeerId;
|
||||
}
|
||||
|
||||
impl<T> NetworkStateInfo for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkStateInfo,
|
||||
{
|
||||
fn external_addresses(&self) -> Vec<Multiaddr> {
|
||||
T::external_addresses(self)
|
||||
}
|
||||
|
||||
fn listen_addresses(&self) -> Vec<Multiaddr> {
|
||||
T::listen_addresses(self)
|
||||
}
|
||||
|
||||
fn local_peer_id(&self) -> PeerId {
|
||||
T::local_peer_id(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserved slot in the notifications buffer, ready to accept data.
|
||||
pub trait NotificationSenderReady {
|
||||
/// Consumes this slots reservation and actually queues the notification.
|
||||
///
|
||||
/// NOTE: Traits can't consume itself, but calling this method second time will return an error.
|
||||
fn send(&mut self, notification: Vec<u8>) -> Result<(), NotificationSenderError>;
|
||||
}
|
||||
|
||||
/// A `NotificationSender` allows for sending notifications to a peer with a chosen protocol.
|
||||
#[async_trait::async_trait]
|
||||
pub trait NotificationSender: Send + Sync + 'static {
|
||||
/// Returns a future that resolves when the `NotificationSender` is ready to send a
|
||||
/// notification.
|
||||
async fn ready(&self)
|
||||
-> Result<Box<dyn NotificationSenderReady + '_>, NotificationSenderError>;
|
||||
}
|
||||
|
||||
/// 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 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.
|
||||
#[error("Protocol name hasn't been registered")]
|
||||
BadProtocol,
|
||||
}
|
||||
|
||||
/// Provides ability to send network requests.
|
||||
#[async_trait::async_trait]
|
||||
pub trait NetworkRequest {
|
||||
/// Sends a single targeted request to a specific peer. On success, returns the response of
|
||||
/// the peer.
|
||||
///
|
||||
/// Request-response protocols are a way to complement notifications protocols, but
|
||||
/// notifications should remain the default ways of communicating information. For example, a
|
||||
/// peer can announce something through a notification, after which the recipient can obtain
|
||||
/// more information by performing a request.
|
||||
/// As such, call this function with `IfDisconnected::ImmediateError` for `connect`. This way
|
||||
/// you will get an error immediately for disconnected peers, instead of waiting for a
|
||||
/// potentially very long connection attempt, which would suggest that something is wrong
|
||||
/// anyway, as you are supposed to be connected because of the notification protocol.
|
||||
///
|
||||
/// No limit or throttling of concurrent outbound requests per peer and protocol are enforced.
|
||||
/// Such restrictions, if desired, need to be enforced at the call site(s).
|
||||
///
|
||||
/// The protocol must have been registered through
|
||||
/// `NetworkConfiguration::request_response_protocols`.
|
||||
async fn request(
|
||||
&self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
connect: IfDisconnected,
|
||||
) -> Result<(Vec<u8>, ProtocolName), RequestFailure>;
|
||||
|
||||
/// Variation of `request` which starts a request whose response is delivered on a provided
|
||||
/// channel.
|
||||
///
|
||||
/// Instead of blocking and waiting for a reply, this function returns immediately, sending
|
||||
/// responses via the passed in sender. This alternative API exists to make it easier to
|
||||
/// integrate with message passing APIs.
|
||||
///
|
||||
/// Keep in mind that the connected receiver might receive a `Canceled` event in case of a
|
||||
/// closing connection. This is expected behaviour. With `request` you would get a
|
||||
/// `RequestFailure::Network(OutboundFailure::ConnectionClosed)` in that case.
|
||||
fn start_request(
|
||||
&self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
);
|
||||
}
|
||||
|
||||
// Manual implementation to avoid extra boxing here
|
||||
impl<T> NetworkRequest for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkRequest,
|
||||
{
|
||||
fn request<'life0, 'async_trait>(
|
||||
&'life0 self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
connect: IfDisconnected,
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<(Vec<u8>, ProtocolName), RequestFailure>>
|
||||
+ Send
|
||||
+ 'async_trait,
|
||||
>,
|
||||
>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
T::request(self, target, protocol, request, fallback_request, connect)
|
||||
}
|
||||
|
||||
fn start_request(
|
||||
&self,
|
||||
target: PeerId,
|
||||
protocol: ProtocolName,
|
||||
request: Vec<u8>,
|
||||
fallback_request: Option<(Vec<u8>, ProtocolName)>,
|
||||
tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
|
||||
connect: IfDisconnected,
|
||||
) {
|
||||
T::start_request(self, target, protocol, request, fallback_request, tx, connect)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides ability to announce blocks to the network.
|
||||
pub trait NetworkBlock<BlockHash, BlockNumber> {
|
||||
/// Make sure an important block is propagated to peers.
|
||||
///
|
||||
/// In chain-based consensus, we often need to make sure non-best forks are
|
||||
/// at least temporarily synced. This function forces such an announcement.
|
||||
fn announce_block(&self, hash: BlockHash, data: Option<Vec<u8>>);
|
||||
|
||||
/// Inform the network service about new best imported block.
|
||||
fn new_best_block_imported(&self, hash: BlockHash, number: BlockNumber);
|
||||
}
|
||||
|
||||
impl<T, BlockHash, BlockNumber> NetworkBlock<BlockHash, BlockNumber> for Arc<T>
|
||||
where
|
||||
T: ?Sized,
|
||||
T: NetworkBlock<BlockHash, BlockNumber>,
|
||||
{
|
||||
fn announce_block(&self, hash: BlockHash, data: Option<Vec<u8>>) {
|
||||
T::announce_block(self, hash, data)
|
||||
}
|
||||
|
||||
fn new_best_block_imported(&self, hash: BlockHash, number: BlockNumber) {
|
||||
T::new_best_block_imported(self, hash, number)
|
||||
}
|
||||
}
|
||||
|
||||
/// Substream acceptance result.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ValidationResult {
|
||||
/// Accept inbound substream.
|
||||
Accept,
|
||||
|
||||
/// Reject inbound substream.
|
||||
Reject,
|
||||
}
|
||||
|
||||
/// Substream direction.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
/// Substream opened by the remote node.
|
||||
Inbound,
|
||||
|
||||
/// Substream opened by the local node.
|
||||
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 {
|
||||
std::matches!(self, Direction::Inbound)
|
||||
}
|
||||
}
|
||||
|
||||
/// Events received by the protocol from `Notifications`.
|
||||
#[derive(Debug)]
|
||||
pub enum NotificationEvent {
|
||||
/// Validate inbound substream.
|
||||
ValidateInboundSubstream {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Received handshake.
|
||||
handshake: Vec<u8>,
|
||||
|
||||
/// `oneshot::Sender` for sending validation result back to `Notifications`
|
||||
result_tx: tokio::sync::oneshot::Sender<ValidationResult>,
|
||||
},
|
||||
|
||||
/// Remote identified by `PeerId` opened a substream and sent `Handshake`.
|
||||
/// Validate `Handshake` and report status (accept/reject) to `Notifications`.
|
||||
NotificationStreamOpened {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Is the substream inbound or outbound.
|
||||
direction: Direction,
|
||||
|
||||
/// Received handshake.
|
||||
handshake: Vec<u8>,
|
||||
|
||||
/// Negotiated fallback.
|
||||
negotiated_fallback: Option<ProtocolName>,
|
||||
},
|
||||
|
||||
/// Substream was closed.
|
||||
NotificationStreamClosed {
|
||||
/// Peer Id.
|
||||
peer: PeerId,
|
||||
},
|
||||
|
||||
/// Notification was received from the substream.
|
||||
NotificationReceived {
|
||||
/// Peer ID.
|
||||
peer: PeerId,
|
||||
|
||||
/// Received notification.
|
||||
notification: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Notification service
|
||||
///
|
||||
/// Defines behaviors that both the protocol implementations and `Notifications` can expect from
|
||||
/// each other.
|
||||
///
|
||||
/// `Notifications` can send two different kinds of information to protocol:
|
||||
/// * substream-related information
|
||||
/// * notification-related information
|
||||
///
|
||||
/// When an unvalidated, inbound substream is received by `Notifications`, it sends the inbound
|
||||
/// stream information (peer ID, handshake) to protocol for validation. Protocol must then verify
|
||||
/// that the handshake is valid (and in the future that it has a slot it can allocate for the peer)
|
||||
/// and then report back the `ValidationResult` which is either `Accept` or `Reject`.
|
||||
///
|
||||
/// After the validation result has been received by `Notifications`, it prepares the
|
||||
/// substream for communication by initializing the necessary sinks and emits
|
||||
/// `NotificationStreamOpened` which informs the protocol that the remote peer is ready to receive
|
||||
/// notifications.
|
||||
///
|
||||
/// Two different flavors of sending options are provided:
|
||||
/// * synchronous sending ([`NotificationService::send_sync_notification()`])
|
||||
/// * asynchronous sending ([`NotificationService::send_async_notification()`])
|
||||
///
|
||||
/// The former is used by the protocols not ready to exercise backpressure and the latter by the
|
||||
/// protocols that can do it.
|
||||
///
|
||||
/// Both local and remote peer can close the substream at any time. Local peer can do so by calling
|
||||
/// [`NotificationService::close_substream()`] which instructs `Notifications` to close the
|
||||
/// substream. Remote closing the substream is indicated to the local peer by receiving
|
||||
/// [`NotificationEvent::NotificationStreamClosed`] event.
|
||||
///
|
||||
/// In case the protocol must update its handshake while it's operating (such as updating the best
|
||||
/// block information), it can do so by calling [`NotificationService::set_handshake()`]
|
||||
/// which instructs `Notifications` to update the handshake it stored during protocol
|
||||
/// initialization.
|
||||
///
|
||||
/// All peer events are multiplexed on the same incoming event stream from `Notifications` and thus
|
||||
/// each event carries a `PeerId` so the protocol knows whose information to update when receiving
|
||||
/// an event.
|
||||
#[async_trait::async_trait]
|
||||
pub trait NotificationService: Debug + Send {
|
||||
/// Instruct `Notifications` to open a new substream for `peer`.
|
||||
///
|
||||
/// `dial_if_disconnected` informs `Notifications` whether to dial
|
||||
// the peer if there is currently no active connection to it.
|
||||
//
|
||||
// NOTE: not offered by the current implementation
|
||||
async fn open_substream(&mut self, peer: PeerId) -> Result<(), ()>;
|
||||
|
||||
/// Instruct `Notifications` to close substream for `peer`.
|
||||
//
|
||||
// NOTE: not offered by the current implementation
|
||||
async fn close_substream(&mut self, peer: PeerId) -> Result<(), ()>;
|
||||
|
||||
/// Send synchronous `notification` to `peer`.
|
||||
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(
|
||||
&mut self,
|
||||
peer: &PeerId,
|
||||
notification: Vec<u8>,
|
||||
) -> Result<(), error::Error>;
|
||||
|
||||
/// Set handshake for the notification protocol replacing the old handshake.
|
||||
async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()>;
|
||||
|
||||
/// Non-blocking variant of `set_handshake()` that attempts to update the handshake
|
||||
/// and returns an error if the channel is blocked.
|
||||
///
|
||||
/// Technically the function can return an error if the channel to `Notifications` is closed
|
||||
/// but that doesn't happen under normal operation.
|
||||
fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()>;
|
||||
|
||||
/// Get next event from the `Notifications` event stream.
|
||||
async fn next_event(&mut self) -> Option<NotificationEvent>;
|
||||
|
||||
/// Make a copy of the object so it can be shared between protocol components
|
||||
/// who wish to have access to the same underlying notification protocol.
|
||||
fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()>;
|
||||
|
||||
/// Get protocol name of the `NotificationService`.
|
||||
fn protocol(&self) -> &ProtocolName;
|
||||
|
||||
/// Get message sink of the peer.
|
||||
fn message_sink(&self, peer: &PeerId) -> Option<Box<dyn MessageSink>>;
|
||||
}
|
||||
|
||||
/// Message sink for peers.
|
||||
///
|
||||
/// If protocol cannot use [`NotificationService`] to send notifications to peers and requires,
|
||||
/// e.g., notifications to be sent in another task, the protocol may acquire a [`MessageSink`]
|
||||
/// object for each peer by calling [`NotificationService::message_sink()`]. Calling this
|
||||
/// function returns an object which allows the protocol to send notifications to the remote peer.
|
||||
///
|
||||
/// Use of this API is discouraged as it's not as performant as sending notifications through
|
||||
/// [`NotificationService`] due to synchronization required to keep the underlying notification
|
||||
/// sink up to date with possible sink replacement events.
|
||||
#[async_trait::async_trait]
|
||||
pub trait MessageSink: Send + Sync {
|
||||
/// Send synchronous `notification` to the peer associated with this [`MessageSink`].
|
||||
fn send_sync_notification(&self, notification: Vec<u8>);
|
||||
|
||||
/// Send an asynchronous `notification` to to the peer associated with this [`MessageSink`],
|
||||
/// allowing sender to exercise backpressure.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Transport that serves as a common ground for all connections.
|
||||
|
||||
use either::Either;
|
||||
use libp2p::{
|
||||
core::{
|
||||
muxing::StreamMuxerBox,
|
||||
transport::{Boxed, OptionalTransport},
|
||||
upgrade,
|
||||
},
|
||||
dns, identity, noise, tcp, websocket, PeerId, Transport, TransportExt,
|
||||
};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
// TODO: Create a wrapper similar to upstream `BandwidthTransport` that tracks sent/received bytes
|
||||
#[allow(deprecated)]
|
||||
pub use libp2p::bandwidth::BandwidthSinks;
|
||||
|
||||
/// Builds the transport that serves as a common ground for all connections.
|
||||
///
|
||||
/// If `memory_only` is true, then only communication within the same process are allowed. Only
|
||||
/// addresses with the format `/memory/...` are allowed.
|
||||
///
|
||||
/// Returns a `BandwidthSinks` object that allows querying the average bandwidth produced by all
|
||||
/// the connections spawned with this transport.
|
||||
#[allow(deprecated)]
|
||||
pub fn build_transport(
|
||||
keypair: identity::Keypair,
|
||||
memory_only: bool,
|
||||
) -> (Boxed<(PeerId, StreamMuxerBox)>, Arc<BandwidthSinks>) {
|
||||
// Build the base layer of the transport.
|
||||
let transport = if !memory_only {
|
||||
// Main transport: DNS(TCP)
|
||||
let tcp_config = tcp::Config::new().nodelay(true);
|
||||
let tcp_trans = tcp::tokio::Transport::new(tcp_config.clone());
|
||||
let dns_init = dns::tokio::Transport::system(tcp_trans);
|
||||
|
||||
Either::Left(if let Ok(dns) = dns_init {
|
||||
// WS + WSS transport
|
||||
//
|
||||
// Main transport can't be used for `/wss` addresses because WSS transport needs
|
||||
// unresolved addresses (BUT WSS transport itself needs an instance of DNS transport to
|
||||
// resolve and dial addresses).
|
||||
let tcp_trans = tcp::tokio::Transport::new(tcp_config);
|
||||
let dns_for_wss = dns::tokio::Transport::system(tcp_trans)
|
||||
.expect("same system_conf & resolver to work");
|
||||
Either::Left(websocket::WsConfig::new(dns_for_wss).or_transport(dns))
|
||||
} else {
|
||||
// In case DNS can't be constructed, fallback to TCP + WS (WSS won't work)
|
||||
let tcp_trans = tcp::tokio::Transport::new(tcp_config.clone());
|
||||
let desktop_trans = websocket::WsConfig::new(tcp_trans)
|
||||
.or_transport(tcp::tokio::Transport::new(tcp_config));
|
||||
Either::Right(desktop_trans)
|
||||
})
|
||||
} else {
|
||||
Either::Right(OptionalTransport::some(libp2p::core::transport::MemoryTransport::default()))
|
||||
};
|
||||
|
||||
let authentication_config = noise::Config::new(&keypair).expect("Can create noise config. qed");
|
||||
let multiplexing_config = libp2p::yamux::Config::default();
|
||||
|
||||
let transport = transport
|
||||
.upgrade(upgrade::Version::V1Lazy)
|
||||
.authenticate(authentication_config)
|
||||
.multiplex(multiplexing_config)
|
||||
.timeout(Duration::from_secs(20))
|
||||
.boxed();
|
||||
|
||||
transport.with_bandwidth_logging()
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `sc-network` type definitions
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// The protocol name transmitted on the wire.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProtocolName {
|
||||
/// The protocol name as a static string.
|
||||
Static(&'static str),
|
||||
/// The protocol name as a dynamically allocated string.
|
||||
OnHeap(Arc<str>),
|
||||
}
|
||||
|
||||
impl From<&'static str> for ProtocolName {
|
||||
fn from(name: &'static str) -> Self {
|
||||
Self::Static(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<str>> for ProtocolName {
|
||||
fn from(name: Arc<str>) -> Self {
|
||||
Self::OnHeap(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ProtocolName {
|
||||
fn from(name: String) -> Self {
|
||||
Self::OnHeap(Arc::from(name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ProtocolName {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &str {
|
||||
match self {
|
||||
Self::Static(name) => name,
|
||||
Self::OnHeap(name) => &name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for ProtocolName {
|
||||
fn borrow(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ProtocolName {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(self as &str) == (other as &str)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ProtocolName {}
|
||||
|
||||
impl Hash for ProtocolName {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
(self as &str).hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProtocolName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ProtocolName {
|
||||
fn as_ref(&self) -> &str {
|
||||
self as &str
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtocolName> for litep2p::ProtocolName {
|
||||
fn from(protocol: ProtocolName) -> Self {
|
||||
match protocol {
|
||||
ProtocolName::Static(inner) => litep2p::ProtocolName::from(inner),
|
||||
ProtocolName::OnHeap(inner) => litep2p::ProtocolName::from(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<litep2p::ProtocolName> for ProtocolName {
|
||||
fn from(protocol: litep2p::ProtocolName) -> Self {
|
||||
match protocol {
|
||||
litep2p::ProtocolName::Static(protocol) => ProtocolName::from(protocol),
|
||||
litep2p::ProtocolName::Allocated(protocol) => ProtocolName::from(protocol),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ProtocolName;
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn protocol_name_keys_are_equivalent_to_str_keys() {
|
||||
const PROTOCOL: &'static str = "/some/protocol/1";
|
||||
let static_protocol_name = ProtocolName::from(PROTOCOL);
|
||||
let on_heap_protocol_name = ProtocolName::from(String::from(PROTOCOL));
|
||||
|
||||
assert_eq!(<ProtocolName as Borrow<str>>::borrow(&static_protocol_name), PROTOCOL);
|
||||
assert_eq!(<ProtocolName as Borrow<str>>::borrow(&on_heap_protocol_name), PROTOCOL);
|
||||
assert_eq!(static_protocol_name, on_heap_protocol_name);
|
||||
|
||||
assert_eq!(hash(static_protocol_name), hash(PROTOCOL));
|
||||
assert_eq!(hash(on_heap_protocol_name), hash(PROTOCOL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_protocol_names_do_not_compare_equal() {
|
||||
const PROTOCOL1: &'static str = "/some/protocol/1";
|
||||
let static_protocol_name1 = ProtocolName::from(PROTOCOL1);
|
||||
let on_heap_protocol_name1 = ProtocolName::from(String::from(PROTOCOL1));
|
||||
|
||||
const PROTOCOL2: &'static str = "/some/protocol/2";
|
||||
let static_protocol_name2 = ProtocolName::from(PROTOCOL2);
|
||||
let on_heap_protocol_name2 = ProtocolName::from(String::from(PROTOCOL2));
|
||||
|
||||
assert_ne!(<ProtocolName as Borrow<str>>::borrow(&static_protocol_name1), PROTOCOL2);
|
||||
assert_ne!(<ProtocolName as Borrow<str>>::borrow(&on_heap_protocol_name1), PROTOCOL2);
|
||||
assert_ne!(static_protocol_name1, static_protocol_name2);
|
||||
assert_ne!(static_protocol_name1, on_heap_protocol_name2);
|
||||
assert_ne!(on_heap_protocol_name1, on_heap_protocol_name2);
|
||||
}
|
||||
|
||||
fn hash<T: Hash>(x: T) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
x.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program 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.
|
||||
|
||||
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! `sc-network` utilities
|
||||
|
||||
use futures::{stream::unfold, FutureExt, Stream, StreamExt};
|
||||
use futures_timer::Delay;
|
||||
use linked_hash_set::LinkedHashSet;
|
||||
|
||||
use std::{hash::Hash, num::NonZeroUsize, time::Duration};
|
||||
|
||||
/// Creates a stream that returns a new value every `duration`.
|
||||
pub fn interval(duration: Duration) -> impl Stream<Item = ()> + Unpin {
|
||||
unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop)
|
||||
}
|
||||
|
||||
/// Wrapper around `LinkedHashSet` with bounded growth.
|
||||
///
|
||||
/// In the limit, for each element inserted the oldest existing element will be removed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LruHashSet<T: Hash + Eq> {
|
||||
set: LinkedHashSet<T>,
|
||||
limit: NonZeroUsize,
|
||||
}
|
||||
|
||||
impl<T: Hash + Eq> LruHashSet<T> {
|
||||
/// Create a new `LruHashSet` with the given (exclusive) limit.
|
||||
pub fn new(limit: NonZeroUsize) -> Self {
|
||||
Self { set: LinkedHashSet::new(), limit }
|
||||
}
|
||||
|
||||
/// Insert element into the set.
|
||||
///
|
||||
/// Returns `true` if this is a new element to the set, `false` otherwise.
|
||||
/// Maintains the limit of the set by removing the oldest entry if necessary.
|
||||
/// Inserting the same element will update its LRU position.
|
||||
pub fn insert(&mut self, e: T) -> bool {
|
||||
if self.set.insert(e) {
|
||||
if self.set.len() == usize::from(self.limit) {
|
||||
self.set.pop_front(); // remove oldest entry
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn maintains_limit() {
|
||||
let three = NonZeroUsize::new(3).unwrap();
|
||||
let mut set = LruHashSet::<u8>::new(three);
|
||||
|
||||
// First element.
|
||||
assert!(set.insert(1));
|
||||
assert_eq!(vec![&1], set.set.iter().collect::<Vec<_>>());
|
||||
|
||||
// Second element.
|
||||
assert!(set.insert(2));
|
||||
assert_eq!(vec![&1, &2], set.set.iter().collect::<Vec<_>>());
|
||||
|
||||
// Inserting the same element updates its LRU position.
|
||||
assert!(!set.insert(1));
|
||||
assert_eq!(vec![&2, &1], set.set.iter().collect::<Vec<_>>());
|
||||
|
||||
// We reached the limit. The next element forces the oldest one out.
|
||||
assert!(set.insert(3));
|
||||
assert_eq!(vec![&1, &3], set.set.iter().collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user