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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
+451
View File
@@ -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"));
}
+997
View File
@@ -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
+90
View File
@@ -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)
}
}
+123
View File
@@ -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)>,
},
}
+310
View File
@@ -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:?}"),
}
}
+73
View File
@@ -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,
}
}
}
+664
View File
@@ -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
}
}
+573
View File
@@ -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);
}
}
+410
View File
@@ -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()
}
+162
View File
@@ -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()
}
}
+88
View File
@@ -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<_>>());
}
}