Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: Make addresses.db accessible from profile
xphoniex committed 2 years ago
commit f526465f1b3b18cb0ce920cf25b7dd12b7c5819d
parent 738d0dcbadfaad76a83ec38830c10370b6ee83d1
16 files changed +797 -731
deleted radicle-node/src/address.rs
@@ -1,5 +0,0 @@
-
mod store;
-
mod types;
-

-
pub use store::*;
-
pub use types::*;
deleted radicle-node/src/address/schema.sql
@@ -1,37 +0,0 @@
-
--
-
-- Address book SQL schema.
-
--
-
create table if not exists "nodes" (
-
  -- Node ID.
-
  "id"                 text      primary key not null,
-
  -- Node features.
-
  "features"           integer   not null,
-
  -- Node alias.
-
  "alias"              text      default null,
-
  -- Node announcement timestamp.
-
  "timestamp"          integer   not null
-
  --
-
) strict;
-

-
create table if not exists "addresses" (
-
  -- Node ID.
-
  "node"               text      not null references "nodes" ("id"),
-
  -- Address type.
-
  "type"               text      not null,
-
  -- Address value.
-
  "value"              text      not null,
-
  -- Where we got this address from.
-
  "source"             text      not null,
-
  -- When this address was announced.
-
  "timestamp"          integer   not null,
-
  -- Local time at which we last attempted to connect to this node.
-
  "last_attempt"       integer   default null,
-
  -- Local time at which we successfully connected to this node.
-
  "last_success"       integer   default null,
-
  -- Nb. This constraint allows more than one node to share the same address.
-
  -- This is useful in circumstances when a node wants to rotate its key, but
-
  -- remain reachable at the same address. The old entry will eventually be
-
  -- pruned.
-
  unique ("node", "type", "value")
-
  --
-
) strict;
deleted radicle-node/src/address/store.rs
@@ -1,544 +0,0 @@
-
use std::path::Path;
-
use std::{fmt, io};
-

-
use radicle::node;
-
use radicle::node::Address;
-
use radicle::prelude::Timestamp;
-
use radicle::sql::transaction;
-
use sqlite as sql;
-
use thiserror::Error;
-

-
use crate::address::types;
-
use crate::address::{KnownAddress, Source};
-
use crate::service::NodeId;
-
use crate::wire::AddressType;
-
use crate::LocalTime;
-

-
#[derive(Error, Debug)]
-
pub enum Error {
-
    /// I/O error.
-
    #[error("i/o error: {0}")]
-
    Io(#[from] io::Error),
-
    /// An Internal error.
-
    #[error("internal error: {0}")]
-
    Internal(#[from] sql::Error),
-
}
-

-
/// A file-backed address book.
-
pub struct Book {
-
    db: sql::Connection,
-
}
-

-
impl fmt::Debug for Book {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        write!(f, "Book(..)")
-
    }
-
}
-

-
impl Book {
-
    const SCHEMA: &str = include_str!("schema.sql");
-

-
    /// Open an address book at the given path. Creates a new address book if it
-
    /// doesn't exist.
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-
        let db = sql::Connection::open(path)?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self { db })
-
    }
-

-
    /// Create a new in-memory address book.
-
    pub fn memory() -> Result<Self, Error> {
-
        let db = sql::Connection::open(":memory:")?;
-
        db.execute(Self::SCHEMA)?;
-

-
        Ok(Self { db })
-
    }
-
}
-

