From ad49b52495e36fc9ca90440fac239363c2540087 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Fri, 3 May 2024 16:40:06 +0100 Subject: [PATCH 01/36] chore: complete request_reponse impementation --- client/src/main.rs | 30 +++- swarm_nl/Cargo.toml | 6 +- swarm_nl/src/{core.rs => core/mod.rs} | 240 +++++++++++++++++++++----- swarm_nl/src/core/prelude.rs | 201 +++++++++++++++++++++ swarm_nl/src/prelude.rs | 71 +------- 5 files changed, 428 insertions(+), 120 deletions(-) rename swarm_nl/src/{core.rs => core/mod.rs} (81%) create mode 100644 swarm_nl/src/core/prelude.rs diff --git a/client/src/main.rs b/client/src/main.rs index f6261f96c..f205178ea 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -68,7 +68,7 @@ mod age_of_empires { struct Empire { soldiers: u32, farmers: u32, - black_smith: u32, + blacksmith: u32, land_mass: u32, gold_reserve: u32 } @@ -85,7 +85,7 @@ mod age_of_empires { _established_in: Duration, mut application_sender: Sender ) { - // We want to only merge with empired that are richer than us. + // We want to only ally with empires that are richer than us. // We we connect, we ask for their reserve and make sure they are richer, // Then we collect a fee of 200 gold coins @@ -97,7 +97,7 @@ mod age_of_empires { // if total_coins_from_peer > self.gold_reserve { - // // ask for merging fee ReqRes::AskForMergingFee + // // ask for alignment fee ReqRes::AskForMergingFee // let m_fee = 100; // // add to our gold reserve @@ -105,9 +105,10 @@ mod age_of_empires { // // deal complete // } else { - // // we dont merge with broke empires + // // we dont ally with broke empires // // disconnect (This is a network operation) // } + } } @@ -122,7 +123,7 @@ mod age_of_empires { let spartan_empire = Empire { soldiers: 1000, farmers: 1000, - black_smith: 1000, + blacksmith: 1000, land_mass: 1000, gold_reserve: 1000, }; @@ -134,6 +135,25 @@ mod age_of_empires { .unwrap(); network.application_sender.try_send(StreamData::ReqRes("gettotalcoins".as_bytes().to_vec())).await; + + let data = network.send_to_network_layer(get_coins(89)).await; //-> 3 seconds + + // Do many task + + let coin = network.recv_from_network_layer(stream_id).await; // no waiting + + // use coin + + + let data = fetch_form_network_layer(Coins).await; + + let coin = fetch_from_network_layer = { + let data = network.send_to_network_layer(get_coins(89)).await; // -> 3 seconds + let coin = network.recv_from_network_layer(stream_id).await; // -> 45 seconds + + coin + }; + } } diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index df91afb73..cfe0720c9 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -9,12 +9,10 @@ edition = "2021" rust-ini = "0.20.0" thiserror = "1.0.58" rand = "0.8.5" -libp2p = { version="0.53.2", "features"=["ping", "tokio", "async-std", "macros", "ping", "async-std", "tokio", "tcp", "noise", "yamux", "quic", "tls", "dns", "kad", "identify"] } +libp2p = { version="0.53.2", "features"=["async-std", "macros", "ping", "tokio", "tcp", "noise", "yamux", "quic", "tls", "dns", "kad", "identify", "request-response", "cbor"] } libp2p-identity = { version="0.2.8", "features"=["secp256k1", "ecdsa", "rsa", "ed25519"] } futures = "0.3.30" -futures-timer = "3.0.3" -void = "1.0.2" -tracing = "0.1.40" +futures-time = "3.0.0" [dependencies.async-std] version = "1.12.0" diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core/mod.rs similarity index 81% rename from swarm_nl/src/core.rs rename to swarm_nl/src/core/mod.rs index d39e25667..c2da8bce2 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core/mod.rs @@ -1,10 +1,11 @@ -//! Core data structures and protocol implementations for building a swarm. - +/// Copyright (c) 2024 Algorealm +/// Core data structures and protocol implementations for building a swarm. use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, io::Error, net::{IpAddr, Ipv4Addr}, num::NonZeroU32, + sync::Arc, time::Duration, }; @@ -12,9 +13,12 @@ use futures::{ channel::mpsc::{self, Receiver, Sender}, select, SinkExt, StreamExt, }; +use futures_time::prelude::*; +use futures_time::time::Duration as AsyncDuration; use libp2p::{ identify::{self, Info}, kad::{self, store::MemoryStore, Record}, + request_response, multiaddr::{self, Protocol}, noise, ping::{self, Failure}, @@ -26,6 +30,15 @@ use super::*; use crate::setup::BootstrapConfig; use ping_config::*; +#[cfg(feature = "async-std-runtime")] +use async_std::sync::Mutex; + +#[cfg(feature = "tokio-runtime")] +use tokio::sync::Mutex; + +mod prelude; +use prelude::*; + /// The Core Behaviour implemented which highlights the various protocols /// we'll be adding support for #[derive(NetworkBehaviour)] @@ -34,6 +47,7 @@ struct CoreBehaviour { ping: ping::Behaviour, kademlia: kad::Behaviour, identify: identify::Behaviour, + request_response: request_response::cbor::Behaviour, } /// Network events generated as a result of supported and configured `NetworkBehaviour`'s @@ -42,6 +56,7 @@ enum CoreEvent { Ping(ping::Event), Kademlia(kad::Event), Identify(identify::Event), + RequestResponse(request_response::Event), } /// Implement ping events for [`CoreEvent`] @@ -65,6 +80,13 @@ impl From for CoreEvent { } } +/// Implement request_response events for [`CoreEvent`] +impl From> for CoreEvent { + fn from(event: request_response::Event) -> Self { + CoreEvent::RequestResponse(event) + } +} + /// Structure containing necessary data to build [`Core`] pub struct CoreBuilder { network_id: StreamProtocol, @@ -73,6 +95,11 @@ pub struct CoreBuilder { boot_nodes: HashMap, /// the network event handler handler: T, + /// Prevents blocking forever due to absence of expected data from the network layer + network_read_delay: AsyncDuration, + /// The size of the stream buffers to use to track application requests to the network layer + /// internally. + stream_size: usize, ip_address: IpAddr, /// Connection keep-alive duration while idle keep_alive_duration: Seconds, @@ -121,6 +148,9 @@ impl CoreBuilder { tcp_udp_port: config.ports(), boot_nodes: config.bootnodes(), handler, + // Timeout defaults to 60 seconds + network_read_delay: AsyncDuration::from_secs(NETWORK_READ_TIMEOUT), + stream_size: usize::MAX, // Default is to listen on all interfaces (ipv4) ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), // Default to 60 seconds @@ -155,6 +185,17 @@ impl CoreBuilder { CoreBuilder { ip_address, ..self } } + /// Configure the timeout for requests to read from the network layer. + /// Reading from the network layer could potentially block if the data corresponding to the + /// [`StreamId`] specified could not be found (or has been read already). This prevents the + /// future from `await`ing forever. Defaults to 60 seconds + pub fn with_network_read_delay(self, network_read_delay: AsyncDuration) -> Self { + CoreBuilder { + network_read_delay, + ..self + } + } + /// Configure how long to keep a connection alive (in seconds) once it is idling. pub fn with_idle_connection_timeout(self, keep_alive_duration: Seconds) -> Self { CoreBuilder { @@ -163,6 +204,16 @@ impl CoreBuilder { } } + /// Configure the size of the stream buffers to use to track application requests to the network + /// layer internally. This should be as large an possible to prevent dropping of requests to the + /// network layer. Defaults to [`usize::MAX`] + pub fn with_stream_size(self, size: usize) -> Self { + CoreBuilder { + stream_size: size, + ..self + } + } + /// Configure the `Ping` protocol for the network. pub fn with_ping(self, config: PingConfig) -> Self { // Set the ping protocol @@ -424,7 +475,7 @@ impl CoreBuilder { // application and the application will comsume it (single consumer) The second stream // will have SwarmNl (being the consumer) recieve data and commands from multiple areas // in the application; - let (mut network_sender, application_receiver) = mpsc::channel::(3); + let (_, application_receiver) = mpsc::channel::(3); let (application_sender, network_receiver) = mpsc::channel::(3); // Set up the ping network info. @@ -464,19 +515,27 @@ impl CoreBuilder { keypair: self.keypair, application_sender, application_receiver, + stream_request_buffer: Arc::new(Mutex::new(StreamRequestBuffer { + size: self.stream_size, + buffer: Default::default(), + })), + stream_response_buffer: Arc::new(Mutex::new(StreamResponseBuffer { + size: self.stream_size, + buffer: Default::default(), + })), + network_read_delay: self.network_read_delay, + current_stream_id: StreamId::new(), }; - // Send message to application to indicate readiness - let _ = network_sender.send(StreamData::Ready).await; - // Spin up task to handle async operations and data on the network. #[cfg(feature = "async-std-runtime")] async_std::task::spawn(Core::handle_async_operations( swarm, network_info, - network_sender, self.handler, network_receiver, + network_core.stream_request_buffer.clone(), + network_core.stream_response_buffer.clone(), )); // Spin up task to handle async operations and data on the network. @@ -484,9 +543,10 @@ impl CoreBuilder { tokio::task::spawn(Core::handle_async_operations( swarm, network_info, - network_sender, self.handler, network_receiver, + network_core.stream_request_buffer.clone(), + network_core.stream_response_buffer.clone(), )); Ok(network_core) @@ -498,9 +558,20 @@ pub struct Core { keypair: Keypair, /// The producing end of the stream that sends data to the network layer from the /// application - pub application_sender: Sender, + application_sender: Sender, /// The consuming end of the stream that recieves data from the network layer - pub application_receiver: Receiver, + application_receiver: Receiver, + /// This serves as a buffer for the results of the requests to the network layer. + /// With this, applications can make async requests and fetch their results at a later time + /// without waiting. This is made possible by storing a [`StreamId`] for a particular stream + /// request. + stream_response_buffer: Arc>, + /// Store a [`StreamId`] representing a network request + stream_request_buffer: Arc>, + /// The network read timeout + network_read_delay: AsyncDuration, + /// Current stream id. Useful for opening new streams, we just have to bump the number by 1 + current_stream_id: StreamId, } impl Core { @@ -529,20 +600,105 @@ impl Core { false } + /// Send data to the network layer and recieve a unique `StreamId` to track the request + /// If the internal stream buffer is full, `None` will be returned. + pub async fn send_to_network(&mut self, request: AppData) -> Option { + // Generate stream id + let stream_id = StreamId::next(self.current_stream_id); + let request = StreamData::Application(stream_id, request); + + // Add to request buffer + if !self.stream_request_buffer.lock().await.insert(stream_id) { + // Buffer appears to be full + return None; + } + + // Send request + self.application_sender.send(request).await; + + // Store latest stream id + self.current_stream_id = stream_id; + + Some(stream_id) + } + + /// This is for intra-network communication + async fn notify_network(&mut self, request: NetworkData) -> Option { + // Generate stream id + let stream_id = StreamId::next(self.current_stream_id); + let request = StreamData::Network(stream_id, request); + + // Add to request buffer + if !self.stream_request_buffer.lock().await.insert(stream_id) { + // Buffer appears to be full + return None; + } + + // Send request + self.application_sender.send(request).await; + + // Store latest stream id + self.current_stream_id = stream_id; + + Some(stream_id) + } + + /// Explicitly rectrieve the reponse to a request sent to the network layer. + /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay + /// and read immediately as the response to the request should already be in the stream response + /// buffer. + pub async fn recv_from_network(&mut self, stream_id: StreamId) -> NetworkResult { + let network_data_future = async { + loop { + // tight loop + if let Some(response_value) = self.stream_response_buffer.lock().await.remove(&stream_id) { + return response_value; + } + } + }; + + Ok(network_data_future + .timeout(self.network_read_delay) + .await + .map_err(|_| NetworkError::NetworkReadTimeout)?) + } + + /// Perform an atomic `send` and `recieve` from the network layer. This function is atomic and + /// blocks until the result of the request is returned from the network layer. This function + /// should mostly be used when the result of the request is needed immediately and delay can be + /// condoned. It will still timeout if the delay exceeds the configured period. + /// If the internal buffer is full, it will return an error. + pub async fn fetch_from_network(&mut self, request: AppData) -> NetworkResult { + // send request + if let Some(stream_id) = self.send_to_network(request).await { + // wait to recieve response from the network + self.recv_from_network(stream_id).await + } else { + Err(NetworkError::StreamBufferOverflow) + } + } + + /// This is for intra-network atomic communication + async fn trigger_network_op(&mut self, request: NetworkData) -> NetworkResult { + // send request + if let Some(stream_id) = self.notify_network(request).await { + // wait to recieve response from the network + self.recv_from_network(stream_id).await + } else { + Err(NetworkError::StreamBufferOverflow) + } + } + /// Return the node's `PeerId` pub fn peer_id(&self) -> String { self.keypair.public().to_peer_id().to_string() } /// Explicitly dial a peer at runtime. - /// We will trigger the dial by sending a message into the stream. This is an intra-network - /// layer communication, multiplexed over the undelying open stream. - pub async fn dial_peer(&mut self, multiaddr: String) { + pub async fn dial_peer(&mut self, multiaddr: String) -> NetworkResult { // send message into stream - let _ = self - .application_sender // `application_sender` is being used here to speak to the network layer (itself) - .send(StreamData::Network(NetworkData::DailPeer(multiaddr))) - .await; + self.trigger_network_op(NetworkData::DailPeer(multiaddr)) + .await } /// Handle async operations, which basically involved handling two major data sources: @@ -552,24 +708,26 @@ impl Core { async fn handle_async_operations( mut swarm: Swarm, mut network_info: NetworkInfo, - mut sender: Sender, mut handler: T, mut receiver: Receiver, + mut stream_request_buffer: Arc>, + mut stream_response_buffer: Arc>, ) { // Loop to handle incoming application streams indefinitely. loop { select! { // handle incoming stream data stream_data = receiver.select_next_some() => match stream_data { - // Not handled - StreamData::Ready => {} - // Put back into the stream what we read from it - StreamData::Echo(message) => { - // Echo message back into stream - let _ = sender.send(StreamData::Echo(message)).await; - } - StreamData::Application(app_data) => { + StreamData::Application(stream_id, app_data) => { match app_data { + // Put back into the stream what we read from it + AppData::Echo(message) => { + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + + // Insert into the response buffer for it to be picked up + stream_response_buffer.lock().await.insert(stream_id, Box::new(message)); + }, // Store a value in the DHT and (optionally) on explicit specific peers AppData::KademliaStoreRecord { key,value,expiration_time, explicit_peers } => { // create a kad record @@ -588,7 +746,7 @@ impl Core { PeerId::from_bytes(peer_id_string.as_bytes()) }).filter_map(Result::ok).collect::>(); - let _ = swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); + swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); } }, // Perform a lookup in the DHT @@ -609,25 +767,30 @@ impl Core { } // Return important routing table info AppData::KademliaGetRoutingTableInfo => { - // send information - let _ = sender.send(StreamData::Network(NetworkData::KademliaDhtInfo { protocol_id: network_info.id.to_string() })).await; + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + + // Insert into the response buffer for it to be picked up + stream_response_buffer.lock().await.insert(stream_id, Box::new(Kademlia::Info { protocol_id: network_info.id.to_string() })); }, // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { // inform the swarm to make the request - + } } } - StreamData::Network(network_data) => { + StreamData::Network(stream_id, network_data) => { match network_data { // Dail peer NetworkData::DailPeer(multiaddr) => { + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { - let _ = swarm.dial(multiaddr); + swarm.dial(multiaddr); } } - // Ignore the remaining network messages, they'll never come _ => {} } } @@ -991,7 +1154,7 @@ pub trait EventHandler { _num_established: NonZeroU32, _concurrent_dial_errors: Option)>>, _established_in: Duration, - application_sender: Sender + application_sender: Sender, ) { // Default implementation } @@ -1170,10 +1333,3 @@ mod ping_config { pub manager: PingManager, } } - -mod tests { - - #[cfg(test)] - fn test() {} - -} \ No newline at end of file diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs new file mode 100644 index 000000000..97ada9b86 --- /dev/null +++ b/swarm_nl/src/core/prelude.rs @@ -0,0 +1,201 @@ +use rand::random; +/// Copyright (c) 2024 Algorealm +use std::time::Instant; +use thiserror::Error; + +use super::*; + +/// Type to indicate the duration (in seconds) to wait for data from the network layer before timing +/// out +pub const NETWORK_READ_TIMEOUT: u64 = 60; + +/// Data exchanged over a stream between the application and network layer +pub(super) enum StreamData { + /// Application data sent over the stream + Application(StreamId, AppData), + /// Network data sent over the stream + Network(StreamId, NetworkData), +} + +/// Data sent from the application layer to the networking layer +#[derive(Debug)] +pub enum AppData { + /// A simple echo message + Echo(String), + /// Store a value associated with a given key in the Kademlia DHT + KademliaStoreRecord { + key: Vec, + value: Vec, + // expiration time for local records + expiration_time: Option, + // store on explicit peers + explicit_peers: Option>, + }, + /// Perform a lookup of a value associated with a given key in the Kademlia DHT + KademliaLookupRecord { key: Vec }, + /// Perform a lookup of peers that store a record + KademliaGetProviders { key: Vec }, + /// Stop providing a record on the network + KademliaStopProviding { key: Vec }, + /// Remove record from local store + KademliaDeleteRecord { key: Vec }, + /// Return important information about the local routing table + KademliaGetRoutingTableInfo, + /// Fetch data(s) quickly from a peer over the network + FetchData { keys: Vec, peer: PeerId }, + // Get network information + // Gossip related requests +} + +/// Data sent from the networking to itself +pub(super) enum NetworkData { + /// A simple echo message + Echo(StreamId, String), + /// Dail peer + DailPeer(MultiaddrString), +} + +/// Results from Kademlia DHT operation +pub enum Kademlia { + /// Return important information about the DHT, this will be increased shortly + Info { protocol_id: String }, + /// Return the result of a DHT lookup operation + Result(DhtOps), +} + +/// Network error type containing errors encountered during network operations +#[derive(Error, Debug)] +pub enum NetworkError { + #[error("timeout occured waiting for data from network layer")] + NetworkReadTimeout, + #[error("internal request stream buffer is full")] + StreamBufferOverflow, +} + +/// Operations performed on the Kademlia DHT +#[derive(Debug)] +pub enum DhtOps { + /// Value found in the DHT + RecordFound { key: Vec, value: Vec }, + /// No value found + RecordNotFound, + /// Nodes found that provide value for key + ProvidersFound { + key: Vec, + providers: Vec, + }, + /// No providers returned (This is most likely due to an error) + NoProvidersFound, +} + +/// A simple struct used to track requests sent from the application layer to the network layer +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct StreamId(u32); + +impl StreamId { + /// Generate a new random stream id. + /// Must only be called once + pub fn new() -> Self { + StreamId(random()) + } + + /// Generate a new random stream id, using the current as guide + pub fn next(current_id: StreamId) -> Self { + StreamId(current_id.0.wrapping_add(1)) + } +} + +/// Type that specifies the result of querying the network layer +pub type NetworkResult = Result, NetworkError>; + +/// Marker trait that indicates a stream reponse data object +pub trait StreamResponseType +where + Self: Send + Sync + 'static, +{ +} + +/// Macro that implements [`StreamResponseType`] for various types to occupy the +/// [`StreamResponseBuffer`] +macro_rules! impl_stream_response_for_types { ( $( $t:ident )* ) => { + $( + impl StreamResponseType for $t {} + )* }; +} + +impl_stream_response_for_types!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 + usize isize f32 f64 String bool Kademlia); + +/// Type that keeps track of the requests from the application layer. +/// This type has a maximum buffer size and will drop subsequent requests when full. +/// It is unlikely to be ever full as the default is usize::MAX except otherwise specified during +/// configuration. It is always good practice to read responses from the internal stream buffer +/// using `fetch_from_network()` or explicitly using `recv_from_network` +#[derive(Clone)] +pub(super) struct StreamRequestBuffer { + /// Max requests we can keep track of + size: usize, + buffer: HashSet, +} + +impl StreamRequestBuffer { + /// Create a new request buffer + pub fn new(buffer_size: usize) -> Self { + Self { + size: buffer_size, + buffer: HashSet::new(), + } + } + + /// Push [`StreamId`]s into buffer. + /// Returns `false` if the buffer is full and request cannot be stored + pub fn insert(&mut self, id: StreamId) -> bool { + if self.buffer.len() < self.size { + self.buffer.insert(id); + return true; + } + false + } + + /// Remove a [`StreamId`] from the buffer + pub fn remove(&mut self, id: &StreamId) { + self.buffer.remove(&id); + } +} + +/// Type that keeps track of the response to the requests from the application layer. +pub(super) struct StreamResponseBuffer { + /// Max responses we can keep track of + size: usize, + buffer: HashMap>, +} + +impl StreamResponseBuffer { + /// Create a new request buffer + pub fn new(buffer_size: usize) -> Self { + Self { + size: buffer_size, + buffer: HashMap::new(), + } + } + + /// Push [`StreamId`]s into buffer. + /// Returns `false` if the buffer is full and request cannot be stored + pub fn insert(&mut self, id: StreamId, response: Box) -> bool { + if self.buffer.len() < self.size { + self.buffer.insert(id, response); + return true; + } + false + } + + /// Remove a [`StreamId`] from the buffer + pub fn remove(&mut self, id: &StreamId) -> Option> { + self.buffer.remove(&id) + } + + /// Check if buffer contains a value + pub fn contains(&mut self, id: &StreamId) -> bool { + self.buffer.contains_key(id) + } +} diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index cd74d4e28..12aa0015a 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -1,9 +1,9 @@ -use libp2p_identity::{KeyType, PeerId}; /// Copyright (c) 2024 Algorealm /// /// This file is part of the SwarmNL library. -use std::time::Instant; use thiserror::Error; +use libp2p_identity::KeyType; + /// Library error type containing all custom errors that could be encountered #[derive(Error, Debug)] @@ -92,70 +92,3 @@ pub struct NotInitialiazed; /// A unique type that indicates that a struct has been default configured pub struct Initialized; - -/// Data exchanged over a stream between the application and network layer -#[derive(Debug)] -pub enum StreamData { - /// This is the first message sent through the stream from the networking layer to the - /// application. It indicates a successful setup and readiness to begin operations. - Ready, - /// A simple echo message - Echo(String), - /// Application data sent over the stream - Application(AppData), - /// Network data sent over the stream - Network(NetworkData), -} - -/// Data sent from the application layer to the networking layer -#[derive(Debug)] -pub enum AppData { - /// Store a value associated with a given key in the Kademlia DHT - KademliaStoreRecord { - key: Vec, - value: Vec, - // expiration time for local records - expiration_time: Option, - // store on explicit peers - explicit_peers: Option>, - }, - /// Perform a lookup of a value associated with a given key in the Kademlia DHT - KademliaLookupRecord { key: Vec }, - /// Perform a lookup of peers that store a record - KademliaGetProviders { key: Vec }, - /// Stop providing a record on the network - KademliaStopProviding { key: Vec }, - /// Remove record from local store - KademliaDeleteRecord { key: Vec }, - /// Return important information about the local routing table - KademliaGetRoutingTableInfo, - /// Fetch data(s) quickly from a peer over the network - FetchData { keys: Vec, peer: PeerId }, -} - -/// Data sent from the networking layer to the application layer or to itself -#[derive(Debug)] -pub(crate) enum NetworkData { - /// Return the result of a DHT lookup - Kademlia(DhtOps), - /// Return important information about the DHT, this will be increased shortly - KademliaDhtInfo { protocol_id: String }, - /// Dail peer - DailPeer(MultiaddrString), -} - -/// Operations performed on the Kademlia DHT -#[derive(Debug)] -pub(crate) enum DhtOps { - /// Value found in the DHT - RecordFound { key: Vec, value: Vec }, - /// No value found - RecordNotFound, - /// Nodes found that provide value for key - ProvidersFound { - key: Vec, - providers: Vec, - }, - /// No providers returned (This is most likely due to an error) - NoProvidersFound, -} From cd9449f921e91ff4c88df111489325b02598120b Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Sun, 5 May 2024 12:46:52 +0100 Subject: [PATCH 02/36] chore: complete request_response impl --- swarm_nl/Cargo.toml | 1 + swarm_nl/src/core/mod.rs | 166 +++++++++++++++++++++++++++-------- swarm_nl/src/core/prelude.rs | 16 ++++ 3 files changed, 145 insertions(+), 38 deletions(-) diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index cfe0720c9..d9bb67e49 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -13,6 +13,7 @@ libp2p = { version="0.53.2", "features"=["async-std", "macros", "ping", "tokio", libp2p-identity = { version="0.2.8", "features"=["secp256k1", "ecdsa", "rsa", "ed25519"] } futures = "0.3.30" futures-time = "3.0.0" +serde = "1.0.200" [dependencies.async-std] version = "1.12.0" diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index c2da8bce2..7c102a5a7 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -18,10 +18,10 @@ use futures_time::time::Duration as AsyncDuration; use libp2p::{ identify::{self, Info}, kad::{self, store::MemoryStore, Record}, - request_response, multiaddr::{self, Protocol}, noise, ping::{self, Failure}, + request_response::{self, cbor::Behaviour, ProtocolSupport}, swarm::{ConnectionError, NetworkBehaviour, SwarmEvent}, tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, }; @@ -47,7 +47,7 @@ struct CoreBehaviour { ping: ping::Behaviour, kademlia: kad::Behaviour, identify: identify::Behaviour, - request_response: request_response::cbor::Behaviour, + request_response: request_response::cbor::Behaviour, } /// Network events generated as a result of supported and configured `NetworkBehaviour`'s @@ -56,7 +56,7 @@ enum CoreEvent { Ping(ping::Event), Kademlia(kad::Event), Identify(identify::Event), - RequestResponse(request_response::Event), + RequestResponse(request_response::Event), } /// Implement ping events for [`CoreEvent`] @@ -111,6 +111,9 @@ pub struct CoreBuilder { kademlia: kad::Behaviour, /// The `Behaviour` of the `Identify` protocol identify: identify::Behaviour, + /// The `Behaviour` of the `Request-Response` protocol. + /// The second field value is the function to handle an incoming request from a peer + request_response: Behaviour, } impl CoreBuilder { @@ -136,11 +139,17 @@ impl CoreBuilder { let store = kad::store::MemoryStore::new(peer_id); let kademlia = kad::Behaviour::with_config(peer_id, store, cfg); - // Set up default config config for Kademlia + // Set up default config for Kademlia let cfg = identify::Config::new(network_id.to_owned(), config.keypair().public()) .with_push_listen_addr_updates(true); let identify = identify::Behaviour::new(cfg); + // Set up default config for Request-Response + let request_response = Behaviour::new( + [(StreamProtocol::new(network_id), ProtocolSupport::Full)], + request_response::Config::default(), + ); + // Initialize struct with information from `BootstrapConfig` CoreBuilder { network_id: StreamProtocol::new(network_id), @@ -163,6 +172,7 @@ impl CoreBuilder { ), kademlia, identify, + request_response: request_response, } } @@ -230,6 +240,23 @@ impl CoreBuilder { } } + /// Configure the RPC protocol for the network. + pub fn with_rpc(self, config: RpcConfig) -> Self + where + F: Fn(Vec) -> Vec, + { + // Set the request-response protocol + CoreBuilder { + request_response: Behaviour::new( + [(self.network_id, ProtocolSupport::Full)], + request_response::Config::default() + .with_request_timeout(config.timeout) + .with_max_concurrent_streams(config.max_concurrent_streams), + ), + ..self + } + } + /// TODO! Kademlia Config has to be cutom because of some setting exposed /// Configure the `Kademlia` protocol for the network. pub fn with_kademlia(self, config: kad::Config) -> Self { @@ -330,7 +357,8 @@ impl CoreBuilder { CoreBehaviour { ping: self.ping.0, kademlia: self.kademlia, - identify: self.identify + identify: self.identify, + response_response: self.request_response }) .map_err(|_| SwarmNlError::ProtocolConfigError)? .with_swarm_config(|cfg| { @@ -401,7 +429,8 @@ impl CoreBuilder { CoreBehaviour { ping: self.ping.0, kademlia: self.kademlia, - identify: self.identify + identify: self.identify, + request_response: self.request_response }) .map_err(|_| SwarmNlError::ProtocolConfigError)? .with_swarm_config(|cfg| { @@ -475,7 +504,7 @@ impl CoreBuilder { // application and the application will comsume it (single consumer) The second stream // will have SwarmNl (being the consumer) recieve data and commands from multiple areas // in the application; - let (_, application_receiver) = mpsc::channel::(3); + let (application_sender, application_receiver) = mpsc::channel::(3); let (application_sender, network_receiver) = mpsc::channel::(3); // Set up the ping network info. @@ -503,7 +532,6 @@ impl CoreBuilder { policy: self.ping.1, manager, }; - // Aggregate the useful network information let network_info = NetworkInfo { id: self.network_id, @@ -534,6 +562,7 @@ impl CoreBuilder { network_info, self.handler, network_receiver, + application_sender, network_core.stream_request_buffer.clone(), network_core.stream_response_buffer.clone(), )); @@ -545,6 +574,7 @@ impl CoreBuilder { network_info, self.handler, network_receiver, + application_sender, network_core.stream_request_buffer.clone(), network_core.stream_response_buffer.clone(), )); @@ -651,7 +681,9 @@ impl Core { let network_data_future = async { loop { // tight loop - if let Some(response_value) = self.stream_response_buffer.lock().await.remove(&stream_id) { + if let Some(response_value) = + self.stream_response_buffer.lock().await.remove(&stream_id) + { return response_value; } } @@ -710,6 +742,7 @@ impl Core { mut network_info: NetworkInfo, mut handler: T, mut receiver: Receiver, + mut sender: Sender, mut stream_request_buffer: Arc>, mut stream_response_buffer: Arc>, ) { @@ -775,8 +808,14 @@ impl Core { }, // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { - // inform the swarm to make the request - + // Construct the RPC object + let rpc = Rpc::ReqResponse { data: keys.into_iter().map(|s| s.into_bytes()).collect() }; + + // Inform the swarm to make the request + swarm + .behaviour_mut() + .request_response + .send_request(&peer, rpc); } } } @@ -841,7 +880,7 @@ impl Core { } // Call custom handler - handler.inbound_ping_success(peer, duration); + handler.inbound_ping_success(peer, duration, sender.clone()); } // Outbound ping failure Err(err_type) => { @@ -910,7 +949,7 @@ impl Core { } // Call custom handler - handler.outbound_ping_error(peer, err_type); + handler.outbound_ping_error(peer, err_type, sender.clone()); } } } @@ -1012,7 +1051,7 @@ impl Core { CoreEvent::Identify(event) => match event { identify::Event::Received { peer_id, info } => { // We just recieved an `Identify` info from a peer.s - handler.identify_info_recieved(peer_id, info.clone()); + handler.identify_info_recieved(peer_id, info.clone(), sender.clone()); // disconnect from peer of the network id is different if info.protocol_version != network_info.id.as_ref() { @@ -1026,6 +1065,43 @@ impl Core { // Remaining `Identify` events are not actively handled _ => {} }, + // Request-response + CoreEvent::RequestResponse(event) => match event { + request_response::Event::Message { peer, message } => match message { + // A request just came in + request_response::Message::Request { request_id, request, channel } => { + // Parse request + match request { + Rpc::ReqResponse { data } => { + // Pass request data to configured request handler + let response_data = handler.handle_incoming_message(data); + + // construct an RPC + let response_rpc = Rpc::ReqResponse { data: response_data }; + + // Send the response + swarm.behaviour_mut().request_response.send_response(channel, response_rpc); + } + } + }, + // We have a response message + request_response::Message::Response { request_id, response } => { + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + + // Insert into the response buffer for it to be picked up + stream_response_buffer.lock().await.insert(stream_id, Box::new(message)); + + }, + }, + request_response::Event::OutboundFailure { peer, request_id, error } => { + + }, + request_response::Event::InboundFailure { peer, request_id, error } => { + + }, + request_response::Event::ResponseSent { peer, request_id } => {}, + } }, SwarmEvent::ConnectionEstablished { peer_id, @@ -1060,6 +1136,7 @@ impl Core { &endpoint, num_established, cause, + sender.clone() ); } SwarmEvent::ExpiredListenAddr { @@ -1067,7 +1144,7 @@ impl Core { address, } => { // call configured handler - handler.expired_listen_addr(listener_id, address); + handler.expired_listen_addr(listener_id, address, sender.clone()); } SwarmEvent::ListenerClosed { listener_id, @@ -1075,33 +1152,33 @@ impl Core { reason: _, } => { // call configured handler - handler.listener_closed(listener_id, addresses); + handler.listener_closed(listener_id, addresses, sender.clone()); } SwarmEvent::ListenerError { listener_id, error: _, } => { // call configured handler - handler.listener_error(listener_id); + handler.listener_error(listener_id, sender.clone()); } SwarmEvent::Dialing { peer_id, connection_id, } => { // call configured handler - handler.dialing(peer_id, connection_id); + handler.dialing(peer_id, connection_id, sender.clone()); } SwarmEvent::NewExternalAddrCandidate { address } => { // call configured handler - handler.new_external_addr_candidate(address); + handler.new_external_addr_candidate(address, sender.clone()); } SwarmEvent::ExternalAddrConfirmed { address } => { // call configured handler - handler.external_addr_confirmed(address); + handler.external_addr_confirmed(address, sender.clone()); } SwarmEvent::ExternalAddrExpired { address } => { // call configured handler - handler.external_addr_expired(address); + handler.external_addr_expired(address, sender.clone()); } SwarmEvent::IncomingConnection { connection_id, @@ -1109,7 +1186,7 @@ impl Core { send_back_addr, } => { // call configured handler - handler.incoming_connection(connection_id, local_addr, send_back_addr); + handler.incoming_connection(connection_id, local_addr, send_back_addr, sender.clone()); } SwarmEvent::IncomingConnectionError { connection_id, @@ -1122,6 +1199,7 @@ impl Core { connection_id, local_addr, send_back_addr, + sender.clone() ); } SwarmEvent::OutgoingConnectionError { @@ -1130,7 +1208,7 @@ impl Core { error: _, } => { // call configured handler - handler.outgoing_connection_error(connection_id, peer_id); + handler.outgoing_connection_error(connection_id, peer_id, sender.clone()); } _ => todo!(), } @@ -1154,7 +1232,7 @@ pub trait EventHandler { _num_established: NonZeroU32, _concurrent_dial_errors: Option)>>, _established_in: Duration, - application_sender: Sender, + _application_sender: Sender, ) { // Default implementation } @@ -1167,42 +1245,43 @@ pub trait EventHandler { _endpoint: &ConnectedPoint, _num_established: u32, _cause: Option, + _application_sender: Sender, ) { // Default implementation } /// Event that announces expired listen address. - fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr) { + fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr, _application_sender: Sender) { // Default implementation } /// Event that announces a closed listener. - fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec) { + fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec, _application_sender: Sender) { // Default implementation } /// Event that announces a listener error. - fn listener_error(&mut self, _listener_id: ListenerId) { + fn listener_error(&mut self, _listener_id: ListenerId, _application_sender: Sender) { // Default implementation } /// Event that announces a dialing attempt. - fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId) { + fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId, _application_sender: Sender) { // Default implementation } /// Event that announces a new external address candidate. - fn new_external_addr_candidate(&mut self, _address: Multiaddr) { + fn new_external_addr_candidate(&mut self, _address: Multiaddr, _application_sender: Sender) { // Default implementation } /// Event that announces a confirmed external address. - fn external_addr_confirmed(&mut self, _address: Multiaddr) { + fn external_addr_confirmed(&mut self, _address: Multiaddr, _application_sender: Sender) { // Default implementation } /// Event that announces an expired external address. - fn external_addr_expired(&mut self, _address: Multiaddr) { + fn external_addr_expired(&mut self, _address: Multiaddr, _application_sender: Sender) { // Default implementation } @@ -1213,6 +1292,7 @@ pub trait EventHandler { _connection_id: ConnectionId, _local_addr: Multiaddr, _send_back_addr: Multiaddr, + _application_sender: Sender ) { // Default implementation } @@ -1224,6 +1304,7 @@ pub trait EventHandler { _connection_id: ConnectionId, _local_addr: Multiaddr, _send_back_addr: Multiaddr, + _application_sender: Sender ) { // Default implementation } @@ -1234,28 +1315,29 @@ pub trait EventHandler { &mut self, _connection_id: ConnectionId, _peer_id: Option, + _application_sender: Sender ) { // Default implementation } /// Event that announces the arrival of a ping message from a peer. /// The duration it took for a round trip is also returned - fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration) { + fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration, _application_sender: Sender) { // Default implementation } /// Event that announces a `Ping` error - fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure) { + fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure, _application_sender: Sender) { // Default implementation } /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol - fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info) { + fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info, _application_sender: Sender) { // Default implementation } /// Event that announces the successful write of a record to the DHT - fn kademlia_put_record_success(&mut self, _key: Vec) { + fn kademlia_put_record_success(&mut self, _key: Vec, _application_sender: Sender) { // Default implementation } @@ -1265,21 +1347,29 @@ pub trait EventHandler { } /// Event that announces a node as a provider of a record in the DHT - fn kademlia_start_providing_success(&mut self, _key: Vec) { + fn kademlia_start_providing_success(&mut self, _key: Vec, _application_sender: Sender) { // Default implementation } /// Event that announces the failure of a node to become a provider of a record in the DHT - fn kademlia_start_providing_error(&mut self) { + fn kademlia_start_providing_error(&mut self, _application_sender: Sender) { // Default implementation } + + /// Event that announces the arrival of an RPC message + fn handle_incoming_message(&mut self, data: Vec>) -> Vec>; } /// Default network event handler pub struct DefaultHandler; /// Implement [`EventHandler`] for [`DefaultHandler`] -impl EventHandler for DefaultHandler {} +impl EventHandler for DefaultHandler { + /// Echo the message back to the sender + fn handle_incoming_message(&mut self, data: Vec>) -> Vec> { + data + } +} /// Important information to obtain from the [`CoreBuilder`], to properly handle network /// operations diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index 97ada9b86..c444f6d3a 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -1,4 +1,5 @@ use rand::random; +use serde::{Deserialize, Serialize}; /// Copyright (c) 2024 Algorealm use std::time::Instant; use thiserror::Error; @@ -199,3 +200,18 @@ impl StreamResponseBuffer { self.buffer.contains_key(id) } } + +/// Type representing the RPC data structure sent between nodes in the network +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub(super) enum Rpc { + /// Using request-response + ReqResponse { data: Vec> }, +} + +/// The configuration for the RPC protocol +pub struct RpcConfig { + /// Timeout for inbound and outbound requests + pub timeout: Duration, + /// Maximum number of concurrent inbound + outbound streams + pub max_concurrent_streams: usize, +} From c9342b58ac3980f92904cca778b0a67ef8ae620c Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Mon, 6 May 2024 03:29:53 +0100 Subject: [PATCH 03/36] fix: impl of the request-response protocol --- client/src/main.rs | 3 + swarm_nl/src/core/mod.rs | 227 ++++++++++++++++++++++++++++------- swarm_nl/src/core/prelude.rs | 36 ++++-- 3 files changed, 212 insertions(+), 54 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index f205178ea..d912e71e2 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -154,6 +154,9 @@ mod age_of_empires { coin }; + + // TODO! SPECICIFY APPDATA TYPE AND ITS RESPONSE! + } } diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 7c102a5a7..d63eae354 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -1,6 +1,7 @@ /// Copyright (c) 2024 Algorealm /// Core data structures and protocol implementations for building a swarm. use std::{ + any::Any, collections::{HashMap, HashSet}, io::Error, net::{IpAddr, Ipv4Addr}, @@ -673,6 +674,7 @@ impl Core { Some(stream_id) } + /// TODO! Buffer clearnup algorithm /// Explicitly rectrieve the reponse to a request sent to the network layer. /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay /// and read immediately as the response to the request should already be in the stream response @@ -681,10 +683,20 @@ impl Core { let network_data_future = async { loop { // tight loop - if let Some(response_value) = - self.stream_response_buffer.lock().await.remove(&stream_id) + if let Some(response_value) = self + .stream_response_buffer + .lock() + .await + .remove(&TrackableStreamId::Id(stream_id)) { - return response_value; + // Make sure the value is not `StreamId`. If it is, then the request is being + // processed and the final value we want has not arrived. + + // Get inner boxed value + let inner_value = &response_value as &dyn Any; + if let None = inner_value.downcast_ref::() { + return response_value; + } } } }; @@ -752,6 +764,8 @@ impl Core { // handle incoming stream data stream_data = receiver.select_next_some() => match stream_data { StreamData::Application(stream_id, app_data) => { + // Trackable stream id + let trackable_stream_id = TrackableStreamId::Id(stream_id); match app_data { // Put back into the stream what we read from it AppData::Echo(message) => { @@ -759,10 +773,10 @@ impl Core { stream_request_buffer.lock().await.remove(&stream_id); // Insert into the response buffer for it to be picked up - stream_response_buffer.lock().await.insert(stream_id, Box::new(message)); + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); }, // Store a value in the DHT and (optionally) on explicit specific peers - AppData::KademliaStoreRecord { key,value,expiration_time, explicit_peers } => { + AppData::KademliaStoreRecord { key, value, expiration_time, explicit_peers } => { // create a kad record let mut record = Record::new(key, value); @@ -770,33 +784,57 @@ impl Core { record.expires = expiration_time; // Insert into DHT - let _ = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One); - - // Cache record on peers explicitly (if specified) - if let Some(explicit_peers) = explicit_peers { - // Extract PeerIds - let peers = explicit_peers.iter().map(|peer_id_string| { - PeerId::from_bytes(peer_id_string.as_bytes()) - }).filter_map(Result::ok).collect::>(); + if let Ok(query_id) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(query_id), Box::new(stream_id)); + + // Cache record on peers explicitly (if specified) + if let Some(explicit_peers) = explicit_peers { + // Extract PeerIds + let peers = explicit_peers.iter().map(|peer_id_string| { + PeerId::from_bytes(peer_id_string.as_bytes()) + }).filter_map(Result::ok).collect::>(); + + swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); + } + } else { + // Delete the request from the stream and then return a write error + stream_request_buffer.lock().await.remove(&stream_id); - swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); + // Return error + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); } }, // Perform a lookup in the DHT AppData::KademliaLookupRecord { key } => { - swarm.behaviour_mut().kademlia.get_record(key.into()); + let query_id = swarm.behaviour_mut().kademlia.get_record(key.into()); + + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(query_id), Box::new(stream_id)); }, // Perform a lookup of peers that store a record AppData::KademliaGetProviders { key } => { - swarm.behaviour_mut().kademlia.get_providers(key.into()); + let query_id = swarm.behaviour_mut().kademlia.get_providers(key.into()); + + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(query_id), Box::new(stream_id)); } // Stop providing a record on the network AppData::KademliaStopProviding { key } => { swarm.behaviour_mut().kademlia.stop_providing(&key.into()); + + // Respond with a success + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); } // Remove record from local store AppData::KademliaDeleteRecord { key } => { swarm.behaviour_mut().kademlia.remove_record(&key.into()); + + // Respond with a success + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); } // Return important routing table info AppData::KademliaGetRoutingTableInfo => { @@ -804,7 +842,7 @@ impl Core { stream_request_buffer.lock().await.remove(&stream_id); // Insert into the response buffer for it to be picked up - stream_response_buffer.lock().await.insert(stream_id, Box::new(Kademlia::Info { protocol_id: network_info.id.to_string() })); + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: network_info.id.to_string() })); }, // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { @@ -812,14 +850,20 @@ impl Core { let rpc = Rpc::ReqResponse { data: keys.into_iter().map(|s| s.into_bytes()).collect() }; // Inform the swarm to make the request - swarm + let outbound_id = swarm .behaviour_mut() .request_response .send_request(&peer, rpc); + + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); } } } StreamData::Network(stream_id, network_data) => { + // Trackable stream id + let trackable_stream_id = TrackableStreamId::Id(stream_id); match network_data { // Dail peer NetworkData::DailPeer(multiaddr) => { @@ -827,7 +871,12 @@ impl Core { stream_request_buffer.lock().await.remove(&stream_id); if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { - swarm.dial(multiaddr); + if let Ok(_) = swarm.dial(multiaddr) { + // Put result into response buffer + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); + } else { + stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); + } } } _ => {} @@ -989,10 +1038,26 @@ impl Core { kad::QueryResult::GetRecord(Err(_)) => { // No record found let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + + // Delete the temporary data and then return error + + // Get `StreamId` + if let Some(opaque_data) = stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + } + } } kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { // Call handler - handler.kademlia_put_record_success(key.to_vec()); + handler.kademlia_put_record_success(key.to_vec(), sender.clone()); } kad::QueryResult::PutRecord(Err(_)) => { // Call handler @@ -1002,11 +1067,11 @@ impl Core { key, })) => { // Call handler - handler.kademlia_start_providing_success(key.to_vec()); + handler.kademlia_start_providing_success(key.to_vec(), sender.clone()); } kad::QueryResult::StartProviding(Err(_)) => { // Call handler - handler.kademlia_start_providing_error(); + handler.kademlia_start_providing_error(sender.clone()); } _ => {} }, @@ -1086,21 +1151,41 @@ impl Core { }, // We have a response message request_response::Message::Response { request_id, response } => { - // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up - // Insert into the response buffer for it to be picked up - stream_response_buffer.lock().await.insert(stream_id, Box::new(message)); + // Get `StreamId` + if let Some(opaque_data) = stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(message.into())); + } + } }, }, request_response::Event::OutboundFailure { peer, request_id, error } => { + // Delete the temporary data and then return error - }, - request_response::Event::InboundFailure { peer, request_id, error } => { + // Get `StreamId` + if let Some(opaque_data) = stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + } + } }, - request_response::Event::ResponseSent { peer, request_id } => {}, + _ => {} } }, SwarmEvent::ConnectionEstablished { @@ -1251,37 +1336,68 @@ pub trait EventHandler { } /// Event that announces expired listen address. - fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr, _application_sender: Sender) { + fn expired_listen_addr( + &mut self, + _listener_id: ListenerId, + _address: Multiaddr, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces a closed listener. - fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec, _application_sender: Sender) { + fn listener_closed( + &mut self, + _listener_id: ListenerId, + _addresses: Vec, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces a listener error. - fn listener_error(&mut self, _listener_id: ListenerId, _application_sender: Sender) { + fn listener_error( + &mut self, + _listener_id: ListenerId, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces a dialing attempt. - fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId, _application_sender: Sender) { + fn dialing( + &mut self, + _peer_id: Option, + _connection_id: ConnectionId, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces a new external address candidate. - fn new_external_addr_candidate(&mut self, _address: Multiaddr, _application_sender: Sender) { + fn new_external_addr_candidate( + &mut self, + _address: Multiaddr, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces a confirmed external address. - fn external_addr_confirmed(&mut self, _address: Multiaddr, _application_sender: Sender) { + fn external_addr_confirmed( + &mut self, + _address: Multiaddr, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces an expired external address. - fn external_addr_expired(&mut self, _address: Multiaddr, _application_sender: Sender) { + fn external_addr_expired( + &mut self, + _address: Multiaddr, + _application_sender: Sender, + ) { // Default implementation } @@ -1292,7 +1408,7 @@ pub trait EventHandler { _connection_id: ConnectionId, _local_addr: Multiaddr, _send_back_addr: Multiaddr, - _application_sender: Sender + _application_sender: Sender, ) { // Default implementation } @@ -1304,7 +1420,7 @@ pub trait EventHandler { _connection_id: ConnectionId, _local_addr: Multiaddr, _send_back_addr: Multiaddr, - _application_sender: Sender + _application_sender: Sender, ) { // Default implementation } @@ -1315,29 +1431,48 @@ pub trait EventHandler { &mut self, _connection_id: ConnectionId, _peer_id: Option, - _application_sender: Sender + _application_sender: Sender, ) { // Default implementation } /// Event that announces the arrival of a ping message from a peer. /// The duration it took for a round trip is also returned - fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration, _application_sender: Sender) { + fn inbound_ping_success( + &mut self, + _peer_id: PeerId, + _duration: Duration, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces a `Ping` error - fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure, _application_sender: Sender) { + fn outbound_ping_error( + &mut self, + _peer_id: PeerId, + _err_type: Failure, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol - fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info, _application_sender: Sender) { + fn identify_info_recieved( + &mut self, + _peer_id: PeerId, + _info: Info, + _application_sender: Sender, + ) { // Default implementation } /// Event that announces the successful write of a record to the DHT - fn kademlia_put_record_success(&mut self, _key: Vec, _application_sender: Sender) { + fn kademlia_put_record_success( + &mut self, + _key: Vec, + _application_sender: Sender, + ) { // Default implementation } @@ -1347,7 +1482,11 @@ pub trait EventHandler { } /// Event that announces a node as a provider of a record in the DHT - fn kademlia_start_providing_success(&mut self, _key: Vec, _application_sender: Sender) { + fn kademlia_start_providing_success( + &mut self, + _key: Vec, + _application_sender: Sender, + ) { // Default implementation } diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index c444f6d3a..1701f1d51 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -1,7 +1,8 @@ +use libp2p::{kad::QueryId, request_response::OutboundRequestId}; use rand::random; use serde::{Deserialize, Serialize}; /// Copyright (c) 2024 Algorealm -use std::time::Instant; +use std::{any::Any, time::Instant}; use thiserror::Error; use super::*; @@ -71,6 +72,10 @@ pub enum NetworkError { NetworkReadTimeout, #[error("internal request stream buffer is full")] StreamBufferOverflow, + #[error("failed to store record in DHT")] + KadStoreRecordError(Vec), + #[error("failed to fetch data from peer")] + RpcDataFetchError } /// Operations performed on the Kademlia DHT @@ -106,18 +111,29 @@ impl StreamId { } } +/// Enum that represents various ids that can be tracked on the [`StreamResponseBuffer`] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum TrackableStreamId { + /// Our custom stream id + Id(StreamId), + /// RPC request id + Outbound(OutboundRequestId), + /// Kademlia query id + Kad(QueryId), +} + /// Type that specifies the result of querying the network layer pub type NetworkResult = Result, NetworkError>; /// Marker trait that indicates a stream reponse data object pub trait StreamResponseType where - Self: Send + Sync + 'static, + Self: Send + Sync + Any + 'static, { } /// Macro that implements [`StreamResponseType`] for various types to occupy the -/// [`StreamResponseBuffer`] +/// [`StreamResponseBuffer`] [`StreamResponseBuffer`] macro_rules! impl_stream_response_for_types { ( $( $t:ident )* ) => { $( impl StreamResponseType for $t {} @@ -125,7 +141,7 @@ macro_rules! impl_stream_response_for_types { ( $( $t:ident )* ) => { } impl_stream_response_for_types!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 - usize isize f32 f64 String bool Kademlia); + usize isize f32 f64 String bool Kademlia StreamId SwarmNlError NetworkError DhtOps); /// Type that keeps track of the requests from the application layer. /// This type has a maximum buffer size and will drop subsequent requests when full. @@ -168,7 +184,7 @@ impl StreamRequestBuffer { pub(super) struct StreamResponseBuffer { /// Max responses we can keep track of size: usize, - buffer: HashMap>, + buffer: HashMap>, } impl StreamResponseBuffer { @@ -180,9 +196,9 @@ impl StreamResponseBuffer { } } - /// Push [`StreamId`]s into buffer. + /// Push a [`TrackableStreamId`] into buffer. /// Returns `false` if the buffer is full and request cannot be stored - pub fn insert(&mut self, id: StreamId, response: Box) -> bool { + pub fn insert(&mut self, id: TrackableStreamId, response: Box) -> bool { if self.buffer.len() < self.size { self.buffer.insert(id, response); return true; @@ -190,13 +206,13 @@ impl StreamResponseBuffer { false } - /// Remove a [`StreamId`] from the buffer - pub fn remove(&mut self, id: &StreamId) -> Option> { + /// Remove a [`TrackableStreamId`] from the buffer + pub fn remove(&mut self, id: &TrackableStreamId) -> Option> { self.buffer.remove(&id) } /// Check if buffer contains a value - pub fn contains(&mut self, id: &StreamId) -> bool { + pub fn contains(&mut self, id: &TrackableStreamId) -> bool { self.buffer.contains_key(id) } } From 6179ed6ccfe96e5741ce9bb4e06cacbae77db09b Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Tue, 7 May 2024 18:40:19 +0100 Subject: [PATCH 04/36] fix: added appropriate network stream interface to communicate with the network layer from the application's event handler --- client/bootstrap_config.ini | 19 ++ client/src/main.rs | 303 ++++++------------ client/test_config.ini | 10 - swarm_nl/src/core/mod.rs | 602 ++++++++++------------------------- swarm_nl/src/core/prelude.rs | 377 +++++++++++++++++++++- swarm_nl/src/prelude.rs | 3 + 6 files changed, 648 insertions(+), 666 deletions(-) create mode 100644 client/bootstrap_config.ini delete mode 100644 client/test_config.ini diff --git a/client/bootstrap_config.ini b/client/bootstrap_config.ini new file mode 100644 index 000000000..eddfa8643 --- /dev/null +++ b/client/bootstrap_config.ini @@ -0,0 +1,19 @@ +; Copyright (c) 2024 Algorealm +; A typical template showing the necessary config to bootstrap a node + +[ports] +; TCP/IP port to listen on +tcp=49200 +; UDP port to listen on +udp=49201 + +; compulsory +[auth] +; Type of keypair to generate for node identity and message auth e.g RSA, EDSA, Ed25519 +crypto=Ed25519 +; The protobuf serialized format of the node's cryptographic keypair +protobuf_keypair=[] + +[bootstrap] +; The boostrap nodes to connect to immediately after start up +boot_nodes=[] \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index d912e71e2..60a97cb3a 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,230 +1,129 @@ /// Copyright (c) 2024 Algorealm -/// This crate is simply for quick-testing the swarmNL library APIs and assisting in -/// developement. It is not the default or de-facto test crate/module, as it is only used -/// in-dev and will be removed subsequently. -use std::collections::HashMap; +/// Age of Empires +/// Objective: Form alliances and conquer as much empires as possible! +/// It is a multi-player game +/// Enjoy! +use std::{num::NonZeroU32, time::Duration}; use swarm_nl::{ - core::{DefaultHandler, EventHandler}, - ListenerId, Multiaddr, MultiaddrString, PeerIdString, StreamData, StreamExt, + core::EventHandler, + core::{AppData, Core, CoreBuilder, NetworkChannel}, + setup::BootstrapConfig, + ConnectedPoint, ConnectionId, PeerId, }; pub static CONFIG_FILE_PATH: &str = "test_config.ini"; -/// Complex Event Handler -struct ComplexHandler; - -impl EventHandler for ComplexHandler { - fn new_listen_addr(&mut self, _listener_id: ListenerId, addr: Multiaddr) { - // Log the address we begin listening on - println!("We're now listening on: {}", addr); - } -} - #[tokio::main] async fn main() { - // handler for events happening in the network layer (majorly for technical use) - // use default handler - let handler = DefaultHandler; - let complex_handler = ComplexHandler; - - // set up node - let mut bootnodes: HashMap = HashMap::new(); - bootnodes.insert( - "12D3KooWBmwXN3rsVfnLsZKbXeBrSLfczHxZHwVjPrbKwpLfYm3t".to_string(), - "/ip4/127.0.0.1/tcp/63307".to_string(), - ); - - // configure default data - let config = swarm_nl::setup::BootstrapConfig::new().with_bootnodes(bootnodes); - - // set up network core - let mut network = swarm_nl::core::CoreBuilder::with_config(config, complex_handler) - .build() - .await - .unwrap(); + // Start our game! Age of Empires! + play_game().await +} - // read first (ready) message - if let Some(StreamData::Ready) = network.application_receiver.next().await { - println!("Database is online"); +#[derive(Clone)] +pub struct Empire { + name: String, + soldiers: u32, + farmers: u32, + blacksmith: u32, + land_mass: u32, + gold_reserve: u32, +} - // begin listening - loop { - if let Some(data) = network.application_receiver.next().await { - println!("{:?}", data); - } +impl Empire { + /// Create a new empire and assign the assets to begin with + pub fn new(name: String) -> Self { + Empire { + name, + soldiers: 100, + farmers: 100, + blacksmith: 100, + land_mass: 100, + gold_reserve: 100, } } } -mod age_of_empires { - use std::{num::NonZeroU32, time::Duration}; - use swarm_nl::{core::EventHandler, PeerId, ConnectionId, ConnectedPoint, Multiaddr, StreamData, Sender, AppData}; - - // Rename Sender during re-export to something more custom for its function - // pub use Sender as StreamComm - - /// The state of the game - struct Empire { - soldiers: u32, - farmers: u32, - blacksmith: u32, - land_mass: u32, - gold_reserve: u32 +impl EventHandler for Empire { + fn new_listen_addr( + &mut self, + channel: NetworkChannel, + local_peer_id: PeerId, + _listener_id: swarm_nl::ListenerId, + addr: swarm_nl::Multiaddr, + ) { + // announce interfaces we're listening on + println!("Peer id: {}", local_peer_id); + println!("We're listening on the {}", addr); + println!( + "There are {} soldiers guarding the {} Empire gate", + self.soldiers, self.name + ); } - /// implement `EventHander` for `Empire` to reponse to network events and make state changes - impl EventHandler for Empire { - fn connection_established( - &mut self, - peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: NonZeroU32, - _concurrent_dial_errors: Option)>>, - _established_in: Duration, - mut application_sender: Sender - ) { - // We want to only ally with empires that are richer than us. - // We we connect, we ask for their reserve and make sure they are richer, - // Then we collect a fee of 200 gold coins - - // construct keys to send - let keys = vec!["get_coins".to_string()]; - - // Ask the network to get the gold reserve of the empire seeking partnership - let total_coins_from_peer = application_sender.try_send(StreamData::Application(AppData::FetchData { keys , peer: peer_id })); - - - // if total_coins_from_peer > self.gold_reserve { - // // ask for alignment fee ReqRes::AskForMergingFee - // let m_fee = 100; - - // // add to our gold reserve - // self.gold_reserve += m_fee; - - // // deal complete - // } else { - // // we dont ally with broke empires - // // disconnect (This is a network operation) - // } - - } - } - - /// Function to run game - fn start_game() {} - - /// Setup network - async fn setup_network() { - // set up a default bootrap config - let config = swarm_nl::setup::BootstrapConfig::new(); - - let spartan_empire = Empire { - soldiers: 1000, - farmers: 1000, - blacksmith: 1000, - land_mass: 1000, - gold_reserve: 1000, - }; - - // set up network core - let mut network = swarm_nl::core::CoreBuilder::with_config(config, spartan_empire) - .build() - .await - .unwrap(); - - network.application_sender.try_send(StreamData::ReqRes("gettotalcoins".as_bytes().to_vec())).await; - - let data = network.send_to_network_layer(get_coins(89)).await; //-> 3 seconds - - // Do many task - - let coin = network.recv_from_network_layer(stream_id).await; // no waiting - - // use coin - - - let data = fetch_form_network_layer(Coins).await; - - let coin = fetch_from_network_layer = { - let data = network.send_to_network_layer(get_coins(89)).await; // -> 3 seconds - let coin = network.recv_from_network_layer(stream_id).await; // -> 45 seconds - - coin - }; - - - // TODO! SPECICIFY APPDATA TYPE AND ITS RESPONSE! + fn connection_established( + &mut self, + channel: NetworkChannel, + peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: NonZeroU32, + _established_in: Duration, + ) { + } + fn handle_incoming_message(&mut self, data: Vec>) -> Vec> { + println!("Incoming data: {:?}", data); + data } } -#[cfg(test)] -mod tests { - use ini::Ini; - use std::borrow::Cow; - - use crate::CONFIG_FILE_PATH; - - /// try to read/write a byte vector to config file - #[test] - fn write_to_ini_file() { - let test_vec = vec![12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98]; - - // try vec to `.ini` file - assert!(write_config( - "auth", - "serialized_keypair", - &format!("{:?}", test_vec) - )); - assert_eq!( - read_config("auth", "serialized_keypair"), - format!("{:?}", test_vec) - ); - - // test that we can read something after it - assert_eq!(read_config("bio", "name"), "@thewoodfish"); - } +/// Setup game (This is for the persian Empire) +/// This requires no bootnodes connection +#[cfg(not(feature = "macedonian"))] +pub async fn setup_game() -> Core { + // First, we want to configure our node + let config = BootstrapConfig::default(); - #[test] - fn test_conversion_fn() { - let test_vec = vec![12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98]; + // State kept by this node + let empire = Empire::new(String::from("Spartan")); - let vec_string = "[12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98,]"; - assert_eq!(string_to_vec(vec_string), test_vec); - } + // Set up network + CoreBuilder::with_config(config, empire) + .build() + .await + .unwrap() +} - /// read value from config file - fn read_config(section: &str, key: &str) -> Cow<'static, str> { - if let Ok(conf) = Ini::load_from_file(CONFIG_FILE_PATH) { - if let Some(section) = conf.section(Some(section)) { - if let Some(value) = section.get(key) { - return Cow::Owned(value.to_owned()); - } - } - } +/// The Macedonian Empire setup. +/// These require bootnodes of empires to form alliance. +/// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot +/// parameters +#[cfg(feature = "macedonian")] +pub async fn setup_game() -> Core { + // First, we want to configure our node with the bootstrap config file on disk + let config = BootstrapConfig::from_file("bootstrap_config.ini"); - "".into() - } + // State kept by this node + let empire = Empire::new(String::from("Macedonian")); - fn string_to_vec(input: &str) -> Vec { - input - .trim_matches(|c| c == '[' || c == ']') - .split(',') - .filter_map(|s| s.trim().parse().ok()) - .collect() - } + // Set up network + CoreBuilder::with_config(config, empire) + .build() + .await + .unwrap() +} - /// write value into config file - fn write_config(section: &str, key: &str, new_value: &str) -> bool { - if let Ok(mut conf) = Ini::load_from_file(CONFIG_FILE_PATH) { - // Set a value: - conf.set_to(Some(section), key.into(), new_value.into()); - if let Ok(_) = conf.write_to_file(CONFIG_FILE_PATH) { - return true; - } - } - false - } +/// Play game +pub async fn play_game() { + // Setup network + let core = setup_game().await; + + // Print game state + println!("Empire Information:"); + println!("Name: {}", core.state.soldiers); + println!("Farmers: {}", core.state.farmers); + println!("Black smiths: {}", core.state.blacksmith); + println!("Land mass: {}", core.state.land_mass); + println!("Gold reserve: {}", core.state.gold_reserve); } diff --git a/client/test_config.ini b/client/test_config.ini deleted file mode 100644 index 9a6a7516b..000000000 --- a/client/test_config.ini +++ /dev/null @@ -1,10 +0,0 @@ -[ports] -tcp=3000 -udp=4000 - -[auth] -crypto=Ed25519 -serialized_keypair=[12, 234, 45, 34, 54, 34, 43, 34, 43, 23, 43, 43, 34, 67, 98] - -[bio] -name=@thewoodfish diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index d63eae354..304b3b89c 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -3,7 +3,6 @@ use std::{ any::Any, collections::{HashMap, HashSet}, - io::Error, net::{IpAddr, Ipv4Addr}, num::NonZeroU32, sync::Arc, @@ -24,12 +23,13 @@ use libp2p::{ ping::{self, Failure}, request_response::{self, cbor::Behaviour, ProtocolSupport}, swarm::{ConnectionError, NetworkBehaviour, SwarmEvent}, - tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, + tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, }; +use self::ping_config::*; + use super::*; use crate::setup::BootstrapConfig; -use ping_config::*; #[cfg(feature = "async-std-runtime")] use async_std::sync::Mutex; @@ -38,7 +38,7 @@ use async_std::sync::Mutex; use tokio::sync::Mutex; mod prelude; -use prelude::*; +pub use prelude::*; /// The Core Behaviour implemented which highlights the various protocols /// we'll be adding support for @@ -89,7 +89,7 @@ impl From> for CoreEvent { } /// Structure containing necessary data to build [`Core`] -pub struct CoreBuilder { +pub struct CoreBuilder { network_id: StreamProtocol, keypair: Keypair, tcp_udp_port: (Port, Port), @@ -117,7 +117,7 @@ pub struct CoreBuilder { request_response: Behaviour, } -impl CoreBuilder { +impl CoreBuilder { /// Return a [`CoreBuilder`] struct configured with [`BootstrapConfig`] and default values. /// Here, it is certain that [`BootstrapConfig`] contains valid data. /// A type that implements [`EventHandler`] is passed to handle and react to network events. @@ -164,7 +164,7 @@ impl CoreBuilder { // Default is to listen on all interfaces (ipv4) ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), // Default to 60 seconds - keep_alive_duration: 60, + keep_alive_duration: DEFAULT_KEEP_ALIVE_DURATION, transport: default_transport, // The peer will be disconnected after 20 successive timeout errors are recorded ping: ( @@ -173,7 +173,7 @@ impl CoreBuilder { ), kademlia, identify, - request_response: request_response, + request_response, } } @@ -249,7 +249,7 @@ impl CoreBuilder { // Set the request-response protocol CoreBuilder { request_response: Behaviour::new( - [(self.network_id, ProtocolSupport::Full)], + [(self.network_id.clone(), ProtocolSupport::Full)], request_response::Config::default() .with_request_timeout(config.timeout) .with_max_concurrent_streams(config.max_concurrent_streams), @@ -274,7 +274,7 @@ impl CoreBuilder { CoreBuilder { transport, ..self } } - /// Configure network event handler. + /// Configure network event handler /// This configures the functions to be called when various network events take place pub fn configure_network_events(self, handler: T) -> Self { CoreBuilder { handler, ..self } @@ -284,7 +284,7 @@ impl CoreBuilder { /// /// Handles the configuration of the libp2p Swarm structure and the selected transport /// protocols, behaviours and node identity. - pub async fn build(self) -> SwarmNlResult { + pub async fn build(self) -> SwarmNlResult> { // Build and configure the libp2p Swarm structure. Thereby configuring the selected // transport protocols, behaviours and node identity. The Swarm is wrapped in the Core // construct which serves as the interface to interact with the internal networking @@ -359,7 +359,7 @@ impl CoreBuilder { ping: self.ping.0, kademlia: self.kademlia, identify: self.identify, - response_response: self.request_response + request_response: self.request_response }) .map_err(|_| SwarmNlError::ProtocolConfigError)? .with_swarm_config(|cfg| { @@ -501,11 +501,8 @@ impl CoreBuilder { // There must be a way for the application to communicate with the underlying networking // core. This will involve acceptiing data and pushing data to the application layer. - // Two streams will be opened: The first mpsc stream will allow SwarmNL push data to the - // application and the application will comsume it (single consumer) The second stream - // will have SwarmNl (being the consumer) recieve data and commands from multiple areas - // in the application; - let (application_sender, application_receiver) = mpsc::channel::(3); + // The stream will have SwarmNl (being the consumer) recieve data and commands from multiple + // areas in the application; let (application_sender, network_receiver) = mpsc::channel::(3); // Set up the ping network info. @@ -533,27 +530,39 @@ impl CoreBuilder { policy: self.ping.1, manager, }; + + // Initials stream id + let stream_id = StreamId::new(); + + // Setup the application event handler's network communication channel + let event_comm_channel = NetworkChannel::new( + Arc::new(Mutex::new(StreamRequestBuffer::new(self.stream_size))), + Arc::new(Mutex::new(StreamResponseBuffer::new(self.stream_size))), + application_sender.clone(), + Arc::new(Mutex::new(stream_id)), + self.network_read_delay, + ); + // Aggregate the useful network information let network_info = NetworkInfo { id: self.network_id, ping: ping_info, + event_comm_channel, }; // Build the network core let network_core = Core { keypair: self.keypair, application_sender, - application_receiver, - stream_request_buffer: Arc::new(Mutex::new(StreamRequestBuffer { - size: self.stream_size, - buffer: Default::default(), - })), - stream_response_buffer: Arc::new(Mutex::new(StreamResponseBuffer { - size: self.stream_size, - buffer: Default::default(), - })), + // application_receiver, + stream_request_buffer: Arc::new(Mutex::new(StreamRequestBuffer::new(self.stream_size))), + stream_response_buffer: Arc::new(Mutex::new(StreamResponseBuffer::new( + self.stream_size, + ))), network_read_delay: self.network_read_delay, - current_stream_id: StreamId::new(), + current_stream_id: Arc::new(Mutex::new(stream_id)), + // Save handler as the state of the application + state: self.handler, }; // Spin up task to handle async operations and data on the network. @@ -561,11 +570,8 @@ impl CoreBuilder { async_std::task::spawn(Core::handle_async_operations( swarm, network_info, - self.handler, network_receiver, - application_sender, - network_core.stream_request_buffer.clone(), - network_core.stream_response_buffer.clone(), + network_core.clone(), )); // Spin up task to handle async operations and data on the network. @@ -573,11 +579,8 @@ impl CoreBuilder { tokio::task::spawn(Core::handle_async_operations( swarm, network_info, - self.handler, network_receiver, - application_sender, - network_core.stream_request_buffer.clone(), - network_core.stream_response_buffer.clone(), + network_core.clone(), )); Ok(network_core) @@ -585,13 +588,14 @@ impl CoreBuilder { } /// The core interface for the application layer to interface with the networking layer -pub struct Core { +#[derive(Clone)] +pub struct Core { keypair: Keypair, /// The producing end of the stream that sends data to the network layer from the /// application application_sender: Sender, /// The consuming end of the stream that recieves data from the network layer - application_receiver: Receiver, + // application_receiver: Receiver, /// This serves as a buffer for the results of the requests to the network layer. /// With this, applications can make async requests and fetch their results at a later time /// without waiting. This is made possible by storing a [`StreamId`] for a particular stream @@ -602,10 +606,12 @@ pub struct Core { /// The network read timeout network_read_delay: AsyncDuration, /// Current stream id. Useful for opening new streams, we just have to bump the number by 1 - current_stream_id: StreamId, + current_stream_id: Arc>, + /// The state of the application + pub state: T, } -impl Core { +impl Core { /// Serialize keypair to protobuf format and write to config file on disk. /// It returns a boolean to indicate success of operation. /// Only key types other than RSA can be serialized to protobuf format. @@ -635,7 +641,7 @@ impl Core { /// If the internal stream buffer is full, `None` will be returned. pub async fn send_to_network(&mut self, request: AppData) -> Option { // Generate stream id - let stream_id = StreamId::next(self.current_stream_id); + let stream_id = StreamId::next(*self.current_stream_id.lock().await); let request = StreamData::Application(stream_id, request); // Add to request buffer @@ -645,18 +651,19 @@ impl Core { } // Send request - self.application_sender.send(request).await; - - // Store latest stream id - self.current_stream_id = stream_id; - - Some(stream_id) + if let Ok(_) = self.application_sender.send(request).await { + // Store latest stream id + *self.current_stream_id.lock().await = stream_id; + Some(stream_id) + } else { + return None; + } } /// This is for intra-network communication async fn notify_network(&mut self, request: NetworkData) -> Option { // Generate stream id - let stream_id = StreamId::next(self.current_stream_id); + let stream_id = StreamId::next(*self.current_stream_id.lock().await); let request = StreamData::Network(stream_id, request); // Add to request buffer @@ -666,12 +673,13 @@ impl Core { } // Send request - self.application_sender.send(request).await; - - // Store latest stream id - self.current_stream_id = stream_id; - - Some(stream_id) + if let Ok(_) = self.application_sender.send(request).await { + // Store latest stream id + *self.current_stream_id.lock().await = stream_id; + Some(stream_id) + } else { + return None; + } } /// TODO! Buffer clearnup algorithm @@ -749,14 +757,11 @@ impl Core { /// - Streams coming from the application layer. /// - Events generated by (libp2p) network activities. /// Important information are sent to the application layer over a (mpsc) stream - async fn handle_async_operations( + async fn handle_async_operations( mut swarm: Swarm, mut network_info: NetworkInfo, - mut handler: T, mut receiver: Receiver, - mut sender: Sender, - mut stream_request_buffer: Arc>, - mut stream_response_buffer: Arc>, + mut network_core: Core, ) { // Loop to handle incoming application streams indefinitely. loop { @@ -770,24 +775,24 @@ impl Core { // Put back into the stream what we read from it AppData::Echo(message) => { // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); // Insert into the response buffer for it to be picked up - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); }, // Store a value in the DHT and (optionally) on explicit specific peers AppData::KademliaStoreRecord { key, value, expiration_time, explicit_peers } => { // create a kad record - let mut record = Record::new(key, value); + let mut record = Record::new(key.clone(), value); // Set (optional) expiration time record.expires = expiration_time; // Insert into DHT - if let Ok(query_id) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { + if let Ok(_) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events // where we can get the response and then replace the buffer with the actual response data - stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(query_id), Box::new(stream_id)); + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); // Cache record on peers explicitly (if specified) if let Some(explicit_peers) = explicit_peers { @@ -800,49 +805,49 @@ impl Core { } } else { // Delete the request from the stream and then return a write error - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); // Return error - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); } }, // Perform a lookup in the DHT AppData::KademliaLookupRecord { key } => { - let query_id = swarm.behaviour_mut().kademlia.get_record(key.into()); + let _ = swarm.behaviour_mut().kademlia.get_record(key.clone().into()); // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events // where we can get the response and then replace the buffer with the actual response data - stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(query_id), Box::new(stream_id)); + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); }, // Perform a lookup of peers that store a record AppData::KademliaGetProviders { key } => { - let query_id = swarm.behaviour_mut().kademlia.get_providers(key.into()); + let _ = swarm.behaviour_mut().kademlia.get_providers(key.clone().into()); // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events // where we can get the response and then replace the buffer with the actual response data - stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(query_id), Box::new(stream_id)); + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); } // Stop providing a record on the network AppData::KademliaStopProviding { key } => { swarm.behaviour_mut().kademlia.stop_providing(&key.into()); // Respond with a success - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); } // Remove record from local store AppData::KademliaDeleteRecord { key } => { swarm.behaviour_mut().kademlia.remove_record(&key.into()); // Respond with a success - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); } // Return important routing table info AppData::KademliaGetRoutingTableInfo => { // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); // Insert into the response buffer for it to be picked up - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: network_info.id.to_string() })); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: network_info.id.to_string() })); }, // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { @@ -857,7 +862,7 @@ impl Core { // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events // where we can get the response and then replace the buffer with the actual response data - stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); } } } @@ -868,18 +873,17 @@ impl Core { // Dail peer NetworkData::DailPeer(multiaddr) => { // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { - if let Ok(_) = swarm.dial(multiaddr) { + if let Ok(_) = swarm.dial(multiaddr.clone()) { // Put result into response buffer - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); } else { - stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); + network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); } } } - _ => {} } } }, @@ -889,7 +893,7 @@ impl Core { address, } => { // call configured handler - handler.new_listen_addr(listener_id, address); + network_core.state.new_listen_addr(network_info.event_comm_channel.clone(), swarm.local_peer_id().to_owned(), listener_id, address); } SwarmEvent::Behaviour(event) => match event { // Ping @@ -929,7 +933,7 @@ impl Core { } // Call custom handler - handler.inbound_ping_success(peer, duration, sender.clone()); + network_core.state.inbound_ping_success(network_info.event_comm_channel.clone(), peer, duration); } // Outbound ping failure Err(err_type) => { @@ -998,7 +1002,7 @@ impl Core { } // Call custom handler - handler.outbound_ping_error(peer, err_type, sender.clone()); + network_core.state.outbound_ping_error(network_info.event_comm_channel.clone(), peer, err_type); } } } @@ -1013,110 +1017,90 @@ impl Core { peer_id.to_base58() }).collect::>(); - // Inform the application that requested for it - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings }) )).await; - } - kad::QueryResult::GetProviders(Err(_)) => { - // No providers for a particular key found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::NoProvidersFound))).await; + // Get `StreamId` + if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + network_core.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings })); + } + } } + kad::QueryResult::GetProviders(Err(_)) => {}, kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( - kad::PeerRecord { - record: kad::Record {key ,value, .. }, - .. - }, + kad::PeerRecord { record:kad::Record{ key, value, .. }, .. }, ))) => { - // Send result of the Kademlia DHT lookup to the application layer - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordFound { - key: key.to_vec(), value - }))).await; - } - kad::QueryResult::GetRecord(Ok(_)) => { - // No record found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up + // Get `StreamId` + if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + network_core.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::RecordFound { key: key.to_vec(), value: value.to_vec() })); + } + } } - kad::QueryResult::GetRecord(Err(_)) => { - // No record found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + kad::QueryResult::GetRecord(Ok(_)) => {}, + kad::QueryResult::GetRecord(Err(e)) => { + let key = match e { + kad::GetRecordError::NotFound { key, .. } => key, + kad::GetRecordError::QuorumFailed { key, .. } => key, + kad::GetRecordError::Timeout { key } => key, + }; // Delete the temporary data and then return error - // Get `StreamId` - if let Some(opaque_data) = stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { // Make sure this is temporary data (i.e `StreamId`) let opaque_data = &opaque_data as &dyn Any; if let Some(stream_id) = opaque_data.downcast_ref::() { // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); // Return error - stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::KadFetchRecordError(key.to_vec()))); } } } kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { // Call handler - handler.kademlia_put_record_success(key.to_vec(), sender.clone()); + network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()); } kad::QueryResult::PutRecord(Err(_)) => { // Call handler - handler.kademlia_put_record_error(); + network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()); } kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { key, })) => { // Call handler - handler.kademlia_start_providing_success(key.to_vec(), sender.clone()); + network_core.state.kademlia_start_providing_success(network_info.event_comm_channel.clone(), key.to_vec()); } kad::QueryResult::StartProviding(Err(_)) => { // Call handler - handler.kademlia_start_providing_error(sender.clone()); + network_core.state.kademlia_start_providing_error(network_info.event_comm_channel.clone()); } _ => {} }, - kad::Event::InboundRequest { request } => match request { - kad::InboundRequest::GetProvider { - num_closer_peers, - num_provider_peers, - } => { - - }, - kad::InboundRequest::AddProvider { record } => { - - }, - kad::InboundRequest::GetRecord { - num_closer_peers, - present_locally, - } => { - - }, - kad::InboundRequest::PutRecord { - source, - connection, - record, - } => { - - }, - _ => {} - }, - kad::Event::RoutingUpdated { - peer, - is_new_peer, - addresses, - bucket_range, - old_peer, - } => todo!(), - kad::Event::UnroutablePeer { peer } => todo!(), - kad::Event::RoutablePeer { peer, address } => todo!(), - kad::Event::PendingRoutablePeer { peer, address } => todo!(), - kad::Event::ModeChanged { new_mode } => todo!(), + // Other events we don't care about + _ => {} }, // Identify CoreEvent::Identify(event) => match event { identify::Event::Received { peer_id, info } => { // We just recieved an `Identify` info from a peer.s - handler.identify_info_recieved(peer_id, info.clone(), sender.clone()); + network_core.state.identify_info_recieved(network_info.event_comm_channel.clone(), peer_id, info.clone()); // disconnect from peer of the network id is different if info.protocol_version != network_info.id.as_ref() { @@ -1132,20 +1116,20 @@ impl Core { }, // Request-response CoreEvent::RequestResponse(event) => match event { - request_response::Event::Message { peer, message } => match message { + request_response::Event::Message { peer: _, message } => match message { // A request just came in - request_response::Message::Request { request_id, request, channel } => { + request_response::Message::Request { request_id: _, request, channel } => { // Parse request match request { Rpc::ReqResponse { data } => { // Pass request data to configured request handler - let response_data = handler.handle_incoming_message(data); + let response_data = network_core.state.handle_incoming_message(data); // construct an RPC let response_rpc = Rpc::ReqResponse { data: response_data }; // Send the response - swarm.behaviour_mut().request_response.send_response(channel, response_rpc); + let _ = swarm.behaviour_mut().request_response.send_response(channel, response_rpc); } } }, @@ -1154,34 +1138,38 @@ impl Core { // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up // Get `StreamId` - if let Some(opaque_data) = stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { // Make sure this is temporary data (i.e `StreamId`) let opaque_data = &opaque_data as &dyn Any; if let Some(stream_id) = opaque_data.downcast_ref::() { // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); - // Insert response message the response buffer for it to be picked up - stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(message.into())); + match response { + Rpc::ReqResponse { data } => { + // Insert response message the response buffer for it to be picked up + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(data)); + }, + } } } }, }, - request_response::Event::OutboundFailure { peer, request_id, error } => { + request_response::Event::OutboundFailure { request_id, .. } => { // Delete the temporary data and then return error // Get `StreamId` - if let Some(opaque_data) = stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { // Make sure this is temporary data (i.e `StreamId`) let opaque_data = &opaque_data as &dyn Any; if let Some(stream_id) = opaque_data.downcast_ref::() { // Remove the request from the request buffer - stream_request_buffer.lock().await.remove(&stream_id); + network_core.stream_request_buffer.lock().await.remove(&stream_id); // Return error - stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); } } }, @@ -1193,18 +1181,17 @@ impl Core { connection_id, endpoint, num_established, - concurrent_dial_errors, + concurrent_dial_errors: _, established_in, } => { // call configured handler - handler.connection_established( + network_core.state.connection_established( + network_info.event_comm_channel.clone(), peer_id, connection_id, &endpoint, num_established, - concurrent_dial_errors, established_in, - sender.clone() ); } SwarmEvent::ConnectionClosed { @@ -1215,13 +1202,13 @@ impl Core { cause, } => { // call configured handler - handler.connection_closed( + network_core.state.connection_closed( + network_info.event_comm_channel.clone(), peer_id, connection_id, &endpoint, num_established, cause, - sender.clone() ); } SwarmEvent::ExpiredListenAddr { @@ -1229,7 +1216,7 @@ impl Core { address, } => { // call configured handler - handler.expired_listen_addr(listener_id, address, sender.clone()); + network_core.state.expired_listen_addr(network_info.event_comm_channel.clone(), listener_id, address); } SwarmEvent::ListenerClosed { listener_id, @@ -1237,33 +1224,33 @@ impl Core { reason: _, } => { // call configured handler - handler.listener_closed(listener_id, addresses, sender.clone()); + network_core.state.listener_closed(network_info.event_comm_channel.clone(), listener_id, addresses); } SwarmEvent::ListenerError { listener_id, error: _, } => { // call configured handler - handler.listener_error(listener_id, sender.clone()); + network_core.state.listener_error(network_info.event_comm_channel.clone(), listener_id); } SwarmEvent::Dialing { peer_id, connection_id, } => { // call configured handler - handler.dialing(peer_id, connection_id, sender.clone()); + network_core.state.dialing(network_info.event_comm_channel.clone(), peer_id, connection_id); } SwarmEvent::NewExternalAddrCandidate { address } => { // call configured handler - handler.new_external_addr_candidate(address, sender.clone()); + network_core.state.new_external_addr_candidate(network_info.event_comm_channel.clone(), address); } SwarmEvent::ExternalAddrConfirmed { address } => { // call configured handler - handler.external_addr_confirmed(address, sender.clone()); + network_core.state.external_addr_confirmed(network_info.event_comm_channel.clone(), address); } SwarmEvent::ExternalAddrExpired { address } => { // call configured handler - handler.external_addr_expired(address, sender.clone()); + network_core.state.external_addr_expired(network_info.event_comm_channel.clone(), address); } SwarmEvent::IncomingConnection { connection_id, @@ -1271,7 +1258,7 @@ impl Core { send_back_addr, } => { // call configured handler - handler.incoming_connection(connection_id, local_addr, send_back_addr, sender.clone()); + network_core.state.incoming_connection(network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr); } SwarmEvent::IncomingConnectionError { connection_id, @@ -1280,11 +1267,11 @@ impl Core { error: _, } => { // call configured handler - handler.incoming_connection_error( + network_core.state.incoming_connection_error( + network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr, - sender.clone() ); } SwarmEvent::OutgoingConnectionError { @@ -1293,7 +1280,7 @@ impl Core { error: _, } => { // call configured handler - handler.outgoing_connection_error(connection_id, peer_id, sender.clone()); + network_core.state.outgoing_connection_error(network_info.event_comm_channel.clone(), connection_id, peer_id); } _ => todo!(), } @@ -1301,264 +1288,3 @@ impl Core { } } } - -/// The high level trait that provides default implementations to handle most supported network -/// swarm events. -pub trait EventHandler { - /// Event that informs the network core that we have started listening on a new multiaddr. - fn new_listen_addr(&mut self, _listener_id: ListenerId, _addr: Multiaddr) {} - - /// Event that informs the network core about a newly established connection to a peer. - fn connection_established( - &mut self, - _peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: NonZeroU32, - _concurrent_dial_errors: Option)>>, - _established_in: Duration, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that informs the network core about a closed connection to a peer. - fn connection_closed( - &mut self, - _peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: u32, - _cause: Option, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces expired listen address. - fn expired_listen_addr( - &mut self, - _listener_id: ListenerId, - _address: Multiaddr, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces a closed listener. - fn listener_closed( - &mut self, - _listener_id: ListenerId, - _addresses: Vec, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces a listener error. - fn listener_error( - &mut self, - _listener_id: ListenerId, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces a dialing attempt. - fn dialing( - &mut self, - _peer_id: Option, - _connection_id: ConnectionId, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces a new external address candidate. - fn new_external_addr_candidate( - &mut self, - _address: Multiaddr, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces a confirmed external address. - fn external_addr_confirmed( - &mut self, - _address: Multiaddr, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces an expired external address. - fn external_addr_expired( - &mut self, - _address: Multiaddr, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces new connection arriving on a listener and in the process of - /// protocol negotiation. - fn incoming_connection( - &mut self, - _connection_id: ConnectionId, - _local_addr: Multiaddr, - _send_back_addr: Multiaddr, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces an error happening on an inbound connection during its initial - /// handshake. - fn incoming_connection_error( - &mut self, - _connection_id: ConnectionId, - _local_addr: Multiaddr, - _send_back_addr: Multiaddr, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces an error happening on an outbound connection during its initial - /// handshake. - fn outgoing_connection_error( - &mut self, - _connection_id: ConnectionId, - _peer_id: Option, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces the arrival of a ping message from a peer. - /// The duration it took for a round trip is also returned - fn inbound_ping_success( - &mut self, - _peer_id: PeerId, - _duration: Duration, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces a `Ping` error - fn outbound_ping_error( - &mut self, - _peer_id: PeerId, - _err_type: Failure, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol - fn identify_info_recieved( - &mut self, - _peer_id: PeerId, - _info: Info, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces the successful write of a record to the DHT - fn kademlia_put_record_success( - &mut self, - _key: Vec, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces the failure of a node to save a record - fn kademlia_put_record_error(&mut self) { - // Default implementation - } - - /// Event that announces a node as a provider of a record in the DHT - fn kademlia_start_providing_success( - &mut self, - _key: Vec, - _application_sender: Sender, - ) { - // Default implementation - } - - /// Event that announces the failure of a node to become a provider of a record in the DHT - fn kademlia_start_providing_error(&mut self, _application_sender: Sender) { - // Default implementation - } - - /// Event that announces the arrival of an RPC message - fn handle_incoming_message(&mut self, data: Vec>) -> Vec>; -} - -/// Default network event handler -pub struct DefaultHandler; - -/// Implement [`EventHandler`] for [`DefaultHandler`] -impl EventHandler for DefaultHandler { - /// Echo the message back to the sender - fn handle_incoming_message(&mut self, data: Vec>) -> Vec> { - data - } -} - -/// Important information to obtain from the [`CoreBuilder`], to properly handle network -/// operations -struct NetworkInfo { - /// The name/id of the network - id: StreamProtocol, - /// Important information to manage `Ping` operations - ping: PingInfo, -} - -/// Module that contains important data structures to manage `Ping` operations on the network -mod ping_config { - use libp2p_identity::PeerId; - use std::{collections::HashMap, time::Duration}; - - /// Policies to handle a `Ping` error - /// - All connections to peers are closed during a disconnect operation. - pub enum PingErrorPolicy { - /// Do not disconnect under any circumstances - NoDisconnect, - /// Disconnect after a number of outbound errors - DisconnectAfterMaxErrors(u16), - /// Disconnect after a certain number of concurrent timeouts - DisconnectAfterMaxTimeouts(u16), - } - - /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] - #[derive(Debug)] - pub struct PingManager { - /// The number of timeout errors encountered from a peer - pub timeouts: HashMap, - /// The number of outbound errors encountered from a peer - pub outbound_errors: HashMap, - } - - /// The configuration for the `Ping` protocol - pub struct PingConfig { - /// The interval between successive pings. - /// Default is 15 seconds - pub interval: Duration, - /// The duration before which the request is considered failure. - /// Default is 20 seconds - pub timeout: Duration, - /// Error policy - pub err_policy: PingErrorPolicy, - } - - /// Critical information to manage `Ping` operations - pub struct PingInfo { - pub policy: PingErrorPolicy, - pub manager: PingManager, - } -} diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index 1701f1d51..8416dff66 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -1,10 +1,12 @@ -use libp2p::{kad::QueryId, request_response::OutboundRequestId}; +/// Copyright (c) 2024 Algorealm +use libp2p::request_response::OutboundRequestId; use rand::random; use serde::{Deserialize, Serialize}; -/// Copyright (c) 2024 Algorealm use std::{any::Any, time::Instant}; use thiserror::Error; +use self::ping_config::PingInfo; + use super::*; /// Type to indicate the duration (in seconds) to wait for data from the network layer before timing @@ -12,6 +14,7 @@ use super::*; pub const NETWORK_READ_TIMEOUT: u64 = 60; /// Data exchanged over a stream between the application and network layer +#[derive(Debug, Clone)] pub(super) enum StreamData { /// Application data sent over the stream Application(StreamId, AppData), @@ -20,7 +23,7 @@ pub(super) enum StreamData { } /// Data sent from the application layer to the networking layer -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum AppData { /// A simple echo message Echo(String), @@ -50,9 +53,8 @@ pub enum AppData { } /// Data sent from the networking to itself +#[derive(Debug, Clone)] pub(super) enum NetworkData { - /// A simple echo message - Echo(StreamId, String), /// Dail peer DailPeer(MultiaddrString), } @@ -61,8 +63,6 @@ pub(super) enum NetworkData { pub enum Kademlia { /// Return important information about the DHT, this will be increased shortly Info { protocol_id: String }, - /// Return the result of a DHT lookup operation - Result(DhtOps), } /// Network error type containing errors encountered during network operations @@ -75,7 +75,9 @@ pub enum NetworkError { #[error("failed to store record in DHT")] KadStoreRecordError(Vec), #[error("failed to fetch data from peer")] - RpcDataFetchError + RpcDataFetchError, + #[error("failed to fetch record from the DHT")] + KadFetchRecordError(Vec), } /// Operations performed on the Kademlia DHT @@ -112,14 +114,14 @@ impl StreamId { } /// Enum that represents various ids that can be tracked on the [`StreamResponseBuffer`] -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum TrackableStreamId { /// Our custom stream id Id(StreamId), /// RPC request id Outbound(OutboundRequestId), - /// Kademlia query id - Kad(QueryId), + /// Kademlia Key + Kad(Vec), } /// Type that specifies the result of querying the network layer @@ -143,6 +145,9 @@ macro_rules! impl_stream_response_for_types { ( $( $t:ident )* ) => { impl_stream_response_for_types!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize f32 f64 String bool Kademlia StreamId SwarmNlError NetworkError DhtOps); +/// Implement [`StreamResponseType`] for `Vec` +impl StreamResponseType for Vec where T: StreamResponseType {} + /// Type that keeps track of the requests from the application layer. /// This type has a maximum buffer size and will drop subsequent requests when full. /// It is unlikely to be ever full as the default is usize::MAX except otherwise specified during @@ -210,11 +215,6 @@ impl StreamResponseBuffer { pub fn remove(&mut self, id: &TrackableStreamId) -> Option> { self.buffer.remove(&id) } - - /// Check if buffer contains a value - pub fn contains(&mut self, id: &TrackableStreamId) -> bool { - self.buffer.contains_key(id) - } } /// Type representing the RPC data structure sent between nodes in the network @@ -231,3 +231,348 @@ pub struct RpcConfig { /// Maximum number of concurrent inbound + outbound streams pub max_concurrent_streams: usize, } + +/// The high level trait that provides default implementations to handle most supported network +/// swarm events. +pub trait EventHandler { + /// Event that informs the network core that we have started listening on a new multiaddr. + fn new_listen_addr( + &mut self, + _channel: NetworkChannel, + _local_peer_id: PeerId, + _listener_id: ListenerId, + _addr: Multiaddr, + ) { + } + + /// Event that informs the network core about a newly established connection to a peer. + fn connection_established( + &mut self, + _channel: NetworkChannel, + _peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: NonZeroU32, + _established_in: Duration, + ) { + // Default implementation + } + + /// Event that informs the network core about a closed connection to a peer. + fn connection_closed( + &mut self, + _channel: NetworkChannel, + _peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: u32, + _cause: Option, + ) { + // Default implementation + } + + /// Event that announces expired listen address. + fn expired_listen_addr( + &mut self, + _channel: NetworkChannel, + _listener_id: ListenerId, + _address: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces a closed listener. + fn listener_closed( + &mut self, + _channel: NetworkChannel, + _listener_id: ListenerId, + _addresses: Vec, + ) { + // Default implementation + } + + /// Event that announces a listener error. + fn listener_error(&mut self, _channel: NetworkChannel, _listener_id: ListenerId) { + // Default implementation + } + + /// Event that announces a dialing attempt. + fn dialing( + &mut self, + _channel: NetworkChannel, + _peer_id: Option, + _connection_id: ConnectionId, + ) { + // Default implementation + } + + /// Event that announces a new external address candidate. + fn new_external_addr_candidate(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces a confirmed external address. + fn external_addr_confirmed(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces an expired external address. + fn external_addr_expired(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces new connection arriving on a listener and in the process of + /// protocol negotiation. + fn incoming_connection( + &mut self, + _channel: NetworkChannel, + _connection_id: ConnectionId, + _local_addr: Multiaddr, + _send_back_addr: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces an error happening on an inbound connection during its initial + /// handshake. + fn incoming_connection_error( + &mut self, + _channel: NetworkChannel, + _connection_id: ConnectionId, + _local_addr: Multiaddr, + _send_back_addr: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces an error happening on an outbound connection during its initial + /// handshake. + fn outgoing_connection_error( + &mut self, + _channel: NetworkChannel, + _connection_id: ConnectionId, + _peer_id: Option, + ) { + // Default implementation + } + + /// Event that announces the arrival of a ping message from a peer. + /// The duration it took for a round trip is also returned + fn inbound_ping_success( + &mut self, + _channel: NetworkChannel, + _peer_id: PeerId, + _duration: Duration, + ) { + // Default implementation + } + + /// Event that announces a `Ping` error + fn outbound_ping_error( + &mut self, + _channel: NetworkChannel, + _peer_id: PeerId, + _err_type: Failure, + ) { + // Default implementation + } + + /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol + fn identify_info_recieved(&mut self, _channel: NetworkChannel, _peer_id: PeerId, _info: Info) { + // Default implementation + } + + /// Event that announces the successful write of a record to the DHT + fn kademlia_put_record_success(&mut self, _channel: NetworkChannel, _key: Vec) { + // Default implementation + } + + /// Event that announces the failure of a node to save a record + fn kademlia_put_record_error(&mut self, _channel: NetworkChannel) { + // Default implementation + } + + /// Event that announces a node as a provider of a record in the DHT + fn kademlia_start_providing_success(&mut self, _channel: NetworkChannel, _key: Vec) { + // Default implementation + } + + /// Event that announces the failure of a node to become a provider of a record in the DHT + fn kademlia_start_providing_error(&mut self, _channel: NetworkChannel) { + // Default implementation + } + + /// Event that announces the arrival of an RPC message + fn handle_incoming_message(&mut self, data: Vec>) -> Vec>; +} + +/// Default network event handler +pub struct DefaultHandler; + +/// Implement [`EventHandler`] for [`DefaultHandler`] +impl EventHandler for DefaultHandler { + /// Echo the message back to the sender + fn handle_incoming_message(&mut self, data: Vec>) -> Vec> { + data + } +} + +/// Important information to obtain from the [`CoreBuilder`], to properly handle network +/// operations +pub(super) struct NetworkInfo { + /// The name/id of the network + pub id: StreamProtocol, + /// Important information to manage `Ping` operations + pub ping: PingInfo, + /// Application's event handler network communication channel + pub event_comm_channel: NetworkChannel, +} + +/// Module that contains important data structures to manage `Ping` operations on the network +pub mod ping_config { + use libp2p_identity::PeerId; + use std::{collections::HashMap, time::Duration}; + + /// Policies to handle a `Ping` error + /// - All connections to peers are closed during a disconnect operation. + pub enum PingErrorPolicy { + /// Do not disconnect under any circumstances + NoDisconnect, + /// Disconnect after a number of outbound errors + DisconnectAfterMaxErrors(u16), + /// Disconnect after a certain number of concurrent timeouts + DisconnectAfterMaxTimeouts(u16), + } + + /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] + #[derive(Debug)] + pub struct PingManager { + /// The number of timeout errors encountered from a peer + pub timeouts: HashMap, + /// The number of outbound errors encountered from a peer + pub outbound_errors: HashMap, + } + + /// The configuration for the `Ping` protocol + pub struct PingConfig { + /// The interval between successive pings. + /// Default is 15 seconds + pub interval: Duration, + /// The duration before which the request is considered failure. + /// Default is 20 seconds + pub timeout: Duration, + /// Error policy + pub err_policy: PingErrorPolicy, + } + + /// Critical information to manage `Ping` operations + pub struct PingInfo { + pub policy: PingErrorPolicy, + pub manager: PingManager, + } +} + +/// Struct that allows the application layer comunicate with the network layer during event handling +#[derive(Clone)] +pub struct NetworkChannel { + /// The request buffer that keeps track of application requests being handled + stream_request_buffer: Arc>, + /// The consuming end of the stream that recieves data from the network layer + // application_receiver: Receiver, + /// This serves as a buffer for the results of the requests to the network layer. + /// With this, applications can make async requests and fetch their results at a later time + /// without waiting. This is made possible by storing a [`StreamId`] for a particular stream + /// request. + stream_response_buffer: Arc>, + /// The stream end for writing to the network layer + application_sender: Sender, + /// Current stream id. Useful for opening new streams, we just have to bump the number by 1 + current_stream_id: Arc>, + /// The network read timeout + network_read_delay: AsyncDuration, +} + +impl NetworkChannel { + /// Create a new application's event handler network communication channel + pub(super) fn new( + stream_request_buffer: Arc>, + stream_response_buffer: Arc>, + application_sender: Sender, + current_stream_id: Arc>, + network_read_delay: AsyncDuration, + ) -> NetworkChannel { + Self { + stream_request_buffer, + stream_response_buffer, + application_sender, + current_stream_id, + network_read_delay, + } + } + + /// Send data to the network layer and recieve a unique `StreamId` to track the request + /// If the internal stream buffer is full, `None` will be returned. + pub async fn send_to_network(&mut self, request: AppData) -> Option { + // Generate stream id + let stream_id = StreamId::next(*self.current_stream_id.lock().await); + let request = StreamData::Application(stream_id, request); + + // Add to request buffer + if !self.stream_request_buffer.lock().await.insert(stream_id) { + // Buffer appears to be full + return None; + } + + // Send request + if let Ok(_) = self.application_sender.send(request).await { + // Store latest stream id + *self.current_stream_id.lock().await = stream_id; + Some(stream_id) + } else { + return None; + } + } + + pub async fn recv_from_network(&mut self, stream_id: StreamId) -> NetworkResult { + let network_data_future = async { + loop { + // tight loop + if let Some(response_value) = self + .stream_response_buffer + .lock() + .await + .remove(&TrackableStreamId::Id(stream_id)) + { + // Make sure the value is not `StreamId`. If it is, then the request is being + // processed and the final value we want has not arrived. + + // Get inner boxed value + let inner_value = &response_value as &dyn Any; + if let None = inner_value.downcast_ref::() { + return response_value; + } + } + } + }; + + Ok(network_data_future + .timeout(self.network_read_delay) + .await + .map_err(|_| NetworkError::NetworkReadTimeout)?) + } + + /// Perform an atomic `send` and `recieve` from the network layer. This function is atomic and + /// blocks until the result of the request is returned from the network layer. This function + /// should mostly be used when the result of the request is needed immediately and delay can be + /// condoned. It will still timeout if the delay exceeds the configured period. + /// If the internal buffer is full, it will return an error. + pub async fn fetch_from_network(&mut self, request: AppData) -> NetworkResult { + // send request + if let Some(stream_id) = self.send_to_network(request).await { + // wait to recieve response from the network + self.recv_from_network(stream_id).await + } else { + Err(NetworkError::StreamBufferOverflow) + } + } +} diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 12aa0015a..25394e2d9 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -42,6 +42,9 @@ pub const MAX_PORT: u16 = 65535; /// Default network ID pub static DEFAULT_NETWORK_ID: &str = "/swarmnl/1.0"; +/// Default keep-alive network duration +pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; + /// Implement From<&str> for libp2p2_identity::KeyType. /// We'll define a custom trait because of the Rust visibility rule to solve this problem pub trait CustomFrom { From 29a35f4a714c68a60dd3869ed0e076460bd74102 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Tue, 7 May 2024 19:49:57 +0100 Subject: [PATCH 05/36] add: added more functionality to age_of_empire --- client/src/main.rs | 72 ++++++++++++++++++++++++++++++------ swarm_nl/Cargo.toml | 1 + swarm_nl/src/core/mod.rs | 2 +- swarm_nl/src/core/prelude.rs | 52 +++++++++++++++----------- swarm_nl/src/lib.rs | 1 + 5 files changed, 94 insertions(+), 34 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index 60a97cb3a..e7c495cee 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,8 +4,9 @@ /// Objective: Form alliances and conquer as much empires as possible! /// It is a multi-player game /// Enjoy! -use std::{num::NonZeroU32, time::Duration}; +use std::{any::Any, borrow::Cow, num::NonZeroU32, time::Duration}; use swarm_nl::{ + async_trait, core::EventHandler, core::{AppData, Core, CoreBuilder, NetworkChannel}, setup::BootstrapConfig, @@ -23,11 +24,11 @@ async fn main() { #[derive(Clone)] pub struct Empire { name: String, - soldiers: u32, - farmers: u32, - blacksmith: u32, - land_mass: u32, - gold_reserve: u32, + soldiers: u8, + farmers: u8, + blacksmith: u8, + land_mass: u8, + gold_reserve: u8, } impl Empire { @@ -44,10 +45,11 @@ impl Empire { } } +#[async_trait] impl EventHandler for Empire { - fn new_listen_addr( + async fn new_listen_addr( &mut self, - channel: NetworkChannel, + _channel: NetworkChannel, local_peer_id: PeerId, _listener_id: swarm_nl::ListenerId, addr: swarm_nl::Multiaddr, @@ -61,20 +63,66 @@ impl EventHandler for Empire { ); } - fn connection_established( + async fn connection_established( &mut self, - channel: NetworkChannel, + mut channel: NetworkChannel, peer_id: PeerId, _connection_id: ConnectionId, _endpoint: &ConnectedPoint, _num_established: NonZeroU32, _established_in: Duration, ) { + // When we establish a new connection, the empires send message to the other empire to knoe + // their military status + let request = vec!["military_status".as_bytes().to_vec()]; + + // Prepare request + let status_request = AppData::FetchData { + keys: request, + peer: peer_id, + }; + + // Send request + if let Some(stream_id) = channel.send_to_network(status_request).await { + // Get response + // AppData::Fetch returns a Vec>, hence we can parse the response from it + let status_response = channel.recv_from_network(stream_id).await; + + let inner_value = &status_response as &dyn Any; + if let Some(status) = inner_value.downcast_ref::>>() { + // Get empire name + let empire_name = String::from_utf8_lossy(&status[0]); + let military_status = status[1][0]; + + // Print the military status of the empire we just contacted + println!("Empire Contacted:"); + println!("Name: {} Empire", empire_name); + println!("Military Capacity: {} Soldiers", military_status); + } else { + println!("Could not decode response") + } + } else { + println!("Could not get military status of the empire at {}", peer_id); + } } + /// Handle any incoming RPC from any neighbouring empire fn handle_incoming_message(&mut self, data: Vec>) -> Vec> { - println!("Incoming data: {:?}", data); - data + // The semantics is left to the application to handle + match String::from_utf8_lossy(&data[0]) { + // Handle the request to get military status + Cow::Borrowed("military_status") => { + // Get empire name + let empire_name = self.name.as_bytes().to_vec(); + + // Get military capacity + let military_capacity = self.soldiers; + + // marshall into accepted format andd then return it + vec![empire_name, vec![military_capacity]] + }, + _ => Default::default(), + } } } diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index d9bb67e49..3f12b6f96 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -14,6 +14,7 @@ libp2p-identity = { version="0.2.8", "features"=["secp256k1", "ecdsa", "rsa", "e futures = "0.3.30" futures-time = "3.0.0" serde = "1.0.200" +async-trait = "0.1.80" [dependencies.async-std] version = "1.12.0" diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 304b3b89c..9bbfaa120 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -852,7 +852,7 @@ impl Core { // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { // Construct the RPC object - let rpc = Rpc::ReqResponse { data: keys.into_iter().map(|s| s.into_bytes()).collect() }; + let rpc = Rpc::ReqResponse { data: keys }; // Inform the swarm to make the request let outbound_id = swarm diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index 8416dff66..01e449f0e 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -1,4 +1,7 @@ +use async_trait::async_trait; /// Copyright (c) 2024 Algorealm + + use libp2p::request_response::OutboundRequestId; use rand::random; use serde::{Deserialize, Serialize}; @@ -47,7 +50,7 @@ pub enum AppData { /// Return important information about the local routing table KademliaGetRoutingTableInfo, /// Fetch data(s) quickly from a peer over the network - FetchData { keys: Vec, peer: PeerId }, + FetchData { keys: Vec>, peer: PeerId }, // Get network information // Gossip related requests } @@ -234,19 +237,21 @@ pub struct RpcConfig { /// The high level trait that provides default implementations to handle most supported network /// swarm events. +#[async_trait] pub trait EventHandler { /// Event that informs the network core that we have started listening on a new multiaddr. - fn new_listen_addr( + async fn new_listen_addr( &mut self, _channel: NetworkChannel, _local_peer_id: PeerId, _listener_id: ListenerId, _addr: Multiaddr, ) { + // Default implementation } /// Event that informs the network core about a newly established connection to a peer. - fn connection_established( + async fn connection_established( &mut self, _channel: NetworkChannel, _peer_id: PeerId, @@ -259,7 +264,7 @@ pub trait EventHandler { } /// Event that informs the network core about a closed connection to a peer. - fn connection_closed( + async fn connection_closed( &mut self, _channel: NetworkChannel, _peer_id: PeerId, @@ -272,7 +277,7 @@ pub trait EventHandler { } /// Event that announces expired listen address. - fn expired_listen_addr( + async fn expired_listen_addr( &mut self, _channel: NetworkChannel, _listener_id: ListenerId, @@ -282,7 +287,7 @@ pub trait EventHandler { } /// Event that announces a closed listener. - fn listener_closed( + async fn listener_closed( &mut self, _channel: NetworkChannel, _listener_id: ListenerId, @@ -292,12 +297,12 @@ pub trait EventHandler { } /// Event that announces a listener error. - fn listener_error(&mut self, _channel: NetworkChannel, _listener_id: ListenerId) { + async fn listener_error(&mut self, _channel: NetworkChannel, _listener_id: ListenerId) { // Default implementation } /// Event that announces a dialing attempt. - fn dialing( + async fn dialing( &mut self, _channel: NetworkChannel, _peer_id: Option, @@ -307,23 +312,23 @@ pub trait EventHandler { } /// Event that announces a new external address candidate. - fn new_external_addr_candidate(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + async fn new_external_addr_candidate(&mut self, _channel: NetworkChannel, _address: Multiaddr) { // Default implementation } /// Event that announces a confirmed external address. - fn external_addr_confirmed(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + async fn external_addr_confirmed(&mut self, _channel: NetworkChannel, _address: Multiaddr) { // Default implementation } /// Event that announces an expired external address. - fn external_addr_expired(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + async fn external_addr_expired(&mut self, _channel: NetworkChannel, _address: Multiaddr) { // Default implementation } /// Event that announces new connection arriving on a listener and in the process of /// protocol negotiation. - fn incoming_connection( + async fn incoming_connection( &mut self, _channel: NetworkChannel, _connection_id: ConnectionId, @@ -335,7 +340,7 @@ pub trait EventHandler { /// Event that announces an error happening on an inbound connection during its initial /// handshake. - fn incoming_connection_error( + async fn incoming_connection_error( &mut self, _channel: NetworkChannel, _connection_id: ConnectionId, @@ -347,7 +352,7 @@ pub trait EventHandler { /// Event that announces an error happening on an outbound connection during its initial /// handshake. - fn outgoing_connection_error( + async fn outgoing_connection_error( &mut self, _channel: NetworkChannel, _connection_id: ConnectionId, @@ -358,7 +363,7 @@ pub trait EventHandler { /// Event that announces the arrival of a ping message from a peer. /// The duration it took for a round trip is also returned - fn inbound_ping_success( + async fn inbound_ping_success( &mut self, _channel: NetworkChannel, _peer_id: PeerId, @@ -368,7 +373,7 @@ pub trait EventHandler { } /// Event that announces a `Ping` error - fn outbound_ping_error( + async fn outbound_ping_error( &mut self, _channel: NetworkChannel, _peer_id: PeerId, @@ -378,27 +383,32 @@ pub trait EventHandler { } /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol - fn identify_info_recieved(&mut self, _channel: NetworkChannel, _peer_id: PeerId, _info: Info) { + async fn identify_info_recieved( + &mut self, + _channel: NetworkChannel, + _peer_id: PeerId, + _info: Info, + ) { // Default implementation } /// Event that announces the successful write of a record to the DHT - fn kademlia_put_record_success(&mut self, _channel: NetworkChannel, _key: Vec) { + async fn kademlia_put_record_success(&mut self, _channel: NetworkChannel, _key: Vec) { // Default implementation } /// Event that announces the failure of a node to save a record - fn kademlia_put_record_error(&mut self, _channel: NetworkChannel) { + async fn kademlia_put_record_error(&mut self, _channel: NetworkChannel) { // Default implementation } /// Event that announces a node as a provider of a record in the DHT - fn kademlia_start_providing_success(&mut self, _channel: NetworkChannel, _key: Vec) { + async fn kademlia_start_providing_success(&mut self, _channel: NetworkChannel, _key: Vec) { // Default implementation } /// Event that announces the failure of a node to become a provider of a record in the DHT - fn kademlia_start_providing_error(&mut self, _channel: NetworkChannel) { + async fn kademlia_start_providing_error(&mut self, _channel: NetworkChannel) { // Default implementation } diff --git a/swarm_nl/src/lib.rs b/swarm_nl/src/lib.rs index 3a08d2bd7..e3cd08180 100644 --- a/swarm_nl/src/lib.rs +++ b/swarm_nl/src/lib.rs @@ -13,6 +13,7 @@ pub use libp2p::{ swarm::ConnectionId, }; pub use libp2p_identity::{rsa::Keypair as RsaKeypair, KeyType, Keypair, PeerId}; +pub use async_trait::async_trait; mod prelude; pub mod util; From fc1538331e80c06ac16258175f2462d8117a6724 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Tue, 7 May 2024 19:56:23 +0100 Subject: [PATCH 06/36] fix: added 'await' to all the futures of the application event handler functions --- swarm_nl/src/core/mod.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 9bbfaa120..6080be2f0 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -893,7 +893,7 @@ impl Core { address, } => { // call configured handler - network_core.state.new_listen_addr(network_info.event_comm_channel.clone(), swarm.local_peer_id().to_owned(), listener_id, address); + network_core.state.new_listen_addr(network_info.event_comm_channel.clone(), swarm.local_peer_id().to_owned(), listener_id, address).await; } SwarmEvent::Behaviour(event) => match event { // Ping @@ -933,7 +933,7 @@ impl Core { } // Call custom handler - network_core.state.inbound_ping_success(network_info.event_comm_channel.clone(), peer, duration); + network_core.state.inbound_ping_success(network_info.event_comm_channel.clone(), peer, duration).await; } // Outbound ping failure Err(err_type) => { @@ -1002,7 +1002,7 @@ impl Core { } // Call custom handler - network_core.state.outbound_ping_error(network_info.event_comm_channel.clone(), peer, err_type); + network_core.state.outbound_ping_error(network_info.event_comm_channel.clone(), peer, err_type).await; } } } @@ -1075,21 +1075,21 @@ impl Core { } kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { // Call handler - network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()); + network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()).await; } kad::QueryResult::PutRecord(Err(_)) => { // Call handler - network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()); + network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()).await; } kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { key, })) => { // Call handler - network_core.state.kademlia_start_providing_success(network_info.event_comm_channel.clone(), key.to_vec()); + network_core.state.kademlia_start_providing_success(network_info.event_comm_channel.clone(), key.to_vec()).await; } kad::QueryResult::StartProviding(Err(_)) => { // Call handler - network_core.state.kademlia_start_providing_error(network_info.event_comm_channel.clone()); + network_core.state.kademlia_start_providing_error(network_info.event_comm_channel.clone()).await; } _ => {} }, @@ -1100,7 +1100,7 @@ impl Core { CoreEvent::Identify(event) => match event { identify::Event::Received { peer_id, info } => { // We just recieved an `Identify` info from a peer.s - network_core.state.identify_info_recieved(network_info.event_comm_channel.clone(), peer_id, info.clone()); + network_core.state.identify_info_recieved(network_info.event_comm_channel.clone(), peer_id, info.clone()).await; // disconnect from peer of the network id is different if info.protocol_version != network_info.id.as_ref() { @@ -1192,7 +1192,7 @@ impl Core { &endpoint, num_established, established_in, - ); + ).await; } SwarmEvent::ConnectionClosed { peer_id, @@ -1209,14 +1209,14 @@ impl Core { &endpoint, num_established, cause, - ); + ).await; } SwarmEvent::ExpiredListenAddr { listener_id, address, } => { // call configured handler - network_core.state.expired_listen_addr(network_info.event_comm_channel.clone(), listener_id, address); + network_core.state.expired_listen_addr(network_info.event_comm_channel.clone(), listener_id, address).await; } SwarmEvent::ListenerClosed { listener_id, @@ -1224,33 +1224,33 @@ impl Core { reason: _, } => { // call configured handler - network_core.state.listener_closed(network_info.event_comm_channel.clone(), listener_id, addresses); + network_core.state.listener_closed(network_info.event_comm_channel.clone(), listener_id, addresses).await; } SwarmEvent::ListenerError { listener_id, error: _, } => { // call configured handler - network_core.state.listener_error(network_info.event_comm_channel.clone(), listener_id); + network_core.state.listener_error(network_info.event_comm_channel.clone(), listener_id).await; } SwarmEvent::Dialing { peer_id, connection_id, } => { // call configured handler - network_core.state.dialing(network_info.event_comm_channel.clone(), peer_id, connection_id); + network_core.state.dialing(network_info.event_comm_channel.clone(), peer_id, connection_id).await; } SwarmEvent::NewExternalAddrCandidate { address } => { // call configured handler - network_core.state.new_external_addr_candidate(network_info.event_comm_channel.clone(), address); + network_core.state.new_external_addr_candidate(network_info.event_comm_channel.clone(), address).await; } SwarmEvent::ExternalAddrConfirmed { address } => { // call configured handler - network_core.state.external_addr_confirmed(network_info.event_comm_channel.clone(), address); + network_core.state.external_addr_confirmed(network_info.event_comm_channel.clone(), address).await; } SwarmEvent::ExternalAddrExpired { address } => { // call configured handler - network_core.state.external_addr_expired(network_info.event_comm_channel.clone(), address); + network_core.state.external_addr_expired(network_info.event_comm_channel.clone(), address).await; } SwarmEvent::IncomingConnection { connection_id, @@ -1258,7 +1258,7 @@ impl Core { send_back_addr, } => { // call configured handler - network_core.state.incoming_connection(network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr); + network_core.state.incoming_connection(network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr).await; } SwarmEvent::IncomingConnectionError { connection_id, @@ -1272,7 +1272,7 @@ impl Core { connection_id, local_addr, send_back_addr, - ); + ).await; } SwarmEvent::OutgoingConnectionError { connection_id, @@ -1280,7 +1280,7 @@ impl Core { error: _, } => { // call configured handler - network_core.state.outgoing_connection_error(network_info.event_comm_channel.clone(), connection_id, peer_id); + network_core.state.outgoing_connection_error(network_info.event_comm_channel.clone(), connection_id, peer_id).await; } _ => todo!(), } From 018445d96d85e7fc1b1856b34de2c0061bf356be Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Wed, 8 May 2024 00:04:44 +0100 Subject: [PATCH 07/36] fix: added base58 decoding of a peer id string --- client/bootstrap_config.ini | 9 +-------- client/src/main.rs | 29 ++++++++++++++++------------- swarm_nl/Cargo.toml | 1 + swarm_nl/src/core/mod.rs | 8 ++++++-- swarm_nl/src/setup.rs | 1 - 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/client/bootstrap_config.ini b/client/bootstrap_config.ini index eddfa8643..e9d0eaefa 100644 --- a/client/bootstrap_config.ini +++ b/client/bootstrap_config.ini @@ -7,13 +7,6 @@ tcp=49200 ; UDP port to listen on udp=49201 -; compulsory -[auth] -; Type of keypair to generate for node identity and message auth e.g RSA, EDSA, Ed25519 -crypto=Ed25519 -; The protobuf serialized format of the node's cryptographic keypair -protobuf_keypair=[] - [bootstrap] ; The boostrap nodes to connect to immediately after start up -boot_nodes=[] \ No newline at end of file +boot_nodes=[12D3KooWCRscGJpuYAy4Cqrj5c9xsg1J8qCsmZpJsSYVdYQ5nmxc:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index e7c495cee..e6d865f2f 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -128,26 +128,26 @@ impl EventHandler for Empire { /// Setup game (This is for the persian Empire) /// This requires no bootnodes connection -#[cfg(not(feature = "macedonian"))] -pub async fn setup_game() -> Core { - // First, we want to configure our node - let config = BootstrapConfig::default(); +// #[cfg(not(feature = "macedonian"))] +// pub async fn setup_game() -> Core { +// // First, we want to configure our node +// let config = BootstrapConfig::default(); - // State kept by this node - let empire = Empire::new(String::from("Spartan")); +// // State kept by this node +// let empire = Empire::new(String::from("Macedonian")); - // Set up network - CoreBuilder::with_config(config, empire) - .build() - .await - .unwrap() -} +// // Set up network +// CoreBuilder::with_config(config, empire) +// .build() +// .await +// .unwrap() +// } /// The Macedonian Empire setup. /// These require bootnodes of empires to form alliance. /// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot /// parameters -#[cfg(feature = "macedonian")] +// #[cfg(feature = "macedonian")] pub async fn setup_game() -> Core { // First, we want to configure our node with the bootstrap config file on disk let config = BootstrapConfig::from_file("bootstrap_config.ini"); @@ -174,4 +174,7 @@ pub async fn play_game() { println!("Black smiths: {}", core.state.blacksmith); println!("Land mass: {}", core.state.land_mass); println!("Gold reserve: {}", core.state.gold_reserve); + + // Keep looping so we can record network events + loop { } } diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index 3f12b6f96..8e6424893 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -15,6 +15,7 @@ futures = "0.3.30" futures-time = "3.0.0" serde = "1.0.200" async-trait = "0.1.80" +base58 = "0.2.0" [dependencies.async-std] version = "1.12.0" diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 6080be2f0..cc9a0e464 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -9,6 +9,8 @@ use std::{ time::Duration, }; +use base58::FromBase58; + use futures::{ channel::mpsc::{self, Receiver, Sender}, select, SinkExt, StreamExt, @@ -478,7 +480,9 @@ impl CoreBuilder { // Add bootnodes to local routing table, if any for peer_info in self.boot_nodes { // PeerId - if let Ok(peer_id) = PeerId::from_bytes(peer_info.0.as_bytes()) { + if let Ok(peer_id) = PeerId::from_bytes(&peer_info.0.from_base58().unwrap_or_default()) + { + println!("{:?}", peer_id); // Multiaddress if let Ok(multiaddr) = multiaddr::from_url(&peer_info.1) { swarm @@ -798,7 +802,7 @@ impl Core { if let Some(explicit_peers) = explicit_peers { // Extract PeerIds let peers = explicit_peers.iter().map(|peer_id_string| { - PeerId::from_bytes(peer_id_string.as_bytes()) + PeerId::from_bytes(&peer_id_string.from_base58().unwrap_or_default()) }).filter_map(Result::ok).collect::>(); swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); diff --git a/swarm_nl/src/setup.rs b/swarm_nl/src/setup.rs index 7abc56955..7cdd42a28 100644 --- a/swarm_nl/src/setup.rs +++ b/swarm_nl/src/setup.rs @@ -113,7 +113,6 @@ impl BootstrapConfig { /// 1. If the key type is valid, but the keypair data is not valid for that key type. /// 2. If the key type is invalid. pub fn generate_keypair_from_protobuf(self, key_type_str: &str, bytes: &mut [u8]) -> Self { - // Parse the key type if let Some(key_type) = ::from(key_type_str) { let raw_keypair = Keypair::from_protobuf_encoding(bytes).unwrap(); From a37cf4660b208c08cb1fba190f2dd488f4cfe988 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Thu, 9 May 2024 00:44:01 +0100 Subject: [PATCH 08/36] chore: bebug request-response --- client/Cargo.toml | 1 + client/bootstrap_config.ini | 2 +- client/src/main.rs | 136 ++-- swarm_nl/src/core/mod.rs | 1316 +++++++++++++++++++++------------- swarm_nl/src/core/prelude.rs | 141 +++- 5 files changed, 1024 insertions(+), 572 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 87733dbb4..87503c9ea 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" rust-ini = "0.20.0" swarm_nl = { path = "../swarm_nl", features = ["tokio-runtime"] } tokio = { version = "1.37.0", features = ["full"] } +base58 = "0.2.0" diff --git a/client/bootstrap_config.ini b/client/bootstrap_config.ini index e9d0eaefa..2d62cdc86 100644 --- a/client/bootstrap_config.ini +++ b/client/bootstrap_config.ini @@ -9,4 +9,4 @@ udp=49201 [bootstrap] ; The boostrap nodes to connect to immediately after start up -boot_nodes=[12D3KooWCRscGJpuYAy4Cqrj5c9xsg1J8qCsmZpJsSYVdYQ5nmxc:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file +boot_nodes=[12D3KooWGsAYg7bBqCfRqvfAn3djpFoXGxX73nXQNFsGfDFSGUSU:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index e6d865f2f..628a00598 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,17 +4,15 @@ /// Objective: Form alliances and conquer as much empires as possible! /// It is a multi-player game /// Enjoy! -use std::{any::Any, borrow::Cow, num::NonZeroU32, time::Duration}; +use std::{any::Any, borrow::Cow, num::NonZeroU32, sync::Arc, time::Duration}; use swarm_nl::{ async_trait, core::EventHandler, - core::{AppData, Core, CoreBuilder, NetworkChannel}, + core::{AppData, Core, CoreBuilder, Mutex, NetworkChannel, StreamId}, setup::BootstrapConfig, - ConnectedPoint, ConnectionId, PeerId, + ConnectedPoint, ConnectionId, PeerId, Sender, SinkExt, }; -pub static CONFIG_FILE_PATH: &str = "test_config.ini"; - #[tokio::main] async fn main() { // Start our game! Age of Empires! @@ -49,7 +47,7 @@ impl Empire { impl EventHandler for Empire { async fn new_listen_addr( &mut self, - _channel: NetworkChannel, + mut channel: NetworkChannel, local_peer_id: PeerId, _listener_id: swarm_nl::ListenerId, addr: swarm_nl::Multiaddr, @@ -72,6 +70,7 @@ impl EventHandler for Empire { _num_established: NonZeroU32, _established_in: Duration, ) { + println!("Connection established with peer: {}", peer_id); // When we establish a new connection, the empires send message to the other empire to knoe // their military status let request = vec!["military_status".as_bytes().to_vec()]; @@ -83,27 +82,33 @@ impl EventHandler for Empire { }; // Send request - if let Some(stream_id) = channel.send_to_network(status_request).await { - // Get response - // AppData::Fetch returns a Vec>, hence we can parse the response from it - let status_response = channel.recv_from_network(stream_id).await; - - let inner_value = &status_response as &dyn Any; - if let Some(status) = inner_value.downcast_ref::>>() { - // Get empire name - let empire_name = String::from_utf8_lossy(&status[0]); - let military_status = status[1][0]; - - // Print the military status of the empire we just contacted - println!("Empire Contacted:"); - println!("Name: {} Empire", empire_name); - println!("Military Capacity: {} Soldiers", military_status); - } else { - println!("Could not decode response") - } + let stream_id = channel + .send_to_network(status_request) + .await + .unwrap(); + + // Get response + // AppData::Fetch returns a Vec>, hence we can parse the response from it + let status_response = channel + .recv_from_network(stream_id) + .await; + + let inner_value = &status_response as &dyn Any; + if let Some(status) = inner_value.downcast_ref::>>() { + // Get empire name + let empire_name = String::from_utf8_lossy(&status[0]); + let military_status = status[1][0]; + + // Print the military status of the empire we just contacted + println!("Empire Contacted:"); + println!("Name: {} Empire", empire_name); + println!("Military Capacity: {} Soldiers", military_status); } else { - println!("Could not get military status of the empire at {}", peer_id); + println!("Could not decode response") } + // } else { + // println!("Could not get military status of the empire at {}", peer_id); + // } } /// Handle any incoming RPC from any neighbouring empire @@ -129,9 +134,28 @@ impl EventHandler for Empire { /// Setup game (This is for the persian Empire) /// This requires no bootnodes connection // #[cfg(not(feature = "macedonian"))] +pub async fn setup_game() -> Core { + // First, we want to configure our node + let config = BootstrapConfig::default(); + + // State kept by this node + let empire = Empire::new(String::from("Spartan")); + + // Set up network + CoreBuilder::with_config(config, empire) + .build() + .await + .unwrap() +} + +/// The Macedonian Empire setup. +/// These require bootnodes of empires to form alliance. +/// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot +/// parameters +// #[cfg(feature = "macedonian")] // pub async fn setup_game() -> Core { -// // First, we want to configure our node -// let config = BootstrapConfig::default(); +// // First, we want to configure our node with the bootstrap config file on disk +// let config = BootstrapConfig::from_file("bootstrap_config.ini"); // // State kept by this node // let empire = Empire::new(String::from("Macedonian")); @@ -143,29 +167,10 @@ impl EventHandler for Empire { // .unwrap() // } -/// The Macedonian Empire setup. -/// These require bootnodes of empires to form alliance. -/// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot -/// parameters -// #[cfg(feature = "macedonian")] -pub async fn setup_game() -> Core { - // First, we want to configure our node with the bootstrap config file on disk - let config = BootstrapConfig::from_file("bootstrap_config.ini"); - - // State kept by this node - let empire = Empire::new(String::from("Macedonian")); - - // Set up network - CoreBuilder::with_config(config, empire) - .build() - .await - .unwrap() -} - /// Play game pub async fn play_game() { // Setup network - let core = setup_game().await; + let mut core = setup_game().await; // Print game state println!("Empire Information:"); @@ -175,6 +180,39 @@ pub async fn play_game() { println!("Land mass: {}", core.state.land_mass); println!("Gold reserve: {}", core.state.gold_reserve); - // Keep looping so we can record network events - loop { } + // let request = vec!["military_status".as_bytes().to_vec()]; + // let peer_id_string = "12D3KooWPcL6iGBjfhDTRYY8YESh94395h79iccCQj7jRQWYzm3w"; + // let peer_id = PeerId::from_bytes(&peer_id_string.from_base58().unwrap_or_default()).unwrap(); + + // // Prepare request + // let status_request = AppData::FetchData { + // keys: request, + // peer: peer_id, + // }; + + // // Send request + // if let Some(stream_id) = core.send_to_network(status_request).await { + // // Get response + // // AppData::Fetch returns a Vec>, hence we can parse the response from it + // let status_response = core.recv_from_network(stream_id).await.unwrap(); + + // let inner_value = &status_response as &dyn Any; + // if let Some(status) = inner_value.downcast_ref::>>() { + // // Get empire name + // let empire_name = String::from_utf8_lossy(&status[0]); + // let military_status = status[1][0]; + + // // Print the military status of the empire we just contacted + // println!("Empire Contacted:"); + // println!("Name: {} Empire", empire_name); + // println!("Military Capacity: {} Soldiers", military_status); + // } else { + // println!("Could not decode response") + // } + // } else { + // println!("Could not get military status of the empire at {}", peer_id); + // } + + // Keep looping so we can record network events + loop {} } diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index cc9a0e464..31eb77709 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -20,7 +20,7 @@ use futures_time::time::Duration as AsyncDuration; use libp2p::{ identify::{self, Info}, kad::{self, store::MemoryStore, Record}, - multiaddr::{self, Protocol}, + multiaddr::Protocol, noise, ping::{self, Failure}, request_response::{self, cbor::Behaviour, ProtocolSupport}, @@ -34,10 +34,10 @@ use super::*; use crate::setup::BootstrapConfig; #[cfg(feature = "async-std-runtime")] -use async_std::sync::Mutex; +pub use async_std::sync::Mutex; #[cfg(feature = "tokio-runtime")] -use tokio::sync::Mutex; +pub use tokio::sync::Mutex; mod prelude; pub use prelude::*; @@ -482,19 +482,18 @@ impl CoreBuilder { // PeerId if let Ok(peer_id) = PeerId::from_bytes(&peer_info.0.from_base58().unwrap_or_default()) { - println!("{:?}", peer_id); // Multiaddress - if let Ok(multiaddr) = multiaddr::from_url(&peer_info.1) { + if let Ok(multiaddr) = peer_info.1.parse::() { swarm .behaviour_mut() .kademlia .add_address(&peer_id, multiaddr.clone()); - println!("{:?}", multiaddr); + println!("Dailing {}", multiaddr); // Dial them swarm - .dial(multiaddr.clone()) + .dial(peer_id) .map_err(|_| SwarmNlError::RemotePeerDialError(multiaddr.to_string()))?; } } @@ -537,11 +536,15 @@ impl CoreBuilder { // Initials stream id let stream_id = StreamId::new(); + let stream_request_buffer = + Arc::new(Mutex::new(StreamRequestBuffer::new(self.stream_size))); + let stream_response_buffer = + Arc::new(Mutex::new(StreamResponseBuffer::new(self.stream_size))); // Setup the application event handler's network communication channel let event_comm_channel = NetworkChannel::new( - Arc::new(Mutex::new(StreamRequestBuffer::new(self.stream_size))), - Arc::new(Mutex::new(StreamResponseBuffer::new(self.stream_size))), + stream_request_buffer.clone(), + stream_response_buffer.clone(), application_sender.clone(), Arc::new(Mutex::new(stream_id)), self.network_read_delay, @@ -559,10 +562,8 @@ impl CoreBuilder { keypair: self.keypair, application_sender, // application_receiver, - stream_request_buffer: Arc::new(Mutex::new(StreamRequestBuffer::new(self.stream_size))), - stream_response_buffer: Arc::new(Mutex::new(StreamResponseBuffer::new( - self.stream_size, - ))), + stream_request_buffer: stream_request_buffer.clone(), + stream_response_buffer: stream_response_buffer.clone(), network_read_delay: self.network_read_delay, current_stream_id: Arc::new(Mutex::new(stream_id)), // Save handler as the state of the application @@ -686,7 +687,7 @@ impl Core { } } - /// TODO! Buffer clearnup algorithm + /// TODO! Buffer cleanup algorithm /// Explicitly rectrieve the reponse to a request sent to the network layer. /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay /// and read immediately as the response to the request should already be in the stream response @@ -771,522 +772,851 @@ impl Core { loop { select! { // handle incoming stream data - stream_data = receiver.select_next_some() => match stream_data { - StreamData::Application(stream_id, app_data) => { - // Trackable stream id - let trackable_stream_id = TrackableStreamId::Id(stream_id); - match app_data { - // Put back into the stream what we read from it - AppData::Echo(message) => { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert into the response buffer for it to be picked up - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); - }, - // Store a value in the DHT and (optionally) on explicit specific peers - AppData::KademliaStoreRecord { key, value, expiration_time, explicit_peers } => { - // create a kad record - let mut record = Record::new(key.clone(), value); - - // Set (optional) expiration time - record.expires = expiration_time; - - // Insert into DHT - if let Ok(_) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - - // Cache record on peers explicitly (if specified) - if let Some(explicit_peers) = explicit_peers { - // Extract PeerIds - let peers = explicit_peers.iter().map(|peer_id_string| { - PeerId::from_bytes(&peer_id_string.from_base58().unwrap_or_default()) - }).filter_map(Result::ok).collect::>(); - - swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); - } - } else { - // Delete the request from the stream and then return a write error - network_core.stream_request_buffer.lock().await.remove(&stream_id); + stream_data = receiver.next() => { + match stream_data { + Some(incoming_data) => { + match incoming_data { + StreamData::Application(stream_id, app_data) => { + // Trackable stream id + let trackable_stream_id = TrackableStreamId::Id(stream_id); + match app_data { + // Put back into the stream what we read from it + AppData::Echo(message) => { + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert into the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); + }); + } - // Return error - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); - } - }, - // Perform a lookup in the DHT - AppData::KademliaLookupRecord { key } => { - let _ = swarm.behaviour_mut().kademlia.get_record(key.clone().into()); + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }, - // Perform a lookup of peers that store a record - AppData::KademliaGetProviders { key } => { - let _ = swarm.behaviour_mut().kademlia.get_providers(key.clone().into()); + // Insert into the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); + }); + } + }, + // Store a value in the DHT and (optionally) on explicit specific peers + AppData::KademliaStoreRecord { key, value, expiration_time, explicit_peers } => { + // create a kad record + let mut record = Record::new(key.clone(), value); + + // Set (optional) expiration time + record.expires = expiration_time; + + // Insert into DHT + if let Ok(_) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); + }); + } - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - } - // Stop providing a record on the network - AppData::KademliaStopProviding { key } => { - swarm.behaviour_mut().kademlia.stop_providing(&key.into()); + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Insert into the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); + }); + } - // Respond with a success - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); - } - // Remove record from local store - AppData::KademliaDeleteRecord { key } => { - swarm.behaviour_mut().kademlia.remove_record(&key.into()); + // Cache record on peers explicitly (if specified) + if let Some(explicit_peers) = explicit_peers { + // Extract PeerIds + let peers = explicit_peers.iter().map(|peer_id_string| { + PeerId::from_bytes(&peer_id_string.from_base58().unwrap_or_default()) + }).filter_map(Result::ok).collect::>(); - // Respond with a success - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); - } - // Return important routing table info - AppData::KademliaGetRoutingTableInfo => { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); + swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); + } + } else { + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + // Delete the request from the stream and then return a write error + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); + }); + } - // Insert into the response buffer for it to be picked up - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: network_info.id.to_string() })); - }, - // Fetch data quickly from a peer over the network - AppData::FetchData { keys, peer } => { - // Construct the RPC object - let rpc = Rpc::ReqResponse { data: keys }; - - // Inform the swarm to make the request - let outbound_id = swarm - .behaviour_mut() - .request_response - .send_request(&peer, rpc); - - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); - } - } - } - StreamData::Network(stream_id, network_data) => { - // Trackable stream id - let trackable_stream_id = TrackableStreamId::Id(stream_id); - match network_data { - // Dail peer - NetworkData::DailPeer(multiaddr) => { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); - - if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { - if let Ok(_) = swarm.dial(multiaddr.clone()) { - // Put result into response buffer - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); - } else { - network_core.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); - } - } - } - } - } - }, - event = swarm.select_next_some() => match event { - SwarmEvent::NewListenAddr { - listener_id, - address, - } => { - // call configured handler - network_core.state.new_listen_addr(network_info.event_comm_channel.clone(), swarm.local_peer_id().to_owned(), listener_id, address).await; - } - SwarmEvent::Behaviour(event) => match event { - // Ping - CoreEvent::Ping(ping::Event { - peer, - connection: _, - result, - }) => { - match result { - // Inbound ping succes - Ok(duration) => { - // In handling the ping error policies, we only bump up an error count when there is CONCURRENT failure. - // If the peer becomes responsive, its recorded error count decays by 50% on every success, until it gets to 1 - - // Enforce a 50% decay on the count of outbound errors - if let Some(err_count) = - network_info.ping.manager.outbound_errors.get(&peer) - { - let new_err_count = (err_count / 2) as u16; - network_info - .ping - .manager - .outbound_errors - .insert(peer, new_err_count); - } + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Delete the request from the stream and then return a write error + netcore.stream_request_buffer.lock().await.remove(&stream_id); - // Enforce a 50% decay on the count of outbound errors - if let Some(timeout_err_count) = - network_info.ping.manager.timeouts.get(&peer) - { - let new_err_count = (timeout_err_count / 2) as u16; - network_info - .ping - .manager - .timeouts - .insert(peer, new_err_count); - } + // Return error + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); + }); + } + } + }, + // Perform a lookup in the DHT + AppData::KademliaLookupRecord { key } => { + let _ = swarm.behaviour_mut().kademlia.get_record(key.clone().into()); + + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); + }); + } - // Call custom handler - network_core.state.inbound_ping_success(network_info.event_comm_channel.clone(), peer, duration).await; - } - // Outbound ping failure - Err(err_type) => { - // Handle error by examining selected policy - match network_info.ping.policy { - PingErrorPolicy::NoDisconnect => { - // Do nothing, we can't disconnect from peer under any circumstances + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); + }); + } + }, + // Perform a lookup of peers that store a record + AppData::KademliaGetProviders { key } => { + let _ = swarm.behaviour_mut().kademlia.get_providers(key.clone().into()); + + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); + }); + } } - PingErrorPolicy::DisconnectAfterMaxErrors(max_errors) => { - // Disconnect after we've recorded a certain number of concurrent errors - - // Get peer entry for outbound errors or initialize peer - let err_count = network_info - .ping - .manager - .outbound_errors - .entry(peer) - .or_insert(0); - - if *err_count != max_errors { - // Disconnect peer - let _ = swarm.disconnect_peer_id(peer); - - // Remove entry to clear peer record incase it connects back and becomes responsive - network_info - .ping - .manager - .outbound_errors - .remove(&peer); - } else { - // Bump the count up - *err_count += 1; + // Stop providing a record on the network + AppData::KademliaStopProviding { key } => { + swarm.behaviour_mut().kademlia.stop_providing(&key.into()); + + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + // Respond with a success + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Respond with a success + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); + }); } } - PingErrorPolicy::DisconnectAfterMaxTimeouts( - max_timeout_errors, - ) => { - // Disconnect after we've recorded a certain number of concurrent TIMEOUT errors - - // First make sure we're dealing with only the timeout errors - if let Failure::Timeout = err_type { - // Get peer entry for outbound errors or initialize peer - let err_count = network_info - .ping - .manager - .timeouts - .entry(peer) - .or_insert(0); - - if *err_count != max_timeout_errors { - // Disconnect peer - let _ = swarm.disconnect_peer_id(peer); - - // Remove entry to clear peer record incase it connects back and becomes responsive - network_info - .ping - .manager - .timeouts - .remove(&peer); - } else { - // Bump the count up - *err_count += 1; - } + // Remove record from local store + AppData::KademliaDeleteRecord { key } => { + swarm.behaviour_mut().kademlia.remove_record(&key.into()); + + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + // Respond with a success + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Respond with a success + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); + }); } } - } + // Return important routing table info + AppData::KademliaGetRoutingTableInfo => { + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + let netinfo = network_info.clone(); + async_std::task::spawn(async move{ + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert into the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: netinfo.id.to_string() })); + }); + } - // Call custom handler - network_core.state.outbound_ping_error(network_info.event_comm_channel.clone(), peer, err_type).await; - } - } - } - // Kademlia - CoreEvent::Kademlia(event) => match event { - kad::Event::OutboundQueryProgressed { result, .. } => match result { - kad::QueryResult::GetProviders(Ok( - kad::GetProvidersOk::FoundProviders { key, providers, .. }, - )) => { - // Stringify the PeerIds - let peer_id_strings = providers.iter().map(|peer_id| { - peer_id.to_base58() - }).collect::>(); - - // Get `StreamId` - if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + let netinfo = network_info.clone(); + tokio::task::spawn(async move{ + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert into the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: netinfo.id.to_string() })); + }); + } + }, + // Fetch data quickly from a peer over the network + AppData::FetchData { keys, peer } => { + // Construct the RPC object + let rpc = Rpc::ReqResponse { data: keys.clone() }; + + // Inform the swarm to make the request + let outbound_id = swarm + .behaviour_mut() + .request_response + .send_request(&peer, rpc); + + println!("keys: {:?}", keys); + + // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events + // where we can get the response and then replace the buffer with the actual response data + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); + }); + } - // Insert response message the response buffer for it to be picked up - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings })); + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); + }); + } } } } - kad::QueryResult::GetProviders(Err(_)) => {}, - kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( - kad::PeerRecord { record:kad::Record{ key, value, .. }, .. }, - ))) => { - // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up - // Get `StreamId` - if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); - // Insert response message the response buffer for it to be picked up - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::RecordFound { key: key.to_vec(), value: value.to_vec() })); - } - } - } - kad::QueryResult::GetRecord(Ok(_)) => {}, - kad::QueryResult::GetRecord(Err(e)) => { - let key = match e { - kad::GetRecordError::NotFound { key, .. } => key, - kad::GetRecordError::QuorumFailed { key, .. } => key, - kad::GetRecordError::Timeout { key } => key, - }; - - // Delete the temporary data and then return error - // Get `StreamId` - if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { + StreamData::Network(stream_id, network_data) => { + // Trackable stream id + let trackable_stream_id = TrackableStreamId::Id(stream_id); + match network_data { + // Dail peer + NetworkData::DailPeer(multiaddr) => { // Remove the request from the request buffer network_core.stream_request_buffer.lock().await.remove(&stream_id); - // Return error - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::KadFetchRecordError(key.to_vec()))); + if let Ok(multiaddr) = multiaddr.parse::() { + if let Ok(_) = swarm.dial(multiaddr.clone()) { + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // Put result into response buffer + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Put result into response buffer + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); + }); + } + } else { + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // Put result into response buffer + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move{ + // Put result into response buffer + netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); + }); + } + } + } } } } - kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { - // Call handler - network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()).await; - } - kad::QueryResult::PutRecord(Err(_)) => { - // Call handler - network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()).await; - } - kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { - key, - })) => { - // Call handler - network_core.state.kademlia_start_providing_success(network_info.event_comm_channel.clone(), key.to_vec()).await; - } - kad::QueryResult::StartProviding(Err(_)) => { - // Call handler - network_core.state.kademlia_start_providing_error(network_info.event_comm_channel.clone()).await; - } - _ => {} - }, - // Other events we don't care about - _ => {} - }, - // Identify - CoreEvent::Identify(event) => match event { - identify::Event::Received { peer_id, info } => { - // We just recieved an `Identify` info from a peer.s - network_core.state.identify_info_recieved(network_info.event_comm_channel.clone(), peer_id, info.clone()).await; - - // disconnect from peer of the network id is different - if info.protocol_version != network_info.id.as_ref() { - // disconnect - let _ = swarm.disconnect_peer_id(peer_id); - } else { - // add to routing table if not present already - let _ = swarm.behaviour_mut().kademlia.add_address(&peer_id, info.listen_addrs[0].clone()); - } } - // Remaining `Identify` events are not actively handled - _ => {} }, - // Request-response - CoreEvent::RequestResponse(event) => match event { - request_response::Event::Message { peer: _, message } => match message { - // A request just came in - request_response::Message::Request { request_id: _, request, channel } => { - // Parse request - match request { - Rpc::ReqResponse { data } => { - // Pass request data to configured request handler - let response_data = network_core.state.handle_incoming_message(data); - - // construct an RPC - let response_rpc = Rpc::ReqResponse { data: response_data }; - - // Send the response - let _ = swarm.behaviour_mut().request_response.send_response(channel, response_rpc); + _ => {} + } + }, + swarm_event = swarm.next() => { + match swarm_event { + Some(event) => { + match event { + SwarmEvent::NewListenAddr { + listener_id, + address, + } => { + // call configured handler + network_core.state.new_listen_addr(network_info.event_comm_channel.clone(), swarm.local_peer_id().to_owned(), listener_id, address).await; + } + SwarmEvent::Behaviour(event) => match event { + // Ping + CoreEvent::Ping(ping::Event { + peer, + connection: _, + result, + }) => { + match result { + // Inbound ping succes + Ok(duration) => { + // In handling the ping error policies, we only bump up an error count when there is CONCURRENT failure. + // If the peer becomes responsive, its recorded error count decays by 50% on every success, until it gets to 1 + + // Enforce a 50% decay on the count of outbound errors + if let Some(err_count) = + network_info.ping.manager.outbound_errors.get(&peer) + { + let new_err_count = (err_count / 2) as u16; + network_info + .ping + .manager + .outbound_errors + .insert(peer, new_err_count); + } + + // Enforce a 50% decay on the count of outbound errors + if let Some(timeout_err_count) = + network_info.ping.manager.timeouts.get(&peer) + { + let new_err_count = (timeout_err_count / 2) as u16; + network_info + .ping + .manager + .timeouts + .insert(peer, new_err_count); + } + + // Call custom handler + network_core.state.inbound_ping_success(network_info.event_comm_channel.clone(), peer, duration).await; } - } - }, - // We have a response message - request_response::Message::Response { request_id, response } => { - // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up - - // Get `StreamId` - if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); - - match response { - Rpc::ReqResponse { data } => { - // Insert response message the response buffer for it to be picked up - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(data)); - }, + // Outbound ping failure + Err(err_type) => { + // Handle error by examining selected policy + match network_info.ping.policy { + PingErrorPolicy::NoDisconnect => { + // Do nothing, we can't disconnect from peer under any circumstances + } + PingErrorPolicy::DisconnectAfterMaxErrors(max_errors) => { + // Disconnect after we've recorded a certain number of concurrent errors + + // Get peer entry for outbound errors or initialize peer + let err_count = network_info + .ping + .manager + .outbound_errors + .entry(peer) + .or_insert(0); + + if *err_count != max_errors { + // Disconnect peer + let _ = swarm.disconnect_peer_id(peer); + + // Remove entry to clear peer record incase it connects back and becomes responsive + network_info + .ping + .manager + .outbound_errors + .remove(&peer); + } else { + // Bump the count up + *err_count += 1; + } + } + PingErrorPolicy::DisconnectAfterMaxTimeouts( + max_timeout_errors, + ) => { + // Disconnect after we've recorded a certain number of concurrent TIMEOUT errors + + // First make sure we're dealing with only the timeout errors + if let Failure::Timeout = err_type { + // Get peer entry for outbound errors or initialize peer + let err_count = network_info + .ping + .manager + .timeouts + .entry(peer) + .or_insert(0); + + if *err_count != max_timeout_errors { + // Disconnect peer + let _ = swarm.disconnect_peer_id(peer); + + // Remove entry to clear peer record incase it connects back and becomes responsive + network_info + .ping + .manager + .timeouts + .remove(&peer); + } else { + // Bump the count up + *err_count += 1; + } + } + } } + + // Call custom handler + network_core.state.outbound_ping_error(network_info.event_comm_channel.clone(), peer, err_type).await; } } - }, - }, - request_response::Event::OutboundFailure { request_id, .. } => { - // Delete the temporary data and then return error + } + // Kademlia + CoreEvent::Kademlia(event) => match event { + kad::Event::OutboundQueryProgressed { result, .. } => match result { + kad::QueryResult::GetProviders(Ok( + kad::GetProvidersOk::FoundProviders { key, providers, .. }, + )) => { + // Stringify the PeerIds + let peer_id_strings = providers.iter().map(|peer_id| { + peer_id.to_base58() + }).collect::>(); + + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move { + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings })); + } + } + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings })); + } + } + }); + } + } + kad::QueryResult::GetProviders(Err(_)) => {}, + kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( + kad::PeerRecord { record:kad::Record{ key, value, .. }, .. }, + ))) => { + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move { + // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::RecordFound { key: key.to_vec(), value: value.to_vec() })); + } + } + }); + } - // Get `StreamId` - if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Insert response message the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::RecordFound { key: key.to_vec(), value: value.to_vec() })); + } + } + }); + } + } + kad::QueryResult::GetRecord(Ok(_)) => {}, + kad::QueryResult::GetRecord(Err(e)) => { + let key = match e { + kad::GetRecordError::NotFound { key, .. } => key, + kad::GetRecordError::QuorumFailed { key, .. } => key, + kad::GetRecordError::Timeout { key } => key, + }; + + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move { + // Delete the temporary data and then return error + // Get `StreamId` + if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + network_core.stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::KadFetchRecordError(key.to_vec()))); + } + } + }); + } - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // Delete the temporary data and then return error + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::KadFetchRecordError(key.to_vec()))); + } + } + }); + } + } + kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { + // Call handler + network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()).await; + } + kad::QueryResult::PutRecord(Err(_)) => { + // Call handler + network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()).await; + } + kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { + key, + })) => { + // Call handler + network_core.state.kademlia_start_providing_success(network_info.event_comm_channel.clone(), key.to_vec()).await; + } + kad::QueryResult::StartProviding(Err(_)) => { + // Call handler + network_core.state.kademlia_start_providing_error(network_info.event_comm_channel.clone()).await; + } + _ => {} + }, + // Other events we don't care about + _ => {} + }, + // Identify + CoreEvent::Identify(event) => match event { + identify::Event::Received { peer_id, info } => { + // We just recieved an `Identify` info from a peer.s + network_core.state.identify_info_recieved(network_info.event_comm_channel.clone(), peer_id, info.clone()).await; + + // disconnect from peer of the network id is different + if info.protocol_version != network_info.id.as_ref() { + // disconnect + let _ = swarm.disconnect_peer_id(peer_id); + } else { + // add to routing table if not present already + let _ = swarm.behaviour_mut().kademlia.add_address(&peer_id, info.listen_addrs[0].clone()); + } + } + // Remaining `Identify` events are not actively handled + _ => {} + }, + // Request-response + CoreEvent::RequestResponse(event) => match event { + request_response::Event::Message { peer: _, message } => match message { + // A request just came in + request_response::Message::Request { request_id: _, request, channel } => { + // Parse request + match request { + Rpc::ReqResponse { data } => { + // Pass request data to configured request handler + let response_data = network_core.state.handle_incoming_message(data); + + // construct an RPC + let response_rpc = Rpc::ReqResponse { data: response_data }; + + // Send the response + let _ = swarm.behaviour_mut().request_response.send_response(channel, response_rpc); + } + } + }, + // We have a response message + request_response::Message::Response { request_id, response } => { + // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move { + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + match response { + Rpc::ReqResponse { data } => { + println!("--> {:?}", data); + // Insert response message the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(data)); + }, + } + } + } + }); + } + + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + match response { + Rpc::ReqResponse { data } => { + println!("--> {:?}", data); + // Insert response message the response buffer for it to be picked up + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(data)); + }, + } + } + } + }); + } + }, + }, + request_response::Event::OutboundFailure { request_id, .. } => { + // Delete the temporary data and then return error + println!("out failure"); + #[cfg(feature = "async-std-runtime")] + { + let netcore = network_core.clone(); + async_std::task::spawn(async move { + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + } + } + }); + } - // Return error - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + #[cfg(feature = "tokio-runtime")] + { + let netcore = network_core.clone(); + tokio::task::spawn(async move { + // Get `StreamId` + if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { + // Make sure this is temporary data (i.e `StreamId`) + let opaque_data = &opaque_data as &dyn Any; + + if let Some(stream_id) = opaque_data.downcast_ref::() { + // Remove the request from the request buffer + netcore.stream_request_buffer.lock().await.remove(&stream_id); + + // Return error + netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); + } + } + }); + } + }, + _ => {} } + }, + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + num_established, + concurrent_dial_errors: _, + established_in, + } => { + // call configured handler + network_core.state.connection_established( + network_info.event_comm_channel.clone(), + peer_id, + connection_id, + &endpoint, + num_established, + established_in, + ).await; } - }, - _ => {} - } - }, - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - num_established, - concurrent_dial_errors: _, - established_in, - } => { - // call configured handler - network_core.state.connection_established( - network_info.event_comm_channel.clone(), - peer_id, - connection_id, - &endpoint, - num_established, - established_in, - ).await; - } - SwarmEvent::ConnectionClosed { - peer_id, - connection_id, - endpoint, - num_established, - cause, - } => { - // call configured handler - network_core.state.connection_closed( - network_info.event_comm_channel.clone(), - peer_id, - connection_id, - &endpoint, - num_established, - cause, - ).await; - } - SwarmEvent::ExpiredListenAddr { - listener_id, - address, - } => { - // call configured handler - network_core.state.expired_listen_addr(network_info.event_comm_channel.clone(), listener_id, address).await; - } - SwarmEvent::ListenerClosed { - listener_id, - addresses, - reason: _, - } => { - // call configured handler - network_core.state.listener_closed(network_info.event_comm_channel.clone(), listener_id, addresses).await; - } - SwarmEvent::ListenerError { - listener_id, - error: _, - } => { - // call configured handler - network_core.state.listener_error(network_info.event_comm_channel.clone(), listener_id).await; - } - SwarmEvent::Dialing { - peer_id, - connection_id, - } => { - // call configured handler - network_core.state.dialing(network_info.event_comm_channel.clone(), peer_id, connection_id).await; - } - SwarmEvent::NewExternalAddrCandidate { address } => { - // call configured handler - network_core.state.new_external_addr_candidate(network_info.event_comm_channel.clone(), address).await; - } - SwarmEvent::ExternalAddrConfirmed { address } => { - // call configured handler - network_core.state.external_addr_confirmed(network_info.event_comm_channel.clone(), address).await; - } - SwarmEvent::ExternalAddrExpired { address } => { - // call configured handler - network_core.state.external_addr_expired(network_info.event_comm_channel.clone(), address).await; - } - SwarmEvent::IncomingConnection { - connection_id, - local_addr, - send_back_addr, - } => { - // call configured handler - network_core.state.incoming_connection(network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr).await; - } - SwarmEvent::IncomingConnectionError { - connection_id, - local_addr, - send_back_addr, - error: _, - } => { - // call configured handler - network_core.state.incoming_connection_error( - network_info.event_comm_channel.clone(), - connection_id, - local_addr, - send_back_addr, - ).await; - } - SwarmEvent::OutgoingConnectionError { - connection_id, - peer_id, - error: _, - } => { - // call configured handler - network_core.state.outgoing_connection_error(network_info.event_comm_channel.clone(), connection_id, peer_id).await; + SwarmEvent::ConnectionClosed { + peer_id, + connection_id, + endpoint, + num_established, + cause, + } => { + // call configured handler + network_core.state.connection_closed( + network_info.event_comm_channel.clone(), + peer_id, + connection_id, + &endpoint, + num_established, + cause, + ).await; + } + SwarmEvent::ExpiredListenAddr { + listener_id, + address, + } => { + // call configured handler + network_core.state.expired_listen_addr(network_info.event_comm_channel.clone(), listener_id, address).await; + } + SwarmEvent::ListenerClosed { + listener_id, + addresses, + reason: _, + } => { + // call configured handler + network_core.state.listener_closed(network_info.event_comm_channel.clone(), listener_id, addresses).await; + } + SwarmEvent::ListenerError { + listener_id, + error: _, + } => { + // call configured handler + network_core.state.listener_error(network_info.event_comm_channel.clone(), listener_id).await; + } + SwarmEvent::Dialing { + peer_id, + connection_id, + } => { + // call configured handler + network_core.state.dialing(network_info.event_comm_channel.clone(), peer_id, connection_id).await; + } + SwarmEvent::NewExternalAddrCandidate { address } => { + // call configured handler + network_core.state.new_external_addr_candidate(network_info.event_comm_channel.clone(), address).await; + } + SwarmEvent::ExternalAddrConfirmed { address } => { + // call configured handler + network_core.state.external_addr_confirmed(network_info.event_comm_channel.clone(), address).await; + } + SwarmEvent::ExternalAddrExpired { address } => { + // call configured handler + network_core.state.external_addr_expired(network_info.event_comm_channel.clone(), address).await; + } + SwarmEvent::IncomingConnection { + connection_id, + local_addr, + send_back_addr, + } => { + // call configured handler + network_core.state.incoming_connection(network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr).await; + } + SwarmEvent::IncomingConnectionError { + connection_id, + local_addr, + send_back_addr, + error: _, + } => { + // call configured handler + network_core.state.incoming_connection_error( + network_info.event_comm_channel.clone(), + connection_id, + local_addr, + send_back_addr, + ).await; + } + SwarmEvent::OutgoingConnectionError { + connection_id, + peer_id, + error: _, + } => { + // call configured handler + network_core.state.outgoing_connection_error(network_info.event_comm_channel.clone(), connection_id, peer_id).await; + } + _ => todo!(), + } + }, + _ => {} } - _ => todo!(), } } } diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index 01e449f0e..fa81c32c1 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -1,9 +1,6 @@ use async_trait::async_trait; /// Copyright (c) 2024 Algorealm - - use libp2p::request_response::OutboundRequestId; -use rand::random; use serde::{Deserialize, Serialize}; use std::{any::Any, time::Instant}; use thiserror::Error; @@ -16,6 +13,10 @@ use super::*; /// out pub const NETWORK_READ_TIMEOUT: u64 = 60; +/// The time it takes for the task to sleep before it can recheck if an output has been placed in +/// the repsonse buffer (7 seconds) +pub const TASK_SLEEP_DURATION: u64 = 7; + /// Data exchanged over a stream between the application and network layer #[derive(Debug, Clone)] pub(super) enum StreamData { @@ -107,7 +108,7 @@ impl StreamId { /// Generate a new random stream id. /// Must only be called once pub fn new() -> Self { - StreamId(random()) + StreamId(0) } /// Generate a new random stream id, using the current as guide @@ -156,7 +157,7 @@ impl StreamResponseType for Vec where T: StreamResponseType {} /// It is unlikely to be ever full as the default is usize::MAX except otherwise specified during /// configuration. It is always good practice to read responses from the internal stream buffer /// using `fetch_from_network()` or explicitly using `recv_from_network` -#[derive(Clone)] +#[derive(Clone, Debug)] pub(super) struct StreamRequestBuffer { /// Max requests we can keep track of size: usize, @@ -429,6 +430,7 @@ impl EventHandler for DefaultHandler { /// Important information to obtain from the [`CoreBuilder`], to properly handle network /// operations +#[derive(Clone)] pub(super) struct NetworkInfo { /// The name/id of the network pub id: StreamProtocol, @@ -445,6 +447,7 @@ pub mod ping_config { /// Policies to handle a `Ping` error /// - All connections to peers are closed during a disconnect operation. + #[derive(Debug, Clone)] pub enum PingErrorPolicy { /// Do not disconnect under any circumstances NoDisconnect, @@ -455,7 +458,7 @@ pub mod ping_config { } /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] - #[derive(Debug)] + #[derive(Debug, Clone)] pub struct PingManager { /// The number of timeout errors encountered from a peer pub timeouts: HashMap, @@ -476,6 +479,7 @@ pub mod ping_config { } /// Critical information to manage `Ping` operations + #[derive(Debug, Clone)] pub struct PingInfo { pub policy: PingErrorPolicy, pub manager: PingManager, @@ -527,48 +531,127 @@ impl NetworkChannel { let stream_id = StreamId::next(*self.current_stream_id.lock().await); let request = StreamData::Application(stream_id, request); + // Acquire lock on stream_request_buffer + let mut stream_request_buffer = self.stream_request_buffer.lock().await; + // Add to request buffer - if !self.stream_request_buffer.lock().await.insert(stream_id) { + if !stream_request_buffer.insert(stream_id) { // Buffer appears to be full return None; } + println!("{:?}", stream_request_buffer); + println!("{:?}", request); + // Send request if let Ok(_) = self.application_sender.send(request).await { // Store latest stream id *self.current_stream_id.lock().await = stream_id; Some(stream_id) } else { - return None; + None } } + /// TODO! Buffer cleanup algorithm + /// Explicitly rectrieve the reponse to a request sent to the network layer. + /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay + /// and read immediately as the response to the request should already be in the stream response + /// buffer. pub async fn recv_from_network(&mut self, stream_id: StreamId) -> NetworkResult { - let network_data_future = async { - loop { - // tight loop - if let Some(response_value) = self - .stream_response_buffer - .lock() - .await - .remove(&TrackableStreamId::Id(stream_id)) - { - // Make sure the value is not `StreamId`. If it is, then the request is being - // processed and the final value we want has not arrived. - - // Get inner boxed value - let inner_value = &response_value as &dyn Any; - if let None = inner_value.downcast_ref::() { - return response_value; + #[cfg(feature = "async-std-runtime")] + { + let channel = self.clone(); + let recv_task = tokio::task::spawn(async move { + let mut loop_count = 0; + loop { + // Attempt to acquire the lock without blocking + let maybe_response_value = channel.stream_response_buffer.try_lock(); + + // Check if the lock was acquired + if let Ok(mut response_buffer) = maybe_response_value { + // Check if the value is available in the response buffer + if let Some(response_value) = + response_buffer.remove(&TrackableStreamId::Id(stream_id)) + { + // Make sure the value is not `StreamId`. If it is, then the request is + // being processed and the final value we want has not arrived. + + // Get inner boxed value + let inner_value = &response_value as &dyn Any; + if inner_value.downcast_ref::().is_none() { + return response_value; + } + } + } + + async_std::task::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + + if loop_count < 6 { + loop_count += 1; + } else { + break; } } + + // Error: return the stream id + Box::new(stream_id) + }) + .await; + + if let Ok(response_value) = recv_task { + Ok(response_value) + } else { + return Err(NetworkError::NetworkReadTimeout); } - }; + } + + #[cfg(feature = "tokio-runtime")] + { + let channel = self.clone(); + let recv_task = tokio::task::spawn(async move { + let mut loop_count = 0; + loop { + // Attempt to acquire the lock without blocking + let maybe_response_value = channel.stream_response_buffer.try_lock(); + + // Check if the lock was acquired + if let Ok(mut response_buffer) = maybe_response_value { + // Check if the value is available in the response buffer + if let Some(response_value) = + response_buffer.remove(&TrackableStreamId::Id(stream_id)) + { + // Make sure the value is not `StreamId`. If it is, then the request is + // being processed and the final value we want has not arrived. + + // Get inner boxed value + let inner_value = &response_value as &dyn Any; + if inner_value.downcast_ref::().is_none() { + return response_value; + } + } + } + + tokio::time::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; - Ok(network_data_future - .timeout(self.network_read_delay) - .await - .map_err(|_| NetworkError::NetworkReadTimeout)?) + if loop_count < 6 { + loop_count += 1; + } else { + break; + } + } + + // Error: return the stream id + Box::new(stream_id) + }) + .await; + + if let Ok(response_value) = recv_task { + Ok(response_value) + } else { + return Err(NetworkError::NetworkReadTimeout); + } + } } /// Perform an atomic `send` and `recieve` from the network layer. This function is atomic and From 6cf0e95a4c5ab51f02f7f49c64491bcfbcb50678 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Thu, 9 May 2024 15:17:27 +0100 Subject: [PATCH 09/36] chore: clean up request-response --- client/bootstrap_config.ini | 2 +- client/src/main.rs | 60 ++- swarm_nl/src/core/mod.rs | 712 ++++++++--------------------------- swarm_nl/src/core/prelude.rs | 267 ++++++------- 4 files changed, 283 insertions(+), 758 deletions(-) diff --git a/client/bootstrap_config.ini b/client/bootstrap_config.ini index 2d62cdc86..ee5ea1522 100644 --- a/client/bootstrap_config.ini +++ b/client/bootstrap_config.ini @@ -9,4 +9,4 @@ udp=49201 [bootstrap] ; The boostrap nodes to connect to immediately after start up -boot_nodes=[12D3KooWGsAYg7bBqCfRqvfAn3djpFoXGxX73nXQNFsGfDFSGUSU:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file +boot_nodes=[12D3KooWMAtuoCTErUU16nJ3n1ig9Cfzr5fG5zdsMRbd45X4D93U:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index 628a00598..7493fa0a1 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,13 +4,13 @@ /// Objective: Form alliances and conquer as much empires as possible! /// It is a multi-player game /// Enjoy! -use std::{any::Any, borrow::Cow, num::NonZeroU32, sync::Arc, time::Duration}; +use std::{any::Any, borrow::Cow, num::NonZeroU32, time::Duration}; use swarm_nl::{ async_trait, core::EventHandler, - core::{AppData, Core, CoreBuilder, Mutex, NetworkChannel, StreamId}, + core::{AppData, Core, CoreBuilder, NetworkChannel}, setup::BootstrapConfig, - ConnectedPoint, ConnectionId, PeerId, Sender, SinkExt, + ConnectedPoint, ConnectionId, PeerId, }; #[tokio::main] @@ -59,6 +59,23 @@ impl EventHandler for Empire { "There are {} soldiers guarding the {} Empire gate", self.soldiers, self.name ); + + let request = vec!["military_status".as_bytes().to_vec()]; + + // Prepare request + let status_request = AppData::FetchData { + keys: request, + peer: local_peer_id, + }; + + // Send request + let stream_id = channel.send_to_network(status_request).await.unwrap(); + + // Get response + // AppData::Fetch returns a Vec>, hence we can parse the response from it + let status_response = channel.recv_from_network(stream_id).await; + + println!("{:?}", status_response); } async fn connection_established( @@ -73,42 +90,7 @@ impl EventHandler for Empire { println!("Connection established with peer: {}", peer_id); // When we establish a new connection, the empires send message to the other empire to knoe // their military status - let request = vec!["military_status".as_bytes().to_vec()]; - - // Prepare request - let status_request = AppData::FetchData { - keys: request, - peer: peer_id, - }; - - // Send request - let stream_id = channel - .send_to_network(status_request) - .await - .unwrap(); - - // Get response - // AppData::Fetch returns a Vec>, hence we can parse the response from it - let status_response = channel - .recv_from_network(stream_id) - .await; - - let inner_value = &status_response as &dyn Any; - if let Some(status) = inner_value.downcast_ref::>>() { - // Get empire name - let empire_name = String::from_utf8_lossy(&status[0]); - let military_status = status[1][0]; - - // Print the military status of the empire we just contacted - println!("Empire Contacted:"); - println!("Name: {} Empire", empire_name); - println!("Military Capacity: {} Soldiers", military_status); - } else { - println!("Could not decode response") - } - // } else { - // println!("Could not get military status of the empire at {}", peer_id); - // } + } /// Handle any incoming RPC from any neighbouring empire diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 31eb77709..ae66747b6 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -1,7 +1,6 @@ /// Copyright (c) 2024 Algorealm /// Core data structures and protocol implementations for building a swarm. use std::{ - any::Any, collections::{HashMap, HashSet}, net::{IpAddr, Ipv4Addr}, num::NonZeroU32, @@ -15,7 +14,6 @@ use futures::{ channel::mpsc::{self, Receiver, Sender}, select, SinkExt, StreamExt, }; -use futures_time::prelude::*; use futures_time::time::Duration as AsyncDuration; use libp2p::{ identify::{self, Info}, @@ -38,6 +36,7 @@ pub use async_std::sync::Mutex; #[cfg(feature = "tokio-runtime")] pub use tokio::sync::Mutex; +use tokio::sync::broadcast; mod prelude; pub use prelude::*; @@ -504,9 +503,12 @@ impl CoreBuilder { // There must be a way for the application to communicate with the underlying networking // core. This will involve acceptiing data and pushing data to the application layer. - // The stream will have SwarmNl (being the consumer) recieve data and commands from multiple - // areas in the application; - let (application_sender, network_receiver) = mpsc::channel::(3); + // Two streams will be opened: The first mpsc stream will allow SwarmNL push data to the + // application and the application will comsume it (single consumer) The second stream + // will have SwarmNl (being the consumer) recieve data and commands from multiple areas + // in the application; + let (application_sender, network_receiver) = mpsc::channel::(100); + let (network_sender, application_receiver) = mpsc::channel::(100); // Set up the ping network info. // `PeerId` does not implement `Default` so we will add the peerId of this node as seed @@ -561,6 +563,7 @@ impl CoreBuilder { let network_core = Core { keypair: self.keypair, application_sender, + // network_sender, // application_receiver, stream_request_buffer: stream_request_buffer.clone(), stream_response_buffer: stream_response_buffer.clone(), @@ -575,6 +578,7 @@ impl CoreBuilder { async_std::task::spawn(Core::handle_async_operations( swarm, network_info, + network_sender, network_receiver, network_core.clone(), )); @@ -584,10 +588,25 @@ impl CoreBuilder { tokio::task::spawn(Core::handle_async_operations( swarm, network_info, + network_sender, network_receiver, network_core.clone(), )); + // Spin up task to listen for responses from the network layer + #[cfg(feature = "async-std-runtime")] + async_std::task::spawn(Core::handle_network_response( + application_receiver, + network_core.clone(), + )); + + // Spin up task to listen for responses from the network layer + #[cfg(feature = "tokio-runtime")] + tokio::task::spawn(Core::handle_network_response( + application_receiver, + network_core.clone(), + )); + Ok(network_core) } } @@ -601,6 +620,8 @@ pub struct Core { application_sender: Sender, /// The consuming end of the stream that recieves data from the network layer // application_receiver: Receiver, + /// The producing end of the stream that sends data from the network layer to the application + // network_sender: Sender, /// This serves as a buffer for the results of the requests to the network layer. /// With this, applications can make async requests and fetch their results at a later time /// without waiting. This is made possible by storing a [`StreamId`] for a particular stream @@ -642,122 +663,47 @@ impl Core { false } - /// Send data to the network layer and recieve a unique `StreamId` to track the request - /// If the internal stream buffer is full, `None` will be returned. - pub async fn send_to_network(&mut self, request: AppData) -> Option { - // Generate stream id - let stream_id = StreamId::next(*self.current_stream_id.lock().await); - let request = StreamData::Application(stream_id, request); - - // Add to request buffer - if !self.stream_request_buffer.lock().await.insert(stream_id) { - // Buffer appears to be full - return None; - } - - // Send request - if let Ok(_) = self.application_sender.send(request).await { - // Store latest stream id - *self.current_stream_id.lock().await = stream_id; - Some(stream_id) - } else { - return None; - } - } - - /// This is for intra-network communication - async fn notify_network(&mut self, request: NetworkData) -> Option { - // Generate stream id - let stream_id = StreamId::next(*self.current_stream_id.lock().await); - let request = StreamData::Network(stream_id, request); - - // Add to request buffer - if !self.stream_request_buffer.lock().await.insert(stream_id) { - // Buffer appears to be full - return None; - } - - // Send request - if let Ok(_) = self.application_sender.send(request).await { - // Store latest stream id - *self.current_stream_id.lock().await = stream_id; - Some(stream_id) - } else { - return None; - } + /// Return the node's `PeerId` + pub fn peer_id(&self) -> String { + self.keypair.public().to_peer_id().to_string() } - /// TODO! Buffer cleanup algorithm - /// Explicitly rectrieve the reponse to a request sent to the network layer. - /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay - /// and read immediately as the response to the request should already be in the stream response - /// buffer. - pub async fn recv_from_network(&mut self, stream_id: StreamId) -> NetworkResult { - let network_data_future = async { - loop { - // tight loop - if let Some(response_value) = self - .stream_response_buffer - .lock() - .await - .remove(&TrackableStreamId::Id(stream_id)) - { - // Make sure the value is not `StreamId`. If it is, then the request is being - // processed and the final value we want has not arrived. - - // Get inner boxed value - let inner_value = &response_value as &dyn Any; - if let None = inner_value.downcast_ref::() { - return response_value; + /// Handle the responses coming from the network layer. This is usually as a result of a request + /// from the application layer + async fn handle_network_response(mut receiver: Receiver, network_core: Core) { + loop { + select! { + response = receiver.select_next_some() => { + if let Ok(mut buffer_guard) = network_core.stream_response_buffer.try_lock() { + match response { + // Send response to request operations specified by the application layer + StreamData::ToApplication(stream_id, response) => match response { + res @ AppResponse::Echo(..) => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::DailPeer(..) => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaStoreRecordSuccess => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaLookupRecord(..) => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaGetProviders{..} => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaGetRoutingTableInfo { .. } => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::FetchData(..) => buffer_guard.insert(stream_id, Ok(res)), + // Error + AppResponse::Error(error) => buffer_guard.insert(stream_id, Err(error)) + }, + _ => false + }; + } else { + // sleep for a few seconds + #[cfg(feature = "async-std-runtime")] + async_std::task::sleep(Duration::from_secs(1)).await; + + // sleep for a few seconds + #[cfg(feature = "tokio-runtime")] + tokio::time::sleep(Duration::from_secs(1)).await; } } } - }; - - Ok(network_data_future - .timeout(self.network_read_delay) - .await - .map_err(|_| NetworkError::NetworkReadTimeout)?) - } - - /// Perform an atomic `send` and `recieve` from the network layer. This function is atomic and - /// blocks until the result of the request is returned from the network layer. This function - /// should mostly be used when the result of the request is needed immediately and delay can be - /// condoned. It will still timeout if the delay exceeds the configured period. - /// If the internal buffer is full, it will return an error. - pub async fn fetch_from_network(&mut self, request: AppData) -> NetworkResult { - // send request - if let Some(stream_id) = self.send_to_network(request).await { - // wait to recieve response from the network - self.recv_from_network(stream_id).await - } else { - Err(NetworkError::StreamBufferOverflow) } } - /// This is for intra-network atomic communication - async fn trigger_network_op(&mut self, request: NetworkData) -> NetworkResult { - // send request - if let Some(stream_id) = self.notify_network(request).await { - // wait to recieve response from the network - self.recv_from_network(stream_id).await - } else { - Err(NetworkError::StreamBufferOverflow) - } - } - - /// Return the node's `PeerId` - pub fn peer_id(&self) -> String { - self.keypair.public().to_peer_id().to_string() - } - - /// Explicitly dial a peer at runtime. - pub async fn dial_peer(&mut self, multiaddr: String) -> NetworkResult { - // send message into stream - self.trigger_network_op(NetworkData::DailPeer(multiaddr)) - .await - } - /// Handle async operations, which basically involved handling two major data sources: /// - Streams coming from the application layer. /// - Events generated by (libp2p) network activities. @@ -765,45 +711,49 @@ impl Core { async fn handle_async_operations( mut swarm: Swarm, mut network_info: NetworkInfo, + mut network_sender: Sender, mut receiver: Receiver, mut network_core: Core, ) { + // Create one-way channels that help the application command receiver synchronize with the + // libp2p generated events + let (sender_channel_1, _) = broadcast::channel::(500); // For AppData::KademliaStoreRecord + let (sender_channel_2, _) = broadcast::channel::(100); // For AppData::KademliaLookupRecord + let (sender_channel_3, _) = broadcast::channel::(100); // For AppData::KademliaGetProviders + let (sender_channel_4, _) = broadcast::channel::(100); // For AppData::FetchData + // Loop to handle incoming application streams indefinitely. loop { + let mut recv_channel_1 = sender_channel_1.subscribe(); + let mut recv_channel_2 = sender_channel_2.subscribe(); + let mut recv_channel_3 = sender_channel_3.subscribe(); + let mut recv_channel_4 = sender_channel_4.subscribe(); + select! { // handle incoming stream data stream_data = receiver.next() => { match stream_data { Some(incoming_data) => { + println!("{:?}", incoming_data); match incoming_data { - StreamData::Application(stream_id, app_data) => { + StreamData::FromApplication(stream_id, app_data) => { // Trackable stream id - let trackable_stream_id = TrackableStreamId::Id(stream_id); + let stream_id = stream_id; match app_data { // Put back into the stream what we read from it AppData::Echo(message) => { - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert into the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert into the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(message)); - }); + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Echo(message))).await; + }, + AppData::DailPeer(multiaddr) => { + if let Ok(multiaddr) = multiaddr.parse::() { + if let Ok(_) = swarm.dial(multiaddr.clone()) { + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::DailPeer(multiaddr.to_string()))).await; + } else { + // Return error + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::DailPeerError))).await; + } } }, // Store a value in the DHT and (optionally) on explicit specific peers @@ -816,25 +766,8 @@ impl Core { // Insert into DHT if let Ok(_) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Insert into the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }); - } + // Send streamId to libp2p events, to track response + let _ = sender_channel_1.clone().send(stream_id); // Cache record on peers explicitly (if specified) if let Some(explicit_peers) = explicit_peers { @@ -846,146 +779,36 @@ impl Core { swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); } } else { - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - // Delete the request from the stream and then return a write error - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Return error - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Delete the request from the stream and then return a write error - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Return error - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(NetworkError::KadStoreRecordError(key))); - }); - } + // Return error + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::KadStoreRecordError(key)))).await; } }, // Perform a lookup in the DHT AppData::KademliaLookupRecord { key } => { let _ = swarm.behaviour_mut().kademlia.get_record(key.clone().into()); - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }); - } + // Send streamId to libp2p events, to track response + let _ = sender_channel_2.clone().send(stream_id); }, // Perform a lookup of peers that store a record AppData::KademliaGetProviders { key } => { let _ = swarm.behaviour_mut().kademlia.get_providers(key.clone().into()); - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Kad(key), Box::new(stream_id)); - }); - } + // Send streamId to libp2p events, to track response + let _ = sender_channel_3.clone().send(stream_id); } // Stop providing a record on the network AppData::KademliaStopProviding { key } => { swarm.behaviour_mut().kademlia.stop_providing(&key.into()); - - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - // Respond with a success - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Respond with a success - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); - }); - } } // Remove record from local store AppData::KademliaDeleteRecord { key } => { swarm.behaviour_mut().kademlia.remove_record(&key.into()); - - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - // Respond with a success - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Respond with a success - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(0)); - }); - } } // Return important routing table info AppData::KademliaGetRoutingTableInfo => { - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - let netinfo = network_info.clone(); - async_std::task::spawn(async move{ - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert into the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: netinfo.id.to_string() })); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - let netinfo = network_info.clone(); - tokio::task::spawn(async move{ - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert into the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(Kademlia::Info { protocol_id: netinfo.id.to_string() })); - }); - } + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaGetRoutingTableInfo{protocol_id: network_info.id.to_string()})).await; }, // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { @@ -993,85 +816,19 @@ impl Core { let rpc = Rpc::ReqResponse { data: keys.clone() }; // Inform the swarm to make the request - let outbound_id = swarm + let _ = swarm .behaviour_mut() .request_response .send_request(&peer, rpc); println!("keys: {:?}", keys); - // Insert temporarily into the `StreamResponseBuffer`. Here we use the `StreamResponseBuffer` as a temporary bridge to `libp2p` events - // where we can get the response and then replace the buffer with the actual response data - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Outbound(outbound_id), Box::new(stream_id)); - }); - } - } - } - } - - StreamData::Network(stream_id, network_data) => { - // Trackable stream id - let trackable_stream_id = TrackableStreamId::Id(stream_id); - match network_data { - // Dail peer - NetworkData::DailPeer(multiaddr) => { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); - - if let Ok(multiaddr) = multiaddr.parse::() { - if let Ok(_) = swarm.dial(multiaddr.clone()) { - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // Put result into response buffer - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Put result into response buffer - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(stream_id)); - }); - } - } else { - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // Put result into response buffer - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move{ - // Put result into response buffer - netcore.stream_response_buffer.lock().await.insert(trackable_stream_id, Box::new(SwarmNlError::RemotePeerDialError(multiaddr.to_string()))); - }); - } - } - } + // Send streamId to libp2p events, to track response + let _ = sender_channel_4.clone().send(stream_id); } } } + _ => {} } }, _ => {} @@ -1210,93 +967,25 @@ impl Core { peer_id.to_base58() }).collect::>(); - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move { - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert response message the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings })); - } - } - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert response message the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings })); - } - } - }); + // Receive data from our one-way channel + if let Ok(stream_id) = recv_channel_3.recv().await { + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaGetProviders{ key: key.to_vec(), providers: peer_id_strings })).await; } } kad::QueryResult::GetProviders(Err(_)) => {}, kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( - kad::PeerRecord { record:kad::Record{ key, value, .. }, .. }, + kad::PeerRecord { record:kad::Record{ value, .. }, .. }, ))) => { - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move { - // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert response message the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::RecordFound { key: key.to_vec(), value: value.to_vec() })); - } - } - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Insert response message the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(DhtOps::RecordFound { key: key.to_vec(), value: value.to_vec() })); - } - } - }); + // Receive data from out one-way channel + if let Ok(stream_id) = recv_channel_2.recv().await { + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaLookupRecord(value))).await; } } - kad::QueryResult::GetRecord(Ok(_)) => {}, + kad::QueryResult::GetRecord(Ok(_)) => { + // TODO!: How do we track this? + }, kad::QueryResult::GetRecord(Err(e)) => { let key = match e { kad::GetRecordError::NotFound { key, .. } => key, @@ -1304,53 +993,33 @@ impl Core { kad::GetRecordError::Timeout { key } => key, }; - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move { - // Delete the temporary data and then return error - // Get `StreamId` - if let Some(opaque_data) = network_core.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - network_core.stream_request_buffer.lock().await.remove(&stream_id); - - // Return error - network_core.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::KadFetchRecordError(key.to_vec()))); - } - } - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // Delete the temporary data and then return error - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Kad(key.to_vec())) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Return error - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::KadFetchRecordError(key.to_vec()))); - } - } - }); + // Receive data from out one-way channel + if let Ok(stream_id) = recv_channel_2.recv().await { + // Send the error back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::KadFetchRecordError(key.to_vec())))).await; } } kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { + // Receive data from our one-way channel + if let Ok(stream_id) = recv_channel_1.recv().await { + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaStoreRecordSuccess)).await; + } + // Call handler network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()).await; } - kad::QueryResult::PutRecord(Err(_)) => { + kad::QueryResult::PutRecord(Err(e)) => { + let key = match e { + kad::PutRecordError::QuorumFailed { key, .. } => key, + kad::PutRecordError::Timeout { key, .. } => key, + }; + + if let Ok(stream_id) = recv_channel_1.recv().await { + // Send the error back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::KadStoreRecordError(key.to_vec())))).await; + } + // Call handler network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()).await; } @@ -1407,101 +1076,30 @@ impl Core { } }, // We have a response message - request_response::Message::Response { request_id, response } => { - // We want to take out the temporary data in the stream buffer and then replace it with valid data, for it to be picked up - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move { - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - match response { - Rpc::ReqResponse { data } => { - println!("--> {:?}", data); - // Insert response message the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(data)); - }, - } - } - } - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - match response { - Rpc::ReqResponse { data } => { - println!("--> {:?}", data); - // Insert response message the response buffer for it to be picked up - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(data)); - }, - } - } - } - }); + request_response::Message::Response { response, .. } => { + // Receive data from our one-way channel + if let Ok(stream_id) = recv_channel_4.recv().await { + match response { + Rpc::ReqResponse { data } => { + println!("--> {:?}", data); + + // Send the response back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::FetchData(data))).await; + }, + } } - }, + }, }, - request_response::Event::OutboundFailure { request_id, .. } => { + request_response::Event::OutboundFailure { .. } => { // Delete the temporary data and then return error - println!("out failure"); - #[cfg(feature = "async-std-runtime")] - { - let netcore = network_core.clone(); - async_std::task::spawn(async move { - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Return error - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); - } - } - }); - } - - #[cfg(feature = "tokio-runtime")] - { - let netcore = network_core.clone(); - tokio::task::spawn(async move { - // Get `StreamId` - if let Some(opaque_data) = netcore.stream_response_buffer.lock().await.remove(&TrackableStreamId::Outbound(request_id)) { - // Make sure this is temporary data (i.e `StreamId`) - let opaque_data = &opaque_data as &dyn Any; - - if let Some(stream_id) = opaque_data.downcast_ref::() { - // Remove the request from the request buffer - netcore.stream_request_buffer.lock().await.remove(&stream_id); - - // Return error - netcore.stream_response_buffer.lock().await.insert(TrackableStreamId::Id(*stream_id), Box::new(NetworkError::RpcDataFetchError)); - } - } - }); - } + println!("fbujsnfd"); + + // Receive data from out one-way channel + // if let Ok(stream_id) = recv_channel_4.recv().await { + // println!("----4-42----"); + // // Send the error back to the application layer + // let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::RpcDataFetchError))).await; + // } }, _ => {} } diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index fa81c32c1..66fd8ff2d 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; /// Copyright (c) 2024 Algorealm use libp2p::request_response::OutboundRequestId; use serde::{Deserialize, Serialize}; -use std::{any::Any, time::Instant}; +use std::{time::Instant}; use thiserror::Error; use self::ping_config::PingInfo; @@ -15,15 +15,17 @@ pub const NETWORK_READ_TIMEOUT: u64 = 60; /// The time it takes for the task to sleep before it can recheck if an output has been placed in /// the repsonse buffer (7 seconds) -pub const TASK_SLEEP_DURATION: u64 = 7; +pub const TASK_SLEEP_DURATION: u64 = 3; +/// Type that represents the response of the network layer to the application layer's event handler +pub type AppResponseResult = Result; /// Data exchanged over a stream between the application and network layer #[derive(Debug, Clone)] pub(super) enum StreamData { /// Application data sent over the stream - Application(StreamId, AppData), - /// Network data sent over the stream - Network(StreamId, NetworkData), + FromApplication(StreamId, AppData), + /// Network response data sent over the stream to the application layer + ToApplication(StreamId, AppResponse), } /// Data sent from the application layer to the networking layer @@ -31,6 +33,8 @@ pub(super) enum StreamData { pub enum AppData { /// A simple echo message Echo(String), + /// Dail peer + DailPeer(MultiaddrString), /// Store a value associated with a given key in the Kademlia DHT KademliaStoreRecord { key: Vec, @@ -56,21 +60,32 @@ pub enum AppData { // Gossip related requests } -/// Data sent from the networking to itself +/// Response to requests sent from the aplication to the network layer #[derive(Debug, Clone)] -pub(super) enum NetworkData { - /// Dail peer - DailPeer(MultiaddrString), -} - -/// Results from Kademlia DHT operation -pub enum Kademlia { - /// Return important information about the DHT, this will be increased shortly - Info { protocol_id: String }, +pub enum AppResponse { + /// The value written to the network + Echo(String), + /// The peer we dailed + DailPeer(String), + /// Store record success + KademliaStoreRecordSuccess, + /// DHT lookup result + KademliaLookupRecord(Vec), + /// Nodes storing a particular record in the DHT + KademliaGetProviders { + key: Vec, + providers: Vec, + }, + /// Routing table information + KademliaGetRoutingTableInfo { protocol_id: String }, + /// RPC result + FetchData(Vec>), + /// A network error occured while executing the request + Error(NetworkError), } /// Network error type containing errors encountered during network operations -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum NetworkError { #[error("timeout occured waiting for data from network layer")] NetworkReadTimeout, @@ -82,22 +97,10 @@ pub enum NetworkError { RpcDataFetchError, #[error("failed to fetch record from the DHT")] KadFetchRecordError(Vec), -} - -/// Operations performed on the Kademlia DHT -#[derive(Debug)] -pub enum DhtOps { - /// Value found in the DHT - RecordFound { key: Vec, value: Vec }, - /// No value found - RecordNotFound, - /// Nodes found that provide value for key - ProvidersFound { - key: Vec, - providers: Vec, - }, - /// No providers returned (This is most likely due to an error) - NoProvidersFound, + #[error("task carrying app response panicked")] + InternalTaskError, + #[error("failed to dail peer")] + DailPeerError, } /// A simple struct used to track requests sent from the application layer to the network layer @@ -117,40 +120,8 @@ impl StreamId { } } -/// Enum that represents various ids that can be tracked on the [`StreamResponseBuffer`] -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum TrackableStreamId { - /// Our custom stream id - Id(StreamId), - /// RPC request id - Outbound(OutboundRequestId), - /// Kademlia Key - Kad(Vec), -} - /// Type that specifies the result of querying the network layer -pub type NetworkResult = Result, NetworkError>; - -/// Marker trait that indicates a stream reponse data object -pub trait StreamResponseType -where - Self: Send + Sync + Any + 'static, -{ -} - -/// Macro that implements [`StreamResponseType`] for various types to occupy the -/// [`StreamResponseBuffer`] [`StreamResponseBuffer`] -macro_rules! impl_stream_response_for_types { ( $( $t:ident )* ) => { - $( - impl StreamResponseType for $t {} - )* }; -} - -impl_stream_response_for_types!(u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 - usize isize f32 f64 String bool Kademlia StreamId SwarmNlError NetworkError DhtOps); - -/// Implement [`StreamResponseType`] for `Vec` -impl StreamResponseType for Vec where T: StreamResponseType {} +pub type NetworkResult = Result; /// Type that keeps track of the requests from the application layer. /// This type has a maximum buffer size and will drop subsequent requests when full. @@ -182,18 +153,13 @@ impl StreamRequestBuffer { } false } - - /// Remove a [`StreamId`] from the buffer - pub fn remove(&mut self, id: &StreamId) { - self.buffer.remove(&id); - } } /// Type that keeps track of the response to the requests from the application layer. pub(super) struct StreamResponseBuffer { /// Max responses we can keep track of size: usize, - buffer: HashMap>, + buffer: HashMap, } impl StreamResponseBuffer { @@ -205,9 +171,9 @@ impl StreamResponseBuffer { } } - /// Push a [`TrackableStreamId`] into buffer. + /// Push a [`StreamId`] into buffer. /// Returns `false` if the buffer is full and request cannot be stored - pub fn insert(&mut self, id: TrackableStreamId, response: Box) -> bool { + pub fn insert(&mut self, id: StreamId, response: AppResponseResult) -> bool { if self.buffer.len() < self.size { self.buffer.insert(id, response); return true; @@ -215,8 +181,8 @@ impl StreamResponseBuffer { false } - /// Remove a [`TrackableStreamId`] from the buffer - pub fn remove(&mut self, id: &TrackableStreamId) -> Option> { + /// Remove a [`StreamId`] from the buffer + pub fn remove(&mut self, id: &StreamId) -> Option { self.buffer.remove(&id) } } @@ -502,8 +468,6 @@ pub struct NetworkChannel { application_sender: Sender, /// Current stream id. Useful for opening new streams, we just have to bump the number by 1 current_stream_id: Arc>, - /// The network read timeout - network_read_delay: AsyncDuration, } impl NetworkChannel { @@ -520,36 +484,44 @@ impl NetworkChannel { stream_response_buffer, application_sender, current_stream_id, - network_read_delay, } } /// Send data to the network layer and recieve a unique `StreamId` to track the request /// If the internal stream buffer is full, `None` will be returned. - pub async fn send_to_network(&mut self, request: AppData) -> Option { + pub async fn send_to_network(&mut self, app_request: AppData) -> Option { // Generate stream id let stream_id = StreamId::next(*self.current_stream_id.lock().await); - let request = StreamData::Application(stream_id, request); - - // Acquire lock on stream_request_buffer - let mut stream_request_buffer = self.stream_request_buffer.lock().await; - - // Add to request buffer - if !stream_request_buffer.insert(stream_id) { - // Buffer appears to be full - return None; - } - - println!("{:?}", stream_request_buffer); - println!("{:?}", request); + let request = StreamData::FromApplication(stream_id, app_request.clone()); + + // Only a few requests should be tracked internally + match app_request { + // Doesn't need any tracking + AppData::KademliaDeleteRecord { .. } | AppData::KademliaStopProviding { .. } => { + // Send request + let _ = self.application_sender.send(request).await; + return None; + }, + // Okay with the rest + _ => { + // Acquire lock on stream_request_buffer + let mut stream_request_buffer = self.stream_request_buffer.lock().await; + + // Add to request buffer + if !stream_request_buffer.insert(stream_id) { + // Buffer appears to be full + return None; + } - // Send request - if let Ok(_) = self.application_sender.send(request).await { - // Store latest stream id - *self.current_stream_id.lock().await = stream_id; - Some(stream_id) - } else { - None + // Send request + if let Ok(_) = self.application_sender.send(request).await { + // Store latest stream id + *self.current_stream_id.lock().await = stream_id; + return Some(stream_id); + } else { + return None; + } + }, } } @@ -562,94 +534,66 @@ impl NetworkChannel { #[cfg(feature = "async-std-runtime")] { let channel = self.clone(); - let recv_task = tokio::task::spawn(async move { + let response_handler = tokio::task::spawn(async move { let mut loop_count = 0; loop { // Attempt to acquire the lock without blocking - let maybe_response_value = channel.stream_response_buffer.try_lock(); - - // Check if the lock was acquired - if let Ok(mut response_buffer) = maybe_response_value { - // Check if the value is available in the response buffer - if let Some(response_value) = - response_buffer.remove(&TrackableStreamId::Id(stream_id)) - { - // Make sure the value is not `StreamId`. If it is, then the request is - // being processed and the final value we want has not arrived. - - // Get inner boxed value - let inner_value = &response_value as &dyn Any; - if inner_value.downcast_ref::().is_none() { - return response_value; - } - } - } + let mut buffer_guard = channel.stream_response_buffer.lock().await; - async_std::task::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + // Check if the value is available in the response buffer + if let Some(result) = buffer_guard.remove(&stream_id) { + return Ok(result); + } - if loop_count < 6 { + // Timeout after 5 trials + if loop_count < 5 { loop_count += 1; } else { - break; + return Err(NetworkError::NetworkReadTimeout); } - } - // Error: return the stream id - Box::new(stream_id) - }) - .await; + // Failed to acquire the lock, sleep and retry + async_std::task::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + } + }); - if let Ok(response_value) = recv_task { - Ok(response_value) - } else { - return Err(NetworkError::NetworkReadTimeout); + // Wait for the spawned task to complete + match response_handler.await { + Ok(result) => result?, + Err(_) => Err(NetworkError::InternalTaskError), } } #[cfg(feature = "tokio-runtime")] { let channel = self.clone(); - let recv_task = tokio::task::spawn(async move { + let response_handler = tokio::task::spawn(async move { let mut loop_count = 0; loop { // Attempt to acquire the lock without blocking - let maybe_response_value = channel.stream_response_buffer.try_lock(); - - // Check if the lock was acquired - if let Ok(mut response_buffer) = maybe_response_value { - // Check if the value is available in the response buffer - if let Some(response_value) = - response_buffer.remove(&TrackableStreamId::Id(stream_id)) - { - // Make sure the value is not `StreamId`. If it is, then the request is - // being processed and the final value we want has not arrived. - - // Get inner boxed value - let inner_value = &response_value as &dyn Any; - if inner_value.downcast_ref::().is_none() { - return response_value; - } - } - } + let mut buffer_guard = channel.stream_response_buffer.lock().await; - tokio::time::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + // Check if the value is available in the response buffer + if let Some(result) = buffer_guard.remove(&stream_id) { + return Ok(result); + } - if loop_count < 6 { + // Timeout after 5 trials + if loop_count < 5 { loop_count += 1; } else { - break; + return Err(NetworkError::NetworkReadTimeout); } - } - // Error: return the stream id - Box::new(stream_id) - }) - .await; + // Failed to acquire the lock, sleep and retry + tokio::time::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + } + }); - if let Ok(response_value) = recv_task { - Ok(response_value) - } else { - return Err(NetworkError::NetworkReadTimeout); + // Wait for the spawned task to complete + match response_handler.await { + Ok(result) => result?, + Err(_) => Err(NetworkError::InternalTaskError), } } } @@ -669,3 +613,4 @@ impl NetworkChannel { } } } + From 6fbaf78486f793b5b622ffd63f676a0fd6663067 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Thu, 9 May 2024 16:56:09 +0100 Subject: [PATCH 10/36] add: implementation of request-response --- client/bootstrap_config.ini | 2 +- client/src/main.rs | 141 ++++++++---------- swarm_nl/src/core/mod.rs | 275 +++++++++++++++++++++++------------ swarm_nl/src/core/prelude.rs | 216 +++++---------------------- swarm_nl/src/util.rs | 7 + 5 files changed, 289 insertions(+), 352 deletions(-) diff --git a/client/bootstrap_config.ini b/client/bootstrap_config.ini index ee5ea1522..67404baa7 100644 --- a/client/bootstrap_config.ini +++ b/client/bootstrap_config.ini @@ -9,4 +9,4 @@ udp=49201 [bootstrap] ; The boostrap nodes to connect to immediately after start up -boot_nodes=[12D3KooWMAtuoCTErUU16nJ3n1ig9Cfzr5fG5zdsMRbd45X4D93U:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file +boot_nodes=[12D3KooWMD3kvZ7hSngeu1p7HAoCCYusSXqPPYDPvzxsa9T4vz3a:/ip4/127.0.0.1/tcp/49152] \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index 7493fa0a1..414f36ce8 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,12 +4,13 @@ /// Objective: Form alliances and conquer as much empires as possible! /// It is a multi-player game /// Enjoy! -use std::{any::Any, borrow::Cow, num::NonZeroU32, time::Duration}; +use std::{borrow::Cow, num::NonZeroU32, time::Duration}; use swarm_nl::{ async_trait, - core::EventHandler, - core::{AppData, Core, CoreBuilder, NetworkChannel}, + core::{EventHandler, AppResponse}, + core::{AppData, Core, CoreBuilder}, setup::BootstrapConfig, + util::string_to_peer_id, ConnectedPoint, ConnectionId, PeerId, }; @@ -47,7 +48,6 @@ impl Empire { impl EventHandler for Empire { async fn new_listen_addr( &mut self, - mut channel: NetworkChannel, local_peer_id: PeerId, _listener_id: swarm_nl::ListenerId, addr: swarm_nl::Multiaddr, @@ -59,28 +59,10 @@ impl EventHandler for Empire { "There are {} soldiers guarding the {} Empire gate", self.soldiers, self.name ); - - let request = vec!["military_status".as_bytes().to_vec()]; - - // Prepare request - let status_request = AppData::FetchData { - keys: request, - peer: local_peer_id, - }; - - // Send request - let stream_id = channel.send_to_network(status_request).await.unwrap(); - - // Get response - // AppData::Fetch returns a Vec>, hence we can parse the response from it - let status_response = channel.recv_from_network(stream_id).await; - - println!("{:?}", status_response); } async fn connection_established( &mut self, - mut channel: NetworkChannel, peer_id: PeerId, _connection_id: ConnectionId, _endpoint: &ConnectedPoint, @@ -88,9 +70,6 @@ impl EventHandler for Empire { _established_in: Duration, ) { println!("Connection established with peer: {}", peer_id); - // When we establish a new connection, the empires send message to the other empire to knoe - // their military status - } /// Handle any incoming RPC from any neighbouring empire @@ -116,31 +95,12 @@ impl EventHandler for Empire { /// Setup game (This is for the persian Empire) /// This requires no bootnodes connection // #[cfg(not(feature = "macedonian"))] -pub async fn setup_game() -> Core { - // First, we want to configure our node - let config = BootstrapConfig::default(); - - // State kept by this node - let empire = Empire::new(String::from("Spartan")); - - // Set up network - CoreBuilder::with_config(config, empire) - .build() - .await - .unwrap() -} - -/// The Macedonian Empire setup. -/// These require bootnodes of empires to form alliance. -/// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot -/// parameters -// #[cfg(feature = "macedonian")] // pub async fn setup_game() -> Core { -// // First, we want to configure our node with the bootstrap config file on disk -// let config = BootstrapConfig::from_file("bootstrap_config.ini"); +// // First, we want to configure our node +// let config = BootstrapConfig::default(); // // State kept by this node -// let empire = Empire::new(String::from("Macedonian")); +// let empire = Empire::new(String::from("Spartan")); // // Set up network // CoreBuilder::with_config(config, empire) @@ -149,11 +109,32 @@ pub async fn setup_game() -> Core { // .unwrap() // } +/// The Macedonian Empire setup. +/// These require bootnodes of empires to form alliance. +/// We will be providing the location (peer id and multiaddress) of the Spartan Empire as boot +/// parameters +// #[cfg(feature = "macedonian")] +pub async fn setup_game() -> Core { + // First, we want to configure our node with the bootstrap config file on disk + let config = BootstrapConfig::from_file("bootstrap_config.ini"); + + // State kept by this node + let empire = Empire::new(String::from("Macedonian")); + + // Set up network + CoreBuilder::with_config(config, empire) + .build() + .await + .unwrap() +} + /// Play game pub async fn play_game() { // Setup network let mut core = setup_game().await; + // TODO: DELAY FOR A WHILE + // Print game state println!("Empire Information:"); println!("Name: {}", core.state.soldiers); @@ -162,38 +143,40 @@ pub async fn play_game() { println!("Land mass: {}", core.state.land_mass); println!("Gold reserve: {}", core.state.gold_reserve); - // let request = vec!["military_status".as_bytes().to_vec()]; - // let peer_id_string = "12D3KooWPcL6iGBjfhDTRYY8YESh94395h79iccCQj7jRQWYzm3w"; - // let peer_id = PeerId::from_bytes(&peer_id_string.from_base58().unwrap_or_default()).unwrap(); - - // // Prepare request - // let status_request = AppData::FetchData { - // keys: request, - // peer: peer_id, - // }; - - // // Send request - // if let Some(stream_id) = core.send_to_network(status_request).await { - // // Get response - // // AppData::Fetch returns a Vec>, hence we can parse the response from it - // let status_response = core.recv_from_network(stream_id).await.unwrap(); - - // let inner_value = &status_response as &dyn Any; - // if let Some(status) = inner_value.downcast_ref::>>() { - // // Get empire name - // let empire_name = String::from_utf8_lossy(&status[0]); - // let military_status = status[1][0]; - - // // Print the military status of the empire we just contacted - // println!("Empire Contacted:"); - // println!("Name: {} Empire", empire_name); - // println!("Military Capacity: {} Soldiers", military_status); - // } else { - // println!("Could not decode response") - // } - // } else { - // println!("Could not get military status of the empire at {}", peer_id); - // } + // TODO! FUNCTION TO CHECK NODES I'M CONNECTED WITH + + // TODO: Wait a little to help the network boot + + // Let them connect first + tokio::time::sleep(Duration::from_secs(6)).await; + + let request = vec!["military_status".as_bytes().to_vec()]; + + // Spartan Empire + let remote_peer_id = "12D3KooWMD3kvZ7hSngeu1p7HAoCCYusSXqPPYDPvzxsa9T4vz3a"; + + // Prepare request + let status_request = AppData::FetchData { + keys: request, + peer: string_to_peer_id(remote_peer_id).unwrap(), + }; + + // Send request + let stream_id = core.send_to_network(status_request).await.unwrap(); + + // Get response + // AppData::Fetch returns a Vec>, hence we can parse the response from it + if let Ok(status_response) = core.recv_from_network(stream_id).await { + if let AppResponse::FetchData(status) = status_response { + let empire_name = String::from_utf8_lossy(&status[0]); + let military_status = status[1][0]; + + // Print the military status of the empire we just contacted + println!("Empire Contacted:"); + println!("Name: {} Empire", empire_name); + println!("Military Capacity: {} Soldiers", military_status); + } + } // Keep looping so we can record network events loop {} diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index ae66747b6..70e239fab 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -29,14 +29,14 @@ use libp2p::{ use self::ping_config::*; use super::*; -use crate::setup::BootstrapConfig; +use crate::{setup::BootstrapConfig, util::string_to_peer_id}; #[cfg(feature = "async-std-runtime")] pub use async_std::sync::Mutex; +use tokio::sync::broadcast; #[cfg(feature = "tokio-runtime")] pub use tokio::sync::Mutex; -use tokio::sync::broadcast; mod prelude; pub use prelude::*; @@ -479,8 +479,7 @@ impl CoreBuilder { // Add bootnodes to local routing table, if any for peer_info in self.boot_nodes { // PeerId - if let Ok(peer_id) = PeerId::from_bytes(&peer_info.0.from_base58().unwrap_or_default()) - { + if let Some(peer_id) = string_to_peer_id(&peer_info.0) { // Multiaddress if let Ok(multiaddr) = peer_info.1.parse::() { swarm @@ -543,20 +542,10 @@ impl CoreBuilder { let stream_response_buffer = Arc::new(Mutex::new(StreamResponseBuffer::new(self.stream_size))); - // Setup the application event handler's network communication channel - let event_comm_channel = NetworkChannel::new( - stream_request_buffer.clone(), - stream_response_buffer.clone(), - application_sender.clone(), - Arc::new(Mutex::new(stream_id)), - self.network_read_delay, - ); - // Aggregate the useful network information let network_info = NetworkInfo { id: self.network_id, ping: ping_info, - event_comm_channel, }; // Build the network core @@ -668,37 +657,155 @@ impl Core { self.keypair.public().to_peer_id().to_string() } + /// Send data to the network layer and recieve a unique `StreamId` to track the request + /// If the internal stream buffer is full, `None` will be returned. + pub async fn send_to_network(&mut self, app_request: AppData) -> Option { + // Generate stream id + let stream_id = StreamId::next(*self.current_stream_id.lock().await); + let request = StreamData::FromApplication(stream_id, app_request.clone()); + + // Only a few requests should be tracked internally + match app_request { + // Doesn't need any tracking + AppData::KademliaDeleteRecord { .. } | AppData::KademliaStopProviding { .. } => { + // Send request + let _ = self.application_sender.send(request).await; + return None; + }, + // Okay with the rest + _ => { + // Acquire lock on stream_request_buffer + let mut stream_request_buffer = self.stream_request_buffer.lock().await; + + // Add to request buffer + if !stream_request_buffer.insert(stream_id) { + // Buffer appears to be full + return None; + } + + // Send request + if let Ok(_) = self.application_sender.send(request).await { + // Store latest stream id + *self.current_stream_id.lock().await = stream_id; + return Some(stream_id); + } else { + return None; + } + }, + } + } + + /// TODO! Buffer cleanup algorithm + /// Explicitly rectrieve the reponse to a request sent to the network layer. + /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay + /// and read immediately as the response to the request should already be in the stream response + /// buffer. + pub async fn recv_from_network(&mut self, stream_id: StreamId) -> NetworkResult { + #[cfg(feature = "async-std-runtime")] + { + let channel = self.clone(); + let response_handler = tokio::task::spawn(async move { + let mut loop_count = 0; + loop { + // Attempt to acquire the lock without blocking + let mut buffer_guard = channel.stream_response_buffer.lock().await; + + // Check if the value is available in the response buffer + if let Some(result) = buffer_guard.remove(&stream_id) { + return Ok(result); + } + + // Timeout after 5 trials + if loop_count < 5 { + loop_count += 1; + } else { + return Err(NetworkError::NetworkReadTimeout); + } + + // Failed to acquire the lock, sleep and retry + async_std::task::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + } + }); + + // Wait for the spawned task to complete + match response_handler.await { + Ok(result) => result?, + Err(_) => Err(NetworkError::InternalTaskError), + } + } + + #[cfg(feature = "tokio-runtime")] + { + let channel = self.clone(); + let response_handler = tokio::task::spawn(async move { + let mut loop_count = 0; + loop { + // Attempt to acquire the lock without blocking + let mut buffer_guard = channel.stream_response_buffer.lock().await; + + // Check if the value is available in the response buffer + if let Some(result) = buffer_guard.remove(&stream_id) { + return Ok(result); + } + + // Timeout after 5 trials + if loop_count < 5 { + loop_count += 1; + } else { + return Err(NetworkError::NetworkReadTimeout); + } + + // Failed to acquire the lock, sleep and retry + tokio::time::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; + } + }); + + // Wait for the spawned task to complete + match response_handler.await { + Ok(result) => result?, + Err(_) => Err(NetworkError::InternalTaskError), + } + } + } + + /// Perform an atomic `send` and `recieve` from the network layer. This function is atomic and + /// blocks until the result of the request is returned from the network layer. This function + /// should mostly be used when the result of the request is needed immediately and delay can be + /// condoned. It will still timeout if the delay exceeds the configured period. + /// If the internal buffer is full, it will return an error. + pub async fn fetch_from_network(&mut self, request: AppData) -> NetworkResult { + // send request + if let Some(stream_id) = self.send_to_network(request).await { + // wait to recieve response from the network + self.recv_from_network(stream_id).await + } else { + Err(NetworkError::StreamBufferOverflow) + } + } + /// Handle the responses coming from the network layer. This is usually as a result of a request /// from the application layer async fn handle_network_response(mut receiver: Receiver, network_core: Core) { loop { select! { response = receiver.select_next_some() => { - if let Ok(mut buffer_guard) = network_core.stream_response_buffer.try_lock() { - match response { - // Send response to request operations specified by the application layer - StreamData::ToApplication(stream_id, response) => match response { - res @ AppResponse::Echo(..) => buffer_guard.insert(stream_id, Ok(res)), - res @ AppResponse::DailPeer(..) => buffer_guard.insert(stream_id, Ok(res)), - res @ AppResponse::KademliaStoreRecordSuccess => buffer_guard.insert(stream_id, Ok(res)), - res @ AppResponse::KademliaLookupRecord(..) => buffer_guard.insert(stream_id, Ok(res)), - res @ AppResponse::KademliaGetProviders{..} => buffer_guard.insert(stream_id, Ok(res)), - res @ AppResponse::KademliaGetRoutingTableInfo { .. } => buffer_guard.insert(stream_id, Ok(res)), - res @ AppResponse::FetchData(..) => buffer_guard.insert(stream_id, Ok(res)), - // Error - AppResponse::Error(error) => buffer_guard.insert(stream_id, Err(error)) - }, - _ => false - }; - } else { - // sleep for a few seconds - #[cfg(feature = "async-std-runtime")] - async_std::task::sleep(Duration::from_secs(1)).await; - - // sleep for a few seconds - #[cfg(feature = "tokio-runtime")] - tokio::time::sleep(Duration::from_secs(1)).await; - } + // Acquire mutex to write to buffer + let mut buffer_guard = network_core.stream_response_buffer.lock().await; + match response { + // Send response to request operations specified by the application layer + StreamData::ToApplication(stream_id, response) => match response { + res @ AppResponse::Echo(..) => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::DailPeer(..) => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaStoreRecordSuccess => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaLookupRecord(..) => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaGetProviders{..} => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::KademliaGetRoutingTableInfo { .. } => buffer_guard.insert(stream_id, Ok(res)), + res @ AppResponse::FetchData(..) => buffer_guard.insert(stream_id, Ok(res)), + // Error + AppResponse::Error(error) => buffer_guard.insert(stream_id, Err(error)) + }, + _ => false + }; } } } @@ -715,26 +822,19 @@ impl Core { mut receiver: Receiver, mut network_core: Core, ) { - // Create one-way channels that help the application command receiver synchronize with the - // libp2p generated events - let (sender_channel_1, _) = broadcast::channel::(500); // For AppData::KademliaStoreRecord - let (sender_channel_2, _) = broadcast::channel::(100); // For AppData::KademliaLookupRecord - let (sender_channel_3, _) = broadcast::channel::(100); // For AppData::KademliaGetProviders - let (sender_channel_4, _) = broadcast::channel::(100); // For AppData::FetchData + + let mut exec_queue_1 = ExecQueue::new(); + let mut exec_queue_2 = ExecQueue::new(); + let mut exec_queue_3 = ExecQueue::new(); + let mut exec_queue_4 = ExecQueue::new(); // Loop to handle incoming application streams indefinitely. loop { - let mut recv_channel_1 = sender_channel_1.subscribe(); - let mut recv_channel_2 = sender_channel_2.subscribe(); - let mut recv_channel_3 = sender_channel_3.subscribe(); - let mut recv_channel_4 = sender_channel_4.subscribe(); - select! { // handle incoming stream data stream_data = receiver.next() => { match stream_data { Some(incoming_data) => { - println!("{:?}", incoming_data); match incoming_data { StreamData::FromApplication(stream_id, app_data) => { // Trackable stream id @@ -767,7 +867,7 @@ impl Core { // Insert into DHT if let Ok(_) = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One) { // Send streamId to libp2p events, to track response - let _ = sender_channel_1.clone().send(stream_id); + exec_queue_1.push(stream_id).await; // Cache record on peers explicitly (if specified) if let Some(explicit_peers) = explicit_peers { @@ -788,14 +888,14 @@ impl Core { let _ = swarm.behaviour_mut().kademlia.get_record(key.clone().into()); // Send streamId to libp2p events, to track response - let _ = sender_channel_2.clone().send(stream_id); + exec_queue_2.push(stream_id).await; }, // Perform a lookup of peers that store a record AppData::KademliaGetProviders { key } => { let _ = swarm.behaviour_mut().kademlia.get_providers(key.clone().into()); // Send streamId to libp2p events, to track response - let _ = sender_channel_3.clone().send(stream_id); + exec_queue_3.push(stream_id).await; } // Stop providing a record on the network AppData::KademliaStopProviding { key } => { @@ -821,10 +921,8 @@ impl Core { .request_response .send_request(&peer, rpc); - println!("keys: {:?}", keys); - // Send streamId to libp2p events, to track response - let _ = sender_channel_4.clone().send(stream_id); + exec_queue_4.push(stream_id).await; } } } @@ -843,7 +941,7 @@ impl Core { address, } => { // call configured handler - network_core.state.new_listen_addr(network_info.event_comm_channel.clone(), swarm.local_peer_id().to_owned(), listener_id, address).await; + network_core.state.new_listen_addr(swarm.local_peer_id().to_owned(), listener_id, address).await; } SwarmEvent::Behaviour(event) => match event { // Ping @@ -883,7 +981,7 @@ impl Core { } // Call custom handler - network_core.state.inbound_ping_success(network_info.event_comm_channel.clone(), peer, duration).await; + network_core.state.inbound_ping_success(peer, duration).await; } // Outbound ping failure Err(err_type) => { @@ -952,7 +1050,7 @@ impl Core { } // Call custom handler - network_core.state.outbound_ping_error(network_info.event_comm_channel.clone(), peer, err_type).await; + network_core.state.outbound_ping_error(peer, err_type).await; } } } @@ -968,7 +1066,7 @@ impl Core { }).collect::>(); // Receive data from our one-way channel - if let Ok(stream_id) = recv_channel_3.recv().await { + if let Some(stream_id) = exec_queue_3.pop().await { // Send the response back to the application layer let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaGetProviders{ key: key.to_vec(), providers: peer_id_strings })).await; } @@ -978,7 +1076,7 @@ impl Core { kad::PeerRecord { record:kad::Record{ value, .. }, .. }, ))) => { // Receive data from out one-way channel - if let Ok(stream_id) = recv_channel_2.recv().await { + if let Some(stream_id) = exec_queue_2.pop().await { // Send the response back to the application layer let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaLookupRecord(value))).await; } @@ -994,20 +1092,20 @@ impl Core { }; // Receive data from out one-way channel - if let Ok(stream_id) = recv_channel_2.recv().await { + if let Some(stream_id) = exec_queue_2.pop().await { // Send the error back to the application layer let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::KadFetchRecordError(key.to_vec())))).await; } } kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { // Receive data from our one-way channel - if let Ok(stream_id) = recv_channel_1.recv().await { + if let Some(stream_id) = exec_queue_1.pop().await { // Send the response back to the application layer let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::KademliaStoreRecordSuccess)).await; } // Call handler - network_core.state.kademlia_put_record_success(network_info.event_comm_channel.clone(), key.to_vec()).await; + network_core.state.kademlia_put_record_success(key.to_vec()).await; } kad::QueryResult::PutRecord(Err(e)) => { let key = match e { @@ -1015,23 +1113,23 @@ impl Core { kad::PutRecordError::Timeout { key, .. } => key, }; - if let Ok(stream_id) = recv_channel_1.recv().await { + if let Some(stream_id) = exec_queue_1.pop().await { // Send the error back to the application layer let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::KadStoreRecordError(key.to_vec())))).await; } // Call handler - network_core.state.kademlia_put_record_error(network_info.event_comm_channel.clone()).await; + network_core.state.kademlia_put_record_error().await; } kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { key, })) => { // Call handler - network_core.state.kademlia_start_providing_success(network_info.event_comm_channel.clone(), key.to_vec()).await; + network_core.state.kademlia_start_providing_success(key.to_vec()).await; } kad::QueryResult::StartProviding(Err(_)) => { // Call handler - network_core.state.kademlia_start_providing_error(network_info.event_comm_channel.clone()).await; + network_core.state.kademlia_start_providing_error().await; } _ => {} }, @@ -1042,7 +1140,7 @@ impl Core { CoreEvent::Identify(event) => match event { identify::Event::Received { peer_id, info } => { // We just recieved an `Identify` info from a peer.s - network_core.state.identify_info_recieved(network_info.event_comm_channel.clone(), peer_id, info.clone()).await; + network_core.state.identify_info_recieved(peer_id, info.clone()).await; // disconnect from peer of the network id is different if info.protocol_version != network_info.id.as_ref() { @@ -1078,11 +1176,9 @@ impl Core { // We have a response message request_response::Message::Response { response, .. } => { // Receive data from our one-way channel - if let Ok(stream_id) = recv_channel_4.recv().await { + if let Some(stream_id) = exec_queue_4.pop().await { match response { Rpc::ReqResponse { data } => { - println!("--> {:?}", data); - // Send the response back to the application layer let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::FetchData(data))).await; }, @@ -1091,15 +1187,11 @@ impl Core { }, }, request_response::Event::OutboundFailure { .. } => { - // Delete the temporary data and then return error - println!("fbujsnfd"); - // Receive data from out one-way channel - // if let Ok(stream_id) = recv_channel_4.recv().await { - // println!("----4-42----"); - // // Send the error back to the application layer - // let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::RpcDataFetchError))).await; - // } + if let Some(stream_id) = exec_queue_4.pop().await { + // Send the error back to the application layer + let _ = network_sender.send(StreamData::ToApplication(stream_id, AppResponse::Error(NetworkError::RpcDataFetchError))).await; + } }, _ => {} } @@ -1114,7 +1206,6 @@ impl Core { } => { // call configured handler network_core.state.connection_established( - network_info.event_comm_channel.clone(), peer_id, connection_id, &endpoint, @@ -1131,7 +1222,6 @@ impl Core { } => { // call configured handler network_core.state.connection_closed( - network_info.event_comm_channel.clone(), peer_id, connection_id, &endpoint, @@ -1144,7 +1234,7 @@ impl Core { address, } => { // call configured handler - network_core.state.expired_listen_addr(network_info.event_comm_channel.clone(), listener_id, address).await; + network_core.state.expired_listen_addr(listener_id, address).await; } SwarmEvent::ListenerClosed { listener_id, @@ -1152,33 +1242,33 @@ impl Core { reason: _, } => { // call configured handler - network_core.state.listener_closed(network_info.event_comm_channel.clone(), listener_id, addresses).await; + network_core.state.listener_closed(listener_id, addresses).await; } SwarmEvent::ListenerError { listener_id, error: _, } => { // call configured handler - network_core.state.listener_error(network_info.event_comm_channel.clone(), listener_id).await; + network_core.state.listener_error(listener_id).await; } SwarmEvent::Dialing { peer_id, connection_id, } => { // call configured handler - network_core.state.dialing(network_info.event_comm_channel.clone(), peer_id, connection_id).await; + network_core.state.dialing(peer_id, connection_id).await; } SwarmEvent::NewExternalAddrCandidate { address } => { // call configured handler - network_core.state.new_external_addr_candidate(network_info.event_comm_channel.clone(), address).await; + network_core.state.new_external_addr_candidate(address).await; } SwarmEvent::ExternalAddrConfirmed { address } => { // call configured handler - network_core.state.external_addr_confirmed(network_info.event_comm_channel.clone(), address).await; + network_core.state.external_addr_confirmed(address).await; } SwarmEvent::ExternalAddrExpired { address } => { // call configured handler - network_core.state.external_addr_expired(network_info.event_comm_channel.clone(), address).await; + network_core.state.external_addr_expired(address).await; } SwarmEvent::IncomingConnection { connection_id, @@ -1186,7 +1276,7 @@ impl Core { send_back_addr, } => { // call configured handler - network_core.state.incoming_connection(network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr).await; + network_core.state.incoming_connection(connection_id, local_addr, send_back_addr).await; } SwarmEvent::IncomingConnectionError { connection_id, @@ -1196,7 +1286,6 @@ impl Core { } => { // call configured handler network_core.state.incoming_connection_error( - network_info.event_comm_channel.clone(), connection_id, local_addr, send_back_addr, @@ -1208,7 +1297,7 @@ impl Core { error: _, } => { // call configured handler - network_core.state.outgoing_connection_error(network_info.event_comm_channel.clone(), connection_id, peer_id).await; + network_core.state.outgoing_connection_error(connection_id, peer_id).await; } _ => todo!(), } diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index 66fd8ff2d..e3f72d38a 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -1,8 +1,7 @@ use async_trait::async_trait; /// Copyright (c) 2024 Algorealm -use libp2p::request_response::OutboundRequestId; use serde::{Deserialize, Serialize}; -use std::{time::Instant}; +use std::{time::Instant, collections::VecDeque}; use thiserror::Error; use self::ping_config::PingInfo; @@ -15,7 +14,7 @@ pub const NETWORK_READ_TIMEOUT: u64 = 60; /// The time it takes for the task to sleep before it can recheck if an output has been placed in /// the repsonse buffer (7 seconds) -pub const TASK_SLEEP_DURATION: u64 = 3; +pub const TASK_SLEEP_DURATION: u64 = 7; /// Type that represents the response of the network layer to the application layer's event handler pub type AppResponseResult = Result; @@ -209,7 +208,7 @@ pub trait EventHandler { /// Event that informs the network core that we have started listening on a new multiaddr. async fn new_listen_addr( &mut self, - _channel: NetworkChannel, + _local_peer_id: PeerId, _listener_id: ListenerId, _addr: Multiaddr, @@ -220,7 +219,7 @@ pub trait EventHandler { /// Event that informs the network core about a newly established connection to a peer. async fn connection_established( &mut self, - _channel: NetworkChannel, + _peer_id: PeerId, _connection_id: ConnectionId, _endpoint: &ConnectedPoint, @@ -233,7 +232,7 @@ pub trait EventHandler { /// Event that informs the network core about a closed connection to a peer. async fn connection_closed( &mut self, - _channel: NetworkChannel, + _peer_id: PeerId, _connection_id: ConnectionId, _endpoint: &ConnectedPoint, @@ -246,7 +245,7 @@ pub trait EventHandler { /// Event that announces expired listen address. async fn expired_listen_addr( &mut self, - _channel: NetworkChannel, + _listener_id: ListenerId, _address: Multiaddr, ) { @@ -256,7 +255,7 @@ pub trait EventHandler { /// Event that announces a closed listener. async fn listener_closed( &mut self, - _channel: NetworkChannel, + _listener_id: ListenerId, _addresses: Vec, ) { @@ -264,14 +263,14 @@ pub trait EventHandler { } /// Event that announces a listener error. - async fn listener_error(&mut self, _channel: NetworkChannel, _listener_id: ListenerId) { + async fn listener_error(&mut self, _listener_id: ListenerId) { // Default implementation } /// Event that announces a dialing attempt. async fn dialing( &mut self, - _channel: NetworkChannel, + _peer_id: Option, _connection_id: ConnectionId, ) { @@ -279,17 +278,17 @@ pub trait EventHandler { } /// Event that announces a new external address candidate. - async fn new_external_addr_candidate(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + async fn new_external_addr_candidate(&mut self, _address: Multiaddr) { // Default implementation } /// Event that announces a confirmed external address. - async fn external_addr_confirmed(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + async fn external_addr_confirmed(&mut self, _address: Multiaddr) { // Default implementation } /// Event that announces an expired external address. - async fn external_addr_expired(&mut self, _channel: NetworkChannel, _address: Multiaddr) { + async fn external_addr_expired(&mut self, _address: Multiaddr) { // Default implementation } @@ -297,7 +296,7 @@ pub trait EventHandler { /// protocol negotiation. async fn incoming_connection( &mut self, - _channel: NetworkChannel, + _connection_id: ConnectionId, _local_addr: Multiaddr, _send_back_addr: Multiaddr, @@ -309,7 +308,7 @@ pub trait EventHandler { /// handshake. async fn incoming_connection_error( &mut self, - _channel: NetworkChannel, + _connection_id: ConnectionId, _local_addr: Multiaddr, _send_back_addr: Multiaddr, @@ -321,7 +320,7 @@ pub trait EventHandler { /// handshake. async fn outgoing_connection_error( &mut self, - _channel: NetworkChannel, + _connection_id: ConnectionId, _peer_id: Option, ) { @@ -332,7 +331,7 @@ pub trait EventHandler { /// The duration it took for a round trip is also returned async fn inbound_ping_success( &mut self, - _channel: NetworkChannel, + _peer_id: PeerId, _duration: Duration, ) { @@ -342,7 +341,7 @@ pub trait EventHandler { /// Event that announces a `Ping` error async fn outbound_ping_error( &mut self, - _channel: NetworkChannel, + _peer_id: PeerId, _err_type: Failure, ) { @@ -352,7 +351,7 @@ pub trait EventHandler { /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol async fn identify_info_recieved( &mut self, - _channel: NetworkChannel, + _peer_id: PeerId, _info: Info, ) { @@ -360,22 +359,22 @@ pub trait EventHandler { } /// Event that announces the successful write of a record to the DHT - async fn kademlia_put_record_success(&mut self, _channel: NetworkChannel, _key: Vec) { + async fn kademlia_put_record_success(&mut self, _key: Vec) { // Default implementation } /// Event that announces the failure of a node to save a record - async fn kademlia_put_record_error(&mut self, _channel: NetworkChannel) { + async fn kademlia_put_record_error(&mut self) { // Default implementation } /// Event that announces a node as a provider of a record in the DHT - async fn kademlia_start_providing_success(&mut self, _channel: NetworkChannel, _key: Vec) { + async fn kademlia_start_providing_success(&mut self, _key: Vec) { // Default implementation } /// Event that announces the failure of a node to become a provider of a record in the DHT - async fn kademlia_start_providing_error(&mut self, _channel: NetworkChannel) { + async fn kademlia_start_providing_error(&mut self) { // Default implementation } @@ -402,8 +401,6 @@ pub(super) struct NetworkInfo { pub id: StreamProtocol, /// Important information to manage `Ping` operations pub ping: PingInfo, - /// Application's event handler network communication channel - pub event_comm_channel: NetworkChannel, } /// Module that contains important data structures to manage `Ping` operations on the network @@ -452,165 +449,26 @@ pub mod ping_config { } } -/// Struct that allows the application layer comunicate with the network layer during event handling -#[derive(Clone)] -pub struct NetworkChannel { - /// The request buffer that keeps track of application requests being handled - stream_request_buffer: Arc>, - /// The consuming end of the stream that recieves data from the network layer - // application_receiver: Receiver, - /// This serves as a buffer for the results of the requests to the network layer. - /// With this, applications can make async requests and fetch their results at a later time - /// without waiting. This is made possible by storing a [`StreamId`] for a particular stream - /// request. - stream_response_buffer: Arc>, - /// The stream end for writing to the network layer - application_sender: Sender, - /// Current stream id. Useful for opening new streams, we just have to bump the number by 1 - current_stream_id: Arc>, -} +/// Network queue that tracks the execution of application requests in the network layer +pub(super) struct ExecQueue { + buffer: Mutex> +} -impl NetworkChannel { - /// Create a new application's event handler network communication channel - pub(super) fn new( - stream_request_buffer: Arc>, - stream_response_buffer: Arc>, - application_sender: Sender, - current_stream_id: Arc>, - network_read_delay: AsyncDuration, - ) -> NetworkChannel { +impl ExecQueue { + // Create new execution queue + pub fn new() -> Self { Self { - stream_request_buffer, - stream_response_buffer, - application_sender, - current_stream_id, - } - } - - /// Send data to the network layer and recieve a unique `StreamId` to track the request - /// If the internal stream buffer is full, `None` will be returned. - pub async fn send_to_network(&mut self, app_request: AppData) -> Option { - // Generate stream id - let stream_id = StreamId::next(*self.current_stream_id.lock().await); - let request = StreamData::FromApplication(stream_id, app_request.clone()); - - // Only a few requests should be tracked internally - match app_request { - // Doesn't need any tracking - AppData::KademliaDeleteRecord { .. } | AppData::KademliaStopProviding { .. } => { - // Send request - let _ = self.application_sender.send(request).await; - return None; - }, - // Okay with the rest - _ => { - // Acquire lock on stream_request_buffer - let mut stream_request_buffer = self.stream_request_buffer.lock().await; - - // Add to request buffer - if !stream_request_buffer.insert(stream_id) { - // Buffer appears to be full - return None; - } - - // Send request - if let Ok(_) = self.application_sender.send(request).await { - // Store latest stream id - *self.current_stream_id.lock().await = stream_id; - return Some(stream_id); - } else { - return None; - } - }, + buffer: Mutex::new(VecDeque::new()) } } - /// TODO! Buffer cleanup algorithm - /// Explicitly rectrieve the reponse to a request sent to the network layer. - /// This function is decoupled from the [`send_to_network()`] function so as to prevent delay - /// and read immediately as the response to the request should already be in the stream response - /// buffer. - pub async fn recv_from_network(&mut self, stream_id: StreamId) -> NetworkResult { - #[cfg(feature = "async-std-runtime")] - { - let channel = self.clone(); - let response_handler = tokio::task::spawn(async move { - let mut loop_count = 0; - loop { - // Attempt to acquire the lock without blocking - let mut buffer_guard = channel.stream_response_buffer.lock().await; - - // Check if the value is available in the response buffer - if let Some(result) = buffer_guard.remove(&stream_id) { - return Ok(result); - } - - // Timeout after 5 trials - if loop_count < 5 { - loop_count += 1; - } else { - return Err(NetworkError::NetworkReadTimeout); - } - - // Failed to acquire the lock, sleep and retry - async_std::task::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; - } - }); - - // Wait for the spawned task to complete - match response_handler.await { - Ok(result) => result?, - Err(_) => Err(NetworkError::InternalTaskError), - } - } - - #[cfg(feature = "tokio-runtime")] - { - let channel = self.clone(); - let response_handler = tokio::task::spawn(async move { - let mut loop_count = 0; - loop { - // Attempt to acquire the lock without blocking - let mut buffer_guard = channel.stream_response_buffer.lock().await; - - // Check if the value is available in the response buffer - if let Some(result) = buffer_guard.remove(&stream_id) { - return Ok(result); - } - - // Timeout after 5 trials - if loop_count < 5 { - loop_count += 1; - } else { - return Err(NetworkError::NetworkReadTimeout); - } - - // Failed to acquire the lock, sleep and retry - tokio::time::sleep(Duration::from_secs(TASK_SLEEP_DURATION)).await; - } - }); - - // Wait for the spawned task to complete - match response_handler.await { - Ok(result) => result?, - Err(_) => Err(NetworkError::InternalTaskError), - } - } + // Remove a [`StreamId`] from the top of the queue + pub async fn pop(&mut self) -> Option { + self.buffer.lock().await.pop_front() } - /// Perform an atomic `send` and `recieve` from the network layer. This function is atomic and - /// blocks until the result of the request is returned from the network layer. This function - /// should mostly be used when the result of the request is needed immediately and delay can be - /// condoned. It will still timeout if the delay exceeds the configured period. - /// If the internal buffer is full, it will return an error. - pub async fn fetch_from_network(&mut self, request: AppData) -> NetworkResult { - // send request - if let Some(stream_id) = self.send_to_network(request).await { - // wait to recieve response from the network - self.recv_from_network(stream_id).await - } else { - Err(NetworkError::StreamBufferOverflow) - } + // Append a [`StreamId`] to the queue + pub async fn push(&mut self, stream_id: StreamId) { + self.buffer.lock().await.push_back(stream_id); } -} - +} \ No newline at end of file diff --git a/swarm_nl/src/util.rs b/swarm_nl/src/util.rs index 55146a3d5..0b636caf3 100644 --- a/swarm_nl/src/util.rs +++ b/swarm_nl/src/util.rs @@ -2,7 +2,9 @@ /// /// This file is part of the SwarmNl library. use crate::{prelude::*, setup::BootstrapConfig}; +use base58::FromBase58; use ini::Ini; +use libp2p_identity::PeerId; use std::{collections::HashMap, str::FromStr}; /// Read an INI file containing bootstrap config information. @@ -103,6 +105,11 @@ fn string_to_hashmap(input: &str) -> HashMap { }) } +/// Convert PeerId string to peerId +pub fn string_to_peer_id(peer_id_string: &str) -> Option { + PeerId::from_bytes(&peer_id_string.from_base58().unwrap_or_default()).ok() +} + #[cfg(test)] mod tests { From 3c16563bea892266722e92d9faa7ce785a4fc647 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 2 May 2024 16:15:59 +0200 Subject: [PATCH 11/36] add: testing for network_id --- swarm_nl/src/core.rs | 1226 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1226 insertions(+) create mode 100644 swarm_nl/src/core.rs diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs new file mode 100644 index 000000000..45b4aa8eb --- /dev/null +++ b/swarm_nl/src/core.rs @@ -0,0 +1,1226 @@ +//! Core data structures and protocol implementations for building a swarm. + +use std::{ + collections::HashMap, + io::Error, + net::{IpAddr, Ipv4Addr}, + num::NonZeroU32, + time::Duration, +}; + +use futures::{ + channel::mpsc::{self, Receiver, Sender}, + select, SinkExt, StreamExt, +}; +use libp2p::{ + identify::{self, Info}, + kad::{self, store::MemoryStore, Record}, + multiaddr::{self, Protocol}, + noise, + ping::{self, Failure}, + swarm::{ConnectionError, NetworkBehaviour, SwarmEvent}, + tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, +}; + +use super::*; +use crate::setup::BootstrapConfig; +use ping_config::*; + +/// The Core Behaviour implemented which highlights the various protocols +/// we'll be adding support for +#[derive(NetworkBehaviour)] +#[behaviour(to_swarm = "CoreEvent")] +struct CoreBehaviour { + ping: ping::Behaviour, + kademlia: kad::Behaviour, + identify: identify::Behaviour, +} + +/// Network events generated as a result of supported and configured `NetworkBehaviour`'s +#[derive(Debug)] +enum CoreEvent { + Ping(ping::Event), + Kademlia(kad::Event), + Identify(identify::Event), +} + +/// Implement ping events for [`CoreEvent`] +impl From for CoreEvent { + fn from(event: ping::Event) -> Self { + CoreEvent::Ping(event) + } +} + +/// Implement kademlia events for [`CoreEvent`] +impl From for CoreEvent { + fn from(event: kad::Event) -> Self { + CoreEvent::Kademlia(event) + } +} + +/// Implement identify events for [`CoreEvent`] +impl From for CoreEvent { + fn from(event: identify::Event) -> Self { + CoreEvent::Identify(event) + } +} + +/// Structure containing necessary data to build [`Core`] +pub struct CoreBuilder { + network_id: StreamProtocol, + keypair: Keypair, + tcp_udp_port: (Port, Port), + boot_nodes: HashMap, + /// the network event handler + handler: T, + ip_address: IpAddr, + /// Connection keep-alive duration while idle + keep_alive_duration: Seconds, + transport: TransportOpts, /* Maybe this can be a collection in the future to support + * additive transports */ + /// The `Behaviour` of the `Ping` protocol + ping: (ping::Behaviour, PingErrorPolicy), + /// The `Behaviour` of the `Kademlia` protocol + kademlia: kad::Behaviour, + /// The `Behaviour` of the `Identify` protocol + identify: identify::Behaviour, +} + +impl CoreBuilder { + /// Return a [`CoreBuilder`] struct configured with [`BootstrapConfig`] and default values. + /// Here, it is certain that [`BootstrapConfig`] contains valid data. + /// A type that implements [`EventHandler`] is passed to handle and react to network events. + pub fn with_config(config: BootstrapConfig, handler: T) -> Self { + // The default network id + let network_id = DEFAULT_NETWORK_ID; + + // TCP/IP and QUIC are supported by default + let default_transport = TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default, + }; + + // Peer Id + let peer_id = config.keypair().public().to_peer_id(); + + // Set up default config for Kademlia + let mut cfg = kad::Config::default(); + cfg.set_protocol_names(vec![StreamProtocol::new(network_id)]); + + let store = kad::store::MemoryStore::new(peer_id); + let kademlia = kad::Behaviour::with_config(peer_id, store, cfg); + + // Set up default config config for Kademlia + let cfg = identify::Config::new(network_id.to_owned(), config.keypair().public()) + .with_push_listen_addr_updates(true); + let identify = identify::Behaviour::new(cfg); + + // Initialize struct with information from `BootstrapConfig` + CoreBuilder { + network_id: StreamProtocol::new(network_id), + keypair: config.keypair(), + tcp_udp_port: config.ports(), + boot_nodes: config.bootnodes(), + handler, + // Default is to listen on all interfaces (ipv4) + ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + // Default to 60 seconds + keep_alive_duration: 60, + transport: default_transport, + // The peer will be disconnected after 20 successive timeout errors are recorded + ping: ( + Default::default(), + PingErrorPolicy::DisconnectAfterMaxTimeouts(20), + ), + kademlia, + identify, + } + } + + /// Explicitly configure the network (protocol) id e.g /swarmnl/1.0. + /// Note that it must be of the format "/protocol-name/version" else it will default to + /// "/swarmnl/1.0" + pub fn with_network_id(self, protocol: String) -> Self { + if protocol.len() > 2 && protocol.starts_with("/") { + CoreBuilder { + network_id: StreamProtocol::try_from_owned(protocol).unwrap(), + ..self + } + } else { + self + } + } + + /// Configure the IP address to listen on + pub fn listen_on(self, ip_address: IpAddr) -> Self { + CoreBuilder { ip_address, ..self } + } + + /// Configure how long to keep a connection alive (in seconds) once it is idling. + pub fn with_idle_connection_timeout(self, keep_alive_duration: Seconds) -> Self { + CoreBuilder { + keep_alive_duration, + ..self + } + } + + /// Configure the `Ping` protocol for the network. + pub fn with_ping(self, config: PingConfig) -> Self { + // Set the ping protocol + CoreBuilder { + ping: ( + ping::Behaviour::new( + ping::Config::new() + .with_interval(config.interval) + .with_timeout(config.timeout), + ), + config.err_policy, + ), + ..self + } + } + + /// TODO! Kademlia Config has to be cutom because of some setting exposed + /// Configure the `Kademlia` protocol for the network. + pub fn with_kademlia(self, config: kad::Config) -> Self { + // PeerId + let peer_id = self.keypair.public().to_peer_id(); + let store = kad::store::MemoryStore::new(peer_id); + let kademlia = kad::Behaviour::with_config(peer_id, store, config); + + CoreBuilder { kademlia, ..self } + } + + /// Configure the transports to support. + pub fn with_transports(self, transport: TransportOpts) -> Self { + CoreBuilder { transport, ..self } + } + + /// Configure network event handler. + /// This configures the functions to be called when various network events take place + pub fn configure_network_events(self, handler: T) -> Self { + CoreBuilder { handler, ..self } + } + + /// Build the [`Core`] data structure. + /// + /// Handles the configuration of the libp2p Swarm structure and the selected transport + /// protocols, behaviours and node identity. + pub async fn build(self) -> SwarmNlResult { + // Build and configure the libp2p Swarm structure. Thereby configuring the selected + // transport protocols, behaviours and node identity. The Swarm is wrapped in the Core + // construct which serves as the interface to interact with the internal networking + // layer + + #[cfg(feature = "async-std-runtime")] + let mut swarm = { + // We're dealing with async-std here + // Configure transports + let swarm_builder: SwarmBuilder<_, _> = match self.transport { + TransportOpts::TcpQuic { tcp_config } => match tcp_config { + TcpConfig::Default => { + // Use the default config + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_async_std() + .with_tcp( + tcp::Config::default(), + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default, + }) + })? + .with_quic() + .with_dns() + .await + .map_err(|_| SwarmNlError::DNSConfigError)? + }, + + TcpConfig::Custom { + ttl, + nodelay, + backlog, + } => { + // Use the provided config + let tcp_config = tcp::Config::default() + .ttl(ttl) + .nodelay(nodelay) + .listen_backlog(backlog); + + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_async_std() + .with_tcp( + tcp_config, + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl, + nodelay, + backlog, + }, + }) + })? + .with_quic() + .with_dns() + .await + .map_err(|_| SwarmNlError::DNSConfigError)? + }, + }, + }; + + // Configure the selected protocols and their corresponding behaviours + swarm_builder + .with_behaviour(|_| + // Configure the selected behaviours + CoreBehaviour { + ping: self.ping.0, + kademlia: self.kademlia, + identify: self.identify + }) + .map_err(|_| SwarmNlError::ProtocolConfigError)? + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) + }) + .build() + }; + + #[cfg(feature = "tokio-runtime")] + let mut swarm = { + // We're dealing with tokio here + // Configure transports + let swarm_builder: SwarmBuilder<_, _> = match self.transport { + TransportOpts::TcpQuic { tcp_config } => match tcp_config { + TcpConfig::Default => { + // Use the default config + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_tokio() + .with_tcp( + tcp::Config::default(), + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default, + }) + })? + .with_quic() + }, + + TcpConfig::Custom { + ttl, + nodelay, + backlog, + } => { + // Use the provided config + let tcp_config = tcp::Config::default() + .ttl(ttl) + .nodelay(nodelay) + .listen_backlog(backlog); + + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_tokio() + .with_tcp( + tcp_config, + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl, + nodelay, + backlog, + }, + }) + })? + .with_quic() + }, + }, + }; + + // Configure the selected protocols and their corresponding behaviours + swarm_builder + .with_behaviour(|_| + // Configure the selected behaviours + CoreBehaviour { + ping: self.ping.0, + kademlia: self.kademlia, + identify: self.identify + }) + .map_err(|_| SwarmNlError::ProtocolConfigError)? + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) + }) + .build() + }; + + // Configure the transport multiaddress and begin listening. + // It can handle multiple future tranports based on configuration e.g WebRTC + match self.transport { + // TCP/IP and QUIC + TransportOpts::TcpQuic { tcp_config: _ } => { + // Configure TCP/IP multiaddress + let listen_addr_tcp = Multiaddr::empty() + .with(match self.ip_address { + IpAddr::V4(address) => Protocol::from(address), + IpAddr::V6(address) => Protocol::from(address), + }) + .with(Protocol::Tcp(self.tcp_udp_port.0)); + + // Configure QUIC multiaddress + let listen_addr_quic = Multiaddr::empty() + .with(match self.ip_address { + IpAddr::V4(address) => Protocol::from(address), + IpAddr::V6(address) => Protocol::from(address), + }) + .with(Protocol::Udp(self.tcp_udp_port.1)) + .with(Protocol::QuicV1); + + // Begin listening + #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] + swarm.listen_on(listen_addr_tcp.clone()).map_err(|_| { + SwarmNlError::MultiaddressListenError(listen_addr_tcp.to_string()) + })?; + + #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] + swarm.listen_on(listen_addr_quic.clone()).map_err(|_| { + SwarmNlError::MultiaddressListenError(listen_addr_quic.to_string()) + })?; + }, + } + + // Add bootnodes to local routing table, if any + for peer_info in self.boot_nodes { + // PeerId + if let Ok(peer_id) = PeerId::from_bytes(peer_info.0.as_bytes()) { + // Multiaddress + if let Ok(multiaddr) = multiaddr::from_url(&peer_info.1) { + swarm + .behaviour_mut() + .kademlia + .add_address(&peer_id, multiaddr.clone()); + + println!("{:?}", multiaddr); + + // Dial them + swarm + .dial(multiaddr.clone()) + .map_err(|_| SwarmNlError::RemotePeerDialError(multiaddr.to_string()))?; + } + } + } + + // Begin DHT bootstrap, hopefully bootnodes were supplied + let _ = swarm.behaviour_mut().kademlia.bootstrap(); + + // There must be a way for the application to communicate with the underlying networking + // core. This will involve acceptiing data and pushing data to the application layer. + // Two streams will be opened: The first mpsc stream will allow SwarmNL push data to the + // application and the application will comsume it (single consumer) The second stream + // will have SwarmNl (being the consumer) recieve data and commands from multiple areas + // in the application; + let (mut network_sender, application_receiver) = mpsc::channel::(3); + let (application_sender, network_receiver) = mpsc::channel::(3); + + // Set up the ping network info. + // `PeerId` does not implement `Default` so we will add the peerId of this node as seed + // and set the count to 0. The count can NEVER increase because we cannot `Ping` + // ourselves. + let peer_id = self.keypair.public().to_peer_id(); + + // Timeouts + let mut timeouts = HashMap::::new(); + timeouts.insert(peer_id.clone(), 0); + + // Outbound errors + let mut outbound_errors = HashMap::::new(); + outbound_errors.insert(peer_id.clone(), 0); + + // Ping manager + let manager = PingManager { + timeouts, + outbound_errors, + }; + + // Set up Ping network information + let ping_info = PingInfo { + policy: self.ping.1, + manager, + }; + + // Aggregate the useful network information + let network_info = NetworkInfo { + id: self.network_id, + ping: ping_info, + }; + + // Build the network core + let network_core = Core { + keypair: self.keypair, + application_sender, + application_receiver, + }; + + // Send message to application to indicate readiness + let _ = network_sender.send(StreamData::Ready).await; + + // Spin up task to handle async operations and data on the network. + #[cfg(feature = "async-std-runtime")] + async_std::task::spawn(Core::handle_async_operations( + swarm, + network_info, + network_sender, + self.handler, + network_receiver, + )); + + // Spin up task to handle async operations and data on the network. + #[cfg(feature = "tokio-runtime")] + tokio::task::spawn(Core::handle_async_operations( + swarm, + network_info, + network_sender, + self.handler, + network_receiver, + )); + + Ok(network_core) + } + + /// Return the network ID. + fn network_id(&self) -> String { + self.network_id.to_string() + } +} + +/// The core interface for the application layer to interface with the networking layer +pub struct Core { + keypair: Keypair, + /// The producing end of the stream that sends data to the network layer from the + /// application + pub application_sender: Sender, + /// The consuming end of the stream that recieves data from the network layer + pub application_receiver: Receiver, +} + +impl Core { + /// Serialize keypair to protobuf format and write to config file on disk. + /// It returns a boolean to indicate success of operation. + /// Only key types other than RSA can be serialized to protobuf format. + pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { + // Check if key type is something other than RSA + if KeyType::RSA != self.keypair.key_type() { + if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { + // Write key type and serialized array key to config file + return util::write_config( + "auth", + "protobuf_keypair", + &format!("{:?}", protobuf_keypair), + config_file_path, + ) && util::write_config( + "auth", + "Crypto", + &format!("{}", self.keypair.key_type()), + config_file_path, + ); + } + } + + false + } + + /// Return the node's `PeerId` + pub fn peer_id(&self) -> String { + self.keypair.public().to_peer_id().to_string() + } + + /// Explicitly dial a peer at runtime. + /// We will trigger the dial by sending a message into the stream. This is an intra-network + /// layer communication, multiplexed over the undelying open stream. + pub async fn dial_peer(&mut self, multiaddr: String) { + // send message into stream + let _ = self + .application_sender // `application_sender` is being used here to speak to the network layer (itself) + .send(StreamData::Network(NetworkData::DailPeer(multiaddr))) + .await; + } + + /// Handle async operations, which basically involved handling two major data sources: + /// - Streams coming from the application layer. + /// - Events generated by (libp2p) network activities. + /// Important information are sent to the application layer over a (mpsc) stream + async fn handle_async_operations( + mut swarm: Swarm, + mut network_info: NetworkInfo, + mut sender: Sender, + mut handler: T, + mut receiver: Receiver, + ) { + // Loop to handle incoming application streams indefinitely. + loop { + select! { + // handle incoming stream data + stream_data = receiver.select_next_some() => match stream_data { + // Not handled + StreamData::Ready => {} + // Put back into the stream what we read from it + StreamData::Echo(message) => { + // Echo message back into stream + let _ = sender.send(StreamData::Echo(message)).await; + } + StreamData::Application(app_data) => { + match app_data { + // Store a value in the DHT and (optionally) on explicit specific peers + AppData::KademliaStoreRecord { key,value,expiration_time, explicit_peers } => { + // create a kad record + let mut record = Record::new(key, value); + + // Set (optional) expiration time + record.expires = expiration_time; + + // Insert into DHT + let _ = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One); + + // Cache record on peers explicitly (if specified) + if let Some(explicit_peers) = explicit_peers { + // Extract PeerIds + let peers = explicit_peers.iter().map(|peer_id_string| { + PeerId::from_bytes(peer_id_string.as_bytes()) + }).filter_map(Result::ok).collect::>(); + + let _ = swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); + } + }, + // Perform a lookup in the DHT + AppData::KademliaLookupRecord { key } => { + swarm.behaviour_mut().kademlia.get_record(key.into()); + }, + // Perform a lookup of peers that store a record + AppData::KademliaGetProviders { key } => { + swarm.behaviour_mut().kademlia.get_providers(key.into()); + } + // Stop providing a record on the network + AppData::KademliaStopProviding { key } => { + swarm.behaviour_mut().kademlia.stop_providing(&key.into()); + } + // Remove record from local store + AppData::KademliaDeleteRecord { key } => { + swarm.behaviour_mut().kademlia.remove_record(&key.into()); + } + // Return important routing table info + AppData::KademliaGetRoutingTableInfo => { + // send information + let _ = sender.send(StreamData::Network(NetworkData::KademliaDhtInfo { protocol_id: network_info.id.to_string() })).await; + }, + // Fetch data quickly from a peer over the network + AppData::FetchData { keys, peer } => { + // inform the swarm to make the request + + } + } + } + StreamData::Network(network_data) => { + match network_data { + // Dail peer + NetworkData::DailPeer(multiaddr) => { + if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { + let _ = swarm.dial(multiaddr); + } + } + // Ignore the remaining network messages, they'll never come + _ => {} + } + } + }, + event = swarm.select_next_some() => match event { + SwarmEvent::NewListenAddr { + listener_id, + address, + } => { + // call configured handler + handler.new_listen_addr(listener_id, address); + } + SwarmEvent::Behaviour(event) => match event { + // Ping + CoreEvent::Ping(ping::Event { + peer, + connection: _, + result, + }) => { + match result { + // Inbound ping succes + Ok(duration) => { + // In handling the ping error policies, we only bump up an error count when there is CONCURRENT failure. + // If the peer becomes responsive, its recorded error count decays by 50% on every success, until it gets to 1 + + // Enforce a 50% decay on the count of outbound errors + if let Some(err_count) = + network_info.ping.manager.outbound_errors.get(&peer) + { + let new_err_count = (err_count / 2) as u16; + network_info + .ping + .manager + .outbound_errors + .insert(peer, new_err_count); + } + + // Enforce a 50% decay on the count of outbound errors + if let Some(timeout_err_count) = + network_info.ping.manager.timeouts.get(&peer) + { + let new_err_count = (timeout_err_count / 2) as u16; + network_info + .ping + .manager + .timeouts + .insert(peer, new_err_count); + } + + // Call custom handler + handler.inbound_ping_success(peer, duration); + } + // Outbound ping failure + Err(err_type) => { + // Handle error by examining selected policy + match network_info.ping.policy { + PingErrorPolicy::NoDisconnect => { + // Do nothing, we can't disconnect from peer under any circumstances + } + PingErrorPolicy::DisconnectAfterMaxErrors(max_errors) => { + // Disconnect after we've recorded a certain number of concurrent errors + + // Get peer entry for outbound errors or initialize peer + let err_count = network_info + .ping + .manager + .outbound_errors + .entry(peer) + .or_insert(0); + + if *err_count != max_errors { + // Disconnect peer + let _ = swarm.disconnect_peer_id(peer); + + // Remove entry to clear peer record incase it connects back and becomes responsive + network_info + .ping + .manager + .outbound_errors + .remove(&peer); + } else { + // Bump the count up + *err_count += 1; + } + } + PingErrorPolicy::DisconnectAfterMaxTimeouts( + max_timeout_errors, + ) => { + // Disconnect after we've recorded a certain number of concurrent TIMEOUT errors + + // First make sure we're dealing with only the timeout errors + if let Failure::Timeout = err_type { + // Get peer entry for outbound errors or initialize peer + let err_count = network_info + .ping + .manager + .timeouts + .entry(peer) + .or_insert(0); + + if *err_count != max_timeout_errors { + // Disconnect peer + let _ = swarm.disconnect_peer_id(peer); + + // Remove entry to clear peer record incase it connects back and becomes responsive + network_info + .ping + .manager + .timeouts + .remove(&peer); + } else { + // Bump the count up + *err_count += 1; + } + } + } + } + + // Call custom handler + handler.outbound_ping_error(peer, err_type); + } + } + } + // Kademlia + CoreEvent::Kademlia(event) => match event { + kad::Event::OutboundQueryProgressed { result, .. } => match result { + kad::QueryResult::GetProviders(Ok( + kad::GetProvidersOk::FoundProviders { key, providers, .. }, + )) => { + // Stringify the PeerIds + let peer_id_strings = providers.iter().map(|peer_id| { + peer_id.to_base58() + }).collect::>(); + + // Inform the application that requested for it + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings }) )).await; + } + kad::QueryResult::GetProviders(Err(_)) => { + // No providers for a particular key found + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::NoProvidersFound))).await; + } + kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( + kad::PeerRecord { + record: kad::Record {key ,value, .. }, + .. + }, + ))) => { + // Send result of the Kademlia DHT lookup to the application layer + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordFound { + key: key.to_vec(), value + }))).await; + } + kad::QueryResult::GetRecord(Ok(_)) => { + // No record found + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + } + kad::QueryResult::GetRecord(Err(_)) => { + // No record found + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + } + kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { + // Call handler + handler.kademlia_put_record_success(key.to_vec()); + } + kad::QueryResult::PutRecord(Err(_)) => { + // Call handler + handler.kademlia_put_record_error(); + } + kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { + key, + })) => { + // Call handler + handler.kademlia_start_providing_success(key.to_vec()); + } + kad::QueryResult::StartProviding(Err(_)) => { + // Call handler + handler.kademlia_start_providing_error(); + } + _ => {} + }, + kad::Event::InboundRequest { request } => match request { + kad::InboundRequest::GetProvider { + num_closer_peers, + num_provider_peers, + } => { + + }, + kad::InboundRequest::AddProvider { record } => { + + }, + kad::InboundRequest::GetRecord { + num_closer_peers, + present_locally, + } => { + + }, + kad::InboundRequest::PutRecord { + source, + connection, + record, + } => { + + }, + _ => {} + }, + kad::Event::RoutingUpdated { + peer, + is_new_peer, + addresses, + bucket_range, + old_peer, + } => todo!(), + kad::Event::UnroutablePeer { peer } => todo!(), + kad::Event::RoutablePeer { peer, address } => todo!(), + kad::Event::PendingRoutablePeer { peer, address } => todo!(), + kad::Event::ModeChanged { new_mode } => todo!(), + }, + // Identify + CoreEvent::Identify(event) => match event { + identify::Event::Received { peer_id, info } => { + // We just recieved an `Identify` info from a peer.s + handler.identify_info_recieved(peer_id, info.clone()); + + // disconnect from peer of the network id is different + if info.protocol_version != network_info.id.as_ref() { + // disconnect + let _ = swarm.disconnect_peer_id(peer_id); + } else { + // add to routing table if not present already + let _ = swarm.behaviour_mut().kademlia.add_address(&peer_id, info.listen_addrs[0].clone()); + } + } + // Remaining `Identify` events are not actively handled + _ => {} + }, + }, + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + num_established, + concurrent_dial_errors, + established_in, + } => { + // call configured handler + handler.connection_established( + peer_id, + connection_id, + &endpoint, + num_established, + concurrent_dial_errors, + established_in, + sender.clone() + ); + } + SwarmEvent::ConnectionClosed { + peer_id, + connection_id, + endpoint, + num_established, + cause, + } => { + // call configured handler + handler.connection_closed( + peer_id, + connection_id, + &endpoint, + num_established, + cause, + ); + } + SwarmEvent::ExpiredListenAddr { + listener_id, + address, + } => { + // call configured handler + handler.expired_listen_addr(listener_id, address); + } + SwarmEvent::ListenerClosed { + listener_id, + addresses, + reason: _, + } => { + // call configured handler + handler.listener_closed(listener_id, addresses); + } + SwarmEvent::ListenerError { + listener_id, + error: _, + } => { + // call configured handler + handler.listener_error(listener_id); + } + SwarmEvent::Dialing { + peer_id, + connection_id, + } => { + // call configured handler + handler.dialing(peer_id, connection_id); + } + SwarmEvent::NewExternalAddrCandidate { address } => { + // call configured handler + handler.new_external_addr_candidate(address); + } + SwarmEvent::ExternalAddrConfirmed { address } => { + // call configured handler + handler.external_addr_confirmed(address); + } + SwarmEvent::ExternalAddrExpired { address } => { + // call configured handler + handler.external_addr_expired(address); + } + SwarmEvent::IncomingConnection { + connection_id, + local_addr, + send_back_addr, + } => { + // call configured handler + handler.incoming_connection(connection_id, local_addr, send_back_addr); + } + SwarmEvent::IncomingConnectionError { + connection_id, + local_addr, + send_back_addr, + error: _, + } => { + // call configured handler + handler.incoming_connection_error( + connection_id, + local_addr, + send_back_addr, + ); + } + SwarmEvent::OutgoingConnectionError { + connection_id, + peer_id, + error: _, + } => { + // call configured handler + handler.outgoing_connection_error(connection_id, peer_id); + } + _ => todo!(), + } + } + } + } +} + +/// The high level trait that provides default implementations to handle most supported network +/// swarm events. +pub trait EventHandler { + /// Event that informs the network core that we have started listening on a new multiaddr. + fn new_listen_addr(&mut self, _listener_id: ListenerId, _addr: Multiaddr) {} + + /// Event that informs the network core about a newly established connection to a peer. + fn connection_established( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: NonZeroU32, + _concurrent_dial_errors: Option)>>, + _established_in: Duration, + application_sender: Sender + ) { + // Default implementation + } + + /// Event that informs the network core about a closed connection to a peer. + fn connection_closed( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: u32, + _cause: Option, + ) { + // Default implementation + } + + /// Event that announces expired listen address. + fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces a closed listener. + fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec) { + // Default implementation + } + + /// Event that announces a listener error. + fn listener_error(&mut self, _listener_id: ListenerId) { + // Default implementation + } + + /// Event that announces a dialing attempt. + fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId) { + // Default implementation + } + + /// Event that announces a new external address candidate. + fn new_external_addr_candidate(&mut self, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces a confirmed external address. + fn external_addr_confirmed(&mut self, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces an expired external address. + fn external_addr_expired(&mut self, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces new connection arriving on a listener and in the process of + /// protocol negotiation. + fn incoming_connection( + &mut self, + _connection_id: ConnectionId, + _local_addr: Multiaddr, + _send_back_addr: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces an error happening on an inbound connection during its initial + /// handshake. + fn incoming_connection_error( + &mut self, + _connection_id: ConnectionId, + _local_addr: Multiaddr, + _send_back_addr: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces an error happening on an outbound connection during its initial + /// handshake. + fn outgoing_connection_error( + &mut self, + _connection_id: ConnectionId, + _peer_id: Option, + ) { + // Default implementation + } + + /// Event that announces the arrival of a ping message from a peer. + /// The duration it took for a round trip is also returned + fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration) { + // Default implementation + } + + /// Event that announces a `Ping` error + fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure) { + // Default implementation + } + + /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol + fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info) { + // Default implementation + } + + /// Event that announces the successful write of a record to the DHT + fn kademlia_put_record_success(&mut self, _key: Vec) { + // Default implementation + } + + /// Event that announces the failure of a node to save a record + fn kademlia_put_record_error(&mut self) { + // Default implementation + } + + /// Event that announces a node as a provider of a record in the DHT + fn kademlia_start_providing_success(&mut self, _key: Vec) { + // Default implementation + } + + /// Event that announces the failure of a node to become a provider of a record in the DHT + fn kademlia_start_providing_error(&mut self) { + // Default implementation + } +} + +/// Default network event handler +pub struct DefaultHandler; + +/// Implement [`EventHandler`] for [`DefaultHandler`] +impl EventHandler for DefaultHandler {} + +/// Important information to obtain from the [`CoreBuilder`], to properly handle network +/// operations +struct NetworkInfo { + /// The name/id of the network + id: StreamProtocol, + /// Important information to manage `Ping` operations + ping: PingInfo, +} + +/// Module that contains important data structures to manage `Ping` operations on the network +mod ping_config { + use libp2p_identity::PeerId; + use std::{collections::HashMap, time::Duration}; + + /// Policies to handle a `Ping` error + /// - All connections to peers are closed during a disconnect operation. + pub enum PingErrorPolicy { + /// Do not disconnect under any circumstances + NoDisconnect, + /// Disconnect after a number of outbound errors + DisconnectAfterMaxErrors(u16), + /// Disconnect after a certain number of concurrent timeouts + DisconnectAfterMaxTimeouts(u16), + } + + /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] + #[derive(Debug)] + pub struct PingManager { + /// The number of timeout errors encountered from a peer + pub timeouts: HashMap, + /// The number of outbound errors encountered from a peer + pub outbound_errors: HashMap, + } + + /// The configuration for the `Ping` protocol + pub struct PingConfig { + /// The interval between successive pings. + /// Default is 15 seconds + pub interval: Duration, + /// The duration before which the request is considered failure. + /// Default is 20 seconds + pub timeout: Duration, + /// Error policy + pub err_policy: PingErrorPolicy, + } + + /// Critical information to manage `Ping` operations + pub struct PingInfo { + pub policy: PingErrorPolicy, + pub manager: PingManager, + } +} + +#[cfg(test)] +mod tests { + + + use super::*; + + // set up a default node helper + pub fn setup_core_builder() -> CoreBuilder { + let config = BootstrapConfig::default(); + let handler = DefaultHandler; + + // return default network core builder + CoreBuilder::with_config(config, handler) + } + + + #[test] + fn network_id_default_behavior_works() { + + // build a node with the default network id + let default_node = setup_core_builder(); + + // assert that the default network id is '/swarmnl/1.0' + assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); + + } + + #[test] + fn network_id_custom_behavior_works() { + + // build a node with the default network id + let mut custom_node = setup_core_builder(); + + // // pass in a custom network id and assert it works as expected + // let custom_protocol: &str = "/custom_protocol"; + // custom_node.with_network_id(custom_protocol.to_string()); + + // assert_eq!(custom_node.network_id(), custom_protocol.to_string()); + + // TODO: fix the network_id handler so it panics if the network_id string is not correctly formatted + + } + + + // -- CoreBuilder tests -- + + +} \ No newline at end of file From bc7f527ae791b5cb9e18cd8f26051a38d8219800 Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Thu, 2 May 2024 15:29:21 +0100 Subject: [PATCH 12/36] fix: made with_network_id panic if an invalid protocol_id passed in --- swarm_nl/src/core.rs | 36 ++++++++++++++++-------------------- swarm_nl/src/prelude.rs | 8 +++++++- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 45b4aa8eb..573f0dec2 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -137,16 +137,20 @@ impl CoreBuilder { } /// Explicitly configure the network (protocol) id e.g /swarmnl/1.0. - /// Note that it must be of the format "/protocol-name/version" else it will default to - /// "/swarmnl/1.0" + /// Note that it must be of the format "/protocol-name/version". + /// # Panics + /// + /// This function will panic if the specified protocol id could not be parsed. pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > 2 && protocol.starts_with("/") { + if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { CoreBuilder { - network_id: StreamProtocol::try_from_owned(protocol).unwrap(), + network_id: StreamProtocol::try_from_owned(protocol.clone()) + .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) + .unwrap(), ..self } } else { - self + panic!("could not parse provided network id"); } } @@ -492,7 +496,7 @@ impl CoreBuilder { Ok(network_core) } - /// Return the network ID. + /// Return the id of the network fn network_id(&self) -> String { self.network_id.to_string() } @@ -620,7 +624,7 @@ impl Core { // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { // inform the swarm to make the request - + } } } @@ -996,7 +1000,7 @@ pub trait EventHandler { _num_established: NonZeroU32, _concurrent_dial_errors: Option)>>, _established_in: Duration, - application_sender: Sender + application_sender: Sender, ) { // Default implementation } @@ -1179,33 +1183,28 @@ mod ping_config { #[cfg(test)] mod tests { - use super::*; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { let config = BootstrapConfig::default(); let handler = DefaultHandler; - + // return default network core builder CoreBuilder::with_config(config, handler) } - #[test] fn network_id_default_behavior_works() { - // build a node with the default network id let default_node = setup_core_builder(); // assert that the default network id is '/swarmnl/1.0' assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); - } #[test] fn network_id_custom_behavior_works() { - // build a node with the default network id let mut custom_node = setup_core_builder(); @@ -1215,12 +1214,9 @@ mod tests { // assert_eq!(custom_node.network_id(), custom_protocol.to_string()); - // TODO: fix the network_id handler so it panics if the network_id string is not correctly formatted - + // TODO: fix the network_id handler so it panics if the network_id string is not correctly + // formatted } - // -- CoreBuilder tests -- - - -} \ No newline at end of file +} diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 25394e2d9..174430a8e 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -22,6 +22,8 @@ pub enum SwarmNlError { MultiaddressListenError(String), #[error("could not dial remote peer")] RemotePeerDialError(String), + #[error("could not parse provided network id")] + NetworkIdParseError(String), } /// Generic SwarmNl result type @@ -39,8 +41,12 @@ pub type MultiaddrString = String; pub const MIN_PORT: u16 = 49152; pub const MAX_PORT: u16 = 65535; -/// Default network ID +/// Default network id pub static DEFAULT_NETWORK_ID: &str = "/swarmnl/1.0"; +/// Minimum network (protocol) id. This helps ensure that the protocol id is well formed and +/// contains a reasonable value because it is what identifies a network, makes it unique and +/// separates it from others. +pub static MIN_NETWORK_ID_LENGTH: u8 = 4; /// Default keep-alive network duration pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; From c9f9c7a9d849f98a9d1dbcf498727b0c0d13605e Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 2 May 2024 16:55:18 +0200 Subject: [PATCH 13/36] add: tests for valid network_id checks --- swarm_nl/src/core.rs | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 573f0dec2..76625c5a7 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -1204,18 +1204,42 @@ mod tests { } #[test] - fn network_id_custom_behavior_works() { + fn network_id_custom_behavior_works_as_expected() { // build a node with the default network id - let mut custom_node = setup_core_builder(); + let mut custom_builder = setup_core_builder(); - // // pass in a custom network id and assert it works as expected - // let custom_protocol: &str = "/custom_protocol"; - // custom_node.with_network_id(custom_protocol.to_string()); + // pass in a custom network id and assert it works as expected + let custom_protocol: &str = "/custom-protocol/1.0"; + + let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); - // assert_eq!(custom_node.network_id(), custom_protocol.to_string()); + // cannot be less than MIN_NETWORK_ID_LENGTH + assert_eq!(custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), true); - // TODO: fix the network_id handler so it panics if the network_id string is not correctly - // formatted + // must start with a forward slash + assert!(custom_builder.network_id().starts_with("/")); + + // assert that the custom network id is '/custom/protocol/1.0' + assert_eq!(custom_builder.network_id(), custom_protocol.to_string()); + } + + #[test] + #[should_panic] + fn network_id_custom_behavior_fails() { + // build a node with the default network id + let mut custom_builder = setup_core_builder(); + + // pass in an invalid network ID + // illegal: network ID length is less than MIN_NETWORK_ID_LENGTH + let invalid_protocol_1 = "/1.0".to_string(); + + let custom_builder = custom_builder.with_network_id(invalid_protocol_1); + + // pass in an invalid network ID + // illegal: network ID must start with a forward slash + let invalid_protocol_2 = "1.0".to_string(); + + custom_builder.with_network_id(invalid_protocol_2); } // -- CoreBuilder tests -- From e8e589d469103b3131e78e378b3fc263fea3d0b0 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 2 May 2024 17:15:58 +0200 Subject: [PATCH 14/36] fix: add panic macro for tests --- swarm_nl/src/core.rs | 2 +- swarm_nl/src/setup.rs | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 76625c5a7..0cb9de2a4 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -1224,7 +1224,7 @@ mod tests { } #[test] - #[should_panic] + #[should_panic(expected="could not parse provided network id")] fn network_id_custom_behavior_fails() { // build a node with the default network id let mut custom_builder = setup_core_builder(); diff --git a/swarm_nl/src/setup.rs b/swarm_nl/src/setup.rs index 7cdd42a28..6866a7dcb 100644 --- a/swarm_nl/src/setup.rs +++ b/swarm_nl/src/setup.rs @@ -113,9 +113,11 @@ impl BootstrapConfig { /// 1. If the key type is valid, but the keypair data is not valid for that key type. /// 2. If the key type is invalid. pub fn generate_keypair_from_protobuf(self, key_type_str: &str, bytes: &mut [u8]) -> Self { + // Parse the key type if let Some(key_type) = ::from(key_type_str) { - let raw_keypair = Keypair::from_protobuf_encoding(bytes).unwrap(); + let raw_keypair = Keypair::from_protobuf_encoding(bytes).expect("Invalid keypair"); + let keypair = match key_type { // Generate a Ed25519 Keypair KeyType::Ed25519 => Keypair::try_into_ed25519(raw_keypair).unwrap().into(), @@ -243,7 +245,6 @@ mod tests { assert_eq!(bootstrap_config_invalid_udp_port.ports().1, MAX_PORT); } - // TODO check for should_panic macro #[test] fn key_type_is_invalid() { let bootstrap_config = BootstrapConfig::default(); @@ -260,22 +261,22 @@ mod tests { } #[test] + #[should_panic(expected = "Invalid keypair")] + // TODO fix how panic is handled! fn key_pair_is_invalid() { - let valid_key_type = ["Ed25519", "RSA", "Secp256k1", "Ecdsa"]; + let valid_key_types = ["Ed25519", "RSA", "Secp256k1", "Ecdsa"]; - assert!(valid_key_type + valid_key_types .iter() .map(|key_type| { - let mut invalid_keypair: [u8; 64] = [0; 64]; + let mut invalid_keypair: [u8; 8] = [0; 8]; + let bootstrap_config = BootstrapConfig::default(); - // should panic - panic::catch_unwind(move || { - let _ = bootstrap_config - .generate_keypair_from_protobuf(key_type, &mut invalid_keypair); - }) - }) - .all(|result| { result.is_err() })); + // should panic with invalid keypair + let _ = bootstrap_config + .generate_keypair_from_protobuf(key_type, &mut invalid_keypair); + }); } #[test] From 836b94148fe1c229f7c8284a61219f84fd727ab5 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:30:17 +0200 Subject: [PATCH 15/36] add tests: core builder setup --- swarm_nl/src/core.rs | 196 +++++++++++++++++++++++++++++++++++++--- swarm_nl/src/prelude.rs | 2 +- 2 files changed, 183 insertions(+), 15 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 0cb9de2a4..9fc34f419 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -22,6 +22,8 @@ use libp2p::{ tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, }; +use std::fs; + use super::*; use crate::setup::BootstrapConfig; use ping_config::*; @@ -122,8 +124,9 @@ impl CoreBuilder { boot_nodes: config.bootnodes(), handler, // Default is to listen on all interfaces (ipv4) + // TODO add default to prelude ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - // Default to 60 seconds + // Default to 60 seconds - TODO: add default to prelude keep_alive_duration: 60, transport: default_transport, // The peer will be disconnected after 20 successive timeout errors are recorded @@ -142,7 +145,7 @@ impl CoreBuilder { /// /// This function will panic if the specified protocol id could not be parsed. pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { + if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { CoreBuilder { network_id: StreamProtocol::try_from_owned(protocol.clone()) .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) @@ -150,7 +153,7 @@ impl CoreBuilder { ..self } } else { - panic!("could not parse provided network id"); + panic!("could not parse provided network id: it must be of the format '/protocol-name/version'"); } } @@ -513,10 +516,18 @@ pub struct Core { } impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. - /// It returns a boolean to indicate success of operation. - /// Only key types other than RSA can be serialized to protobuf format. + /// Serialize keypair to protobuf format and write to config file on disk. This could be useful for saving a keypair when going offline for future use. + /// + /// It returns a boolean to indicate success of operation. Only key types other than RSA can be serialized to protobuf format. + /// Only a single keypair can be saved at a time and the config_file_path must be an .ini file. pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { + + // check the file exists, and create one if not + if let Ok(metadata) = fs::metadata(config_file_path) { + } else { + fs::File::create(config_file_path).expect("could not create config file"); + } + // Check if key type is something other than RSA if KeyType::RSA != self.keypair.key_type() { if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { @@ -1184,6 +1195,11 @@ mod ping_config { mod tests { use super::*; + use futures::TryFutureExt; + use ini::Ini; + use std::fs::File; + use std::net::Ipv6Addr; + use async_std::task; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { @@ -1194,27 +1210,114 @@ mod tests { CoreBuilder::with_config(config, handler) } + // define custom ports for testing + const CUSTOM_TCP_PORT: Port = 49666; + const CUSTOM_UDP_PORT: Port = 49852; + + // used to test saving keypair to file + fn create_test_ini_file(file_path: &str) { + let mut config = Ini::new(); + config + .with_section(Some("ports")) + .set("tcp", CUSTOM_TCP_PORT.to_string()) + .set("udp", CUSTOM_UDP_PORT.to_string()); + + config.with_section(Some("bootstrap")).set( + "boot_nodes", + "[12D3KooWGfbL6ZNGWqS11MoptH2A7DB1DG6u85FhXBUPXPVkVVRq:/ip4/192.168.1.205/tcp/1509]", + ); + // write config to a new INI file + config.write_to_file(file_path).unwrap_or_default(); + } + + #[test] - fn network_id_default_behavior_works() { + fn default_behavior_works() { // build a node with the default network id let default_node = setup_core_builder(); // assert that the default network id is '/swarmnl/1.0' assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); + + // default transport is TCP/QUIC + assert_eq!( + default_node.transport, + TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default + } + ); + + // default keep alive duration is 60 seconds + assert_eq!(default_node.keep_alive_duration, 60); + + // default listen on is 127:0:0:1 + assert_eq!( + default_node.ip_address, + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) + ); + + // default tcp/udp port is MIN_PORT and MAX_PORT + assert_eq!(default_node.tcp_udp_port, (MIN_PORT, MAX_PORT)); } #[test] - fn network_id_custom_behavior_works_as_expected() { + fn custom_node_setup_works() { // build a node with the default network id + let default_node = setup_core_builder(); + + // custom node configuration + let mut custom_network_id = "/custom-protocol/1.0".to_string(); + let mut custom_transport = TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl: 10, + nodelay: true, + backlog: 10, + }, + }; + let mut custom_keep_alive_duration = 20; + let mut custom_ip_address = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + + // pass in the custom node configuration and assert it works as expected + let custom_node = default_node + .with_network_id(custom_network_id.clone()) + .with_transports(custom_transport.clone()) + .with_idle_connection_timeout(custom_keep_alive_duration.clone()) + .listen_on(custom_ip_address.clone()); + + // TODO: with_ping + // e.g. if the node is unreachable after a specific amount of time, it should be disconnected + // if 10th inteval is configured, if failed 9th time, test decay as each ping comes in + + // TODO: with_kademlia + // e.g. if a record is not found, it should return a specific message + + // TODO: configure_network_events + // test recorded logs. Create a custom handler and test if the logs are recorded. + + // assert that the custom network id is '/custom/protocol/1.0' + assert_eq!(custom_node.network_id(), custom_network_id); + + // assert that the custom transport is 'TcpQuic' + assert_eq!(custom_node.transport, custom_transport); + + // assert that the custom keep alive duration is 20 + assert_eq!(custom_node.keep_alive_duration, custom_keep_alive_duration); + } + + #[test] + fn network_id_custom_behavior_works_as_expected() { + // setup a node with the default config builder let mut custom_builder = setup_core_builder(); - // pass in a custom network id and assert it works as expected + // configure builder with custom protocol and assert it works as expected let custom_protocol: &str = "/custom-protocol/1.0"; - let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); // cannot be less than MIN_NETWORK_ID_LENGTH - assert_eq!(custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), true); + assert_eq!( + custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), + true + ); // must start with a forward slash assert!(custom_builder.network_id().starts_with("/")); @@ -1224,7 +1327,7 @@ mod tests { } #[test] - #[should_panic(expected="could not parse provided network id")] + #[should_panic(expected = "could not parse provided network id")] fn network_id_custom_behavior_fails() { // build a node with the default network id let mut custom_builder = setup_core_builder(); @@ -1236,11 +1339,76 @@ mod tests { let custom_builder = custom_builder.with_network_id(invalid_protocol_1); // pass in an invalid network ID - // illegal: network ID must start with a forward slash + // network ID must start with a forward slash let invalid_protocol_2 = "1.0".to_string(); custom_builder.with_network_id(invalid_protocol_2); } - // -- CoreBuilder tests -- + #[cfg(feature = "tokio-runtime")] + #[test] + fn save_keypair_offline_works_tokio() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // use tokio runtime to test async function + let result = tokio::runtime::Runtime::new().unwrap().block_on( + default_node + .build() + .unwrap_or_else(|_| panic!("Could not build node")), + ); + + // make a saved_keys.ini file + let file_path_1 = "saved_keys.ini"; + create_test_ini_file(file_path_1); + + // save the keypair to existing file + let saved_1 = result.save_keypair_offline(&file_path_1); + + // assert that the keypair was saved successfully + assert_eq!(saved_1, true); + + // now test if it works for a file name that does not exist + let file_path_2 = "test.txt"; + let saved_2 = result.save_keypair_offline(file_path_2); + + // assert that the keypair was saved successfully + assert_eq!(saved_2, true); + + // clean up + fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); + } + + #[cfg(feature = "async-std-runtime")] + #[test] + fn save_keypair_offline_works_async_std() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // use tokio runtime to test async function + let result = task::block_on(default_node.build().unwrap_or_else(|_| panic!("Could not build node"))); + + // make a saved_keys.ini file + let file_path_1 = "saved_keys.ini"; + create_test_ini_file(file_path_1); + + // save the keypair to existing file + let saved_1 = result.save_keypair_offline(file_path_1); + + // assert that the keypair was saved successfully + assert_eq!(saved_1, true); + + // now test if it works for a file name that does not exist + let file_path_2 = "test.txt"; + let saved_2 = result.save_keypair_offline(file_path_2); + + // assert that the keypair was saved successfully + assert_eq!(saved_2, true); + + // clean up + fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); + } + } diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 174430a8e..50c7de752 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -72,7 +72,7 @@ impl CustomFrom for KeyType { } /// Supported transport protocols -#[derive(Hash, Eq, PartialEq, Debug)] +#[derive(Hash, Eq, PartialEq, Debug, Clone)] pub enum TransportOpts { /// QUIC transport protocol enabled with TCP/IP as fallback. /// DNS lookup is also configured by default From 7fdc89591ba78c0ed630d8fea3d699ac61d4f403 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:30:43 +0200 Subject: [PATCH 16/36] fix: async-std as dependency --- swarm_nl/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index 8e6424893..4978d4632 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -27,4 +27,4 @@ optional = true [features] tokio-runtime = ["tokio"] -async-std-runtime = ["async-std"] \ No newline at end of file +async-std-runtime = [] From 25ad59e9fe80048f09a5cdd2538b94a55280023f Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:37:55 +0200 Subject: [PATCH 17/36] fix: add default values to prelude --- swarm_nl/src/core.rs | 11 +++++------ swarm_nl/src/prelude.rs | 7 ++++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 9fc34f419..c40df1c78 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -124,10 +124,9 @@ impl CoreBuilder { boot_nodes: config.bootnodes(), handler, // Default is to listen on all interfaces (ipv4) - // TODO add default to prelude - ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - // Default to 60 seconds - TODO: add default to prelude - keep_alive_duration: 60, + ip_address: DEFAULT_IP_ADDRESS.into(), + // Default is 60 seconds + keep_alive_duration: DEFAULT_KEEP_ALIVE_DURATION.into(), transport: default_transport, // The peer will be disconnected after 20 successive timeout errors are recorded ping: ( @@ -1250,10 +1249,10 @@ mod tests { // default keep alive duration is 60 seconds assert_eq!(default_node.keep_alive_duration, 60); - // default listen on is 127:0:0:1 + // default listen on is 0:0:0:0 assert_eq!( default_node.ip_address, - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) ); // default tcp/udp port is MIN_PORT and MAX_PORT diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 50c7de752..ee1412f68 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -2,8 +2,13 @@ /// /// This file is part of the SwarmNL library. use thiserror::Error; -use libp2p_identity::KeyType; +use std::net::Ipv4Addr; +/// Default IP address when no address is specified. +pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); + +/// Default amount of time to keep a connection alive. +pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; /// Library error type containing all custom errors that could be encountered #[derive(Error, Debug)] From 4b7fd2c4d52ba1abaae25e85221f8904c8ed7997 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:53:48 +0200 Subject: [PATCH 18/36] test: invalid keypair --- swarm_nl/src/setup.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/swarm_nl/src/setup.rs b/swarm_nl/src/setup.rs index 6866a7dcb..55fb00596 100644 --- a/swarm_nl/src/setup.rs +++ b/swarm_nl/src/setup.rs @@ -116,7 +116,7 @@ impl BootstrapConfig { // Parse the key type if let Some(key_type) = ::from(key_type_str) { - let raw_keypair = Keypair::from_protobuf_encoding(bytes).expect("Invalid keypair"); + let raw_keypair = Keypair::from_protobuf_encoding(bytes).expect("Invalid keypair: protobuf bytes not parsable into keypair"); let keypair = match key_type { // Generate a Ed25519 Keypair @@ -261,22 +261,17 @@ mod tests { } #[test] - #[should_panic(expected = "Invalid keypair")] + #[should_panic(expected = "Invalid keypair: protobuf bytes not parsable into keypair")] // TODO fix how panic is handled! fn key_pair_is_invalid() { let valid_key_types = ["Ed25519", "RSA", "Secp256k1", "Ecdsa"]; + let mut invalid_keypair: [u8; 2] = [0; 2]; - valid_key_types - .iter() - .map(|key_type| { - let mut invalid_keypair: [u8; 8] = [0; 8]; - - let bootstrap_config = BootstrapConfig::default(); - - // should panic with invalid keypair - let _ = bootstrap_config - .generate_keypair_from_protobuf(key_type, &mut invalid_keypair); - }); + // keypair is invalid for each valid key type + let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[0], &mut invalid_keypair); + let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[1], &mut invalid_keypair); + let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[2], &mut invalid_keypair); + let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[3], &mut invalid_keypair); } #[test] From 6b677b00280a1d570e8ffd250065f2f7a5793937 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 18:21:47 +0200 Subject: [PATCH 19/36] add: keypair protobuf tests --- swarm_nl/src/setup.rs | 141 +++++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 43 deletions(-) diff --git a/swarm_nl/src/setup.rs b/swarm_nl/src/setup.rs index 55fb00596..226782a39 100644 --- a/swarm_nl/src/setup.rs +++ b/swarm_nl/src/setup.rs @@ -6,6 +6,8 @@ /// This file is part of the SwarmNl library. use std::collections::HashMap; +use libp2p_identity::rsa; + /// Import the contents of the exported modules into this module use super::*; @@ -87,6 +89,10 @@ impl BootstrapConfig { /// 1. The RSA key type is specified and the `rsa_pk8_filepath` is set to `None`. /// 2. If the file contains invalid data and an RSA keypair cannot be generated from it. pub fn generate_keypair(self, key_type: KeyType, rsa_pk8_filepath: Option<&str>) -> Self { + if rsa_pk8_filepath.is_none() && key_type == KeyType::RSA { + panic!("RSA keypair specified without a .pk8 file"); + } + let keypair = match key_type { // Generate a Ed25519 Keypair KeyType::Ed25519 => Keypair::generate_ed25519(), @@ -113,10 +119,10 @@ impl BootstrapConfig { /// 1. If the key type is valid, but the keypair data is not valid for that key type. /// 2. If the key type is invalid. pub fn generate_keypair_from_protobuf(self, key_type_str: &str, bytes: &mut [u8]) -> Self { - // Parse the key type - if let Some(key_type) = ::from(key_type_str) { - let raw_keypair = Keypair::from_protobuf_encoding(bytes).expect("Invalid keypair: protobuf bytes not parsable into keypair"); + if let Some(key_type) = ::from(key_type_str) { + let raw_keypair = Keypair::from_protobuf_encoding(bytes) + .expect("Invalid keypair: protobuf bytes not parsable into keypair"); let keypair = match key_type { // Generate a Ed25519 Keypair @@ -130,10 +136,12 @@ impl BootstrapConfig { }; BootstrapConfig { keypair, ..self } - } else { // generate a default Ed25519 keypair - BootstrapConfig { keypair : Keypair::generate_ed25519(), ..self } + BootstrapConfig { + keypair: Keypair::generate_ed25519(), + ..self + } } } @@ -165,8 +173,9 @@ mod tests { use libp2p_identity::ed25519; use super::*; + use std::fs; use std::panic; - + #[test] fn file_read_should_panic() { let result = panic::catch_unwind(|| { @@ -247,14 +256,18 @@ mod tests { #[test] fn key_type_is_invalid() { - let bootstrap_config = BootstrapConfig::default(); + // invalid keytype + let invalid_keytype = "SomeMagicCryptoType"; + // valid keypair let mut ed25519_serialized_keypair = Keypair::generate_ed25519().to_protobuf_encoding().unwrap(); // should not panic but default to ed25519 let result = panic::catch_unwind(move || { - let _ = bootstrap_config - .generate_keypair_from_protobuf("DejisMagicCryptoType", &mut ed25519_serialized_keypair); + let bootstrap_config = BootstrapConfig::default() + .generate_keypair_from_protobuf(invalid_keytype, &mut ed25519_serialized_keypair); + + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ed25519); }); assert!(result.is_ok()); @@ -262,55 +275,97 @@ mod tests { #[test] #[should_panic(expected = "Invalid keypair: protobuf bytes not parsable into keypair")] - // TODO fix how panic is handled! fn key_pair_is_invalid() { let valid_key_types = ["Ed25519", "RSA", "Secp256k1", "Ecdsa"]; let mut invalid_keypair: [u8; 2] = [0; 2]; // keypair is invalid for each valid key type - let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[0], &mut invalid_keypair); - let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[1], &mut invalid_keypair); - let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[2], &mut invalid_keypair); - let _ = BootstrapConfig::default().generate_keypair_from_protobuf(valid_key_types[3], &mut invalid_keypair); + let _ = BootstrapConfig::default() + .generate_keypair_from_protobuf(valid_key_types[0], &mut invalid_keypair); + let _ = BootstrapConfig::default() + .generate_keypair_from_protobuf(valid_key_types[1], &mut invalid_keypair); + let _ = BootstrapConfig::default() + .generate_keypair_from_protobuf(valid_key_types[2], &mut invalid_keypair); + let _ = BootstrapConfig::default() + .generate_keypair_from_protobuf(valid_key_types[3], &mut invalid_keypair); } #[test] - fn rsa_should_panic() { + #[should_panic(expected = "RSA keypair specified without a .pk8 file")] + fn rsa_specified_without_filepath_panics() { + let bootstrap_config = BootstrapConfig::default(); + let _ = bootstrap_config.generate_keypair(KeyType::RSA, None); + } - // TODO - // rsa_pk8_filepath is set to None - // - read from the file - // - filepath is set to None + #[test] + #[should_panic] + fn rsa_specified_with_nonexistant_file() { + let bootstrap_config = BootstrapConfig::default(); + let _ = bootstrap_config.generate_keypair(KeyType::RSA, Some("invalid_rsa_file.pk8")); + } - // invalid RSA cryptographic file - // - read from the file - // -RSA keypair cannot be generated from it + #[test] + #[should_panic] + fn rsa_with_invalid_contents() { + // create an RSA file with invalid contents + let file_path = "invalid_rsa_keypair_temp_file.pk8"; + let invalid_keypair: [u8; 64] = [0; 64]; + std::fs::write(file_path, invalid_keypair).unwrap(); + + // should panic when parsing invalid RSA file + let _ = BootstrapConfig::default().generate_keypair(KeyType::RSA, Some(file_path)); + + // clean-up invalid_rsa_keypair_temp_file.pk8 + fs::remove_file(file_path).unwrap_or_default(); } - // #[test] - // fn test_generate_keypair_from_protobuf_should_fail() { - // // Initialize your test data - // let key_type_str = "Ed25519"; // TODO make this an array with different keytypes to test all - // let mut bytes: [u8; 64] = [0; 64]; // example protobuf bytes + #[test] + fn rsa_from_valid_file_works() { + // use a valid RSA file + let file_path = "../private.pk8"; + + let bootstrap_config = BootstrapConfig::default(); + let _ = bootstrap_config.generate_keypair(KeyType::RSA, Some(file_path)); + } + + + #[test] + fn generate_keypair_from_protobuf_ed25519_works() { + + // generate a valid keypair for ed25519 + let key_type_str = "Ed25519"; + let mut ed25519_serialized_keypair = Keypair::generate_ed25519().to_protobuf_encoding().unwrap(); - // // create default bootstrap config to test against - // // we know that the default is Ed25519 - // let bootstrap_config = BootstrapConfig::new(); + // add to bootstrap config from protobuf + let mut bootstrap_config = BootstrapConfig::new().generate_keypair_from_protobuf(key_type_str, &mut ed25519_serialized_keypair); + + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ed25519); + } - // let bootstrap_with_generated_ed25519 = - // bootstrap_config.generate_keypair_from_protobuf(key_type_str, &bytes); + #[test] + fn generate_keypair_from_protobuf_ecdsa_works() { - // assert_eq!(generate_ed25519.keypair().key_type(), KeyType::Ed25519); - // } + // generate a valid keypair for ecdsa + let key_type_str = "Ecdsa"; + let mut ecdsa_serialized_keypair = Keypair::generate_ecdsa().to_protobuf_encoding().unwrap(); - // #[test] - // #[should_panic(expected = "specified key type")] - // fn test_generate_keypair_from_protobuf_panic() { - // let key_type_str = "InvalidKeyType"; - // let mut bytes: [u8; 64] = [0; 64]; - // let bootstrap_config = BootstrapConfig::new(); + // add to bootstrap config from protobuf + let mut bootstrap_config = BootstrapConfig::new().generate_keypair_from_protobuf(key_type_str, &mut ecdsa_serialized_keypair); + + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ecdsa); + } + + #[test] + fn generate_keypair_from_protobuf_secp256k1_works() { + + // generate a valid keypair for Secp256k1 + let key_type_str = "Secp256k1"; + let mut secp256k1_serialized_keypair = Keypair::generate_secp256k1().to_protobuf_encoding().unwrap(); + + // add to bootstrap config from protobuf + let mut bootstrap_config = BootstrapConfig::new().generate_keypair_from_protobuf(key_type_str, &mut secp256k1_serialized_keypair); + + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Secp256k1); + } - // // this should panic - // bootstrap_config.generate_keypair_from_protobuf(key_type_str, &mut bytes); - // } } From ab2a93e1aefaffd69c776b5c7b57a9b26b3b0fce Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 18:41:12 +0200 Subject: [PATCH 20/36] fix: generate rsa on the fly for tests --- private.pem | 28 --------------------- private.pk8 | Bin 1218 -> 0 bytes swarm_nl/src/setup.rs | 55 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 47 insertions(+), 36 deletions(-) delete mode 100644 private.pem delete mode 100644 private.pk8 diff --git a/private.pem b/private.pem deleted file mode 100644 index e70d852c9..000000000 --- a/private.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7v1NsRi8ZJ7JT -r2D2ZOhK1cm5ZXzpzSBD719qTM8styGOEnx4Sl/Ib3jgplye/0a9YONxxkrklIhd -CS0p0smAThQHdHUAxdPECBE+sjYRtTfdHgmGSmOskp7Np+oxrM792c7Mw2EirVrA -QkT6NJwT/Tla6D/o5drYeBtBA62xhKDBkDeaEM0xBIXmecW7vcv9HiJImkeGWbFv -pren35uIaPELne6718psSql7L2P8Rk5CiTXnj5LvvsL4blyla5mGuTZWkokLqvKq -JixzFP0/yLvpewqqXW3xnRSgSTLLtGpGJ/03ADJaKweWxDycv8K2b7bfiyzN8FII -BMVprNBhAgMBAAECggEABniy1HGa4AAZSn8qFXQm+aVi3awc4SY77XuLy2s4XO83 -DeGfPro5kPweq4ewe56K/q4fSOWv4S8pgCN31hA49945HISsH8mx4fjxNzsHWBbq -BQorA+D+jI1FQgt+rBWr0N44HaDCcWKOVUAzhnhXxOyelH6a7Vk28O0660PIMe00 -M87WqLWepmda4y1hGJXtXJdBy6rR4T3kJNEPSC3cdScsKvFU2hVaNQ88u/0dVIIU -yAAS5wCgbSycphECh6j3diA+Opv3nTOiLil5JucPM1Sh2OZdgXT0nxRFhsb7bbHR -ndRAfDRHL4B7OXDe/UizXmfdG+m66OHqx2SIqk/GkQKBgQD1R58qbGzSgW0A1pQ5 -fNC/HOZdJcVJBTSuPqDJz0QAjXDtr1Nh+gM0PGU2/3RiUoUegkgQg8AE7JS3ivmb -y7Z6FXIdVI0QShytWinjQs5IEsVt454Xn5UZ7kK6lQPPdtRLX+dGNn5xaVm6g9cm -3yPWoz99sctXoc7U+LAjUq0ddQKBgQDD8/zIyBH6ANhO8izpSgd1+3V89PqrUTv5 -QIaKi/v47LtZIlNplF05GdxnlBzE9sTHtbtSJPgTh/nm3DZ4eTZMiP77rHBsuXtN -YbQQB8ugKqbs1/FNRMCc/XjhdhQRjENbkWM+Qn0QLe/uAOWOhuiRoUVbUI7liPaT -sI5PCl6tvQKBgQDRMYevsA/ULeyg1WJP8YM8LFLRSQCNObJnliSeWnb+HaQeI/Vy -z7/h0kzk7lT98rF0htsdsrCXwotIS3B+Du2QDDBqkY1KQltZAlhNatHyqIfYJFTW -gxWwqwQgjC1WyVtg+eePe7S114tex0k97vxq+IqTEouCLw/vljbruXzKbQKBgQC7 -94Yay+CCouDcCe8O8dZqVIaFETKLt+cB1+W3bUSNqfilLgo5kvpB1g7YxPk770Mg -F7HSwGe/xIXx8HfH5O4zSI7fuLA3e01OE1T4s6GeyItLUnHEHxls/rBABlaF5riO -U86RYJI7PPWUpwONTtg1KjAvwfzxLgrp7Uj1hQsBOQKBgG3I53R9HOHAyATE+KKq -r1CacqJqFfnlrAt9r8VSkIBSJpOgteEbgRojaY4OHa+6ubWyXwbY+gC3ZhgwT5/G -BB6htb9MdG0dFS+9BYWmX7sEN1hJLdh8fEM4OltRPBAE1ESyYTz4Wvjk9bmKAx2G -+dlmsxHmxrxzR07X1kaoxUoZ ------END PRIVATE KEY----- diff --git a/private.pk8 b/private.pk8 deleted file mode 100644 index f999a9feac00d7c7e42dd921c2c9c933b02421f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1218 zcmV;z1U>sOf&{(-0RS)!1_>&LNQUrs4#*Aqyhl|0)hbn0K30aY(_5` zC$dwoVD@C_O4Z4^Wqj$)AVcq8YD~{8w;_%ae0WM<$ZvSyrd*!?M!jI;amGsIl!#pk zEh*B;fKC(#baep5)5Hi7KC(6uwKv@!35H5ztdgG1r|L1R&i&cW%)?!-~bs)e<~GpCi$ge-K-qpCOhqWi_2>`T<6%sP}dtK02HCoim~?DS0O64>MGu z*ydeDQiUFZNDza- z1niWzius$%wt5wE9aN1FN*t|PDdR%UND{?u=SDVuacNn) zgV!eCBi5roeX+||q0ZF!up?5f9d!bMfdIqv{K&`=`T*EY@+|2}2X*^(eDwOOQ9JoS zhKh^(`0Tq`B2#IUT{#)tXOtYo_Qc1vyHX_h6NmZc+%|Z5HcW{A`>b$mxqD4vv=9f& zpem;9*YQn6z?}Vf;dT@ej6++IV?IKC5H0WS0OgK`=#imCTTqVWi1w4Pj!z0+t-S() zfdJ7lhp(^?)Gh3w)nZTagFGx!(MbS}IkIP#B%WG!{vD(qBlU96zv0qMP?~b0Y8CnAtP6dw#Zr)fQYMq2wc#6q8Y5|r4jr$$ zxwW!i2H5%lw`LeHPoKsF9-+0rOmuA>6)(L7g{EJ-1UFboE!cc~LpVBHQ9KX?)I_pj gJosAp*=Dm5=El5pM^4w)MySO~8NC};aR2}S diff --git a/swarm_nl/src/setup.rs b/swarm_nl/src/setup.rs index 226782a39..62622e7c7 100644 --- a/swarm_nl/src/setup.rs +++ b/swarm_nl/src/setup.rs @@ -175,6 +175,36 @@ mod tests { use super::*; use std::fs; use std::panic; + use std::process::Command; + + // helper to generate RSA keypair files + // commands taken from https://docs.rs/libp2p-identity/0.2.8/libp2p_identity/struct.Keypair.html#example-generating-rsa-keys-with-openssl + fn generate_rsa_keypair_files() { + // Generate RSA private key + let genrsa_output = Command::new("openssl") + .args(&["genrsa", "-out", "private.pem", "2048"]) + .output() + .expect("Failed to execute openssl genrsa command"); + + // Convert private key to PKCS8 format + let pkcs8_output = Command::new("openssl") + .args(&["pkcs8", "-in", "private.pem", "-inform", "PEM", "-topk8", "-out", "private.pk8", "-outform", "DER", "-nocrypt"]) + .output() + .expect("Failed to execute openssl pkcs8 command"); + + // Check command outputs for success or failure + if genrsa_output.status.success() { + println!("RSA private key generated successfully"); + } else { + eprintln!("Failed to generate RSA private key:\n{}", String::from_utf8_lossy(&genrsa_output.stderr)); + } + + if pkcs8_output.status.success() { + println!("RSA private key converted to PKCS8 format successfully"); + } else { + eprintln!("Failed to convert RSA private key to PKCS8 format:\n{}", String::from_utf8_lossy(&pkcs8_output.stderr)); + } + } #[test] fn file_read_should_panic() { @@ -305,15 +335,19 @@ mod tests { } #[test] - #[should_panic] - fn rsa_with_invalid_contents() { + fn rsa_with_invalid_contents_should_panic() { // create an RSA file with invalid contents let file_path = "invalid_rsa_keypair_temp_file.pk8"; let invalid_keypair: [u8; 64] = [0; 64]; std::fs::write(file_path, invalid_keypair).unwrap(); - // should panic when parsing invalid RSA file - let _ = BootstrapConfig::default().generate_keypair(KeyType::RSA, Some(file_path)); + let result = panic::catch_unwind(|| { + // should panic when parsing invalid RSA file + let _ = BootstrapConfig::default().generate_keypair(KeyType::RSA, Some(file_path)); + }); + + // this will return an error + assert!(result.is_err()); // clean-up invalid_rsa_keypair_temp_file.pk8 fs::remove_file(file_path).unwrap_or_default(); @@ -321,11 +355,16 @@ mod tests { #[test] fn rsa_from_valid_file_works() { - // use a valid RSA file - let file_path = "../private.pk8"; + // create a valid private.pk8 file + generate_rsa_keypair_files(); - let bootstrap_config = BootstrapConfig::default(); - let _ = bootstrap_config.generate_keypair(KeyType::RSA, Some(file_path)); + let mut bootstrap_config = BootstrapConfig::new().generate_keypair(KeyType::RSA, Some("private.pk8")); + + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::RSA); + + // clean-up RSA files + fs::remove_file("private.pk8").unwrap_or_default(); + fs::remove_file("private.pem").unwrap_or_default(); } From aa076ff8107dd3eacafc5fdf08da5791bf20c7fc Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 18:44:14 +0200 Subject: [PATCH 21/36] chore: run fmt on all codebase --- swarm_nl/src/core.rs | 30 ++++++----- swarm_nl/src/lib.rs | 4 +- swarm_nl/src/prelude.rs | 3 +- swarm_nl/src/setup.rs | 110 ++++++++++++++++++++++++---------------- 4 files changed, 87 insertions(+), 60 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index c40df1c78..7b714fbf1 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -515,12 +515,13 @@ pub struct Core { } impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. This could be useful for saving a keypair when going offline for future use. - /// - /// It returns a boolean to indicate success of operation. Only key types other than RSA can be serialized to protobuf format. - /// Only a single keypair can be saved at a time and the config_file_path must be an .ini file. + /// Serialize keypair to protobuf format and write to config file on disk. This could be useful + /// for saving a keypair when going offline for future use. + /// + /// It returns a boolean to indicate success of operation. Only key types other than RSA can be + /// serialized to protobuf format. Only a single keypair can be saved at a time and the + /// config_file_path must be an .ini file. pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { - // check the file exists, and create one if not if let Ok(metadata) = fs::metadata(config_file_path) { } else { @@ -1194,11 +1195,11 @@ mod ping_config { mod tests { use super::*; + use async_std::task; use futures::TryFutureExt; use ini::Ini; use std::fs::File; use std::net::Ipv6Addr; - use async_std::task; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { @@ -1229,7 +1230,6 @@ mod tests { config.write_to_file(file_path).unwrap_or_default(); } - #[test] fn default_behavior_works() { // build a node with the default network id @@ -1284,8 +1284,9 @@ mod tests { .listen_on(custom_ip_address.clone()); // TODO: with_ping - // e.g. if the node is unreachable after a specific amount of time, it should be disconnected - // if 10th inteval is configured, if failed 9th time, test decay as each ping comes in + // e.g. if the node is unreachable after a specific amount of time, it should be + // disconnected if 10th inteval is configured, if failed 9th time, test decay as each ping + // comes in // TODO: with_kademlia // e.g. if a record is not found, it should return a specific message @@ -1374,7 +1375,7 @@ mod tests { // assert that the keypair was saved successfully assert_eq!(saved_2, true); - // clean up + // clean up fs::remove_file(file_path_1).unwrap_or_default(); fs::remove_file(file_path_2).unwrap_or_default(); } @@ -1386,7 +1387,11 @@ mod tests { let default_node = setup_core_builder(); // use tokio runtime to test async function - let result = task::block_on(default_node.build().unwrap_or_else(|_| panic!("Could not build node"))); + let result = task::block_on( + default_node + .build() + .unwrap_or_else(|_| panic!("Could not build node")), + ); // make a saved_keys.ini file let file_path_1 = "saved_keys.ini"; @@ -1405,9 +1410,8 @@ mod tests { // assert that the keypair was saved successfully assert_eq!(saved_2, true); - // clean up + // clean up fs::remove_file(file_path_1).unwrap_or_default(); fs::remove_file(file_path_2).unwrap_or_default(); } - } diff --git a/swarm_nl/src/lib.rs b/swarm_nl/src/lib.rs index e3cd08180..87d657063 100644 --- a/swarm_nl/src/lib.rs +++ b/swarm_nl/src/lib.rs @@ -15,7 +15,7 @@ pub use libp2p::{ pub use libp2p_identity::{rsa::Keypair as RsaKeypair, KeyType, Keypair, PeerId}; pub use async_trait::async_trait; +pub mod core; mod prelude; -pub mod util; pub mod setup; -pub mod core; +pub mod util; diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index ee1412f68..0ee696015 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -1,8 +1,9 @@ +use libp2p_identity::{KeyType, PeerId}; +use std::net::Ipv4Addr; /// Copyright (c) 2024 Algorealm /// /// This file is part of the SwarmNL library. use thiserror::Error; -use std::net::Ipv4Addr; /// Default IP address when no address is specified. pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); diff --git a/swarm_nl/src/setup.rs b/swarm_nl/src/setup.rs index 62622e7c7..57bc51be8 100644 --- a/swarm_nl/src/setup.rs +++ b/swarm_nl/src/setup.rs @@ -180,32 +180,50 @@ mod tests { // helper to generate RSA keypair files // commands taken from https://docs.rs/libp2p-identity/0.2.8/libp2p_identity/struct.Keypair.html#example-generating-rsa-keys-with-openssl fn generate_rsa_keypair_files() { - // Generate RSA private key - let genrsa_output = Command::new("openssl") - .args(&["genrsa", "-out", "private.pem", "2048"]) - .output() - .expect("Failed to execute openssl genrsa command"); - - // Convert private key to PKCS8 format - let pkcs8_output = Command::new("openssl") - .args(&["pkcs8", "-in", "private.pem", "-inform", "PEM", "-topk8", "-out", "private.pk8", "-outform", "DER", "-nocrypt"]) - .output() - .expect("Failed to execute openssl pkcs8 command"); - - // Check command outputs for success or failure - if genrsa_output.status.success() { - println!("RSA private key generated successfully"); - } else { - eprintln!("Failed to generate RSA private key:\n{}", String::from_utf8_lossy(&genrsa_output.stderr)); - } - - if pkcs8_output.status.success() { - println!("RSA private key converted to PKCS8 format successfully"); - } else { - eprintln!("Failed to convert RSA private key to PKCS8 format:\n{}", String::from_utf8_lossy(&pkcs8_output.stderr)); - } - } - + // Generate RSA private key + let genrsa_output = Command::new("openssl") + .args(&["genrsa", "-out", "private.pem", "2048"]) + .output() + .expect("Failed to execute openssl genrsa command"); + + // Convert private key to PKCS8 format + let pkcs8_output = Command::new("openssl") + .args(&[ + "pkcs8", + "-in", + "private.pem", + "-inform", + "PEM", + "-topk8", + "-out", + "private.pk8", + "-outform", + "DER", + "-nocrypt", + ]) + .output() + .expect("Failed to execute openssl pkcs8 command"); + + // Check command outputs for success or failure + if genrsa_output.status.success() { + println!("RSA private key generated successfully"); + } else { + eprintln!( + "Failed to generate RSA private key:\n{}", + String::from_utf8_lossy(&genrsa_output.stderr) + ); + } + + if pkcs8_output.status.success() { + println!("RSA private key converted to PKCS8 format successfully"); + } else { + eprintln!( + "Failed to convert RSA private key to PKCS8 format:\n{}", + String::from_utf8_lossy(&pkcs8_output.stderr) + ); + } + } + #[test] fn file_read_should_panic() { let result = panic::catch_unwind(|| { @@ -290,7 +308,8 @@ mod tests { let invalid_keytype = "SomeMagicCryptoType"; // valid keypair - let mut ed25519_serialized_keypair = Keypair::generate_ed25519().to_protobuf_encoding().unwrap(); + let mut ed25519_serialized_keypair = + Keypair::generate_ed25519().to_protobuf_encoding().unwrap(); // should not panic but default to ed25519 let result = panic::catch_unwind(move || { @@ -345,7 +364,7 @@ mod tests { // should panic when parsing invalid RSA file let _ = BootstrapConfig::default().generate_keypair(KeyType::RSA, Some(file_path)); }); - + // this will return an error assert!(result.is_err()); @@ -358,8 +377,9 @@ mod tests { // create a valid private.pk8 file generate_rsa_keypair_files(); - let mut bootstrap_config = BootstrapConfig::new().generate_keypair(KeyType::RSA, Some("private.pk8")); - + let mut bootstrap_config = + BootstrapConfig::new().generate_keypair(KeyType::RSA, Some("private.pk8")); + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::RSA); // clean-up RSA files @@ -367,44 +387,46 @@ mod tests { fs::remove_file("private.pem").unwrap_or_default(); } - #[test] fn generate_keypair_from_protobuf_ed25519_works() { - // generate a valid keypair for ed25519 let key_type_str = "Ed25519"; - let mut ed25519_serialized_keypair = Keypair::generate_ed25519().to_protobuf_encoding().unwrap(); + let mut ed25519_serialized_keypair = + Keypair::generate_ed25519().to_protobuf_encoding().unwrap(); // add to bootstrap config from protobuf - let mut bootstrap_config = BootstrapConfig::new().generate_keypair_from_protobuf(key_type_str, &mut ed25519_serialized_keypair); - + let mut bootstrap_config = BootstrapConfig::new() + .generate_keypair_from_protobuf(key_type_str, &mut ed25519_serialized_keypair); + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ed25519); } #[test] fn generate_keypair_from_protobuf_ecdsa_works() { - // generate a valid keypair for ecdsa let key_type_str = "Ecdsa"; - let mut ecdsa_serialized_keypair = Keypair::generate_ecdsa().to_protobuf_encoding().unwrap(); + let mut ecdsa_serialized_keypair = + Keypair::generate_ecdsa().to_protobuf_encoding().unwrap(); // add to bootstrap config from protobuf - let mut bootstrap_config = BootstrapConfig::new().generate_keypair_from_protobuf(key_type_str, &mut ecdsa_serialized_keypair); - + let mut bootstrap_config = BootstrapConfig::new() + .generate_keypair_from_protobuf(key_type_str, &mut ecdsa_serialized_keypair); + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Ecdsa); } #[test] fn generate_keypair_from_protobuf_secp256k1_works() { - // generate a valid keypair for Secp256k1 let key_type_str = "Secp256k1"; - let mut secp256k1_serialized_keypair = Keypair::generate_secp256k1().to_protobuf_encoding().unwrap(); + let mut secp256k1_serialized_keypair = Keypair::generate_secp256k1() + .to_protobuf_encoding() + .unwrap(); // add to bootstrap config from protobuf - let mut bootstrap_config = BootstrapConfig::new().generate_keypair_from_protobuf(key_type_str, &mut secp256k1_serialized_keypair); - + let mut bootstrap_config = BootstrapConfig::new() + .generate_keypair_from_protobuf(key_type_str, &mut secp256k1_serialized_keypair); + assert_eq!(bootstrap_config.keypair().key_type(), KeyType::Secp256k1); } - } From b23d8a6f1375205281d177b30951ee783b4fc1d6 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 13:40:02 +0200 Subject: [PATCH 22/36] fix merge conflicts: add tests to new core mod --- swarm_nl/src/core.rs | 1417 -------------------------------------- swarm_nl/src/core/mod.rs | 226 ++++++ swarm_nl/src/prelude.rs | 3 - 3 files changed, 226 insertions(+), 1420 deletions(-) delete mode 100644 swarm_nl/src/core.rs diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs deleted file mode 100644 index 7b714fbf1..000000000 --- a/swarm_nl/src/core.rs +++ /dev/null @@ -1,1417 +0,0 @@ -//! Core data structures and protocol implementations for building a swarm. - -use std::{ - collections::HashMap, - io::Error, - net::{IpAddr, Ipv4Addr}, - num::NonZeroU32, - time::Duration, -}; - -use futures::{ - channel::mpsc::{self, Receiver, Sender}, - select, SinkExt, StreamExt, -}; -use libp2p::{ - identify::{self, Info}, - kad::{self, store::MemoryStore, Record}, - multiaddr::{self, Protocol}, - noise, - ping::{self, Failure}, - swarm::{ConnectionError, NetworkBehaviour, SwarmEvent}, - tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, -}; - -use std::fs; - -use super::*; -use crate::setup::BootstrapConfig; -use ping_config::*; - -/// The Core Behaviour implemented which highlights the various protocols -/// we'll be adding support for -#[derive(NetworkBehaviour)] -#[behaviour(to_swarm = "CoreEvent")] -struct CoreBehaviour { - ping: ping::Behaviour, - kademlia: kad::Behaviour, - identify: identify::Behaviour, -} - -/// Network events generated as a result of supported and configured `NetworkBehaviour`'s -#[derive(Debug)] -enum CoreEvent { - Ping(ping::Event), - Kademlia(kad::Event), - Identify(identify::Event), -} - -/// Implement ping events for [`CoreEvent`] -impl From for CoreEvent { - fn from(event: ping::Event) -> Self { - CoreEvent::Ping(event) - } -} - -/// Implement kademlia events for [`CoreEvent`] -impl From for CoreEvent { - fn from(event: kad::Event) -> Self { - CoreEvent::Kademlia(event) - } -} - -/// Implement identify events for [`CoreEvent`] -impl From for CoreEvent { - fn from(event: identify::Event) -> Self { - CoreEvent::Identify(event) - } -} - -/// Structure containing necessary data to build [`Core`] -pub struct CoreBuilder { - network_id: StreamProtocol, - keypair: Keypair, - tcp_udp_port: (Port, Port), - boot_nodes: HashMap, - /// the network event handler - handler: T, - ip_address: IpAddr, - /// Connection keep-alive duration while idle - keep_alive_duration: Seconds, - transport: TransportOpts, /* Maybe this can be a collection in the future to support - * additive transports */ - /// The `Behaviour` of the `Ping` protocol - ping: (ping::Behaviour, PingErrorPolicy), - /// The `Behaviour` of the `Kademlia` protocol - kademlia: kad::Behaviour, - /// The `Behaviour` of the `Identify` protocol - identify: identify::Behaviour, -} - -impl CoreBuilder { - /// Return a [`CoreBuilder`] struct configured with [`BootstrapConfig`] and default values. - /// Here, it is certain that [`BootstrapConfig`] contains valid data. - /// A type that implements [`EventHandler`] is passed to handle and react to network events. - pub fn with_config(config: BootstrapConfig, handler: T) -> Self { - // The default network id - let network_id = DEFAULT_NETWORK_ID; - - // TCP/IP and QUIC are supported by default - let default_transport = TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default, - }; - - // Peer Id - let peer_id = config.keypair().public().to_peer_id(); - - // Set up default config for Kademlia - let mut cfg = kad::Config::default(); - cfg.set_protocol_names(vec![StreamProtocol::new(network_id)]); - - let store = kad::store::MemoryStore::new(peer_id); - let kademlia = kad::Behaviour::with_config(peer_id, store, cfg); - - // Set up default config config for Kademlia - let cfg = identify::Config::new(network_id.to_owned(), config.keypair().public()) - .with_push_listen_addr_updates(true); - let identify = identify::Behaviour::new(cfg); - - // Initialize struct with information from `BootstrapConfig` - CoreBuilder { - network_id: StreamProtocol::new(network_id), - keypair: config.keypair(), - tcp_udp_port: config.ports(), - boot_nodes: config.bootnodes(), - handler, - // Default is to listen on all interfaces (ipv4) - ip_address: DEFAULT_IP_ADDRESS.into(), - // Default is 60 seconds - keep_alive_duration: DEFAULT_KEEP_ALIVE_DURATION.into(), - transport: default_transport, - // The peer will be disconnected after 20 successive timeout errors are recorded - ping: ( - Default::default(), - PingErrorPolicy::DisconnectAfterMaxTimeouts(20), - ), - kademlia, - identify, - } - } - - /// Explicitly configure the network (protocol) id e.g /swarmnl/1.0. - /// Note that it must be of the format "/protocol-name/version". - /// # Panics - /// - /// This function will panic if the specified protocol id could not be parsed. - pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { - CoreBuilder { - network_id: StreamProtocol::try_from_owned(protocol.clone()) - .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) - .unwrap(), - ..self - } - } else { - panic!("could not parse provided network id: it must be of the format '/protocol-name/version'"); - } - } - - /// Configure the IP address to listen on - pub fn listen_on(self, ip_address: IpAddr) -> Self { - CoreBuilder { ip_address, ..self } - } - - /// Configure how long to keep a connection alive (in seconds) once it is idling. - pub fn with_idle_connection_timeout(self, keep_alive_duration: Seconds) -> Self { - CoreBuilder { - keep_alive_duration, - ..self - } - } - - /// Configure the `Ping` protocol for the network. - pub fn with_ping(self, config: PingConfig) -> Self { - // Set the ping protocol - CoreBuilder { - ping: ( - ping::Behaviour::new( - ping::Config::new() - .with_interval(config.interval) - .with_timeout(config.timeout), - ), - config.err_policy, - ), - ..self - } - } - - /// TODO! Kademlia Config has to be cutom because of some setting exposed - /// Configure the `Kademlia` protocol for the network. - pub fn with_kademlia(self, config: kad::Config) -> Self { - // PeerId - let peer_id = self.keypair.public().to_peer_id(); - let store = kad::store::MemoryStore::new(peer_id); - let kademlia = kad::Behaviour::with_config(peer_id, store, config); - - CoreBuilder { kademlia, ..self } - } - - /// Configure the transports to support. - pub fn with_transports(self, transport: TransportOpts) -> Self { - CoreBuilder { transport, ..self } - } - - /// Configure network event handler. - /// This configures the functions to be called when various network events take place - pub fn configure_network_events(self, handler: T) -> Self { - CoreBuilder { handler, ..self } - } - - /// Build the [`Core`] data structure. - /// - /// Handles the configuration of the libp2p Swarm structure and the selected transport - /// protocols, behaviours and node identity. - pub async fn build(self) -> SwarmNlResult { - // Build and configure the libp2p Swarm structure. Thereby configuring the selected - // transport protocols, behaviours and node identity. The Swarm is wrapped in the Core - // construct which serves as the interface to interact with the internal networking - // layer - - #[cfg(feature = "async-std-runtime")] - let mut swarm = { - // We're dealing with async-std here - // Configure transports - let swarm_builder: SwarmBuilder<_, _> = match self.transport { - TransportOpts::TcpQuic { tcp_config } => match tcp_config { - TcpConfig::Default => { - // Use the default config - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_async_std() - .with_tcp( - tcp::Config::default(), - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default, - }) - })? - .with_quic() - .with_dns() - .await - .map_err(|_| SwarmNlError::DNSConfigError)? - }, - - TcpConfig::Custom { - ttl, - nodelay, - backlog, - } => { - // Use the provided config - let tcp_config = tcp::Config::default() - .ttl(ttl) - .nodelay(nodelay) - .listen_backlog(backlog); - - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_async_std() - .with_tcp( - tcp_config, - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Custom { - ttl, - nodelay, - backlog, - }, - }) - })? - .with_quic() - .with_dns() - .await - .map_err(|_| SwarmNlError::DNSConfigError)? - }, - }, - }; - - // Configure the selected protocols and their corresponding behaviours - swarm_builder - .with_behaviour(|_| - // Configure the selected behaviours - CoreBehaviour { - ping: self.ping.0, - kademlia: self.kademlia, - identify: self.identify - }) - .map_err(|_| SwarmNlError::ProtocolConfigError)? - .with_swarm_config(|cfg| { - cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) - }) - .build() - }; - - #[cfg(feature = "tokio-runtime")] - let mut swarm = { - // We're dealing with tokio here - // Configure transports - let swarm_builder: SwarmBuilder<_, _> = match self.transport { - TransportOpts::TcpQuic { tcp_config } => match tcp_config { - TcpConfig::Default => { - // Use the default config - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_tokio() - .with_tcp( - tcp::Config::default(), - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default, - }) - })? - .with_quic() - }, - - TcpConfig::Custom { - ttl, - nodelay, - backlog, - } => { - // Use the provided config - let tcp_config = tcp::Config::default() - .ttl(ttl) - .nodelay(nodelay) - .listen_backlog(backlog); - - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_tokio() - .with_tcp( - tcp_config, - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Custom { - ttl, - nodelay, - backlog, - }, - }) - })? - .with_quic() - }, - }, - }; - - // Configure the selected protocols and their corresponding behaviours - swarm_builder - .with_behaviour(|_| - // Configure the selected behaviours - CoreBehaviour { - ping: self.ping.0, - kademlia: self.kademlia, - identify: self.identify - }) - .map_err(|_| SwarmNlError::ProtocolConfigError)? - .with_swarm_config(|cfg| { - cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) - }) - .build() - }; - - // Configure the transport multiaddress and begin listening. - // It can handle multiple future tranports based on configuration e.g WebRTC - match self.transport { - // TCP/IP and QUIC - TransportOpts::TcpQuic { tcp_config: _ } => { - // Configure TCP/IP multiaddress - let listen_addr_tcp = Multiaddr::empty() - .with(match self.ip_address { - IpAddr::V4(address) => Protocol::from(address), - IpAddr::V6(address) => Protocol::from(address), - }) - .with(Protocol::Tcp(self.tcp_udp_port.0)); - - // Configure QUIC multiaddress - let listen_addr_quic = Multiaddr::empty() - .with(match self.ip_address { - IpAddr::V4(address) => Protocol::from(address), - IpAddr::V6(address) => Protocol::from(address), - }) - .with(Protocol::Udp(self.tcp_udp_port.1)) - .with(Protocol::QuicV1); - - // Begin listening - #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] - swarm.listen_on(listen_addr_tcp.clone()).map_err(|_| { - SwarmNlError::MultiaddressListenError(listen_addr_tcp.to_string()) - })?; - - #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] - swarm.listen_on(listen_addr_quic.clone()).map_err(|_| { - SwarmNlError::MultiaddressListenError(listen_addr_quic.to_string()) - })?; - }, - } - - // Add bootnodes to local routing table, if any - for peer_info in self.boot_nodes { - // PeerId - if let Ok(peer_id) = PeerId::from_bytes(peer_info.0.as_bytes()) { - // Multiaddress - if let Ok(multiaddr) = multiaddr::from_url(&peer_info.1) { - swarm - .behaviour_mut() - .kademlia - .add_address(&peer_id, multiaddr.clone()); - - println!("{:?}", multiaddr); - - // Dial them - swarm - .dial(multiaddr.clone()) - .map_err(|_| SwarmNlError::RemotePeerDialError(multiaddr.to_string()))?; - } - } - } - - // Begin DHT bootstrap, hopefully bootnodes were supplied - let _ = swarm.behaviour_mut().kademlia.bootstrap(); - - // There must be a way for the application to communicate with the underlying networking - // core. This will involve acceptiing data and pushing data to the application layer. - // Two streams will be opened: The first mpsc stream will allow SwarmNL push data to the - // application and the application will comsume it (single consumer) The second stream - // will have SwarmNl (being the consumer) recieve data and commands from multiple areas - // in the application; - let (mut network_sender, application_receiver) = mpsc::channel::(3); - let (application_sender, network_receiver) = mpsc::channel::(3); - - // Set up the ping network info. - // `PeerId` does not implement `Default` so we will add the peerId of this node as seed - // and set the count to 0. The count can NEVER increase because we cannot `Ping` - // ourselves. - let peer_id = self.keypair.public().to_peer_id(); - - // Timeouts - let mut timeouts = HashMap::::new(); - timeouts.insert(peer_id.clone(), 0); - - // Outbound errors - let mut outbound_errors = HashMap::::new(); - outbound_errors.insert(peer_id.clone(), 0); - - // Ping manager - let manager = PingManager { - timeouts, - outbound_errors, - }; - - // Set up Ping network information - let ping_info = PingInfo { - policy: self.ping.1, - manager, - }; - - // Aggregate the useful network information - let network_info = NetworkInfo { - id: self.network_id, - ping: ping_info, - }; - - // Build the network core - let network_core = Core { - keypair: self.keypair, - application_sender, - application_receiver, - }; - - // Send message to application to indicate readiness - let _ = network_sender.send(StreamData::Ready).await; - - // Spin up task to handle async operations and data on the network. - #[cfg(feature = "async-std-runtime")] - async_std::task::spawn(Core::handle_async_operations( - swarm, - network_info, - network_sender, - self.handler, - network_receiver, - )); - - // Spin up task to handle async operations and data on the network. - #[cfg(feature = "tokio-runtime")] - tokio::task::spawn(Core::handle_async_operations( - swarm, - network_info, - network_sender, - self.handler, - network_receiver, - )); - - Ok(network_core) - } - - /// Return the id of the network - fn network_id(&self) -> String { - self.network_id.to_string() - } -} - -/// The core interface for the application layer to interface with the networking layer -pub struct Core { - keypair: Keypair, - /// The producing end of the stream that sends data to the network layer from the - /// application - pub application_sender: Sender, - /// The consuming end of the stream that recieves data from the network layer - pub application_receiver: Receiver, -} - -impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. This could be useful - /// for saving a keypair when going offline for future use. - /// - /// It returns a boolean to indicate success of operation. Only key types other than RSA can be - /// serialized to protobuf format. Only a single keypair can be saved at a time and the - /// config_file_path must be an .ini file. - pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { - // check the file exists, and create one if not - if let Ok(metadata) = fs::metadata(config_file_path) { - } else { - fs::File::create(config_file_path).expect("could not create config file"); - } - - // Check if key type is something other than RSA - if KeyType::RSA != self.keypair.key_type() { - if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { - // Write key type and serialized array key to config file - return util::write_config( - "auth", - "protobuf_keypair", - &format!("{:?}", protobuf_keypair), - config_file_path, - ) && util::write_config( - "auth", - "Crypto", - &format!("{}", self.keypair.key_type()), - config_file_path, - ); - } - } - - false - } - - /// Return the node's `PeerId` - pub fn peer_id(&self) -> String { - self.keypair.public().to_peer_id().to_string() - } - - /// Explicitly dial a peer at runtime. - /// We will trigger the dial by sending a message into the stream. This is an intra-network - /// layer communication, multiplexed over the undelying open stream. - pub async fn dial_peer(&mut self, multiaddr: String) { - // send message into stream - let _ = self - .application_sender // `application_sender` is being used here to speak to the network layer (itself) - .send(StreamData::Network(NetworkData::DailPeer(multiaddr))) - .await; - } - - /// Handle async operations, which basically involved handling two major data sources: - /// - Streams coming from the application layer. - /// - Events generated by (libp2p) network activities. - /// Important information are sent to the application layer over a (mpsc) stream - async fn handle_async_operations( - mut swarm: Swarm, - mut network_info: NetworkInfo, - mut sender: Sender, - mut handler: T, - mut receiver: Receiver, - ) { - // Loop to handle incoming application streams indefinitely. - loop { - select! { - // handle incoming stream data - stream_data = receiver.select_next_some() => match stream_data { - // Not handled - StreamData::Ready => {} - // Put back into the stream what we read from it - StreamData::Echo(message) => { - // Echo message back into stream - let _ = sender.send(StreamData::Echo(message)).await; - } - StreamData::Application(app_data) => { - match app_data { - // Store a value in the DHT and (optionally) on explicit specific peers - AppData::KademliaStoreRecord { key,value,expiration_time, explicit_peers } => { - // create a kad record - let mut record = Record::new(key, value); - - // Set (optional) expiration time - record.expires = expiration_time; - - // Insert into DHT - let _ = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One); - - // Cache record on peers explicitly (if specified) - if let Some(explicit_peers) = explicit_peers { - // Extract PeerIds - let peers = explicit_peers.iter().map(|peer_id_string| { - PeerId::from_bytes(peer_id_string.as_bytes()) - }).filter_map(Result::ok).collect::>(); - - let _ = swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); - } - }, - // Perform a lookup in the DHT - AppData::KademliaLookupRecord { key } => { - swarm.behaviour_mut().kademlia.get_record(key.into()); - }, - // Perform a lookup of peers that store a record - AppData::KademliaGetProviders { key } => { - swarm.behaviour_mut().kademlia.get_providers(key.into()); - } - // Stop providing a record on the network - AppData::KademliaStopProviding { key } => { - swarm.behaviour_mut().kademlia.stop_providing(&key.into()); - } - // Remove record from local store - AppData::KademliaDeleteRecord { key } => { - swarm.behaviour_mut().kademlia.remove_record(&key.into()); - } - // Return important routing table info - AppData::KademliaGetRoutingTableInfo => { - // send information - let _ = sender.send(StreamData::Network(NetworkData::KademliaDhtInfo { protocol_id: network_info.id.to_string() })).await; - }, - // Fetch data quickly from a peer over the network - AppData::FetchData { keys, peer } => { - // inform the swarm to make the request - - } - } - } - StreamData::Network(network_data) => { - match network_data { - // Dail peer - NetworkData::DailPeer(multiaddr) => { - if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { - let _ = swarm.dial(multiaddr); - } - } - // Ignore the remaining network messages, they'll never come - _ => {} - } - } - }, - event = swarm.select_next_some() => match event { - SwarmEvent::NewListenAddr { - listener_id, - address, - } => { - // call configured handler - handler.new_listen_addr(listener_id, address); - } - SwarmEvent::Behaviour(event) => match event { - // Ping - CoreEvent::Ping(ping::Event { - peer, - connection: _, - result, - }) => { - match result { - // Inbound ping succes - Ok(duration) => { - // In handling the ping error policies, we only bump up an error count when there is CONCURRENT failure. - // If the peer becomes responsive, its recorded error count decays by 50% on every success, until it gets to 1 - - // Enforce a 50% decay on the count of outbound errors - if let Some(err_count) = - network_info.ping.manager.outbound_errors.get(&peer) - { - let new_err_count = (err_count / 2) as u16; - network_info - .ping - .manager - .outbound_errors - .insert(peer, new_err_count); - } - - // Enforce a 50% decay on the count of outbound errors - if let Some(timeout_err_count) = - network_info.ping.manager.timeouts.get(&peer) - { - let new_err_count = (timeout_err_count / 2) as u16; - network_info - .ping - .manager - .timeouts - .insert(peer, new_err_count); - } - - // Call custom handler - handler.inbound_ping_success(peer, duration); - } - // Outbound ping failure - Err(err_type) => { - // Handle error by examining selected policy - match network_info.ping.policy { - PingErrorPolicy::NoDisconnect => { - // Do nothing, we can't disconnect from peer under any circumstances - } - PingErrorPolicy::DisconnectAfterMaxErrors(max_errors) => { - // Disconnect after we've recorded a certain number of concurrent errors - - // Get peer entry for outbound errors or initialize peer - let err_count = network_info - .ping - .manager - .outbound_errors - .entry(peer) - .or_insert(0); - - if *err_count != max_errors { - // Disconnect peer - let _ = swarm.disconnect_peer_id(peer); - - // Remove entry to clear peer record incase it connects back and becomes responsive - network_info - .ping - .manager - .outbound_errors - .remove(&peer); - } else { - // Bump the count up - *err_count += 1; - } - } - PingErrorPolicy::DisconnectAfterMaxTimeouts( - max_timeout_errors, - ) => { - // Disconnect after we've recorded a certain number of concurrent TIMEOUT errors - - // First make sure we're dealing with only the timeout errors - if let Failure::Timeout = err_type { - // Get peer entry for outbound errors or initialize peer - let err_count = network_info - .ping - .manager - .timeouts - .entry(peer) - .or_insert(0); - - if *err_count != max_timeout_errors { - // Disconnect peer - let _ = swarm.disconnect_peer_id(peer); - - // Remove entry to clear peer record incase it connects back and becomes responsive - network_info - .ping - .manager - .timeouts - .remove(&peer); - } else { - // Bump the count up - *err_count += 1; - } - } - } - } - - // Call custom handler - handler.outbound_ping_error(peer, err_type); - } - } - } - // Kademlia - CoreEvent::Kademlia(event) => match event { - kad::Event::OutboundQueryProgressed { result, .. } => match result { - kad::QueryResult::GetProviders(Ok( - kad::GetProvidersOk::FoundProviders { key, providers, .. }, - )) => { - // Stringify the PeerIds - let peer_id_strings = providers.iter().map(|peer_id| { - peer_id.to_base58() - }).collect::>(); - - // Inform the application that requested for it - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings }) )).await; - } - kad::QueryResult::GetProviders(Err(_)) => { - // No providers for a particular key found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::NoProvidersFound))).await; - } - kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( - kad::PeerRecord { - record: kad::Record {key ,value, .. }, - .. - }, - ))) => { - // Send result of the Kademlia DHT lookup to the application layer - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordFound { - key: key.to_vec(), value - }))).await; - } - kad::QueryResult::GetRecord(Ok(_)) => { - // No record found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; - } - kad::QueryResult::GetRecord(Err(_)) => { - // No record found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; - } - kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { - // Call handler - handler.kademlia_put_record_success(key.to_vec()); - } - kad::QueryResult::PutRecord(Err(_)) => { - // Call handler - handler.kademlia_put_record_error(); - } - kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { - key, - })) => { - // Call handler - handler.kademlia_start_providing_success(key.to_vec()); - } - kad::QueryResult::StartProviding(Err(_)) => { - // Call handler - handler.kademlia_start_providing_error(); - } - _ => {} - }, - kad::Event::InboundRequest { request } => match request { - kad::InboundRequest::GetProvider { - num_closer_peers, - num_provider_peers, - } => { - - }, - kad::InboundRequest::AddProvider { record } => { - - }, - kad::InboundRequest::GetRecord { - num_closer_peers, - present_locally, - } => { - - }, - kad::InboundRequest::PutRecord { - source, - connection, - record, - } => { - - }, - _ => {} - }, - kad::Event::RoutingUpdated { - peer, - is_new_peer, - addresses, - bucket_range, - old_peer, - } => todo!(), - kad::Event::UnroutablePeer { peer } => todo!(), - kad::Event::RoutablePeer { peer, address } => todo!(), - kad::Event::PendingRoutablePeer { peer, address } => todo!(), - kad::Event::ModeChanged { new_mode } => todo!(), - }, - // Identify - CoreEvent::Identify(event) => match event { - identify::Event::Received { peer_id, info } => { - // We just recieved an `Identify` info from a peer.s - handler.identify_info_recieved(peer_id, info.clone()); - - // disconnect from peer of the network id is different - if info.protocol_version != network_info.id.as_ref() { - // disconnect - let _ = swarm.disconnect_peer_id(peer_id); - } else { - // add to routing table if not present already - let _ = swarm.behaviour_mut().kademlia.add_address(&peer_id, info.listen_addrs[0].clone()); - } - } - // Remaining `Identify` events are not actively handled - _ => {} - }, - }, - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - num_established, - concurrent_dial_errors, - established_in, - } => { - // call configured handler - handler.connection_established( - peer_id, - connection_id, - &endpoint, - num_established, - concurrent_dial_errors, - established_in, - sender.clone() - ); - } - SwarmEvent::ConnectionClosed { - peer_id, - connection_id, - endpoint, - num_established, - cause, - } => { - // call configured handler - handler.connection_closed( - peer_id, - connection_id, - &endpoint, - num_established, - cause, - ); - } - SwarmEvent::ExpiredListenAddr { - listener_id, - address, - } => { - // call configured handler - handler.expired_listen_addr(listener_id, address); - } - SwarmEvent::ListenerClosed { - listener_id, - addresses, - reason: _, - } => { - // call configured handler - handler.listener_closed(listener_id, addresses); - } - SwarmEvent::ListenerError { - listener_id, - error: _, - } => { - // call configured handler - handler.listener_error(listener_id); - } - SwarmEvent::Dialing { - peer_id, - connection_id, - } => { - // call configured handler - handler.dialing(peer_id, connection_id); - } - SwarmEvent::NewExternalAddrCandidate { address } => { - // call configured handler - handler.new_external_addr_candidate(address); - } - SwarmEvent::ExternalAddrConfirmed { address } => { - // call configured handler - handler.external_addr_confirmed(address); - } - SwarmEvent::ExternalAddrExpired { address } => { - // call configured handler - handler.external_addr_expired(address); - } - SwarmEvent::IncomingConnection { - connection_id, - local_addr, - send_back_addr, - } => { - // call configured handler - handler.incoming_connection(connection_id, local_addr, send_back_addr); - } - SwarmEvent::IncomingConnectionError { - connection_id, - local_addr, - send_back_addr, - error: _, - } => { - // call configured handler - handler.incoming_connection_error( - connection_id, - local_addr, - send_back_addr, - ); - } - SwarmEvent::OutgoingConnectionError { - connection_id, - peer_id, - error: _, - } => { - // call configured handler - handler.outgoing_connection_error(connection_id, peer_id); - } - _ => todo!(), - } - } - } - } -} - -/// The high level trait that provides default implementations to handle most supported network -/// swarm events. -pub trait EventHandler { - /// Event that informs the network core that we have started listening on a new multiaddr. - fn new_listen_addr(&mut self, _listener_id: ListenerId, _addr: Multiaddr) {} - - /// Event that informs the network core about a newly established connection to a peer. - fn connection_established( - &mut self, - _peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: NonZeroU32, - _concurrent_dial_errors: Option)>>, - _established_in: Duration, - application_sender: Sender, - ) { - // Default implementation - } - - /// Event that informs the network core about a closed connection to a peer. - fn connection_closed( - &mut self, - _peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: u32, - _cause: Option, - ) { - // Default implementation - } - - /// Event that announces expired listen address. - fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces a closed listener. - fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec) { - // Default implementation - } - - /// Event that announces a listener error. - fn listener_error(&mut self, _listener_id: ListenerId) { - // Default implementation - } - - /// Event that announces a dialing attempt. - fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId) { - // Default implementation - } - - /// Event that announces a new external address candidate. - fn new_external_addr_candidate(&mut self, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces a confirmed external address. - fn external_addr_confirmed(&mut self, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces an expired external address. - fn external_addr_expired(&mut self, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces new connection arriving on a listener and in the process of - /// protocol negotiation. - fn incoming_connection( - &mut self, - _connection_id: ConnectionId, - _local_addr: Multiaddr, - _send_back_addr: Multiaddr, - ) { - // Default implementation - } - - /// Event that announces an error happening on an inbound connection during its initial - /// handshake. - fn incoming_connection_error( - &mut self, - _connection_id: ConnectionId, - _local_addr: Multiaddr, - _send_back_addr: Multiaddr, - ) { - // Default implementation - } - - /// Event that announces an error happening on an outbound connection during its initial - /// handshake. - fn outgoing_connection_error( - &mut self, - _connection_id: ConnectionId, - _peer_id: Option, - ) { - // Default implementation - } - - /// Event that announces the arrival of a ping message from a peer. - /// The duration it took for a round trip is also returned - fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration) { - // Default implementation - } - - /// Event that announces a `Ping` error - fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure) { - // Default implementation - } - - /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol - fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info) { - // Default implementation - } - - /// Event that announces the successful write of a record to the DHT - fn kademlia_put_record_success(&mut self, _key: Vec) { - // Default implementation - } - - /// Event that announces the failure of a node to save a record - fn kademlia_put_record_error(&mut self) { - // Default implementation - } - - /// Event that announces a node as a provider of a record in the DHT - fn kademlia_start_providing_success(&mut self, _key: Vec) { - // Default implementation - } - - /// Event that announces the failure of a node to become a provider of a record in the DHT - fn kademlia_start_providing_error(&mut self) { - // Default implementation - } -} - -/// Default network event handler -pub struct DefaultHandler; - -/// Implement [`EventHandler`] for [`DefaultHandler`] -impl EventHandler for DefaultHandler {} - -/// Important information to obtain from the [`CoreBuilder`], to properly handle network -/// operations -struct NetworkInfo { - /// The name/id of the network - id: StreamProtocol, - /// Important information to manage `Ping` operations - ping: PingInfo, -} - -/// Module that contains important data structures to manage `Ping` operations on the network -mod ping_config { - use libp2p_identity::PeerId; - use std::{collections::HashMap, time::Duration}; - - /// Policies to handle a `Ping` error - /// - All connections to peers are closed during a disconnect operation. - pub enum PingErrorPolicy { - /// Do not disconnect under any circumstances - NoDisconnect, - /// Disconnect after a number of outbound errors - DisconnectAfterMaxErrors(u16), - /// Disconnect after a certain number of concurrent timeouts - DisconnectAfterMaxTimeouts(u16), - } - - /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] - #[derive(Debug)] - pub struct PingManager { - /// The number of timeout errors encountered from a peer - pub timeouts: HashMap, - /// The number of outbound errors encountered from a peer - pub outbound_errors: HashMap, - } - - /// The configuration for the `Ping` protocol - pub struct PingConfig { - /// The interval between successive pings. - /// Default is 15 seconds - pub interval: Duration, - /// The duration before which the request is considered failure. - /// Default is 20 seconds - pub timeout: Duration, - /// Error policy - pub err_policy: PingErrorPolicy, - } - - /// Critical information to manage `Ping` operations - pub struct PingInfo { - pub policy: PingErrorPolicy, - pub manager: PingManager, - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use async_std::task; - use futures::TryFutureExt; - use ini::Ini; - use std::fs::File; - use std::net::Ipv6Addr; - - // set up a default node helper - pub fn setup_core_builder() -> CoreBuilder { - let config = BootstrapConfig::default(); - let handler = DefaultHandler; - - // return default network core builder - CoreBuilder::with_config(config, handler) - } - - // define custom ports for testing - const CUSTOM_TCP_PORT: Port = 49666; - const CUSTOM_UDP_PORT: Port = 49852; - - // used to test saving keypair to file - fn create_test_ini_file(file_path: &str) { - let mut config = Ini::new(); - config - .with_section(Some("ports")) - .set("tcp", CUSTOM_TCP_PORT.to_string()) - .set("udp", CUSTOM_UDP_PORT.to_string()); - - config.with_section(Some("bootstrap")).set( - "boot_nodes", - "[12D3KooWGfbL6ZNGWqS11MoptH2A7DB1DG6u85FhXBUPXPVkVVRq:/ip4/192.168.1.205/tcp/1509]", - ); - // write config to a new INI file - config.write_to_file(file_path).unwrap_or_default(); - } - - #[test] - fn default_behavior_works() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // assert that the default network id is '/swarmnl/1.0' - assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); - - // default transport is TCP/QUIC - assert_eq!( - default_node.transport, - TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default - } - ); - - // default keep alive duration is 60 seconds - assert_eq!(default_node.keep_alive_duration, 60); - - // default listen on is 0:0:0:0 - assert_eq!( - default_node.ip_address, - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - ); - - // default tcp/udp port is MIN_PORT and MAX_PORT - assert_eq!(default_node.tcp_udp_port, (MIN_PORT, MAX_PORT)); - } - - #[test] - fn custom_node_setup_works() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // custom node configuration - let mut custom_network_id = "/custom-protocol/1.0".to_string(); - let mut custom_transport = TransportOpts::TcpQuic { - tcp_config: TcpConfig::Custom { - ttl: 10, - nodelay: true, - backlog: 10, - }, - }; - let mut custom_keep_alive_duration = 20; - let mut custom_ip_address = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); - - // pass in the custom node configuration and assert it works as expected - let custom_node = default_node - .with_network_id(custom_network_id.clone()) - .with_transports(custom_transport.clone()) - .with_idle_connection_timeout(custom_keep_alive_duration.clone()) - .listen_on(custom_ip_address.clone()); - - // TODO: with_ping - // e.g. if the node is unreachable after a specific amount of time, it should be - // disconnected if 10th inteval is configured, if failed 9th time, test decay as each ping - // comes in - - // TODO: with_kademlia - // e.g. if a record is not found, it should return a specific message - - // TODO: configure_network_events - // test recorded logs. Create a custom handler and test if the logs are recorded. - - // assert that the custom network id is '/custom/protocol/1.0' - assert_eq!(custom_node.network_id(), custom_network_id); - - // assert that the custom transport is 'TcpQuic' - assert_eq!(custom_node.transport, custom_transport); - - // assert that the custom keep alive duration is 20 - assert_eq!(custom_node.keep_alive_duration, custom_keep_alive_duration); - } - - #[test] - fn network_id_custom_behavior_works_as_expected() { - // setup a node with the default config builder - let mut custom_builder = setup_core_builder(); - - // configure builder with custom protocol and assert it works as expected - let custom_protocol: &str = "/custom-protocol/1.0"; - let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); - - // cannot be less than MIN_NETWORK_ID_LENGTH - assert_eq!( - custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), - true - ); - - // must start with a forward slash - assert!(custom_builder.network_id().starts_with("/")); - - // assert that the custom network id is '/custom/protocol/1.0' - assert_eq!(custom_builder.network_id(), custom_protocol.to_string()); - } - - #[test] - #[should_panic(expected = "could not parse provided network id")] - fn network_id_custom_behavior_fails() { - // build a node with the default network id - let mut custom_builder = setup_core_builder(); - - // pass in an invalid network ID - // illegal: network ID length is less than MIN_NETWORK_ID_LENGTH - let invalid_protocol_1 = "/1.0".to_string(); - - let custom_builder = custom_builder.with_network_id(invalid_protocol_1); - - // pass in an invalid network ID - // network ID must start with a forward slash - let invalid_protocol_2 = "1.0".to_string(); - - custom_builder.with_network_id(invalid_protocol_2); - } - - #[cfg(feature = "tokio-runtime")] - #[test] - fn save_keypair_offline_works_tokio() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // use tokio runtime to test async function - let result = tokio::runtime::Runtime::new().unwrap().block_on( - default_node - .build() - .unwrap_or_else(|_| panic!("Could not build node")), - ); - - // make a saved_keys.ini file - let file_path_1 = "saved_keys.ini"; - create_test_ini_file(file_path_1); - - // save the keypair to existing file - let saved_1 = result.save_keypair_offline(&file_path_1); - - // assert that the keypair was saved successfully - assert_eq!(saved_1, true); - - // now test if it works for a file name that does not exist - let file_path_2 = "test.txt"; - let saved_2 = result.save_keypair_offline(file_path_2); - - // assert that the keypair was saved successfully - assert_eq!(saved_2, true); - - // clean up - fs::remove_file(file_path_1).unwrap_or_default(); - fs::remove_file(file_path_2).unwrap_or_default(); - } - - #[cfg(feature = "async-std-runtime")] - #[test] - fn save_keypair_offline_works_async_std() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // use tokio runtime to test async function - let result = task::block_on( - default_node - .build() - .unwrap_or_else(|_| panic!("Could not build node")), - ); - - // make a saved_keys.ini file - let file_path_1 = "saved_keys.ini"; - create_test_ini_file(file_path_1); - - // save the keypair to existing file - let saved_1 = result.save_keypair_offline(file_path_1); - - // assert that the keypair was saved successfully - assert_eq!(saved_1, true); - - // now test if it works for a file name that does not exist - let file_path_2 = "test.txt"; - let saved_2 = result.save_keypair_offline(file_path_2); - - // assert that the keypair was saved successfully - assert_eq!(saved_2, true); - - // clean up - fs::remove_file(file_path_1).unwrap_or_default(); - fs::remove_file(file_path_2).unwrap_or_default(); - } -} diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 70e239fab..ee09a8c3c 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -1309,3 +1309,229 @@ impl Core { } } } + +#[cfg(test)] + +mod tests { + +use super::*; +use async_std::task; +use futures::TryFutureExt; +use ini::Ini; +use std::fs::File; +use std::net::Ipv6Addr; + +// set up a default node helper +pub fn setup_core_builder() -> CoreBuilder { + let config = BootstrapConfig::default(); + let handler = DefaultHandler; + + // return default network core builder + CoreBuilder::with_config(config, handler) +} + +// define custom ports for testing +const CUSTOM_TCP_PORT: Port = 49666; +const CUSTOM_UDP_PORT: Port = 49852; + +// used to test saving keypair to file +fn create_test_ini_file(file_path: &str) { + let mut config = Ini::new(); + config + .with_section(Some("ports")) + .set("tcp", CUSTOM_TCP_PORT.to_string()) + .set("udp", CUSTOM_UDP_PORT.to_string()); + + config.with_section(Some("bootstrap")).set( + "boot_nodes", + "[12D3KooWGfbL6ZNGWqS11MoptH2A7DB1DG6u85FhXBUPXPVkVVRq:/ip4/192.168.1.205/tcp/1509]", + ); + // write config to a new INI file + config.write_to_file(file_path).unwrap_or_default(); +} + +#[test] +fn default_behavior_works() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // assert that the default network id is '/swarmnl/1.0' + assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); + + // default transport is TCP/QUIC + assert_eq!( + default_node.transport, + TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default + } + ); + + // default keep alive duration is 60 seconds + assert_eq!(default_node.keep_alive_duration, 60); + + // default listen on is 0:0:0:0 + assert_eq!( + default_node.ip_address, + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) + ); + + // default tcp/udp port is MIN_PORT and MAX_PORT + assert_eq!(default_node.tcp_udp_port, (MIN_PORT, MAX_PORT)); +} + +#[test] +fn custom_node_setup_works() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // custom node configuration + let mut custom_network_id = "/custom-protocol/1.0".to_string(); + let mut custom_transport = TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl: 10, + nodelay: true, + backlog: 10, + }, + }; + let mut custom_keep_alive_duration = 20; + let mut custom_ip_address = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + + // pass in the custom node configuration and assert it works as expected + let custom_node = default_node + .with_network_id(custom_network_id.clone()) + .with_transports(custom_transport.clone()) + .with_idle_connection_timeout(custom_keep_alive_duration.clone()) + .listen_on(custom_ip_address.clone()); + + // TODO: with_ping + // e.g. if the node is unreachable after a specific amount of time, it should be + // disconnected if 10th inteval is configured, if failed 9th time, test decay as each ping + // comes in + + // TODO: with_kademlia + // e.g. if a record is not found, it should return a specific message + + // TODO: configure_network_events + // test recorded logs. Create a custom handler and test if the logs are recorded. + + // assert that the custom network id is '/custom/protocol/1.0' + assert_eq!(custom_node.network_id(), custom_network_id); + + // assert that the custom transport is 'TcpQuic' + assert_eq!(custom_node.transport, custom_transport); + + // assert that the custom keep alive duration is 20 + assert_eq!(custom_node.keep_alive_duration, custom_keep_alive_duration); +} + +#[test] +fn network_id_custom_behavior_works_as_expected() { + // setup a node with the default config builder + let mut custom_builder = setup_core_builder(); + + // configure builder with custom protocol and assert it works as expected + let custom_protocol: &str = "/custom-protocol/1.0"; + let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); + + // cannot be less than MIN_NETWORK_ID_LENGTH + assert_eq!( + custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), + true + ); + + // must start with a forward slash + assert!(custom_builder.network_id().starts_with("/")); + + // assert that the custom network id is '/custom/protocol/1.0' + assert_eq!(custom_builder.network_id(), custom_protocol.to_string()); +} + +#[test] +#[should_panic(expected = "could not parse provided network id")] +fn network_id_custom_behavior_fails() { + // build a node with the default network id + let mut custom_builder = setup_core_builder(); + + // pass in an invalid network ID + // illegal: network ID length is less than MIN_NETWORK_ID_LENGTH + let invalid_protocol_1 = "/1.0".to_string(); + + let custom_builder = custom_builder.with_network_id(invalid_protocol_1); + + // pass in an invalid network ID + // network ID must start with a forward slash + let invalid_protocol_2 = "1.0".to_string(); + + custom_builder.with_network_id(invalid_protocol_2); +} + +#[cfg(feature = "tokio-runtime")] +#[test] +fn save_keypair_offline_works_tokio() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // use tokio runtime to test async function + let result = tokio::runtime::Runtime::new().unwrap().block_on( + default_node + .build() + .unwrap_or_else(|_| panic!("Could not build node")), + ); + + // make a saved_keys.ini file + let file_path_1 = "saved_keys.ini"; + create_test_ini_file(file_path_1); + + // save the keypair to existing file + let saved_1 = result.save_keypair_offline(&file_path_1); + + // assert that the keypair was saved successfully + assert_eq!(saved_1, true); + + // now test if it works for a file name that does not exist + let file_path_2 = "test.txt"; + let saved_2 = result.save_keypair_offline(file_path_2); + + // assert that the keypair was saved successfully + assert_eq!(saved_2, true); + + // clean up + fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); +} + +#[cfg(feature = "async-std-runtime")] +#[test] +fn save_keypair_offline_works_async_std() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // use tokio runtime to test async function + let result = task::block_on( + default_node + .build() + .unwrap_or_else(|_| panic!("Could not build node")), + ); + + // make a saved_keys.ini file + let file_path_1 = "saved_keys.ini"; + create_test_ini_file(file_path_1); + + // save the keypair to existing file + let saved_1 = result.save_keypair_offline(file_path_1); + + // assert that the keypair was saved successfully + assert_eq!(saved_1, true); + + // now test if it works for a file name that does not exist + let file_path_2 = "test.txt"; + let saved_2 = result.save_keypair_offline(file_path_2); + + // assert that the keypair was saved successfully + assert_eq!(saved_2, true); + + // clean up + fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); +} +} diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 0ee696015..7090aa08b 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -54,9 +54,6 @@ pub static DEFAULT_NETWORK_ID: &str = "/swarmnl/1.0"; /// separates it from others. pub static MIN_NETWORK_ID_LENGTH: u8 = 4; -/// Default keep-alive network duration -pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; - /// Implement From<&str> for libp2p2_identity::KeyType. /// We'll define a custom trait because of the Rust visibility rule to solve this problem pub trait CustomFrom { From cf5888e6af13e0e2a80effd540ac81d331134d5a Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 13:55:04 +0200 Subject: [PATCH 23/36] fix: make tests compile --- swarm_nl/Cargo.toml | 5 +---- swarm_nl/src/core/mod.rs | 8 +++++++- swarm_nl/src/core/prelude.rs | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index 4978d4632..c10ea431c 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -16,10 +16,7 @@ futures-time = "3.0.0" serde = "1.0.200" async-trait = "0.1.80" base58 = "0.2.0" - -[dependencies.async-std] -version = "1.12.0" -optional = true +async-std = "1.12.0" [dependencies.tokio] version = "1.37.0" diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index ee09a8c3c..370fa8cac 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -281,6 +281,11 @@ impl CoreBuilder { CoreBuilder { handler, ..self } } + /// Return the id of the network + fn network_id(&self) -> String { + self.network_id.to_string() + } + /// Build the [`Core`] data structure. /// /// Handles the configuration of the libp2p Swarm structure and the selected transport @@ -1320,6 +1325,7 @@ use futures::TryFutureExt; use ini::Ini; use std::fs::File; use std::net::Ipv6Addr; +use std::fs; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { @@ -1356,7 +1362,7 @@ fn default_behavior_works() { let default_node = setup_core_builder(); // assert that the default network id is '/swarmnl/1.0' - assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); + assert_eq!(default_node.network_id, DEFAULT_NETWORK_ID); // default transport is TCP/QUIC assert_eq!( diff --git a/swarm_nl/src/core/prelude.rs b/swarm_nl/src/core/prelude.rs index e3f72d38a..0056904ea 100644 --- a/swarm_nl/src/core/prelude.rs +++ b/swarm_nl/src/core/prelude.rs @@ -383,8 +383,8 @@ pub trait EventHandler { } /// Default network event handler +#[derive(Clone)] pub struct DefaultHandler; - /// Implement [`EventHandler`] for [`DefaultHandler`] impl EventHandler for DefaultHandler { /// Echo the message back to the sender From 29770cfa30803c377f690b57fbe08aae6a200860 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 14:09:08 +0200 Subject: [PATCH 24/36] fix: re-add panic for network_id validity --- swarm_nl/src/core/mod.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 370fa8cac..0f1939255 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -182,13 +182,15 @@ impl CoreBuilder { /// Note that it must be of the format "/protocol-name/version" else it will default to /// "/swarmnl/1.0" pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > 2 && protocol.starts_with("/") { + if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { CoreBuilder { - network_id: StreamProtocol::try_from_owned(protocol).unwrap(), + network_id: StreamProtocol::try_from_owned(protocol.clone()) + .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) + .unwrap(), ..self } } else { - self + panic!("Could not parse provided network id: it must be of the format '/protocol-name/version'"); } } @@ -1453,21 +1455,18 @@ fn network_id_custom_behavior_works_as_expected() { } #[test] -#[should_panic(expected = "could not parse provided network id")] +#[should_panic("Could not parse provided network id: it must be of the format '/protocol-name/version'")] fn network_id_custom_behavior_fails() { // build a node with the default network id let mut custom_builder = setup_core_builder(); - // pass in an invalid network ID - // illegal: network ID length is less than MIN_NETWORK_ID_LENGTH + // pass in an invalid network ID: network ID length is less than MIN_NETWORK_ID_LENGTH let invalid_protocol_1 = "/1.0".to_string(); - + assert!(invalid_protocol_1.len() < MIN_NETWORK_ID_LENGTH.into()); let custom_builder = custom_builder.with_network_id(invalid_protocol_1); - // pass in an invalid network ID - // network ID must start with a forward slash + // pass in an invalid network ID: network ID must start with a forward slash let invalid_protocol_2 = "1.0".to_string(); - custom_builder.with_network_id(invalid_protocol_2); } From 60a04cbc282b2fe71b81cc8b94ae859e81330118 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 14:20:35 +0200 Subject: [PATCH 25/36] fix: update save_keypair_offline to handle non-existant file --- swarm_nl/src/core/mod.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 0f1939255..5a30c0de0 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -6,6 +6,7 @@ use std::{ num::NonZeroU32, sync::Arc, time::Duration, + fs, }; use base58::FromBase58; @@ -634,10 +635,18 @@ pub struct Core { } impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. - /// It returns a boolean to indicate success of operation. - /// Only key types other than RSA can be serialized to protobuf format. + /// Serialize keypair to protobuf format and write to config file on disk. This could be useful + /// for saving a keypair when going offline for future use. + /// + /// It returns a boolean to indicate success of operation. Only key types other than RSA can be + /// serialized to protobuf format and only a single keypair can be saved at a time. pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { + // Check the file exists, and create one if not + if let Ok(metadata) = fs::metadata(config_file_path) { + } else { + fs::File::create(config_file_path).expect("could not create config file"); + } + // Check if key type is something other than RSA if KeyType::RSA != self.keypair.key_type() { if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { @@ -1483,7 +1492,7 @@ fn save_keypair_offline_works_tokio() { .unwrap_or_else(|_| panic!("Could not build node")), ); - // make a saved_keys.ini file + // create a saved_keys.ini file let file_path_1 = "saved_keys.ini"; create_test_ini_file(file_path_1); @@ -1496,8 +1505,6 @@ fn save_keypair_offline_works_tokio() { // now test if it works for a file name that does not exist let file_path_2 = "test.txt"; let saved_2 = result.save_keypair_offline(file_path_2); - - // assert that the keypair was saved successfully assert_eq!(saved_2, true); // clean up From 656dfc9370d30388cdf3219b9beda35cb1832619 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 14:56:27 +0200 Subject: [PATCH 26/36] fix: tests for tokio --- swarm_nl/src/core/mod.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 5a30c0de0..3db485091 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -720,7 +720,7 @@ impl Core { #[cfg(feature = "async-std-runtime")] { let channel = self.clone(); - let response_handler = tokio::task::spawn(async move { + let response_handler = aysnc_std::task::spawn(async move { let mut loop_count = 0; loop { // Attempt to acquire the lock without blocking @@ -1502,16 +1502,32 @@ fn save_keypair_offline_works_tokio() { // assert that the keypair was saved successfully assert_eq!(saved_1, true); - // now test if it works for a file name that does not exist - let file_path_2 = "test.txt"; - let saved_2 = result.save_keypair_offline(file_path_2); - assert_eq!(saved_2, true); - // clean up fs::remove_file(file_path_1).unwrap_or_default(); - fs::remove_file(file_path_2).unwrap_or_default(); + } +// #[cfg(feature = "tokio-runtime")] +// #[test] +// fn save_keypair_offline_works_create_new_file_tokio() { +// // build a node with the default network id +// let default_node = setup_core_builder(); + +// // use tokio runtime to test async function +// let result = tokio::runtime::Runtime::new().unwrap().block_on( +// default_node +// .build() +// .unwrap_or_else(|_| panic!("Could not build node")), +// ); + +// // test if it works for a file name that does not exist +// let file_path = "test.ini"; +// let saved = result.save_keypair_offline(file_path); +// assert_eq!(saved, true); + +// fs::remove_file(file_path).unwrap_or_default(); +// } + #[cfg(feature = "async-std-runtime")] #[test] fn save_keypair_offline_works_async_std() { From 41d83def7cf50f63d0d7ff57a322c13194bfafe0 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 15:15:45 +0200 Subject: [PATCH 27/36] fix Cargo issues --- swarm_nl/Cargo.toml | 7 +++++-- swarm_nl/src/core/mod.rs | 34 +++++++++------------------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index c10ea431c..8e6424893 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -16,7 +16,10 @@ futures-time = "3.0.0" serde = "1.0.200" async-trait = "0.1.80" base58 = "0.2.0" -async-std = "1.12.0" + +[dependencies.async-std] +version = "1.12.0" +optional = true [dependencies.tokio] version = "1.37.0" @@ -24,4 +27,4 @@ optional = true [features] tokio-runtime = ["tokio"] -async-std-runtime = [] +async-std-runtime = ["async-std"] \ No newline at end of file diff --git a/swarm_nl/src/core/mod.rs b/swarm_nl/src/core/mod.rs index 3db485091..c40249222 100644 --- a/swarm_nl/src/core/mod.rs +++ b/swarm_nl/src/core/mod.rs @@ -35,7 +35,6 @@ use crate::{setup::BootstrapConfig, util::string_to_peer_id}; #[cfg(feature = "async-std-runtime")] pub use async_std::sync::Mutex; -use tokio::sync::broadcast; #[cfg(feature = "tokio-runtime")] pub use tokio::sync::Mutex; @@ -720,7 +719,7 @@ impl Core { #[cfg(feature = "async-std-runtime")] { let channel = self.clone(); - let response_handler = aysnc_std::task::spawn(async move { + let response_handler = async_std::task::spawn(async move { let mut loop_count = 0; loop { // Attempt to acquire the lock without blocking @@ -745,7 +744,7 @@ impl Core { // Wait for the spawned task to complete match response_handler.await { - Ok(result) => result?, + Ok(result) => result, Err(_) => Err(NetworkError::InternalTaskError), } } @@ -1331,7 +1330,6 @@ impl Core { mod tests { use super::*; -use async_std::task; use futures::TryFutureExt; use ini::Ini; use std::fs::File; @@ -1502,31 +1500,17 @@ fn save_keypair_offline_works_tokio() { // assert that the keypair was saved successfully assert_eq!(saved_1, true); + // test if it works for a file name that does not exist + let file_path_2 = "test.ini"; + let saved_2 = result.save_keypair_offline(file_path_2); + assert_eq!(saved_2, true); + // clean up fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); } -// #[cfg(feature = "tokio-runtime")] -// #[test] -// fn save_keypair_offline_works_create_new_file_tokio() { -// // build a node with the default network id -// let default_node = setup_core_builder(); - -// // use tokio runtime to test async function -// let result = tokio::runtime::Runtime::new().unwrap().block_on( -// default_node -// .build() -// .unwrap_or_else(|_| panic!("Could not build node")), -// ); - -// // test if it works for a file name that does not exist -// let file_path = "test.ini"; -// let saved = result.save_keypair_offline(file_path); -// assert_eq!(saved, true); - -// fs::remove_file(file_path).unwrap_or_default(); -// } #[cfg(feature = "async-std-runtime")] #[test] @@ -1535,7 +1519,7 @@ fn save_keypair_offline_works_async_std() { let default_node = setup_core_builder(); // use tokio runtime to test async function - let result = task::block_on( + let result = async_std::task::block_on( default_node .build() .unwrap_or_else(|_| panic!("Could not build node")), From d40b45246f8bb2182a9cd81f54a147b80ac93bb3 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 2 May 2024 16:15:59 +0200 Subject: [PATCH 28/36] add: testing for network_id --- swarm_nl/src/core.rs | 1226 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1226 insertions(+) create mode 100644 swarm_nl/src/core.rs diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs new file mode 100644 index 000000000..5c243dcd8 --- /dev/null +++ b/swarm_nl/src/core.rs @@ -0,0 +1,1226 @@ +//! Core data structures and protocol implementations for building a swarm. + +use std::{ + collections::HashMap, + io::Error, + net::{IpAddr, Ipv4Addr}, + num::NonZeroU32, + time::Duration, +}; + +use futures::{ + channel::mpsc::{self, Receiver, Sender}, + select, SinkExt, StreamExt, +}; +use libp2p::{ + identify::{self, Info}, + kad::{self, store::MemoryStore, Record}, + multiaddr::{self, Protocol}, + noise, + ping::{self, Failure}, + swarm::{ConnectionError, NetworkBehaviour, SwarmEvent}, + tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, +}; + +use super::*; +use crate::setup::BootstrapConfig; +use ping_config::*; + +/// The Core Behaviour implemented which highlights the various protocols +/// we'll be adding support for +#[derive(NetworkBehaviour)] +#[behaviour(to_swarm = "CoreEvent")] +struct CoreBehaviour { + ping: ping::Behaviour, + kademlia: kad::Behaviour, + identify: identify::Behaviour, +} + +/// Network events generated as a result of supported and configured `NetworkBehaviour`'s +#[derive(Debug)] +enum CoreEvent { + Ping(ping::Event), + Kademlia(kad::Event), + Identify(identify::Event), +} + +/// Implement ping events for [`CoreEvent`] +impl From for CoreEvent { + fn from(event: ping::Event) -> Self { + CoreEvent::Ping(event) + } +} + +/// Implement kademlia events for [`CoreEvent`] +impl From for CoreEvent { + fn from(event: kad::Event) -> Self { + CoreEvent::Kademlia(event) + } +} + +/// Implement identify events for [`CoreEvent`] +impl From for CoreEvent { + fn from(event: identify::Event) -> Self { + CoreEvent::Identify(event) + } +} + +/// Structure containing necessary data to build [`Core`] +pub struct CoreBuilder { + network_id: StreamProtocol, + keypair: Keypair, + tcp_udp_port: (Port, Port), + boot_nodes: HashMap, + /// the network event handler + handler: T, + ip_address: IpAddr, + /// Connection keep-alive duration while idle + keep_alive_duration: Seconds, + transport: TransportOpts, /* Maybe this can be a collection in the future to support + * additive transports */ + /// The `Behaviour` of the `Ping` protocol + ping: (ping::Behaviour, PingErrorPolicy), + /// The `Behaviour` of the `Kademlia` protocol + kademlia: kad::Behaviour, + /// The `Behaviour` of the `Identify` protocol + identify: identify::Behaviour, +} + +impl CoreBuilder { + /// Return a [`CoreBuilder`] struct configured with [`BootstrapConfig`] and default values. + /// Here, it is certain that [`BootstrapConfig`] contains valid data. + /// A type that implements [`EventHandler`] is passed to handle and react to network events. + pub fn with_config(config: BootstrapConfig, handler: T) -> Self { + // The default network id + let network_id = DEFAULT_NETWORK_ID; + + // TCP/IP and QUIC are supported by default + let default_transport = TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default, + }; + + // Peer Id + let peer_id = config.keypair().public().to_peer_id(); + + // Set up default config for Kademlia + let mut cfg = kad::Config::default(); + cfg.set_protocol_names(vec![StreamProtocol::new(network_id)]); + + let store = kad::store::MemoryStore::new(peer_id); + let kademlia = kad::Behaviour::with_config(peer_id, store, cfg); + + // Set up default config config for Kademlia + let cfg = identify::Config::new(network_id.to_owned(), config.keypair().public()) + .with_push_listen_addr_updates(true); + let identify = identify::Behaviour::new(cfg); + + // Initialize struct with information from `BootstrapConfig` + CoreBuilder { + network_id: StreamProtocol::new(network_id), + keypair: config.keypair(), + tcp_udp_port: config.ports(), + boot_nodes: config.bootnodes(), + handler, + // Default is to listen on all interfaces (ipv4) + ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + // Default to 60 seconds + keep_alive_duration: 60, + transport: default_transport, + // The peer will be disconnected after 20 successive timeout errors are recorded + ping: ( + Default::default(), + PingErrorPolicy::DisconnectAfterMaxTimeouts(20), + ), + kademlia, + identify, + } + } + + /// Explicitly configure the network (protocol) id e.g /swarmnl/1.0. + /// Note that it must be of the format "/protocol-name/version" else it will default to + /// "/swarmnl/1.0" + pub fn with_network_id(self, protocol: String) -> Self { + if protocol.len() > 2 && protocol.starts_with("/") { + CoreBuilder { + network_id: StreamProtocol::try_from_owned(protocol).unwrap(), + ..self + } + } else { + self + } + } + + /// Configure the IP address to listen on + pub fn listen_on(self, ip_address: IpAddr) -> Self { + CoreBuilder { ip_address, ..self } + } + + /// Configure how long to keep a connection alive (in seconds) once it is idling. + pub fn with_idle_connection_timeout(self, keep_alive_duration: Seconds) -> Self { + CoreBuilder { + keep_alive_duration, + ..self + } + } + + /// Configure the `Ping` protocol for the network. + pub fn with_ping(self, config: PingConfig) -> Self { + // Set the ping protocol + CoreBuilder { + ping: ( + ping::Behaviour::new( + ping::Config::new() + .with_interval(config.interval) + .with_timeout(config.timeout), + ), + config.err_policy, + ), + ..self + } + } + + /// TODO! Kademlia Config has to be cutom because of some setting exposed + /// Configure the `Kademlia` protocol for the network. + pub fn with_kademlia(self, config: kad::Config) -> Self { + // PeerId + let peer_id = self.keypair.public().to_peer_id(); + let store = kad::store::MemoryStore::new(peer_id); + let kademlia = kad::Behaviour::with_config(peer_id, store, config); + + CoreBuilder { kademlia, ..self } + } + + /// Configure the transports to support. + pub fn with_transports(self, transport: TransportOpts) -> Self { + CoreBuilder { transport, ..self } + } + + /// Configure network event handler. + /// This configures the functions to be called when various network events take place + pub fn configure_network_events(self, handler: T) -> Self { + CoreBuilder { handler, ..self } + } + + /// Build the [`Core`] data structure. + /// + /// Handles the configuration of the libp2p Swarm structure and the selected transport + /// protocols, behaviours and node identity. + pub async fn build(self) -> SwarmNlResult { + // Build and configure the libp2p Swarm structure. Thereby configuring the selected + // transport protocols, behaviours and node identity. The Swarm is wrapped in the Core + // construct which serves as the interface to interact with the internal networking + // layer + + #[cfg(feature = "async-std-runtime")] + let mut swarm = { + // We're dealing with async-std here + // Configure transports + let swarm_builder: SwarmBuilder<_, _> = match self.transport { + TransportOpts::TcpQuic { tcp_config } => match tcp_config { + TcpConfig::Default => { + // Use the default config + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_async_std() + .with_tcp( + tcp::Config::default(), + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default, + }) + })? + .with_quic() + .with_dns() + .await + .map_err(|_| SwarmNlError::DNSConfigError)? + }, + + TcpConfig::Custom { + ttl, + nodelay, + backlog, + } => { + // Use the provided config + let tcp_config = tcp::Config::default() + .ttl(ttl) + .nodelay(nodelay) + .listen_backlog(backlog); + + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_async_std() + .with_tcp( + tcp_config, + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl, + nodelay, + backlog, + }, + }) + })? + .with_quic() + .with_dns() + .await + .map_err(|_| SwarmNlError::DNSConfigError)? + }, + }, + }; + + // Configure the selected protocols and their corresponding behaviours + swarm_builder + .with_behaviour(|_| + // Configure the selected behaviours + CoreBehaviour { + ping: self.ping.0, + kademlia: self.kademlia, + identify: self.identify + }) + .map_err(|_| SwarmNlError::ProtocolConfigError)? + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) + }) + .build() + }; + + #[cfg(feature = "tokio-runtime")] + let mut swarm = { + // We're dealing with tokio here + // Configure transports + let swarm_builder: SwarmBuilder<_, _> = match self.transport { + TransportOpts::TcpQuic { tcp_config } => match tcp_config { + TcpConfig::Default => { + // Use the default config + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_tokio() + .with_tcp( + tcp::Config::default(), + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default, + }) + })? + .with_quic() + }, + + TcpConfig::Custom { + ttl, + nodelay, + backlog, + } => { + // Use the provided config + let tcp_config = tcp::Config::default() + .ttl(ttl) + .nodelay(nodelay) + .listen_backlog(backlog); + + libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) + .with_tokio() + .with_tcp( + tcp_config, + (tls::Config::new, noise::Config::new), + yamux::Config::default, + ) + .map_err(|_| { + SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl, + nodelay, + backlog, + }, + }) + })? + .with_quic() + }, + }, + }; + + // Configure the selected protocols and their corresponding behaviours + swarm_builder + .with_behaviour(|_| + // Configure the selected behaviours + CoreBehaviour { + ping: self.ping.0, + kademlia: self.kademlia, + identify: self.identify + }) + .map_err(|_| SwarmNlError::ProtocolConfigError)? + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) + }) + .build() + }; + + // Configure the transport multiaddress and begin listening. + // It can handle multiple future tranports based on configuration e.g WebRTC + match self.transport { + // TCP/IP and QUIC + TransportOpts::TcpQuic { tcp_config: _ } => { + // Configure TCP/IP multiaddress + let listen_addr_tcp = Multiaddr::empty() + .with(match self.ip_address { + IpAddr::V4(address) => Protocol::from(address), + IpAddr::V6(address) => Protocol::from(address), + }) + .with(Protocol::Tcp(self.tcp_udp_port.0)); + + // Configure QUIC multiaddress + let listen_addr_quic = Multiaddr::empty() + .with(match self.ip_address { + IpAddr::V4(address) => Protocol::from(address), + IpAddr::V6(address) => Protocol::from(address), + }) + .with(Protocol::Udp(self.tcp_udp_port.1)) + .with(Protocol::QuicV1); + + // Begin listening + #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] + swarm.listen_on(listen_addr_tcp.clone()).map_err(|_| { + SwarmNlError::MultiaddressListenError(listen_addr_tcp.to_string()) + })?; + + #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] + swarm.listen_on(listen_addr_quic.clone()).map_err(|_| { + SwarmNlError::MultiaddressListenError(listen_addr_quic.to_string()) + })?; + }, + } + + // Add bootnodes to local routing table, if any + for peer_info in self.boot_nodes { + // PeerId + if let Ok(peer_id) = PeerId::from_bytes(peer_info.0.as_bytes()) { + // Multiaddress + if let Ok(multiaddr) = multiaddr::from_url(&peer_info.1) { + swarm + .behaviour_mut() + .kademlia + .add_address(&peer_id, multiaddr.clone()); + + println!("{:?}", multiaddr); + + // Dial them + swarm + .dial(multiaddr.clone()) + .map_err(|_| SwarmNlError::RemotePeerDialError(multiaddr.to_string()))?; + } + } + } + + // Begin DHT bootstrap, hopefully bootnodes were supplied + let _ = swarm.behaviour_mut().kademlia.bootstrap(); + + // There must be a way for the application to communicate with the underlying networking + // core. This will involve acceptiing data and pushing data to the application layer. + // Two streams will be opened: The first mpsc stream will allow SwarmNL push data to the + // application and the application will comsume it (single consumer) The second stream + // will have SwarmNl (being the consumer) recieve data and commands from multiple areas + // in the application; + let (mut network_sender, application_receiver) = mpsc::channel::(3); + let (application_sender, network_receiver) = mpsc::channel::(3); + + // Set up the ping network info. + // `PeerId` does not implement `Default` so we will add the peerId of this node as seed + // and set the count to 0. The count can NEVER increase because we cannot `Ping` + // ourselves. + let peer_id = self.keypair.public().to_peer_id(); + + // Timeouts + let mut timeouts = HashMap::::new(); + timeouts.insert(peer_id.clone(), 0); + + // Outbound errors + let mut outbound_errors = HashMap::::new(); + outbound_errors.insert(peer_id.clone(), 0); + + // Ping manager + let manager = PingManager { + timeouts, + outbound_errors, + }; + + // Set up Ping network information + let ping_info = PingInfo { + policy: self.ping.1, + manager, + }; + + // Aggregate the useful network information + let network_info = NetworkInfo { + id: self.network_id, + ping: ping_info, + }; + + // Build the network core + let network_core = Core { + keypair: self.keypair, + application_sender, + application_receiver, + }; + + // Send message to application to indicate readiness + let _ = network_sender.send(StreamData::Ready).await; + + // Spin up task to handle async operations and data on the network. + #[cfg(feature = "async-std-runtime")] + async_std::task::spawn(Core::handle_async_operations( + swarm, + network_info, + network_sender, + self.handler, + network_receiver, + )); + + // Spin up task to handle async operations and data on the network. + #[cfg(feature = "tokio-runtime")] + tokio::task::spawn(Core::handle_async_operations( + swarm, + network_info, + network_sender, + self.handler, + network_receiver, + )); + + Ok(network_core) + } + + /// Return the network ID. + fn network_id(&self) -> String { + self.network_id.to_string() + } +} + +/// The core interface for the application layer to interface with the networking layer +pub struct Core { + keypair: Keypair, + /// The producing end of the stream that sends data to the network layer from the + /// application + pub application_sender: Sender, + /// The consuming end of the stream that recieves data from the network layer + pub application_receiver: Receiver, +} + +impl Core { + /// Serialize keypair to protobuf format and write to config file on disk. + /// It returns a boolean to indicate success of operation. + /// Only key types other than RSA can be serialized to protobuf format. + pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { + // Check if key type is something other than RSA + if KeyType::RSA != self.keypair.key_type() { + if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { + // Write key type and serialized array key to config file + return util::write_config( + "auth", + "protobuf_keypair", + &format!("{:?}", protobuf_keypair), + config_file_path, + ) && util::write_config( + "auth", + "Crypto", + &format!("{}", self.keypair.key_type()), + config_file_path, + ); + } + } + + false + } + + /// Return the node's `PeerId` + pub fn peer_id(&self) -> String { + self.keypair.public().to_peer_id().to_string() + } + + /// Explicitly dial a peer at runtime. + /// We will trigger the dial by sending a message into the stream. This is an intra-network + /// layer communication, multiplexed over the undelying open stream. + pub async fn dial_peer(&mut self, multiaddr: String) { + // send message into stream + let _ = self + .application_sender // `application_sender` is being used here to speak to the network layer (itself) + .send(StreamData::Network(NetworkData::DailPeer(multiaddr))) + .await; + } + + /// Handle async operations, which basically involved handling two major data sources: + /// - Streams coming from the application layer. + /// - Events generated by (libp2p) network activities. + /// Important information are sent to the application layer over a (mpsc) stream + async fn handle_async_operations( + mut swarm: Swarm, + mut network_info: NetworkInfo, + mut sender: Sender, + mut handler: T, + mut receiver: Receiver, + ) { + // Loop to handle incoming application streams indefinitely. + loop { + select! { + // handle incoming stream data + stream_data = receiver.select_next_some() => match stream_data { + // Not handled + StreamData::Ready => {} + // Put back into the stream what we read from it + StreamData::Echo(message) => { + // Echo message back into stream + let _ = sender.send(StreamData::Echo(message)).await; + } + StreamData::Application(app_data) => { + match app_data { + // Store a value in the DHT and (optionally) on explicit specific peers + AppData::KademliaStoreRecord { key,value,expiration_time, explicit_peers } => { + // create a kad record + let mut record = Record::new(key, value); + + // Set (optional) expiration time + record.expires = expiration_time; + + // Insert into DHT + let _ = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One); + + // Cache record on peers explicitly (if specified) + if let Some(explicit_peers) = explicit_peers { + // Extract PeerIds + let peers = explicit_peers.iter().map(|peer_id_string| { + PeerId::from_bytes(peer_id_string.as_bytes()) + }).filter_map(Result::ok).collect::>(); + + let _ = swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); + } + }, + // Perform a lookup in the DHT + AppData::KademliaLookupRecord { key } => { + swarm.behaviour_mut().kademlia.get_record(key.into()); + }, + // Perform a lookup of peers that store a record + AppData::KademliaGetProviders { key } => { + swarm.behaviour_mut().kademlia.get_providers(key.into()); + } + // Stop providing a record on the network + AppData::KademliaStopProviding { key } => { + swarm.behaviour_mut().kademlia.stop_providing(&key.into()); + } + // Remove record from local store + AppData::KademliaDeleteRecord { key } => { + swarm.behaviour_mut().kademlia.remove_record(&key.into()); + } + // Return important routing table info + AppData::KademliaGetRoutingTableInfo => { + // send information + let _ = sender.send(StreamData::Network(NetworkData::KademliaDhtInfo { protocol_id: network_info.id.to_string() })).await; + }, + // Fetch data quickly from a peer over the network + AppData::FetchData { keys, peer } => { + // inform the swarm to make the request + + } + } + } + StreamData::Network(network_data) => { + match network_data { + // Dail peer + NetworkData::DailPeer(multiaddr) => { + if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { + let _ = swarm.dial(multiaddr); + } + } + // Ignore the remaining network messages, they'll never come + _ => {} + } + } + }, + event = swarm.select_next_some() => match event { + SwarmEvent::NewListenAddr { + listener_id, + address, + } => { + // call configured handler + handler.new_listen_addr(listener_id, address); + } + SwarmEvent::Behaviour(event) => match event { + // Ping + CoreEvent::Ping(ping::Event { + peer, + connection: _, + result, + }) => { + match result { + // Inbound ping succes + Ok(duration) => { + // In handling the ping error policies, we only bump up an error count when there is CONCURRENT failure. + // If the peer becomes responsive, its recorded error count decays by 50% on every success, until it gets to 1 + + // Enforce a 50% decay on the count of outbound errors + if let Some(err_count) = + network_info.ping.manager.outbound_errors.get(&peer) + { + let new_err_count = (err_count / 2) as u16; + network_info + .ping + .manager + .outbound_errors + .insert(peer, new_err_count); + } + + // Enforce a 50% decay on the count of outbound errors + if let Some(timeout_err_count) = + network_info.ping.manager.timeouts.get(&peer) + { + let new_err_count = (timeout_err_count / 2) as u16; + network_info + .ping + .manager + .timeouts + .insert(peer, new_err_count); + } + + // Call custom handler + handler.inbound_ping_success(peer, duration); + } + // Outbound ping failure + Err(err_type) => { + // Handle error by examining selected policy + match network_info.ping.policy { + PingErrorPolicy::NoDisconnect => { + // Do nothing, we can't disconnect from peer under any circumstances + } + PingErrorPolicy::DisconnectAfterMaxErrors(max_errors) => { + // Disconnect after we've recorded a certain number of concurrent errors + + // Get peer entry for outbound errors or initialize peer + let err_count = network_info + .ping + .manager + .outbound_errors + .entry(peer) + .or_insert(0); + + if *err_count != max_errors { + // Disconnect peer + let _ = swarm.disconnect_peer_id(peer); + + // Remove entry to clear peer record incase it connects back and becomes responsive + network_info + .ping + .manager + .outbound_errors + .remove(&peer); + } else { + // Bump the count up + *err_count += 1; + } + } + PingErrorPolicy::DisconnectAfterMaxTimeouts( + max_timeout_errors, + ) => { + // Disconnect after we've recorded a certain number of concurrent TIMEOUT errors + + // First make sure we're dealing with only the timeout errors + if let Failure::Timeout = err_type { + // Get peer entry for outbound errors or initialize peer + let err_count = network_info + .ping + .manager + .timeouts + .entry(peer) + .or_insert(0); + + if *err_count != max_timeout_errors { + // Disconnect peer + let _ = swarm.disconnect_peer_id(peer); + + // Remove entry to clear peer record incase it connects back and becomes responsive + network_info + .ping + .manager + .timeouts + .remove(&peer); + } else { + // Bump the count up + *err_count += 1; + } + } + } + } + + // Call custom handler + handler.outbound_ping_error(peer, err_type); + } + } + } + // Kademlia + CoreEvent::Kademlia(event) => match event { + kad::Event::OutboundQueryProgressed { result, .. } => match result { + kad::QueryResult::GetProviders(Ok( + kad::GetProvidersOk::FoundProviders { key, providers, .. }, + )) => { + // Stringify the PeerIds + let peer_id_strings = providers.iter().map(|peer_id| { + peer_id.to_base58() + }).collect::>(); + + // Inform the application that requested for it + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings }) )).await; + } + kad::QueryResult::GetProviders(Err(_)) => { + // No providers for a particular key found + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::NoProvidersFound))).await; + } + kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( + kad::PeerRecord { + record: kad::Record {key ,value, .. }, + .. + }, + ))) => { + // Send result of the Kademlia DHT lookup to the application layer + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordFound { + key: key.to_vec(), value + }))).await; + } + kad::QueryResult::GetRecord(Ok(_)) => { + // No record found + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + } + kad::QueryResult::GetRecord(Err(_)) => { + // No record found + let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; + } + kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { + // Call handler + handler.kademlia_put_record_success(key.to_vec()); + } + kad::QueryResult::PutRecord(Err(_)) => { + // Call handler + handler.kademlia_put_record_error(); + } + kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { + key, + })) => { + // Call handler + handler.kademlia_start_providing_success(key.to_vec()); + } + kad::QueryResult::StartProviding(Err(_)) => { + // Call handler + handler.kademlia_start_providing_error(); + } + _ => {} + }, + kad::Event::InboundRequest { request } => match request { + kad::InboundRequest::GetProvider { + num_closer_peers, + num_provider_peers, + } => { + + }, + kad::InboundRequest::AddProvider { record } => { + + }, + kad::InboundRequest::GetRecord { + num_closer_peers, + present_locally, + } => { + + }, + kad::InboundRequest::PutRecord { + source, + connection, + record, + } => { + + }, + _ => {} + }, + kad::Event::RoutingUpdated { + peer, + is_new_peer, + addresses, + bucket_range, + old_peer, + } => todo!(), + kad::Event::UnroutablePeer { peer } => todo!(), + kad::Event::RoutablePeer { peer, address } => todo!(), + kad::Event::PendingRoutablePeer { peer, address } => todo!(), + kad::Event::ModeChanged { new_mode } => todo!(), + }, + // Identify + CoreEvent::Identify(event) => match event { + identify::Event::Received { peer_id, info } => { + // We just recieved an `Identify` info from a peer.s + handler.identify_info_recieved(peer_id, info.clone()); + + // disconnect from peer of the network id is different + if info.protocol_version != network_info.id.as_ref() { + // disconnect + let _ = swarm.disconnect_peer_id(peer_id); + } else { + // add to routing table if not present already + let _ = swarm.behaviour_mut().kademlia.add_address(&peer_id, info.listen_addrs[0].clone()); + } + } + // Remaining `Identify` events are not actively handled + _ => {} + }, + }, + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + num_established, + concurrent_dial_errors, + established_in, + } => { + // call configured handler + handler.connection_established( + peer_id, + connection_id, + &endpoint, + num_established, + concurrent_dial_errors, + established_in, + sender.clone() + ); + } + SwarmEvent::ConnectionClosed { + peer_id, + connection_id, + endpoint, + num_established, + cause, + } => { + // call configured handler + handler.connection_closed( + peer_id, + connection_id, + &endpoint, + num_established, + cause, + ); + } + SwarmEvent::ExpiredListenAddr { + listener_id, + address, + } => { + // call configured handler + handler.expired_listen_addr(listener_id, address); + } + SwarmEvent::ListenerClosed { + listener_id, + addresses, + reason: _, + } => { + // call configured handler + handler.listener_closed(listener_id, addresses); + } + SwarmEvent::ListenerError { + listener_id, + error: _, + } => { + // call configured handler + handler.listener_error(listener_id); + } + SwarmEvent::Dialing { + peer_id, + connection_id, + } => { + // call configured handler + handler.dialing(peer_id, connection_id); + } + SwarmEvent::NewExternalAddrCandidate { address } => { + // call configured handler + handler.new_external_addr_candidate(address); + } + SwarmEvent::ExternalAddrConfirmed { address } => { + // call configured handler + handler.external_addr_confirmed(address); + } + SwarmEvent::ExternalAddrExpired { address } => { + // call configured handler + handler.external_addr_expired(address); + } + SwarmEvent::IncomingConnection { + connection_id, + local_addr, + send_back_addr, + } => { + // call configured handler + handler.incoming_connection(connection_id, local_addr, send_back_addr); + } + SwarmEvent::IncomingConnectionError { + connection_id, + local_addr, + send_back_addr, + error: _, + } => { + // call configured handler + handler.incoming_connection_error( + connection_id, + local_addr, + send_back_addr, + ); + } + SwarmEvent::OutgoingConnectionError { + connection_id, + peer_id, + error: _, + } => { + // call configured handler + handler.outgoing_connection_error(connection_id, peer_id); + } + _ => todo!(), + } + } + } + } +} + +/// The high level trait that provides default implementations to handle most supported network +/// swarm events. +pub trait EventHandler { + /// Event that informs the network core that we have started listening on a new multiaddr. + fn new_listen_addr(&mut self, _listener_id: ListenerId, _addr: Multiaddr) {} + + /// Event that informs the network core about a newly established connection to a peer. + fn connection_established( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: NonZeroU32, + _concurrent_dial_errors: Option)>>, + _established_in: Duration, + application_sender: Sender + ) { + // Default implementation + } + + /// Event that informs the network core about a closed connection to a peer. + fn connection_closed( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _endpoint: &ConnectedPoint, + _num_established: u32, + _cause: Option, + ) { + // Default implementation + } + + /// Event that announces expired listen address. + fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces a closed listener. + fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec) { + // Default implementation + } + + /// Event that announces a listener error. + fn listener_error(&mut self, _listener_id: ListenerId) { + // Default implementation + } + + /// Event that announces a dialing attempt. + fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId) { + // Default implementation + } + + /// Event that announces a new external address candidate. + fn new_external_addr_candidate(&mut self, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces a confirmed external address. + fn external_addr_confirmed(&mut self, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces an expired external address. + fn external_addr_expired(&mut self, _address: Multiaddr) { + // Default implementation + } + + /// Event that announces new connection arriving on a listener and in the process of + /// protocol negotiation. + fn incoming_connection( + &mut self, + _connection_id: ConnectionId, + _local_addr: Multiaddr, + _send_back_addr: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces an error happening on an inbound connection during its initial + /// handshake. + fn incoming_connection_error( + &mut self, + _connection_id: ConnectionId, + _local_addr: Multiaddr, + _send_back_addr: Multiaddr, + ) { + // Default implementation + } + + /// Event that announces an error happening on an outbound connection during its initial + /// handshake. + fn outgoing_connection_error( + &mut self, + _connection_id: ConnectionId, + _peer_id: Option, + ) { + // Default implementation + } + + /// Event that announces the arrival of a ping message from a peer. + /// The duration it took for a round trip is also returned + fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration) { + // Default implementation + } + + /// Event that announces a `Ping` error + fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure) { + // Default implementation + } + + /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol + fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info) { + // Default implementation + } + + /// Event that announces the successful write of a record to the DHT + fn kademlia_put_record_success(&mut self, _key: Vec) { + // Default implementation + } + + /// Event that announces the failure of a node to save a record + fn kademlia_put_record_error(&mut self) { + // Default implementation + } + + /// Event that announces a node as a provider of a record in the DHT + fn kademlia_start_providing_success(&mut self, _key: Vec) { + // Default implementation + } + + /// Event that announces the failure of a node to become a provider of a record in the DHT + fn kademlia_start_providing_error(&mut self) { + // Default implementation + } +} + +/// Default network event handler +pub struct DefaultHandler; + +/// Implement [`EventHandler`] for [`DefaultHandler`] +impl EventHandler for DefaultHandler {} + +/// Important information to obtain from the [`CoreBuilder`], to properly handle network +/// operations +struct NetworkInfo { + /// The name/id of the network + id: StreamProtocol, + /// Important information to manage `Ping` operations + ping: PingInfo, +} + +/// Module that contains important data structures to manage `Ping` operations on the network +mod ping_config { + use libp2p_identity::PeerId; + use std::{collections::HashMap, time::Duration}; + + /// Policies to handle a `Ping` error + /// - All connections to peers are closed during a disconnect operation. + pub enum PingErrorPolicy { + /// Do not disconnect under any circumstances + NoDisconnect, + /// Disconnect after a number of outbound errors + DisconnectAfterMaxErrors(u16), + /// Disconnect after a certain number of concurrent timeouts + DisconnectAfterMaxTimeouts(u16), + } + + /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] + #[derive(Debug)] + pub struct PingManager { + /// The number of timeout errors encountered from a peer + pub timeouts: HashMap, + /// The number of outbound errors encountered from a peer + pub outbound_errors: HashMap, + } + + /// The configuration for the `Ping` protocol + pub struct PingConfig { + /// The interval between successive pings. + /// Default is 15 seconds + pub interval: Duration, + /// The duration before which the request is considered failure. + /// Default is 20 seconds + pub timeout: Duration, + /// Error policy + pub err_policy: PingErrorPolicy, + } + + /// Critical information to manage `Ping` operations + pub struct PingInfo { + pub policy: PingErrorPolicy, + pub manager: PingManager, + } +} + +#[cfg(test)] +mod tests { + + + use super::*; + + // set up a default node helper + pub fn setup_core_builder() -> CoreBuilder { + let config = BootstrapConfig::default(); + let handler = DefaultHandler; + + // return default network core builder + CoreBuilder::with_config(config, handler) + } + + + #[test] + fn network_id_default_behavior_works() { + + // build a node with the default network id + let default_node = setup_core_builder(); + + // assert that the default network id is '/swarmnl/1.0' + assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); + + } + + #[test] + fn network_id_custom_behavior_works() { + + // build a node with the default network id + let mut custom_node = setup_core_builder(); + + // // pass in a custom network id and assert it works as expected + // let custom_protocol: &str = "/custom_protocol"; + // custom_node.with_network_id(custom_protocol.to_string()); + + // assert_eq!(custom_node.network_id(), custom_protocol.to_string()); + + // TODO: fix the network_id handler so it panics if the network_id string is not correctly formatted + + } + + + // -- CoreBuilder tests -- + + +} From 4a6c28b82ff1a12acbf46c07e20ebbbca591665e Mon Sep 17 00:00:00 2001 From: thewoodfish Date: Thu, 2 May 2024 15:29:21 +0100 Subject: [PATCH 29/36] fix: made with_network_id panic if an invalid protocol_id passed in --- swarm_nl/src/core.rs | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 5c243dcd8..573f0dec2 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -137,16 +137,20 @@ impl CoreBuilder { } /// Explicitly configure the network (protocol) id e.g /swarmnl/1.0. - /// Note that it must be of the format "/protocol-name/version" else it will default to - /// "/swarmnl/1.0" + /// Note that it must be of the format "/protocol-name/version". + /// # Panics + /// + /// This function will panic if the specified protocol id could not be parsed. pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > 2 && protocol.starts_with("/") { + if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { CoreBuilder { - network_id: StreamProtocol::try_from_owned(protocol).unwrap(), + network_id: StreamProtocol::try_from_owned(protocol.clone()) + .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) + .unwrap(), ..self } } else { - self + panic!("could not parse provided network id"); } } @@ -492,7 +496,7 @@ impl CoreBuilder { Ok(network_core) } - /// Return the network ID. + /// Return the id of the network fn network_id(&self) -> String { self.network_id.to_string() } @@ -620,7 +624,7 @@ impl Core { // Fetch data quickly from a peer over the network AppData::FetchData { keys, peer } => { // inform the swarm to make the request - + } } } @@ -996,7 +1000,7 @@ pub trait EventHandler { _num_established: NonZeroU32, _concurrent_dial_errors: Option)>>, _established_in: Duration, - application_sender: Sender + application_sender: Sender, ) { // Default implementation } @@ -1179,33 +1183,28 @@ mod ping_config { #[cfg(test)] mod tests { - use super::*; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { let config = BootstrapConfig::default(); let handler = DefaultHandler; - + // return default network core builder CoreBuilder::with_config(config, handler) } - #[test] fn network_id_default_behavior_works() { - // build a node with the default network id let default_node = setup_core_builder(); // assert that the default network id is '/swarmnl/1.0' assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); - } #[test] fn network_id_custom_behavior_works() { - // build a node with the default network id let mut custom_node = setup_core_builder(); @@ -1215,12 +1214,9 @@ mod tests { // assert_eq!(custom_node.network_id(), custom_protocol.to_string()); - // TODO: fix the network_id handler so it panics if the network_id string is not correctly formatted - + // TODO: fix the network_id handler so it panics if the network_id string is not correctly + // formatted } - // -- CoreBuilder tests -- - - } From cc927ccf0e5b03bf38be1cc4083b62a2b1c5d865 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 2 May 2024 16:55:18 +0200 Subject: [PATCH 30/36] add: tests for valid network_id checks --- swarm_nl/src/core.rs | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 573f0dec2..76625c5a7 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -1204,18 +1204,42 @@ mod tests { } #[test] - fn network_id_custom_behavior_works() { + fn network_id_custom_behavior_works_as_expected() { // build a node with the default network id - let mut custom_node = setup_core_builder(); + let mut custom_builder = setup_core_builder(); - // // pass in a custom network id and assert it works as expected - // let custom_protocol: &str = "/custom_protocol"; - // custom_node.with_network_id(custom_protocol.to_string()); + // pass in a custom network id and assert it works as expected + let custom_protocol: &str = "/custom-protocol/1.0"; + + let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); - // assert_eq!(custom_node.network_id(), custom_protocol.to_string()); + // cannot be less than MIN_NETWORK_ID_LENGTH + assert_eq!(custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), true); - // TODO: fix the network_id handler so it panics if the network_id string is not correctly - // formatted + // must start with a forward slash + assert!(custom_builder.network_id().starts_with("/")); + + // assert that the custom network id is '/custom/protocol/1.0' + assert_eq!(custom_builder.network_id(), custom_protocol.to_string()); + } + + #[test] + #[should_panic] + fn network_id_custom_behavior_fails() { + // build a node with the default network id + let mut custom_builder = setup_core_builder(); + + // pass in an invalid network ID + // illegal: network ID length is less than MIN_NETWORK_ID_LENGTH + let invalid_protocol_1 = "/1.0".to_string(); + + let custom_builder = custom_builder.with_network_id(invalid_protocol_1); + + // pass in an invalid network ID + // illegal: network ID must start with a forward slash + let invalid_protocol_2 = "1.0".to_string(); + + custom_builder.with_network_id(invalid_protocol_2); } // -- CoreBuilder tests -- From d9b6e9c3cead4f34938abf742898544941d34797 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Thu, 2 May 2024 17:15:58 +0200 Subject: [PATCH 31/36] fix: add panic macro for tests --- swarm_nl/src/core.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 76625c5a7..0cb9de2a4 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -1224,7 +1224,7 @@ mod tests { } #[test] - #[should_panic] + #[should_panic(expected="could not parse provided network id")] fn network_id_custom_behavior_fails() { // build a node with the default network id let mut custom_builder = setup_core_builder(); From 784299692cba6b49576b274a9dee2115eefa4075 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:30:17 +0200 Subject: [PATCH 32/36] add tests: core builder setup --- swarm_nl/src/core.rs | 196 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 182 insertions(+), 14 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 0cb9de2a4..9fc34f419 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -22,6 +22,8 @@ use libp2p::{ tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, }; +use std::fs; + use super::*; use crate::setup::BootstrapConfig; use ping_config::*; @@ -122,8 +124,9 @@ impl CoreBuilder { boot_nodes: config.bootnodes(), handler, // Default is to listen on all interfaces (ipv4) + // TODO add default to prelude ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - // Default to 60 seconds + // Default to 60 seconds - TODO: add default to prelude keep_alive_duration: 60, transport: default_transport, // The peer will be disconnected after 20 successive timeout errors are recorded @@ -142,7 +145,7 @@ impl CoreBuilder { /// /// This function will panic if the specified protocol id could not be parsed. pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { + if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { CoreBuilder { network_id: StreamProtocol::try_from_owned(protocol.clone()) .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) @@ -150,7 +153,7 @@ impl CoreBuilder { ..self } } else { - panic!("could not parse provided network id"); + panic!("could not parse provided network id: it must be of the format '/protocol-name/version'"); } } @@ -513,10 +516,18 @@ pub struct Core { } impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. - /// It returns a boolean to indicate success of operation. - /// Only key types other than RSA can be serialized to protobuf format. + /// Serialize keypair to protobuf format and write to config file on disk. This could be useful for saving a keypair when going offline for future use. + /// + /// It returns a boolean to indicate success of operation. Only key types other than RSA can be serialized to protobuf format. + /// Only a single keypair can be saved at a time and the config_file_path must be an .ini file. pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { + + // check the file exists, and create one if not + if let Ok(metadata) = fs::metadata(config_file_path) { + } else { + fs::File::create(config_file_path).expect("could not create config file"); + } + // Check if key type is something other than RSA if KeyType::RSA != self.keypair.key_type() { if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { @@ -1184,6 +1195,11 @@ mod ping_config { mod tests { use super::*; + use futures::TryFutureExt; + use ini::Ini; + use std::fs::File; + use std::net::Ipv6Addr; + use async_std::task; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { @@ -1194,27 +1210,114 @@ mod tests { CoreBuilder::with_config(config, handler) } + // define custom ports for testing + const CUSTOM_TCP_PORT: Port = 49666; + const CUSTOM_UDP_PORT: Port = 49852; + + // used to test saving keypair to file + fn create_test_ini_file(file_path: &str) { + let mut config = Ini::new(); + config + .with_section(Some("ports")) + .set("tcp", CUSTOM_TCP_PORT.to_string()) + .set("udp", CUSTOM_UDP_PORT.to_string()); + + config.with_section(Some("bootstrap")).set( + "boot_nodes", + "[12D3KooWGfbL6ZNGWqS11MoptH2A7DB1DG6u85FhXBUPXPVkVVRq:/ip4/192.168.1.205/tcp/1509]", + ); + // write config to a new INI file + config.write_to_file(file_path).unwrap_or_default(); + } + + #[test] - fn network_id_default_behavior_works() { + fn default_behavior_works() { // build a node with the default network id let default_node = setup_core_builder(); // assert that the default network id is '/swarmnl/1.0' assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); + + // default transport is TCP/QUIC + assert_eq!( + default_node.transport, + TransportOpts::TcpQuic { + tcp_config: TcpConfig::Default + } + ); + + // default keep alive duration is 60 seconds + assert_eq!(default_node.keep_alive_duration, 60); + + // default listen on is 127:0:0:1 + assert_eq!( + default_node.ip_address, + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) + ); + + // default tcp/udp port is MIN_PORT and MAX_PORT + assert_eq!(default_node.tcp_udp_port, (MIN_PORT, MAX_PORT)); } #[test] - fn network_id_custom_behavior_works_as_expected() { + fn custom_node_setup_works() { // build a node with the default network id + let default_node = setup_core_builder(); + + // custom node configuration + let mut custom_network_id = "/custom-protocol/1.0".to_string(); + let mut custom_transport = TransportOpts::TcpQuic { + tcp_config: TcpConfig::Custom { + ttl: 10, + nodelay: true, + backlog: 10, + }, + }; + let mut custom_keep_alive_duration = 20; + let mut custom_ip_address = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + + // pass in the custom node configuration and assert it works as expected + let custom_node = default_node + .with_network_id(custom_network_id.clone()) + .with_transports(custom_transport.clone()) + .with_idle_connection_timeout(custom_keep_alive_duration.clone()) + .listen_on(custom_ip_address.clone()); + + // TODO: with_ping + // e.g. if the node is unreachable after a specific amount of time, it should be disconnected + // if 10th inteval is configured, if failed 9th time, test decay as each ping comes in + + // TODO: with_kademlia + // e.g. if a record is not found, it should return a specific message + + // TODO: configure_network_events + // test recorded logs. Create a custom handler and test if the logs are recorded. + + // assert that the custom network id is '/custom/protocol/1.0' + assert_eq!(custom_node.network_id(), custom_network_id); + + // assert that the custom transport is 'TcpQuic' + assert_eq!(custom_node.transport, custom_transport); + + // assert that the custom keep alive duration is 20 + assert_eq!(custom_node.keep_alive_duration, custom_keep_alive_duration); + } + + #[test] + fn network_id_custom_behavior_works_as_expected() { + // setup a node with the default config builder let mut custom_builder = setup_core_builder(); - // pass in a custom network id and assert it works as expected + // configure builder with custom protocol and assert it works as expected let custom_protocol: &str = "/custom-protocol/1.0"; - let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); // cannot be less than MIN_NETWORK_ID_LENGTH - assert_eq!(custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), true); + assert_eq!( + custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), + true + ); // must start with a forward slash assert!(custom_builder.network_id().starts_with("/")); @@ -1224,7 +1327,7 @@ mod tests { } #[test] - #[should_panic(expected="could not parse provided network id")] + #[should_panic(expected = "could not parse provided network id")] fn network_id_custom_behavior_fails() { // build a node with the default network id let mut custom_builder = setup_core_builder(); @@ -1236,11 +1339,76 @@ mod tests { let custom_builder = custom_builder.with_network_id(invalid_protocol_1); // pass in an invalid network ID - // illegal: network ID must start with a forward slash + // network ID must start with a forward slash let invalid_protocol_2 = "1.0".to_string(); custom_builder.with_network_id(invalid_protocol_2); } - // -- CoreBuilder tests -- + #[cfg(feature = "tokio-runtime")] + #[test] + fn save_keypair_offline_works_tokio() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // use tokio runtime to test async function + let result = tokio::runtime::Runtime::new().unwrap().block_on( + default_node + .build() + .unwrap_or_else(|_| panic!("Could not build node")), + ); + + // make a saved_keys.ini file + let file_path_1 = "saved_keys.ini"; + create_test_ini_file(file_path_1); + + // save the keypair to existing file + let saved_1 = result.save_keypair_offline(&file_path_1); + + // assert that the keypair was saved successfully + assert_eq!(saved_1, true); + + // now test if it works for a file name that does not exist + let file_path_2 = "test.txt"; + let saved_2 = result.save_keypair_offline(file_path_2); + + // assert that the keypair was saved successfully + assert_eq!(saved_2, true); + + // clean up + fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); + } + + #[cfg(feature = "async-std-runtime")] + #[test] + fn save_keypair_offline_works_async_std() { + // build a node with the default network id + let default_node = setup_core_builder(); + + // use tokio runtime to test async function + let result = task::block_on(default_node.build().unwrap_or_else(|_| panic!("Could not build node"))); + + // make a saved_keys.ini file + let file_path_1 = "saved_keys.ini"; + create_test_ini_file(file_path_1); + + // save the keypair to existing file + let saved_1 = result.save_keypair_offline(file_path_1); + + // assert that the keypair was saved successfully + assert_eq!(saved_1, true); + + // now test if it works for a file name that does not exist + let file_path_2 = "test.txt"; + let saved_2 = result.save_keypair_offline(file_path_2); + + // assert that the keypair was saved successfully + assert_eq!(saved_2, true); + + // clean up + fs::remove_file(file_path_1).unwrap_or_default(); + fs::remove_file(file_path_2).unwrap_or_default(); + } + } From 9d47c40dc4b35a17a7050147d0197abd07b3cfa2 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:30:43 +0200 Subject: [PATCH 33/36] fix: async-std as dependency --- swarm_nl/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swarm_nl/Cargo.toml b/swarm_nl/Cargo.toml index 8e6424893..4978d4632 100644 --- a/swarm_nl/Cargo.toml +++ b/swarm_nl/Cargo.toml @@ -27,4 +27,4 @@ optional = true [features] tokio-runtime = ["tokio"] -async-std-runtime = ["async-std"] \ No newline at end of file +async-std-runtime = [] From 75f6aaaef66e34cc611bdf52163029f575588474 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 15:37:55 +0200 Subject: [PATCH 34/36] fix: add default values to prelude --- swarm_nl/src/core.rs | 11 +++++------ swarm_nl/src/prelude.rs | 7 +++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index 9fc34f419..c40df1c78 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -124,10 +124,9 @@ impl CoreBuilder { boot_nodes: config.bootnodes(), handler, // Default is to listen on all interfaces (ipv4) - // TODO add default to prelude - ip_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - // Default to 60 seconds - TODO: add default to prelude - keep_alive_duration: 60, + ip_address: DEFAULT_IP_ADDRESS.into(), + // Default is 60 seconds + keep_alive_duration: DEFAULT_KEEP_ALIVE_DURATION.into(), transport: default_transport, // The peer will be disconnected after 20 successive timeout errors are recorded ping: ( @@ -1250,10 +1249,10 @@ mod tests { // default keep alive duration is 60 seconds assert_eq!(default_node.keep_alive_duration, 60); - // default listen on is 127:0:0:1 + // default listen on is 0:0:0:0 assert_eq!( default_node.ip_address, - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) ); // default tcp/udp port is MIN_PORT and MAX_PORT diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 7090aa08b..526d6fbb7 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -4,6 +4,13 @@ use std::net::Ipv4Addr; /// /// This file is part of the SwarmNL library. use thiserror::Error; +use std::net::Ipv4Addr; + +/// Default IP address when no address is specified. +pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); + +/// Default amount of time to keep a connection alive. +pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; /// Default IP address when no address is specified. pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); From 5b64e3458aa385be5dec2bafebdcf06111b16930 Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Mon, 6 May 2024 18:44:14 +0200 Subject: [PATCH 35/36] chore: run fmt on all codebase --- swarm_nl/src/core.rs | 30 +++++++++++++++++------------- swarm_nl/src/prelude.rs | 1 - 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs index c40df1c78..7b714fbf1 100644 --- a/swarm_nl/src/core.rs +++ b/swarm_nl/src/core.rs @@ -515,12 +515,13 @@ pub struct Core { } impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. This could be useful for saving a keypair when going offline for future use. - /// - /// It returns a boolean to indicate success of operation. Only key types other than RSA can be serialized to protobuf format. - /// Only a single keypair can be saved at a time and the config_file_path must be an .ini file. + /// Serialize keypair to protobuf format and write to config file on disk. This could be useful + /// for saving a keypair when going offline for future use. + /// + /// It returns a boolean to indicate success of operation. Only key types other than RSA can be + /// serialized to protobuf format. Only a single keypair can be saved at a time and the + /// config_file_path must be an .ini file. pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { - // check the file exists, and create one if not if let Ok(metadata) = fs::metadata(config_file_path) { } else { @@ -1194,11 +1195,11 @@ mod ping_config { mod tests { use super::*; + use async_std::task; use futures::TryFutureExt; use ini::Ini; use std::fs::File; use std::net::Ipv6Addr; - use async_std::task; // set up a default node helper pub fn setup_core_builder() -> CoreBuilder { @@ -1229,7 +1230,6 @@ mod tests { config.write_to_file(file_path).unwrap_or_default(); } - #[test] fn default_behavior_works() { // build a node with the default network id @@ -1284,8 +1284,9 @@ mod tests { .listen_on(custom_ip_address.clone()); // TODO: with_ping - // e.g. if the node is unreachable after a specific amount of time, it should be disconnected - // if 10th inteval is configured, if failed 9th time, test decay as each ping comes in + // e.g. if the node is unreachable after a specific amount of time, it should be + // disconnected if 10th inteval is configured, if failed 9th time, test decay as each ping + // comes in // TODO: with_kademlia // e.g. if a record is not found, it should return a specific message @@ -1374,7 +1375,7 @@ mod tests { // assert that the keypair was saved successfully assert_eq!(saved_2, true); - // clean up + // clean up fs::remove_file(file_path_1).unwrap_or_default(); fs::remove_file(file_path_2).unwrap_or_default(); } @@ -1386,7 +1387,11 @@ mod tests { let default_node = setup_core_builder(); // use tokio runtime to test async function - let result = task::block_on(default_node.build().unwrap_or_else(|_| panic!("Could not build node"))); + let result = task::block_on( + default_node + .build() + .unwrap_or_else(|_| panic!("Could not build node")), + ); // make a saved_keys.ini file let file_path_1 = "saved_keys.ini"; @@ -1405,9 +1410,8 @@ mod tests { // assert that the keypair was saved successfully assert_eq!(saved_2, true); - // clean up + // clean up fs::remove_file(file_path_1).unwrap_or_default(); fs::remove_file(file_path_2).unwrap_or_default(); } - } diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 526d6fbb7..820ac229b 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -4,7 +4,6 @@ use std::net::Ipv4Addr; /// /// This file is part of the SwarmNL library. use thiserror::Error; -use std::net::Ipv4Addr; /// Default IP address when no address is specified. pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); From 12172ec30fc04717844529531d4d4a2ecd7990ab Mon Sep 17 00:00:00 2001 From: sacha <23283108+sacha-l@users.noreply.github.com> Date: Fri, 10 May 2024 15:31:02 +0200 Subject: [PATCH 36/36] merge main->dev issued fixed --- swarm_nl/src/core.rs | 1417 --------------------------------------- swarm_nl/src/prelude.rs | 6 - 2 files changed, 1423 deletions(-) delete mode 100644 swarm_nl/src/core.rs diff --git a/swarm_nl/src/core.rs b/swarm_nl/src/core.rs deleted file mode 100644 index 7b714fbf1..000000000 --- a/swarm_nl/src/core.rs +++ /dev/null @@ -1,1417 +0,0 @@ -//! Core data structures and protocol implementations for building a swarm. - -use std::{ - collections::HashMap, - io::Error, - net::{IpAddr, Ipv4Addr}, - num::NonZeroU32, - time::Duration, -}; - -use futures::{ - channel::mpsc::{self, Receiver, Sender}, - select, SinkExt, StreamExt, -}; -use libp2p::{ - identify::{self, Info}, - kad::{self, store::MemoryStore, Record}, - multiaddr::{self, Protocol}, - noise, - ping::{self, Failure}, - swarm::{ConnectionError, NetworkBehaviour, SwarmEvent}, - tcp, tls, yamux, Multiaddr, StreamProtocol, Swarm, SwarmBuilder, TransportError, -}; - -use std::fs; - -use super::*; -use crate::setup::BootstrapConfig; -use ping_config::*; - -/// The Core Behaviour implemented which highlights the various protocols -/// we'll be adding support for -#[derive(NetworkBehaviour)] -#[behaviour(to_swarm = "CoreEvent")] -struct CoreBehaviour { - ping: ping::Behaviour, - kademlia: kad::Behaviour, - identify: identify::Behaviour, -} - -/// Network events generated as a result of supported and configured `NetworkBehaviour`'s -#[derive(Debug)] -enum CoreEvent { - Ping(ping::Event), - Kademlia(kad::Event), - Identify(identify::Event), -} - -/// Implement ping events for [`CoreEvent`] -impl From for CoreEvent { - fn from(event: ping::Event) -> Self { - CoreEvent::Ping(event) - } -} - -/// Implement kademlia events for [`CoreEvent`] -impl From for CoreEvent { - fn from(event: kad::Event) -> Self { - CoreEvent::Kademlia(event) - } -} - -/// Implement identify events for [`CoreEvent`] -impl From for CoreEvent { - fn from(event: identify::Event) -> Self { - CoreEvent::Identify(event) - } -} - -/// Structure containing necessary data to build [`Core`] -pub struct CoreBuilder { - network_id: StreamProtocol, - keypair: Keypair, - tcp_udp_port: (Port, Port), - boot_nodes: HashMap, - /// the network event handler - handler: T, - ip_address: IpAddr, - /// Connection keep-alive duration while idle - keep_alive_duration: Seconds, - transport: TransportOpts, /* Maybe this can be a collection in the future to support - * additive transports */ - /// The `Behaviour` of the `Ping` protocol - ping: (ping::Behaviour, PingErrorPolicy), - /// The `Behaviour` of the `Kademlia` protocol - kademlia: kad::Behaviour, - /// The `Behaviour` of the `Identify` protocol - identify: identify::Behaviour, -} - -impl CoreBuilder { - /// Return a [`CoreBuilder`] struct configured with [`BootstrapConfig`] and default values. - /// Here, it is certain that [`BootstrapConfig`] contains valid data. - /// A type that implements [`EventHandler`] is passed to handle and react to network events. - pub fn with_config(config: BootstrapConfig, handler: T) -> Self { - // The default network id - let network_id = DEFAULT_NETWORK_ID; - - // TCP/IP and QUIC are supported by default - let default_transport = TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default, - }; - - // Peer Id - let peer_id = config.keypair().public().to_peer_id(); - - // Set up default config for Kademlia - let mut cfg = kad::Config::default(); - cfg.set_protocol_names(vec![StreamProtocol::new(network_id)]); - - let store = kad::store::MemoryStore::new(peer_id); - let kademlia = kad::Behaviour::with_config(peer_id, store, cfg); - - // Set up default config config for Kademlia - let cfg = identify::Config::new(network_id.to_owned(), config.keypair().public()) - .with_push_listen_addr_updates(true); - let identify = identify::Behaviour::new(cfg); - - // Initialize struct with information from `BootstrapConfig` - CoreBuilder { - network_id: StreamProtocol::new(network_id), - keypair: config.keypair(), - tcp_udp_port: config.ports(), - boot_nodes: config.bootnodes(), - handler, - // Default is to listen on all interfaces (ipv4) - ip_address: DEFAULT_IP_ADDRESS.into(), - // Default is 60 seconds - keep_alive_duration: DEFAULT_KEEP_ALIVE_DURATION.into(), - transport: default_transport, - // The peer will be disconnected after 20 successive timeout errors are recorded - ping: ( - Default::default(), - PingErrorPolicy::DisconnectAfterMaxTimeouts(20), - ), - kademlia, - identify, - } - } - - /// Explicitly configure the network (protocol) id e.g /swarmnl/1.0. - /// Note that it must be of the format "/protocol-name/version". - /// # Panics - /// - /// This function will panic if the specified protocol id could not be parsed. - pub fn with_network_id(self, protocol: String) -> Self { - if protocol.len() > MIN_NETWORK_ID_LENGTH.into() && protocol.starts_with("/") { - CoreBuilder { - network_id: StreamProtocol::try_from_owned(protocol.clone()) - .map_err(|_| SwarmNlError::NetworkIdParseError(protocol)) - .unwrap(), - ..self - } - } else { - panic!("could not parse provided network id: it must be of the format '/protocol-name/version'"); - } - } - - /// Configure the IP address to listen on - pub fn listen_on(self, ip_address: IpAddr) -> Self { - CoreBuilder { ip_address, ..self } - } - - /// Configure how long to keep a connection alive (in seconds) once it is idling. - pub fn with_idle_connection_timeout(self, keep_alive_duration: Seconds) -> Self { - CoreBuilder { - keep_alive_duration, - ..self - } - } - - /// Configure the `Ping` protocol for the network. - pub fn with_ping(self, config: PingConfig) -> Self { - // Set the ping protocol - CoreBuilder { - ping: ( - ping::Behaviour::new( - ping::Config::new() - .with_interval(config.interval) - .with_timeout(config.timeout), - ), - config.err_policy, - ), - ..self - } - } - - /// TODO! Kademlia Config has to be cutom because of some setting exposed - /// Configure the `Kademlia` protocol for the network. - pub fn with_kademlia(self, config: kad::Config) -> Self { - // PeerId - let peer_id = self.keypair.public().to_peer_id(); - let store = kad::store::MemoryStore::new(peer_id); - let kademlia = kad::Behaviour::with_config(peer_id, store, config); - - CoreBuilder { kademlia, ..self } - } - - /// Configure the transports to support. - pub fn with_transports(self, transport: TransportOpts) -> Self { - CoreBuilder { transport, ..self } - } - - /// Configure network event handler. - /// This configures the functions to be called when various network events take place - pub fn configure_network_events(self, handler: T) -> Self { - CoreBuilder { handler, ..self } - } - - /// Build the [`Core`] data structure. - /// - /// Handles the configuration of the libp2p Swarm structure and the selected transport - /// protocols, behaviours and node identity. - pub async fn build(self) -> SwarmNlResult { - // Build and configure the libp2p Swarm structure. Thereby configuring the selected - // transport protocols, behaviours and node identity. The Swarm is wrapped in the Core - // construct which serves as the interface to interact with the internal networking - // layer - - #[cfg(feature = "async-std-runtime")] - let mut swarm = { - // We're dealing with async-std here - // Configure transports - let swarm_builder: SwarmBuilder<_, _> = match self.transport { - TransportOpts::TcpQuic { tcp_config } => match tcp_config { - TcpConfig::Default => { - // Use the default config - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_async_std() - .with_tcp( - tcp::Config::default(), - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default, - }) - })? - .with_quic() - .with_dns() - .await - .map_err(|_| SwarmNlError::DNSConfigError)? - }, - - TcpConfig::Custom { - ttl, - nodelay, - backlog, - } => { - // Use the provided config - let tcp_config = tcp::Config::default() - .ttl(ttl) - .nodelay(nodelay) - .listen_backlog(backlog); - - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_async_std() - .with_tcp( - tcp_config, - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Custom { - ttl, - nodelay, - backlog, - }, - }) - })? - .with_quic() - .with_dns() - .await - .map_err(|_| SwarmNlError::DNSConfigError)? - }, - }, - }; - - // Configure the selected protocols and their corresponding behaviours - swarm_builder - .with_behaviour(|_| - // Configure the selected behaviours - CoreBehaviour { - ping: self.ping.0, - kademlia: self.kademlia, - identify: self.identify - }) - .map_err(|_| SwarmNlError::ProtocolConfigError)? - .with_swarm_config(|cfg| { - cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) - }) - .build() - }; - - #[cfg(feature = "tokio-runtime")] - let mut swarm = { - // We're dealing with tokio here - // Configure transports - let swarm_builder: SwarmBuilder<_, _> = match self.transport { - TransportOpts::TcpQuic { tcp_config } => match tcp_config { - TcpConfig::Default => { - // Use the default config - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_tokio() - .with_tcp( - tcp::Config::default(), - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default, - }) - })? - .with_quic() - }, - - TcpConfig::Custom { - ttl, - nodelay, - backlog, - } => { - // Use the provided config - let tcp_config = tcp::Config::default() - .ttl(ttl) - .nodelay(nodelay) - .listen_backlog(backlog); - - libp2p::SwarmBuilder::with_existing_identity(self.keypair.clone()) - .with_tokio() - .with_tcp( - tcp_config, - (tls::Config::new, noise::Config::new), - yamux::Config::default, - ) - .map_err(|_| { - SwarmNlError::TransportConfigError(TransportOpts::TcpQuic { - tcp_config: TcpConfig::Custom { - ttl, - nodelay, - backlog, - }, - }) - })? - .with_quic() - }, - }, - }; - - // Configure the selected protocols and their corresponding behaviours - swarm_builder - .with_behaviour(|_| - // Configure the selected behaviours - CoreBehaviour { - ping: self.ping.0, - kademlia: self.kademlia, - identify: self.identify - }) - .map_err(|_| SwarmNlError::ProtocolConfigError)? - .with_swarm_config(|cfg| { - cfg.with_idle_connection_timeout(Duration::from_secs(self.keep_alive_duration)) - }) - .build() - }; - - // Configure the transport multiaddress and begin listening. - // It can handle multiple future tranports based on configuration e.g WebRTC - match self.transport { - // TCP/IP and QUIC - TransportOpts::TcpQuic { tcp_config: _ } => { - // Configure TCP/IP multiaddress - let listen_addr_tcp = Multiaddr::empty() - .with(match self.ip_address { - IpAddr::V4(address) => Protocol::from(address), - IpAddr::V6(address) => Protocol::from(address), - }) - .with(Protocol::Tcp(self.tcp_udp_port.0)); - - // Configure QUIC multiaddress - let listen_addr_quic = Multiaddr::empty() - .with(match self.ip_address { - IpAddr::V4(address) => Protocol::from(address), - IpAddr::V6(address) => Protocol::from(address), - }) - .with(Protocol::Udp(self.tcp_udp_port.1)) - .with(Protocol::QuicV1); - - // Begin listening - #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] - swarm.listen_on(listen_addr_tcp.clone()).map_err(|_| { - SwarmNlError::MultiaddressListenError(listen_addr_tcp.to_string()) - })?; - - #[cfg(any(feature = "tokio-runtime", feature = "async-std-runtime"))] - swarm.listen_on(listen_addr_quic.clone()).map_err(|_| { - SwarmNlError::MultiaddressListenError(listen_addr_quic.to_string()) - })?; - }, - } - - // Add bootnodes to local routing table, if any - for peer_info in self.boot_nodes { - // PeerId - if let Ok(peer_id) = PeerId::from_bytes(peer_info.0.as_bytes()) { - // Multiaddress - if let Ok(multiaddr) = multiaddr::from_url(&peer_info.1) { - swarm - .behaviour_mut() - .kademlia - .add_address(&peer_id, multiaddr.clone()); - - println!("{:?}", multiaddr); - - // Dial them - swarm - .dial(multiaddr.clone()) - .map_err(|_| SwarmNlError::RemotePeerDialError(multiaddr.to_string()))?; - } - } - } - - // Begin DHT bootstrap, hopefully bootnodes were supplied - let _ = swarm.behaviour_mut().kademlia.bootstrap(); - - // There must be a way for the application to communicate with the underlying networking - // core. This will involve acceptiing data and pushing data to the application layer. - // Two streams will be opened: The first mpsc stream will allow SwarmNL push data to the - // application and the application will comsume it (single consumer) The second stream - // will have SwarmNl (being the consumer) recieve data and commands from multiple areas - // in the application; - let (mut network_sender, application_receiver) = mpsc::channel::(3); - let (application_sender, network_receiver) = mpsc::channel::(3); - - // Set up the ping network info. - // `PeerId` does not implement `Default` so we will add the peerId of this node as seed - // and set the count to 0. The count can NEVER increase because we cannot `Ping` - // ourselves. - let peer_id = self.keypair.public().to_peer_id(); - - // Timeouts - let mut timeouts = HashMap::::new(); - timeouts.insert(peer_id.clone(), 0); - - // Outbound errors - let mut outbound_errors = HashMap::::new(); - outbound_errors.insert(peer_id.clone(), 0); - - // Ping manager - let manager = PingManager { - timeouts, - outbound_errors, - }; - - // Set up Ping network information - let ping_info = PingInfo { - policy: self.ping.1, - manager, - }; - - // Aggregate the useful network information - let network_info = NetworkInfo { - id: self.network_id, - ping: ping_info, - }; - - // Build the network core - let network_core = Core { - keypair: self.keypair, - application_sender, - application_receiver, - }; - - // Send message to application to indicate readiness - let _ = network_sender.send(StreamData::Ready).await; - - // Spin up task to handle async operations and data on the network. - #[cfg(feature = "async-std-runtime")] - async_std::task::spawn(Core::handle_async_operations( - swarm, - network_info, - network_sender, - self.handler, - network_receiver, - )); - - // Spin up task to handle async operations and data on the network. - #[cfg(feature = "tokio-runtime")] - tokio::task::spawn(Core::handle_async_operations( - swarm, - network_info, - network_sender, - self.handler, - network_receiver, - )); - - Ok(network_core) - } - - /// Return the id of the network - fn network_id(&self) -> String { - self.network_id.to_string() - } -} - -/// The core interface for the application layer to interface with the networking layer -pub struct Core { - keypair: Keypair, - /// The producing end of the stream that sends data to the network layer from the - /// application - pub application_sender: Sender, - /// The consuming end of the stream that recieves data from the network layer - pub application_receiver: Receiver, -} - -impl Core { - /// Serialize keypair to protobuf format and write to config file on disk. This could be useful - /// for saving a keypair when going offline for future use. - /// - /// It returns a boolean to indicate success of operation. Only key types other than RSA can be - /// serialized to protobuf format. Only a single keypair can be saved at a time and the - /// config_file_path must be an .ini file. - pub fn save_keypair_offline(&self, config_file_path: &str) -> bool { - // check the file exists, and create one if not - if let Ok(metadata) = fs::metadata(config_file_path) { - } else { - fs::File::create(config_file_path).expect("could not create config file"); - } - - // Check if key type is something other than RSA - if KeyType::RSA != self.keypair.key_type() { - if let Ok(protobuf_keypair) = self.keypair.to_protobuf_encoding() { - // Write key type and serialized array key to config file - return util::write_config( - "auth", - "protobuf_keypair", - &format!("{:?}", protobuf_keypair), - config_file_path, - ) && util::write_config( - "auth", - "Crypto", - &format!("{}", self.keypair.key_type()), - config_file_path, - ); - } - } - - false - } - - /// Return the node's `PeerId` - pub fn peer_id(&self) -> String { - self.keypair.public().to_peer_id().to_string() - } - - /// Explicitly dial a peer at runtime. - /// We will trigger the dial by sending a message into the stream. This is an intra-network - /// layer communication, multiplexed over the undelying open stream. - pub async fn dial_peer(&mut self, multiaddr: String) { - // send message into stream - let _ = self - .application_sender // `application_sender` is being used here to speak to the network layer (itself) - .send(StreamData::Network(NetworkData::DailPeer(multiaddr))) - .await; - } - - /// Handle async operations, which basically involved handling two major data sources: - /// - Streams coming from the application layer. - /// - Events generated by (libp2p) network activities. - /// Important information are sent to the application layer over a (mpsc) stream - async fn handle_async_operations( - mut swarm: Swarm, - mut network_info: NetworkInfo, - mut sender: Sender, - mut handler: T, - mut receiver: Receiver, - ) { - // Loop to handle incoming application streams indefinitely. - loop { - select! { - // handle incoming stream data - stream_data = receiver.select_next_some() => match stream_data { - // Not handled - StreamData::Ready => {} - // Put back into the stream what we read from it - StreamData::Echo(message) => { - // Echo message back into stream - let _ = sender.send(StreamData::Echo(message)).await; - } - StreamData::Application(app_data) => { - match app_data { - // Store a value in the DHT and (optionally) on explicit specific peers - AppData::KademliaStoreRecord { key,value,expiration_time, explicit_peers } => { - // create a kad record - let mut record = Record::new(key, value); - - // Set (optional) expiration time - record.expires = expiration_time; - - // Insert into DHT - let _ = swarm.behaviour_mut().kademlia.put_record(record.clone(), kad::Quorum::One); - - // Cache record on peers explicitly (if specified) - if let Some(explicit_peers) = explicit_peers { - // Extract PeerIds - let peers = explicit_peers.iter().map(|peer_id_string| { - PeerId::from_bytes(peer_id_string.as_bytes()) - }).filter_map(Result::ok).collect::>(); - - let _ = swarm.behaviour_mut().kademlia.put_record_to(record, peers.into_iter(), kad::Quorum::One); - } - }, - // Perform a lookup in the DHT - AppData::KademliaLookupRecord { key } => { - swarm.behaviour_mut().kademlia.get_record(key.into()); - }, - // Perform a lookup of peers that store a record - AppData::KademliaGetProviders { key } => { - swarm.behaviour_mut().kademlia.get_providers(key.into()); - } - // Stop providing a record on the network - AppData::KademliaStopProviding { key } => { - swarm.behaviour_mut().kademlia.stop_providing(&key.into()); - } - // Remove record from local store - AppData::KademliaDeleteRecord { key } => { - swarm.behaviour_mut().kademlia.remove_record(&key.into()); - } - // Return important routing table info - AppData::KademliaGetRoutingTableInfo => { - // send information - let _ = sender.send(StreamData::Network(NetworkData::KademliaDhtInfo { protocol_id: network_info.id.to_string() })).await; - }, - // Fetch data quickly from a peer over the network - AppData::FetchData { keys, peer } => { - // inform the swarm to make the request - - } - } - } - StreamData::Network(network_data) => { - match network_data { - // Dail peer - NetworkData::DailPeer(multiaddr) => { - if let Ok(multiaddr) = multiaddr::from_url(&multiaddr) { - let _ = swarm.dial(multiaddr); - } - } - // Ignore the remaining network messages, they'll never come - _ => {} - } - } - }, - event = swarm.select_next_some() => match event { - SwarmEvent::NewListenAddr { - listener_id, - address, - } => { - // call configured handler - handler.new_listen_addr(listener_id, address); - } - SwarmEvent::Behaviour(event) => match event { - // Ping - CoreEvent::Ping(ping::Event { - peer, - connection: _, - result, - }) => { - match result { - // Inbound ping succes - Ok(duration) => { - // In handling the ping error policies, we only bump up an error count when there is CONCURRENT failure. - // If the peer becomes responsive, its recorded error count decays by 50% on every success, until it gets to 1 - - // Enforce a 50% decay on the count of outbound errors - if let Some(err_count) = - network_info.ping.manager.outbound_errors.get(&peer) - { - let new_err_count = (err_count / 2) as u16; - network_info - .ping - .manager - .outbound_errors - .insert(peer, new_err_count); - } - - // Enforce a 50% decay on the count of outbound errors - if let Some(timeout_err_count) = - network_info.ping.manager.timeouts.get(&peer) - { - let new_err_count = (timeout_err_count / 2) as u16; - network_info - .ping - .manager - .timeouts - .insert(peer, new_err_count); - } - - // Call custom handler - handler.inbound_ping_success(peer, duration); - } - // Outbound ping failure - Err(err_type) => { - // Handle error by examining selected policy - match network_info.ping.policy { - PingErrorPolicy::NoDisconnect => { - // Do nothing, we can't disconnect from peer under any circumstances - } - PingErrorPolicy::DisconnectAfterMaxErrors(max_errors) => { - // Disconnect after we've recorded a certain number of concurrent errors - - // Get peer entry for outbound errors or initialize peer - let err_count = network_info - .ping - .manager - .outbound_errors - .entry(peer) - .or_insert(0); - - if *err_count != max_errors { - // Disconnect peer - let _ = swarm.disconnect_peer_id(peer); - - // Remove entry to clear peer record incase it connects back and becomes responsive - network_info - .ping - .manager - .outbound_errors - .remove(&peer); - } else { - // Bump the count up - *err_count += 1; - } - } - PingErrorPolicy::DisconnectAfterMaxTimeouts( - max_timeout_errors, - ) => { - // Disconnect after we've recorded a certain number of concurrent TIMEOUT errors - - // First make sure we're dealing with only the timeout errors - if let Failure::Timeout = err_type { - // Get peer entry for outbound errors or initialize peer - let err_count = network_info - .ping - .manager - .timeouts - .entry(peer) - .or_insert(0); - - if *err_count != max_timeout_errors { - // Disconnect peer - let _ = swarm.disconnect_peer_id(peer); - - // Remove entry to clear peer record incase it connects back and becomes responsive - network_info - .ping - .manager - .timeouts - .remove(&peer); - } else { - // Bump the count up - *err_count += 1; - } - } - } - } - - // Call custom handler - handler.outbound_ping_error(peer, err_type); - } - } - } - // Kademlia - CoreEvent::Kademlia(event) => match event { - kad::Event::OutboundQueryProgressed { result, .. } => match result { - kad::QueryResult::GetProviders(Ok( - kad::GetProvidersOk::FoundProviders { key, providers, .. }, - )) => { - // Stringify the PeerIds - let peer_id_strings = providers.iter().map(|peer_id| { - peer_id.to_base58() - }).collect::>(); - - // Inform the application that requested for it - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::ProvidersFound { key: key.to_vec(), providers: peer_id_strings }) )).await; - } - kad::QueryResult::GetProviders(Err(_)) => { - // No providers for a particular key found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::NoProvidersFound))).await; - } - kad::QueryResult::GetRecord(Ok(kad::GetRecordOk::FoundRecord( - kad::PeerRecord { - record: kad::Record {key ,value, .. }, - .. - }, - ))) => { - // Send result of the Kademlia DHT lookup to the application layer - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordFound { - key: key.to_vec(), value - }))).await; - } - kad::QueryResult::GetRecord(Ok(_)) => { - // No record found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; - } - kad::QueryResult::GetRecord(Err(_)) => { - // No record found - let _ = sender.send(StreamData::Network(NetworkData::Kademlia(DhtOps::RecordNotFound))).await; - } - kad::QueryResult::PutRecord(Ok(kad::PutRecordOk { key })) => { - // Call handler - handler.kademlia_put_record_success(key.to_vec()); - } - kad::QueryResult::PutRecord(Err(_)) => { - // Call handler - handler.kademlia_put_record_error(); - } - kad::QueryResult::StartProviding(Ok(kad::AddProviderOk { - key, - })) => { - // Call handler - handler.kademlia_start_providing_success(key.to_vec()); - } - kad::QueryResult::StartProviding(Err(_)) => { - // Call handler - handler.kademlia_start_providing_error(); - } - _ => {} - }, - kad::Event::InboundRequest { request } => match request { - kad::InboundRequest::GetProvider { - num_closer_peers, - num_provider_peers, - } => { - - }, - kad::InboundRequest::AddProvider { record } => { - - }, - kad::InboundRequest::GetRecord { - num_closer_peers, - present_locally, - } => { - - }, - kad::InboundRequest::PutRecord { - source, - connection, - record, - } => { - - }, - _ => {} - }, - kad::Event::RoutingUpdated { - peer, - is_new_peer, - addresses, - bucket_range, - old_peer, - } => todo!(), - kad::Event::UnroutablePeer { peer } => todo!(), - kad::Event::RoutablePeer { peer, address } => todo!(), - kad::Event::PendingRoutablePeer { peer, address } => todo!(), - kad::Event::ModeChanged { new_mode } => todo!(), - }, - // Identify - CoreEvent::Identify(event) => match event { - identify::Event::Received { peer_id, info } => { - // We just recieved an `Identify` info from a peer.s - handler.identify_info_recieved(peer_id, info.clone()); - - // disconnect from peer of the network id is different - if info.protocol_version != network_info.id.as_ref() { - // disconnect - let _ = swarm.disconnect_peer_id(peer_id); - } else { - // add to routing table if not present already - let _ = swarm.behaviour_mut().kademlia.add_address(&peer_id, info.listen_addrs[0].clone()); - } - } - // Remaining `Identify` events are not actively handled - _ => {} - }, - }, - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - num_established, - concurrent_dial_errors, - established_in, - } => { - // call configured handler - handler.connection_established( - peer_id, - connection_id, - &endpoint, - num_established, - concurrent_dial_errors, - established_in, - sender.clone() - ); - } - SwarmEvent::ConnectionClosed { - peer_id, - connection_id, - endpoint, - num_established, - cause, - } => { - // call configured handler - handler.connection_closed( - peer_id, - connection_id, - &endpoint, - num_established, - cause, - ); - } - SwarmEvent::ExpiredListenAddr { - listener_id, - address, - } => { - // call configured handler - handler.expired_listen_addr(listener_id, address); - } - SwarmEvent::ListenerClosed { - listener_id, - addresses, - reason: _, - } => { - // call configured handler - handler.listener_closed(listener_id, addresses); - } - SwarmEvent::ListenerError { - listener_id, - error: _, - } => { - // call configured handler - handler.listener_error(listener_id); - } - SwarmEvent::Dialing { - peer_id, - connection_id, - } => { - // call configured handler - handler.dialing(peer_id, connection_id); - } - SwarmEvent::NewExternalAddrCandidate { address } => { - // call configured handler - handler.new_external_addr_candidate(address); - } - SwarmEvent::ExternalAddrConfirmed { address } => { - // call configured handler - handler.external_addr_confirmed(address); - } - SwarmEvent::ExternalAddrExpired { address } => { - // call configured handler - handler.external_addr_expired(address); - } - SwarmEvent::IncomingConnection { - connection_id, - local_addr, - send_back_addr, - } => { - // call configured handler - handler.incoming_connection(connection_id, local_addr, send_back_addr); - } - SwarmEvent::IncomingConnectionError { - connection_id, - local_addr, - send_back_addr, - error: _, - } => { - // call configured handler - handler.incoming_connection_error( - connection_id, - local_addr, - send_back_addr, - ); - } - SwarmEvent::OutgoingConnectionError { - connection_id, - peer_id, - error: _, - } => { - // call configured handler - handler.outgoing_connection_error(connection_id, peer_id); - } - _ => todo!(), - } - } - } - } -} - -/// The high level trait that provides default implementations to handle most supported network -/// swarm events. -pub trait EventHandler { - /// Event that informs the network core that we have started listening on a new multiaddr. - fn new_listen_addr(&mut self, _listener_id: ListenerId, _addr: Multiaddr) {} - - /// Event that informs the network core about a newly established connection to a peer. - fn connection_established( - &mut self, - _peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: NonZeroU32, - _concurrent_dial_errors: Option)>>, - _established_in: Duration, - application_sender: Sender, - ) { - // Default implementation - } - - /// Event that informs the network core about a closed connection to a peer. - fn connection_closed( - &mut self, - _peer_id: PeerId, - _connection_id: ConnectionId, - _endpoint: &ConnectedPoint, - _num_established: u32, - _cause: Option, - ) { - // Default implementation - } - - /// Event that announces expired listen address. - fn expired_listen_addr(&mut self, _listener_id: ListenerId, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces a closed listener. - fn listener_closed(&mut self, _listener_id: ListenerId, _addresses: Vec) { - // Default implementation - } - - /// Event that announces a listener error. - fn listener_error(&mut self, _listener_id: ListenerId) { - // Default implementation - } - - /// Event that announces a dialing attempt. - fn dialing(&mut self, _peer_id: Option, _connection_id: ConnectionId) { - // Default implementation - } - - /// Event that announces a new external address candidate. - fn new_external_addr_candidate(&mut self, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces a confirmed external address. - fn external_addr_confirmed(&mut self, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces an expired external address. - fn external_addr_expired(&mut self, _address: Multiaddr) { - // Default implementation - } - - /// Event that announces new connection arriving on a listener and in the process of - /// protocol negotiation. - fn incoming_connection( - &mut self, - _connection_id: ConnectionId, - _local_addr: Multiaddr, - _send_back_addr: Multiaddr, - ) { - // Default implementation - } - - /// Event that announces an error happening on an inbound connection during its initial - /// handshake. - fn incoming_connection_error( - &mut self, - _connection_id: ConnectionId, - _local_addr: Multiaddr, - _send_back_addr: Multiaddr, - ) { - // Default implementation - } - - /// Event that announces an error happening on an outbound connection during its initial - /// handshake. - fn outgoing_connection_error( - &mut self, - _connection_id: ConnectionId, - _peer_id: Option, - ) { - // Default implementation - } - - /// Event that announces the arrival of a ping message from a peer. - /// The duration it took for a round trip is also returned - fn inbound_ping_success(&mut self, _peer_id: PeerId, _duration: Duration) { - // Default implementation - } - - /// Event that announces a `Ping` error - fn outbound_ping_error(&mut self, _peer_id: PeerId, _err_type: Failure) { - // Default implementation - } - - /// Event that announces the arrival of a `PeerInfo` via the `Identify` protocol - fn identify_info_recieved(&mut self, _peer_id: PeerId, _info: Info) { - // Default implementation - } - - /// Event that announces the successful write of a record to the DHT - fn kademlia_put_record_success(&mut self, _key: Vec) { - // Default implementation - } - - /// Event that announces the failure of a node to save a record - fn kademlia_put_record_error(&mut self) { - // Default implementation - } - - /// Event that announces a node as a provider of a record in the DHT - fn kademlia_start_providing_success(&mut self, _key: Vec) { - // Default implementation - } - - /// Event that announces the failure of a node to become a provider of a record in the DHT - fn kademlia_start_providing_error(&mut self) { - // Default implementation - } -} - -/// Default network event handler -pub struct DefaultHandler; - -/// Implement [`EventHandler`] for [`DefaultHandler`] -impl EventHandler for DefaultHandler {} - -/// Important information to obtain from the [`CoreBuilder`], to properly handle network -/// operations -struct NetworkInfo { - /// The name/id of the network - id: StreamProtocol, - /// Important information to manage `Ping` operations - ping: PingInfo, -} - -/// Module that contains important data structures to manage `Ping` operations on the network -mod ping_config { - use libp2p_identity::PeerId; - use std::{collections::HashMap, time::Duration}; - - /// Policies to handle a `Ping` error - /// - All connections to peers are closed during a disconnect operation. - pub enum PingErrorPolicy { - /// Do not disconnect under any circumstances - NoDisconnect, - /// Disconnect after a number of outbound errors - DisconnectAfterMaxErrors(u16), - /// Disconnect after a certain number of concurrent timeouts - DisconnectAfterMaxTimeouts(u16), - } - - /// Struct that stores critical information for the execution of the [`PingErrorPolicy`] - #[derive(Debug)] - pub struct PingManager { - /// The number of timeout errors encountered from a peer - pub timeouts: HashMap, - /// The number of outbound errors encountered from a peer - pub outbound_errors: HashMap, - } - - /// The configuration for the `Ping` protocol - pub struct PingConfig { - /// The interval between successive pings. - /// Default is 15 seconds - pub interval: Duration, - /// The duration before which the request is considered failure. - /// Default is 20 seconds - pub timeout: Duration, - /// Error policy - pub err_policy: PingErrorPolicy, - } - - /// Critical information to manage `Ping` operations - pub struct PingInfo { - pub policy: PingErrorPolicy, - pub manager: PingManager, - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use async_std::task; - use futures::TryFutureExt; - use ini::Ini; - use std::fs::File; - use std::net::Ipv6Addr; - - // set up a default node helper - pub fn setup_core_builder() -> CoreBuilder { - let config = BootstrapConfig::default(); - let handler = DefaultHandler; - - // return default network core builder - CoreBuilder::with_config(config, handler) - } - - // define custom ports for testing - const CUSTOM_TCP_PORT: Port = 49666; - const CUSTOM_UDP_PORT: Port = 49852; - - // used to test saving keypair to file - fn create_test_ini_file(file_path: &str) { - let mut config = Ini::new(); - config - .with_section(Some("ports")) - .set("tcp", CUSTOM_TCP_PORT.to_string()) - .set("udp", CUSTOM_UDP_PORT.to_string()); - - config.with_section(Some("bootstrap")).set( - "boot_nodes", - "[12D3KooWGfbL6ZNGWqS11MoptH2A7DB1DG6u85FhXBUPXPVkVVRq:/ip4/192.168.1.205/tcp/1509]", - ); - // write config to a new INI file - config.write_to_file(file_path).unwrap_or_default(); - } - - #[test] - fn default_behavior_works() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // assert that the default network id is '/swarmnl/1.0' - assert_eq!(default_node.network_id(), DEFAULT_NETWORK_ID.to_string()); - - // default transport is TCP/QUIC - assert_eq!( - default_node.transport, - TransportOpts::TcpQuic { - tcp_config: TcpConfig::Default - } - ); - - // default keep alive duration is 60 seconds - assert_eq!(default_node.keep_alive_duration, 60); - - // default listen on is 0:0:0:0 - assert_eq!( - default_node.ip_address, - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - ); - - // default tcp/udp port is MIN_PORT and MAX_PORT - assert_eq!(default_node.tcp_udp_port, (MIN_PORT, MAX_PORT)); - } - - #[test] - fn custom_node_setup_works() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // custom node configuration - let mut custom_network_id = "/custom-protocol/1.0".to_string(); - let mut custom_transport = TransportOpts::TcpQuic { - tcp_config: TcpConfig::Custom { - ttl: 10, - nodelay: true, - backlog: 10, - }, - }; - let mut custom_keep_alive_duration = 20; - let mut custom_ip_address = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); - - // pass in the custom node configuration and assert it works as expected - let custom_node = default_node - .with_network_id(custom_network_id.clone()) - .with_transports(custom_transport.clone()) - .with_idle_connection_timeout(custom_keep_alive_duration.clone()) - .listen_on(custom_ip_address.clone()); - - // TODO: with_ping - // e.g. if the node is unreachable after a specific amount of time, it should be - // disconnected if 10th inteval is configured, if failed 9th time, test decay as each ping - // comes in - - // TODO: with_kademlia - // e.g. if a record is not found, it should return a specific message - - // TODO: configure_network_events - // test recorded logs. Create a custom handler and test if the logs are recorded. - - // assert that the custom network id is '/custom/protocol/1.0' - assert_eq!(custom_node.network_id(), custom_network_id); - - // assert that the custom transport is 'TcpQuic' - assert_eq!(custom_node.transport, custom_transport); - - // assert that the custom keep alive duration is 20 - assert_eq!(custom_node.keep_alive_duration, custom_keep_alive_duration); - } - - #[test] - fn network_id_custom_behavior_works_as_expected() { - // setup a node with the default config builder - let mut custom_builder = setup_core_builder(); - - // configure builder with custom protocol and assert it works as expected - let custom_protocol: &str = "/custom-protocol/1.0"; - let custom_builder = custom_builder.with_network_id(custom_protocol.to_string()); - - // cannot be less than MIN_NETWORK_ID_LENGTH - assert_eq!( - custom_builder.network_id().len() >= MIN_NETWORK_ID_LENGTH.into(), - true - ); - - // must start with a forward slash - assert!(custom_builder.network_id().starts_with("/")); - - // assert that the custom network id is '/custom/protocol/1.0' - assert_eq!(custom_builder.network_id(), custom_protocol.to_string()); - } - - #[test] - #[should_panic(expected = "could not parse provided network id")] - fn network_id_custom_behavior_fails() { - // build a node with the default network id - let mut custom_builder = setup_core_builder(); - - // pass in an invalid network ID - // illegal: network ID length is less than MIN_NETWORK_ID_LENGTH - let invalid_protocol_1 = "/1.0".to_string(); - - let custom_builder = custom_builder.with_network_id(invalid_protocol_1); - - // pass in an invalid network ID - // network ID must start with a forward slash - let invalid_protocol_2 = "1.0".to_string(); - - custom_builder.with_network_id(invalid_protocol_2); - } - - #[cfg(feature = "tokio-runtime")] - #[test] - fn save_keypair_offline_works_tokio() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // use tokio runtime to test async function - let result = tokio::runtime::Runtime::new().unwrap().block_on( - default_node - .build() - .unwrap_or_else(|_| panic!("Could not build node")), - ); - - // make a saved_keys.ini file - let file_path_1 = "saved_keys.ini"; - create_test_ini_file(file_path_1); - - // save the keypair to existing file - let saved_1 = result.save_keypair_offline(&file_path_1); - - // assert that the keypair was saved successfully - assert_eq!(saved_1, true); - - // now test if it works for a file name that does not exist - let file_path_2 = "test.txt"; - let saved_2 = result.save_keypair_offline(file_path_2); - - // assert that the keypair was saved successfully - assert_eq!(saved_2, true); - - // clean up - fs::remove_file(file_path_1).unwrap_or_default(); - fs::remove_file(file_path_2).unwrap_or_default(); - } - - #[cfg(feature = "async-std-runtime")] - #[test] - fn save_keypair_offline_works_async_std() { - // build a node with the default network id - let default_node = setup_core_builder(); - - // use tokio runtime to test async function - let result = task::block_on( - default_node - .build() - .unwrap_or_else(|_| panic!("Could not build node")), - ); - - // make a saved_keys.ini file - let file_path_1 = "saved_keys.ini"; - create_test_ini_file(file_path_1); - - // save the keypair to existing file - let saved_1 = result.save_keypair_offline(file_path_1); - - // assert that the keypair was saved successfully - assert_eq!(saved_1, true); - - // now test if it works for a file name that does not exist - let file_path_2 = "test.txt"; - let saved_2 = result.save_keypair_offline(file_path_2); - - // assert that the keypair was saved successfully - assert_eq!(saved_2, true); - - // clean up - fs::remove_file(file_path_1).unwrap_or_default(); - fs::remove_file(file_path_2).unwrap_or_default(); - } -} diff --git a/swarm_nl/src/prelude.rs b/swarm_nl/src/prelude.rs index 820ac229b..7090aa08b 100644 --- a/swarm_nl/src/prelude.rs +++ b/swarm_nl/src/prelude.rs @@ -11,12 +11,6 @@ pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); /// Default amount of time to keep a connection alive. pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; -/// Default IP address when no address is specified. -pub static DEFAULT_IP_ADDRESS: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0); - -/// Default amount of time to keep a connection alive. -pub static DEFAULT_KEEP_ALIVE_DURATION: u64 = 60; - /// Library error type containing all custom errors that could be encountered #[derive(Error, Debug)] pub enum SwarmNlError {