From aeba5238b045ceecd75605cede40f86abc753a11 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 19 May 2020 17:35:06 +1000 Subject: [PATCH 01/35] Gossipsub message signing --- examples/gossipsub-chat.rs | 16 +- protocols/gossipsub/Cargo.toml | 1 + protocols/gossipsub/src/behaviour.rs | 239 +++++++++++---------- protocols/gossipsub/src/behaviour/tests.rs | 26 +-- protocols/gossipsub/src/config.rs | 68 +++++- protocols/gossipsub/src/handler.rs | 14 +- protocols/gossipsub/src/mcache.rs | 15 +- protocols/gossipsub/src/protocol.rs | 196 ++++++++++++++++- protocols/gossipsub/src/rpc.proto | 2 + protocols/gossipsub/tests/smoke.rs | 11 +- 10 files changed, 421 insertions(+), 167 deletions(-) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index b7ef6fafffe..41cf7a40fc3 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -51,14 +51,14 @@ use env_logger::{Builder, Env}; use futures::prelude::*; use libp2p::gossipsub::protocol::MessageId; use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, Topic}; -use libp2p::{ - gossipsub, identity, - PeerId, -}; +use libp2p::{gossipsub, identity, PeerId}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::time::Duration; -use std::{error::Error, task::{Context, Poll}}; +use std::{ + error::Error, + task::{Context, Poll}, +}; fn main() -> Result<(), Box> { Builder::from_env(Env::default().default_filter_or("info")).init(); @@ -69,7 +69,7 @@ fn main() -> Result<(), Box> { println!("Local peer id: {:?}", local_peer_id); // Set up an encrypted TCP Transport over the Mplex and Yamux protocols - let transport = libp2p::build_development_transport(local_key)?; + let transport = libp2p::build_development_transport(local_key.clone())?; // Create a Gossipsub topic let topic = Topic::new("test-net".into()); @@ -90,10 +90,12 @@ fn main() -> Result<(), Box> { let gossipsub_config = gossipsub::GossipsubConfigBuilder::new() .heartbeat_interval(Duration::from_secs(10)) .message_id_fn(message_id_fn) // content-address messages. No two messages of the + .allow_unsigned_messages(false) + .sign_messages(true) //same content will be propagated. .build(); // build a gossipsub network behaviour - let mut gossipsub = gossipsub::Gossipsub::new(local_peer_id.clone(), gossipsub_config); + let mut gossipsub = gossipsub::Gossipsub::new(local_key, gossipsub_config); gossipsub.subscribe(topic.clone()); libp2p::Swarm::new(transport, gossipsub, local_peer_id) }; diff --git a/protocols/gossipsub/Cargo.toml b/protocols/gossipsub/Cargo.toml index 8d91104d327..c0ce5184af1 100644 --- a/protocols/gossipsub/Cargo.toml +++ b/protocols/gossipsub/Cargo.toml @@ -33,6 +33,7 @@ env_logger = "0.7.1" libp2p-plaintext = { version = "0.19.0", path = "../plaintext" } libp2p-yamux = { version = "0.19.0", path = "../../muxers/yamux" } quickcheck = "0.9.2" +hex = "0.4.2" [build-dependencies] prost-build = "0.6" diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 63611df9820..5aa7c9efe03 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -27,16 +27,11 @@ use crate::protocol::{ }; use crate::topic::{Topic, TopicHash}; use futures::prelude::*; -use libp2p_core::{Multiaddr, PeerId, connection::ConnectionId}; +use libp2p_core::{connection::ConnectionId, identity::Keypair, Multiaddr, PeerId}; use libp2p_swarm::{ - NetworkBehaviour, - NetworkBehaviourAction, - NotifyHandler, - PollParameters, - ProtocolsHandler + NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters, ProtocolsHandler, }; use log::{debug, error, info, trace, warn}; -use lru::LruCache; use rand; use rand::{seq::SliceRandom, thread_rng}; use std::{ @@ -62,8 +57,12 @@ pub struct Gossipsub { /// Pools non-urgent control messages between heartbeats. control_pool: HashMap>, - /// Peer id of the local node. Used for the source of the messages that we publish. - local_peer_id: PeerId, + /// The local libp2p keypair, used for message source identification and signing. + keypair: Option, + + /// The peer_id that will be the source of published messages. This can be set to the identity + /// via a config, otherwise will be derived from the libp2p keypair. + message_source_id: PeerId, /// A map of all connected peers - A map of topic hash to a list of gossipsub peer Ids. topic_peers: HashMap>, @@ -83,28 +82,31 @@ pub struct Gossipsub { /// Message cache for the last few heartbeats. mcache: MessageCache, - // We keep track of the messages we received (in the format `string(source ID, seq_no)`) so that - // we don't dispatch the same message twice if we receive it twice on the network. - received: LruCache, - /// Heartbeat interval stream. heartbeat: Interval, } impl Gossipsub { /// Creates a `Gossipsub` struct given a set of parameters specified by `gs_config`. - pub fn new(local_peer_id: PeerId, gs_config: GossipsubConfig) -> Self { - let local_peer_id = if gs_config.no_source_id { + pub fn new(keypair: Keypair, gs_config: GossipsubConfig) -> Self { + let message_source_id = if gs_config.no_source_id { PeerId::from_bytes(crate::config::IDENTITY_SOURCE.to_vec()).expect("Valid peer id") } else { - local_peer_id + keypair.public().into_peer_id() + }; + + let keypair = if gs_config.sign_messages { + Some(keypair) + } else { + None }; Gossipsub { config: gs_config.clone(), events: VecDeque::new(), control_pool: HashMap::new(), - local_peer_id, + keypair, + message_source_id, topic_peers: HashMap::new(), peer_topics: HashMap::new(), mesh: HashMap::new(), @@ -115,7 +117,6 @@ impl Gossipsub { gs_config.history_length, gs_config.message_id_fn, ), - received: LruCache::new(256), // keep track of the last 256 messages heartbeat: Interval::new_at( Instant::now() + gs_config.heartbeat_initial_delay, gs_config.heartbeat_interval, @@ -152,11 +153,12 @@ impl Gossipsub { for peer in peer_list { debug!("Sending SUBSCRIBE to peer: {:?}", peer); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - handler: NotifyHandler::Any, - event: event.clone(), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer.clone(), + handler: NotifyHandler::Any, + event: event.clone(), + }); } } @@ -198,11 +200,12 @@ impl Gossipsub { for peer in peer_list { debug!("Sending UNSUBSCRIBE to peer: {:?}", peer); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - event: event.clone(), - handler: NotifyHandler::Any, - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer.clone(), + event: event.clone(), + handler: NotifyHandler::Any, + }); } } @@ -226,7 +229,7 @@ impl Gossipsub { data: impl Into>, ) { let message = GossipsubMessage { - source: self.local_peer_id.clone(), + source: self.message_source_id.clone(), data: data.into(), // To be interoperable with the go-implementation this is treated as a 64-bit // big-endian uint. @@ -234,14 +237,26 @@ impl Gossipsub { topics: topic.into_iter().map(|t| self.topic_hash(t)).collect(), }; + let msg_id = (self.config.message_id_fn)(&message); + // add published message to our received caches + if self.mcache.put(message.clone()).is_some() { + // this message has already been seen. We don't re-publish messages that have already + // been published on the network + warn!( + "Not publishing a message that has already been published. Msg-id {}", + msg_id + ); + return; + } + debug!( "Publishing message: {:?}", (self.config.message_id_fn)(&message) ); // forward the message to mesh peers - let local_peer_id = self.local_peer_id.clone(); - self.forward_msg(message.clone(), &local_peer_id); + let message_source = &self.message_source_id.clone(); + self.forward_msg(message.clone(), message_source); let mut recipient_peers = HashSet::new(); for topic_hash in &message.topics { @@ -274,11 +289,6 @@ impl Gossipsub { } } - // add published message to our received caches - let msg_id = (self.config.message_id_fn)(&message); - self.mcache.put(message.clone()); - self.received.put(msg_id.clone(), ()); - info!("Published message: {:?}", msg_id); let event = Arc::new(GossipsubRpc { @@ -289,11 +299,12 @@ impl Gossipsub { // Send to peers we know are subscribed to the topic. for peer_id in recipient_peers.iter() { debug!("Sending message to peer: {:?}", peer_id); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer_id.clone(), - event: event.clone(), - handler: NotifyHandler::Any, - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer_id.clone(), + event: event.clone(), + handler: NotifyHandler::Any, + }); } } @@ -431,7 +442,7 @@ impl Gossipsub { } for id in ids { - if !self.received.contains(&id) { + if self.mcache.get(&id).is_none() { // have not seen this message, request it iwant_ids.insert(id); } @@ -470,15 +481,16 @@ impl Gossipsub { debug!("IWANT: Sending cached messages to peer: {:?}", peer_id); // Send the messages to the peer let message_list = cached_messages.into_iter().map(|entry| entry.1).collect(); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer_id.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: message_list, - control_msgs: Vec::new(), - }), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer_id.clone(), + handler: NotifyHandler::Any, + event: Arc::new(GossipsubRpc { + subscriptions: Vec::new(), + messages: message_list, + control_msgs: Vec::new(), + }), + }); } debug!("Completed IWANT handling for peer: {:?}", peer_id); } @@ -518,15 +530,16 @@ impl Gossipsub { "GRAFT: Not subscribed to topics - Sending PRUNE to peer: {:?}", peer_id ); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer_id.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: prune_messages, - }), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer_id.clone(), + handler: NotifyHandler::Any, + event: Arc::new(GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: prune_messages, + }), + }); } debug!("Completed GRAFT handling for peer: {:?}", peer_id); } @@ -555,14 +568,11 @@ impl Gossipsub { "Handling message: {:?} from peer: {:?}", msg_id, propagation_source ); - if self.received.put(msg_id.clone(), ()).is_some() { + if self.mcache.put(msg.clone()).is_some() { debug!("Message already received, ignoring. Message: {:?}", msg_id); return; } - // add to the memcache - self.mcache.put(msg.clone()); - // dispatch the message to the user if self.mesh.keys().any(|t| msg.topics.iter().any(|u| t == u)) { debug!("Sending received message to user"); @@ -862,15 +872,16 @@ impl Gossipsub { grafts.append(&mut prunes); // send the control messages - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: grafts, - }), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer.clone(), + handler: NotifyHandler::Any, + event: Arc::new(GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: grafts, + }), + }); } // handle the remaining prunes @@ -881,15 +892,16 @@ impl Gossipsub { topic_hash: topic_hash.clone(), }) .collect(); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: remaining_prunes, - }), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer.clone(), + handler: NotifyHandler::Any, + event: Arc::new(GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: remaining_prunes, + }), + }); } } @@ -921,11 +933,12 @@ impl Gossipsub { for peer in recipient_peers.iter() { debug!("Sending message: {:?} to peer {:?}", msg_id, peer); - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - event: event.clone(), - handler: NotifyHandler::Any, - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer.clone(), + event: event.clone(), + handler: NotifyHandler::Any, + }); } } debug!("Completed forwarding message"); @@ -984,15 +997,16 @@ impl Gossipsub { /// Takes each control action mapping and turns it into a message fn flush_control_pool(&mut self) { for (peer, controls) in self.control_pool.drain() { - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer, - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: controls, - }), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: peer, + handler: NotifyHandler::Any, + event: Arc::new(GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: controls, + }), + }); } } } @@ -1005,6 +1019,8 @@ impl NetworkBehaviour for Gossipsub { GossipsubHandler::new( self.config.protocol_id.clone(), self.config.max_transmit_size, + self.keypair.clone(), + self.config.allow_unsigned_messages, ) } @@ -1025,15 +1041,16 @@ impl NetworkBehaviour for Gossipsub { if !subscriptions.is_empty() { // send our subscriptions to the peer - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: id.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - messages: Vec::new(), - subscriptions, - control_msgs: Vec::new(), - }), - }); + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id: id.clone(), + handler: NotifyHandler::Any, + event: Arc::new(GossipsubRpc { + messages: Vec::new(), + subscriptions, + control_msgs: Vec::new(), + }), + }); } // For the time being assume all gossipsub peers @@ -1145,16 +1162,22 @@ impl NetworkBehaviour for Gossipsub { // clone send event reference if others references are present match event { NetworkBehaviourAction::NotifyHandler { - peer_id, handler, event: send_event, + peer_id, + handler, + event: send_event, } => match Arc::try_unwrap(send_event) { Ok(event) => { return Poll::Ready(NetworkBehaviourAction::NotifyHandler { - peer_id, event, handler + peer_id, + event, + handler, }); } Err(event) => { return Poll::Ready(NetworkBehaviourAction::NotifyHandler { - peer_id, event: (*event).clone(), handler + peer_id, + event: (*event).clone(), + handler, }); } }, diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index e207315111a..d65b854bae4 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -36,7 +36,7 @@ mod tests { // generate a default GossipsubConfig let gs_config = GossipsubConfig::default(); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(PeerId::random(), gs_config); + let mut gs: Gossipsub = Gossipsub::new(Keypair::generate_secp256k1(), gs_config); let mut topic_hashes = vec![]; @@ -53,10 +53,7 @@ mod tests { for _ in 0..peer_no { let peer = PeerId::random(); peers.push(peer.clone()); - ::inject_connected( - &mut gs, - &peer, - ); + ::inject_connected(&mut gs, &peer); if to_subscribe { gs.handle_received_subscriptions( &topic_hashes @@ -345,10 +342,6 @@ mod tests { gs.mcache.get(&msg_id).is_some(), "Message cache should contain published message" ); - assert!( - gs.received.get(&msg_id).is_some(), - "Received cache should contain published message" - ); } /// Test local node publish to unsubscribed topic @@ -412,10 +405,6 @@ mod tests { gs.mcache.get(&msg_id).is_some(), "Message cache should contain published message" ); - assert!( - gs.received.get(&msg_id).is_some(), - "Received cache should contain published message" - ); } #[test] @@ -558,8 +547,9 @@ mod tests { fn test_get_random_peers() { // generate a default GossipsubConfig let gs_config = GossipsubConfig::default(); + let key = Keypair::generate_secp256k1(); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(PeerId::random(), gs_config); + let mut gs: Gossipsub = Gossipsub::new(key, gs_config); // create a topic and fill it with some peers let topic_hash = Topic::new("Test".into()).no_hash().clone(); @@ -588,10 +578,9 @@ mod tests { let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, { |_| false }); assert!(random_peers.len() == 0, "Expected 0 peers to be returned"); - let random_peers = - Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 10, { - |peer| peers.contains(peer) - }); + let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 10, { + |peer| peers.contains(peer) + }); assert!(random_peers.len() == 10, "Expected 10 peers to be returned"); } @@ -731,7 +720,6 @@ mod tests { build_and_inject_nodes(20, vec![String::from("topic1")], true); let msg_id = MessageId(String::from("known id")); - gs.received.put(msg_id.clone(), ()); let events_before = gs.events.len(); gs.handle_ihave(&peers[7], vec![(topic_hashes[0].clone(), vec![msg_id])]); diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 5a715848c39..accd64ee933 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -73,9 +73,16 @@ pub struct GossipsubConfig { /// When set to `true`, prevents automatic forwarding of all received messages. This setting /// allows a user to validate the messages before propagating them to their peers. If set to /// true, the user must manually call `propagate_message()` on the behaviour to forward message - /// once validated (default is false). + /// once validated (default is `false`). pub manual_propagation: bool, + /// When set to `true` all published messages are signed by the libp2p key (default is `true`). + pub sign_messages: bool, + + /// Determines whether unsigned messages will be accepted. If set to false, unsigned messages + /// will be dropped. Default value is `true`. + pub allow_unsigned_messages: bool, + /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this /// parameter allows the user to address packets arbitrarily. One example is content based @@ -104,6 +111,8 @@ impl Default for GossipsubConfig { hash_topics: false, // default compatibility with floodsub no_source_id: false, manual_propagation: false, + sign_messages: true, + allow_unsigned_messages: true, message_id_fn: |message| { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); @@ -114,6 +123,7 @@ impl Default for GossipsubConfig { } } +/// The builder struct for constructing a gossipsub configuration. pub struct GossipsubConfigBuilder { config: GossipsubConfig, } @@ -132,11 +142,13 @@ impl GossipsubConfigBuilder { GossipsubConfigBuilder::default() } + /// The protocol id to negotiate this protocol (default is `/meshsub/1.0.0`). pub fn protocol_id(&mut self, protocol_id: impl Into>) -> &mut Self { self.config.protocol_id = protocol_id.into(); self } + /// Number of heartbeats to keep in the `memcache` (default is 5). pub fn history_length(&mut self, history_length: usize) -> &mut Self { assert!( history_length >= self.config.history_gossip, @@ -146,6 +158,7 @@ impl GossipsubConfigBuilder { self } + /// Number of past heartbeats to gossip about (default is 3). pub fn history_gossip(&mut self, history_gossip: usize) -> &mut Self { assert!( self.config.history_length >= history_gossip, @@ -155,6 +168,7 @@ impl GossipsubConfigBuilder { self } + /// Target number of peers for the mesh network (D in the spec, default is 6). pub fn mesh_n(&mut self, mesh_n: usize) -> &mut Self { assert!( self.config.mesh_n_low <= mesh_n && mesh_n <= self.config.mesh_n_high, @@ -164,6 +178,7 @@ impl GossipsubConfigBuilder { self } + /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 4). pub fn mesh_n_low(&mut self, mesh_n_low: usize) -> &mut Self { assert!( mesh_n_low <= self.config.mesh_n && self.config.mesh_n <= self.config.mesh_n_high, @@ -173,6 +188,8 @@ impl GossipsubConfigBuilder { self } + /// Maximum number of peers in mesh network before removing some (D_high in the spec, default + /// is 12). pub fn mesh_n_high(&mut self, mesh_n_high: usize) -> &mut Self { assert!( self.config.mesh_n_low <= self.config.mesh_n && self.config.mesh_n <= mesh_n_high, @@ -182,48 +199,84 @@ impl GossipsubConfigBuilder { self } + /// Number of peers to emit gossip to during a heartbeat (D_lazy in the spec, default is 6). pub fn gossip_lazy(&mut self, gossip_lazy: usize) -> &mut Self { self.config.gossip_lazy = gossip_lazy; self } + /// Initial delay in each heartbeat (default is 5 seconds). pub fn heartbeat_initial_delay(&mut self, heartbeat_initial_delay: Duration) -> &mut Self { self.config.heartbeat_initial_delay = heartbeat_initial_delay; self } + + /// Time between each heartbeat (default is 1 second). pub fn heartbeat_interval(&mut self, heartbeat_interval: Duration) -> &mut Self { self.config.heartbeat_interval = heartbeat_interval; self } + + /// Time to live for fanout peers (default is 60 seconds). pub fn fanout_ttl(&mut self, fanout_ttl: Duration) -> &mut Self { self.config.fanout_ttl = fanout_ttl; self } + + /// The maximum byte size for each gossip (default is 2048 bytes). pub fn max_transmit_size(&mut self, max_transmit_size: usize) -> &mut Self { self.config.max_transmit_size = max_transmit_size; self } - pub fn hash_topics(&mut self) -> &mut Self { - self.config.hash_topics = true; + /// Flag determining if gossipsub topics are hashed or sent as plain strings (default is false). + pub fn hash_topics(&mut self, value: bool) -> &mut Self { + self.config.hash_topics = value; + self + } + + /// When set, all published messages will have a 0 source `PeerId` (default is false). + pub fn no_source_id(&mut self, value: bool) -> &mut Self { + self.config.no_source_id = value; + self + } + + /// When set to `true`, prevents automatic forwarding of all received messages. This setting + /// allows a user to validate the messages before propagating them to their peers. If set to + /// true, the user must manually call `propagate_message()` on the behaviour to forward message + /// once validated (default is `false`). + pub fn manual_propagation(&mut self, value: bool) -> &mut Self { + self.config.manual_propagation = value; self } - pub fn no_source_id(&mut self) -> &mut Self { - self.config.no_source_id = true; + /// When set to `true` all published messages are signed by the libp2p key (default is `true`). + pub fn sign_messages(&mut self, value: bool) -> &mut Self { + self.config.sign_messages = value; self } - pub fn manual_propagation(&mut self) -> &mut Self { - self.config.manual_propagation = true; + /// Determines whether unsigned messages will be accepted. If set to false, unsigned messages + /// will be dropped. Default value is `true`. + pub fn allow_unsigned_messages(&mut self, value: bool) -> &mut Self { + self.config.allow_unsigned_messages = value; self } + /// A user-defined function allowing the user to specify the message id of a gossipsub message. + /// The default value is to concatenate the source peer id with a sequence number. Setting this + /// parameter allows the user to address packets arbitrarily. One example is content based + /// addressing, where this function may be set to `hash(message)`. This would prevent messages + /// of the same content from being duplicated. + /// + /// The function takes a `GossipsubMessage` as input and outputs a String to be interpreted as + /// the message id. pub fn message_id_fn(&mut self, id_fn: fn(&GossipsubMessage) -> MessageId) -> &mut Self { self.config.message_id_fn = id_fn; self } + /// Constructs a `GossipsubConfig` from the given configuration. pub fn build(&self) -> GossipsubConfig { self.config.clone() } @@ -246,6 +299,7 @@ impl std::fmt::Debug for GossipsubConfig { let _ = builder.field("hash_topics", &self.hash_topics); let _ = builder.field("no_source_id", &self.no_source_id); let _ = builder.field("manual_propagation", &self.manual_propagation); + let _ = builder.field("sign_messages", &self.sign_messages); builder.finish() } } diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index b9cf95592ff..58f50e34e78 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -22,6 +22,7 @@ use crate::behaviour::GossipsubRpc; use crate::protocol::{GossipsubCodec, ProtocolConfig}; use futures::prelude::*; use futures_codec::Framed; +use libp2p_core::identity::Keypair; use libp2p_core::upgrade::{InboundUpgrade, OutboundUpgrade}; use libp2p_swarm::protocols_handler::{ KeepAlive, ProtocolsHandler, ProtocolsHandlerEvent, ProtocolsHandlerUpgrErr, SubstreamProtocol, @@ -80,11 +81,18 @@ enum OutboundSubstreamState { impl GossipsubHandler { /// Builds a new `GossipsubHandler`. - pub fn new(protocol_id: impl Into>, max_transmit_size: usize) -> Self { + pub fn new( + protocol_id: impl Into>, + max_transmit_size: usize, + keypair: Option, + allow_unsigned: bool, + ) -> Self { GossipsubHandler { listen_protocol: SubstreamProtocol::new(ProtocolConfig::new( protocol_id, max_transmit_size, + keypair, + allow_unsigned, )), inbound_substream: None, outbound_substream: None, @@ -198,9 +206,9 @@ impl ProtocolsHandler for GossipsubHandler { return Poll::Ready(ProtocolsHandlerEvent::Custom(message)); } Poll::Ready(Some(Err(e))) => { - debug!("Inbound substream error while awaiting input: {:?}", e); + warn!("Invalid message received. Error: {}", e); self.inbound_substream = - Some(InboundSubstreamState::Closing(substream)); + Some(InboundSubstreamState::WaitingInput(substream)); } // peer closed the stream Poll::Ready(None) => { diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index 8e74308ca88..7ea30daab57 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -72,17 +72,22 @@ impl MessageCache { } } - /// Put a message into the memory cache - pub fn put(&mut self, msg: GossipsubMessage) { + /// Put a message into the memory cache. + /// + /// Returns the message if it already exists. + pub fn put(&mut self, msg: GossipsubMessage) -> Option { let message_id = (self.msg_id)(&msg); let cache_entry = CacheEntry { mid: message_id.clone(), topics: msg.topics.clone(), }; - self.msgs.insert(message_id, msg); - - self.history[0].push(cache_entry); + let seen_message = self.msgs.insert(message_id, msg); + if seen_message.is_none() { + // don't add duplicates entries to the cache + self.history[0].push(cache_entry); + } + seen_message } /// Get a message with `message_id` diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index 14a8c6ddd39..fc8bfea9131 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -27,16 +27,26 @@ use bytes::BytesMut; use futures::future; use futures::prelude::*; use futures_codec::{Decoder, Encoder, Framed}; -use libp2p_core::{InboundUpgrade, OutboundUpgrade, PeerId, UpgradeInfo}; +use libp2p_core::{ + identity::{Keypair, PublicKey}, + InboundUpgrade, OutboundUpgrade, PeerId, UpgradeInfo, +}; +use log::warn; use prost::Message as ProtobufMessage; use std::{borrow::Cow, io, iter, pin::Pin}; use unsigned_varint::codec; /// Implementation of the `ConnectionUpgrade` for the Gossipsub protocol. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct ProtocolConfig { + /// The gossipsub protocol id to listen on. protocol_id: Cow<'static, [u8]>, + /// The maximum transmit size for a packet. max_transmit_size: usize, + /// The keypair used to sign messages, if signing is required. + keypair: Option, + /// Whether to allow unsigned incoming messages. + allow_unsigned: bool, } impl Default for ProtocolConfig { @@ -44,6 +54,8 @@ impl Default for ProtocolConfig { Self { protocol_id: Cow::Borrowed(b"/meshsub/1.0.0"), max_transmit_size: 2048, + keypair: None, + allow_unsigned: true, } } } @@ -54,10 +66,14 @@ impl ProtocolConfig { pub fn new( protocol_id: impl Into>, max_transmit_size: usize, + keypair: Option, + allow_unsigned: bool, ) -> ProtocolConfig { ProtocolConfig { protocol_id: protocol_id.into(), max_transmit_size, + keypair, + allow_unsigned, } } } @@ -84,7 +100,11 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec { length_codec }, + GossipsubCodec { + length_codec, + keypair: self.keypair.clone(), + allow_unsigned: self.allow_unsigned, + }, ))) } } @@ -102,7 +122,11 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec { length_codec }, + GossipsubCodec { + length_codec, + keypair: self.keypair.clone(), + allow_unsigned: self.allow_unsigned, + }, ))) } } @@ -112,6 +136,8 @@ where pub struct GossipsubCodec { /// Codec to encode/decode the Unsigned varint length prefix of the frames. length_codec: codec::UviBytes, + keypair: Option, + allow_unsigned: bool, } impl Encoder for GossipsubCodec { @@ -120,7 +146,7 @@ impl Encoder for GossipsubCodec { fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { // messages - let publish = item + let mut publish = item .messages .into_iter() .map(|message| rpc_proto::Message { @@ -132,9 +158,18 @@ impl Encoder for GossipsubCodec { .into_iter() .map(TopicHash::into_string) .collect(), + signature: None, + key: None, }) .collect::>(); + // sign the messages if required + if let Some(keypair) = self.keypair.as_ref() { + for message in publish.iter_mut() { + sign_message(keypair, message)?; + } + } + // subscriptions let subscriptions = item .subscriptions @@ -222,9 +257,22 @@ impl Decoder for GossipsubCodec { let rpc = rpc_proto::Rpc::decode(&packet[..])?; let mut messages = Vec::with_capacity(rpc.publish.len()); - for publish in rpc.publish.into_iter() { + for message in rpc.publish.into_iter() { + // verify message signatures if required + if !self.allow_unsigned { + // if a single message is unsigned, we will return an error and drop all of them + // Most implementations should not have a list of mixed signed/not-signed messages in a single RPC + if !verify_message(&message)? { + warn!("Message dropped. Invalid signature"); + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid Signature", + )); + } + } + // ensure the sequence number is a u64 - let seq_no = publish.seqno.ok_or_else(|| { + let seq_no = message.seqno.ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, "sequence number was not provided", @@ -236,12 +284,13 @@ impl Decoder for GossipsubCodec { "sequence number has an incorrect size", )); } + messages.push(GossipsubMessage { - source: PeerId::from_bytes(publish.from.unwrap_or_default()) + source: PeerId::from_bytes(message.from.unwrap_or_default()) .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid Peer Id"))?, - data: publish.data.unwrap_or_default(), + data: message.data.unwrap_or_default(), sequence_number: BigEndian::read_u64(&seq_no), - topics: publish + topics: message .topic_ids .into_iter() .map(TopicHash::from_raw) @@ -319,6 +368,87 @@ impl Decoder for GossipsubCodec { } } +/// Signs a gossipsub message, adding the key and signature fields. +fn sign_message(keypair: &Keypair, message: &mut rpc_proto::Message) -> Result<(), io::Error> { + let mut buf = Vec::with_capacity(message.encoded_len()); + message + .encode(&mut buf) + .expect("Buffer has sufficient capacity"); + // the signature is over the bytes "libp2p-pubsub:" + let mut signature_bytes = b"libp2p-pubsub:".to_vec(); + signature_bytes.extend_from_slice(&buf); + match keypair.sign(&signature_bytes) { + Ok(signature) => { + message.signature = Some(signature); + + let key = { + let key_enc = keypair.public().into_protobuf_encoding(); + if key_enc.len() <= 42 { + // public key can be inlined, so we don't include it in the protobuf + None + } else { + Some(key_enc) + } + }; + message.key = key; + Ok(()) + } + Err(e) => { + warn!("Could not sign message. Error: {}", e); + Err(io::Error::new( + io::ErrorKind::Other, + format!("Signing error: {}", e), + )) + } + } +} + +fn verify_message(message: &rpc_proto::Message) -> Result { + let from = message + .from + .as_ref() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No source id given"))?; + let source = PeerId::from_bytes(from.clone()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid Peer Id"))?; + + let signature = message + .signature + .as_ref() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Signature required"))?; + // if there is a key value in the protobuf, use that key otherwise the key must be + // obtained from the inlined source peer_id. + let public_key = match message + .key + .as_ref() + .map(|key| PublicKey::from_protobuf_encoding(&key)) + { + Some(Ok(key)) => key, + _ => PublicKey::from_protobuf_encoding(&source.as_bytes()[2..]).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "No valid public key supplied") + })?, + }; + + // the key must match the peer_id + if source != public_key.clone().into_peer_id() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Public key doesn't match source peer id", + )); + } + + // construct the signature bytes + let mut message_sig = message.clone(); + message_sig.signature = None; + message_sig.key = None; + let mut buf = Vec::with_capacity(message_sig.encoded_len()); + message_sig + .encode(&mut buf) + .expect("Buffer has sufficient capacity"); + let mut signature_bytes = b"libp2p-pubsub:".to_vec(); + signature_bytes.extend_from_slice(&buf); + Ok(public_key.verify(&signature_bytes, signature)) +} + /// A type for gossipsub message ids. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MessageId(pub String); @@ -397,3 +527,49 @@ pub enum GossipsubControlAction { topic_hash: TopicHash, }, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sign_and_verify_message_peer_inline() { + let source_key = Keypair::generate_secp256k1(); + let peer_id = source_key.public().into_peer_id(); + let mut message = rpc_proto::Message { + from: Some(peer_id.clone().into_bytes()), + data: Some(vec![1, 2, 3, 4, 5, 6]), + seqno: Some(10u64.to_be_bytes().to_vec()), + topic_ids: vec!["test1".into(), "test2".into()], + signature: None, + key: None, + }; + + // sign the message + sign_message(&source_key, &mut message).unwrap(); + + // verify the signed message + assert!(verify_message(&message).unwrap()); + } + + #[test] + fn sign_and_verify_message() { + let mut rsa_key = hex::decode("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100ef930f41a71288b643c1cbecbf5f72ab53992249e2b00835bf07390b6745419f3848cbcc5b030faa127bc88cdcda1c1d6f3ff699f0524c15ab9d2c9d8015f5d4bd09881069aad4e9f91b8b0d2964d215cdbbae83ddd31a7622a8228acee07079f6e501aea95508fa26c6122816ef7b00ac526d422bd12aed347c37fff6c1c307f3ba57bb28a7f28609e0bdcc839da4eedca39f5d2fa855ba4b0f9c763e9764937db929a1839054642175312a3de2d3405c9d27bdf6505ef471ce85c5e015eee85bf7874b3d512f715de58d0794fd8afe021c197fbd385bb88a930342fac8da31c27166e2edab00fa55dc1c3814448ba38363077f4e8fe2bdea1c081f85f1aa6f02030100010282010028ff427a1aac1a470e7b4879601a6656193d3857ea79f33db74df61e14730e92bf9ffd78200efb0c40937c3356cbe049cd32e5f15be5c96d5febcaa9bd3484d7fded76a25062d282a3856a1b3b7d2c525cdd8434beae147628e21adf241dd64198d5819f310d033743915ba40ea0b6acdbd0533022ad6daa1ff42de51885f9e8bab2306c6ef1181902d1cd7709006eba1ab0587842b724e0519f295c24f6d848907f772ae9a0953fc931f4af16a07df450fb8bfa94572562437056613647818c238a6ff3f606cffa0533e4b8755da33418dfbc64a85110b1a036623c947400a536bb8df65e5ebe46f2dfd0cfc86e7aeeddd7574c253e8fbf755562b3669525d902818100f9fff30c6677b78dd31ec7a634361438457e80be7a7faf390903067ea8355faa78a1204a82b6e99cb7d9058d23c1ecf6cfe4a900137a00cecc0113fd68c5931602980267ea9a95d182d48ba0a6b4d5dd32fdac685cb2e5d8b42509b2eb59c9579ea6a67ccc7547427e2bd1fb1f23b0ccb4dd6ba7d206c8dd93253d70a451701302818100f5530dfef678d73ce6a401ae47043af10a2e3f224c71ae933035ecd68ccbc4df52d72bc6ca2b17e8faf3e548b483a2506c0369ab80df3b137b54d53fac98f95547c2bc245b416e650ce617e0d29db36066f1335a9ba02ad3e0edf9dc3d58fd835835042663edebce81803972696c789012847cb1f854ab2ac0a1bd3867ac7fb502818029c53010d456105f2bf52a9a8482bca2224a5eac74bf3cc1a4d5d291fafcdffd15a6a6448cce8efdd661f6617ca5fc37c8c885cc3374e109ac6049bcbf72b37eabf44602a2da2d4a1237fd145c863e6d75059976de762d9d258c42b0984e2a2befa01c95217c3ee9c736ff209c355466ff99375194eff943bc402ea1d172a1ed02818027175bf493bbbfb8719c12b47d967bf9eac061c90a5b5711172e9095c38bb8cc493c063abffe4bea110b0a2f22ac9311b3947ba31b7ef6bfecf8209eebd6d86c316a2366bbafda7279b2b47d5bb24b6202254f249205dcad347b574433f6593733b806f84316276c1990a016ce1bbdbe5f650325acc7791aefe515ecc60063bd02818100b6a2077f4adcf15a17092d9c4a346d6022ac48f3861b73cf714f84c440a07419a7ce75a73b9cbff4597c53c128bf81e87b272d70428a272d99f90cd9b9ea1033298e108f919c6477400145a102df3fb5601ffc4588203cf710002517bfa24e6ad32f4d09c6b1a995fa28a3104131bedd9072f3b4fb4a5c2056232643d310453f").unwrap(); + let source_key = Keypair::rsa_from_pkcs8(&mut rsa_key).unwrap(); + let peer_id = source_key.public().into_peer_id(); + let mut message = rpc_proto::Message { + from: Some(peer_id.clone().into_bytes()), + data: Some(vec![1, 2, 3, 4, 5, 6]), + seqno: Some(10u64.to_be_bytes().to_vec()), + topic_ids: vec!["test1".into(), "test2".into()], + signature: None, + key: None, + }; + + // sign the message + sign_message(&source_key, &mut message).unwrap(); + + // verify the signed message + assert!(verify_message(&message).unwrap()); + } +} diff --git a/protocols/gossipsub/src/rpc.proto b/protocols/gossipsub/src/rpc.proto index 1aa19430aa2..13ab9ac8609 100644 --- a/protocols/gossipsub/src/rpc.proto +++ b/protocols/gossipsub/src/rpc.proto @@ -19,6 +19,8 @@ message Message { optional bytes data = 2; optional bytes seqno = 3; repeated string topic_ids = 4; + optional bytes signature = 5; + optional bytes key = 6; } message ControlMessage { diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index f16486e66cc..6a911ac86ba 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -30,13 +30,8 @@ use std::{ }; use libp2p_core::{ - Multiaddr, - Transport, - identity, - multiaddr::Protocol, - muxing::StreamMuxerBox, - transport::MemoryTransport, - upgrade, + identity, multiaddr::Protocol, muxing::StreamMuxerBox, transport::MemoryTransport, upgrade, + Multiaddr, Transport, }; use libp2p_gossipsub::{Gossipsub, GossipsubConfig, GossipsubEvent, Topic}; use libp2p_plaintext::PlainText2Config; @@ -150,7 +145,7 @@ fn build_node() -> (Multiaddr, Swarm) { .boxed(); let peer_id = public_key.clone().into_peer_id(); - let behaviour = Gossipsub::new(peer_id.clone(), GossipsubConfig::default()); + let behaviour = Gossipsub::new(key, GossipsubConfig::default()); let mut swarm = Swarm::new(transport, behaviour, peer_id); let port = 1 + random::(); From 573e4b6471113e28024339ee083b8ca0c892c7af Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 19 May 2020 17:52:56 +1000 Subject: [PATCH 02/35] Update ipfs-private example --- examples/ipfs-private.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/ipfs-private.rs b/examples/ipfs-private.rs index 9d3476853f0..30e1b91846c 100644 --- a/examples/ipfs-private.rs +++ b/examples/ipfs-private.rs @@ -178,18 +178,14 @@ fn main() -> Result<(), Box> { ping: Ping, } - impl NetworkBehaviourEventProcess - for MyBehaviour - { + impl NetworkBehaviourEventProcess for MyBehaviour { // Called when `identify` produces an event. fn inject_event(&mut self, event: IdentifyEvent) { println!("identify: {:?}", event); } } - impl NetworkBehaviourEventProcess - for MyBehaviour - { + impl NetworkBehaviourEventProcess for MyBehaviour { // Called when `gossipsub` produces an event. fn inject_event(&mut self, event: GossipsubEvent) { match event { @@ -204,9 +200,7 @@ fn main() -> Result<(), Box> { } } - impl NetworkBehaviourEventProcess - for MyBehaviour - { + impl NetworkBehaviourEventProcess for MyBehaviour { // Called when `ping` produces an event. fn inject_event(&mut self, event: PingEvent) { use ping::handler::{PingFailure, PingSuccess}; @@ -249,7 +243,7 @@ fn main() -> Result<(), Box> { .max_transmit_size(262144) .build(); let mut behaviour = MyBehaviour { - gossipsub: Gossipsub::new(local_peer_id.clone(), gossipsub_config), + gossipsub: Gossipsub::new(local_key.clone(), gossipsub_config), identify: Identify::new( "/ipfs/0.1.0".into(), "rust-ipfs-example".into(), From d2babd4d8e255319c4a6eee13bb335466a9890dd Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 22 May 2020 12:50:46 +1000 Subject: [PATCH 03/35] Add optimisation to prevent key calculation each message --- protocols/gossipsub/src/protocol.rs | 65 +++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index fc8bfea9131..f3c976e8d9c 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -100,11 +100,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec { - length_codec, - keypair: self.keypair.clone(), - allow_unsigned: self.allow_unsigned, - }, + GossipsubCodec::new(length_codec, self.keypair.clone(), self.allow_unsigned), ))) } } @@ -122,11 +118,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec { - length_codec, - keypair: self.keypair.clone(), - allow_unsigned: self.allow_unsigned, - }, + GossipsubCodec::new(length_codec, self.keypair.clone(), self.allow_unsigned), ))) } } @@ -136,8 +128,46 @@ where pub struct GossipsubCodec { /// Codec to encode/decode the Unsigned varint length prefix of the frames. length_codec: codec::UviBytes, + /// Libp2p Keypair if message signing is required. keypair: Option, + /// Whether to accept un-signed keypairs. allow_unsigned: bool, + /// The key to be tagged on to messages if the public key cannot be inlined in the PeerId and + /// signing messages is required. + key: Option>, +} + +impl GossipsubCodec { + pub fn new( + length_codec: codec::UviBytes, + keypair: Option, + allow_unsigned: bool, + ) -> Self { + // determine if the public key needs to be included in each message + let key = { + if let Some(keypair) = keypair.as_ref() { + // signing is required + let key_enc = keypair.public().into_protobuf_encoding(); + if key_enc.len() <= 42 { + // public key can be inlined, so we don't include it in the protobuf + None + } else { + // include the protobuf encoding of the public key in the message + Some(key_enc) + } + } else { + // signing is not required, no key needs to be added + None + } + }; + + GossipsubCodec { + length_codec, + keypair, + allow_unsigned, + key, + } + } } impl Encoder for GossipsubCodec { @@ -167,6 +197,8 @@ impl Encoder for GossipsubCodec { if let Some(keypair) = self.keypair.as_ref() { for message in publish.iter_mut() { sign_message(keypair, message)?; + // add the peer_id key if required + message.key = self.key.clone(); } } @@ -380,17 +412,6 @@ fn sign_message(keypair: &Keypair, message: &mut rpc_proto::Message) -> Result<( match keypair.sign(&signature_bytes) { Ok(signature) => { message.signature = Some(signature); - - let key = { - let key_enc = keypair.public().into_protobuf_encoding(); - if key_enc.len() <= 42 { - // public key can be inlined, so we don't include it in the protobuf - None - } else { - Some(key_enc) - } - }; - message.key = key; Ok(()) } Err(e) => { @@ -568,6 +589,8 @@ mod tests { // sign the message sign_message(&source_key, &mut message).unwrap(); + // key is not inlined + message.key = Some(source_key.public().into_protobuf_encoding()); // verify the signed message assert!(verify_message(&message).unwrap()); From ac3c977393c93e67128471e07a555c93bffa64f0 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 25 May 2020 15:14:23 +1000 Subject: [PATCH 04/35] Handle forwarding of signed messages and reviewers comments --- examples/gossipsub-chat.rs | 2 - protocols/gossipsub/src/behaviour.rs | 24 +- protocols/gossipsub/src/behaviour/tests.rs | 4 + protocols/gossipsub/src/config.rs | 55 ++-- protocols/gossipsub/src/handler.rs | 17 +- protocols/gossipsub/src/mcache.rs | 4 +- protocols/gossipsub/src/protocol.rs | 279 ++++++++++++--------- 7 files changed, 205 insertions(+), 180 deletions(-) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index 41cf7a40fc3..fe46dd30612 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -90,8 +90,6 @@ fn main() -> Result<(), Box> { let gossipsub_config = gossipsub::GossipsubConfigBuilder::new() .heartbeat_interval(Duration::from_secs(10)) .message_id_fn(message_id_fn) // content-address messages. No two messages of the - .allow_unsigned_messages(false) - .sign_messages(true) //same content will be propagated. .build(); // build a gossipsub network behaviour diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 5aa7c9efe03..0d1762932ff 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -95,10 +95,10 @@ impl Gossipsub { keypair.public().into_peer_id() }; - let keypair = if gs_config.sign_messages { - Some(keypair) - } else { + let keypair = if gs_config.disable_message_signing { None + } else { + Some(keypair) }; Gossipsub { @@ -235,13 +235,15 @@ impl Gossipsub { // big-endian uint. sequence_number: rand::random(), topics: topic.into_iter().map(|t| self.topic_hash(t)).collect(), + signature: None, // signature will get created when being published + key: None, }; let msg_id = (self.config.message_id_fn)(&message); - // add published message to our received caches + // Add published message to our received caches if self.mcache.put(message.clone()).is_some() { - // this message has already been seen. We don't re-publish messages that have already - // been published on the network + // This message has already been seen. We don't re-publish messages that have already + // been published on the network. warn!( "Not publishing a message that has already been published. Msg-id {}", msg_id @@ -254,17 +256,17 @@ impl Gossipsub { (self.config.message_id_fn)(&message) ); - // forward the message to mesh peers + // Forward the message to mesh peers let message_source = &self.message_source_id.clone(); self.forward_msg(message.clone(), message_source); let mut recipient_peers = HashSet::new(); for topic_hash in &message.topics { - // if not subscribed to the topic, use fanout peers + // If not subscribed to the topic, use fanout peers if self.mesh.get(&topic_hash).is_none() { debug!("Topic: {:?} not in the mesh", topic_hash); - // build a list of peers to forward the message to - // if we have fanout peers add them to the map + // Build a list of peers to forward the message to + // if we have fanout peers add them to the map. if self.fanout.contains_key(&topic_hash) { for peer in self.fanout.get(&topic_hash).expect("Topic must exist") { recipient_peers.insert(peer.clone()); @@ -1018,9 +1020,9 @@ impl NetworkBehaviour for Gossipsub { fn new_handler(&mut self) -> Self::ProtocolsHandler { GossipsubHandler::new( self.config.protocol_id.clone(), + self.message_source_id.clone(), self.config.max_transmit_size, self.keypair.clone(), - self.config.allow_unsigned_messages, ) } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index d65b854bae4..18e1e036cb4 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -596,6 +596,8 @@ mod tests { data: vec![1, 2, 3, 4], sequence_number: 1u64, topics: Vec::new(), + signature: None, + key: None, }; let msg_id = id(&message); gs.mcache.put(message.clone()); @@ -635,6 +637,8 @@ mod tests { data: vec![1, 2, 3, 4], sequence_number: shift, topics: Vec::new(), + signature: None, + key: None, }; let msg_id = id(&message); gs.mcache.put(message.clone()); diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index accd64ee933..cb91205afc8 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -76,12 +76,9 @@ pub struct GossipsubConfig { /// once validated (default is `false`). pub manual_propagation: bool, - /// When set to `true` all published messages are signed by the libp2p key (default is `true`). - pub sign_messages: bool, - - /// Determines whether unsigned messages will be accepted. If set to false, unsigned messages - /// will be dropped. Default value is `true`. - pub allow_unsigned_messages: bool, + /// Message signing is on by default. When this parameter is set, + /// published messages are not signed by the libp2p key. + pub disable_message_signing: bool, /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this @@ -111,8 +108,7 @@ impl Default for GossipsubConfig { hash_topics: false, // default compatibility with floodsub no_source_id: false, manual_propagation: false, - sign_messages: true, - allow_unsigned_messages: true, + disable_message_signing: false, message_id_fn: |message| { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); @@ -229,37 +225,34 @@ impl GossipsubConfigBuilder { self } - /// Flag determining if gossipsub topics are hashed or sent as plain strings (default is false). - pub fn hash_topics(&mut self, value: bool) -> &mut Self { - self.config.hash_topics = value; - self - } - - /// When set, all published messages will have a 0 source `PeerId` (default is false). - pub fn no_source_id(&mut self, value: bool) -> &mut Self { - self.config.no_source_id = value; + /// When set, gossipsub topics are hashed instead of being sent as plain strings. + pub fn hash_topics(&mut self) -> &mut Self { + self.config.hash_topics = true; self } - /// When set to `true`, prevents automatic forwarding of all received messages. This setting - /// allows a user to validate the messages before propagating them to their peers. If set to - /// true, the user must manually call `propagate_message()` on the behaviour to forward message - /// once validated (default is `false`). - pub fn manual_propagation(&mut self, value: bool) -> &mut Self { - self.config.manual_propagation = value; + /// When set, all published messages will have a 0 source `PeerId` + pub fn no_source_id(&mut self) -> &mut Self { + assert!( + self.config.disable_message_signing, + "Message signing must be disabled in order to mask the source peer id. Cannot sign for the 0 peer_id" + ); + self.config.no_source_id = true; self } - /// When set to `true` all published messages are signed by the libp2p key (default is `true`). - pub fn sign_messages(&mut self, value: bool) -> &mut Self { - self.config.sign_messages = value; + /// When set, prevents automatic forwarding of all received messages. This setting + /// allows a user to validate the messages before propagating them to their peers. If set, + /// the user must manually call `propagate_message()` on the behaviour to forward a message + /// once validated. + pub fn manual_propagation(&mut self) -> &mut Self { + self.config.manual_propagation = true; self } - /// Determines whether unsigned messages will be accepted. If set to false, unsigned messages - /// will be dropped. Default value is `true`. - pub fn allow_unsigned_messages(&mut self, value: bool) -> &mut Self { - self.config.allow_unsigned_messages = value; + /// Disables message signing for all published messages. + pub fn disable_message_signing(&mut self) -> &mut Self { + self.config.disable_message_signing = true; self } @@ -299,7 +292,7 @@ impl std::fmt::Debug for GossipsubConfig { let _ = builder.field("hash_topics", &self.hash_topics); let _ = builder.field("no_source_id", &self.no_source_id); let _ = builder.field("manual_propagation", &self.manual_propagation); - let _ = builder.field("sign_messages", &self.sign_messages); + let _ = builder.field("disable_message_signing", &self.disable_message_signing); builder.finish() } } diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index 58f50e34e78..cf945cc4ef5 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -24,6 +24,7 @@ use futures::prelude::*; use futures_codec::Framed; use libp2p_core::identity::Keypair; use libp2p_core::upgrade::{InboundUpgrade, OutboundUpgrade}; +use libp2p_core::PeerId; use libp2p_swarm::protocols_handler::{ KeepAlive, ProtocolsHandler, ProtocolsHandlerEvent, ProtocolsHandlerUpgrErr, SubstreamProtocol, }; @@ -83,16 +84,16 @@ impl GossipsubHandler { /// Builds a new `GossipsubHandler`. pub fn new( protocol_id: impl Into>, + local_peer_id: PeerId, max_transmit_size: usize, keypair: Option, - allow_unsigned: bool, ) -> Self { GossipsubHandler { listen_protocol: SubstreamProtocol::new(ProtocolConfig::new( protocol_id, + local_peer_id, max_transmit_size, keypair, - allow_unsigned, )), inbound_substream: None, outbound_substream: None, @@ -102,18 +103,6 @@ impl GossipsubHandler { } } -impl Default for GossipsubHandler { - fn default() -> Self { - GossipsubHandler { - listen_protocol: SubstreamProtocol::new(ProtocolConfig::default()), - inbound_substream: None, - outbound_substream: None, - send_queue: SmallVec::new(), - keep_alive: KeepAlive::Yes, - } - } -} - impl ProtocolsHandler for GossipsubHandler { type InEvent = GossipsubRpc; type OutEvent = GossipsubRpc; diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index 7ea30daab57..91959807950 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -84,7 +84,7 @@ impl MessageCache { let seen_message = self.msgs.insert(message_id, msg); if seen_message.is_none() { - // don't add duplicates entries to the cache + // Don't add duplicate entries to the cache self.history[0].push(cache_entry); } seen_message @@ -147,6 +147,8 @@ mod tests { data, sequence_number, topics, + signature: None, + key: None, }; m } diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index f3c976e8d9c..1a15ab18879 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -31,33 +31,24 @@ use libp2p_core::{ identity::{Keypair, PublicKey}, InboundUpgrade, OutboundUpgrade, PeerId, UpgradeInfo, }; -use log::warn; +use log::{debug, warn}; use prost::Message as ProtobufMessage; use std::{borrow::Cow, io, iter, pin::Pin}; use unsigned_varint::codec; +const SIGNING_PREFIX: &'static [u8] = b"libp2p-pubsub:"; + /// Implementation of the `ConnectionUpgrade` for the Gossipsub protocol. #[derive(Clone)] pub struct ProtocolConfig { + /// Our local `PeerId`. + local_peer_id: PeerId, /// The gossipsub protocol id to listen on. protocol_id: Cow<'static, [u8]>, /// The maximum transmit size for a packet. max_transmit_size: usize, /// The keypair used to sign messages, if signing is required. keypair: Option, - /// Whether to allow unsigned incoming messages. - allow_unsigned: bool, -} - -impl Default for ProtocolConfig { - fn default() -> Self { - Self { - protocol_id: Cow::Borrowed(b"/meshsub/1.0.0"), - max_transmit_size: 2048, - keypair: None, - allow_unsigned: true, - } - } } impl ProtocolConfig { @@ -65,15 +56,15 @@ impl ProtocolConfig { /// Sets the maximum gossip transmission size. pub fn new( protocol_id: impl Into>, + local_peer_id: PeerId, max_transmit_size: usize, keypair: Option, - allow_unsigned: bool, ) -> ProtocolConfig { ProtocolConfig { + local_peer_id, protocol_id: protocol_id.into(), max_transmit_size, keypair, - allow_unsigned, } } } @@ -100,7 +91,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec::new(length_codec, self.keypair.clone(), self.allow_unsigned), + GossipsubCodec::new(length_codec, self.keypair.clone(), self.local_peer_id), ))) } } @@ -118,7 +109,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec::new(length_codec, self.keypair.clone(), self.allow_unsigned), + GossipsubCodec::new(length_codec, self.keypair.clone(), self.local_peer_id), ))) } } @@ -130,8 +121,9 @@ pub struct GossipsubCodec { length_codec: codec::UviBytes, /// Libp2p Keypair if message signing is required. keypair: Option, - /// Whether to accept un-signed keypairs. - allow_unsigned: bool, + /// Our `PeerId` to determine if outgoing messages are being forwarded or published. This is an + /// optimisation to prevent conversion of our keypair for each message. + local_peer_id: PeerId, /// The key to be tagged on to messages if the public key cannot be inlined in the PeerId and /// signing messages is required. key: Option>, @@ -141,7 +133,7 @@ impl GossipsubCodec { pub fn new( length_codec: codec::UviBytes, keypair: Option, - allow_unsigned: bool, + local_peer_id: PeerId, ) -> Self { // determine if the public key needs to be included in each message let key = { @@ -149,14 +141,15 @@ impl GossipsubCodec { // signing is required let key_enc = keypair.public().into_protobuf_encoding(); if key_enc.len() <= 42 { - // public key can be inlined, so we don't include it in the protobuf + // The public key can be inlined in [`rpc_proto::Message::from`], so we don't include it + // specifically in the [`rpc_proto::Message::key`] field. None } else { - // include the protobuf encoding of the public key in the message + // Include the protobuf encoding of the public key in the message Some(key_enc) } } else { - // signing is not required, no key needs to be added + // Signing is not required, no key needs to be added None } }; @@ -164,10 +157,96 @@ impl GossipsubCodec { GossipsubCodec { length_codec, keypair, - allow_unsigned, + local_peer_id, key, } } + + /// Signs a gossipsub message, adding the [`rpc_proto::Message::signature`] field. + fn sign_message(&self, message: &mut rpc_proto::Message) -> Result<(), String> { + let keypair = self + .keypair + .as_ref() + .ok_or_else(|| "Key signing not enabled")?; + let mut buf = Vec::with_capacity(message.encoded_len()); + message + .encode(&mut buf) + .expect("Buffer has sufficient capacity"); + // the signature is over the bytes "libp2p-pubsub:" + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + match keypair.sign(&signature_bytes) { + Ok(signature) => { + message.signature = Some(signature); + Ok(()) + } + Err(e) => Err(format!("Signing error: {}", e)), + } + } + + /// Verifies a gossipsub message. This returns either a success or failure. All errors + /// are logged, which prevents error handling in the codec and handler. We simply drop invalid + /// messages and log warnings, rather than propagating errors through the codec. + fn verify_message(message: &rpc_proto::Message) -> bool { + let from = match message.from.as_ref() { + Some(v) => v, + None => { + debug!("Signature verification failed: No source id given"); + return false; + } + }; + + let source = match PeerId::from_bytes(from.clone()) { + Ok(v) => v, + Err(_) => { + debug!("Signature verification failed: Invalid Peer Id"); + return false; + } + }; + + let signature = match message.signature.as_ref() { + Some(v) => v, + None => { + debug!("Signature verification failed: No signature provided"); + return false; + } + }; + + // If there is a key value in the protobuf, use that key otherwise the key must be + // obtained from the inlined source peer_id. + let public_key = match message + .key + .as_ref() + .map(|key| PublicKey::from_protobuf_encoding(&key)) + { + Some(Ok(key)) => key, + _ => match PublicKey::from_protobuf_encoding(&source.as_bytes()[2..]) { + Ok(v) => v, + Err(_) => { + warn!("Signature verification failed: No valid public key supplied"); + return false; + } + }, + }; + + // The key must match the peer_id + if source != public_key.clone().into_peer_id() { + warn!("Signature verification failed: Public key doesn't match source peer id"); + return false; + } + + // Construct the signature bytes + let mut message_sig = message.clone(); + message_sig.signature = None; + message_sig.key = None; + let mut buf = Vec::with_capacity(message_sig.encoded_len()); + message_sig + .encode(&mut buf) + .expect("Buffer has sufficient capacity"); + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + public_key.verify(&signature_bytes, signature) + } } impl Encoder for GossipsubCodec { @@ -175,11 +254,18 @@ impl Encoder for GossipsubCodec { type Error = io::Error; fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> { - // messages - let mut publish = item - .messages - .into_iter() - .map(|message| rpc_proto::Message { + // Messages + let mut publish = Vec::new(); + + for message in item.messages.into_iter() { + // Determine if we need to sign the message; We are the source of the message, signing + // messages is enabled and there is not already a signature associated with the + // messsage. + let sign_message = self.keypair.is_some() + && self.local_peer_id == message.source + && message.signature.is_none(); + + let mut message = rpc_proto::Message { from: Some(message.source.into_bytes()), data: Some(message.data), seqno: Some(message.sequence_number.to_be_bytes().to_vec()), @@ -188,18 +274,25 @@ impl Encoder for GossipsubCodec { .into_iter() .map(TopicHash::into_string) .collect(), - signature: None, - key: None, - }) - .collect::>(); + signature: message.signature, + key: message.key, + }; + + // Sign the messages if required and we are publishing a message (i.e publish.from == + // our_peer_id). + // Note: Duplicates should be filtered so we shouldn't be re-signing any of the + // messages we have already published. + if sign_message { + if let Err(signing_error) = self.sign_message(&mut message) { + // Log the warning and return an error + warn!("{}", signing_error); + return Err(io::Error::new(io::ErrorKind::Other, "Signing failure")); + } - // sign the messages if required - if let Some(keypair) = self.keypair.as_ref() { - for message in publish.iter_mut() { - sign_message(keypair, message)?; - // add the peer_id key if required + // Add the public key if not already inlined via the peer id in [`rpc_proto::Message::from`] message.key = self.key.clone(); } + publish.push(message); } // subscriptions @@ -291,15 +384,15 @@ impl Decoder for GossipsubCodec { let mut messages = Vec::with_capacity(rpc.publish.len()); for message in rpc.publish.into_iter() { // verify message signatures if required - if !self.allow_unsigned { - // if a single message is unsigned, we will return an error and drop all of them + if self.keypair.is_some() { + // If a single message is unsigned, we will drop all of them // Most implementations should not have a list of mixed signed/not-signed messages in a single RPC - if !verify_message(&message)? { + // NOTE: Invalid messages are simply dropped with a warning log. We don't throw an + // error to avoid extra logic to deal with these errors in the handler. + if !GossipsubCodec::verify_message(&message) { warn!("Message dropped. Invalid signature"); - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid Signature", - )); + // Drop the message + return Ok(None); } } @@ -327,6 +420,8 @@ impl Decoder for GossipsubCodec { .into_iter() .map(TopicHash::from_raw) .collect(), + signature: message.signature, + key: message.key, }); } @@ -400,76 +495,6 @@ impl Decoder for GossipsubCodec { } } -/// Signs a gossipsub message, adding the key and signature fields. -fn sign_message(keypair: &Keypair, message: &mut rpc_proto::Message) -> Result<(), io::Error> { - let mut buf = Vec::with_capacity(message.encoded_len()); - message - .encode(&mut buf) - .expect("Buffer has sufficient capacity"); - // the signature is over the bytes "libp2p-pubsub:" - let mut signature_bytes = b"libp2p-pubsub:".to_vec(); - signature_bytes.extend_from_slice(&buf); - match keypair.sign(&signature_bytes) { - Ok(signature) => { - message.signature = Some(signature); - Ok(()) - } - Err(e) => { - warn!("Could not sign message. Error: {}", e); - Err(io::Error::new( - io::ErrorKind::Other, - format!("Signing error: {}", e), - )) - } - } -} - -fn verify_message(message: &rpc_proto::Message) -> Result { - let from = message - .from - .as_ref() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "No source id given"))?; - let source = PeerId::from_bytes(from.clone()) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid Peer Id"))?; - - let signature = message - .signature - .as_ref() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Signature required"))?; - // if there is a key value in the protobuf, use that key otherwise the key must be - // obtained from the inlined source peer_id. - let public_key = match message - .key - .as_ref() - .map(|key| PublicKey::from_protobuf_encoding(&key)) - { - Some(Ok(key)) => key, - _ => PublicKey::from_protobuf_encoding(&source.as_bytes()[2..]).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidData, "No valid public key supplied") - })?, - }; - - // the key must match the peer_id - if source != public_key.clone().into_peer_id() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Public key doesn't match source peer id", - )); - } - - // construct the signature bytes - let mut message_sig = message.clone(); - message_sig.signature = None; - message_sig.key = None; - let mut buf = Vec::with_capacity(message_sig.encoded_len()); - message_sig - .encode(&mut buf) - .expect("Buffer has sufficient capacity"); - let mut signature_bytes = b"libp2p-pubsub:".to_vec(); - signature_bytes.extend_from_slice(&buf); - Ok(public_key.verify(&signature_bytes, signature)) -} - /// A type for gossipsub message ids. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MessageId(pub String); @@ -502,6 +527,12 @@ pub struct GossipsubMessage { /// /// Each message can belong to multiple topics at once. pub topics: Vec, + + /// The signature of the message if it's signed. + pub signature: Option>, + + /// The public key of the message if it is signed and the source `PeerId` cannot be inlined. + pub key: Option>, } /// A subscription received by the gossipsub system. @@ -566,11 +597,12 @@ mod tests { key: None, }; - // sign the message - sign_message(&source_key, &mut message).unwrap(); + let codec = GossipsubCodec::new(codec::UviBytes::default(), Some(source_key), peer_id); + // sign the message + codec.sign_message(&mut message).unwrap(); // verify the signed message - assert!(verify_message(&message).unwrap()); + assert!(GossipsubCodec::verify_message(&message)); } #[test] @@ -587,12 +619,17 @@ mod tests { key: None, }; + let codec = GossipsubCodec::new( + codec::UviBytes::default(), + Some(source_key.clone()), + peer_id, + ); // sign the message - sign_message(&source_key, &mut message).unwrap(); + codec.sign_message(&mut message).unwrap(); // key is not inlined message.key = Some(source_key.public().into_protobuf_encoding()); // verify the signed message - assert!(verify_message(&message).unwrap()); + assert!(GossipsubCodec::verify_message(&message)); } } From 3d1a025b9dbff48d758aa6a4a5aed069c1bfa398 Mon Sep 17 00:00:00 2001 From: Max Inden Date: Mon, 1 Jun 2020 07:21:39 +0200 Subject: [PATCH 05/35] protocols/gossipsub/src/protocol: Use quickcheck to test signing (#31) --- protocols/gossipsub/src/protocol.rs | 123 ++++++++++++++++++---------- 1 file changed, 82 insertions(+), 41 deletions(-) diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index 1a15ab18879..917335954fe 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -583,53 +583,94 @@ pub enum GossipsubControlAction { #[cfg(test)] mod tests { use super::*; + use crate::topic::Topic; + use quickcheck::*; + use rand::Rng; + + #[derive(Clone, Debug)] + struct Message(GossipsubMessage); + + impl Arbitrary for Message { + fn arbitrary(g: &mut G) -> Self { + Message(GossipsubMessage { + source: PeerId::random(), + data: (0..g.gen_range(1, 1024)).map(|_| g.gen()).collect(), + sequence_number: g.gen(), + topics: Vec::arbitrary(g).into_iter().map(|id: TopicId| id.0).collect(), + signature: None, + key: None, + }) + } + } - #[test] - fn sign_and_verify_message_peer_inline() { - let source_key = Keypair::generate_secp256k1(); - let peer_id = source_key.public().into_peer_id(); - let mut message = rpc_proto::Message { - from: Some(peer_id.clone().into_bytes()), - data: Some(vec![1, 2, 3, 4, 5, 6]), - seqno: Some(10u64.to_be_bytes().to_vec()), - topic_ids: vec!["test1".into(), "test2".into()], - signature: None, - key: None, - }; + #[derive(Clone, Debug)] + struct TopicId(TopicHash); + + impl Arbitrary for TopicId { + fn arbitrary(g: &mut G) -> Self { + TopicId(Topic::new((0..g.gen_range(0, 1024)).map(|_| g.gen::()).collect()).sha256_hash()) + } + } + + #[derive(Clone)] + struct TestKeypair(Keypair); + + impl Arbitrary for TestKeypair { + fn arbitrary(g: &mut G) -> Self { + let keypair = if g.gen() { + // Small enough to be inlined. + Keypair::generate_secp256k1() + } else { + // Too large to be inlined. + let mut rsa_key = hex::decode("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100ef930f41a71288b643c1cbecbf5f72ab53992249e2b00835bf07390b6745419f3848cbcc5b030faa127bc88cdcda1c1d6f3ff699f0524c15ab9d2c9d8015f5d4bd09881069aad4e9f91b8b0d2964d215cdbbae83ddd31a7622a8228acee07079f6e501aea95508fa26c6122816ef7b00ac526d422bd12aed347c37fff6c1c307f3ba57bb28a7f28609e0bdcc839da4eedca39f5d2fa855ba4b0f9c763e9764937db929a1839054642175312a3de2d3405c9d27bdf6505ef471ce85c5e015eee85bf7874b3d512f715de58d0794fd8afe021c197fbd385bb88a930342fac8da31c27166e2edab00fa55dc1c3814448ba38363077f4e8fe2bdea1c081f85f1aa6f02030100010282010028ff427a1aac1a470e7b4879601a6656193d3857ea79f33db74df61e14730e92bf9ffd78200efb0c40937c3356cbe049cd32e5f15be5c96d5febcaa9bd3484d7fded76a25062d282a3856a1b3b7d2c525cdd8434beae147628e21adf241dd64198d5819f310d033743915ba40ea0b6acdbd0533022ad6daa1ff42de51885f9e8bab2306c6ef1181902d1cd7709006eba1ab0587842b724e0519f295c24f6d848907f772ae9a0953fc931f4af16a07df450fb8bfa94572562437056613647818c238a6ff3f606cffa0533e4b8755da33418dfbc64a85110b1a036623c947400a536bb8df65e5ebe46f2dfd0cfc86e7aeeddd7574c253e8fbf755562b3669525d902818100f9fff30c6677b78dd31ec7a634361438457e80be7a7faf390903067ea8355faa78a1204a82b6e99cb7d9058d23c1ecf6cfe4a900137a00cecc0113fd68c5931602980267ea9a95d182d48ba0a6b4d5dd32fdac685cb2e5d8b42509b2eb59c9579ea6a67ccc7547427e2bd1fb1f23b0ccb4dd6ba7d206c8dd93253d70a451701302818100f5530dfef678d73ce6a401ae47043af10a2e3f224c71ae933035ecd68ccbc4df52d72bc6ca2b17e8faf3e548b483a2506c0369ab80df3b137b54d53fac98f95547c2bc245b416e650ce617e0d29db36066f1335a9ba02ad3e0edf9dc3d58fd835835042663edebce81803972696c789012847cb1f854ab2ac0a1bd3867ac7fb502818029c53010d456105f2bf52a9a8482bca2224a5eac74bf3cc1a4d5d291fafcdffd15a6a6448cce8efdd661f6617ca5fc37c8c885cc3374e109ac6049bcbf72b37eabf44602a2da2d4a1237fd145c863e6d75059976de762d9d258c42b0984e2a2befa01c95217c3ee9c736ff209c355466ff99375194eff943bc402ea1d172a1ed02818027175bf493bbbfb8719c12b47d967bf9eac061c90a5b5711172e9095c38bb8cc493c063abffe4bea110b0a2f22ac9311b3947ba31b7ef6bfecf8209eebd6d86c316a2366bbafda7279b2b47d5bb24b6202254f249205dcad347b574433f6593733b806f84316276c1990a016ce1bbdbe5f650325acc7791aefe515ecc60063bd02818100b6a2077f4adcf15a17092d9c4a346d6022ac48f3861b73cf714f84c440a07419a7ce75a73b9cbff4597c53c128bf81e87b272d70428a272d99f90cd9b9ea1033298e108f919c6477400145a102df3fb5601ffc4588203cf710002517bfa24e6ad32f4d09c6b1a995fa28a3104131bedd9072f3b4fb4a5c2056232643d310453f").unwrap(); + Keypair::rsa_from_pkcs8(&mut rsa_key).unwrap() + }; + + TestKeypair(keypair) + } + } - let codec = GossipsubCodec::new(codec::UviBytes::default(), Some(source_key), peer_id); - // sign the message - codec.sign_message(&mut message).unwrap(); - // verify the signed message - assert!(GossipsubCodec::verify_message(&message)); + impl std::fmt::Debug for TestKeypair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TestKeypair") + .field("public", &self.0.public()) + .finish() + } } #[test] - fn sign_and_verify_message() { - let mut rsa_key = hex::decode("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100ef930f41a71288b643c1cbecbf5f72ab53992249e2b00835bf07390b6745419f3848cbcc5b030faa127bc88cdcda1c1d6f3ff699f0524c15ab9d2c9d8015f5d4bd09881069aad4e9f91b8b0d2964d215cdbbae83ddd31a7622a8228acee07079f6e501aea95508fa26c6122816ef7b00ac526d422bd12aed347c37fff6c1c307f3ba57bb28a7f28609e0bdcc839da4eedca39f5d2fa855ba4b0f9c763e9764937db929a1839054642175312a3de2d3405c9d27bdf6505ef471ce85c5e015eee85bf7874b3d512f715de58d0794fd8afe021c197fbd385bb88a930342fac8da31c27166e2edab00fa55dc1c3814448ba38363077f4e8fe2bdea1c081f85f1aa6f02030100010282010028ff427a1aac1a470e7b4879601a6656193d3857ea79f33db74df61e14730e92bf9ffd78200efb0c40937c3356cbe049cd32e5f15be5c96d5febcaa9bd3484d7fded76a25062d282a3856a1b3b7d2c525cdd8434beae147628e21adf241dd64198d5819f310d033743915ba40ea0b6acdbd0533022ad6daa1ff42de51885f9e8bab2306c6ef1181902d1cd7709006eba1ab0587842b724e0519f295c24f6d848907f772ae9a0953fc931f4af16a07df450fb8bfa94572562437056613647818c238a6ff3f606cffa0533e4b8755da33418dfbc64a85110b1a036623c947400a536bb8df65e5ebe46f2dfd0cfc86e7aeeddd7574c253e8fbf755562b3669525d902818100f9fff30c6677b78dd31ec7a634361438457e80be7a7faf390903067ea8355faa78a1204a82b6e99cb7d9058d23c1ecf6cfe4a900137a00cecc0113fd68c5931602980267ea9a95d182d48ba0a6b4d5dd32fdac685cb2e5d8b42509b2eb59c9579ea6a67ccc7547427e2bd1fb1f23b0ccb4dd6ba7d206c8dd93253d70a451701302818100f5530dfef678d73ce6a401ae47043af10a2e3f224c71ae933035ecd68ccbc4df52d72bc6ca2b17e8faf3e548b483a2506c0369ab80df3b137b54d53fac98f95547c2bc245b416e650ce617e0d29db36066f1335a9ba02ad3e0edf9dc3d58fd835835042663edebce81803972696c789012847cb1f854ab2ac0a1bd3867ac7fb502818029c53010d456105f2bf52a9a8482bca2224a5eac74bf3cc1a4d5d291fafcdffd15a6a6448cce8efdd661f6617ca5fc37c8c885cc3374e109ac6049bcbf72b37eabf44602a2da2d4a1237fd145c863e6d75059976de762d9d258c42b0984e2a2befa01c95217c3ee9c736ff209c355466ff99375194eff943bc402ea1d172a1ed02818027175bf493bbbfb8719c12b47d967bf9eac061c90a5b5711172e9095c38bb8cc493c063abffe4bea110b0a2f22ac9311b3947ba31b7ef6bfecf8209eebd6d86c316a2366bbafda7279b2b47d5bb24b6202254f249205dcad347b574433f6593733b806f84316276c1990a016ce1bbdbe5f650325acc7791aefe515ecc60063bd02818100b6a2077f4adcf15a17092d9c4a346d6022ac48f3861b73cf714f84c440a07419a7ce75a73b9cbff4597c53c128bf81e87b272d70428a272d99f90cd9b9ea1033298e108f919c6477400145a102df3fb5601ffc4588203cf710002517bfa24e6ad32f4d09c6b1a995fa28a3104131bedd9072f3b4fb4a5c2056232643d310453f").unwrap(); - let source_key = Keypair::rsa_from_pkcs8(&mut rsa_key).unwrap(); - let peer_id = source_key.public().into_peer_id(); - let mut message = rpc_proto::Message { - from: Some(peer_id.clone().into_bytes()), - data: Some(vec![1, 2, 3, 4, 5, 6]), - seqno: Some(10u64.to_be_bytes().to_vec()), - topic_ids: vec!["test1".into(), "test2".into()], - signature: None, - key: None, - }; + fn encode_decode() { + fn prop(message: Message, source_key: TestKeypair) { + let mut message = message.0; + let source_key = source_key.0; + + let peer_id = source_key.public().into_peer_id(); + message.source = peer_id.clone(); + + let rpc = GossipsubRpc { + messages: vec![message], + subscriptions: vec![], + control_msgs: vec![], + }; + + let mut codec = GossipsubCodec::new( + codec::UviBytes::default(), + Some(source_key.clone()), + peer_id, + ); + + let mut buf = BytesMut::new(); + + codec.encode(rpc.clone(), &mut buf).unwrap(); + let mut decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); + + decoded_rpc.messages[0].signature = None; + decoded_rpc.messages[0].key = None; + + assert_eq!(rpc, decoded_rpc); + } - let codec = GossipsubCodec::new( - codec::UviBytes::default(), - Some(source_key.clone()), - peer_id, - ); - // sign the message - codec.sign_message(&mut message).unwrap(); - // key is not inlined - message.key = Some(source_key.public().into_protobuf_encoding()); - - // verify the signed message - assert!(GossipsubCodec::verify_message(&message)); + QuickCheck::new().quickcheck(prop as fn(_, _) -> _) } } From 31b6849e5e7732aa1a50aa3f51add05bcdae596e Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 1 Jun 2020 18:16:16 +1000 Subject: [PATCH 06/35] Shift signing into behaviour --- protocols/gossipsub/src/behaviour.rs | 113 ++++++++++++++--- protocols/gossipsub/src/handler.rs | 8 +- protocols/gossipsub/src/protocol.rs | 178 +++++++-------------------- protocols/gossipsub/src/topic.rs | 10 +- 4 files changed, 143 insertions(+), 166 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 0d1762932ff..734fff09134 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -23,8 +23,9 @@ use crate::handler::GossipsubHandler; use crate::mcache::MessageCache; use crate::protocol::{ GossipsubControlAction, GossipsubMessage, GossipsubSubscription, GossipsubSubscriptionAction, - MessageId, + MessageId, SIGNING_PREFIX, }; +use crate::rpc_proto; use crate::topic::{Topic, TopicHash}; use futures::prelude::*; use libp2p_core::{connection::ConnectionId, identity::Keypair, Multiaddr, PeerId}; @@ -32,6 +33,7 @@ use libp2p_swarm::{ NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters, ProtocolsHandler, }; use log::{debug, error, info, trace, warn}; +use prost::Message; use rand; use rand::{seq::SliceRandom, thread_rng}; use std::{ @@ -84,23 +86,51 @@ pub struct Gossipsub { /// Heartbeat interval stream. heartbeat: Interval, + + /// The peer_id public to add to messages if signing is enabled and the current libp2p key is + /// not inlined in the `PeerId`. + key: Option>, } impl Gossipsub { /// Creates a `Gossipsub` struct given a set of parameters specified by `gs_config`. pub fn new(keypair: Keypair, gs_config: GossipsubConfig) -> Self { + // Set up the router given the configuration settings. + + // Sets up the source_id of published messages. let message_source_id = if gs_config.no_source_id { PeerId::from_bytes(crate::config::IDENTITY_SOURCE.to_vec()).expect("Valid peer id") } else { keypair.public().into_peer_id() }; + // If signing is not enabled, we don't need to store the keypair. let keypair = if gs_config.disable_message_signing { None } else { Some(keypair) }; + // If we are signing and the public key cannot be inlined in the `PeerId`, we store the + // public key to attach to published messages. + let key = { + if let Some(keypair) = keypair.as_ref() { + // signing is required + let key_enc = keypair.public().into_protobuf_encoding(); + if key_enc.len() <= 42 { + // The public key can be inlined in [`rpc_proto::Message::from`], so we don't include it + // specifically in the [`rpc_proto::Message::key`] field. + None + } else { + // Include the protobuf encoding of the public key in the message + Some(key_enc) + } + } else { + // Signing is not required, no key needs to be added + None + } + }; + Gossipsub { config: gs_config.clone(), events: VecDeque::new(), @@ -121,6 +151,7 @@ impl Gossipsub { Instant::now() + gs_config.heartbeat_initial_delay, gs_config.heartbeat_interval, ), + key, } } @@ -225,18 +256,18 @@ impl Gossipsub { /// Publishes a message with multiple topics to the network. pub fn publish_many( &mut self, - topic: impl IntoIterator, + topics: impl IntoIterator, data: impl Into>, ) { - let message = GossipsubMessage { - source: self.message_source_id.clone(), - data: data.into(), - // To be interoperable with the go-implementation this is treated as a 64-bit - // big-endian uint. - sequence_number: rand::random(), - topics: topic.into_iter().map(|t| self.topic_hash(t)).collect(), - signature: None, // signature will get created when being published - key: None, + let message = match self.build_message( + topics.into_iter().map(|t| self.topic_hash(t)).collect(), + data.into(), + ) { + Ok(m) => m, + Err(e) => { + error!("{}", e); + return; + } }; let msg_id = (self.config.message_id_fn)(&message); @@ -251,10 +282,7 @@ impl Gossipsub { return; } - debug!( - "Publishing message: {:?}", - (self.config.message_id_fn)(&message) - ); + debug!("Publishing message: {:?}", msg_id); // Forward the message to mesh peers let message_source = &self.message_source_id.clone(); @@ -701,8 +729,8 @@ impl Gossipsub { // too little peers - add some if peers.len() < self.config.mesh_n_low { debug!( - "HEARTBEAT: Mesh low. Topic: {:?} Contains: {:?} needs: {:?}", - topic_hash.clone().into_string(), + "HEARTBEAT: Mesh low. Topic: {} Contains: {} needs: {}", + topic_hash, peers.len(), self.config.mesh_n_low ); @@ -724,7 +752,7 @@ impl Gossipsub { // too many peers - remove some if peers.len() > self.config.mesh_n_high { debug!( - "HEARTBEAT: Mesh high. Topic: {:?} Contains: {:?} needs: {:?}", + "HEARTBEAT: Mesh high. Topic: {} Contains: {} needs: {}", topic_hash, peers.len(), self.config.mesh_n_high @@ -946,6 +974,52 @@ impl Gossipsub { debug!("Completed forwarding message"); } + /// Constructs a `GossipsubMessage` performing message signing if required. + pub(crate) fn build_message( + &self, + topics: Vec, + data: Vec, + ) -> Result { + let sequence_number: u64 = rand::random(); + let message = rpc_proto::Message { + from: Some(self.message_source_id.clone().into_bytes()), + data: Some(data), + seqno: Some(sequence_number.to_be_bytes().to_vec()), + topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(), + signature: None, + key: None, + }; + + // If a signature is required, generate it + let signature = if let Some(keypair) = self.keypair.as_ref() { + let mut buf = Vec::with_capacity(message.encoded_len()); + message + .encode(&mut buf) + .expect("Buffer has sufficient capacity"); + // the signature is over the bytes "libp2p-pubsub:" + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + Some( + keypair + .sign(&signature_bytes) + .map_err(|e| format!("Signing error: {}", e))?, + ) + } else { + None + }; + + Ok(GossipsubMessage { + source: self.message_source_id.clone(), + data: message.data.expect("data exists"), + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number, + topics, + signature, + key: self.key.clone(), + }) + } + /// Helper function to get a set of `n` random gossipsub peers for a `topic_hash` /// filtered by the function `f`. fn get_random_peers( @@ -1020,9 +1094,8 @@ impl NetworkBehaviour for Gossipsub { fn new_handler(&mut self) -> Self::ProtocolsHandler { GossipsubHandler::new( self.config.protocol_id.clone(), - self.message_source_id.clone(), self.config.max_transmit_size, - self.keypair.clone(), + self.keypair.is_some(), // if a keypair is stored we want to verify signatures ) } diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index cf945cc4ef5..7bbbd0ced6e 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -22,9 +22,7 @@ use crate::behaviour::GossipsubRpc; use crate::protocol::{GossipsubCodec, ProtocolConfig}; use futures::prelude::*; use futures_codec::Framed; -use libp2p_core::identity::Keypair; use libp2p_core::upgrade::{InboundUpgrade, OutboundUpgrade}; -use libp2p_core::PeerId; use libp2p_swarm::protocols_handler::{ KeepAlive, ProtocolsHandler, ProtocolsHandlerEvent, ProtocolsHandlerUpgrErr, SubstreamProtocol, }; @@ -84,16 +82,14 @@ impl GossipsubHandler { /// Builds a new `GossipsubHandler`. pub fn new( protocol_id: impl Into>, - local_peer_id: PeerId, max_transmit_size: usize, - keypair: Option, + verify_signatures: bool, ) -> Self { GossipsubHandler { listen_protocol: SubstreamProtocol::new(ProtocolConfig::new( protocol_id, - local_peer_id, max_transmit_size, - keypair, + verify_signatures, )), inbound_substream: None, outbound_substream: None, diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index 917335954fe..fd871641e83 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -27,28 +27,23 @@ use bytes::BytesMut; use futures::future; use futures::prelude::*; use futures_codec::{Decoder, Encoder, Framed}; -use libp2p_core::{ - identity::{Keypair, PublicKey}, - InboundUpgrade, OutboundUpgrade, PeerId, UpgradeInfo, -}; +use libp2p_core::{identity::PublicKey, InboundUpgrade, OutboundUpgrade, PeerId, UpgradeInfo}; use log::{debug, warn}; use prost::Message as ProtobufMessage; use std::{borrow::Cow, io, iter, pin::Pin}; use unsigned_varint::codec; -const SIGNING_PREFIX: &'static [u8] = b"libp2p-pubsub:"; +pub const SIGNING_PREFIX: &'static [u8] = b"libp2p-pubsub:"; /// Implementation of the `ConnectionUpgrade` for the Gossipsub protocol. #[derive(Clone)] pub struct ProtocolConfig { - /// Our local `PeerId`. - local_peer_id: PeerId, /// The gossipsub protocol id to listen on. protocol_id: Cow<'static, [u8]>, /// The maximum transmit size for a packet. max_transmit_size: usize, - /// The keypair used to sign messages, if signing is required. - keypair: Option, + /// Determines whether to check and verify signatures of incoming messages. + verify_signatures: bool, } impl ProtocolConfig { @@ -56,15 +51,13 @@ impl ProtocolConfig { /// Sets the maximum gossip transmission size. pub fn new( protocol_id: impl Into>, - local_peer_id: PeerId, max_transmit_size: usize, - keypair: Option, + verify_signatures: bool, ) -> ProtocolConfig { ProtocolConfig { - local_peer_id, protocol_id: protocol_id.into(), max_transmit_size, - keypair, + verify_signatures, } } } @@ -91,7 +84,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec::new(length_codec, self.keypair.clone(), self.local_peer_id), + GossipsubCodec::new(length_codec, self.verify_signatures), ))) } } @@ -109,7 +102,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec::new(length_codec, self.keypair.clone(), self.local_peer_id), + GossipsubCodec::new(length_codec, self.verify_signatures), ))) } } @@ -119,75 +112,22 @@ where pub struct GossipsubCodec { /// Codec to encode/decode the Unsigned varint length prefix of the frames. length_codec: codec::UviBytes, - /// Libp2p Keypair if message signing is required. - keypair: Option, - /// Our `PeerId` to determine if outgoing messages are being forwarded or published. This is an - /// optimisation to prevent conversion of our keypair for each message. - local_peer_id: PeerId, - /// The key to be tagged on to messages if the public key cannot be inlined in the PeerId and - /// signing messages is required. - key: Option>, + /// Determines whether to check and verify signatures of incoming messages. + verify_signatures: bool, } impl GossipsubCodec { - pub fn new( - length_codec: codec::UviBytes, - keypair: Option, - local_peer_id: PeerId, - ) -> Self { - // determine if the public key needs to be included in each message - let key = { - if let Some(keypair) = keypair.as_ref() { - // signing is required - let key_enc = keypair.public().into_protobuf_encoding(); - if key_enc.len() <= 42 { - // The public key can be inlined in [`rpc_proto::Message::from`], so we don't include it - // specifically in the [`rpc_proto::Message::key`] field. - None - } else { - // Include the protobuf encoding of the public key in the message - Some(key_enc) - } - } else { - // Signing is not required, no key needs to be added - None - } - }; - + pub fn new(length_codec: codec::UviBytes, verify_signatures: bool) -> Self { GossipsubCodec { length_codec, - keypair, - local_peer_id, - key, - } - } - - /// Signs a gossipsub message, adding the [`rpc_proto::Message::signature`] field. - fn sign_message(&self, message: &mut rpc_proto::Message) -> Result<(), String> { - let keypair = self - .keypair - .as_ref() - .ok_or_else(|| "Key signing not enabled")?; - let mut buf = Vec::with_capacity(message.encoded_len()); - message - .encode(&mut buf) - .expect("Buffer has sufficient capacity"); - // the signature is over the bytes "libp2p-pubsub:" - let mut signature_bytes = SIGNING_PREFIX.to_vec(); - signature_bytes.extend_from_slice(&buf); - match keypair.sign(&signature_bytes) { - Ok(signature) => { - message.signature = Some(signature); - Ok(()) - } - Err(e) => Err(format!("Signing error: {}", e)), + verify_signatures, } } /// Verifies a gossipsub message. This returns either a success or failure. All errors /// are logged, which prevents error handling in the codec and handler. We simply drop invalid /// messages and log warnings, rather than propagating errors through the codec. - fn verify_message(message: &rpc_proto::Message) -> bool { + fn verify_signature(message: &rpc_proto::Message) -> bool { let from = match message.from.as_ref() { Some(v) => v, None => { @@ -258,40 +198,14 @@ impl Encoder for GossipsubCodec { let mut publish = Vec::new(); for message in item.messages.into_iter() { - // Determine if we need to sign the message; We are the source of the message, signing - // messages is enabled and there is not already a signature associated with the - // messsage. - let sign_message = self.keypair.is_some() - && self.local_peer_id == message.source - && message.signature.is_none(); - - let mut message = rpc_proto::Message { + let message = rpc_proto::Message { from: Some(message.source.into_bytes()), data: Some(message.data), seqno: Some(message.sequence_number.to_be_bytes().to_vec()), - topic_ids: message - .topics - .into_iter() - .map(TopicHash::into_string) - .collect(), + topic_ids: message.topics.into_iter().map(TopicHash::into).collect(), signature: message.signature, key: message.key, }; - - // Sign the messages if required and we are publishing a message (i.e publish.from == - // our_peer_id). - // Note: Duplicates should be filtered so we shouldn't be re-signing any of the - // messages we have already published. - if sign_message { - if let Err(signing_error) = self.sign_message(&mut message) { - // Log the warning and return an error - warn!("{}", signing_error); - return Err(io::Error::new(io::ErrorKind::Other, "Signing failure")); - } - - // Add the public key if not already inlined via the peer id in [`rpc_proto::Message::from`] - message.key = self.key.clone(); - } publish.push(message); } @@ -301,7 +215,7 @@ impl Encoder for GossipsubCodec { .into_iter() .map(|sub| rpc_proto::rpc::SubOpts { subscribe: Some(sub.action == GossipsubSubscriptionAction::Subscribe), - topic_id: Some(sub.topic_hash.into_string()), + topic_id: Some(sub.topic_hash.into()), }) .collect::>(); @@ -323,7 +237,7 @@ impl Encoder for GossipsubCodec { message_ids, } => { let rpc_ihave = rpc_proto::ControlIHave { - topic_id: Some(topic_hash.into_string()), + topic_id: Some(topic_hash.into()), message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), }; control.ihave.push(rpc_ihave); @@ -336,13 +250,13 @@ impl Encoder for GossipsubCodec { } GossipsubControlAction::Graft { topic_hash } => { let rpc_graft = rpc_proto::ControlGraft { - topic_id: Some(topic_hash.into_string()), + topic_id: Some(topic_hash.into()), }; control.graft.push(rpc_graft); } GossipsubControlAction::Prune { topic_hash } => { let rpc_prune = rpc_proto::ControlPrune { - topic_id: Some(topic_hash.into_string()), + topic_id: Some(topic_hash.into()), }; control.prune.push(rpc_prune); } @@ -384,12 +298,12 @@ impl Decoder for GossipsubCodec { let mut messages = Vec::with_capacity(rpc.publish.len()); for message in rpc.publish.into_iter() { // verify message signatures if required - if self.keypair.is_some() { + if self.verify_signatures { // If a single message is unsigned, we will drop all of them // Most implementations should not have a list of mixed signed/not-signed messages in a single RPC // NOTE: Invalid messages are simply dropped with a warning log. We don't throw an // error to avoid extra logic to deal with these errors in the handler. - if !GossipsubCodec::verify_message(&message) { + if !GossipsubCodec::verify_signature(&message) { warn!("Message dropped. Invalid signature"); // Drop the message return Ok(None); @@ -584,6 +498,8 @@ pub enum GossipsubControlAction { mod tests { use super::*; use crate::topic::Topic; + use crate::{Gossipsub, GossipsubConfig}; + use libp2p_core::identity::Keypair; use quickcheck::*; use rand::Rng; @@ -592,14 +508,16 @@ mod tests { impl Arbitrary for Message { fn arbitrary(g: &mut G) -> Self { - Message(GossipsubMessage { - source: PeerId::random(), - data: (0..g.gen_range(1, 1024)).map(|_| g.gen()).collect(), - sequence_number: g.gen(), - topics: Vec::arbitrary(g).into_iter().map(|id: TopicId| id.0).collect(), - signature: None, - key: None, - }) + let keypair = TestKeypair::arbitrary(g); + + // generate an arbitrary GossipsubMessage using the behaviour signing functionality + let gs = Gossipsub::new(keypair.0.clone(), GossipsubConfig::default()); + let data = (0..g.gen_range(1, 1024)).map(|_| g.gen()).collect(); + let topics = Vec::arbitrary(g) + .into_iter() + .map(|id: TopicId| id.0) + .collect(); + Message(gs.build_message(topics, data).unwrap()) } } @@ -608,7 +526,10 @@ mod tests { impl Arbitrary for TopicId { fn arbitrary(g: &mut G) -> Self { - TopicId(Topic::new((0..g.gen_range(0, 1024)).map(|_| g.gen::()).collect()).sha256_hash()) + TopicId( + Topic::new((0..g.gen_range(0, 1024)).map(|_| g.gen::()).collect()) + .sha256_hash(), + ) } } @@ -625,12 +546,10 @@ mod tests { let mut rsa_key = hex::decode("308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100ef930f41a71288b643c1cbecbf5f72ab53992249e2b00835bf07390b6745419f3848cbcc5b030faa127bc88cdcda1c1d6f3ff699f0524c15ab9d2c9d8015f5d4bd09881069aad4e9f91b8b0d2964d215cdbbae83ddd31a7622a8228acee07079f6e501aea95508fa26c6122816ef7b00ac526d422bd12aed347c37fff6c1c307f3ba57bb28a7f28609e0bdcc839da4eedca39f5d2fa855ba4b0f9c763e9764937db929a1839054642175312a3de2d3405c9d27bdf6505ef471ce85c5e015eee85bf7874b3d512f715de58d0794fd8afe021c197fbd385bb88a930342fac8da31c27166e2edab00fa55dc1c3814448ba38363077f4e8fe2bdea1c081f85f1aa6f02030100010282010028ff427a1aac1a470e7b4879601a6656193d3857ea79f33db74df61e14730e92bf9ffd78200efb0c40937c3356cbe049cd32e5f15be5c96d5febcaa9bd3484d7fded76a25062d282a3856a1b3b7d2c525cdd8434beae147628e21adf241dd64198d5819f310d033743915ba40ea0b6acdbd0533022ad6daa1ff42de51885f9e8bab2306c6ef1181902d1cd7709006eba1ab0587842b724e0519f295c24f6d848907f772ae9a0953fc931f4af16a07df450fb8bfa94572562437056613647818c238a6ff3f606cffa0533e4b8755da33418dfbc64a85110b1a036623c947400a536bb8df65e5ebe46f2dfd0cfc86e7aeeddd7574c253e8fbf755562b3669525d902818100f9fff30c6677b78dd31ec7a634361438457e80be7a7faf390903067ea8355faa78a1204a82b6e99cb7d9058d23c1ecf6cfe4a900137a00cecc0113fd68c5931602980267ea9a95d182d48ba0a6b4d5dd32fdac685cb2e5d8b42509b2eb59c9579ea6a67ccc7547427e2bd1fb1f23b0ccb4dd6ba7d206c8dd93253d70a451701302818100f5530dfef678d73ce6a401ae47043af10a2e3f224c71ae933035ecd68ccbc4df52d72bc6ca2b17e8faf3e548b483a2506c0369ab80df3b137b54d53fac98f95547c2bc245b416e650ce617e0d29db36066f1335a9ba02ad3e0edf9dc3d58fd835835042663edebce81803972696c789012847cb1f854ab2ac0a1bd3867ac7fb502818029c53010d456105f2bf52a9a8482bca2224a5eac74bf3cc1a4d5d291fafcdffd15a6a6448cce8efdd661f6617ca5fc37c8c885cc3374e109ac6049bcbf72b37eabf44602a2da2d4a1237fd145c863e6d75059976de762d9d258c42b0984e2a2befa01c95217c3ee9c736ff209c355466ff99375194eff943bc402ea1d172a1ed02818027175bf493bbbfb8719c12b47d967bf9eac061c90a5b5711172e9095c38bb8cc493c063abffe4bea110b0a2f22ac9311b3947ba31b7ef6bfecf8209eebd6d86c316a2366bbafda7279b2b47d5bb24b6202254f249205dcad347b574433f6593733b806f84316276c1990a016ce1bbdbe5f650325acc7791aefe515ecc60063bd02818100b6a2077f4adcf15a17092d9c4a346d6022ac48f3861b73cf714f84c440a07419a7ce75a73b9cbff4597c53c128bf81e87b272d70428a272d99f90cd9b9ea1033298e108f919c6477400145a102df3fb5601ffc4588203cf710002517bfa24e6ad32f4d09c6b1a995fa28a3104131bedd9072f3b4fb4a5c2056232643d310453f").unwrap(); Keypair::rsa_from_pkcs8(&mut rsa_key).unwrap() }; - TestKeypair(keypair) } } - impl std::fmt::Debug for TestKeypair { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TestKeypair") @@ -641,12 +560,8 @@ mod tests { #[test] fn encode_decode() { - fn prop(message: Message, source_key: TestKeypair) { - let mut message = message.0; - let source_key = source_key.0; - - let peer_id = source_key.public().into_peer_id(); - message.source = peer_id.clone(); + fn prop(message: Message) { + let message = message.0; let rpc = GossipsubRpc { messages: vec![message], @@ -654,23 +569,14 @@ mod tests { control_msgs: vec![], }; - let mut codec = GossipsubCodec::new( - codec::UviBytes::default(), - Some(source_key.clone()), - peer_id, - ); - + let mut codec = GossipsubCodec::new(codec::UviBytes::default(), true); let mut buf = BytesMut::new(); - codec.encode(rpc.clone(), &mut buf).unwrap(); - let mut decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); - - decoded_rpc.messages[0].signature = None; - decoded_rpc.messages[0].key = None; + let decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); assert_eq!(rpc, decoded_rpc); } - QuickCheck::new().quickcheck(prop as fn(_, _) -> _) + QuickCheck::new().quickcheck(prop as fn(_) -> _) } } diff --git a/protocols/gossipsub/src/topic.rs b/protocols/gossipsub/src/topic.rs index 6eacb9b3265..549dbd4547e 100644 --- a/protocols/gossipsub/src/topic.rs +++ b/protocols/gossipsub/src/topic.rs @@ -35,10 +35,6 @@ impl TopicHash { TopicHash { hash: hash.into() } } - pub fn into_string(self) -> String { - self.hash - } - pub fn as_str(&self) -> &str { &self.hash } @@ -80,6 +76,12 @@ impl Topic { } } +impl Into for TopicHash { + fn into(self) -> String { + self.hash + } +} + impl fmt::Display for Topic { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.topic) From 2b38112172f5c181e6213a54e0512feb04bcc7fa Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 1 Jul 2020 16:45:19 +1000 Subject: [PATCH 07/35] Address reviewers suggestions --- core/src/identity.rs | 46 +++---- core/src/identity/error.rs | 25 +++- protocols/gossipsub/src/behaviour.rs | 153 ++++++++++----------- protocols/gossipsub/src/behaviour/tests.rs | 40 +++--- protocols/gossipsub/src/config.rs | 72 ++++------ protocols/gossipsub/src/error.rs | 40 ++++++ protocols/gossipsub/src/lib.rs | 3 +- protocols/gossipsub/src/mcache.rs | 2 +- protocols/gossipsub/src/protocol.rs | 3 +- protocols/gossipsub/tests/smoke.rs | 7 +- 10 files changed, 211 insertions(+), 180 deletions(-) create mode 100644 protocols/gossipsub/src/error.rs diff --git a/core/src/identity.rs b/core/src/identity.rs index 4b65f1a5dbd..c7e01896757 100644 --- a/core/src/identity.rs +++ b/core/src/identity.rs @@ -29,7 +29,7 @@ pub mod secp256k1; pub mod error; use self::error::*; -use crate::{PeerId, keys_proto}; +use crate::{keys_proto, PeerId}; /// Identity keypair of a node. /// @@ -57,7 +57,7 @@ pub enum Keypair { Rsa(rsa::Keypair), /// A Secp256k1 keypair. #[cfg(feature = "secp256k1")] - Secp256k1(secp256k1::Keypair) + Secp256k1(secp256k1::Keypair), } impl Keypair { @@ -100,7 +100,7 @@ impl Keypair { #[cfg(not(target_arch = "wasm32"))] Rsa(ref pair) => pair.sign(msg), #[cfg(feature = "secp256k1")] - Secp256k1(ref pair) => pair.secret().sign(msg) + Secp256k1(ref pair) => pair.secret().sign(msg), } } @@ -127,7 +127,7 @@ pub enum PublicKey { Rsa(rsa::PublicKey), #[cfg(feature = "secp256k1")] /// A public Secp256k1 key. - Secp256k1(secp256k1::PublicKey) + Secp256k1(secp256k1::PublicKey), } impl PublicKey { @@ -142,7 +142,7 @@ impl PublicKey { #[cfg(not(target_arch = "wasm32"))] Rsa(pk) => pk.verify(msg, sig), #[cfg(feature = "secp256k1")] - Secp256k1(pk) => pk.verify(msg, sig) + Secp256k1(pk) => pk.verify(msg, sig), } } @@ -152,27 +152,26 @@ impl PublicKey { use prost::Message; let public_key = match self { - PublicKey::Ed25519(key) => - keys_proto::PublicKey { - r#type: keys_proto::KeyType::Ed25519 as i32, - data: key.encode().to_vec() - }, + PublicKey::Ed25519(key) => keys_proto::PublicKey { + r#type: keys_proto::KeyType::Ed25519 as i32, + data: key.encode().to_vec(), + }, #[cfg(not(target_arch = "wasm32"))] - PublicKey::Rsa(key) => - keys_proto::PublicKey { - r#type: keys_proto::KeyType::Rsa as i32, - data: key.encode_x509() - }, + PublicKey::Rsa(key) => keys_proto::PublicKey { + r#type: keys_proto::KeyType::Rsa as i32, + data: key.encode_x509(), + }, #[cfg(feature = "secp256k1")] - PublicKey::Secp256k1(key) => - keys_proto::PublicKey { - r#type: keys_proto::KeyType::Secp256k1 as i32, - data: key.encode().to_vec() - } + PublicKey::Secp256k1(key) => keys_proto::PublicKey { + r#type: keys_proto::KeyType::Secp256k1 as i32, + data: key.encode().to_vec(), + }, }; let mut buf = Vec::with_capacity(public_key.encoded_len()); - public_key.encode(&mut buf).expect("Vec provides capacity as needed"); + public_key + .encode(&mut buf) + .expect("Vec provides capacity as needed"); buf } @@ -191,7 +190,7 @@ impl PublicKey { match key_type { keys_proto::KeyType::Ed25519 => { ed25519::PublicKey::decode(&pubkey.data).map(PublicKey::Ed25519) - }, + } #[cfg(not(target_arch = "wasm32"))] keys_proto::KeyType::Rsa => { rsa::PublicKey::decode_x509(&pubkey.data).map(PublicKey::Rsa) @@ -200,7 +199,7 @@ impl PublicKey { keys_proto::KeyType::Rsa => { log::debug!("support for RSA was disabled at compile-time"); Err(DecodingError::new("Unsupported")) - }, + } #[cfg(feature = "secp256k1")] keys_proto::KeyType::Secp256k1 => { secp256k1::PublicKey::decode(&pubkey.data).map(PublicKey::Secp256k1) @@ -218,4 +217,3 @@ impl PublicKey { self.into() } } - diff --git a/core/src/identity/error.rs b/core/src/identity/error.rs index d89967a7477..5409d2a4ff7 100644 --- a/core/src/identity/error.rs +++ b/core/src/identity/error.rs @@ -27,16 +27,22 @@ use std::fmt; #[derive(Debug)] pub struct DecodingError { msg: String, - source: Option> + source: Option>, } impl DecodingError { pub(crate) fn new(msg: S) -> Self { - Self { msg: msg.to_string(), source: None } + Self { + msg: msg.to_string(), + source: None, + } } pub(crate) fn source(self, source: impl Error + Send + Sync + 'static) -> Self { - Self { source: Some(Box::new(source)), .. self } + Self { + source: Some(Box::new(source)), + ..self + } } } @@ -56,17 +62,23 @@ impl Error for DecodingError { #[derive(Debug)] pub struct SigningError { msg: String, - source: Option> + source: Option>, } /// An error during encoding of key material. impl SigningError { pub(crate) fn new(msg: S) -> Self { - Self { msg: msg.to_string(), source: None } + Self { + msg: msg.to_string(), + source: None, + } } pub(crate) fn source(self, source: impl Error + Send + Sync + 'static) -> Self { - Self { source: Some(Box::new(source)), .. self } + Self { + source: Some(Box::new(source)), + ..self + } } } @@ -81,4 +93,3 @@ impl Error for SigningError { self.source.as_ref().map(|s| &**s as &dyn Error) } } - diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 5090d219136..f53dbe341b6 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -18,7 +18,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::config::GossipsubConfig; +use crate::config::{GossipsubConfig, Signing}; +use crate::error::PublishError; use crate::handler::GossipsubHandler; use crate::mcache::MessageCache; use crate::protocol::{ @@ -28,7 +29,7 @@ use crate::protocol::{ use crate::rpc_proto; use crate::topic::{Topic, TopicHash}; use futures::prelude::*; -use libp2p_core::{connection::ConnectionId, identity::Keypair, Multiaddr, PeerId}; +use libp2p_core::{connection::ConnectionId, identity::error::SigningError, Multiaddr, PeerId}; use libp2p_swarm::{ NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters, ProtocolsHandler, }; @@ -59,12 +60,9 @@ pub struct Gossipsub { /// Pools non-urgent control messages between heartbeats. control_pool: HashMap>, - /// The local libp2p keypair, used for message source identification and signing. - keypair: Option, - - /// The peer_id that will be the source of published messages. This can be set to the identity - /// via a config, otherwise will be derived from the libp2p keypair. - message_source_id: PeerId, + /// The `PeerId` that will be the source of published messages. This can be set to an arbitrary + /// PeerId when the config is initialised via the `Signing` enum. + message_author: PeerId, /// A map of all connected peers - A map of topic hash to a list of gossipsub peer Ids. topic_peers: HashMap>, @@ -87,71 +85,54 @@ pub struct Gossipsub { /// Heartbeat interval stream. heartbeat: Interval, - /// The peer_id public to add to messages if signing is enabled and the current libp2p key is + /// The public `peer_id` to add to messages if signing is enabled and the current libp2p key is /// not inlined in the `PeerId`. key: Option>, } impl Gossipsub { - /// Creates a `Gossipsub` struct given a set of parameters specified by `gs_config`. - pub fn new(keypair: Keypair, gs_config: GossipsubConfig) -> Self { + /// Creates a `Gossipsub` struct given a set of parameters specified by via a `GossipsubConfig`. + pub fn new(config: GossipsubConfig) -> Self { // Set up the router given the configuration settings. - // Sets up the source_id of published messages. - let message_source_id = if gs_config.no_source_id { - PeerId::from_bytes(crate::config::IDENTITY_SOURCE.to_vec()).expect("Valid peer id") - } else { - keypair.public().into_peer_id() - }; - - // If signing is not enabled, we don't need to store the keypair. - let keypair = if gs_config.disable_message_signing { - None - } else { - Some(keypair) - }; - - // If we are signing and the public key cannot be inlined in the `PeerId`, we store the - // public key to attach to published messages. - let key = { - if let Some(keypair) = keypair.as_ref() { - // signing is required - let key_enc = keypair.public().into_protobuf_encoding(); - if key_enc.len() <= 42 { + // Set up the author and inlined key if required. + let (message_author, inlined_key) = match config.signing { + Signing::Enabled(ref kp) => { + let public_key = kp.public(); + let key_enc = public_key.clone().into_protobuf_encoding(); + let key = if key_enc.len() <= 42 { // The public key can be inlined in [`rpc_proto::Message::from`], so we don't include it // specifically in the [`rpc_proto::Message::key`] field. None } else { - // Include the protobuf encoding of the public key in the message + // Include the protobuf encoding of the public key in the message. Some(key_enc) - } - } else { - // Signing is not required, no key needs to be added - None + }; + (public_key.into_peer_id(), key) } + Signing::Disabled(ref peer_id) => (peer_id.clone(), None), }; Gossipsub { - config: gs_config.clone(), events: VecDeque::new(), control_pool: HashMap::new(), - keypair, - message_source_id, + message_author, topic_peers: HashMap::new(), peer_topics: HashMap::new(), mesh: HashMap::new(), fanout: HashMap::new(), fanout_last_pub: HashMap::new(), mcache: MessageCache::new( - gs_config.history_gossip, - gs_config.history_length, - gs_config.message_id_fn, + config.history_gossip, + config.history_length, + config.message_id_fn, ), heartbeat: Interval::new_at( - Instant::now() + gs_config.heartbeat_initial_delay, - gs_config.heartbeat_interval, + Instant::now() + config.heartbeat_initial_delay, + config.heartbeat_interval, ), - key, + key: inlined_key, + config, } } @@ -249,7 +230,7 @@ impl Gossipsub { } /// Publishes a message to the network. - pub fn publish(&mut self, topic: &Topic, data: impl Into>) { + pub fn publish(&mut self, topic: &Topic, data: impl Into>) -> Result<(), PublishError> { self.publish_many(iter::once(topic.clone()), data) } @@ -258,18 +239,11 @@ impl Gossipsub { &mut self, topics: impl IntoIterator, data: impl Into>, - ) { - let message = match self.build_message( + ) -> Result<(), PublishError> { + let message = self.build_message( topics.into_iter().map(|t| self.topic_hash(t)).collect(), data.into(), - ) { - Ok(m) => m, - Err(e) => { - error!("{}", e); - return; - } - }; - + )?; let msg_id = (self.config.message_id_fn)(&message); // Add published message to our received caches if self.mcache.put(message.clone()).is_some() { @@ -279,18 +253,18 @@ impl Gossipsub { "Not publishing a message that has already been published. Msg-id {}", msg_id ); - return; + return Err(PublishError::Duplicate); } debug!("Publishing message: {:?}", msg_id); - // Forward the message to mesh peers - let message_source = &self.message_source_id.clone(); - self.forward_msg(message.clone(), message_source); + // Forward the message to mesh peers. + let message_source = &self.message_author.clone(); + let mesh_peers_sent = self.forward_msg(message.clone(), message_source); let mut recipient_peers = HashSet::new(); for topic_hash in &message.topics { - // If not subscribed to the topic, use fanout peers + // If not subscribed to the topic, use fanout peers. if self.mesh.get(&topic_hash).is_none() { debug!("Topic: {:?} not in the mesh", topic_hash); // Build a list of peers to forward the message to @@ -319,7 +293,9 @@ impl Gossipsub { } } - info!("Published message: {:?}", msg_id); + if recipient_peers.is_empty() && !mesh_peers_sent { + return Err(PublishError::InsufficientPeers); + } let event = Arc::new(GossipsubRpc { subscriptions: Vec::new(), @@ -336,6 +312,9 @@ impl Gossipsub { handler: NotifyHandler::Any, }); } + + info!("Published message: {:?}", msg_id); + Ok(()) } /// This function should be called when `config.manual_propagation` is `true` in order to @@ -936,7 +915,8 @@ impl Gossipsub { } /// Helper function which forwards a message to mesh\[topic\] peers. - fn forward_msg(&mut self, message: GossipsubMessage, source: &PeerId) { + /// Returns true if at least one peer was messaged. + fn forward_msg(&mut self, message: GossipsubMessage, source: &PeerId) -> bool { let msg_id = (self.config.message_id_fn)(&message); debug!("Forwarding message: {:?}", msg_id); let mut recipient_peers = HashSet::new(); @@ -970,8 +950,11 @@ impl Gossipsub { handler: NotifyHandler::Any, }); } + debug!("Completed forwarding message"); + true + } else { + false } - debug!("Completed forwarding message"); } /// Constructs a `GossipsubMessage` performing message signing if required. @@ -979,19 +962,20 @@ impl Gossipsub { &self, topics: Vec, data: Vec, - ) -> Result { + ) -> Result { let sequence_number: u64 = rand::random(); - let message = rpc_proto::Message { - from: Some(self.message_source_id.clone().into_bytes()), - data: Some(data), - seqno: Some(sequence_number.to_be_bytes().to_vec()), - topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(), - signature: None, - key: None, - }; // If a signature is required, generate it - let signature = if let Some(keypair) = self.keypair.as_ref() { + let signature = if let Signing::Enabled(ref keypair) = self.config.signing { + let message = rpc_proto::Message { + from: Some(self.message_author.clone().into_bytes()), + data: Some(data.clone()), + seqno: Some(sequence_number.to_be_bytes().to_vec()), + topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(), + signature: None, + key: None, + }; + let mut buf = Vec::with_capacity(message.encoded_len()); message .encode(&mut buf) @@ -999,18 +983,14 @@ impl Gossipsub { // the signature is over the bytes "libp2p-pubsub:" let mut signature_bytes = SIGNING_PREFIX.to_vec(); signature_bytes.extend_from_slice(&buf); - Some( - keypair - .sign(&signature_bytes) - .map_err(|e| format!("Signing error: {}", e))?, - ) + Some(keypair.sign(&signature_bytes)?) } else { None }; Ok(GossipsubMessage { - source: self.message_source_id.clone(), - data: message.data.expect("data exists"), + source: self.message_author.clone(), + data, // To be interoperable with the go-implementation this is treated as a 64-bit // big-endian uint. sequence_number, @@ -1092,10 +1072,17 @@ impl NetworkBehaviour for Gossipsub { type OutEvent = GossipsubEvent; fn new_handler(&mut self) -> Self::ProtocolsHandler { + // let the handler know if we should verify signatures. If message signing is enabled, we + // verify signatures of messages. + let verify_signatures = match self.config.signing { + Signing::Enabled(_) => true, + Signing::Disabled(_) => false, + }; + GossipsubHandler::new( self.config.protocol_id.clone(), self.config.max_transmit_size, - self.keypair.is_some(), // if a keypair is stored we want to verify signatures + verify_signatures, ) } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 21da840aaa3..725d4754628 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -33,10 +33,11 @@ mod tests { topics: Vec, to_subscribe: bool, ) -> (Gossipsub, Vec, Vec) { - // generate a default GossipsubConfig - let gs_config = GossipsubConfig::default(); + let keypair = libp2p_core::identity::Keypair::generate_secp256k1(); + // generate a default GossipsubConfig with signing + let gs_config = GossipsubConfig::new(Signing::Enabled(keypair)); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(Keypair::generate_secp256k1(), gs_config); + let mut gs: Gossipsub = Gossipsub::new(gs_config); let mut topic_hashes = vec![]; @@ -312,9 +313,16 @@ mod tests { "Subscribe should add a new entry to the mesh[topic] hashmap" ); + // peers should be subscribed to the topic + assert!( + gs.topic_peers.get(&topic_hashes[0]).map(|p| p.is_empty()) == Some(false), + "Peers should be subscribed to the topic" + ); + // publish on topic let publish_data = vec![0; 42]; - gs.publish(&Topic::new(publish_topic), publish_data); + gs.publish(&Topic::new(publish_topic), publish_data) + .unwrap(); // Collect all publish messages let publishes = gs @@ -367,7 +375,8 @@ mod tests { // Publish on unsubscribed topic let publish_data = vec![0; 42]; - gs.publish(&Topic::new(fanout_topic.clone()), publish_data); + gs.publish(&Topic::new(fanout_topic.clone()), publish_data) + .unwrap(); assert_eq!( gs.fanout @@ -546,10 +555,10 @@ mod tests { /// Test Gossipsub.get_random_peers() function fn test_get_random_peers() { // generate a default GossipsubConfig - let gs_config = GossipsubConfig::default(); - let key = Keypair::generate_secp256k1(); + let key = libp2p_core::identity::Keypair::generate_secp256k1(); + let gs_config = GossipsubConfig::new(Signing::Enabled(key)); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(key, gs_config); + let mut gs: Gossipsub = Gossipsub::new(gs_config); // create a topic and fill it with some peers let topic_hash = Topic::new("Test".into()).no_hash().clone(); @@ -560,23 +569,18 @@ mod tests { gs.topic_peers.insert(topic_hash.clone(), peers.clone()); - let random_peers = - Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, |_| true); + let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, |_| true); assert!(random_peers.len() == 5, "Expected 5 peers to be returned"); - let random_peers = - Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 30, |_| true); + let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 30, |_| true); assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); assert!(random_peers == peers, "Expected no shuffling"); - let random_peers = - Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 20, |_| true); + let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 20, |_| true); assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); assert!(random_peers == peers, "Expected no shuffling"); - let random_peers = - Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 0, |_| true); + let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 0, |_| true); assert!(random_peers.len() == 0, "Expected 0 peers to be returned"); // test the filter - let random_peers = - Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, |_| false); + let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, |_| false); assert!(random_peers.len() == 0, "Expected 0 peers to be returned"); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 10, { |peer| peers.contains(peer) diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index cb91205afc8..829c81bbef8 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -19,12 +19,23 @@ // DEALINGS IN THE SOFTWARE. use crate::protocol::{GossipsubMessage, MessageId}; +use libp2p_core::{identity::Keypair, PeerId}; use std::borrow::Cow; use std::time::Duration; -/// If the `no_source_id` flag is set, the IDENTITY_SOURCE value is used as the source of the -/// packet. -pub const IDENTITY_SOURCE: [u8; 3] = [0, 1, 0]; +/// Determines message signing is enabled or not. +/// +/// If message signing is disabled a `PeerId` can be entered which will be used as the author of +/// any published message. +#[derive(Clone)] +pub enum Signing { + /// Message signing is enabled. All messages will be signed and all received messages will be + /// verified. + Enabled(Keypair), + /// Message signing is disabled and the associated `PeerId` will be used as the author for any + /// published message. + Disabled(PeerId), +} /// Configuration parameters that define the performance of the gossipsub network. #[derive(Clone)] @@ -67,19 +78,12 @@ pub struct GossipsubConfig { /// Flag determining if gossipsub topics are hashed or sent as plain strings (default is false). pub hash_topics: bool, - /// When set, all published messages will have a 0 source `PeerId` (default is false). - pub no_source_id: bool, - /// When set to `true`, prevents automatic forwarding of all received messages. This setting /// allows a user to validate the messages before propagating them to their peers. If set to /// true, the user must manually call `propagate_message()` on the behaviour to forward message /// once validated (default is `false`). pub manual_propagation: bool, - /// Message signing is on by default. When this parameter is set, - /// published messages are not signed by the libp2p key. - pub disable_message_signing: bool, - /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this /// parameter allows the user to address packets arbitrarily. One example is content based @@ -89,10 +93,13 @@ pub struct GossipsubConfig { /// The function takes a `GossipsubMessage` as input and outputs a String to be interpreted as /// the message id. pub message_id_fn: fn(&GossipsubMessage) -> MessageId, + + /// Determines if message signing is enabled or not. + pub signing: Signing, } -impl Default for GossipsubConfig { - fn default() -> GossipsubConfig { +impl GossipsubConfig { + pub fn new(signing: Signing) -> GossipsubConfig { GossipsubConfig { protocol_id: Cow::Borrowed(b"/meshsub/1.0.0"), history_length: 5, @@ -106,15 +113,14 @@ impl Default for GossipsubConfig { fanout_ttl: Duration::from_secs(60), max_transmit_size: 2048, hash_topics: false, // default compatibility with floodsub - no_source_id: false, manual_propagation: false, - disable_message_signing: false, message_id_fn: |message| { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); source_string.push_str(&message.sequence_number.to_string()); MessageId(source_string) }, + signing, } } } @@ -124,18 +130,12 @@ pub struct GossipsubConfigBuilder { config: GossipsubConfig, } -impl Default for GossipsubConfigBuilder { - fn default() -> GossipsubConfigBuilder { - GossipsubConfigBuilder { - config: GossipsubConfig::default(), - } - } -} - impl GossipsubConfigBuilder { // set default values - pub fn new() -> GossipsubConfigBuilder { - GossipsubConfigBuilder::default() + pub fn new(signing: Signing) -> GossipsubConfigBuilder { + GossipsubConfigBuilder { + config: GossipsubConfig::new(signing), + } } /// The protocol id to negotiate this protocol (default is `/meshsub/1.0.0`). @@ -231,16 +231,6 @@ impl GossipsubConfigBuilder { self } - /// When set, all published messages will have a 0 source `PeerId` - pub fn no_source_id(&mut self) -> &mut Self { - assert!( - self.config.disable_message_signing, - "Message signing must be disabled in order to mask the source peer id. Cannot sign for the 0 peer_id" - ); - self.config.no_source_id = true; - self - } - /// When set, prevents automatic forwarding of all received messages. This setting /// allows a user to validate the messages before propagating them to their peers. If set, /// the user must manually call `propagate_message()` on the behaviour to forward a message @@ -250,12 +240,6 @@ impl GossipsubConfigBuilder { self } - /// Disables message signing for all published messages. - pub fn disable_message_signing(&mut self) -> &mut Self { - self.config.disable_message_signing = true; - self - } - /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this /// parameter allows the user to address packets arbitrarily. One example is content based @@ -279,6 +263,12 @@ impl std::fmt::Debug for GossipsubConfig { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let mut builder = f.debug_struct("GossipsubConfig"); let _ = builder.field("protocol_id", &self.protocol_id); + let (author, signing) = match &self.signing { + Signing::Enabled(kp) => (kp.public().into_peer_id(), true), + Signing::Disabled(author) => (author.clone(), false), + }; + let _ = builder.field("signing", &signing); + let _ = builder.field("message_author", &author); let _ = builder.field("history_length", &self.history_length); let _ = builder.field("history_gossip", &self.history_gossip); let _ = builder.field("mesh_n", &self.mesh_n); @@ -290,9 +280,7 @@ impl std::fmt::Debug for GossipsubConfig { let _ = builder.field("fanout_ttl", &self.fanout_ttl); let _ = builder.field("max_transmit_size", &self.max_transmit_size); let _ = builder.field("hash_topics", &self.hash_topics); - let _ = builder.field("no_source_id", &self.no_source_id); let _ = builder.field("manual_propagation", &self.manual_propagation); - let _ = builder.field("disable_message_signing", &self.disable_message_signing); builder.finish() } } diff --git a/protocols/gossipsub/src/error.rs b/protocols/gossipsub/src/error.rs new file mode 100644 index 00000000000..371032f79fb --- /dev/null +++ b/protocols/gossipsub/src/error.rs @@ -0,0 +1,40 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Error types than can result from gossipsub. + +use libp2p_core::identity::error::SigningError; + +/// Error associated with publishing a gossipsub message. +#[derive(Debug)] +pub enum PublishError { + /// This message has already been published. + Duplicate, + /// An error occurred whilst signing the message. + SigningError(SigningError), + /// There were no peers to send this message to. + InsufficientPeers, +} + +impl From for PublishError { + fn from(error: SigningError) -> Self { + PublishError::SigningError(error) + } +} diff --git a/protocols/gossipsub/src/lib.rs b/protocols/gossipsub/src/lib.rs index e0efa955714..58f278b7955 100644 --- a/protocols/gossipsub/src/lib.rs +++ b/protocols/gossipsub/src/lib.rs @@ -135,6 +135,7 @@ //! println!("Listening on {:?}", addr); //! ``` +pub mod error; pub mod protocol; mod behaviour; @@ -148,6 +149,6 @@ mod rpc_proto { } pub use self::behaviour::{Gossipsub, GossipsubEvent, GossipsubRpc}; -pub use self::config::{GossipsubConfig, GossipsubConfigBuilder}; +pub use self::config::{GossipsubConfig, GossipsubConfigBuilder, Signing}; pub use self::protocol::{GossipsubMessage, MessageId}; pub use self::topic::{Topic, TopicHash}; diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index 91959807950..e91a0602c6f 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -84,7 +84,7 @@ impl MessageCache { let seen_message = self.msgs.insert(message_id, msg); if seen_message.is_none() { - // Don't add duplicate entries to the cache + // Don't add duplicate entries to the cache. self.history[0].push(cache_entry); } seen_message diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index fd871641e83..fe10df28c00 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -511,7 +511,8 @@ mod tests { let keypair = TestKeypair::arbitrary(g); // generate an arbitrary GossipsubMessage using the behaviour signing functionality - let gs = Gossipsub::new(keypair.0.clone(), GossipsubConfig::default()); + let config = GossipsubConfig::new(crate::config::Signing::Enabled(keypair.0.clone())); + let gs = Gossipsub::new(config); let data = (0..g.gen_range(1, 1024)).map(|_| g.gen()).collect(); let topics = Vec::arbitrary(g) .into_iter() diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index 6a911ac86ba..c049501efe6 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -33,7 +33,7 @@ use libp2p_core::{ identity, multiaddr::Protocol, muxing::StreamMuxerBox, transport::MemoryTransport, upgrade, Multiaddr, Transport, }; -use libp2p_gossipsub::{Gossipsub, GossipsubConfig, GossipsubEvent, Topic}; +use libp2p_gossipsub::{Gossipsub, GossipsubConfig, GossipsubEvent, Signing, Topic}; use libp2p_plaintext::PlainText2Config; use libp2p_swarm::Swarm; use libp2p_yamux as yamux; @@ -145,7 +145,8 @@ fn build_node() -> (Multiaddr, Swarm) { .boxed(); let peer_id = public_key.clone().into_peer_id(); - let behaviour = Gossipsub::new(key, GossipsubConfig::default()); + let config = GossipsubConfig::new(Signing::Enabled(key)); + let behaviour = Gossipsub::new(config); let mut swarm = Swarm::new(transport, behaviour, peer_id); let port = 1 + random::(); @@ -193,7 +194,7 @@ fn multi_hop_propagation() { }); // Publish a single message. - graph.nodes[0].1.publish(&topic, vec![1, 2, 3]); + graph.nodes[0].1.publish(&topic, vec![1, 2, 3]).unwrap(); // Wait for all nodes to receive the published message. let mut received_msgs = 0; From 7d678f593e0677994c0fa804b765130b9deed5a1 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 1 Jul 2020 16:52:51 +1000 Subject: [PATCH 08/35] Send subscriptions to all peers --- protocols/gossipsub/src/behaviour.rs | 52 ++++++++++++---------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index f53dbe341b6..89c1ab09007 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -147,21 +147,17 @@ impl Gossipsub { return false; } - // send subscription request to all peers in the topic - if let Some(peer_list) = self.topic_peers.get(&topic_hash) { - let mut fixed_event = None; // initialise the event once if needed - if fixed_event.is_none() { - fixed_event = Some(Arc::new(GossipsubRpc { - messages: Vec::new(), - subscriptions: vec![GossipsubSubscription { - topic_hash: topic_hash.clone(), - action: GossipsubSubscriptionAction::Subscribe, - }], - control_msgs: Vec::new(), - })); - } - - let event = fixed_event.expect("event has been initialised"); + // send subscription request to all peers + let peer_list = self.peer_topics.keys().collect::>(); + if !peer_list.is_empty() { + let event = Arc::new(GossipsubRpc { + messages: Vec::new(), + subscriptions: vec![GossipsubSubscription { + topic_hash: topic_hash.clone(), + action: GossipsubSubscriptionAction::Subscribe, + }], + control_msgs: Vec::new(), + }); for peer in peer_list { debug!("Sending SUBSCRIBE to peer: {:?}", peer); @@ -194,21 +190,17 @@ impl Gossipsub { return false; } - // announce to all peers in the topic - let mut fixed_event = None; // initialise the event once if needed - if let Some(peer_list) = self.topic_peers.get(topic_hash) { - if fixed_event.is_none() { - fixed_event = Some(Arc::new(GossipsubRpc { - messages: Vec::new(), - subscriptions: vec![GossipsubSubscription { - topic_hash: topic_hash.clone(), - action: GossipsubSubscriptionAction::Unsubscribe, - }], - control_msgs: Vec::new(), - })); - } - - let event = fixed_event.expect("event has been initialised"); + // announce to all peers + let peer_list = self.peer_topics.keys().collect::>(); + if !peer_list.is_empty() { + let event = Arc::new(GossipsubRpc { + messages: Vec::new(), + subscriptions: vec![GossipsubSubscription { + topic_hash: topic_hash.clone(), + action: GossipsubSubscriptionAction::Unsubscribe, + }], + control_msgs: Vec::new(), + }); for peer in peer_list { debug!("Sending UNSUBSCRIBE to peer: {:?}", peer); From dc050dd622d01ef52a05c69fa1eb18a3ac38d114 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 1 Jul 2020 17:16:10 +1000 Subject: [PATCH 09/35] Update examples --- examples/gossipsub-chat.rs | 12 +++++++----- examples/ipfs-private.rs | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index fe46dd30612..e2fc4f1e2a8 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -50,7 +50,7 @@ use async_std::{io, task}; use env_logger::{Builder, Env}; use futures::prelude::*; use libp2p::gossipsub::protocol::MessageId; -use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, Topic}; +use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, Signing, Topic}; use libp2p::{gossipsub, identity, PeerId}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -87,13 +87,13 @@ fn main() -> Result<(), Box> { }; // set custom gossipsub - let gossipsub_config = gossipsub::GossipsubConfigBuilder::new() + let gossipsub_config = gossipsub::GossipsubConfigBuilder::new(Signing::Enabled(local_key)) .heartbeat_interval(Duration::from_secs(10)) .message_id_fn(message_id_fn) // content-address messages. No two messages of the //same content will be propagated. .build(); // build a gossipsub network behaviour - let mut gossipsub = gossipsub::Gossipsub::new(local_key, gossipsub_config); + let mut gossipsub = gossipsub::Gossipsub::new(gossipsub_config); gossipsub.subscribe(topic.clone()); libp2p::Swarm::new(transport, gossipsub, local_peer_id) }; @@ -120,11 +120,13 @@ fn main() -> Result<(), Box> { let mut listening = false; task::block_on(future::poll_fn(move |cx: &mut Context| { loop { - match stdin.try_poll_next_unpin(cx)? { + if let Err(e) = match stdin.try_poll_next_unpin(cx)? { Poll::Ready(Some(line)) => swarm.publish(&topic, line.as_bytes()), Poll::Ready(None) => panic!("Stdin closed"), Poll::Pending => break, - }; + } { + println!("Publish error: {:?}", e); + } } loop { diff --git a/examples/ipfs-private.rs b/examples/ipfs-private.rs index 30e1b91846c..23837a41e13 100644 --- a/examples/ipfs-private.rs +++ b/examples/ipfs-private.rs @@ -35,7 +35,7 @@ use async_std::{io, task}; use futures::{future, prelude::*}; use libp2p::{ core::{either::EitherTransport, transport::upgrade::Version, StreamMuxer}, - gossipsub::{self, Gossipsub, GossipsubConfigBuilder, GossipsubEvent}, + gossipsub::{self, Gossipsub, GossipsubConfigBuilder, GossipsubEvent, Signing}, identify::{Identify, IdentifyEvent}, identity, multiaddr::Protocol, @@ -239,11 +239,11 @@ fn main() -> Result<(), Box> { // Create a Swarm to manage peers and events let mut swarm = { - let gossipsub_config = GossipsubConfigBuilder::default() + let gossipsub_config = GossipsubConfigBuilder::new(Signing::Enabled(local_key.clone())) .max_transmit_size(262144) .build(); let mut behaviour = MyBehaviour { - gossipsub: Gossipsub::new(local_key.clone(), gossipsub_config), + gossipsub: Gossipsub::new(gossipsub_config), identify: Identify::new( "/ipfs/0.1.0".into(), "rust-ipfs-example".into(), @@ -274,12 +274,14 @@ fn main() -> Result<(), Box> { let mut listening = false; task::block_on(future::poll_fn(move |cx: &mut Context| { loop { - match stdin.try_poll_next_unpin(cx)? { + if let Err(e) = match stdin.try_poll_next_unpin(cx)? { Poll::Ready(Some(line)) => { - swarm.gossipsub.publish(&gossipsub_topic, line.as_bytes()); + swarm.gossipsub.publish(&gossipsub_topic, line.as_bytes()) } Poll::Ready(None) => panic!("Stdin closed"), Poll::Pending => break, + } { + println!("Publish error: {:?}", e); } } loop { From 5bf1cfcf89307ae2e99029f2a89fba400647d2e8 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 1 Jul 2020 18:08:30 +1000 Subject: [PATCH 10/35] Shift signing option into the behaviour --- examples/gossipsub-chat.rs | 5 +-- examples/ipfs-private.rs | 4 +-- protocols/gossipsub/src/behaviour.rs | 41 +++++++++++++++------- protocols/gossipsub/src/behaviour/tests.rs | 8 ++--- protocols/gossipsub/src/config.rs | 41 +++++++--------------- protocols/gossipsub/src/lib.rs | 4 +-- protocols/gossipsub/src/protocol.rs | 4 +-- protocols/gossipsub/tests/smoke.rs | 4 +-- 8 files changed, 56 insertions(+), 55 deletions(-) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index e2fc4f1e2a8..da9a60ebac6 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -87,13 +87,14 @@ fn main() -> Result<(), Box> { }; // set custom gossipsub - let gossipsub_config = gossipsub::GossipsubConfigBuilder::new(Signing::Enabled(local_key)) + let gossipsub_config = gossipsub::GossipsubConfigBuilder::new() .heartbeat_interval(Duration::from_secs(10)) .message_id_fn(message_id_fn) // content-address messages. No two messages of the //same content will be propagated. .build(); // build a gossipsub network behaviour - let mut gossipsub = gossipsub::Gossipsub::new(gossipsub_config); + let mut gossipsub = + gossipsub::Gossipsub::new(Signing::Enabled(local_key), gossipsub_config); gossipsub.subscribe(topic.clone()); libp2p::Swarm::new(transport, gossipsub, local_peer_id) }; diff --git a/examples/ipfs-private.rs b/examples/ipfs-private.rs index 23837a41e13..727f29a8696 100644 --- a/examples/ipfs-private.rs +++ b/examples/ipfs-private.rs @@ -239,11 +239,11 @@ fn main() -> Result<(), Box> { // Create a Swarm to manage peers and events let mut swarm = { - let gossipsub_config = GossipsubConfigBuilder::new(Signing::Enabled(local_key.clone())) + let gossipsub_config = GossipsubConfigBuilder::new() .max_transmit_size(262144) .build(); let mut behaviour = MyBehaviour { - gossipsub: Gossipsub::new(gossipsub_config), + gossipsub: Gossipsub::new(Signing::Enabled(local_key.clone()), gossipsub_config), identify: Identify::new( "/ipfs/0.1.0".into(), "rust-ipfs-example".into(), diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 89c1ab09007..5bd6ca3b619 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -18,7 +18,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::config::{GossipsubConfig, Signing}; +use crate::config::GossipsubConfig; use crate::error::PublishError; use crate::handler::GossipsubHandler; use crate::mcache::MessageCache; @@ -29,7 +29,9 @@ use crate::protocol::{ use crate::rpc_proto; use crate::topic::{Topic, TopicHash}; use futures::prelude::*; -use libp2p_core::{connection::ConnectionId, identity::error::SigningError, Multiaddr, PeerId}; +use libp2p_core::{ + connection::ConnectionId, identity::error::SigningError, identity::Keypair, Multiaddr, PeerId, +}; use libp2p_swarm::{ NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters, ProtocolsHandler, }; @@ -49,6 +51,20 @@ use wasm_timer::{Instant, Interval}; mod tests; +/// Determines message signing is enabled or not. +/// +/// If message signing is disabled a `PeerId` can be entered which will be used as the author of +/// any published message. +#[derive(Clone)] +pub enum Signing { + /// Message signing is enabled. All messages will be signed and all received messages will be + /// verified. + Enabled(Keypair), + /// Message signing is disabled and the associated `PeerId` will be used as the author for any + /// published message. + Disabled(PeerId), +} + /// Network behaviour that handles the gossipsub protocol. pub struct Gossipsub { /// Configuration providing gossipsub performance parameters. @@ -64,6 +80,9 @@ pub struct Gossipsub { /// PeerId when the config is initialised via the `Signing` enum. message_author: PeerId, + /// An optional keypair for message signing. + keypair: Option, + /// A map of all connected peers - A map of topic hash to a list of gossipsub peer Ids. topic_peers: HashMap>, @@ -92,12 +111,12 @@ pub struct Gossipsub { impl Gossipsub { /// Creates a `Gossipsub` struct given a set of parameters specified by via a `GossipsubConfig`. - pub fn new(config: GossipsubConfig) -> Self { + pub fn new(signing: Signing, config: GossipsubConfig) -> Self { // Set up the router given the configuration settings. // Set up the author and inlined key if required. - let (message_author, inlined_key) = match config.signing { - Signing::Enabled(ref kp) => { + let (message_author, keypair, inlined_key) = match signing { + Signing::Enabled(kp) => { let public_key = kp.public(); let key_enc = public_key.clone().into_protobuf_encoding(); let key = if key_enc.len() <= 42 { @@ -108,15 +127,16 @@ impl Gossipsub { // Include the protobuf encoding of the public key in the message. Some(key_enc) }; - (public_key.into_peer_id(), key) + (public_key.into_peer_id(), Some(kp), key) } - Signing::Disabled(ref peer_id) => (peer_id.clone(), None), + Signing::Disabled(peer_id) => (peer_id, None, None), }; Gossipsub { events: VecDeque::new(), control_pool: HashMap::new(), message_author, + keypair, topic_peers: HashMap::new(), peer_topics: HashMap::new(), mesh: HashMap::new(), @@ -958,7 +978,7 @@ impl Gossipsub { let sequence_number: u64 = rand::random(); // If a signature is required, generate it - let signature = if let Signing::Enabled(ref keypair) = self.config.signing { + let signature = if let Some(keypair) = self.keypair.as_ref() { let message = rpc_proto::Message { from: Some(self.message_author.clone().into_bytes()), data: Some(data.clone()), @@ -1066,10 +1086,7 @@ impl NetworkBehaviour for Gossipsub { fn new_handler(&mut self) -> Self::ProtocolsHandler { // let the handler know if we should verify signatures. If message signing is enabled, we // verify signatures of messages. - let verify_signatures = match self.config.signing { - Signing::Enabled(_) => true, - Signing::Disabled(_) => false, - }; + let verify_signatures = self.keypair.is_some(); GossipsubHandler::new( self.config.protocol_id.clone(), diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 725d4754628..c33be79f99a 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -35,9 +35,9 @@ mod tests { ) -> (Gossipsub, Vec, Vec) { let keypair = libp2p_core::identity::Keypair::generate_secp256k1(); // generate a default GossipsubConfig with signing - let gs_config = GossipsubConfig::new(Signing::Enabled(keypair)); + let gs_config = GossipsubConfig::default(); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(gs_config); + let mut gs: Gossipsub = Gossipsub::new(Signing::Enabled(keypair), gs_config); let mut topic_hashes = vec![]; @@ -556,9 +556,9 @@ mod tests { fn test_get_random_peers() { // generate a default GossipsubConfig let key = libp2p_core::identity::Keypair::generate_secp256k1(); - let gs_config = GossipsubConfig::new(Signing::Enabled(key)); + let gs_config = GossipsubConfig::default(); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(gs_config); + let mut gs: Gossipsub = Gossipsub::new(Signing::Enabled(key), gs_config); // create a topic and fill it with some peers let topic_hash = Topic::new("Test".into()).no_hash().clone(); diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 829c81bbef8..21e349a3494 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -19,24 +19,9 @@ // DEALINGS IN THE SOFTWARE. use crate::protocol::{GossipsubMessage, MessageId}; -use libp2p_core::{identity::Keypair, PeerId}; use std::borrow::Cow; use std::time::Duration; -/// Determines message signing is enabled or not. -/// -/// If message signing is disabled a `PeerId` can be entered which will be used as the author of -/// any published message. -#[derive(Clone)] -pub enum Signing { - /// Message signing is enabled. All messages will be signed and all received messages will be - /// verified. - Enabled(Keypair), - /// Message signing is disabled and the associated `PeerId` will be used as the author for any - /// published message. - Disabled(PeerId), -} - /// Configuration parameters that define the performance of the gossipsub network. #[derive(Clone)] pub struct GossipsubConfig { @@ -93,13 +78,10 @@ pub struct GossipsubConfig { /// The function takes a `GossipsubMessage` as input and outputs a String to be interpreted as /// the message id. pub message_id_fn: fn(&GossipsubMessage) -> MessageId, - - /// Determines if message signing is enabled or not. - pub signing: Signing, } -impl GossipsubConfig { - pub fn new(signing: Signing) -> GossipsubConfig { +impl Default for GossipsubConfig { + fn default() -> GossipsubConfig { GossipsubConfig { protocol_id: Cow::Borrowed(b"/meshsub/1.0.0"), history_length: 5, @@ -120,7 +102,6 @@ impl GossipsubConfig { source_string.push_str(&message.sequence_number.to_string()); MessageId(source_string) }, - signing, } } } @@ -130,11 +111,19 @@ pub struct GossipsubConfigBuilder { config: GossipsubConfig, } +impl Default for GossipsubConfigBuilder { + fn default() -> GossipsubConfigBuilder { + GossipsubConfigBuilder { + config: GossipsubConfig::default(), + } + } +} + impl GossipsubConfigBuilder { // set default values - pub fn new(signing: Signing) -> GossipsubConfigBuilder { + pub fn new() -> GossipsubConfigBuilder { GossipsubConfigBuilder { - config: GossipsubConfig::new(signing), + config: GossipsubConfig::default(), } } @@ -263,12 +252,6 @@ impl std::fmt::Debug for GossipsubConfig { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let mut builder = f.debug_struct("GossipsubConfig"); let _ = builder.field("protocol_id", &self.protocol_id); - let (author, signing) = match &self.signing { - Signing::Enabled(kp) => (kp.public().into_peer_id(), true), - Signing::Disabled(author) => (author.clone(), false), - }; - let _ = builder.field("signing", &signing); - let _ = builder.field("message_author", &author); let _ = builder.field("history_length", &self.history_length); let _ = builder.field("history_gossip", &self.history_gossip); let _ = builder.field("mesh_n", &self.mesh_n); diff --git a/protocols/gossipsub/src/lib.rs b/protocols/gossipsub/src/lib.rs index 58f278b7955..22d8448462e 100644 --- a/protocols/gossipsub/src/lib.rs +++ b/protocols/gossipsub/src/lib.rs @@ -148,7 +148,7 @@ mod rpc_proto { include!(concat!(env!("OUT_DIR"), "/gossipsub.pb.rs")); } -pub use self::behaviour::{Gossipsub, GossipsubEvent, GossipsubRpc}; -pub use self::config::{GossipsubConfig, GossipsubConfigBuilder, Signing}; +pub use self::behaviour::{Gossipsub, GossipsubEvent, GossipsubRpc, Signing}; +pub use self::config::{GossipsubConfig, GossipsubConfigBuilder}; pub use self::protocol::{GossipsubMessage, MessageId}; pub use self::topic::{Topic, TopicHash}; diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index fe10df28c00..a5129c57afb 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -511,8 +511,8 @@ mod tests { let keypair = TestKeypair::arbitrary(g); // generate an arbitrary GossipsubMessage using the behaviour signing functionality - let config = GossipsubConfig::new(crate::config::Signing::Enabled(keypair.0.clone())); - let gs = Gossipsub::new(config); + let config = GossipsubConfig::default(); + let gs = Gossipsub::new(crate::Signing::Enabled(keypair.0.clone()), config); let data = (0..g.gen_range(1, 1024)).map(|_| g.gen()).collect(); let topics = Vec::arbitrary(g) .into_iter() diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index c049501efe6..4caf5c5e6e4 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -145,8 +145,8 @@ fn build_node() -> (Multiaddr, Swarm) { .boxed(); let peer_id = public_key.clone().into_peer_id(); - let config = GossipsubConfig::new(Signing::Enabled(key)); - let behaviour = Gossipsub::new(config); + let config = GossipsubConfig::default(); + let behaviour = Gossipsub::new(Signing::Disabled(peer_id.clone()), config); let mut swarm = Swarm::new(transport, behaviour, peer_id); let port = 1 + random::(); From 4ffe7280f3c46ffcb0b45cbcd997bf0e2de842e6 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Thu, 9 Jul 2020 11:37:07 +1000 Subject: [PATCH 11/35] Revert changes to core/ --- core/src/identity.rs | 46 +++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/core/src/identity.rs b/core/src/identity.rs index c7e01896757..4b65f1a5dbd 100644 --- a/core/src/identity.rs +++ b/core/src/identity.rs @@ -29,7 +29,7 @@ pub mod secp256k1; pub mod error; use self::error::*; -use crate::{keys_proto, PeerId}; +use crate::{PeerId, keys_proto}; /// Identity keypair of a node. /// @@ -57,7 +57,7 @@ pub enum Keypair { Rsa(rsa::Keypair), /// A Secp256k1 keypair. #[cfg(feature = "secp256k1")] - Secp256k1(secp256k1::Keypair), + Secp256k1(secp256k1::Keypair) } impl Keypair { @@ -100,7 +100,7 @@ impl Keypair { #[cfg(not(target_arch = "wasm32"))] Rsa(ref pair) => pair.sign(msg), #[cfg(feature = "secp256k1")] - Secp256k1(ref pair) => pair.secret().sign(msg), + Secp256k1(ref pair) => pair.secret().sign(msg) } } @@ -127,7 +127,7 @@ pub enum PublicKey { Rsa(rsa::PublicKey), #[cfg(feature = "secp256k1")] /// A public Secp256k1 key. - Secp256k1(secp256k1::PublicKey), + Secp256k1(secp256k1::PublicKey) } impl PublicKey { @@ -142,7 +142,7 @@ impl PublicKey { #[cfg(not(target_arch = "wasm32"))] Rsa(pk) => pk.verify(msg, sig), #[cfg(feature = "secp256k1")] - Secp256k1(pk) => pk.verify(msg, sig), + Secp256k1(pk) => pk.verify(msg, sig) } } @@ -152,26 +152,27 @@ impl PublicKey { use prost::Message; let public_key = match self { - PublicKey::Ed25519(key) => keys_proto::PublicKey { - r#type: keys_proto::KeyType::Ed25519 as i32, - data: key.encode().to_vec(), - }, + PublicKey::Ed25519(key) => + keys_proto::PublicKey { + r#type: keys_proto::KeyType::Ed25519 as i32, + data: key.encode().to_vec() + }, #[cfg(not(target_arch = "wasm32"))] - PublicKey::Rsa(key) => keys_proto::PublicKey { - r#type: keys_proto::KeyType::Rsa as i32, - data: key.encode_x509(), - }, + PublicKey::Rsa(key) => + keys_proto::PublicKey { + r#type: keys_proto::KeyType::Rsa as i32, + data: key.encode_x509() + }, #[cfg(feature = "secp256k1")] - PublicKey::Secp256k1(key) => keys_proto::PublicKey { - r#type: keys_proto::KeyType::Secp256k1 as i32, - data: key.encode().to_vec(), - }, + PublicKey::Secp256k1(key) => + keys_proto::PublicKey { + r#type: keys_proto::KeyType::Secp256k1 as i32, + data: key.encode().to_vec() + } }; let mut buf = Vec::with_capacity(public_key.encoded_len()); - public_key - .encode(&mut buf) - .expect("Vec provides capacity as needed"); + public_key.encode(&mut buf).expect("Vec provides capacity as needed"); buf } @@ -190,7 +191,7 @@ impl PublicKey { match key_type { keys_proto::KeyType::Ed25519 => { ed25519::PublicKey::decode(&pubkey.data).map(PublicKey::Ed25519) - } + }, #[cfg(not(target_arch = "wasm32"))] keys_proto::KeyType::Rsa => { rsa::PublicKey::decode_x509(&pubkey.data).map(PublicKey::Rsa) @@ -199,7 +200,7 @@ impl PublicKey { keys_proto::KeyType::Rsa => { log::debug!("support for RSA was disabled at compile-time"); Err(DecodingError::new("Unsupported")) - } + }, #[cfg(feature = "secp256k1")] keys_proto::KeyType::Secp256k1 => { secp256k1::PublicKey::decode(&pubkey.data).map(PublicKey::Secp256k1) @@ -217,3 +218,4 @@ impl PublicKey { self.into() } } + From 24b738cdccdfcda10fb6cdfa12ec0f558da462f4 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Thu, 9 Jul 2020 11:38:43 +1000 Subject: [PATCH 12/35] Reviewers suggestion --- protocols/gossipsub/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/gossipsub/src/error.rs b/protocols/gossipsub/src/error.rs index 371032f79fb..6f774607117 100644 --- a/protocols/gossipsub/src/error.rs +++ b/protocols/gossipsub/src/error.rs @@ -18,7 +18,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -//! Error types than can result from gossipsub. +//! Error types that can result from gossipsub. use libp2p_core::identity::error::SigningError; From 2c50ba26eebd94ea027fa073f6a021d758cc8c3a Mon Sep 17 00:00:00 2001 From: Age Manning Date: Thu, 23 Jul 2020 15:01:27 +1000 Subject: [PATCH 13/35] Revert changes to core/ --- core/src/identity/error.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/core/src/identity/error.rs b/core/src/identity/error.rs index 5409d2a4ff7..d89967a7477 100644 --- a/core/src/identity/error.rs +++ b/core/src/identity/error.rs @@ -27,22 +27,16 @@ use std::fmt; #[derive(Debug)] pub struct DecodingError { msg: String, - source: Option>, + source: Option> } impl DecodingError { pub(crate) fn new(msg: S) -> Self { - Self { - msg: msg.to_string(), - source: None, - } + Self { msg: msg.to_string(), source: None } } pub(crate) fn source(self, source: impl Error + Send + Sync + 'static) -> Self { - Self { - source: Some(Box::new(source)), - ..self - } + Self { source: Some(Box::new(source)), .. self } } } @@ -62,23 +56,17 @@ impl Error for DecodingError { #[derive(Debug)] pub struct SigningError { msg: String, - source: Option>, + source: Option> } /// An error during encoding of key material. impl SigningError { pub(crate) fn new(msg: S) -> Self { - Self { - msg: msg.to_string(), - source: None, - } + Self { msg: msg.to_string(), source: None } } pub(crate) fn source(self, source: impl Error + Send + Sync + 'static) -> Self { - Self { - source: Some(Box::new(source)), - ..self - } + Self { source: Some(Box::new(source)), .. self } } } @@ -93,3 +81,4 @@ impl Error for SigningError { self.source.as_ref().map(|s| &**s as &dyn Error) } } + From 4de878e1dcc4d91494dde091e2d790e21f4914a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Klaehn?= Date: Thu, 23 Jul 2020 18:52:32 +0200 Subject: [PATCH 14/35] Switch gossipsub state to use sets instead of vectors --- protocols/gossipsub/src/behaviour.rs | 76 ++++++++++------------ protocols/gossipsub/src/behaviour/tests.rs | 4 +- protocols/gossipsub/src/topic.rs | 4 +- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 2a17efaf8d9..f79582c6216 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -40,7 +40,7 @@ use lru::LruCache; use rand; use rand::{seq::SliceRandom, thread_rng}; use std::{ - collections::hash_map::HashMap, + collections::{BTreeSet, hash_map::HashMap}, collections::HashSet, collections::VecDeque, iter, @@ -66,16 +66,16 @@ pub struct Gossipsub { local_peer_id: PeerId, /// A map of all connected peers - A map of topic hash to a list of gossipsub peer Ids. - topic_peers: HashMap>, + topic_peers: HashMap>, /// A map of all connected peers to their subscribed topics. - peer_topics: HashMap>, + peer_topics: HashMap>, /// Overlay network of connected peers - Maps topics to connected gossipsub peers. - mesh: HashMap>, + mesh: HashMap>, /// Map of topics to list of peers that we publish to, but don't subscribe to. - fanout: HashMap>, + fanout: HashMap>, /// The last publish time for fanout topics. fanout_last_pub: HashMap, @@ -349,9 +349,9 @@ impl Gossipsub { "JOIN: Adding {:?} peers from the fanout for topic: {:?}", add_peers, topic_hash ); - added_peers.extend_from_slice(&peers[..add_peers]); + added_peers.extend(peers.iter().cloned().take(add_peers)); self.mesh - .insert(topic_hash.clone(), peers[..add_peers].to_vec()); + .insert(topic_hash.clone(), peers.into_iter().take(add_peers).collect()); // remove the last published time self.fanout_last_pub.remove(topic_hash); } @@ -365,7 +365,7 @@ impl Gossipsub { self.config.mesh_n - added_peers.len(), |_| true, ); - added_peers.extend_from_slice(&new_peers); + added_peers.extend(new_peers.clone()); // add them to the mesh debug!( "JOIN: Inserting {:?} random peers into the mesh", @@ -374,8 +374,8 @@ impl Gossipsub { let mesh_peers = self .mesh .entry(topic_hash.clone()) - .or_insert_with(|| Vec::new()); - mesh_peers.extend_from_slice(&new_peers); + .or_insert_with(|| Default::default()); + mesh_peers.extend(new_peers); } for peer_id in added_peers { @@ -498,7 +498,7 @@ impl Gossipsub { ); // ensure peer is not already added if !peers.contains(peer_id) { - peers.push(peer_id.clone()); + peers.insert(peer_id.clone()); } } else { to_prune_topics.insert(topic_hash.clone()); @@ -541,7 +541,7 @@ impl Gossipsub { "PRUNE: Removing peer: {:?} from the mesh for topic: {:?}", peer_id, topic_hash ); - peers.retain(|p| p != peer_id); + peers.remove(peer_id); } } debug!("Completed PRUNE handling for peer: {:?}", peer_id); @@ -602,16 +602,16 @@ impl Gossipsub { let peer_list = self .topic_peers .entry(subscription.topic_hash.clone()) - .or_insert_with(Vec::new); + .or_insert_with(Default::default); match subscription.action { GossipsubSubscriptionAction::Subscribe => { - if !peer_list.contains(&propagation_source) { + if !peer_list.contains(propagation_source) { debug!( "SUBSCRIPTION: topic_peer: Adding gossip peer: {:?} to topic: {:?}", propagation_source, subscription.topic_hash ); - peer_list.push(propagation_source.clone()); + peer_list.insert(propagation_source.clone()); } // add to the peer_topics mapping @@ -620,7 +620,7 @@ impl Gossipsub { "SUBSCRIPTION: Adding peer: {:?} to topic: {:?}", propagation_source, subscription.topic_hash ); - subscribed_topics.push(subscription.topic_hash.clone()); + subscribed_topics.insert(subscription.topic_hash.clone()); } // if the mesh needs peers add the peer to the mesh @@ -631,7 +631,7 @@ impl Gossipsub { propagation_source, ); } - peers.push(propagation_source.clone()); + peers.insert(propagation_source.clone()); } // generates a subscription event to be polled self.events.push_back(NetworkBehaviourAction::GenerateEvent( @@ -642,23 +642,17 @@ impl Gossipsub { )); } GossipsubSubscriptionAction::Unsubscribe => { - if let Some(pos) = peer_list.iter().position(|p| p == propagation_source) { + if peer_list.remove(propagation_source) { info!( "SUBSCRIPTION: Removing gossip peer: {:?} from topic: {:?}", propagation_source, subscription.topic_hash ); - peer_list.remove(pos); } // remove topic from the peer_topics mapping - if let Some(pos) = subscribed_topics - .iter() - .position(|t| t == &subscription.topic_hash) - { - subscribed_topics.remove(pos); - } + subscribed_topics.remove(&subscription.topic_hash); // remove the peer from the mesh if it exists if let Some(peers) = self.mesh.get_mut(&subscription.topic_hash) { - peers.retain(|peer| peer != propagation_source); + peers.remove(propagation_source); } // generate an unsubscribe event to be polled @@ -720,10 +714,11 @@ impl Gossipsub { let excess_peer_no = peers.len() - self.config.mesh_n; // shuffle the peers let mut rng = thread_rng(); - peers.shuffle(&mut rng); + let mut shuffled = peers.iter().cloned().collect::>(); + shuffled.shuffle(&mut rng); // remove the first excess_peer_no peers adding them to to_prune for _ in 0..excess_peer_no { - let peer = peers + let peer = shuffled .pop() .expect("There should always be enough peers to remove"); let current_topic = to_prune.entry(peer).or_insert_with(|| vec![]); @@ -771,7 +766,9 @@ impl Gossipsub { } } } - peers.retain(|peer| to_remove_peers.contains(&peer)); + for to_remove in to_remove_peers { + peers.remove(&to_remove); + } // not enough peers if peers.len() < self.config.mesh_n { @@ -934,11 +931,11 @@ impl Gossipsub { /// Helper function to get a set of `n` random gossipsub peers for a `topic_hash` /// filtered by the function `f`. fn get_random_peers( - topic_peers: &HashMap>, + topic_peers: &HashMap>, topic_hash: &TopicHash, n: usize, mut f: impl FnMut(&PeerId) -> bool, - ) -> Vec { + ) -> BTreeSet { let mut gossip_peers = match topic_peers.get(topic_hash) { // if they exist, filter the peers by `f` Some(peer_list) => peer_list.iter().cloned().filter(|p| f(p)).collect(), @@ -948,7 +945,7 @@ impl Gossipsub { // if we have less than needed, return them if gossip_peers.len() <= n { debug!("RANDOM PEERS: Got {:?} peers", gossip_peers.len()); - return gossip_peers.to_vec(); + return gossip_peers.into_iter().collect(); } // we have more peers than needed, shuffle them and return n of them @@ -957,7 +954,7 @@ impl Gossipsub { debug!("RANDOM PEERS: Got {:?} peers", n); - gossip_peers[..n].to_vec() + gossip_peers.into_iter().take(n).collect() } // adds a control action to control_pool @@ -1037,7 +1034,7 @@ impl NetworkBehaviour for Gossipsub { } // For the time being assume all gossipsub peers - self.peer_topics.insert(id.clone(), Vec::new()); + self.peer_topics.insert(id.clone(), Default::default()); } fn inject_disconnected(&mut self, id: &PeerId) { @@ -1057,18 +1054,13 @@ impl NetworkBehaviour for Gossipsub { // check the mesh for the topic if let Some(mesh_peers) = self.mesh.get_mut(&topic) { // check if the peer is in the mesh and remove it - if let Some(pos) = mesh_peers.iter().position(|p| p == id) { - mesh_peers.remove(pos); - } + mesh_peers.remove(id); } // remove from topic_peers if let Some(peer_list) = self.topic_peers.get_mut(&topic) { - if let Some(pos) = peer_list.iter().position(|p| p == id) { - peer_list.remove(pos); - } + if !peer_list.remove(id) { // debugging purposes - else { warn!("Disconnected node: {:?} not in topic_peers peer list", &id); } } else { @@ -1081,7 +1073,7 @@ impl NetworkBehaviour for Gossipsub { // remove from fanout self.fanout .get_mut(&topic) - .map(|peers| peers.retain(|p| p != id)); + .map(|peers| peers.remove(id)); } } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index c933659434b..6c915bd240b 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -254,11 +254,11 @@ mod tests { // verify fanout nodes // add 3 random peers to the fanout[topic1] - gs.fanout.insert(topic_hashes[1].clone(), vec![]); + gs.fanout.insert(topic_hashes[1].clone(), Default::default()); let new_peers = vec![]; for _ in 0..3 { let fanout_peers = gs.fanout.get_mut(&topic_hashes[1]).unwrap(); - fanout_peers.push(PeerId::random()); + fanout_peers.insert(PeerId::random()); } // subscribe to topic1 diff --git a/protocols/gossipsub/src/topic.rs b/protocols/gossipsub/src/topic.rs index 6eacb9b3265..9d8522c7682 100644 --- a/protocols/gossipsub/src/topic.rs +++ b/protocols/gossipsub/src/topic.rs @@ -24,7 +24,7 @@ use prost::Message; use sha2::{Digest, Sha256}; use std::fmt; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct TopicHash { /// The topic hash. Stored as a string to align with the protobuf API. hash: String, @@ -45,7 +45,7 @@ impl TopicHash { } /// A gossipsub topic. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Topic { topic: String, } From e43f0a0e2bbae074b61cd445a22c9f2d96b564b6 Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 23 Jul 2020 20:40:54 +0200 Subject: [PATCH 15/35] Make tests pass --- protocols/gossipsub/src/behaviour.rs | 2 +- protocols/gossipsub/src/behaviour/tests.rs | 62 ++++++++++------------ 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index f79582c6216..14fce01f49b 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -768,7 +768,7 @@ impl Gossipsub { } for to_remove in to_remove_peers { peers.remove(&to_remove); - } + } // not enough peers if peers.len() < self.config.mesh_n { diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 6c915bd240b..f75ade51f23 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -230,21 +230,23 @@ mod tests { "Should have added 6 nodes to the mesh" ); + fn collect_grafts(mut collected_grafts: Vec, (_, controls): (&PeerId, &Vec)) -> Vec { + for c in controls.iter() { + match c { + GossipsubControlAction::Graft { topic_hash: _ } => { + collected_grafts.push(c.clone()) + } + _ => {} + } + } + collected_grafts + } + // there should be mesh_n GRAFT messages. let graft_messages = gs.control_pool .iter() - .fold(vec![], |mut collected_grafts, (_, controls)| { - for c in controls.iter() { - match c { - GossipsubControlAction::Graft { topic_hash: _ } => { - collected_grafts.push(c.clone()) - } - _ => {} - } - } - collected_grafts - }); + .fold(vec![], collect_grafts); assert_eq!( graft_messages.len(), @@ -255,7 +257,7 @@ mod tests { // verify fanout nodes // add 3 random peers to the fanout[topic1] gs.fanout.insert(topic_hashes[1].clone(), Default::default()); - let new_peers = vec![]; + let new_peers: Vec = vec![]; for _ in 0..3 { let fanout_peers = gs.fanout.get_mut(&topic_hashes[1]).unwrap(); fanout_peers.insert(PeerId::random()); @@ -272,7 +274,7 @@ mod tests { let mesh_peers = gs.mesh.get(&topic_hashes[1]).unwrap(); for new_peer in new_peers { assert!( - mesh_peers.contains(new_peer), + mesh_peers.contains(&new_peer), "Fanout peer should be included in the mesh" ); } @@ -281,17 +283,7 @@ mod tests { let graft_messages = gs.control_pool .iter() - .fold(vec![], |mut collected_grafts, (_, controls)| { - for c in controls.iter() { - match c { - GossipsubControlAction::Graft { topic_hash: _ } => { - collected_grafts.push(c.clone()) - } - _ => {} - } - } - collected_grafts - }); + .fold(vec![], collect_grafts); assert!( graft_messages.len() == 12, @@ -461,7 +453,7 @@ mod tests { for peer in peers { let known_topics = gs.peer_topics.get(&peer).unwrap(); assert!( - known_topics == &topic_hashes, + known_topics == &topic_hashes.iter().cloned().collect(), "The topics for each node should all topics" ); } @@ -508,12 +500,12 @@ mod tests { let peer_topics = gs.peer_topics.get(&peers[0]).unwrap().clone(); assert!( - peer_topics == topic_hashes[..3].to_vec(), + peer_topics == topic_hashes.iter().take(3).cloned().collect(), "First peer should be subscribed to three topics" ); let peer_topics = gs.peer_topics.get(&peers[1]).unwrap().clone(); assert!( - peer_topics == topic_hashes[..3].to_vec(), + peer_topics == topic_hashes.iter().take(3).cloned().collect(), "Second peer should be subscribed to three topics" ); @@ -525,7 +517,7 @@ mod tests { for topic_hash in topic_hashes[..3].iter() { let topic_peers = gs.topic_peers.get(topic_hash).unwrap().clone(); assert!( - topic_peers == peers[..2].to_vec(), + topic_peers == peers[..2].into_iter().cloned().collect(), "Two peers should be added to the first three topics" ); } @@ -542,13 +534,13 @@ mod tests { let peer_topics = gs.peer_topics.get(&peers[0]).unwrap().clone(); assert!( - peer_topics == topic_hashes[1..3].to_vec(), + peer_topics == topic_hashes[1..3].into_iter().cloned().collect(), "Peer should be subscribed to two topics" ); let topic_peers = gs.topic_peers.get(&topic_hashes[0]).unwrap().clone(); // only gossipsub at the moment assert!( - topic_peers == peers[1..2].to_vec(), + topic_peers == peers[1..2].into_iter().cloned().collect(), "Only the second peers should be in the first topic" ); } @@ -568,19 +560,19 @@ mod tests { peers.push(PeerId::random()) } - gs.topic_peers.insert(topic_hash.clone(), peers.clone()); + gs.topic_peers.insert(topic_hash.clone(), peers.iter().cloned().collect()); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, |_| true); - assert!(random_peers.len() == 5, "Expected 5 peers to be returned"); + assert_eq!(random_peers.len(), 5, "Expected 5 peers to be returned"); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 30, |_| true); assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); - assert!(random_peers == peers, "Expected no shuffling"); + assert!(random_peers == peers.iter().cloned().collect(), "Expected no shuffling"); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 20, |_| true); assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); - assert!(random_peers == peers, "Expected no shuffling"); + assert!(random_peers == peers.iter().cloned().collect(), "Expected no shuffling"); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 0, |_| true); assert!(random_peers.len() == 0, "Expected 0 peers to be returned"); @@ -836,7 +828,7 @@ mod tests { build_and_inject_nodes(20, vec![String::from("topic1")], true); // insert peer into our mesh for 'topic1' - gs.mesh.insert(topic_hashes[0].clone(), peers.clone()); + gs.mesh.insert(topic_hashes[0].clone(), peers.iter().cloned().collect()); assert!( gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), "Expected peer to be in mesh" From 57f07aa0942553cec8c4747cd752cb7efc90a91a Mon Sep 17 00:00:00 2001 From: Ruediger Klaehn Date: Thu, 23 Jul 2020 20:57:14 +0200 Subject: [PATCH 16/35] Some clippy --- protocols/gossipsub/src/behaviour.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 14fce01f49b..ebaf6191648 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -374,7 +374,7 @@ impl Gossipsub { let mesh_peers = self .mesh .entry(topic_hash.clone()) - .or_insert_with(|| Default::default()); + .or_insert_with(Default::default); mesh_peers.extend(new_peers); } @@ -695,7 +695,7 @@ impl Gossipsub { |peer| !peers.contains(peer) }); for peer in &peer_list { - let current_topic = to_graft.entry(peer.clone()).or_insert_with(|| vec![]); + let current_topic = to_graft.entry(peer.clone()).or_insert_with(Vec::new); current_topic.push(topic_hash.clone()); } // update the mesh @@ -721,7 +721,7 @@ impl Gossipsub { let peer = shuffled .pop() .expect("There should always be enough peers to remove"); - let current_topic = to_prune.entry(peer).or_insert_with(|| vec![]); + let current_topic = to_prune.entry(peer).or_insert_with(Vec::new); current_topic.push(topic_hash.clone()); } } From b6eeb3cccecc03cec9de816e4efed7487a02d12e Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 24 Jul 2020 13:09:36 +1000 Subject: [PATCH 17/35] Prevent duplicate finding during JOIN --- protocols/gossipsub/src/behaviour.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 5bd6ca3b619..dd5a866a1b7 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -395,7 +395,7 @@ impl Gossipsub { &self.topic_peers, topic_hash, self.config.mesh_n - added_peers.len(), - |_| true, + |peer| !added_peers.contains(peer), ); added_peers.extend_from_slice(&new_peers); // add them to the mesh From aec51e0a2de4dcd30410428f31810af900affc87 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 24 Jul 2020 17:46:45 +1000 Subject: [PATCH 18/35] Update tests, improve logging --- protocols/gossipsub/src/behaviour.rs | 87 ++++++++++++---------- protocols/gossipsub/src/behaviour/tests.rs | 15 ++-- protocols/gossipsub/tests/smoke.rs | 15 +++- 3 files changed, 69 insertions(+), 48 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 49feb4b8367..bac2e8fda88 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -40,9 +40,9 @@ use prost::Message; use rand; use rand::{seq::SliceRandom, thread_rng}; use std::{ - collections::{BTreeSet, hash_map::HashMap}, collections::HashSet, collections::VecDeque, + collections::{hash_map::HashMap, BTreeSet}, iter, sync::Arc, task::{Context, Poll}, @@ -382,8 +382,10 @@ impl Gossipsub { add_peers, topic_hash ); added_peers.extend(peers.iter().cloned().take(add_peers)); - self.mesh - .insert(topic_hash.clone(), peers.into_iter().take(add_peers).collect()); + self.mesh.insert( + topic_hash.clone(), + peers.into_iter().take(add_peers).collect(), + ); // remove the last published time self.fanout_last_pub.remove(topic_hash); } @@ -529,10 +531,8 @@ impl Gossipsub { "GRAFT: Mesh link added for peer: {:?} in topic: {:?}", peer_id, topic_hash ); - // ensure peer is not already added - if !peers.contains(peer_id) { - peers.insert(peer_id.clone()); - } + // Duplicates are ignored + peers.insert(peer_id.clone()); } else { to_prune_topics.insert(topic_hash.clone()); } @@ -567,18 +567,20 @@ impl Gossipsub { /// Handles PRUNE control messages. Removes peer from the mesh. fn handle_prune(&mut self, peer_id: &PeerId, topics: Vec) { - debug!("Handling PRUNE message for peer: {:?}", peer_id); + debug!("Handling PRUNE message for peer: {}", peer_id.to_string()); for topic_hash in topics { if let Some(peers) = self.mesh.get_mut(&topic_hash) { // remove the peer if it exists in the mesh - info!( - "PRUNE: Removing peer: {:?} from the mesh for topic: {:?}", - peer_id, topic_hash - ); - peers.remove(peer_id); + if peers.remove(peer_id) { + info!( + "PRUNE: Removing peer: {} from the mesh for topic: {:?}", + peer_id.to_string(), + topic_hash + ); + } } } - debug!("Completed PRUNE handling for peer: {:?}", peer_id); + debug!("Completed PRUNE handling for peer: {}", peer_id.to_string()); } /// Handles a newly received GossipsubMessage. @@ -586,8 +588,9 @@ impl Gossipsub { fn handle_received_message(&mut self, msg: GossipsubMessage, propagation_source: &PeerId) { let msg_id = (self.config.message_id_fn)(&msg); debug!( - "Handling message: {:?} from peer: {:?}", - msg_id, propagation_source + "Handling message: {:?} from peer: {}", + msg_id, + propagation_source.to_string() ); if self.mcache.put(msg.clone()).is_some() { debug!("Message already received, ignoring. Message: {:?}", msg_id); @@ -617,13 +620,17 @@ impl Gossipsub { propagation_source: &PeerId, ) { debug!( - "Handling subscriptions: {:?}, from source: {:?}", - subscriptions, propagation_source + "Handling subscriptions: {:?}, from source: {}", + subscriptions, + propagation_source.to_string() ); let subscribed_topics = match self.peer_topics.get_mut(propagation_source) { Some(topics) => topics, None => { - error!("Subscription by unknown peer: {:?}", &propagation_source); + error!( + "Subscription by unknown peer: {}", + propagation_source.to_string() + ); return; } }; @@ -637,32 +644,33 @@ impl Gossipsub { match subscription.action { GossipsubSubscriptionAction::Subscribe => { - if !peer_list.contains(propagation_source) { + if peer_list.insert(propagation_source.clone()) { debug!( - "SUBSCRIPTION: topic_peer: Adding gossip peer: {:?} to topic: {:?}", - propagation_source, subscription.topic_hash + "SUBSCRIPTION: topic_peer: Adding gossip peer: {} to topic: {:?}", + propagation_source.to_string(), + subscription.topic_hash ); - peer_list.insert(propagation_source.clone()); } // add to the peer_topics mapping - if !subscribed_topics.contains(&subscription.topic_hash) { + if subscribed_topics.insert(subscription.topic_hash.clone()) { info!( - "SUBSCRIPTION: Adding peer: {:?} to topic: {:?}", - propagation_source, subscription.topic_hash + "SUBSCRIPTION: Adding peer: {} to topic: {:?}", + propagation_source.to_string(), + subscription.topic_hash ); - subscribed_topics.insert(subscription.topic_hash.clone()); } // if the mesh needs peers add the peer to the mesh if let Some(peers) = self.mesh.get_mut(&subscription.topic_hash) { if peers.len() < self.config.mesh_n_low { - debug!( - "SUBSCRIPTION: Adding peer {:?} to the mesh", - propagation_source, - ); + if peers.insert(propagation_source.clone()) { + debug!( + "SUBSCRIPTION: Adding peer {} to the mesh", + propagation_source.to_string(), + ); + } } - peers.insert(propagation_source.clone()); } // generates a subscription event to be polled self.events.push_back(NetworkBehaviourAction::GenerateEvent( @@ -675,8 +683,9 @@ impl Gossipsub { GossipsubSubscriptionAction::Unsubscribe => { if peer_list.remove(propagation_source) { info!( - "SUBSCRIPTION: Removing gossip peer: {:?} from topic: {:?}", - propagation_source, subscription.topic_hash + "SUBSCRIPTION: Removing gossip peer: {} from topic: {:?}", + propagation_source.to_string(), + subscription.topic_hash ); } // remove topic from the peer_topics mapping @@ -1148,7 +1157,7 @@ impl NetworkBehaviour for Gossipsub { // remove from topic_peers if let Some(peer_list) = self.topic_peers.get_mut(&topic) { if !peer_list.remove(id) { - // debugging purposes + // debugging purposes warn!("Disconnected node: {:?} not in topic_peers peer list", &id); } } else { @@ -1159,9 +1168,7 @@ impl NetworkBehaviour for Gossipsub { } // remove from fanout - self.fanout - .get_mut(&topic) - .map(|peers| peers.remove(id)); + self.fanout.get_mut(&topic).map(|peers| peers.remove(id)); } } @@ -1173,7 +1180,9 @@ impl NetworkBehaviour for Gossipsub { fn inject_event(&mut self, propagation_source: PeerId, _: ConnectionId, event: GossipsubRpc) { // Handle subscriptions // Update connected peers topics - self.handle_received_subscriptions(&event.subscriptions, &propagation_source); + if !event.subscriptions.is_empty() { + self.handle_received_subscriptions(&event.subscriptions, &propagation_source); + } // Handle messages for message in event.messages { diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index ecb0377f995..91bcf109f64 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -303,9 +303,10 @@ mod tests { "Subscribe should add a new entry to the mesh[topic] hashmap" ); - // peers should be subscribed to the topic - assert!( - gs.topic_peers.get(&topic_hashes[0]).map(|p| p.is_empty()) == Some(false), + // all peers should be subscribed to the topic + assert_eq!( + gs.topic_peers.get(&topic_hashes[0]).map(|p| p.len()), + Some(20), "Peers should be subscribed to the topic" ); @@ -331,8 +332,10 @@ mod tests { let msg_id = (gs.config.message_id_fn)(&publishes.first().expect("Should contain > 0 entries")); - assert!( - publishes.len() == 20, + let config = GossipsubConfig::default(); + assert_eq!( + publishes.len(), + config.mesh_n_low, "Should send a publish message to all known peers" ); @@ -786,7 +789,7 @@ mod tests { ); assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), "Expected peer to have been added to mesh" ); } diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index 4caf5c5e6e4..cce516b1e62 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -33,7 +33,7 @@ use libp2p_core::{ identity, multiaddr::Protocol, muxing::StreamMuxerBox, transport::MemoryTransport, upgrade, Multiaddr, Transport, }; -use libp2p_gossipsub::{Gossipsub, GossipsubConfig, GossipsubEvent, Signing, Topic}; +use libp2p_gossipsub::{Gossipsub, GossipsubConfigBuilder, GossipsubEvent, Signing, Topic}; use libp2p_plaintext::PlainText2Config; use libp2p_swarm::Swarm; use libp2p_yamux as yamux; @@ -145,7 +145,16 @@ fn build_node() -> (Multiaddr, Swarm) { .boxed(); let peer_id = public_key.clone().into_peer_id(); - let config = GossipsubConfig::default(); + + // NOTE: The graph of created nodes can be disconnected from the mesh point of view as nodes + // can reach their d_lo value and not add other nodes to their mesh. To speed up this test, we + // reduce the default values of the heartbeat, so that all nodes will receive gossip in a + // timely fashion. + + let config = GossipsubConfigBuilder::new() + .heartbeat_initial_delay(Duration::from_millis(50)) + .heartbeat_interval(Duration::from_millis(100)) + .build(); let behaviour = Gossipsub::new(Signing::Disabled(peer_id.clone()), config); let mut swarm = Swarm::new(transport, behaviour, peer_id); @@ -213,6 +222,6 @@ fn multi_hop_propagation() { } QuickCheck::new() - .max_tests(10) + .max_tests(1) .quickcheck(prop as fn(usize, u64) -> TestResult) } From 759a5ad15f6c8b9ea8ca0f688ec80921e951bb8f Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 24 Jul 2020 17:50:37 +1000 Subject: [PATCH 19/35] Update the default d_lo constant --- protocols/gossipsub/src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 21e349a3494..46931407026 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -38,7 +38,7 @@ pub struct GossipsubConfig { /// Target number of peers for the mesh network (D in the spec, default is 6). pub mesh_n: usize, - /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 4). + /// Minimum number of peers in mesh network before adding more (D_lo in the spec, default is 5). pub mesh_n_low: usize, /// Maximum number of peers in mesh network before removing some (D_high in the spec, default @@ -87,7 +87,7 @@ impl Default for GossipsubConfig { history_length: 5, history_gossip: 3, mesh_n: 6, - mesh_n_low: 4, + mesh_n_low: 5, mesh_n_high: 12, gossip_lazy: 6, // default to mesh_n heartbeat_initial_delay: Duration::from_secs(5), From 495ffe488081944b65b4ff0b927bf1c5d01c97ab Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 24 Jul 2020 17:56:56 +1000 Subject: [PATCH 20/35] Handle errors correctly --- protocols/gossipsub/src/handler.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index 7bbbd0ced6e..6202688f3b2 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -191,9 +191,21 @@ impl ProtocolsHandler for GossipsubHandler { return Poll::Ready(ProtocolsHandlerEvent::Custom(message)); } Poll::Ready(Some(Err(e))) => { - warn!("Invalid message received. Error: {}", e); - self.inbound_substream = - Some(InboundSubstreamState::WaitingInput(substream)); + match e.kind() { + std::io::ErrorKind::InvalidData => { + // Invalid message, ignore it and reset to waiting + warn!("Invalid message received. Error: {}", e); + self.inbound_substream = + Some(InboundSubstreamState::WaitingInput(substream)); + } + _ => { + // More serious errors, close this side of the stream. If the + // peer is still around, they will re-establish their + // connection + self.inbound_substream = + Some(InboundSubstreamState::Closing(substream)); + } + } } // peer closed the stream Poll::Ready(None) => { @@ -235,7 +247,7 @@ impl ProtocolsHandler for GossipsubHandler { break; } Some(InboundSubstreamState::Poisoned) => { - panic!("Error occurred during inbound stream processing") + unreachable!("Error occurred during inbound stream processing") } } } @@ -331,7 +343,7 @@ impl ProtocolsHandler for GossipsubHandler { break; } Some(OutboundSubstreamState::Poisoned) => { - panic!("Error occurred during outbound stream processing") + unreachable!("Error occurred during outbound stream processing") } } } From 1ea2dd9d222ee5015c5624e970f967d40255d2f4 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 24 Jul 2020 18:58:09 +1000 Subject: [PATCH 21/35] Add privacy and validation options in the config --- protocols/gossipsub/src/config.rs | 93 +++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 46931407026..020b7d7ef54 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -19,9 +19,51 @@ // DEALINGS IN THE SOFTWARE. use crate::protocol::{GossipsubMessage, MessageId}; +use log::warn; use std::borrow::Cow; use std::time::Duration; +/// The types of privacy settings that can be employed by gossipsub. These are related to how +/// anonymous the publisher of the message would like to be. +#[derive(Debug, Clone)] +pub enum PrivacySetting { + /// This is the default setting. The PeerId publishing the message will be broadcast with the + /// message along with a random sequence number. + None, + /// A randomized `PeerId` will be used as the publisher of a message. + /// + /// NOTE: This mode cannot be used in conjunction with message signing. + RandomAuthor, + /// The author of the message and the sequence numbers are excluded from the message. + /// + /// NOTE: This mode cannot be used in conjunction with message signing. Also not that excluding + /// these fields may make these messages invalid by other nodes who enforce validation of these + /// fields. See [`ValidationSetting`] for how to customise this for rust-libp2p gossipsub. + Anonymous, +} + +/// The types of message validation that can be employed by gossipsub. +#[derive(Debug, Clone)] +pub enum ValidationSetting { + /// This is the default setting. This requires the message author to be a valid `PeerId` and to + /// be present as well as the sequence number. All messages must have valid signatures. + /// + /// NOTE: This setting will reject messages from nodes using `PrivacySetting::Anonymous` and + /// all messages that do not have signatures. + Strict, + /// This setting permits messages that have no author, sequence number or signature. If any of + /// these fields exist in the message these are validated. + Permissive, + /// This setting requires the author, sequence number and signature fields of a message to be + /// empty. Any message that contains these fields is considered invalid. + Anonymous, + /// This setting does not check the author, sequence number or signature fields of incoming + /// messages. If these fields contain data, they are simply ignored. + /// + /// NOTE: This setting will consider messages with invalid signatures as valid messages. + None, +} + /// Configuration parameters that define the performance of the gossipsub network. #[derive(Clone)] pub struct GossipsubConfig { @@ -69,6 +111,14 @@ pub struct GossipsubConfig { /// once validated (default is `false`). pub manual_propagation: bool, + /// Determines the level of privacy for the node when publishing a message. See [`PrivacySetting`] for + /// the available types. The default is PrivacySetting::None. + pub privacy_mode: PrivacySetting, + + /// Determines the level of validation used when receiving messages. See [`ValidationSetting`] + /// for the available types. The default is ValidationSetting::Strict. + pub validation_mode: ValidationSetting, + /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this /// parameter allows the user to address packets arbitrarily. One example is content based @@ -96,6 +146,8 @@ impl Default for GossipsubConfig { max_transmit_size: 2048, hash_topics: false, // default compatibility with floodsub manual_propagation: false, + privacy_mode: PrivacySetting::None, + validation_mode: ValidationSetting::Strict, message_id_fn: |message| { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); @@ -229,6 +281,26 @@ impl GossipsubConfigBuilder { self } + /// Determines the level of privacy for the node when publishing a message. See [`PrivacySetting`] for + /// the available types. + pub fn privacy_mode(&mut self, privacy_setting: PrivacySetting) -> &mut Self { + self.config.privacy_mode = privacy_setting; + + // warn the user about odd combinations of privacy/validation + self.check_privacy_validation(); + self + } + + /// Determines the level of validation used when receiving messages. See [`ValidationSetting`] + /// for the available types. The default is ValidationSetting::Strict. + pub fn validation_mode(&mut self, validation_setting: ValidationSetting) -> &mut Self { + self.config.validation_mode = validation_setting; + + // warn the user about odd combinations of privacy/validation + self.check_privacy_validation(); + self + } + /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this /// parameter allows the user to address packets arbitrarily. One example is content based @@ -246,6 +318,27 @@ impl GossipsubConfigBuilder { pub fn build(&self) -> GossipsubConfig { self.config.clone() } + + // Warns the user for odd combinations of privacy and validation. + fn check_privacy_validation(&self) { + match (&self.config.validation_mode, &self.config.privacy_mode) { + (ValidationSetting::Strict, PrivacySetting::RandomAuthor) => warn!( + "Messages will be + published unsigned and incoming unsigned messages will be rejected. Consider adjusting + the validation or privacy settings in the config" + ), + (ValidationSetting::Strict, PrivacySetting::Anonymous) => { + warn!("Messages will not be signed or contain an author, but incoming messages requires this. Consider adjusting the validation or privacy settings in the config") + } + (ValidationSetting::Anonymous, PrivacySetting::None) => { + warn!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") + } + (ValidationSetting::Anonymous, PrivacySetting::RandomAuthor) => { + warn!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") + } + (_, _) => {} + } + } } impl std::fmt::Debug for GossipsubConfig { From 1f73ded66021cf11c42eb3240b992eb3fca65a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Klaehn?= Date: Fri, 24 Jul 2020 11:50:21 +0200 Subject: [PATCH 22/35] A few improvements (#32) * Make the GossipsubRpc debug instance a bit nicer Basically just don't show empty components to reduce visual noise * Introduce send_message helper function to avoid repetition of boilerplate This also serves to have a central place to log or otherwise intercept all outgoing messages to other peers of the behaviour. * Simplify the arc removal in poll --- protocols/gossipsub/src/behaviour.rs | 207 ++++++++++++--------------- 1 file changed, 89 insertions(+), 118 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index bac2e8fda88..307c5920b06 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -46,6 +46,7 @@ use std::{ iter, sync::Arc, task::{Context, Poll}, + fmt, }; use wasm_timer::{Instant, Interval}; @@ -168,7 +169,7 @@ impl Gossipsub { } // send subscription request to all peers - let peer_list = self.peer_topics.keys().collect::>(); + let peer_list = self.peer_topics.keys().cloned().collect::>(); if !peer_list.is_empty() { let event = Arc::new(GossipsubRpc { messages: Vec::new(), @@ -181,12 +182,7 @@ impl Gossipsub { for peer in peer_list { debug!("Sending SUBSCRIBE to peer: {:?}", peer); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - handler: NotifyHandler::Any, - event: event.clone(), - }); + self.send_message(peer, event.clone()); } } @@ -211,7 +207,7 @@ impl Gossipsub { } // announce to all peers - let peer_list = self.peer_topics.keys().collect::>(); + let peer_list = self.peer_topics.keys().cloned().collect::>(); if !peer_list.is_empty() { let event = Arc::new(GossipsubRpc { messages: Vec::new(), @@ -224,12 +220,7 @@ impl Gossipsub { for peer in peer_list { debug!("Sending UNSUBSCRIBE to peer: {:?}", peer); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - event: event.clone(), - handler: NotifyHandler::Any, - }); + self.send_message(peer, event.clone()); } } @@ -317,12 +308,7 @@ impl Gossipsub { // Send to peers we know are subscribed to the topic. for peer_id in recipient_peers.iter() { debug!("Sending message to peer: {:?}", peer_id); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer_id.clone(), - event: event.clone(), - handler: NotifyHandler::Any, - }); + self.send_message(peer_id.clone(), event.clone()); } info!("Published message: {:?}", msg_id); @@ -504,16 +490,11 @@ impl Gossipsub { debug!("IWANT: Sending cached messages to peer: {:?}", peer_id); // Send the messages to the peer let message_list = cached_messages.into_iter().map(|entry| entry.1).collect(); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer_id.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: message_list, - control_msgs: Vec::new(), - }), - }); + self.send_message(peer_id.clone(), GossipsubRpc { + subscriptions: Vec::new(), + messages: message_list, + control_msgs: Vec::new(), + }); } debug!("Completed IWANT handling for peer: {:?}", peer_id); } @@ -551,16 +532,11 @@ impl Gossipsub { "GRAFT: Not subscribed to topics - Sending PRUNE to peer: {:?}", peer_id ); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer_id.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: prune_messages, - }), - }); + self.send_message(peer_id.clone(), GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: prune_messages, + }); } debug!("Completed GRAFT handling for peer: {:?}", peer_id); } @@ -899,16 +875,11 @@ impl Gossipsub { grafts.append(&mut prunes); // send the control messages - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: grafts, - }), - }); + self.send_message(peer.clone(), GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: grafts, + }); } // handle the remaining prunes @@ -919,16 +890,11 @@ impl Gossipsub { topic_hash: topic_hash.clone(), }) .collect(); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: remaining_prunes, - }), - }); + self.send_message(peer.clone(), GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: remaining_prunes, + }); } } @@ -961,12 +927,7 @@ impl Gossipsub { for peer in recipient_peers.iter() { debug!("Sending message: {:?} to peer {:?}", msg_id, peer); - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer.clone(), - event: event.clone(), - handler: NotifyHandler::Any, - }); + self.send_message(peer.clone(), event.clone()); } debug!("Completed forwarding message"); true @@ -1070,19 +1031,24 @@ impl Gossipsub { /// Takes each control action mapping and turns it into a message fn flush_control_pool(&mut self) { - for (peer, controls) in self.control_pool.drain() { - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: peer, - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: controls, - }), - }); + for (peer, controls) in self.control_pool.drain().collect::>() { + self.send_message(peer, GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: controls, + }); } } + + /// Send a GossipsubRpc message to a peer. This will wrap the message in an arc if it + /// is not already an arc. + fn send_message(&mut self, peer_id: PeerId, message: impl Into>) { + self.events.push_back(NetworkBehaviourAction::NotifyHandler { + peer_id, + event: message.into(), + handler: NotifyHandler::Any, + }) + } } impl NetworkBehaviour for Gossipsub { @@ -1118,16 +1084,11 @@ impl NetworkBehaviour for Gossipsub { if !subscriptions.is_empty() { // send our subscriptions to the peer - self.events - .push_back(NetworkBehaviourAction::NotifyHandler { - peer_id: id.clone(), - handler: NotifyHandler::Any, - event: Arc::new(GossipsubRpc { - messages: Vec::new(), - subscriptions, - control_msgs: Vec::new(), - }), - }); + self.send_message(id.clone(), GossipsubRpc { + messages: Vec::new(), + subscriptions, + control_msgs: Vec::new(), + }); } // For the time being assume all gossipsub peers @@ -1231,41 +1192,34 @@ impl NetworkBehaviour for Gossipsub { >, > { if let Some(event) = self.events.pop_front() { - // clone send event reference if others references are present - match event { - NetworkBehaviourAction::NotifyHandler { - peer_id, - handler, - event: send_event, - } => match Arc::try_unwrap(send_event) { - Ok(event) => { - return Poll::Ready(NetworkBehaviourAction::NotifyHandler { + return Poll::Ready( + match event { + NetworkBehaviourAction::NotifyHandler { + peer_id, + handler, + event: send_event, + } => { + // clone send event reference if others references are present + let event = Arc::try_unwrap(send_event).unwrap_or_else(|e| (*e).clone()); + NetworkBehaviourAction::NotifyHandler { peer_id, event, handler, - }); + } + }, + NetworkBehaviourAction::GenerateEvent(e) => { + NetworkBehaviourAction::GenerateEvent(e) } - Err(event) => { - return Poll::Ready(NetworkBehaviourAction::NotifyHandler { - peer_id, - event: (*event).clone(), - handler, - }); + NetworkBehaviourAction::DialAddress { address } => { + NetworkBehaviourAction::DialAddress { address } } - }, - NetworkBehaviourAction::GenerateEvent(e) => { - return Poll::Ready(NetworkBehaviourAction::GenerateEvent(e)); - } - NetworkBehaviourAction::DialAddress { address } => { - return Poll::Ready(NetworkBehaviourAction::DialAddress { address }); - } - NetworkBehaviourAction::DialPeer { peer_id, condition } => { - return Poll::Ready(NetworkBehaviourAction::DialPeer { peer_id, condition }); - } - NetworkBehaviourAction::ReportObservedAddr { address } => { - return Poll::Ready(NetworkBehaviourAction::ReportObservedAddr { address }); - } - } + NetworkBehaviourAction::DialPeer { peer_id, condition } => { + NetworkBehaviourAction::DialPeer { peer_id, condition } + } + NetworkBehaviourAction::ReportObservedAddr { address } => { + NetworkBehaviourAction::ReportObservedAddr { address } + } + }) } while let Poll::Ready(Some(())) = self.heartbeat.poll_next_unpin(cx) { @@ -1277,7 +1231,7 @@ impl NetworkBehaviour for Gossipsub { } /// An RPC received/sent. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct GossipsubRpc { /// List of messages that were part of this RPC query. pub messages: Vec, @@ -1287,6 +1241,23 @@ pub struct GossipsubRpc { pub control_msgs: Vec, } +impl fmt::Debug for GossipsubRpc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut b = f.debug_struct("GossipsubRpc"); + if !self.messages.is_empty() { + b.field("messages", &self.messages); + } + if !self.subscriptions.is_empty() { + b.field("subscriptions", &self.subscriptions); + } + if !self.control_msgs.is_empty() { + b.field("control_msgs", &self.control_msgs); + } + b.finish() + } + +} + /// Event that can happen on the gossipsub behaviour. #[derive(Debug)] pub enum GossipsubEvent { From 7b76c8fe2fc1d7fbb831a22786c2c884fffb7fbc Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 24 Jul 2020 19:54:19 +1000 Subject: [PATCH 23/35] Improve signing validation logic --- protocols/gossipsub/src/behaviour.rs | 44 ++++++++++++++++++----- protocols/gossipsub/src/config.rs | 52 +++++++++++++--------------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 307c5920b06..0113f06131d 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -18,7 +18,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::config::GossipsubConfig; +use crate::config::{GossipsubConfig, PrivacySetting, ValidationSetting}; use crate::error::PublishError; use crate::handler::GossipsubHandler; use crate::mcache::MessageCache; @@ -53,20 +53,22 @@ use wasm_timer::{Instant, Interval}; mod tests; /// Determines message signing is enabled or not. -/// -/// If message signing is disabled a `PeerId` can be entered which will be used as the author of -/// any published message. #[derive(Clone)] pub enum Signing { - /// Message signing is enabled. All messages will be signed and all received messages will be - /// verified. + /// Message signing is enabled. Enabled(Keypair), - /// Message signing is disabled and the associated `PeerId` will be used as the author for any - /// published message. - Disabled(PeerId), + /// Message signing is disabled. + /// + /// NOTE: The default validation settings are to require signatures. The [`ValidationSetting`] + /// should be updated in the [`GossipsubConfig`] to allow for unsigned messages. + Disabled, } /// Network behaviour that handles the gossipsub protocol. +/// +/// NOTE: Initialisation requires a [`Signing`] and [`GossipsubConfig`] instance. If Signing is set to +/// disabled, the [`ValidationSetting`] in the config should be adjusted to an appropriate level to +/// accept unsigned messages. pub struct Gossipsub { /// Configuration providing gossipsub performance parameters. config: GossipsubConfig, @@ -115,6 +117,11 @@ impl Gossipsub { pub fn new(signing: Signing, config: GossipsubConfig) -> Self { // Set up the router given the configuration settings. + // We do not allow configurations where a published message would also be rejected if it + // were received locally. + config.validate_privacy_validation(); + validate_config(&signing, &config); + // Set up the author and inlined key if required. let (message_author, keypair, inlined_key) = match signing { Signing::Enabled(kp) => { @@ -1282,3 +1289,22 @@ pub enum GossipsubEvent { topic: TopicHash, }, } + +/// Validates the combination of signing, privacy and message validation to ensure the +/// configuration will not reject published messages. +fn validate_config(signing: &Signing, config: &GossipsubConfig) { + match signing { + Signing::Enabled(_) => { + if let PrivacySetting::RandomAuthor | PrivacySetting::Anonymous = config.privacy_mode { + panic!("Cannot enable message signing without an author or with a random author. Adjust the PrivacySetting in the configuration."); + } + + // Config validation prevents anonymous validation. + } + Signing::Disabled => { + if let ValidationSetting::Strict = config.validation_mode { + panic!("Cannot disable signing with message validation set to `Strict`. Adjust the ValidationSetting in the configuration."); + } + } + } +} diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 020b7d7ef54..f5994d82bef 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -19,7 +19,6 @@ // DEALINGS IN THE SOFTWARE. use crate::protocol::{GossipsubMessage, MessageId}; -use log::warn; use std::borrow::Cow; use std::time::Duration; @@ -158,6 +157,29 @@ impl Default for GossipsubConfig { } } +impl GossipsubConfig { + // Prevent users from using settings where the published messages will be rejected based on combinations of privacy and validation. + pub fn validate_privacy_validation(&self) { + match (&self.validation_mode, &self.privacy_mode) { + (ValidationSetting::Strict, PrivacySetting::RandomAuthor) => panic!( + "Messages will be + published unsigned and incoming unsigned messages will be rejected. Consider adjusting + the validation or privacy settings in the config" + ), + (ValidationSetting::Strict, PrivacySetting::Anonymous) => { + panic!("Messages will not be signed or contain an author, but incoming messages requires this. Consider adjusting the validation or privacy settings in the config") + } + (ValidationSetting::Anonymous, PrivacySetting::None) => { + panic!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") + } + (ValidationSetting::Anonymous, PrivacySetting::RandomAuthor) => { + panic!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") + } + (_, _) => {} + } + } +} + /// The builder struct for constructing a gossipsub configuration. pub struct GossipsubConfigBuilder { config: GossipsubConfig, @@ -285,9 +307,6 @@ impl GossipsubConfigBuilder { /// the available types. pub fn privacy_mode(&mut self, privacy_setting: PrivacySetting) -> &mut Self { self.config.privacy_mode = privacy_setting; - - // warn the user about odd combinations of privacy/validation - self.check_privacy_validation(); self } @@ -295,9 +314,6 @@ impl GossipsubConfigBuilder { /// for the available types. The default is ValidationSetting::Strict. pub fn validation_mode(&mut self, validation_setting: ValidationSetting) -> &mut Self { self.config.validation_mode = validation_setting; - - // warn the user about odd combinations of privacy/validation - self.check_privacy_validation(); self } @@ -316,29 +332,9 @@ impl GossipsubConfigBuilder { /// Constructs a `GossipsubConfig` from the given configuration. pub fn build(&self) -> GossipsubConfig { + self.config.validate_privacy_validation(); self.config.clone() } - - // Warns the user for odd combinations of privacy and validation. - fn check_privacy_validation(&self) { - match (&self.config.validation_mode, &self.config.privacy_mode) { - (ValidationSetting::Strict, PrivacySetting::RandomAuthor) => warn!( - "Messages will be - published unsigned and incoming unsigned messages will be rejected. Consider adjusting - the validation or privacy settings in the config" - ), - (ValidationSetting::Strict, PrivacySetting::Anonymous) => { - warn!("Messages will not be signed or contain an author, but incoming messages requires this. Consider adjusting the validation or privacy settings in the config") - } - (ValidationSetting::Anonymous, PrivacySetting::None) => { - warn!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") - } - (ValidationSetting::Anonymous, PrivacySetting::RandomAuthor) => { - warn!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") - } - (_, _) => {} - } - } } impl std::fmt::Debug for GossipsubConfig { From f5d32b21327f13b20f983afb50eb12b46099fa8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Klaehn?= Date: Sat, 25 Jul 2020 05:57:05 +0200 Subject: [PATCH 24/35] Change the gossipsub rpc protocol to use bytes for message ids (#34) And do the corresponding changes to make this work. --- examples/gossipsub-chat.rs | 2 +- protocols/gossipsub/Cargo.toml | 1 + protocols/gossipsub/src/behaviour.rs | 2 +- protocols/gossipsub/src/behaviour/tests.rs | 13 ++++------ protocols/gossipsub/src/config.rs | 2 +- protocols/gossipsub/src/mcache.rs | 8 +++---- protocols/gossipsub/src/protocol.rs | 28 +++++++++++++++------- protocols/gossipsub/src/rpc.proto | 4 ++-- 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index da9a60ebac6..9e32adf8338 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -83,7 +83,7 @@ fn main() -> Result<(), Box> { let message_id_fn = |message: &GossipsubMessage| { let mut s = DefaultHasher::new(); message.data.hash(&mut s); - MessageId(s.finish().to_string()) + MessageId::from(s.finish().to_string()) }; // set custom gossipsub diff --git a/protocols/gossipsub/Cargo.toml b/protocols/gossipsub/Cargo.toml index 30bcfcad368..6ed3955323a 100644 --- a/protocols/gossipsub/Cargo.toml +++ b/protocols/gossipsub/Cargo.toml @@ -26,6 +26,7 @@ base64 = "0.11.0" lru = "0.4.3" smallvec = "1.1.0" prost = "0.6.1" +hex_fmt = "0.3.0" [dev-dependencies] async-std = "1.6.2" diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 0113f06131d..33d2738d0af 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -339,7 +339,7 @@ impl Gossipsub { None => { warn!( "Message not in cache. Ignoring forwarding. Message Id: {}", - message_id.0 + message_id ); return false; } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 91bcf109f64..9996bc651b8 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -680,7 +680,7 @@ mod tests { let (mut gs, peers, _) = build_and_inject_nodes(20, Vec::new(), true); let events_before = gs.events.len(); - gs.handle_iwant(&peers[7], vec![MessageId(String::from("unknown id"))]); + gs.handle_iwant(&peers[7], vec![MessageId::new(b"unknown id")]); let events_after = gs.events.len(); assert_eq!( @@ -697,10 +697,7 @@ mod tests { gs.handle_ihave( &peers[7], - vec![( - topic_hashes[0].clone(), - vec![MessageId(String::from("unknown id"))], - )], + vec![(topic_hashes[0].clone(), vec![MessageId::new(b"unknown id")])], ); // check that we sent an IWANT request for `unknown id` @@ -708,7 +705,7 @@ mod tests { Some(controls) => controls.iter().any(|c| match c { GossipsubControlAction::IWant { message_ids } => message_ids .iter() - .any(|m| *m.0 == String::from("unknown id")), + .any(|m| *m == MessageId::new(b"unknown id")), _ => false, }), _ => false, @@ -727,7 +724,7 @@ mod tests { let (mut gs, peers, topic_hashes) = build_and_inject_nodes(20, vec![String::from("topic1")], true); - let msg_id = MessageId(String::from("known id")); + let msg_id = MessageId::new(b"known id"); let events_before = gs.events.len(); gs.handle_ihave(&peers[7], vec![(topic_hashes[0].clone(), vec![msg_id])]); @@ -750,7 +747,7 @@ mod tests { &peers[7], vec![( TopicHash::from_raw(String::from("unsubscribed topic")), - vec![MessageId(String::from("irrelevant id"))], + vec![MessageId::new(b"irrelevant id")], )], ); let events_after = gs.events.len(); diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index f5994d82bef..e36802e3ac4 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -151,7 +151,7 @@ impl Default for GossipsubConfig { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); source_string.push_str(&message.sequence_number.to_string()); - MessageId(source_string) + MessageId::from(source_string) }, } } diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index e91a0602c6f..108a17c8820 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -62,7 +62,7 @@ impl MessageCache { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); source_string.push_str(&message.sequence_number.to_string()); - MessageId(source_string) + MessageId::from(source_string) }; MessageCache { gossip, @@ -160,7 +160,7 @@ mod tests { // default message id is: source + sequence number let mut source_string = message.source.to_base58(); source_string.push_str(&message.sequence_number.to_string()); - MessageId(source_string) + MessageId::from(source_string) }; let x: usize = 3; let mc = MessageCache::new(x, 5, default_id); @@ -207,7 +207,7 @@ mod tests { mc.put(m.clone()); // Try to get an incorrect ID - let wrong_id = MessageId(String::from("wrongid")); + let wrong_id = MessageId::new(b"wrongid"); let fetched = mc.get(&wrong_id); assert_eq!(fetched.is_none(), true); } @@ -218,7 +218,7 @@ mod tests { let mc = MessageCache::new_default(10, 15); // Try to get an incorrect ID - let wrong_string = MessageId(String::from("imempty")); + let wrong_string = MessageId::new(b"imempty"); let fetched = mc.get(&wrong_string); assert_eq!(fetched.is_none(), true); } diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index a5129c57afb..d49e018a867 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -351,7 +351,7 @@ impl Decoder for GossipsubCodec { message_ids: ihave .message_ids .into_iter() - .map(|x| MessageId(x)) + .map(MessageId::from) .collect::>(), }) .collect(); @@ -363,7 +363,7 @@ impl Decoder for GossipsubCodec { message_ids: iwant .message_ids .into_iter() - .map(|x| MessageId(x)) + .map(MessageId::from) .collect::>(), }) .collect(); @@ -410,18 +410,30 @@ impl Decoder for GossipsubCodec { } /// A type for gossipsub message ids. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct MessageId(pub String); +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct MessageId(Vec); + +impl MessageId { + pub fn new(value: &[u8]) -> Self { + Self(value.to_vec()) + } +} + +impl>> From for MessageId { + fn from(value: T) -> Self { + Self(value.into()) + } +} impl std::fmt::Display for MessageId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) + write!(f, "{}", hex_fmt::HexFmt(&self.0)) } } -impl Into for MessageId { - fn into(self) -> String { - self.0.into() +impl std::fmt::Debug for MessageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MessageId({})", hex_fmt::HexFmt(&self.0)) } } diff --git a/protocols/gossipsub/src/rpc.proto b/protocols/gossipsub/src/rpc.proto index 13ab9ac8609..499b3b43af8 100644 --- a/protocols/gossipsub/src/rpc.proto +++ b/protocols/gossipsub/src/rpc.proto @@ -32,11 +32,11 @@ message ControlMessage { message ControlIHave { optional string topic_id = 1; - repeated string message_ids = 2; + repeated bytes message_ids = 2; } message ControlIWant { - repeated string message_ids= 1; + repeated bytes message_ids= 1; } message ControlGraft { From c3a7757a64da2e6b60bbe942efca4d8f29064dbd Mon Sep 17 00:00:00 2001 From: Age Manning Date: Sun, 26 Jul 2020 19:13:27 +1000 Subject: [PATCH 25/35] Add optional privacy and validation settings --- examples/gossipsub-chat.rs | 4 +- examples/ipfs-private.rs | 4 +- protocols/gossipsub/src/behaviour.rs | 434 +++++++++++++-------- protocols/gossipsub/src/behaviour/tests.rs | 16 +- protocols/gossipsub/src/config.rs | 87 +---- protocols/gossipsub/src/handler.rs | 5 +- protocols/gossipsub/src/lib.rs | 4 +- protocols/gossipsub/src/mcache.rs | 52 +-- protocols/gossipsub/src/protocol.rs | 120 ++++-- protocols/gossipsub/tests/smoke.rs | 7 +- 10 files changed, 425 insertions(+), 308 deletions(-) diff --git a/examples/gossipsub-chat.rs b/examples/gossipsub-chat.rs index 9e32adf8338..6e0757cbf33 100644 --- a/examples/gossipsub-chat.rs +++ b/examples/gossipsub-chat.rs @@ -50,7 +50,7 @@ use async_std::{io, task}; use env_logger::{Builder, Env}; use futures::prelude::*; use libp2p::gossipsub::protocol::MessageId; -use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, Signing, Topic}; +use libp2p::gossipsub::{GossipsubEvent, GossipsubMessage, MessageAuthenticity, Topic}; use libp2p::{gossipsub, identity, PeerId}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -94,7 +94,7 @@ fn main() -> Result<(), Box> { .build(); // build a gossipsub network behaviour let mut gossipsub = - gossipsub::Gossipsub::new(Signing::Enabled(local_key), gossipsub_config); + gossipsub::Gossipsub::new(MessageAuthenticity::Signed(local_key), gossipsub_config); gossipsub.subscribe(topic.clone()); libp2p::Swarm::new(transport, gossipsub, local_peer_id) }; diff --git a/examples/ipfs-private.rs b/examples/ipfs-private.rs index 727f29a8696..a7fdbfc1a86 100644 --- a/examples/ipfs-private.rs +++ b/examples/ipfs-private.rs @@ -35,7 +35,7 @@ use async_std::{io, task}; use futures::{future, prelude::*}; use libp2p::{ core::{either::EitherTransport, transport::upgrade::Version, StreamMuxer}, - gossipsub::{self, Gossipsub, GossipsubConfigBuilder, GossipsubEvent, Signing}, + gossipsub::{self, Gossipsub, GossipsubConfigBuilder, GossipsubEvent, MessageAuthenticity}, identify::{Identify, IdentifyEvent}, identity, multiaddr::Protocol, @@ -243,7 +243,7 @@ fn main() -> Result<(), Box> { .max_transmit_size(262144) .build(); let mut behaviour = MyBehaviour { - gossipsub: Gossipsub::new(Signing::Enabled(local_key.clone()), gossipsub_config), + gossipsub: Gossipsub::new(MessageAuthenticity::Signed(local_key.clone()), gossipsub_config), identify: Identify::new( "/ipfs/0.1.0".into(), "rust-ipfs-example".into(), diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 33d2738d0af..cccd0466bee 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -18,7 +18,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use crate::config::{GossipsubConfig, PrivacySetting, ValidationSetting}; +use crate::config::{GossipsubConfig, ValidationMode}; use crate::error::PublishError; use crate::handler::GossipsubHandler; use crate::mcache::MessageCache; @@ -43,31 +43,110 @@ use std::{ collections::HashSet, collections::VecDeque, collections::{hash_map::HashMap, BTreeSet}, - iter, + fmt, iter, sync::Arc, task::{Context, Poll}, - fmt, }; use wasm_timer::{Instant, Interval}; mod tests; -/// Determines message signing is enabled or not. +/// Determines if published messages should be signed or not. +/// +/// Without signing, a number of privacy preserving modes can be selected. +/// +/// NOTE: The default validation settings are to require signatures. The [`ValidationSetting`] +/// should be updated in the [`GossipsubConfig`] to allow for unsigned messages. #[derive(Clone)] -pub enum Signing { - /// Message signing is enabled. - Enabled(Keypair), +pub enum MessageAuthenticity { + /// Message signing is enabled. The author will be the owner of the key and the sequence number + /// will be a random number. + Signed(Keypair), + /// Message signing is disabled. + /// + /// The specified `PeerId` will be used as the author of all published messages. The sequence + /// number will be randomized. + Author(PeerId), /// Message signing is disabled. /// - /// NOTE: The default validation settings are to require signatures. The [`ValidationSetting`] - /// should be updated in the [`GossipsubConfig`] to allow for unsigned messages. - Disabled, + /// A random `PeerId` will be used when publishing each message. The sequence number will be a + /// random number. + RandomAuthor, + /// Message signing is disabled. + /// + /// The author of the message and the sequence numbers are excluded from the message. + /// + /// NOTE: Excluding these fields may make these messages invalid by other nodes who enforce validation of these + /// fields. See [`ValidationMode`] in the `GossipsubConfig` for how to customise this for rust-libp2p gossipsub. A custom message_id function will need to be set to prevent all messages from a peer being filtered as duplicates. + Anonymous, +} + +impl MessageAuthenticity { + /// Returns true if signing is enabled. + fn is_signing(&self) -> bool { + match self { + MessageAuthenticity::Signed(_) => true, + _ => false, + } + } + + fn is_anonymous(&self) -> bool { + match self { + MessageAuthenticity::Anonymous => true, + _ => false, + } + } +} + +/// A data structure for storing information for publishing messages. +enum PublishInfo { + /// Message signing is enabled and this contains relevant information for publishing + /// signed messages. + Signing { + keypair: Keypair, + author: PeerId, + inline_key: Option>, + }, + // Signing is disabled, but this author is used to publish messages. + Author(PeerId), + /// The author is radomized each message. + RandomAuthor, + /// The from and sequence number fields are excluded from the message + Anonymous, +} + +impl From for PublishInfo { + fn from(authenticity: MessageAuthenticity) -> Self { + match authenticity { + MessageAuthenticity::Signed(keypair) => { + let public_key = keypair.public(); + let key_enc = public_key.clone().into_protobuf_encoding(); + let key = if key_enc.len() <= 42 { + // The public key can be inlined in [`rpc_proto::Message::from`], so we don't include it + // specifically in the [`rpc_proto::Message::key`] field. + None + } else { + // Include the protobuf encoding of the public key in the message. + Some(key_enc) + }; + + PublishInfo::Signing { + keypair, + author: public_key.into_peer_id(), + inline_key: key, + } + } + MessageAuthenticity::Author(peer_id) => PublishInfo::Author(peer_id), + MessageAuthenticity::RandomAuthor => PublishInfo::RandomAuthor, + MessageAuthenticity::Anonymous => PublishInfo::Anonymous, + } + } } /// Network behaviour that handles the gossipsub protocol. /// -/// NOTE: Initialisation requires a [`Signing`] and [`GossipsubConfig`] instance. If Signing is set to -/// disabled, the [`ValidationSetting`] in the config should be adjusted to an appropriate level to +/// NOTE: Initialisation requires a [`MessageAuthenticity`] and [`GossipsubConfig`] instance. If message signing is +/// disabled, the [`ValidationMode`] in the config should be adjusted to an appropriate level to /// accept unsigned messages. pub struct Gossipsub { /// Configuration providing gossipsub performance parameters. @@ -79,12 +158,8 @@ pub struct Gossipsub { /// Pools non-urgent control messages between heartbeats. control_pool: HashMap>, - /// The `PeerId` that will be the source of published messages. This can be set to an arbitrary - /// PeerId when the config is initialised via the `Signing` enum. - message_author: PeerId, - - /// An optional keypair for message signing. - keypair: Option, + /// Information used for publishing messages. + publish_info: PublishInfo, /// A map of all connected peers - A map of topic hash to a list of gossipsub peer Ids. topic_peers: HashMap>, @@ -106,45 +181,23 @@ pub struct Gossipsub { /// Heartbeat interval stream. heartbeat: Interval, - - /// The public `peer_id` to add to messages if signing is enabled and the current libp2p key is - /// not inlined in the `PeerId`. - key: Option>, } impl Gossipsub { /// Creates a `Gossipsub` struct given a set of parameters specified by via a `GossipsubConfig`. - pub fn new(signing: Signing, config: GossipsubConfig) -> Self { + pub fn new(privacy: MessageAuthenticity, config: GossipsubConfig) -> Self { // Set up the router given the configuration settings. // We do not allow configurations where a published message would also be rejected if it // were received locally. - config.validate_privacy_validation(); - validate_config(&signing, &config); + validate_config(&privacy, &config.validation_mode); - // Set up the author and inlined key if required. - let (message_author, keypair, inlined_key) = match signing { - Signing::Enabled(kp) => { - let public_key = kp.public(); - let key_enc = public_key.clone().into_protobuf_encoding(); - let key = if key_enc.len() <= 42 { - // The public key can be inlined in [`rpc_proto::Message::from`], so we don't include it - // specifically in the [`rpc_proto::Message::key`] field. - None - } else { - // Include the protobuf encoding of the public key in the message. - Some(key_enc) - }; - (public_key.into_peer_id(), Some(kp), key) - } - Signing::Disabled(peer_id) => (peer_id, None, None), - }; + // Set up message publishing parameters. Gossipsub { events: VecDeque::new(), control_pool: HashMap::new(), - message_author, - keypair, + publish_info: privacy.into(), topic_peers: HashMap::new(), peer_topics: HashMap::new(), mesh: HashMap::new(), @@ -159,7 +212,6 @@ impl Gossipsub { Instant::now() + config.heartbeat_initial_delay, config.heartbeat_interval, ), - key: inlined_key, config, } } @@ -226,7 +278,7 @@ impl Gossipsub { }); for peer in peer_list { - debug!("Sending UNSUBSCRIBE to peer: {:?}", peer); + debug!("Sending UNSUBSCRIBE to peer: {}", peer.to_string()); self.send_message(peer, event.clone()); } } @@ -269,8 +321,7 @@ impl Gossipsub { debug!("Publishing message: {:?}", msg_id); // Forward the message to mesh peers. - let message_source = &self.message_author.clone(); - let mesh_peers_sent = self.forward_msg(message.clone(), message_source); + let mesh_peers_sent = self.forward_msg(message.clone(), None); let mut recipient_peers = HashSet::new(); for topic_hash in &message.topics { @@ -344,7 +395,7 @@ impl Gossipsub { return false; } }; - self.forward_msg(message, propagation_source); + self.forward_msg(message, Some(propagation_source)); true } @@ -497,11 +548,14 @@ impl Gossipsub { debug!("IWANT: Sending cached messages to peer: {:?}", peer_id); // Send the messages to the peer let message_list = cached_messages.into_iter().map(|entry| entry.1).collect(); - self.send_message(peer_id.clone(), GossipsubRpc { - subscriptions: Vec::new(), - messages: message_list, - control_msgs: Vec::new(), - }); + self.send_message( + peer_id.clone(), + GossipsubRpc { + subscriptions: Vec::new(), + messages: message_list, + control_msgs: Vec::new(), + }, + ); } debug!("Completed IWANT handling for peer: {:?}", peer_id); } @@ -539,11 +593,14 @@ impl Gossipsub { "GRAFT: Not subscribed to topics - Sending PRUNE to peer: {:?}", peer_id ); - self.send_message(peer_id.clone(), GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: prune_messages, - }); + self.send_message( + peer_id.clone(), + GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: prune_messages, + }, + ); } debug!("Completed GRAFT handling for peer: {:?}", peer_id); } @@ -591,7 +648,7 @@ impl Gossipsub { // forward the message to mesh peers, if no validation is required if !self.config.manual_propagation { let message_id = (self.config.message_id_fn)(&msg); - self.forward_msg(msg, propagation_source); + self.forward_msg(msg, Some(propagation_source)); debug!("Completed message handling for message: {:?}", message_id); } } @@ -882,11 +939,14 @@ impl Gossipsub { grafts.append(&mut prunes); // send the control messages - self.send_message(peer.clone(), GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: grafts, - }); + self.send_message( + peer.clone(), + GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: grafts, + }, + ); } // handle the remaining prunes @@ -897,17 +957,20 @@ impl Gossipsub { topic_hash: topic_hash.clone(), }) .collect(); - self.send_message(peer.clone(), GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: remaining_prunes, - }); + self.send_message( + peer.clone(), + GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: remaining_prunes, + }, + ); } } /// Helper function which forwards a message to mesh\[topic\] peers. /// Returns true if at least one peer was messaged. - fn forward_msg(&mut self, message: GossipsubMessage, source: &PeerId) -> bool { + fn forward_msg(&mut self, message: GossipsubMessage, source: Option<&PeerId>) -> bool { let msg_id = (self.config.message_id_fn)(&message); debug!("Forwarding message: {:?}", msg_id); let mut recipient_peers = HashSet::new(); @@ -917,7 +980,7 @@ impl Gossipsub { // mesh if let Some(mesh_peers) = self.mesh.get(&topic) { for peer_id in mesh_peers { - if peer_id != source { + if Some(peer_id) != source { recipient_peers.insert(peer_id.clone()); } } @@ -949,41 +1012,84 @@ impl Gossipsub { topics: Vec, data: Vec, ) -> Result { - let sequence_number: u64 = rand::random(); - - // If a signature is required, generate it - let signature = if let Some(keypair) = self.keypair.as_ref() { - let message = rpc_proto::Message { - from: Some(self.message_author.clone().into_bytes()), - data: Some(data.clone()), - seqno: Some(sequence_number.to_be_bytes().to_vec()), - topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(), - signature: None, - key: None, - }; - - let mut buf = Vec::with_capacity(message.encoded_len()); - message - .encode(&mut buf) - .expect("Buffer has sufficient capacity"); - // the signature is over the bytes "libp2p-pubsub:" - let mut signature_bytes = SIGNING_PREFIX.to_vec(); - signature_bytes.extend_from_slice(&buf); - Some(keypair.sign(&signature_bytes)?) - } else { - None - }; + match &self.publish_info { + PublishInfo::Signing { + ref keypair, + author, + inline_key, + } => { + // Build and sign the message + let sequence_number: u64 = rand::random(); + + let signature = { + let message = rpc_proto::Message { + from: Some(author.clone().into_bytes()), + data: Some(data.clone()), + seqno: Some(sequence_number.to_be_bytes().to_vec()), + topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(), + signature: None, + key: None, + }; + + let mut buf = Vec::with_capacity(message.encoded_len()); + message + .encode(&mut buf) + .expect("Buffer has sufficient capacity"); + + // the signature is over the bytes "libp2p-pubsub:" + let mut signature_bytes = SIGNING_PREFIX.to_vec(); + signature_bytes.extend_from_slice(&buf); + Some(keypair.sign(&signature_bytes)?) + }; - Ok(GossipsubMessage { - source: self.message_author.clone(), - data, - // To be interoperable with the go-implementation this is treated as a 64-bit - // big-endian uint. - sequence_number, - topics, - signature, - key: self.key.clone(), - }) + Ok(GossipsubMessage { + source: Some(author.clone()), + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: Some(sequence_number), + topics, + signature, + key: inline_key.clone(), + }) + } + PublishInfo::Author(peer_id) => { + Ok(GossipsubMessage { + source: Some(peer_id.clone()), + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: rand::random(), + topics, + signature: None, + key: None, + }) + } + PublishInfo::RandomAuthor => { + Ok(GossipsubMessage { + source: Some(PeerId::random()), + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: rand::random(), + topics, + signature: None, + key: None, + }) + } + PublishInfo::Anonymous => { + Ok(GossipsubMessage { + source: None, + data, + // To be interoperable with the go-implementation this is treated as a 64-bit + // big-endian uint. + sequence_number: None, + topics, + signature: None, + key: None, + }) + } + } } /// Helper function to get a set of `n` random gossipsub peers for a `topic_hash` @@ -1039,22 +1145,26 @@ impl Gossipsub { /// Takes each control action mapping and turns it into a message fn flush_control_pool(&mut self) { for (peer, controls) in self.control_pool.drain().collect::>() { - self.send_message(peer, GossipsubRpc { - subscriptions: Vec::new(), - messages: Vec::new(), - control_msgs: controls, - }); + self.send_message( + peer, + GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: controls, + }, + ); } } /// Send a GossipsubRpc message to a peer. This will wrap the message in an arc if it /// is not already an arc. fn send_message(&mut self, peer_id: PeerId, message: impl Into>) { - self.events.push_back(NetworkBehaviourAction::NotifyHandler { - peer_id, - event: message.into(), - handler: NotifyHandler::Any, - }) + self.events + .push_back(NetworkBehaviourAction::NotifyHandler { + peer_id, + event: message.into(), + handler: NotifyHandler::Any, + }) } } @@ -1063,14 +1173,10 @@ impl NetworkBehaviour for Gossipsub { type OutEvent = GossipsubEvent; fn new_handler(&mut self) -> Self::ProtocolsHandler { - // let the handler know if we should verify signatures. If message signing is enabled, we - // verify signatures of messages. - let verify_signatures = self.keypair.is_some(); - GossipsubHandler::new( self.config.protocol_id.clone(), self.config.max_transmit_size, - verify_signatures, + self.config.validation_mode.clone(), ) } @@ -1091,11 +1197,14 @@ impl NetworkBehaviour for Gossipsub { if !subscriptions.is_empty() { // send our subscriptions to the peer - self.send_message(id.clone(), GossipsubRpc { - messages: Vec::new(), - subscriptions, - control_msgs: Vec::new(), - }); + self.send_message( + id.clone(), + GossipsubRpc { + messages: Vec::new(), + subscriptions, + control_msgs: Vec::new(), + }, + ); } // For the time being assume all gossipsub peers @@ -1199,34 +1308,33 @@ impl NetworkBehaviour for Gossipsub { >, > { if let Some(event) = self.events.pop_front() { - return Poll::Ready( - match event { + return Poll::Ready(match event { + NetworkBehaviourAction::NotifyHandler { + peer_id, + handler, + event: send_event, + } => { + // clone send event reference if others references are present + let event = Arc::try_unwrap(send_event).unwrap_or_else(|e| (*e).clone()); NetworkBehaviourAction::NotifyHandler { peer_id, + event, handler, - event: send_event, - } => { - // clone send event reference if others references are present - let event = Arc::try_unwrap(send_event).unwrap_or_else(|e| (*e).clone()); - NetworkBehaviourAction::NotifyHandler { - peer_id, - event, - handler, - } - }, - NetworkBehaviourAction::GenerateEvent(e) => { - NetworkBehaviourAction::GenerateEvent(e) } - NetworkBehaviourAction::DialAddress { address } => { - NetworkBehaviourAction::DialAddress { address } - } - NetworkBehaviourAction::DialPeer { peer_id, condition } => { - NetworkBehaviourAction::DialPeer { peer_id, condition } - } - NetworkBehaviourAction::ReportObservedAddr { address } => { - NetworkBehaviourAction::ReportObservedAddr { address } - } - }) + } + NetworkBehaviourAction::GenerateEvent(e) => { + NetworkBehaviourAction::GenerateEvent(e) + } + NetworkBehaviourAction::DialAddress { address } => { + NetworkBehaviourAction::DialAddress { address } + } + NetworkBehaviourAction::DialPeer { peer_id, condition } => { + NetworkBehaviourAction::DialPeer { peer_id, condition } + } + NetworkBehaviourAction::ReportObservedAddr { address } => { + NetworkBehaviourAction::ReportObservedAddr { address } + } + }); } while let Poll::Ready(Some(())) = self.heartbeat.poll_next_unpin(cx) { @@ -1262,7 +1370,6 @@ impl fmt::Debug for GossipsubRpc { } b.finish() } - } /// Event that can happen on the gossipsub behaviour. @@ -1292,19 +1399,26 @@ pub enum GossipsubEvent { /// Validates the combination of signing, privacy and message validation to ensure the /// configuration will not reject published messages. -fn validate_config(signing: &Signing, config: &GossipsubConfig) { - match signing { - Signing::Enabled(_) => { - if let PrivacySetting::RandomAuthor | PrivacySetting::Anonymous = config.privacy_mode { - panic!("Cannot enable message signing without an author or with a random author. Adjust the PrivacySetting in the configuration."); +fn validate_config(authenticity: &MessageAuthenticity, validation_mode: &ValidationMode) { + match validation_mode { + ValidationMode::Anonymous => { + if authenticity.is_signing() { + panic!("Cannot enable message signing with an Anonymous validation mode. Consider changing either the ValidationMode or MessageAuthenticity"); } - // Config validation prevents anonymous validation. + if !authenticity.is_anonymous() { + panic!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config"); + } } - Signing::Disabled => { - if let ValidationSetting::Strict = config.validation_mode { - panic!("Cannot disable signing with message validation set to `Strict`. Adjust the ValidationSetting in the configuration."); + ValidationMode::Strict => { + if !authenticity.is_signing() { + panic!( + "Messages will be + published unsigned and incoming unsigned messages will be rejected. Consider adjusting + the validation or privacy settings in the config" + ); } } + _ => {} } } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 9996bc651b8..3d16ac08692 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -37,7 +37,7 @@ mod tests { // generate a default GossipsubConfig with signing let gs_config = GossipsubConfig::default(); // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(Signing::Enabled(keypair), gs_config); + let mut gs: Gossipsub = Gossipsub::new(MessageAuthenticity::Signed(keypair), gs_config); let mut topic_hashes = vec![]; @@ -548,10 +548,10 @@ mod tests { /// Test Gossipsub.get_random_peers() function fn test_get_random_peers() { // generate a default GossipsubConfig - let key = libp2p_core::identity::Keypair::generate_secp256k1(); - let gs_config = GossipsubConfig::default(); + let mut gs_config = GossipsubConfig::default(); + gs_config.validation_mode = ValidationMode::Anonymous; // create a gossipsub struct - let mut gs: Gossipsub = Gossipsub::new(Signing::Enabled(key), gs_config); + let mut gs: Gossipsub = Gossipsub::new(MessageAuthenticity::Anonymous, gs_config); // create a topic and fill it with some peers let topic_hash = Topic::new("Test".into()).no_hash().clone(); @@ -596,9 +596,9 @@ mod tests { let id = gs.config.message_id_fn; let message = GossipsubMessage { - source: peers[11].clone(), + source: Some(peers[11].clone()), data: vec![1, 2, 3, 4], - sequence_number: 1u64, + sequence_number: Some(1u64), topics: Vec::new(), signature: None, key: None, @@ -637,9 +637,9 @@ mod tests { // perform 10 memshifts and check that it leaves the cache for shift in 1..10 { let message = GossipsubMessage { - source: peers[11].clone(), + source: Some(peers[11].clone()), data: vec![1, 2, 3, 4], - sequence_number: shift, + sequence_number: Some(shift), topics: Vec::new(), signature: None, key: None, diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index e36802e3ac4..0983b9223e7 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -19,35 +19,17 @@ // DEALINGS IN THE SOFTWARE. use crate::protocol::{GossipsubMessage, MessageId}; +use libp2p_core::PeerId; use std::borrow::Cow; use std::time::Duration; -/// The types of privacy settings that can be employed by gossipsub. These are related to how -/// anonymous the publisher of the message would like to be. -#[derive(Debug, Clone)] -pub enum PrivacySetting { - /// This is the default setting. The PeerId publishing the message will be broadcast with the - /// message along with a random sequence number. - None, - /// A randomized `PeerId` will be used as the publisher of a message. - /// - /// NOTE: This mode cannot be used in conjunction with message signing. - RandomAuthor, - /// The author of the message and the sequence numbers are excluded from the message. - /// - /// NOTE: This mode cannot be used in conjunction with message signing. Also not that excluding - /// these fields may make these messages invalid by other nodes who enforce validation of these - /// fields. See [`ValidationSetting`] for how to customise this for rust-libp2p gossipsub. - Anonymous, -} - /// The types of message validation that can be employed by gossipsub. #[derive(Debug, Clone)] -pub enum ValidationSetting { +pub enum ValidationMode { /// This is the default setting. This requires the message author to be a valid `PeerId` and to /// be present as well as the sequence number. All messages must have valid signatures. /// - /// NOTE: This setting will reject messages from nodes using `PrivacySetting::Anonymous` and + /// NOTE: This setting will reject messages from nodes using `PrivacyMode::Anonymous` and /// all messages that do not have signatures. Strict, /// This setting permits messages that have no author, sequence number or signature. If any of @@ -110,13 +92,9 @@ pub struct GossipsubConfig { /// once validated (default is `false`). pub manual_propagation: bool, - /// Determines the level of privacy for the node when publishing a message. See [`PrivacySetting`] for - /// the available types. The default is PrivacySetting::None. - pub privacy_mode: PrivacySetting, - - /// Determines the level of validation used when receiving messages. See [`ValidationSetting`] - /// for the available types. The default is ValidationSetting::Strict. - pub validation_mode: ValidationSetting, + /// Determines the level of validation used when receiving messages. See [`ValidationMode`] + /// for the available types. The default is ValidationMode::Strict. + pub validation_mode: ValidationMode, /// A user-defined function allowing the user to specify the message id of a gossipsub message. /// The default value is to concatenate the source peer id with a sequence number. Setting this @@ -145,41 +123,24 @@ impl Default for GossipsubConfig { max_transmit_size: 2048, hash_topics: false, // default compatibility with floodsub manual_propagation: false, - privacy_mode: PrivacySetting::None, - validation_mode: ValidationSetting::Strict, + validation_mode: ValidationMode::Strict, message_id_fn: |message| { // default message id is: source + sequence number - let mut source_string = message.source.to_base58(); - source_string.push_str(&message.sequence_number.to_string()); + // NOTE: If either the peer_id or source is not provided, we set to 0; + let mut source_string = if let Some(peer_id) = message.source.as_ref() { + peer_id.to_base58() + } else { + PeerId::from_bytes(vec![0, 1, 0]) + .expect("Valid peer id") + .to_base58() + }; + source_string.push_str(&message.sequence_number.unwrap_or_default().to_string()); MessageId::from(source_string) }, } } } -impl GossipsubConfig { - // Prevent users from using settings where the published messages will be rejected based on combinations of privacy and validation. - pub fn validate_privacy_validation(&self) { - match (&self.validation_mode, &self.privacy_mode) { - (ValidationSetting::Strict, PrivacySetting::RandomAuthor) => panic!( - "Messages will be - published unsigned and incoming unsigned messages will be rejected. Consider adjusting - the validation or privacy settings in the config" - ), - (ValidationSetting::Strict, PrivacySetting::Anonymous) => { - panic!("Messages will not be signed or contain an author, but incoming messages requires this. Consider adjusting the validation or privacy settings in the config") - } - (ValidationSetting::Anonymous, PrivacySetting::None) => { - panic!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") - } - (ValidationSetting::Anonymous, PrivacySetting::RandomAuthor) => { - panic!("Published messages contain an author but incoming messages with an author will be rejected. Consider adjusting the validation or privacy settings in the config") - } - (_, _) => {} - } - } -} - /// The builder struct for constructing a gossipsub configuration. pub struct GossipsubConfigBuilder { config: GossipsubConfig, @@ -303,17 +264,10 @@ impl GossipsubConfigBuilder { self } - /// Determines the level of privacy for the node when publishing a message. See [`PrivacySetting`] for - /// the available types. - pub fn privacy_mode(&mut self, privacy_setting: PrivacySetting) -> &mut Self { - self.config.privacy_mode = privacy_setting; - self - } - - /// Determines the level of validation used when receiving messages. See [`ValidationSetting`] - /// for the available types. The default is ValidationSetting::Strict. - pub fn validation_mode(&mut self, validation_setting: ValidationSetting) -> &mut Self { - self.config.validation_mode = validation_setting; + /// Determines the level of validation used when receiving messages. See [`ValidationMode`] + /// for the available types. The default is ValidationMode::Strict. + pub fn validation_mode(&mut self, validation_mode: ValidationMode) -> &mut Self { + self.config.validation_mode = validation_mode; self } @@ -332,7 +286,6 @@ impl GossipsubConfigBuilder { /// Constructs a `GossipsubConfig` from the given configuration. pub fn build(&self) -> GossipsubConfig { - self.config.validate_privacy_validation(); self.config.clone() } } diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index 6202688f3b2..937a5bbd77f 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -19,6 +19,7 @@ // DEALINGS IN THE SOFTWARE. use crate::behaviour::GossipsubRpc; +use crate::config::ValidationMode; use crate::protocol::{GossipsubCodec, ProtocolConfig}; use futures::prelude::*; use futures_codec::Framed; @@ -83,13 +84,13 @@ impl GossipsubHandler { pub fn new( protocol_id: impl Into>, max_transmit_size: usize, - verify_signatures: bool, + validation_mode: ValidationMode, ) -> Self { GossipsubHandler { listen_protocol: SubstreamProtocol::new(ProtocolConfig::new( protocol_id, max_transmit_size, - verify_signatures, + validation_mode, )), inbound_substream: None, outbound_substream: None, diff --git a/protocols/gossipsub/src/lib.rs b/protocols/gossipsub/src/lib.rs index 22d8448462e..a81614eed17 100644 --- a/protocols/gossipsub/src/lib.rs +++ b/protocols/gossipsub/src/lib.rs @@ -148,7 +148,7 @@ mod rpc_proto { include!(concat!(env!("OUT_DIR"), "/gossipsub.pb.rs")); } -pub use self::behaviour::{Gossipsub, GossipsubEvent, GossipsubRpc, Signing}; -pub use self::config::{GossipsubConfig, GossipsubConfigBuilder}; +pub use self::behaviour::{Gossipsub, GossipsubEvent, GossipsubRpc, MessageAuthenticity}; +pub use self::config::{GossipsubConfig, GossipsubConfigBuilder, ValidationMode}; pub use self::protocol::{GossipsubMessage, MessageId}; pub use self::topic::{Topic, TopicHash}; diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index 108a17c8820..6bc72a3e02d 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -55,23 +55,6 @@ impl MessageCache { } } - /// Creates a `MessageCache` with a default message id function. - #[allow(dead_code)] - pub fn new_default(gossip: usize, history_capacity: usize) -> MessageCache { - let default_id = |message: &GossipsubMessage| { - // default message id is: source + sequence number - let mut source_string = message.source.to_base58(); - source_string.push_str(&message.sequence_number.to_string()); - MessageId::from(source_string) - }; - MessageCache { - gossip, - msgs: HashMap::default(), - history: vec![Vec::new(); history_capacity], - msg_id: default_id, - } - } - /// Put a message into the memory cache. /// /// Returns the message if it already exists. @@ -138,9 +121,9 @@ mod tests { fn gen_testm(x: u64, topics: Vec) -> GossipsubMessage { let u8x: u8 = x as u8; - let source = PeerId::random(); + let source = Some(PeerId::random()); let data: Vec = vec![u8x]; - let sequence_number = x; + let sequence_number = Some(x); let m = GossipsubMessage { source, @@ -153,17 +136,22 @@ mod tests { m } - #[test] - /// Test that the message cache can be created. - fn test_new_cache() { + fn new_cache(gossip_size: usize, history: usize) -> MessageCache { let default_id = |message: &GossipsubMessage| { // default message id is: source + sequence number - let mut source_string = message.source.to_base58(); - source_string.push_str(&message.sequence_number.to_string()); + let mut source_string = message.source.as_ref().unwrap().to_base58(); + source_string.push_str(&message.sequence_number.unwrap().to_string()); MessageId::from(source_string) }; + + MessageCache::new(gossip_size, history, default_id) + } + + #[test] + /// Test that the message cache can be created. + fn test_new_cache() { let x: usize = 3; - let mc = MessageCache::new(x, 5, default_id); + let mc = new_cache(x, 5); assert_eq!(mc.gossip, x); } @@ -171,7 +159,7 @@ mod tests { #[test] /// Test you can put one message and get one. fn test_put_get_one() { - let mut mc = MessageCache::new_default(10, 15); + let mut mc = new_cache(10, 15); let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); @@ -197,7 +185,7 @@ mod tests { #[test] /// Test attempting to 'get' with a wrong id. fn test_get_wrong() { - let mut mc = MessageCache::new_default(10, 15); + let mut mc = new_cache(10, 15); let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); @@ -215,7 +203,7 @@ mod tests { #[test] /// Test attempting to 'get' empty message cache. fn test_get_empty() { - let mc = MessageCache::new_default(10, 15); + let mc = new_cache(10, 15); // Try to get an incorrect ID let wrong_string = MessageId::new(b"imempty"); @@ -226,7 +214,7 @@ mod tests { #[test] /// Test adding a message with no topics. fn test_no_topic_put() { - let mut mc = MessageCache::new_default(3, 5); + let mut mc = new_cache(3, 5); // Build the message let m = gen_testm(1, vec![]); @@ -244,7 +232,7 @@ mod tests { #[test] /// Test shift mechanism. fn test_shift() { - let mut mc = MessageCache::new_default(1, 5); + let mut mc = new_cache(1, 5); let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); @@ -268,7 +256,7 @@ mod tests { #[test] /// Test Shift with no additions. fn test_empty_shift() { - let mut mc = MessageCache::new_default(1, 5); + let mut mc = new_cache(1, 5); let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); @@ -294,7 +282,7 @@ mod tests { #[test] /// Test shift to see if the last history messages are removed. fn test_remove_last_from_shift() { - let mut mc = MessageCache::new_default(4, 5); + let mut mc = new_cache(4, 5); let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index d49e018a867..db5deb61841 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -19,6 +19,7 @@ // DEALINGS IN THE SOFTWARE. use crate::behaviour::GossipsubRpc; +use crate::config::ValidationMode; use crate::rpc_proto; use crate::topic::TopicHash; use byteorder::{BigEndian, ByteOrder}; @@ -42,8 +43,8 @@ pub struct ProtocolConfig { protocol_id: Cow<'static, [u8]>, /// The maximum transmit size for a packet. max_transmit_size: usize, - /// Determines whether to check and verify signatures of incoming messages. - verify_signatures: bool, + /// Determines the level of validation to be done on incoming messages. + validation_mode: ValidationMode, } impl ProtocolConfig { @@ -52,12 +53,12 @@ impl ProtocolConfig { pub fn new( protocol_id: impl Into>, max_transmit_size: usize, - verify_signatures: bool, + validation_mode: ValidationMode, ) -> ProtocolConfig { ProtocolConfig { protocol_id: protocol_id.into(), max_transmit_size, - verify_signatures, + validation_mode, } } } @@ -84,7 +85,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec::new(length_codec, self.verify_signatures), + GossipsubCodec::new(length_codec, self.validation_mode), ))) } } @@ -102,7 +103,7 @@ where length_codec.set_max_len(self.max_transmit_size); Box::pin(future::ok(Framed::new( socket, - GossipsubCodec::new(length_codec, self.verify_signatures), + GossipsubCodec::new(length_codec, self.validation_mode), ))) } } @@ -112,15 +113,15 @@ where pub struct GossipsubCodec { /// Codec to encode/decode the Unsigned varint length prefix of the frames. length_codec: codec::UviBytes, - /// Determines whether to check and verify signatures of incoming messages. - verify_signatures: bool, + /// Determines the level of validation performed on incoming messages. + validation_mode: ValidationMode, } impl GossipsubCodec { - pub fn new(length_codec: codec::UviBytes, verify_signatures: bool) -> Self { + pub fn new(length_codec: codec::UviBytes, validation_mode: ValidationMode) -> Self { GossipsubCodec { length_codec, - verify_signatures, + validation_mode, } } @@ -199,9 +200,9 @@ impl Encoder for GossipsubCodec { for message in item.messages.into_iter() { let message = rpc_proto::Message { - from: Some(message.source.into_bytes()), + from: message.source.map(|m| m.into_bytes()), data: Some(message.data), - seqno: Some(message.sequence_number.to_be_bytes().to_vec()), + seqno: message.sequence_number.map(|s| s.to_be_bytes().to_vec()), topic_ids: message.topics.into_iter().map(TopicHash::into).collect(), signature: message.signature, key: message.key, @@ -297,8 +298,48 @@ impl Decoder for GossipsubCodec { let mut messages = Vec::with_capacity(rpc.publish.len()); for message in rpc.publish.into_iter() { + let mut verify_signature = false; + let mut verify_sequence_no = false; + let mut verify_source = false; + + match self.validation_mode { + ValidationMode::Strict => { + // Validate everything + verify_signature = true; + verify_sequence_no = true; + verify_source = true; + } + ValidationMode::Permissive => { + // If the fields exist, validate them + if message.signature.is_some() { + verify_signature = true; + } + if message.seqno.is_some() { + verify_sequence_no = true; + } + if message.from.is_some() { + verify_source = true; + } + } + ValidationMode::Anonymous => { + if message.signature.is_some() { + warn!("Message dropped. Signature field was non-empty and anonymous validation mode is set"); + return Ok(None); + } + if message.seqno.is_some() { + warn!("Message dropped. Sequence number was non-empty and anonymous validation mode is set"); + return Ok(None); + } + if message.from.is_some() { + warn!("Message dropped. Message source was non-empty and anonymous validation mode is set"); + return Ok(None); + } + } + ValidationMode::None => {} + } + // verify message signatures if required - if self.verify_signatures { + if verify_signature { // If a single message is unsigned, we will drop all of them // Most implementations should not have a list of mixed signed/not-signed messages in a single RPC // NOTE: Invalid messages are simply dropped with a warning log. We don't throw an @@ -311,24 +352,38 @@ impl Decoder for GossipsubCodec { } // ensure the sequence number is a u64 - let seq_no = message.seqno.ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - "sequence number was not provided", + let sequence_number = if verify_sequence_no { + let seq_no = message.seqno.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "sequence number was not provided", + ) + })?; + if seq_no.len() != 8 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "sequence number has an incorrect size", + )); + } + Some(BigEndian::read_u64(&seq_no)) + } else { + None + }; + + let source = if verify_source { + Some( + PeerId::from_bytes(message.from.unwrap_or_default()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "Invalid Peer Id") + })?, ) - })?; - if seq_no.len() != 8 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "sequence number has an incorrect size", - )); - } + } else { + None + }; messages.push(GossipsubMessage { - source: PeerId::from_bytes(message.from.unwrap_or_default()) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid Peer Id"))?, + source, data: message.data.unwrap_or_default(), - sequence_number: BigEndian::read_u64(&seq_no), + sequence_number, topics: message .topic_ids .into_iter() @@ -441,13 +496,13 @@ impl std::fmt::Debug for MessageId { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct GossipsubMessage { /// Id of the peer that published this message. - pub source: PeerId, + pub source: Option, /// Content of the message. Its meaning is out of scope of this library. pub data: Vec, /// A random sequence number. - pub sequence_number: u64, + pub sequence_number: Option, /// List of topics this message belongs to. /// @@ -524,7 +579,10 @@ mod tests { // generate an arbitrary GossipsubMessage using the behaviour signing functionality let config = GossipsubConfig::default(); - let gs = Gossipsub::new(crate::Signing::Enabled(keypair.0.clone()), config); + let gs = Gossipsub::new( + crate::MessageAuthenticity::Signed(keypair.0.clone()), + config, + ); let data = (0..g.gen_range(1, 1024)).map(|_| g.gen()).collect(); let topics = Vec::arbitrary(g) .into_iter() @@ -582,7 +640,7 @@ mod tests { control_msgs: vec![], }; - let mut codec = GossipsubCodec::new(codec::UviBytes::default(), true); + let mut codec = GossipsubCodec::new(codec::UviBytes::default(), ValidationMode::Strict); let mut buf = BytesMut::new(); codec.encode(rpc.clone(), &mut buf).unwrap(); let decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index cce516b1e62..85258f81575 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -33,7 +33,9 @@ use libp2p_core::{ identity, multiaddr::Protocol, muxing::StreamMuxerBox, transport::MemoryTransport, upgrade, Multiaddr, Transport, }; -use libp2p_gossipsub::{Gossipsub, GossipsubConfigBuilder, GossipsubEvent, Signing, Topic}; +use libp2p_gossipsub::{ + Gossipsub, GossipsubConfigBuilder, GossipsubEvent, MessageAuthenticity, Topic, ValidationMode, +}; use libp2p_plaintext::PlainText2Config; use libp2p_swarm::Swarm; use libp2p_yamux as yamux; @@ -154,8 +156,9 @@ fn build_node() -> (Multiaddr, Swarm) { let config = GossipsubConfigBuilder::new() .heartbeat_initial_delay(Duration::from_millis(50)) .heartbeat_interval(Duration::from_millis(100)) + .validation_mode(ValidationMode::Permissive) .build(); - let behaviour = Gossipsub::new(Signing::Disabled(peer_id.clone()), config); + let behaviour = Gossipsub::new(MessageAuthenticity::Author(peer_id.clone()), config); let mut swarm = Swarm::new(transport, behaviour, peer_id); let port = 1 + random::(); From 0c43ed4dc18e400bb75bb5eeb7eace71841a8afd Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 27 Jul 2020 14:25:03 +1000 Subject: [PATCH 26/35] Correct doc link --- protocols/gossipsub/src/behaviour.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index cccd0466bee..0aa367f6f68 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -55,7 +55,7 @@ mod tests; /// /// Without signing, a number of privacy preserving modes can be selected. /// -/// NOTE: The default validation settings are to require signatures. The [`ValidationSetting`] +/// NOTE: The default validation settings are to require signatures. The [`ValidationMode`] /// should be updated in the [`GossipsubConfig`] to allow for unsigned messages. #[derive(Clone)] pub enum MessageAuthenticity { From efa40e33bdaf2fbecb00bbfbfd7feb4abb108836 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 27 Jul 2020 15:35:19 +1000 Subject: [PATCH 27/35] Prevent invalid messages from being gossiped --- protocols/gossipsub/src/behaviour.rs | 28 ++++++++++++++++++----- protocols/gossipsub/src/config.rs | 34 +++++++++++++++++++++------- protocols/gossipsub/src/mcache.rs | 16 ++++++++++++- protocols/gossipsub/src/protocol.rs | 4 ++++ 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 0aa367f6f68..2ffc3f8a718 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -373,19 +373,24 @@ impl Gossipsub { Ok(()) } - /// This function should be called when `config.manual_propagation` is `true` in order to - /// propagate messages. Messages are stored in the ['Memcache'] and validation is expected to be + /// This function should be called when `config.validate_messages` is `true` in order to + /// validate and propagate messages. Messages are stored in the ['Memcache'] and validation is expected to be /// fast enough that the messages should still exist in the cache. /// /// Calling this function will propagate a message stored in the cache, if it still exists. /// If the message still exists in the cache, it will be forwarded and this function will return true, /// otherwise it will return false. - pub fn propagate_message( + /// + /// The `propagation_source` parameter indicates who the message was received by and will not + /// be forwarded back to that peer. + /// + /// This should only be called once per message. + pub fn validate_message( &mut self, message_id: &MessageId, propagation_source: &PeerId, ) -> bool { - let message = match self.mcache.get(message_id) { + let message = match self.mcache.validate(message_id) { Some(message) => message.clone(), None => { warn!( @@ -625,13 +630,20 @@ impl Gossipsub { /// Handles a newly received GossipsubMessage. /// Forwards the message to all peers in the mesh. - fn handle_received_message(&mut self, msg: GossipsubMessage, propagation_source: &PeerId) { + fn handle_received_message(&mut self, mut msg: GossipsubMessage, propagation_source: &PeerId) { let msg_id = (self.config.message_id_fn)(&msg); debug!( "Handling message: {:?} from peer: {}", msg_id, propagation_source.to_string() ); + + // If we are not validating messages, assume this message is validated + // This will allow the message to be gossiped without explicitly calling + // `validate_message`. + if !self.config.validate_messages { + msg.validated = true; + } if self.mcache.put(msg.clone()).is_some() { debug!("Message already received, ignoring. Message: {:?}", msg_id); return; @@ -646,7 +658,7 @@ impl Gossipsub { } // forward the message to mesh peers, if no validation is required - if !self.config.manual_propagation { + if !self.config.validate_messages { let message_id = (self.config.message_id_fn)(&msg); self.forward_msg(msg, Some(propagation_source)); debug!("Completed message handling for message: {:?}", message_id); @@ -1051,6 +1063,7 @@ impl Gossipsub { topics, signature, key: inline_key.clone(), + validated: true, // all published messages are valid }) } PublishInfo::Author(peer_id) => { @@ -1063,6 +1076,7 @@ impl Gossipsub { topics, signature: None, key: None, + validated: true, // all published messages are valid }) } PublishInfo::RandomAuthor => { @@ -1075,6 +1089,7 @@ impl Gossipsub { topics, signature: None, key: None, + validated: true, // all published messages are valid }) } PublishInfo::Anonymous => { @@ -1087,6 +1102,7 @@ impl Gossipsub { topics, signature: None, key: None, + validated: true, // all published messages are valid }) } } diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 0983b9223e7..3d60f4c46db 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -83,14 +83,21 @@ pub struct GossipsubConfig { /// The maximum byte size for each gossip (default is 2048 bytes). pub max_transmit_size: usize, + /// Duplicates are prevented by storing message id's of known messages in an LRU time cache. + /// This settings sets the time period that messages are stored in the cache. Duplicates can be + /// received if duplicate messages are sent at a time greater than this setting apart. The + /// default is 1 minute. + pub duplicate_cache_time: Duration, + /// Flag determining if gossipsub topics are hashed or sent as plain strings (default is false). pub hash_topics: bool, /// When set to `true`, prevents automatic forwarding of all received messages. This setting /// allows a user to validate the messages before propagating them to their peers. If set to - /// true, the user must manually call `propagate_message()` on the behaviour to forward message - /// once validated (default is `false`). - pub manual_propagation: bool, + /// true, the user must manually call `validate_message()` on the behaviour to forward message + /// once validated (default is `false`). Furthermore, the application may optionally call + /// `invalidate_message()` on the behaviour to remove the message from the memcache. + pub validate_messages: bool, /// Determines the level of validation used when receiving messages. See [`ValidationMode`] /// for the available types. The default is ValidationMode::Strict. @@ -121,8 +128,9 @@ impl Default for GossipsubConfig { heartbeat_interval: Duration::from_secs(1), fanout_ttl: Duration::from_secs(60), max_transmit_size: 2048, + duplicate_cache_time: Duration::from_secs(60), hash_topics: false, // default compatibility with floodsub - manual_propagation: false, + validate_messages: false, validation_mode: ValidationMode::Strict, message_id_fn: |message| { // default message id is: source + sequence number @@ -249,6 +257,15 @@ impl GossipsubConfigBuilder { self } + /// Duplicates are prevented by storing message id's of known messages in an LRU time cache. + /// This settings sets the time period that messages are stored in the cache. Duplicates can be + /// received if duplicate messages are sent at a time greater than this setting apart. The + /// default is 1 minute. + pub fn duplicate_cache_time(&mut self, cache_size: Duration) -> &mut Self { + self.config.duplicate_cache_time = cache_size; + self + } + /// When set, gossipsub topics are hashed instead of being sent as plain strings. pub fn hash_topics(&mut self) -> &mut Self { self.config.hash_topics = true; @@ -257,10 +274,10 @@ impl GossipsubConfigBuilder { /// When set, prevents automatic forwarding of all received messages. This setting /// allows a user to validate the messages before propagating them to their peers. If set, - /// the user must manually call `propagate_message()` on the behaviour to forward a message + /// the user must manually call `validate_message()` on the behaviour to forward a message /// once validated. - pub fn manual_propagation(&mut self) -> &mut Self { - self.config.manual_propagation = true; + pub fn validate_messages(&mut self) -> &mut Self { + self.config.validate_messages = true; self } @@ -304,8 +321,9 @@ impl std::fmt::Debug for GossipsubConfig { let _ = builder.field("heartbeat_interval", &self.heartbeat_interval); let _ = builder.field("fanout_ttl", &self.fanout_ttl); let _ = builder.field("max_transmit_size", &self.max_transmit_size); + let _ = builder.field("duplicate_cache_time", &self.duplicate_cache_time); let _ = builder.field("hash_topics", &self.hash_topics); - let _ = builder.field("manual_propagation", &self.manual_propagation); + let _ = builder.field("validate_messages", &self.validate_messages); builder.finish() } } diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index 6bc72a3e02d..dcb3e5d1a05 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -78,6 +78,14 @@ impl MessageCache { self.msgs.get(message_id) } + /// Gets and validates a message with `message_id`. + pub fn validate(&mut self, message_id: &MessageId) -> Option<&GossipsubMessage> { + self.msgs.get_mut(message_id).map(|message| { + message.validated = true; + &*message + }) + } + /// Get a list of GossipIds for a given topic pub fn get_gossip_ids(&self, topic: &TopicHash) -> Vec { self.history[..self.gossip] @@ -88,7 +96,13 @@ impl MessageCache { .iter() .filter_map(|entry| { if entry.topics.iter().any(|t| t == topic) { - Some(entry.mid.clone()) + let mid = &entry.mid; + // Only gossip validated messages + if let Some(true) = self.msgs.get(mid).map(|msg| msg.validated) { + Some(mid.clone()) + } else { + None + } } else { None } diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index db5deb61841..884ef8c2896 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -391,6 +391,7 @@ impl Decoder for GossipsubCodec { .collect(), signature: message.signature, key: message.key, + validated: false, }); } @@ -514,6 +515,9 @@ pub struct GossipsubMessage { /// The public key of the message if it is signed and the source `PeerId` cannot be inlined. pub key: Option>, + + /// Flag indicating if this message has been validated by the application or not. + pub validated: bool, } /// A subscription received by the gossipsub system. From a514c604a8e0d205a3c91119f8d980731ab6cd02 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 27 Jul 2020 16:05:09 +1000 Subject: [PATCH 28/35] Remove unvalidated messages from gossip. Reintroduce duplicate cache --- protocols/gossipsub/Cargo.toml | 2 +- protocols/gossipsub/src/behaviour.rs | 16 +++++++++++++--- protocols/gossipsub/src/behaviour/tests.rs | 2 ++ protocols/gossipsub/src/config.rs | 3 ++- protocols/gossipsub/src/mcache.rs | 1 + protocols/gossipsub/src/protocol.rs | 6 ++++-- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/protocols/gossipsub/Cargo.toml b/protocols/gossipsub/Cargo.toml index 6ed3955323a..1541d6f9359 100644 --- a/protocols/gossipsub/Cargo.toml +++ b/protocols/gossipsub/Cargo.toml @@ -23,10 +23,10 @@ unsigned-varint = { version = "0.4.0", features = ["futures-codec"] } log = "0.4.8" sha2 = "0.8.1" base64 = "0.11.0" -lru = "0.4.3" smallvec = "1.1.0" prost = "0.6.1" hex_fmt = "0.3.0" +lru_time_cache = "0.10.0" [dev-dependencies] async-std = "1.6.2" diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 2ffc3f8a718..7ffc8688f3c 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -36,6 +36,7 @@ use libp2p_swarm::{ NetworkBehaviour, NetworkBehaviourAction, NotifyHandler, PollParameters, ProtocolsHandler, }; use log::{debug, error, info, trace, warn}; +use lru_time_cache::LruCache; use prost::Message; use rand; use rand::{seq::SliceRandom, thread_rng}; @@ -161,6 +162,10 @@ pub struct Gossipsub { /// Information used for publishing messages. publish_info: PublishInfo, + /// An LRU Time cache for storing seen messages (based on their ID). This cache prevents + /// duplicates from being propagated to the application and on the network. + duplication_cache: LruCache, + /// A map of all connected peers - A map of topic hash to a list of gossipsub peer Ids. topic_peers: HashMap>, @@ -198,6 +203,7 @@ impl Gossipsub { events: VecDeque::new(), control_pool: HashMap::new(), publish_info: privacy.into(), + duplication_cache: LruCache::with_expiry_duration(config.duplicate_cache_time), topic_peers: HashMap::new(), peer_topics: HashMap::new(), mesh: HashMap::new(), @@ -307,8 +313,9 @@ impl Gossipsub { data.into(), )?; let msg_id = (self.config.message_id_fn)(&message); - // Add published message to our received caches - if self.mcache.put(message.clone()).is_some() { + // Add published message to our memcache and add it to the duplicate cache. + self.mcache.put(message.clone()); + if self.duplication_cache.insert(msg_id.clone(), ()).is_some() { // This message has already been seen. We don't re-publish messages that have already // been published on the network. warn!( @@ -644,7 +651,10 @@ impl Gossipsub { if !self.config.validate_messages { msg.validated = true; } - if self.mcache.put(msg.clone()).is_some() { + + // Add the message to the duplication cache and memcache. + self.mcache.put(msg.clone()); + if self.duplication_cache.insert(msg_id.clone(), ()).is_some() { debug!("Message already received, ignoring. Message: {:?}", msg_id); return; } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 3d16ac08692..e95b7cbc31f 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -602,6 +602,7 @@ mod tests { topics: Vec::new(), signature: None, key: None, + validated: true, }; let msg_id = id(&message); gs.mcache.put(message.clone()); @@ -643,6 +644,7 @@ mod tests { topics: Vec::new(), signature: None, key: None, + validated: true, }; let msg_id = id(&message); gs.mcache.put(message.clone()); diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 3d60f4c46db..b32e3793a86 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -96,7 +96,8 @@ pub struct GossipsubConfig { /// allows a user to validate the messages before propagating them to their peers. If set to /// true, the user must manually call `validate_message()` on the behaviour to forward message /// once validated (default is `false`). Furthermore, the application may optionally call - /// `invalidate_message()` on the behaviour to remove the message from the memcache. + /// `invalidate_message()` on the behaviour to remove the message from the memcache. The + /// default is false. pub validate_messages: bool, /// Determines the level of validation used when receiving messages. See [`ValidationMode`] diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index dcb3e5d1a05..27dccbc2c3d 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -146,6 +146,7 @@ mod tests { topics, signature: None, key: None, + validated: true, }; m } diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index 884ef8c2896..783270724ee 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -466,7 +466,7 @@ impl Decoder for GossipsubCodec { } /// A type for gossipsub message ids. -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct MessageId(Vec); impl MessageId { @@ -647,7 +647,9 @@ mod tests { let mut codec = GossipsubCodec::new(codec::UviBytes::default(), ValidationMode::Strict); let mut buf = BytesMut::new(); codec.encode(rpc.clone(), &mut buf).unwrap(); - let decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); + let mut decoded_rpc = codec.decode(&mut buf).unwrap().unwrap(); + // mark as validated as its a published message + decoded_rpc.messages[0].validated = true; assert_eq!(rpc, decoded_rpc); } From b2570250994ebf71428ac3e449e35f3126562baa Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 28 Jul 2020 15:04:45 +1000 Subject: [PATCH 29/35] Send grafts when subscriptions are added to the mesh --- protocols/gossipsub/src/behaviour.rs | 83 +++++++++++++++------- protocols/gossipsub/src/behaviour/tests.rs | 4 +- protocols/gossipsub/src/handler.rs | 13 +++- protocols/gossipsub/tests/smoke.rs | 37 ++++++++-- 4 files changed, 104 insertions(+), 33 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 7ffc8688f3c..e74b94a0bf6 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -697,6 +697,12 @@ impl Gossipsub { } }; + // Collect potential graft messages for the peer. + let mut grafts = Vec::new(); + + // Notify the application about the subscription, after the grafts are sent. + let mut application_event = Vec::new(); + for subscription in subscriptions { // get the peers from the mapping, or insert empty lists if topic doesn't exist let peer_list = self @@ -708,34 +714,38 @@ impl Gossipsub { GossipsubSubscriptionAction::Subscribe => { if peer_list.insert(propagation_source.clone()) { debug!( - "SUBSCRIPTION: topic_peer: Adding gossip peer: {} to topic: {:?}", + "SUBSCRIPTION: Adding gossip peer: {} to topic: {:?}", propagation_source.to_string(), subscription.topic_hash ); } // add to the peer_topics mapping - if subscribed_topics.insert(subscription.topic_hash.clone()) { - info!( - "SUBSCRIPTION: Adding peer: {} to topic: {:?}", - propagation_source.to_string(), - subscription.topic_hash - ); - } + subscribed_topics.insert(subscription.topic_hash.clone()); // if the mesh needs peers add the peer to the mesh if let Some(peers) = self.mesh.get_mut(&subscription.topic_hash) { if peers.len() < self.config.mesh_n_low { if peers.insert(propagation_source.clone()) { debug!( - "SUBSCRIPTION: Adding peer {} to the mesh", + "SUBSCRIPTION: Adding peer {} to the mesh for topic {:?}", propagation_source.to_string(), + subscription.topic_hash ); + // send graft to the peer + debug!( + "Sending GRAFT to peer {} for topic {:?}", + propagation_source.to_string(), + subscription.topic_hash + ); + grafts.push(GossipsubControlAction::Graft { + topic_hash: subscription.topic_hash.clone(), + }); } } } // generates a subscription event to be polled - self.events.push_back(NetworkBehaviourAction::GenerateEvent( + application_event.push(NetworkBehaviourAction::GenerateEvent( GossipsubEvent::Subscribed { peer_id: propagation_source.clone(), topic: subscription.topic_hash.clone(), @@ -755,10 +765,11 @@ impl Gossipsub { // remove the peer from the mesh if it exists if let Some(peers) = self.mesh.get_mut(&subscription.topic_hash) { peers.remove(propagation_source); + // the peer requested the unsubscription so we don't need to send a PRUNE. } // generate an unsubscribe event to be polled - self.events.push_back(NetworkBehaviourAction::GenerateEvent( + application_event.push(NetworkBehaviourAction::GenerateEvent( GossipsubEvent::Unsubscribed { peer_id: propagation_source.clone(), topic: subscription.topic_hash.clone(), @@ -767,6 +778,25 @@ impl Gossipsub { } } } + + // If we need to send grafts to peer, do so immediately, rather than waiting for the + // heartbeat. + if !grafts.is_empty() { + self.send_message( + propagation_source.clone(), + GossipsubRpc { + subscriptions: Vec::new(), + messages: Vec::new(), + control_msgs: grafts, + }, + ); + } + + // Notify the application of the subscriptions + for event in application_event { + self.events.push_back(event); + } + trace!( "Completed handling subscriptions from source: {:?}", propagation_source @@ -906,7 +936,6 @@ impl Gossipsub { /// Emits gossip - Send IHAVE messages to a random set of gossip peers. This is applied to mesh /// and fanout peers fn emit_gossip(&mut self) { - debug!("Started gossip"); for (topic_hash, peers) in self.mesh.iter().chain(self.fanout.iter()) { let message_ids = self.mcache.get_gossip_ids(&topic_hash); if message_ids.is_empty() { @@ -920,6 +949,9 @@ impl Gossipsub { self.config.gossip_lazy, |peer| !peers.contains(peer), ); + + debug!("Gossiping IHAVE to {} peers.", to_msg_peers.len()); + for peer in to_msg_peers { // send an IHAVE message Self::control_pool_add( @@ -932,7 +964,6 @@ impl Gossipsub { ); } } - debug!("Completed gossip"); } /// Handles multiple GRAFT/PRUNE messages and coalesces them into chunked gossip control @@ -942,23 +973,25 @@ impl Gossipsub { to_graft: HashMap>, mut to_prune: HashMap>, ) { - // handle the grafts and overlapping prunes + // handle the grafts and overlapping prunes per peer for (peer, topics) in to_graft.iter() { - let mut grafts: Vec = topics + let mut control_msgs: Vec = topics .iter() .map(|topic_hash| GossipsubControlAction::Graft { topic_hash: topic_hash.clone(), }) .collect(); - let mut prunes: Vec = to_prune - .remove(peer) - .unwrap_or_else(|| vec![]) - .iter() - .map(|topic_hash| GossipsubControlAction::Prune { - topic_hash: topic_hash.clone(), - }) - .collect(); - grafts.append(&mut prunes); + + // If there are prunes associated with the same peer add them. + if let Some(topics) = to_prune.remove(peer) { + let mut prunes = topics + .iter() + .map(|topic_hash| GossipsubControlAction::Prune { + topic_hash: topic_hash.clone(), + }) + .collect::>(); + control_msgs.append(&mut prunes); + } // send the control messages self.send_message( @@ -966,7 +999,7 @@ impl Gossipsub { GossipsubRpc { subscriptions: Vec::new(), messages: Vec::new(), - control_msgs: grafts, + control_msgs, }, ); } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index e95b7cbc31f..372266a8bdf 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -424,7 +424,9 @@ mod tests { .events .iter() .filter(|e| match e { - NetworkBehaviourAction::NotifyHandler { .. } => true, + NetworkBehaviourAction::NotifyHandler { event, .. } => { + !event.subscriptions.is_empty() + } _ => false, }) .collect(); diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index 937a5bbd77f..1db71e494c5 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -51,6 +51,10 @@ pub struct GossipsubHandler { /// Queue of values that we want to send to the remote. send_queue: SmallVec<[GossipsubRpc; 16]>, + /// Flag indicating that an outbound substream is being established to prevent duplicate + /// requests. + outbound_substream_establishing: bool, + /// Flag determining whether to maintain the connection to the peer. keep_alive: KeepAlive, } @@ -94,6 +98,7 @@ impl GossipsubHandler { )), inbound_substream: None, outbound_substream: None, + outbound_substream_establishing: false, send_queue: SmallVec::new(), keep_alive: KeepAlive::Yes, } @@ -126,6 +131,7 @@ impl ProtocolsHandler for GossipsubHandler { substream: >::Output, message: Self::OutboundOpenInfo, ) { + self.outbound_substream_establishing = false; // Should never establish a new outbound substream if one already exists. // If this happens, an outbound message is not sent. if self.outbound_substream.is_some() { @@ -148,6 +154,7 @@ impl ProtocolsHandler for GossipsubHandler { >::Error, >, ) { + self.outbound_substream_establishing = false; // Ignore upgrade errors for now. // If a peer doesn't support this protocol, this will just ignore them, but not disconnect // them. @@ -169,9 +176,13 @@ impl ProtocolsHandler for GossipsubHandler { >, > { // determine if we need to create the stream - if !self.send_queue.is_empty() && self.outbound_substream.is_none() { + if !self.send_queue.is_empty() + && self.outbound_substream.is_none() + && !self.outbound_substream_establishing + { let message = self.send_queue.remove(0); self.send_queue.shrink_to_fit(); + self.outbound_substream_establishing = true; return Poll::Ready(ProtocolsHandlerEvent::OutboundSubstreamRequest { protocol: self.listen_protocol.clone(), info: message, diff --git a/protocols/gossipsub/tests/smoke.rs b/protocols/gossipsub/tests/smoke.rs index 85258f81575..34b4cd1e95a 100644 --- a/protocols/gossipsub/tests/smoke.rs +++ b/protocols/gossipsub/tests/smoke.rs @@ -130,6 +130,25 @@ impl Graph { futures::executor::block_on(fut).unwrap() } + + /// Polls the graph until Poll::Pending is obtained, completing the underlying polls. + fn drain_poll(self) -> Self { + // The future below should return self. Given that it is a FnMut and not a FnOnce, one needs + // to wrap `self` in an Option, leaving a `None` behind after the final `Poll::Ready`. + let mut this = Some(self); + + let fut = futures::future::poll_fn(move |cx| match &mut this { + Some(graph) => loop { + match graph.poll_unpin(cx) { + Poll::Ready(_) => {} + Poll::Pending => return Poll::Ready(this.take().unwrap()), + } + }, + None => panic!("future called after final return"), + }); + let fut = async_std::future::timeout(Duration::from_secs(10), fut); + futures::executor::block_on(fut).unwrap() + } } fn build_node() -> (Multiaddr, Swarm) { @@ -154,8 +173,10 @@ fn build_node() -> (Multiaddr, Swarm) { // timely fashion. let config = GossipsubConfigBuilder::new() - .heartbeat_initial_delay(Duration::from_millis(50)) - .heartbeat_interval(Duration::from_millis(100)) + .heartbeat_initial_delay(Duration::from_millis(100)) + .heartbeat_interval(Duration::from_millis(200)) + .history_length(10) + .history_gossip(10) .validation_mode(ValidationMode::Permissive) .build(); let behaviour = Gossipsub::new(MessageAuthenticity::Author(peer_id.clone()), config); @@ -176,14 +197,14 @@ fn build_node() -> (Multiaddr, Swarm) { fn multi_hop_propagation() { let _ = env_logger::try_init(); - fn prop(num_nodes: usize, seed: u64) -> TestResult { + fn prop(num_nodes: u8, seed: u64) -> TestResult { if num_nodes < 2 || num_nodes > 100 { return TestResult::discard(); } debug!("number nodes: {:?}, seed: {:?}", num_nodes, seed); - let mut graph = Graph::new_connected(num_nodes, seed); + let mut graph = Graph::new_connected(num_nodes as usize, seed); let number_nodes = graph.nodes.len(); // Subscribe each node to the same topic. @@ -205,6 +226,10 @@ fn multi_hop_propagation() { false }); + // It can happen that the publish occurs before all grafts have completed causing this test + // to fail. We drain all the poll messages before publishing. + graph = graph.drain_poll(); + // Publish a single message. graph.nodes[0].1.publish(&topic, vec![1, 2, 3]).unwrap(); @@ -225,6 +250,6 @@ fn multi_hop_propagation() { } QuickCheck::new() - .max_tests(1) - .quickcheck(prop as fn(usize, u64) -> TestResult) + .max_tests(10) + .quickcheck(prop as fn(u8, u64) -> TestResult) } From 2f4a50ad051a5cb36e28abfcefde45faa35d3d0f Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 28 Jul 2020 22:28:48 +1000 Subject: [PATCH 30/35] Only add messages to memcache if not duplicates --- protocols/gossipsub/src/behaviour.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index d70439af25f..6094b637e52 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -313,8 +313,8 @@ impl Gossipsub { data.into(), )?; let msg_id = (self.config.message_id_fn)(&message); - // Add published message to our memcache and add it to the duplicate cache. - self.mcache.put(message.clone()); + + // Add published message to the duplicate cache. if self.duplication_cache.insert(msg_id.clone(), ()).is_some() { // This message has already been seen. We don't re-publish messages that have already // been published on the network. @@ -325,6 +325,9 @@ impl Gossipsub { return Err(PublishError::Duplicate); } + // If the message isn't a duplicate add it to the memcache. + self.mcache.put(message.clone()); + debug!("Publishing message: {:?}", msg_id); // Forward the message to mesh peers. @@ -653,11 +656,11 @@ impl Gossipsub { } // Add the message to the duplication cache and memcache. - self.mcache.put(msg.clone()); if self.duplication_cache.insert(msg_id.clone(), ()).is_some() { debug!("Message already received, ignoring. Message: {:?}", msg_id); return; } + self.mcache.put(msg.clone()); // dispatch the message to the user if self.mesh.keys().any(|t| msg.topics.iter().any(|u| t == u)) { From be7c9e228e52118f450768a0915dc158d6b7f715 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 29 Jul 2020 12:18:09 +1000 Subject: [PATCH 31/35] Add mesh maintenance tests and remove excess peers from mesh --- protocols/gossipsub/src/behaviour.rs | 1 + protocols/gossipsub/src/behaviour/tests.rs | 49 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 6094b637e52..23f558f3198 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -856,6 +856,7 @@ impl Gossipsub { let peer = shuffled .pop() .expect("There should always be enough peers to remove"); + peers.remove(&peer); let current_topic = to_prune.entry(peer).or_insert_with(Vec::new); current_topic.push(topic_hash.clone()); } diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs index 372266a8bdf..73a6a0b2904 100644 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ b/protocols/gossipsub/src/behaviour/tests.rs @@ -846,4 +846,53 @@ mod tests { "Expected peer to be removed from mesh" ); } + + #[test] + // Tests the mesh maintenance addition + fn test_mesh_addition() { + let config = GossipsubConfig::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let (mut gs, peers, topics) = + build_and_inject_nodes(config.mesh_n + 1, vec!["test".into()], true); + + let to_remove_peers = config.mesh_n + 1 - config.mesh_n_low - 1; + + for index in 0..to_remove_peers { + gs.handle_prune(&peers[index], topics.clone()); + } + + // Verify the pruned peers are removed from the mesh. + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + config.mesh_n_low - 1 + ); + + // run a heartbeat + gs.heartbeat(); + + // Peers should be added to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n); + } + + #[test] + // Tests the mesh maintenance subtraction + fn test_mesh_subtraction() { + let config = GossipsubConfig::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let (mut gs, peers, topics) = + build_and_inject_nodes(config.mesh_n_high + 10, vec!["test".into()], true); + + // graft all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // run a heartbeat + gs.heartbeat(); + + // Peers should be removed to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n); + } } From e049ff73a96f68ca8eaddf9ede6a103fffdc863b Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 29 Jul 2020 12:22:56 +1000 Subject: [PATCH 32/35] Apply reviewers suggestions --- protocols/gossipsub/src/behaviour.rs | 36 +++++++++++++--------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 23f558f3198..3373c553c3b 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -70,8 +70,8 @@ pub enum MessageAuthenticity { Author(PeerId), /// Message signing is disabled. /// - /// A random `PeerId` will be used when publishing each message. The sequence number will be a - /// random number. + /// A random `PeerId` will be used when publishing each message. The sequence number will be + /// randomized. RandomAuthor, /// Message signing is disabled. /// @@ -99,24 +99,20 @@ impl MessageAuthenticity { } } -/// A data structure for storing information for publishing messages. -enum PublishInfo { - /// Message signing is enabled and this contains relevant information for publishing - /// signed messages. +/// A data structure for storing configuration for publishing messages. See [`MessageAuthenticity`] +/// for further details. +enum PublishConfig { Signing { keypair: Keypair, author: PeerId, inline_key: Option>, }, - // Signing is disabled, but this author is used to publish messages. Author(PeerId), - /// The author is radomized each message. RandomAuthor, - /// The from and sequence number fields are excluded from the message Anonymous, } -impl From for PublishInfo { +impl From for PublishConfig { fn from(authenticity: MessageAuthenticity) -> Self { match authenticity { MessageAuthenticity::Signed(keypair) => { @@ -131,15 +127,15 @@ impl From for PublishInfo { Some(key_enc) }; - PublishInfo::Signing { + PublishConfig::Signing { keypair, author: public_key.into_peer_id(), inline_key: key, } } - MessageAuthenticity::Author(peer_id) => PublishInfo::Author(peer_id), - MessageAuthenticity::RandomAuthor => PublishInfo::RandomAuthor, - MessageAuthenticity::Anonymous => PublishInfo::Anonymous, + MessageAuthenticity::Author(peer_id) => PublishConfig::Author(peer_id), + MessageAuthenticity::RandomAuthor => PublishConfig::RandomAuthor, + MessageAuthenticity::Anonymous => PublishConfig::Anonymous, } } } @@ -160,7 +156,7 @@ pub struct Gossipsub { control_pool: HashMap>, /// Information used for publishing messages. - publish_info: PublishInfo, + publish_info: PublishConfig, /// An LRU Time cache for storing seen messages (based on their ID). This cache prevents /// duplicates from being propagated to the application and on the network. @@ -189,7 +185,7 @@ pub struct Gossipsub { } impl Gossipsub { - /// Creates a `Gossipsub` struct given a set of parameters specified by via a `GossipsubConfig`. + /// Creates a `Gossipsub` struct given a set of parameters specified via a `GossipsubConfig`. pub fn new(privacy: MessageAuthenticity, config: GossipsubConfig) -> Self { // Set up the router given the configuration settings. @@ -1072,7 +1068,7 @@ impl Gossipsub { data: Vec, ) -> Result { match &self.publish_info { - PublishInfo::Signing { + PublishConfig::Signing { ref keypair, author, inline_key, @@ -1113,7 +1109,7 @@ impl Gossipsub { validated: true, // all published messages are valid }) } - PublishInfo::Author(peer_id) => { + PublishConfig::Author(peer_id) => { Ok(GossipsubMessage { source: Some(peer_id.clone()), data, @@ -1126,7 +1122,7 @@ impl Gossipsub { validated: true, // all published messages are valid }) } - PublishInfo::RandomAuthor => { + PublishConfig::RandomAuthor => { Ok(GossipsubMessage { source: Some(PeerId::random()), data, @@ -1139,7 +1135,7 @@ impl Gossipsub { validated: true, // all published messages are valid }) } - PublishInfo::Anonymous => { + PublishConfig::Anonymous => { Ok(GossipsubMessage { source: None, data, From 6bac865088466a7e52f24a54015f3e99cc43172a Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 29 Jul 2020 12:38:25 +1000 Subject: [PATCH 33/35] Wrap comments --- protocols/gossipsub/src/behaviour.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 3373c553c3b..85941b46e37 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -77,8 +77,11 @@ pub enum MessageAuthenticity { /// /// The author of the message and the sequence numbers are excluded from the message. /// - /// NOTE: Excluding these fields may make these messages invalid by other nodes who enforce validation of these - /// fields. See [`ValidationMode`] in the `GossipsubConfig` for how to customise this for rust-libp2p gossipsub. A custom message_id function will need to be set to prevent all messages from a peer being filtered as duplicates. + /// NOTE: Excluding these fields may make these messages invalid by other nodes who + /// enforce validation of these fields. See [`ValidationMode`] in the `GossipsubConfig` + /// for how to customise this for rust-libp2p gossipsub. A custom `message_id` + /// function will need to be set to prevent all messages from a peer being filtered + /// as duplicates. Anonymous, } From 94469cffcfe9a9aedb630d6c2fafbaa254a1f9d4 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Sun, 2 Aug 2020 15:35:42 +1000 Subject: [PATCH 34/35] Ensure sequence number is sent --- protocols/gossipsub/src/behaviour.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 85941b46e37..58155592af8 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -1118,7 +1118,7 @@ impl Gossipsub { data, // To be interoperable with the go-implementation this is treated as a 64-bit // big-endian uint. - sequence_number: rand::random(), + sequence_number: Some(rand::random()), topics, signature: None, key: None, @@ -1131,7 +1131,7 @@ impl Gossipsub { data, // To be interoperable with the go-implementation this is treated as a 64-bit // big-endian uint. - sequence_number: rand::random(), + sequence_number: Some(rand::random()), topics, signature: None, key: None, From fef66ddeb72700f7f57d0ffc3e8329c610e5d647 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Sun, 2 Aug 2020 16:07:50 +1000 Subject: [PATCH 35/35] Maintain the debug trait --- protocols/gossipsub/src/behaviour.rs | 39 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index a5604f409ea..7636c2c06b1 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -148,7 +148,6 @@ impl From for PublishConfig { /// NOTE: Initialisation requires a [`MessageAuthenticity`] and [`GossipsubConfig`] instance. If message signing is /// disabled, the [`ValidationMode`] in the config should be adjusted to an appropriate level to /// accept unsigned messages. -#[derive(Debug)] pub struct Gossipsub { /// Configuration providing gossipsub performance parameters. config: GossipsubConfig, @@ -160,7 +159,7 @@ pub struct Gossipsub { control_pool: HashMap>, /// Information used for publishing messages. - publish_info: PublishConfig, + publish_config: PublishConfig, /// An LRU Time cache for storing seen messages (based on their ID). This cache prevents /// duplicates from being propagated to the application and on the network. @@ -202,7 +201,7 @@ impl Gossipsub { Gossipsub { events: VecDeque::new(), control_pool: HashMap::new(), - publish_info: privacy.into(), + publish_config: privacy.into(), duplication_cache: LruCache::with_expiry_duration(config.duplicate_cache_time), topic_peers: HashMap::new(), peer_topics: HashMap::new(), @@ -1071,7 +1070,7 @@ impl Gossipsub { topics: Vec, data: Vec, ) -> Result { - match &self.publish_info { + match &self.publish_config { PublishConfig::Signing { ref keypair, author, @@ -1485,3 +1484,35 @@ fn validate_config(authenticity: &MessageAuthenticity, validation_mode: &Validat _ => {} } } + + + +impl fmt::Debug for Gossipsub { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Gossipsub") + .field("config", &self.config) + .field("events", &self.events) + .field("control_pool", &self.control_pool) + .field("publish_config", &self.publish_config) + .field("topic_peers", &self.topic_peers) + .field("peer_topics", &self.peer_topics) + .field("mesh", &self.mesh) + .field("fanout", &self.fanout) + .field("fanout_last_pub", &self.fanout_last_pub) + .field("mcache", &self.mcache) + .field("heartbeat", &self.heartbeat) + .finish() + } +} + +impl fmt::Debug for PublishConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PublishConfig::Signing { author, .. } => f.write_fmt(format_args!("PublishConfig::Signing({})", author)), + PublishConfig::Author(author) => f.write_fmt(format_args!("PublishConfig::Author({})", author)), + PublishConfig::RandomAuthor => f.write_fmt(format_args!("PublishConfig::RandomAuthor")), + PublishConfig::Anonymous => f.write_fmt(format_args!("PublishConfig::Anonymous")), + } + } +} +