-
impl Store for Book {
-
    fn get(&self, node: &NodeId) -> Result<Option<types::Node>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT features, alias, timestamp FROM nodes WHERE id = ?")?;
-

-
        stmt.bind((1, node))?;
-

-
        if let Some(Ok(row)) = stmt.into_iter().next() {
-
            let features = row.read::<node::Features, _>("features");
-
            let alias = row.read::<&str, _>("alias").to_owned();
-
            let timestamp = row.read::<i64, _>("timestamp") as Timestamp;
-
            let mut addrs = Vec::new();
-

-
            let mut stmt = self
-
                .db
-
                .prepare("SELECT type, value, source FROM addresses WHERE node = ?")?;
-
            stmt.bind((1, node))?;
-

-
            for row in stmt.into_iter() {
-
                let row = row?;
-
                let _typ = row.read::<AddressType, _>("type");
-
                let addr = row.read::<Address, _>("value");
-
                let source = row.read::<Source, _>("source");
-

-
                addrs.push(KnownAddress {
-
                    addr,
-
                    source,
-
                    last_success: None,
-
                    last_attempt: None,
-
                });
-
            }
-

-
            Ok(Some(types::Node {
-
                features,
-
                alias,
-
                timestamp,
-
                addrs,
-
            }))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    fn len(&self) -> Result<usize, Error> {
-
        let row = self
-
            .db
-
            .prepare("SELECT COUNT(*) FROM addresses")?
-
            .into_iter()
-
            .next()
-
            .unwrap()
-
            .unwrap();
-
        let count = row.read::<i64, _>(0) as usize;
-

-
        Ok(count)
-
    }
-

-
    fn insert(
-
        &mut self,
-
        node: &NodeId,
-
        features: node::Features,
-
        alias: &str,
-
        timestamp: Timestamp,
-
        addrs: impl IntoIterator<Item = KnownAddress>,
-
    ) -> Result<bool, Error> {
-
        transaction(&self.db, move |db| {
-
            let mut stmt = db.prepare(
-
                "INSERT INTO nodes (id, features, alias, timestamp)
-
                 VALUES (?1, ?2, ?3, ?4)
-
                 ON CONFLICT DO UPDATE
-
                 SET features = ?2, alias = ?3, timestamp = ?4
-
                 WHERE timestamp < ?4",
-
            )?;
-

-
            stmt.bind((1, node))?;
-
            stmt.bind((2, features))?;
-
            stmt.bind((3, alias))?;
-
            stmt.bind((4, timestamp as i64))?;
-
            stmt.next()?;
-

-
            for addr in addrs {
-
                let mut stmt = db.prepare(
-
                    "INSERT INTO addresses (node, type, value, source, timestamp)
-
                     VALUES (?1, ?2, ?3, ?4, ?5)
-
                     ON CONFLICT DO UPDATE
-
                     SET timestamp = ?5
-
                     WHERE timestamp < ?5",
-
                )?;
-
                stmt.bind((1, node))?;
-
                stmt.bind((2, AddressType::from(&addr.addr)))?;
-
                stmt.bind((3, &addr.addr))?;
-
                stmt.bind((4, addr.source))?;
-
                stmt.bind((5, timestamp as i64))?;
-
                stmt.next()?;
-
            }
-
            Ok(db.change_count() > 0)
-
        })
-
        .map_err(Error::from)
-
    }
-

-
    fn remove(&mut self, node: &NodeId) -> Result<bool, Error> {
-
        transaction(&self.db, move |db| {
-
            db.prepare("DELETE FROM nodes WHERE id = ?")?
-
                .into_iter()
-
                .bind(&[node][..])?
-
                .next();
-

-
            db.prepare("DELETE FROM addresses WHERE node = ?")?
-
                .into_iter()
-
                .bind(&[node][..])?
-
                .next();
-

-
            Ok(db.change_count() > 0)
-
        })
-
        .map_err(Error::from)
-
    }
-

-
    fn entries(&self) -> Result<Box<dyn Iterator<Item = (NodeId, KnownAddress)>>, Error> {
-
        let mut stmt = self
-
            .db
-
            .prepare("SELECT node, type, value, source, last_success, last_attempt FROM addresses ORDER BY node")?
-
            .into_iter();
-
        let mut entries = Vec::new();
-

-
        while let Some(Ok(row)) = stmt.next() {
-
            let node = row.read::<NodeId, _>("node");
-
            let _typ = row.read::<AddressType, _>("type");
-
            let addr = row.read::<Address, _>("value");
-
            let source = row.read::<Source, _>("source");
-
            let last_success = row.read::<Option<i64>, _>("last_success");
-
            let last_attempt = row.read::<Option<i64>, _>("last_attempt");
-
            let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
-
            let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
-

-
            entries.push((
-
                node,
-
                KnownAddress {
-
                    addr,
-
                    source,
-
                    last_success,
-
                    last_attempt,
-
                },
-
            ));
-
        }
-
        Ok(Box::new(entries.into_iter()))
-
    }
-

-
    fn attempted(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error> {
-
        let mut stmt = self.db.prepare(
-
            "UPDATE `addresses`
-
             SET last_attempt = ?1
-
             WHERE node = ?2
-
             AND type = ?3
-
             AND value = ?4",
-
        )?;
-

-
        stmt.bind((1, time as i64))?;
-
        stmt.bind((2, nid))?;
-
        stmt.bind((3, AddressType::from(addr)))?;
-
        stmt.bind((4, addr))?;
-
        stmt.next()?;
-

-
        Ok(())
-
    }
-

-
    fn connected(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error> {
-
        let mut stmt = self.db.prepare(
-
            "UPDATE `addresses`
-
             SET last_success = ?1
-
             WHERE node = ?2
-
             AND type = ?3
-
             AND value = ?4",
-
        )?;
-

-
        stmt.bind((1, time as i64))?;
-
        stmt.bind((2, nid))?;
-
        stmt.bind((3, AddressType::from(addr)))?;
-
        stmt.bind((4, addr))?;
-
        stmt.next()?;
-

-
        Ok(())
-
    }
-
}
-

-
/// Address store.
-
///
-
/// Used to store node addresses and metadata.
-
pub trait Store {
-
    /// Get a known peer address.
-
    fn get(&self, id: &NodeId) -> Result<Option<types::Node>, Error>;
-
    /// Insert a node with associated addresses into the store.
-
    ///
-
    /// Returns `true` if the node or addresses were updated, and `false` otherwise.
-
    fn insert(
-
        &mut self,
-
        node: &NodeId,
-
        features: node::Features,
-
        alias: &str,
-
        timestamp: Timestamp,
-
        addrs: impl IntoIterator<Item = KnownAddress>,
-
    ) -> Result<bool, Error>;
-
    /// Remove an address from the store.
-
    fn remove(&mut self, id: &NodeId) -> Result<bool, Error>;
-
    /// Returns the number of addresses.
-
    fn len(&self) -> Result<usize, Error>;
-
    /// Returns true if there are no addresses.
-
    fn is_empty(&self) -> Result<bool, Error> {
-
        self.len().map(|l| l == 0)
-
    }
-
    /// Get the address entries in the store.
-
    fn entries(&self) -> Result<Box<dyn Iterator<Item = (NodeId, KnownAddress)>>, Error>;
-
    /// Mark a node as attempted at a certain time.
-
    fn attempted(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error>;
-
    /// Mark a node as successfully connected at a certain time.
-
    fn connected(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error>;
-
}
-

-
impl TryFrom<&sql::Value> for Source {
-
    type Error = sql::Error;
-

-
    fn try_from(value: &sql::Value) -> Result<Self, Self::Error> {
-
        let err = sql::Error {
-
            code: None,
-
            message: Some("sql: invalid source".to_owned()),
-
        };
-
        match value {
-
            sql::Value::String(s) => match s.as_str() {
-
                "dns" => Ok(Source::Dns),
-
                "peer" => Ok(Source::Peer),
-
                "imported" => Ok(Source::Imported),
-
                _ => Err(err),
-
            },
-
            _ => Err(err),
-
        }
-
    }
-
}
-

-
impl sql::BindableWithIndex for Source {
-
    fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
-
        match self {
-
            Self::Dns => "dns".bind(stmt, i),
-
            Self::Peer => "peer".bind(stmt, i),
-
            Self::Imported => "imported".bind(stmt, i),
-
        }
-
    }
-
}
-

-
impl TryFrom<&sql::Value> for AddressType {
-
    type Error = sql::Error;
-

-
    fn try_from(value: &sql::Value) -> Result<Self, Self::Error> {
-
        let err = sql::Error {
-
            code: None,
-
            message: Some("sql: invalid address type".to_owned()),
-
        };
-
        match value {
-
            sql::Value::String(s) => match s.as_str() {
-
                "ipv4" => Ok(AddressType::Ipv4),
-
                "ipv6" => Ok(AddressType::Ipv6),
-
                "hostname" => Ok(AddressType::Hostname),
-
                "onion" => Ok(AddressType::Onion),
-
                _ => Err(err),
-
            },
-
            _ => Err(err),
-
        }
-
    }
-
}
-

-
impl sql::BindableWithIndex for AddressType {
-
    fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
-
        match self {
-
            Self::Ipv4 => "ipv4".bind(stmt, i),
-
            Self::Ipv6 => "ipv6".bind(stmt, i),
-
            Self::Hostname => "hostname".bind(stmt, i),
-
            Self::Onion => "onion".bind(stmt, i),
-
        }
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use std::net;
-

-
    use super::*;
-
    use crate::test::arbitrary;
-
    use crate::LocalTime;
-

-
    #[test]
-
    fn test_empty() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let path = tmp.path().join("cache");
-
        let cache = Book::open(path).unwrap();
-

-
        assert!(cache.is_empty().unwrap());
-
    }
-

-
    #[test]
-
    fn test_get_none() {
-
        let alice = arbitrary::gen::<NodeId>(1);
-
        let cache = Book::memory().unwrap();
-
        let result = cache.get(&alice).unwrap();
-

-
        assert!(result.is_none());
-
    }
-

-
    #[test]
-
    fn test_remove_nothing() {
-
        let alice = arbitrary::gen::<NodeId>(1);
-
        let mut cache = Book::memory().unwrap();
-
        let removed = cache.remove(&alice).unwrap();
-

-
        assert!(!removed);
-
    }
-

-
    #[test]
-
    fn test_insert_and_get() {
-
        let alice = arbitrary::gen::<NodeId>(1);
-
        let mut cache = Book::memory().unwrap();
-
        let features = node::Features::SEED;
-
        let timestamp = LocalTime::now().as_millis();
-

-
        let ka = KnownAddress {
-
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
-
            source: Source::Peer,
-
            last_success: None,
-
            last_attempt: None,
-
        };
-
        let inserted = cache
-
            .insert(&alice, features, "alice", timestamp, [ka.clone()])
-
            .unwrap();
-
        assert!(inserted);
-

-
        let node = cache.get(&alice).unwrap().unwrap();
-

-
        assert_eq!(node.features, features);
-
        assert_eq!(node.timestamp, timestamp);
-
        assert_eq!(node.alias.as_str(), "alice");
-
        assert_eq!(node.addrs, vec![ka]);
-
    }
-

-
    #[test]
-
    fn test_insert_duplicate() {
-
        let alice = arbitrary::gen::<NodeId>(1);
-
        let mut cache = Book::memory().unwrap();
-
        let features = node::Features::SEED;
-
        let timestamp = LocalTime::now().as_millis();
-

-
        let ka = KnownAddress {
-
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
-
            source: Source::Peer,
-
            last_success: None,
-
            last_attempt: None,
-
        };
-
        let inserted = cache
-
            .insert(&alice, features, "alice", timestamp, [ka.clone()])
-
            .unwrap();
-
        assert!(inserted);
-

-
        let inserted = cache
-
            .insert(&alice, features, "alice", timestamp, [ka])
-
            .unwrap();
-
        assert!(!inserted);
-

-
        assert_eq!(cache.len().unwrap(), 1);
-
    }
-

-
    #[test]
-
    fn test_insert_and_update() {
-
        let alice = arbitrary::gen::<NodeId>(1);
-
        let mut cache = Book::memory().unwrap();
-
        let timestamp = LocalTime::now().as_millis();
-
        let features = node::Features::SEED;
-
        let ka = KnownAddress {
-
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
-
            source: Source::Peer,
-
            last_success: None,
-
            last_attempt: None,
-
        };
-

-
        let updated = cache
-
            .insert(&alice, features, "alice", timestamp, [ka.clone()])
-
            .unwrap();
-
        assert!(updated);
-

-
        let updated = cache
-
            .insert(&alice, features, "~alice~", timestamp, [])
-
            .unwrap();
-
        assert!(!updated, "Can't update using the same timestamp");
-

-
        let updated = cache
-
            .insert(&alice, features, "~alice~", timestamp - 1, [])
-
            .unwrap();
-
        assert!(!updated, "Can't update using  a smaller timestamp");
-

-
        let node = cache.get(&alice).unwrap().unwrap();
-
        assert_eq!(node.alias, "alice");
-
        assert_eq!(node.timestamp, timestamp);
-

-
        let updated = cache
-
            .insert(&alice, features, "~alice~", timestamp + 1, [])
-
            .unwrap();
-
        assert!(updated, "Can update with a larger timestamp");
-

-
        let updated = cache
-
            .insert(&alice, node::Features::NONE, "~alice~", timestamp + 2, [])
-
            .unwrap();
-
        assert!(updated);
-

-
        let node = cache.get(&alice).unwrap().unwrap();
-
        assert_eq!(node.features, node::Features::NONE);
-
        assert_eq!(node.alias, "~alice~");
-
        assert_eq!(node.timestamp, timestamp + 2);
-
        assert_eq!(node.addrs, vec![ka]);
-
    }
-

-
    #[test]
-
    fn test_insert_and_remove() {
-
        let alice = arbitrary::gen::<NodeId>(1);
-
        let bob = arbitrary::gen::<NodeId>(1);
-
        let mut cache = Book::memory().unwrap();
-
        let timestamp = LocalTime::now().as_millis();
-
        let features = node::Features::SEED;
-

-
        for addr in [
-
            ([4, 4, 4, 4], 8776),
-
            ([7, 7, 7, 7], 8776),
-
            ([9, 9, 9, 9], 8776),
-
        ] {
-
            let ka = KnownAddress {
-
                addr: net::SocketAddr::from(addr).into(),
-
                source: Source::Peer,
-
                last_success: None,
-
                last_attempt: None,
-
            };
-
            cache
-
                .insert(&alice, features, "alice", timestamp, [ka.clone()])
-
                .unwrap();
-
            cache
-
                .insert(&bob, features, "bob", timestamp, [ka])
-
                .unwrap();
-
        }
-
        assert_eq!(cache.len().unwrap(), 6);
-

-
        let removed = cache.remove(&alice).unwrap();
-
        assert!(removed);
-
        assert_eq!(cache.len().unwrap(), 3);
-

-
        let removed = cache.remove(&bob).unwrap();
-
        assert!(removed);
-
        assert_eq!(cache.len().unwrap(), 0);
-
    }
-

-
    #[test]
-
    fn test_entries() {
-
        let ids = arbitrary::vec::<NodeId>(16);
-
        let rng = fastrand::Rng::new();
-
        let mut cache = Book::memory().unwrap();
-
        let mut expected = Vec::new();
-
        let timestamp = LocalTime::now().as_millis();
-
        let features = node::Features::SEED;
-

-
        for id in ids {
-
            let ip = rng.u32(..);
-
            let addr = net::SocketAddr::from((net::Ipv4Addr::from(ip), rng.u16(..)));
-
            let ka = KnownAddress {
-
                addr: addr.into(),
-
                source: Source::Dns,
-
                // TODO: Test times as well.
-
                last_success: None,
-
                last_attempt: None,
-
            };
-
            expected.push((id, ka.clone()));
-
            cache
-
                .insert(&id, features, "alias", timestamp, [ka])
-
                .unwrap();
-
        }
-

-
        let mut actual = cache.entries().unwrap().collect::<Vec<_>>();
-

-
        actual.sort_by_key(|(i, _)| *i);
-
        expected.sort_by_key(|(i, _)| *i);
-

-
        assert_eq!(cache.len().unwrap(), actual.len());
-
        assert_eq!(actual, expected);
-
    }
-
}
deleted radicle-node/src/address/types.rs
@@ -1,135 +0,0 @@
-
use std::ops::{Deref, DerefMut};
-

-
use nonempty::NonEmpty;
-
use radicle::node;
-
use radicle::node::Address;
-
use radicle::prelude::Timestamp;
-

-
use crate::collections::HashMap;
-
use crate::LocalTime;
-

-
/// A map with the ability to randomly select values.
-
#[derive(Debug, Clone)]
-
pub struct AddressBook<K, V> {
-
    inner: HashMap<K, V>,
-
    rng: fastrand::Rng,
-
}
-

-
impl<K, V> AddressBook<K, V> {
-
    /// Create a new address book.
-
    pub fn new(rng: fastrand::Rng) -> Self {
-
        Self {
-
            inner: HashMap::with_hasher(rng.clone().into()),
-
            rng,
-
        }
-
    }
-

-
    /// Pick a random value in the book.
-
    pub fn sample(&self) -> Option<(&K, &V)> {
-
        self.sample_with(|_, _| true)
-
    }
-

-
    /// Pick a random value in the book matching a predicate.
-
    pub fn sample_with(&self, mut predicate: impl FnMut(&K, &V) -> bool) -> Option<(&K, &V)> {
-
        if let Some(pairs) = NonEmpty::from_vec(
-
            self.inner
-
                .iter()
-
                .filter(|(k, v)| predicate(*k, *v))
-
                .collect(),
-
        ) {
-
            let ix = self.rng.usize(..pairs.len());
-
            let pair = pairs[ix]; // Can't fail.
-

-
            Some(pair)
-
        } else {
-
            None
-
        }
-
    }
-

-
    /// Cycle through the keys at random. The random cycle repeats ad-infintum.
-
    pub fn cycle(&self) -> impl Iterator<Item = &K> {
-
        self.shuffled().map(|(k, _)| k).cycle()
-
    }
-

-
    /// Return a shuffled iterator over the keys.
-
    pub fn shuffled(&self) -> std::vec::IntoIter<(&K, &V)> {
-
        let mut keys = self.inner.iter().collect::<Vec<_>>();
-
        self.rng.shuffle(&mut keys);
-

-
        keys.into_iter()
-
    }
-
}
-

-
impl<K, V> Deref for AddressBook<K, V> {
-
    type Target = HashMap<K, V>;
-

-
    fn deref(&self) -> &Self::Target {
-
        &self.inner
-
    }
-
}
-

-
impl<K, V> DerefMut for AddressBook<K, V> {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.inner
-
    }
-
}
-

-
/// Node public data.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Node {
-
    /// Advertized alias.
-
    pub alias: String,
-
    /// Advertized features.
-
    pub features: node::Features,
-
    /// Advertized addresses
-
    pub addrs: Vec<KnownAddress>,
-
    /// When this data was published.
-
    pub timestamp: Timestamp,
-
}
-

-
/// A known address.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct KnownAddress {
-
    /// Network address.
-
    pub addr: Address,
-
    /// Address of the peer who sent us this address.
-
    pub source: Source,
-
    /// Last time this address was used to successfully connect to a peer.
-
    pub last_success: Option<LocalTime>,
-
    /// Last time this address was tried.
-
    pub last_attempt: Option<LocalTime>,
-
}
-

