Repository: radicle-dev/heartwood (rust)

Issue:
Node announcement handling
To build an address book, peers communicate via gossip, sending `NodeAnnouncement` messages.

These need to be handled by storing the addresses in an address book (`address_book` module), and the addresses need to be persisted to disk, via an `sqlite` backend (same as routing table).

Node announcement messages should also be relayed to peers like other gossip messages (related: #17).

* [x] Storage
* [x] Relay
* [x] Message validation
* [ ] Test refs announcement relay logic

Pull request:
Refs announcement tests
Closes #24 

Final work to close #24.

Patch:
diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs
index 1c3b4ff8..a2f96b46 100644
--- a/radicle-node/src/service.rs
+++ b/radicle-node/src/service.rs
@@ -643,8 +643,9 @@ where
                         return Ok(false);
                     }
                     // TODO: Check refs to see if we should try to fetch or not.
-                    // FIXME: This code is wrong: we shouldn't be fetching from the connected peer,
-                    // we should fetch from the origin.
+                    // Refs are only supposed to be relayed by peers who are tracking
+                    // the resource. Therefore, it's safe to fetch from the remote
+                    // peer, even though it isn't the announcer.
                     let updated = self.storage.fetch(message.id, git).unwrap();
                     let is_updated = !updated.is_empty();
 
diff --git a/radicle-node/src/service/message.rs b/radicle-node/src/service/message.rs
index 836c29ad..1545a33b 100644
--- a/radicle-node/src/service/message.rs
+++ b/radicle-node/src/service/message.rs
@@ -122,6 +122,16 @@ pub struct Subscribe {
     pub until: Timestamp,
 }
 
+impl Subscribe {
+    pub fn all() -> Self {
+        Self {
+            filter: Filter::default(),
+            since: Timestamp::MIN,
+            until: Timestamp::MAX,
+        }
+    }
+}
+
 /// Node announcing itself to the network.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct NodeAnnouncement {


Test patch:
diff --git a/radicle-node/src/test/peer.rs b/radicle-node/src/test/peer.rs
index 38ff71a5..47dd863c 100644
--- a/radicle-node/src/test/peer.rs
+++ b/radicle-node/src/test/peer.rs
@@ -7,8 +7,10 @@ use log::*;
 
 use crate::address;
 use crate::clock::{RefClock, Timestamp};
+use crate::crypto::Signer;
 use crate::git;
 use crate::git::Url;
+use crate::identity::Id;
 use crate::node;
 use crate::prelude::NodeId;
 use crate::service;
@@ -23,12 +25,12 @@ use crate::test::simulator;
 use crate::{Link, LocalTime};
 
 /// Service instantiation used for testing.
-pub type Service<S> = service::Service<routing::Table, address::Book, S, MockSigner>;
+pub type Service<S, G> = service::Service<routing::Table, address::Book, S, G>;
 
 #[derive(Debug)]
-pub struct Peer<S> {
+pub struct Peer<S, G> {
     pub name: &'static str,
-    pub service: Service<S>,
+    pub service: Service<S, G>,
     pub ip: net::IpAddr,
     pub rng: fastrand::Rng,
     pub local_time: LocalTime,
@@ -37,9 +39,10 @@ pub struct Peer<S> {
     initialized: bool,
 }
 
-impl<S> simulator::Peer<S> for Peer<S>
+impl<S, G> simulator::Peer<S, G> for Peer<S, G>
 where
     S: WriteStorage + 'static,
+    G: Signer + 'static,
 {
     fn init(&mut self) {
         self.initialize()
@@ -50,21 +53,21 @@ where
     }
 }
 
-impl<S> Deref for Peer<S> {
-    type Target = Service<S>;
+impl<S, G> Deref for Peer<S, G> {
+    type Target = Service<S, G>;
 
     fn deref(&self) -> &Self::Target {
         &self.service
     }
 }
 
-impl<S> DerefMut for Peer<S> {
+impl<S, G> DerefMut for Peer<S, G> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.service
     }
 }
 
-impl<S> Peer<S>
+impl<S> Peer<S, MockSigner>
 where
     S: WriteStorage + 'static,
 {
@@ -75,6 +78,8 @@ where
 
             ..git::Url::default()
         };
+        let mut rng = fastrand::Rng::new();
+        let signer = MockSigner::new(&mut rng);
 
         Self::config(
             name,
@@ -84,20 +89,27 @@ where
             },
             ip,
             storage,
-            fastrand::Rng::new(),
+            signer,
+            rng,
         )
     }
