From 5bce6edff97e884822e667cafe10b8d7c96dc6d5 Mon Sep 17 00:00:00 2001 From: Andrew Mackenzie Date: Wed, 26 Oct 2022 02:43:14 +0200 Subject: [PATCH] examples/gossipsub-chat: Add mDNS peer discovery (#2996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Oliveira Co-authored-by: Max Inden --- examples/README.md | 2 +- examples/gossipsub-chat.rs | 166 ++++++++++++++++++------------------- 2 files changed, 83 insertions(+), 85 deletions(-) diff --git a/examples/README.md b/examples/README.md index 13edf1aaa76..e791ebb803a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,7 +17,7 @@ A set of examples showcasing how to use rust-libp2p. - [Gossipsub chat](./gossipsub-chat.rs) - Same as the chat example but using the Gossipsub protocol. + Same as the chat example but using mDNS and the Gossipsub protocol. - [Tokio based chat](./chat-tokio.rs) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index 807209324f6..1bf9ce27e27 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -18,42 +18,46 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -//! A basic chat application with logs demonstrating libp2p and the gossipsub protocol. +//! A basic chat application with logs demonstrating libp2p and the gossipsub protocol +//! combined with mDNS for the discovery of peers to gossip with. //! -//! Using two terminal windows, start two instances. Type a message in either terminal and hit return: the -//! message is sent and printed in the other terminal. Close with Ctrl-c. -//! -//! You can of course open more terminal windows and add more participants. -//! Dialing any of the other peers will propagate the new participant to all -//! chat members and everyone will receive all messages. -//! -//! In order to get the nodes to connect, take note of the listening addresses of the first -//! instance and start the second with one of the addresses as the first argument. In the first -//! terminal window, run: +//! Using two terminal windows, start two instances, typing the following in each: //! //! ```sh //! cargo run --example gossipsub-chat --features=full //! ``` //! -//! It will print the [`PeerId`] and the listening addresses, e.g. `Listening on -//! "/ip4/0.0.0.0/tcp/24915"` -//! -//! In the second terminal window, start a new instance of the example with: +//! Mutual mDNS discovery may take a few seconds. When each peer does discover the other +//! it will print a message like: //! //! ```sh -//! cargo run --example gossipsub-chat --features=full -- /ip4/127.0.0.1/tcp/24915 +//! mDNS discovered a new peer: {peerId} //! ``` //! -//! The two nodes should then connect. +//! Type a message and hit return: the message is sent and printed in the other terminal. +//! Close with Ctrl-c. +//! +//! You can open more terminal windows and add more peers using the same line above. +//! +//! Once an additional peer is mDNS discovered it can participate in the conversation +//! and all peers will receive messages sent from it. +//! +//! If a participant exits (Control-C or otherwise) the other peers will receive an mDNS expired +//! event and remove the expired peer from the list of known peers. use async_std::io; -use env_logger::{Builder, Env}; use futures::{prelude::*, select}; use libp2p::gossipsub::MessageId; use libp2p::gossipsub::{ - GossipsubEvent, GossipsubMessage, IdentTopic as Topic, MessageAuthenticity, ValidationMode, + Gossipsub, GossipsubEvent, GossipsubMessage, IdentTopic as Topic, MessageAuthenticity, + ValidationMode, +}; +use libp2p::{ + gossipsub, identity, + mdns::{Mdns, MdnsConfig, MdnsEvent}, + swarm::SwarmEvent, + NetworkBehaviour, PeerId, Swarm, }; -use libp2p::{gossipsub, identity, swarm::SwarmEvent, Multiaddr, PeerId}; use std::collections::hash_map::DefaultHasher; use std::error::Error; use std::hash::{Hash, Hasher}; @@ -61,98 +65,92 @@ use std::time::Duration; #[async_std::main] async fn main() -> Result<(), Box> { - Builder::from_env(Env::default().default_filter_or("info")).init(); - // Create a random PeerId let local_key = identity::Keypair::generate_ed25519(); let local_peer_id = PeerId::from(local_key.public()); - println!("Local peer id: {:?}", local_peer_id); + println!("Local peer id: {}", local_peer_id); - // Set up an encrypted TCP Transport over the Mplex and Yamux protocols + // Set up an encrypted DNS-enabled TCP Transport over the Mplex protocol. let transport = libp2p::development_transport(local_key.clone()).await?; - // Create a Gossipsub topic - let topic = Topic::new("test-net"); + // We create a custom network behaviour that combines Gossipsub and Mdns. + #[derive(NetworkBehaviour)] + struct MyBehaviour { + gossipsub: Gossipsub, + mdns: Mdns, + } - // Create a Swarm to manage peers and events - let mut swarm = { - // To content-address message, we can take the hash of message and use it as an ID. - let message_id_fn = |message: &GossipsubMessage| { - let mut s = DefaultHasher::new(); - message.data.hash(&mut s); - MessageId::from(s.finish().to_string()) - }; + // To content-address message, we can take the hash of message and use it as an ID. + let message_id_fn = |message: &GossipsubMessage| { + let mut s = DefaultHasher::new(); + message.data.hash(&mut s); + MessageId::from(s.finish().to_string()) + }; - // Set a custom gossipsub - let gossipsub_config = gossipsub::GossipsubConfigBuilder::default() - .heartbeat_interval(Duration::from_secs(10)) // This is set to aid debugging by not cluttering the log space - .validation_mode(ValidationMode::Strict) // This sets the kind of message validation. The default is Strict (enforce message signing) - .message_id_fn(message_id_fn) // content-address messages. No two messages of the - // same content will be propagated. - .build() - .expect("Valid config"); - // build a gossipsub network behaviour - let mut gossipsub: gossipsub::Gossipsub = - gossipsub::Gossipsub::new(MessageAuthenticity::Signed(local_key), gossipsub_config) - .expect("Correct configuration"); + // Set a custom gossipsub configuration + let gossipsub_config = gossipsub::GossipsubConfigBuilder::default() + .heartbeat_interval(Duration::from_secs(10)) // This is set to aid debugging by not cluttering the log space + .validation_mode(ValidationMode::Strict) // This sets the kind of message validation. The default is Strict (enforce message signing) + .message_id_fn(message_id_fn) // content-address messages. No two messages of the same content will be propagated. + .build() + .expect("Valid config"); - // subscribes to our topic - gossipsub.subscribe(&topic).unwrap(); + // build a gossipsub network behaviour + let mut gossipsub = Gossipsub::new(MessageAuthenticity::Signed(local_key), gossipsub_config) + .expect("Correct configuration"); - // add an explicit peer if one was provided - if let Some(explicit) = std::env::args().nth(2) { - match explicit.parse() { - Ok(id) => gossipsub.add_explicit_peer(&id), - Err(err) => println!("Failed to parse explicit peer id: {:?}", err), - } - } - - // build the swarm - libp2p::Swarm::new(transport, gossipsub, local_peer_id) - }; + // Create a Gossipsub topic + let topic = Topic::new("test-net"); - // Listen on all interfaces and whatever port the OS assigns - swarm - .listen_on("/ip4/0.0.0.0/tcp/0".parse().unwrap()) - .unwrap(); + // subscribes to our topic + gossipsub.subscribe(&topic)?; - // Reach out to another node if specified - if let Some(to_dial) = std::env::args().nth(1) { - let address: Multiaddr = to_dial.parse().expect("User to provide valid address."); - match swarm.dial(address.clone()) { - Ok(_) => println!("Dialed {:?}", address), - Err(e) => println!("Dial {:?} failed: {:?}", address, e), - }; - } + // Create a Swarm to manage peers and events + let mut swarm = { + let mdns = Mdns::new(MdnsConfig::default())?; + let behaviour = MyBehaviour { gossipsub, mdns }; + Swarm::new(transport, behaviour, local_peer_id) + }; // Read full lines from stdin let mut stdin = io::BufReader::new(io::stdin()).lines().fuse(); + // Listen on all interfaces and whatever port the OS assigns + swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; + + println!("Enter messages via STDIN and they will be sent to connected peers using Gossipsub"); + // Kick it off loop { select! { line = stdin.select_next_some() => { if let Err(e) = swarm - .behaviour_mut() - .publish(topic.clone(), line.expect("Stdin not to close").as_bytes()) - { + .behaviour_mut().gossipsub + .publish(topic.clone(), line.expect("Stdin not to close").as_bytes()) { println!("Publish error: {:?}", e); } }, event = swarm.select_next_some() => match event { - SwarmEvent::Behaviour(GossipsubEvent::Message { + SwarmEvent::Behaviour(MyBehaviourEvent::Mdns(MdnsEvent::Discovered(list))) => { + for (peer_id, _multiaddr) in list { + println!("mDNS discovered a new peer: {}", peer_id); + swarm.behaviour_mut().gossipsub.add_explicit_peer(&peer_id); + } + }, + SwarmEvent::Behaviour(MyBehaviourEvent::Mdns(MdnsEvent::Expired(list))) => { + for (peer_id, _multiaddr) in list { + println!("mDNS discover peer has expired: {}", peer_id); + swarm.behaviour_mut().gossipsub.remove_explicit_peer(&peer_id); + } + }, + SwarmEvent::Behaviour(MyBehaviourEvent::Gossipsub(GossipsubEvent::Message { propagation_source: peer_id, message_id: id, message, - }) => println!( - "Got message: {} with id: {} from peer: {:?}", - String::from_utf8_lossy(&message.data), - id, - peer_id - ), - SwarmEvent::NewListenAddr { address, .. } => { - println!("Listening on {:?}", address); - } + })) => println!( + "Got message: '{}' with id: {id} from peer: {peer_id}", + String::from_utf8_lossy(&message.data), + ), _ => {} } }