-
impl KnownAddress {
-
    /// Create a new known address.
-
    pub fn new(addr: Address, source: Source) -> Self {
-
        Self {
-
            addr,
-
            source,
-
            last_success: None,
-
            last_attempt: None,
-
        }
-
    }
-
}
-

-
/// Address source. Specifies where an address originated from.
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub enum Source {
-
    /// An address that was shared by another peer.
-
    Peer,
-
    /// An address that came from a DNS seed.
-
    Dns,
-
    /// An address that came from some source external to the system, eg.
-
    /// specified by the user or added directly to the address manager.
-
    Imported,
-
}
-

-
impl std::fmt::Display for Source {
-
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-
        match self {
-
            Self::Peer => write!(f, "Peer"),
-
            Self::Dns => write!(f, "DNS"),
-
            Self::Imported => write!(f, "Imported"),
-
        }
-
    }
-
}
modified radicle-node/src/lib.rs
@@ -1,4 +1,3 @@
-
pub mod address;
pub mod bounded;
pub mod control;
pub mod deserializer;
modified radicle-node/src/runtime.rs
@@ -14,12 +14,12 @@ use reactor::Reactor;
use thiserror::Error;

use radicle::git;
+
use radicle::node::address;
use radicle::node::Handle as _;
use radicle::node::{ADDRESS_DB_FILE, ROUTING_DB_FILE, TRACKING_DB_FILE};
use radicle::profile::Home;
use radicle::Storage;