+}
 
+impl<S, G> Peer<S, G>
+where
+    S: WriteStorage + 'static,
+    G: Signer + 'static,
+{
     pub fn config(
         name: &'static str,
         config: Config,
         ip: impl Into<net::IpAddr>,
         storage: S,
-        mut rng: fastrand::Rng,
+        signer: G,
+        rng: fastrand::Rng,
     ) -> Self {
         let local_time = LocalTime::now();
         let clock = RefClock::from(local_time);
-        let signer = MockSigner::new(&mut rng);
         let routing = routing::Table::memory().unwrap();
         let addrs = address::Book::memory().unwrap();
         let service = Service::new(config, clock, routing, storage, addrs, signer, rng.clone());
@@ -170,9 +182,7 @@ where
         )
     }
 
-    pub fn refs_announcement(&self) -> Message {
-        let inv = self.inventory().unwrap();
-        let id = inv[self.rng.usize(..inv.len())];
+    pub fn refs_announcement(&self, id: Id) -> Message {
         let refs = BTreeMap::new().into();
         let ann = AnnouncementMessage::from(RefsAnnouncement {
             id,
@@ -185,7 +195,7 @@ where
     }
 
     pub fn connect_from(&mut self, peer: &Self) {
-        let remote = simulator::Peer::<S>::addr(peer);
+        let remote = simulator::Peer::<S, G>::addr(peer);
         let local = net::SocketAddr::new(self.ip, self.rng.u16(..));
         let git = format!("file:///{}.git", remote.ip());
         let git = Url::from_bytes(git.as_bytes()).unwrap();
@@ -213,7 +223,7 @@ where
     }
 
     pub fn connect_to(&mut self, peer: &Self) {
-        let remote = simulator::Peer::<S>::addr(peer);
+        let remote = simulator::Peer::<S, G>::addr(peer);
 
         self.initialize();
         self.service.attempted(&remote);
diff --git a/radicle-node/src/test/simulator.rs b/radicle-node/src/test/simulator.rs
index 3627397e..15897a00 100644
--- a/radicle-node/src/test/simulator.rs
+++ b/radicle-node/src/test/simulator.rs
@@ -14,6 +14,7 @@ use log::*;
 use nakamoto_net as nakamoto;
 use nakamoto_net::{Link, LocalDuration, LocalTime};
 
+use crate::crypto::Signer;
 use crate::service::reactor::Io;
 use crate::service::{DisconnectReason, Envelope, Event};
 use crate::storage::WriteStorage;
@@ -29,7 +30,9 @@ pub const MAX_EVENTS: usize = 2048;
 type NodeId = std::net::IpAddr;
 
 /// A simulated peer. Service instances have to be wrapped in this type to be simulated.
-pub trait Peer<S>: Deref<Target = Service<S>> + DerefMut<Target = Service<S>> + 'static {
+pub trait Peer<S, G>:
+    Deref<Target = Service<S, G>> + DerefMut<Target = Service<S, G>> + 'static
+{
     /// Initialize the peer. This should at minimum initialize the service with the
     /// current time.
     fn init(&mut self);
@@ -166,7 +169,7 @@ impl Default for Options {
 }
 
 /// A peer-to-peer node simulation.
-pub struct Simulation<S> {
+pub struct Simulation<S, G> {
     /// Inbox of inputs to be delivered by the simulation.
     inbox: Inbox,
     /// Events emitted during simulation.
@@ -191,9 +194,11 @@ pub struct Simulation<S> {
     rng: fastrand::Rng,
     /// Storage type.
     storage: PhantomData<S>,
+    /// Signer type.
+    signer: PhantomData<G>,
 }
 
-impl<S: WriteStorage + 'static> Simulation<S> {
+impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
     /// Create a new simulation.
     pub fn new(time: LocalTime, rng: fastrand::Rng, opts: Options) -> Self {
         Self {
@@ -211,6 +216,7 @@ impl<S: WriteStorage + 'static> Simulation<S> {
             time,
             rng,
             storage: PhantomData,
+            signer: PhantomData,
         }
     }
 
@@ -267,7 +273,7 @@ impl<S: WriteStorage + 'static> Simulation<S> {
     /// Initialize peers.
     pub fn initialize<'a, P>(self, peers: impl IntoIterator<Item = &'a mut P>) -> Self
     where
-        P: Peer<S>,
+        P: Peer<S, G>,
     {
         for peer in peers.into_iter() {
             peer.init();
@@ -281,7 +287,7 @@ impl<S: WriteStorage + 'static> Simulation<S> {
         peers: impl IntoIterator<Item = &'a mut P>,
         pred: impl Fn(&Self) -> bool,
     ) where
-        P: Peer<S>,
+        P: Peer<S, G>,
     {
         let mut nodes: BTreeMap<_, _> = peers.into_iter().map(|p| (p.addr().ip(), p)).collect();
 
@@ -295,12 +301,12 @@ impl<S: WriteStorage + 'static> Simulation<S> {
     /// Process one scheduled input from the inbox, using the provided peers.
     /// This function should be called until it returns `false`, or some desired state is reached.
     /// Returns `true` if there are more messages to process.
-    pub fn step<'a, P: Peer<S>>(&mut self, peers: impl IntoIterator<Item = &'a mut P>) -> bool {
+    pub fn step<'a, P: Peer<S, G>>(&mut self, peers: impl IntoIterator<Item = &'a mut P>) -> bool {
         let mut nodes: BTreeMap<_, _> = peers.into_iter().map(|p| (p.addr().ip(), p)).collect();
         self.step_(&mut nodes)
     }
 
-    fn step_<P: Peer<S>>(&mut self, nodes: &mut BTreeMap<NodeId, &mut P>) -> bool {
+    fn step_<P: Peer<S, G>>(&mut self, nodes: &mut BTreeMap<NodeId, &mut P>) -> bool {
         if !self.opts.latency.is_empty() {
             // Configure latencies.
             for (i, from) in nodes.keys().enumerate() {
diff --git a/radicle-node/src/test/tests.rs b/radicle-node/src/test/tests.rs
index 89dac792..e114fcbc 100644
--- a/radicle-node/src/test/tests.rs
+++ b/radicle-node/src/test/tests.rs
@@ -15,6 +15,7 @@ use crate::service::ServiceState as _;
 use crate::service::*;
 use crate::storage::git::Storage;
 use crate::storage::ReadStorage;
+use crate::test::arbitrary;
 use crate::test::assert_matches;
 use crate::test::fixtures;
 #[allow(unused)]
@@ -77,14 +78,21 @@ fn test_inbound_connection() {
 
 #[test]
 fn test_persistent_peer_connect() {
-    let rng = fastrand::Rng::new();
+    let mut rng = fastrand::Rng::new();
     let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
     let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
     let config = Config {
         connect: vec![bob.address(), eve.address()],
         ..Config::default()
     };
-    let mut alice = Peer::config("alice", config, [7, 7, 7, 7], MockStorage::empty(), rng);
+    let mut alice = Peer::config(
+        "alice",
+        config,
+        [7, 7, 7, 7],
+        MockStorage::empty(),
+        MockSigner::new(&mut rng),
+        rng,
+    );
 
     alice.initialize();
 
@@ -158,6 +166,7 @@ fn test_tracking() {
         },
         [7, 7, 7, 7],
         MockStorage::empty(),
+        MockSigner::default(),
         fastrand::Rng::new(),
     );
     let proj_id: identity::Id = test::arbitrary::gen(1);
@@ -207,7 +216,7 @@ fn test_inventory_relay_bad_timestamp() {
 }
 
 #[test]
-fn test_gossip_rebroadcast() {
+fn test_announcement_rebroadcast() {
     let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
     let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
     let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
@@ -234,7 +243,7 @@ fn test_gossip_rebroadcast() {
 }
 
 #[test]
-fn test_gossip_rebroadcast_timestamp_filtered() {
+fn test_announcement_rebroadcast_timestamp_filtered() {
     let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
     let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
     let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
@@ -334,6 +343,94 @@ fn test_announcement_relay() {
     );
 }
 
+#[test]
+fn test_refs_announcement_relay() {
+    let tmp = tempfile::tempdir().unwrap();
+    let mut alice = Peer::new(
+        "alice",
+        [7, 7, 7, 7],
+        Storage::open(tmp.path().join("alice")).unwrap(),
+    );
+    let eve = Peer::new(
+        "eve",
+        [8, 8, 8, 8],
+        Storage::open(tmp.path().join("eve")).unwrap(),
+    );
+
+    let bob = {
+        let mut rng = fastrand::Rng::new();
+        let signer = MockSigner::new(&mut rng);
+        let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
+
+        Peer::config(
+            "bob",
+            Config {
+                git_url: git::Url {
+                    scheme: git::url::Scheme::File,
+                    path: storage.path().to_string_lossy().to_string().into(),
+
+                    ..git::Url::default()
+                },
+                ..Config::default()
+            },
+            [9, 9, 9, 9],
+            storage,
+            signer,
+            rng,
+        )
+    };
+    let bob_inv = bob.inventory().unwrap();
+
+    alice.track(bob_inv[0]);
+    alice.track(bob_inv[1]);
+    alice.track(bob_inv[2]);
+    alice.connect_to(&bob);
+    alice.connect_to(&eve);
+    alice.receive(&eve.addr(), Message::Subscribe(Subscribe::all()));
+
+    alice.receive(&bob.addr(), bob.refs_announcement(bob_inv[0]));
+    assert_matches!(
+        alice.messages(&eve.addr()).next(),
+        Some(Message::Announcement(_)),
+        "A refs announcement from Bob is relayed to Eve"
+    );
+
+    alice.receive(&bob.addr(), bob.refs_announcement(bob_inv[0]));
+    assert!(
+        alice.messages(&eve.addr()).next().is_none(),
+        "The same ref announement is not relayed"
+    );
+
+    alice.receive(&bob.addr(), bob.refs_announcement(bob_inv[1]));
+    assert_matches!(
+        alice.messages(&eve.addr()).next(),
+        Some(Message::Announcement(_)),
+        "But a different one is"
+    );
+
+    alice.receive(&bob.addr(), bob.refs_announcement(bob_inv[2]));
+    assert_matches!(
+        alice.messages(&eve.addr()).next(),
+        Some(Message::Announcement(_)),
+        "And a third one is as well"
+    );
+}
+
+#[test]
+fn test_refs_announcement_no_subscribe() {
+    let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
+    let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
+    let eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
+    let id = arbitrary::gen(1);
+
+    alice.track(id);
+    alice.connect_to(&bob);
+    alice.connect_to(&eve);
+    alice.receive(&bob.addr(), bob.refs_announcement(id));
+
+    assert!(alice.messages(&eve.addr()).next().is_none());
+}
+
 #[test]
 fn test_inventory_relay() {
     // Topology is eve <-> alice <-> bob
@@ -442,6 +539,7 @@ fn test_persistent_peer_reconnect() {
         },
         [7, 7, 7, 7],
         MockStorage::empty(),
+        MockSigner::default(),
         fastrand::Rng::new(),
     );