-
use crate::address;
use crate::control;
use crate::crypto::Signer;
use crate::node::{routing, NodeId};
modified radicle-node/src/service.rs
@@ -19,8 +19,9 @@ use fastrand::Rng;
use localtime::{LocalDuration, LocalTime};
use log::*;

-
use crate::address;
-
use crate::address::{AddressBook, KnownAddress};
+
use radicle::node::address;
+
use radicle::node::address::{AddressBook, KnownAddress};
+

use crate::crypto;
use crate::crypto::{Signer, Verified};
use crate::identity::IdentityError;
modified radicle-node/src/test/peer.rs
@@ -4,12 +4,13 @@ use std::net;
use std::ops::{Deref, DerefMut};

use log::*;
+

+
use radicle::node::address;
+
use radicle::node::address::Store;
use radicle::rad;
use radicle::storage::ReadRepository;
use radicle::Storage;

-
use crate::address;
-
use crate::address::Store;
use crate::crypto::test::signer::MockSigner;
use crate::crypto::Signer;
use crate::identity::Id;
modified radicle-node/src/wire/protocol.rs
@@ -22,11 +22,12 @@ use netservices::{NetConnection, NetProtocol, NetReader, NetWriter};
use reactor::Timestamp;

use radicle::collections::HashMap;
-
use radicle::node::{routing, NodeId};
+
use radicle::node::{address, routing, NodeId};
use radicle::storage::WriteStorage;

use crate::crypto::Signer;
use crate::prelude::Deserializer;
+
use crate::service;
use crate::service::io::Io;
use crate::service::{session, DisconnectReason, Service};
use crate::wire::frame;
@@ -35,7 +36,6 @@ use crate::wire::Encode;
use crate::worker;
use crate::worker::{ChannelEvent, FetchRequest, FetchResult, Task, TaskResult};
use crate::Link;
-
use crate::{address, service};

/// NoiseXK handshake pattern.
pub const NOISE_XK: HandshakePattern = HandshakePattern {
modified radicle/src/lib.rs
@@ -1,7 +1,6 @@
#![allow(clippy::match_like_matches_macro)]
#![allow(clippy::explicit_auto_deref)] // TODO: This can be removed when the clippy bugs are fixed
#![allow(clippy::iter_nth_zero)]
-
#![cfg_attr(not(test), warn(clippy::unwrap_used))]

pub extern crate radicle_crypto as crypto;

modified radicle/src/node.rs
@@ -1,5 +1,6 @@
mod features;

+
pub mod address;
pub mod events;
pub mod routing;
pub mod tracking;
added radicle/src/node/address.rs
@@ -0,0 +1,51 @@
+
mod store;
+
mod types;
+

+
pub use store::*;
+
pub use types::*;
+

+
use crate::node::Address;
+
use cyphernet::addr::HostName;
+
use std::net;
+

+
/// Address type.
+
#[repr(u8)]
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+
pub enum AddressType {
+
    Ipv4 = 1,
+
    Ipv6 = 2,
+
    Hostname = 3,
+
    Onion = 4,
+
}
+

+
impl From<AddressType> for u8 {
+
    fn from(other: AddressType) -> Self {
+
        other as u8
+
    }
+
}
+

+
impl From<&Address> for AddressType {
+
    fn from(a: &Address) -> Self {
+
        match a.host {
+
            HostName::Ip(net::IpAddr::V4(_)) => AddressType::Ipv4,
+
            HostName::Ip(net::IpAddr::V6(_)) => AddressType::Ipv6,
+
            HostName::Dns(_) => AddressType::Hostname,
+
            HostName::Tor(_) => AddressType::Onion,
+
            _ => todo!(), // FIXME(cloudhead): Maxim will remove `non-exhaustive`
+
        }
+
    }
+
}
+

+
impl TryFrom<u8> for AddressType {
+
    type Error = u8;
+

+
    fn try_from(other: u8) -> Result<Self, Self::Error> {
+
        match other {
+
            1 => Ok(AddressType::Ipv4),
+
            2 => Ok(AddressType::Ipv6),
+
            3 => Ok(AddressType::Hostname),
+
            4 => Ok(AddressType::Onion),
+
            _ => Err(other),
+
        }
+
    }
+
}
added radicle/src/node/address/schema.sql
@@ -0,0 +1,37 @@
+
--
+
-- Address book SQL schema.
+
--
+
create table if not exists "nodes" (
+
  -- Node ID.
+
  "id"                 text      primary key not null,
+
  -- Node features.
+
  "features"           integer   not null,
+
  -- Node alias.
+
  "alias"              text      default null,
+
  -- Node announcement timestamp.
+
  "timestamp"          integer   not null
+
  --
+
) strict;
+

+
create table if not exists "addresses" (
+
  -- Node ID.
+
  "node"               text      not null references "nodes" ("id"),
+
  -- Address type.
+
  "type"               text      not null,
+
  -- Address value.
+
  "value"              text      not null,
+
  -- Where we got this address from.
+
  "source"             text      not null,
+
  -- When this address was announced.
+
  "timestamp"          integer   not null,
+
  -- Local time at which we last attempted to connect to this node.
+
  "last_attempt"       integer   default null,
+
  -- Local time at which we successfully connected to this node.
+
  "last_success"       integer   default null,
+
  -- Nb. This constraint allows more than one node to share the same address.
+
  -- This is useful in circumstances when a node wants to rotate its key, but
+
  -- remain reachable at the same address. The old entry will eventually be
+
  -- pruned.
+
  unique ("node", "type", "value")
+
  --
+
) strict;
added radicle/src/node/address/store.rs
@@ -0,0 +1,554 @@
+
use std::path::Path;
+
use std::{fmt, io};
+

+
use localtime::LocalTime;
+
use sqlite as sql;
+
use thiserror::Error;
+

+
use crate::node;
+
use crate::node::address::{KnownAddress, Source};
+
use crate::node::Address;
+
use crate::node::NodeId;
+
use crate::prelude::Timestamp;
+
use crate::sql::transaction;
+

+
use super::types;
+
use super::AddressType;
+

+
#[derive(Error, Debug)]
+
pub enum Error {
+
    /// I/O error.
+
    #[error("i/o error: {0}")]
+
    Io(#[from] io::Error),
+
    /// An Internal error.
+
    #[error("internal error: {0}")]
+
    Internal(#[from] sql::Error),
+
}
+

+
/// A file-backed address book.
+
pub struct Book {
+
    db: sql::Connection,
+
}
+

+
impl fmt::Debug for Book {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        write!(f, "Book(..)")
+
    }
+
}
+

+
impl Book {
+
    const SCHEMA: &str = include_str!("schema.sql");
+

+
    /// Open an address book at the given path. Creates a new address book if it
+
    /// doesn't exist.
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let db = sql::Connection::open(path)?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self { db })
+
    }
+

+
    /// Same as [`Self::open`], but in read-only mode. This is useful to have multiple
+
    /// open databases, as no locking is required.
+
    pub fn reader<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let db = sql::Connection::open_with_flags(path, sqlite::OpenFlags::new().set_read_only())?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self { db })
+
    }
+

+
    /// Create a new in-memory address book.
+
    pub fn memory() -> Result<Self, Error> {
+
        let db = sql::Connection::open(":memory:")?;
+
        db.execute(Self::SCHEMA)?;
+

+
        Ok(Self { db })
+
    }
+
}
+

+
impl Store for Book {
+
    fn get(&self, node: &NodeId) -> Result<Option<types::Node>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT features, alias, timestamp FROM nodes WHERE id = ?")?;
+

+
        stmt.bind((1, node))?;
+

+
        if let Some(Ok(row)) = stmt.into_iter().next() {
+
            let features = row.read::<node::Features, _>("features");
+
            let alias = row.read::<&str, _>("alias").to_owned();
+
            let timestamp = row.read::<i64, _>("timestamp") as Timestamp;
+
            let mut addrs = Vec::new();
+

+
            let mut stmt = self
+
                .db
+
                .prepare("SELECT type, value, source FROM addresses WHERE node = ?")?;
+
            stmt.bind((1, node))?;
+

+
            for row in stmt.into_iter() {
+
                let row = row?;
+
                let _typ = row.read::<AddressType, _>("type");
+
                let addr = row.read::<Address, _>("value");
+
                let source = row.read::<Source, _>("source");
+

+
                addrs.push(KnownAddress {
+
                    addr,
+
                    source,
+
                    last_success: None,
+
                    last_attempt: None,
+
                });
+
            }
+

+
            Ok(Some(types::Node {
+
                features,
+
                alias,
+
                timestamp,
+
                addrs,
+
            }))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    fn len(&self) -> Result<usize, Error> {
+
        let row = self
+
            .db
+
            .prepare("SELECT COUNT(*) FROM addresses")?
+
            .into_iter()
+
            .next()
+
            .unwrap()
+
            .unwrap();
+
        let count = row.read::<i64, _>(0) as usize;
+

+
        Ok(count)
+
    }
+

+
    fn insert(
+
        &mut self,
+
        node: &NodeId,
+
        features: node::Features,
+
        alias: &str,
+
        timestamp: Timestamp,
+
        addrs: impl IntoIterator<Item = KnownAddress>,
+
    ) -> Result<bool, Error> {
+
        transaction(&self.db, move |db| {
+
            let mut stmt = db.prepare(
+
                "INSERT INTO nodes (id, features, alias, timestamp)
+
                 VALUES (?1, ?2, ?3, ?4)
+
                 ON CONFLICT DO UPDATE
+
                 SET features = ?2, alias = ?3, timestamp = ?4
+
                 WHERE timestamp < ?4",
+
            )?;
+

+
            stmt.bind((1, node))?;
+
            stmt.bind((2, features))?;
+
            stmt.bind((3, alias))?;
+
            stmt.bind((4, timestamp as i64))?;
+
            stmt.next()?;
+

+
            for addr in addrs {
+
                let mut stmt = db.prepare(
+
                    "INSERT INTO addresses (node, type, value, source, timestamp)
+
                     VALUES (?1, ?2, ?3, ?4, ?5)
+
                     ON CONFLICT DO UPDATE
+
                     SET timestamp = ?5
+
                     WHERE timestamp < ?5",
+
                )?;
+
                stmt.bind((1, node))?;
+
                stmt.bind((2, AddressType::from(&addr.addr)))?;
+
                stmt.bind((3, &addr.addr))?;
+
                stmt.bind((4, addr.source))?;
+
                stmt.bind((5, timestamp as i64))?;
+
                stmt.next()?;
+
            }
+
            Ok(db.change_count() > 0)
+
        })
+
        .map_err(Error::from)
+
    }
+

+
    fn remove(&mut self, node: &NodeId) -> Result<bool, Error> {
+
        transaction(&self.db, move |db| {
+
            db.prepare("DELETE FROM nodes WHERE id = ?")?
+
                .into_iter()
+
                .bind(&[node][..])?
+
                .next();
+

+
            db.prepare("DELETE FROM addresses WHERE node = ?")?
+
                .into_iter()
+
                .bind(&[node][..])?
+
                .next();
+

+
            Ok(db.change_count() > 0)
+
        })
+
        .map_err(Error::from)
+
    }
+

+
    fn entries(&self) -> Result<Box<dyn Iterator<Item = (NodeId, KnownAddress)>>, Error> {
+
        let mut stmt = self
+
            .db
+
            .prepare("SELECT node, type, value, source, last_success, last_attempt FROM addresses ORDER BY node")?
+
            .into_iter();
+
        let mut entries = Vec::new();
+

+
        while let Some(Ok(row)) = stmt.next() {
+
            let node = row.read::<NodeId, _>("node");
+
            let _typ = row.read::<AddressType, _>("type");
+
            let addr = row.read::<Address, _>("value");
+
            let source = row.read::<Source, _>("source");
+
            let last_success = row.read::<Option<i64>, _>("last_success");
+
            let last_attempt = row.read::<Option<i64>, _>("last_attempt");
+
            let last_success = last_success.map(|t| LocalTime::from_millis(t as u128));
+
            let last_attempt = last_attempt.map(|t| LocalTime::from_millis(t as u128));
+

+
            entries.push((
+
                node,
+
                KnownAddress {
+
                    addr,
+
                    source,
+
                    last_success,
+
                    last_attempt,
+
                },
+
            ));
+
        }
+
        Ok(Box::new(entries.into_iter()))
+
    }
+

+
    fn attempted(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error> {
+
        let mut stmt = self.db.prepare(
+
            "UPDATE `addresses`
+
             SET last_attempt = ?1
+
             WHERE node = ?2
+
             AND type = ?3
+
             AND value = ?4",
+
        )?;
+

+
        stmt.bind((1, time as i64))?;
+
        stmt.bind((2, nid))?;
+
        stmt.bind((3, AddressType::from(addr)))?;
+
        stmt.bind((4, addr))?;
+
        stmt.next()?;
+

+
        Ok(())
+
    }
+

+
    fn connected(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error> {
+
        let mut stmt = self.db.prepare(
+
            "UPDATE `addresses`
+
             SET last_success = ?1
+
             WHERE node = ?2
+
             AND type = ?3
+
             AND value = ?4",
+
        )?;
+

+
        stmt.bind((1, time as i64))?;
+
        stmt.bind((2, nid))?;
+
        stmt.bind((3, AddressType::from(addr)))?;
+
        stmt.bind((4, addr))?;
+
        stmt.next()?;
+

+
        Ok(())
+
    }
+
}
+

+
/// Address store.
+
///
+
/// Used to store node addresses and metadata.
+
pub trait Store {
+
    /// Get a known peer address.
+
    fn get(&self, id: &NodeId) -> Result<Option<types::Node>, Error>;
+
    /// Insert a node with associated addresses into the store.
+
    ///
+
    /// Returns `true` if the node or addresses were updated, and `false` otherwise.
+
    fn insert(
+
        &mut self,
+
        node: &NodeId,
+
        features: node::Features,
+
        alias: &str,
+
        timestamp: Timestamp,
+
        addrs: impl IntoIterator<Item = KnownAddress>,
+
    ) -> Result<bool, Error>;
+
    /// Remove an address from the store.
+
    fn remove(&mut self, id: &NodeId) -> Result<bool, Error>;
+
    /// Returns the number of addresses.
+
    fn len(&self) -> Result<usize, Error>;
+
    /// Returns true if there are no addresses.
+
    fn is_empty(&self) -> Result<bool, Error> {
+
        self.len().map(|l| l == 0)
+
    }
+
    /// Get the address entries in the store.
+
    fn entries(&self) -> Result<Box<dyn Iterator<Item = (NodeId, KnownAddress)>>, Error>;
+
    /// Mark a node as attempted at a certain time.
+
    fn attempted(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error>;
+
    /// Mark a node as successfully connected at a certain time.
+
    fn connected(&self, nid: &NodeId, addr: &Address, time: Timestamp) -> Result<(), Error>;
+
}
+

+
impl TryFrom<&sql::Value> for Source {
+
    type Error = sql::Error;
+

+
    fn try_from(value: &sql::Value) -> Result<Self, Self::Error> {
+
        let err = sql::Error {
+
            code: None,
+
            message: Some("sql: invalid source".to_owned()),
+
        };
+
        match value {
+
            sql::Value::String(s) => match s.as_str() {
+
                "dns" => Ok(Source::Dns),
+
                "peer" => Ok(Source::Peer),
+
                "imported" => Ok(Source::Imported),
+
                _ => Err(err),
+
            },
+
            _ => Err(err),
+
        }
+
    }
+
}
+

+
impl sql::BindableWithIndex for Source {
+
    fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
+
        match self {
+
            Self::Dns => "dns".bind(stmt, i),
+
            Self::Peer => "peer".bind(stmt, i),
+
            Self::Imported => "imported".bind(stmt, i),
+
        }
+
    }
+
}
+

+
impl TryFrom<&sql::Value> for AddressType {
+
    type Error = sql::Error;
+

+
    fn try_from(value: &sql::Value) -> Result<Self, Self::Error> {
+
        let err = sql::Error {
+
            code: None,
+
            message: Some("sql: invalid address type".to_owned()),
+
        };
+
        match value {
+
            sql::Value::String(s) => match s.as_str() {
+
                "ipv4" => Ok(AddressType::Ipv4),
+
                "ipv6" => Ok(AddressType::Ipv6),
+
                "hostname" => Ok(AddressType::Hostname),
+
                "onion" => Ok(AddressType::Onion),
+
                _ => Err(err),
+
            },
+
            _ => Err(err),
+
        }
+
    }
+
}
+

+
impl sql::BindableWithIndex for AddressType {
+
    fn bind<I: sql::ParameterIndex>(self, stmt: &mut sql::Statement<'_>, i: I) -> sql::Result<()> {
+
        match self {
+
            Self::Ipv4 => "ipv4".bind(stmt, i),
+
            Self::Ipv6 => "ipv6".bind(stmt, i),
+
            Self::Hostname => "hostname".bind(stmt, i),
+
            Self::Onion => "onion".bind(stmt, i),
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use std::net;
+

+
    use super::*;
+
    use crate::test::arbitrary;
+
    use localtime::LocalTime;
+

+
    #[test]
+
    fn test_empty() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let path = tmp.path().join("cache");
+
        let cache = Book::open(path).unwrap();
+

+
        assert!(cache.is_empty().unwrap());
+
    }
+

+
    #[test]
+
    fn test_get_none() {
+
        let alice = arbitrary::gen::<NodeId>(1);
+
        let cache = Book::memory().unwrap();
+
        let result = cache.get(&alice).unwrap();
+

+
        assert!(result.is_none());
+
    }
+

+
    #[test]
+
    fn test_remove_nothing() {
+
        let alice = arbitrary::gen::<NodeId>(1);
+
        let mut cache = Book::memory().unwrap();
+
        let removed = cache.remove(&alice).unwrap();
+

+
        assert!(!removed);
+
    }
+

+
    #[test]
+
    fn test_insert_and_get() {
+
        let alice = arbitrary::gen::<NodeId>(1);
+
        let mut cache = Book::memory().unwrap();
+
        let features = node::Features::SEED;
+
        let timestamp = LocalTime::now().as_millis();
+

+
        let ka = KnownAddress {
+
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
+
            source: Source::Peer,
+
            last_success: None,
+
            last_attempt: None,
+
        };
+
        let inserted = cache
+
            .insert(&alice, features, "alice", timestamp, [ka.clone()])
+
            .unwrap();
+
        assert!(inserted);
+

+
        let node = cache.get(&alice).unwrap().unwrap();
+

+
        assert_eq!(node.features, features);
+
        assert_eq!(node.timestamp, timestamp);
+
        assert_eq!(node.alias.as_str(), "alice");
+
        assert_eq!(node.addrs, vec![ka]);
+
    }
+

+
    #[test]
+
    fn test_insert_duplicate() {
+
        let alice = arbitrary::gen::<NodeId>(1);
+
        let mut cache = Book::memory().unwrap();
+
        let features = node::Features::SEED;
+
        let timestamp = LocalTime::now().as_millis();
+

+
        let ka = KnownAddress {
+
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
+
            source: Source::Peer,
+
            last_success: None,
+
            last_attempt: None,
+
        };
+
        let inserted = cache
+
            .insert(&alice, features, "alice", timestamp, [ka.clone()])
+
            .unwrap();
+
        assert!(inserted);
+

+
        let inserted = cache
+
            .insert(&alice, features, "alice", timestamp, [ka])
+
            .unwrap();
+
        assert!(!inserted);
+

+
        assert_eq!(cache.len().unwrap(), 1);
+
    }
+

+
    #[test]
+
    fn test_insert_and_update() {
+
        let alice = arbitrary::gen::<NodeId>(1);
+
        let mut cache = Book::memory().unwrap();
+
        let timestamp = LocalTime::now().as_millis();
+
        let features = node::Features::SEED;
+
        let ka = KnownAddress {
+
            addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
+
            source: Source::Peer,
+
            last_success: None,
+
            last_attempt: None,
+
        };
+

+
        let updated = cache
+
            .insert(&alice, features, "alice", timestamp, [ka.clone()])
+
            .unwrap();
+
        assert!(updated);
+

+
        let updated = cache
+
            .insert(&alice, features, "~alice~", timestamp, [])
+
            .unwrap();
+
        assert!(!updated, "Can't update using the same timestamp");
+

+
        let updated = cache
+
            .insert(&alice, features, "~alice~", timestamp - 1, [])
+
            .unwrap();
+
        assert!(!updated, "Can't update using  a smaller timestamp");
+

+
        let node = cache.get(&alice).unwrap().unwrap();
+
        assert_eq!(node.alias, "alice");
+
        assert_eq!(node.timestamp, timestamp);
+

+
        let updated = cache
+
            .insert(&alice, features, "~alice~", timestamp + 1, [])
+
            .unwrap();
+
        assert!(updated, "Can update with a larger timestamp");
+

+
        let updated = cache
+
            .insert(&alice, node::Features::NONE, "~alice~", timestamp + 2, [])
+
            .unwrap();
+
        assert!(updated);
+

+
        let node = cache.get(&alice).unwrap().unwrap();
+
        assert_eq!(node.features, node::Features::NONE);
+
        assert_eq!(node.alias, "~alice~");
+
        assert_eq!(node.timestamp, timestamp + 2);
+
        assert_eq!(node.addrs, vec![ka]);
+
    }
+

+
    #[test]
+
    fn test_insert_and_remove() {
+
        let alice = arbitrary::gen::<NodeId>(1);
+
        let bob = arbitrary::gen::<NodeId>(1);
+
        let mut cache = Book::memory().unwrap();
+
        let timestamp = LocalTime::now().as_millis();
+
        let features = node::Features::SEED;
+

+
        for addr in [
+
            ([4, 4, 4, 4], 8776),
+
            ([7, 7, 7, 7], 8776),
+
            ([9, 9, 9, 9], 8776),
+
        ] {
+
            let ka = KnownAddress {
+
                addr: net::SocketAddr::from(addr).into(),
+
                source: Source::Peer,
+
                last_success: None,
+
                last_attempt: None,
+
            };
+
            cache
+
                .insert(&alice, features, "alice", timestamp, [ka.clone()])
+
                .unwrap();
+
            cache
+
                .insert(&bob, features, "bob", timestamp, [ka])
+
                .unwrap();
+
        }
+
        assert_eq!(cache.len().unwrap(), 6);
+

+
        let removed = cache.remove(&alice).unwrap();
+
        assert!(removed);
+
        assert_eq!(cache.len().unwrap(), 3);
+

+
        let removed = cache.remove(&bob).unwrap();
+
        assert!(removed);
+
        assert_eq!(cache.len().unwrap(), 0);
+
    }
+

+
    #[test]
+
    fn test_entries() {
+
        let ids = arbitrary::vec::<NodeId>(16);
+
        let rng = fastrand::Rng::new();
+
        let mut cache = Book::memory().unwrap();
+
        let mut expected = Vec::new();
+
        let timestamp = LocalTime::now().as_millis();
+
        let features = node::Features::SEED;
+

+
        for id in ids {
+
            let ip = rng.u32(..);
+
            let addr = net::SocketAddr::from((net::Ipv4Addr::from(ip), rng.u16(..)));
+
            let ka = KnownAddress {
+
                addr: addr.into(),
+
                source: Source::Dns,
+
                // TODO: Test times as well.
+
                last_success: None,
+
                last_attempt: None,
+
            };
+
            expected.push((id, ka.clone()));
+
            cache
+
                .insert(&id, features, "alias", timestamp, [ka])
+
                .unwrap();
+
        }
+

+
        let mut actual = cache.entries().unwrap().collect::<Vec<_>>();
+

+
        actual.sort_by_key(|(i, _)| *i);
+
        expected.sort_by_key(|(i, _)| *i);
+

+
        assert_eq!(cache.len().unwrap(), actual.len());
+
        assert_eq!(actual, expected);
+
    }
+
}
added radicle/src/node/address/types.rs
@@ -0,0 +1,135 @@
+
use std::ops::{Deref, DerefMut};
+

+
use localtime::LocalTime;
+
use nonempty::NonEmpty;
+

+
use crate::collections::HashMap;
+
use crate::node;
+
use crate::node::Address;
+
use crate::prelude::Timestamp;
+

+
/// A map with the ability to randomly select values.
+
#[derive(Debug, Clone)]
+
pub struct AddressBook<K, V> {
+
    inner: HashMap<K, V>,
+
    rng: fastrand::Rng,
+
}
+

+
impl<K, V> AddressBook<K, V> {
+
    /// Create a new address book.
+
    pub fn new(rng: fastrand::Rng) -> Self {
+
        Self {
+
            inner: HashMap::with_hasher(rng.clone().into()),
+
            rng,
+
        }
+
    }
+

+
    /// Pick a random value in the book.
+
    pub fn sample(&self) -> Option<(&K, &V)> {
+
        self.sample_with(|_, _| true)
+
    }
+

+
    /// Pick a random value in the book matching a predicate.
+
    pub fn sample_with(&self, mut predicate: impl FnMut(&K, &V) -> bool) -> Option<(&K, &V)> {
+
        if let Some(pairs) = NonEmpty::from_vec(
+
            self.inner
+
                .iter()
+
                .filter(|(k, v)| predicate(*k, *v))
+
                .collect(),
+
        ) {
+
            let ix = self.rng.usize(..pairs.len());
+
            let pair = pairs[ix]; // Can't fail.
+

+
            Some(pair)
+
        } else {
+
            None
+
        }
+
    }
+

+
    /// Cycle through the keys at random. The random cycle repeats ad-infintum.
+
    pub fn cycle(&self) -> impl Iterator<Item = &K> {
+
        self.shuffled().map(|(k, _)| k).cycle()
+
    }
+

+
    /// Return a shuffled iterator over the keys.
+
    pub fn shuffled(&self) -> std::vec::IntoIter<(&K, &V)> {
+
        let mut keys = self.inner.iter().collect::<Vec<_>>();
+
        self.rng.shuffle(&mut keys);
+

+
        keys.into_iter()
+
    }
+
}
+

+
impl<K, V> Deref for AddressBook<K, V> {
+
    type Target = HashMap<K, V>;
+

+
    fn deref(&self) -> &Self::Target {
+
        &self.inner
+
    }
+
}
+

+
impl<K, V> DerefMut for AddressBook<K, V> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.inner
+
    }
+
}
+

+
/// Node public data.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Node {
+
    /// Advertized alias.
+
    pub alias: String,
+
    /// Advertized features.
+
    pub features: node::Features,
+
    /// Advertized addresses
+
    pub addrs: Vec<KnownAddress>,
+
    /// When this data was published.
+
    pub timestamp: Timestamp,
+
}
+

+
/// A known address.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct KnownAddress {
+
    /// Network address.
+
    pub addr: Address,
+
    /// Address of the peer who sent us this address.
+
    pub source: Source,
+
    /// Last time this address was used to successfully connect to a peer.
+
    pub last_success: Option<LocalTime>,
+
    /// Last time this address was tried.
+
    pub last_attempt: Option<LocalTime>,
+
}
+

+
impl KnownAddress {
+
    /// Create a new known address.
+
    pub fn new(addr: Address, source: Source) -> Self {
+
        Self {
+
            addr,
+
            source,
+
            last_success: None,
+
            last_attempt: None,
+
        }
+
    }
+
}
+

+
/// Address source. Specifies where an address originated from.
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub enum Source {
+
    /// An address that was shared by another peer.
+
    Peer,
+
    /// An address that came from a DNS seed.
+
    Dns,
+
    /// An address that came from some source external to the system, eg.
+
    /// specified by the user or added directly to the address manager.
+
    Imported,
+
}
+

+
impl std::fmt::Display for Source {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        match self {
+
            Self::Peer => write!(f, "Peer"),
+
            Self::Dns => write!(f, "DNS"),
+
            Self::Imported => write!(f, "Imported"),
+
        }
+
    }
+
}
modified radicle/src/profile.rs
@@ -18,7 +18,8 @@ use thiserror::Error;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
use crate::crypto::{PublicKey, Signer};
-
use crate::node::{self, routing, tracking};
+
use crate::node;
+
use crate::node::{address, routing, tracking};
use crate::prelude::Did;
use crate::storage::git::transport;
use crate::storage::git::Storage;
@@ -143,6 +144,14 @@ impl Profile {
        Ok(router)
    }

+
    /// Return a handle to the address database of the node.
+
    pub fn addresses(&self) -> Result<address::Book, address::Error> {
+
        let path = self.home.node().join(node::ADDRESS_DB_FILE);
+
        let addresses = address::Book::reader(path)?;
+

+
        Ok(addresses)
+
    }
+

    /// Return the path to the keys folder.
    pub fn keys(&self) -> PathBuf {
        self.home.keys()