Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Rename folder to `radicle-node`
Alexis Sellier committed 3 years ago
commit d1ccf1b807252aa1e2c8f7e1eb693df72e20a043
parent d3d706bbf73671f8fa4c91525c31695b9b027db1
83 files changed +9614 -9614
modified Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-
members = ["node"]
+
members = ["radicle-node"]

[patch.crates-io.radicle-git-ext]
git = "https://github.com/radicle-dev/radicle-link"
deleted node/Cargo.toml
@@ -1,38 +0,0 @@
-
[package]
-
name = "radicle-node"
-
license = "MIT OR Apache-2.0"
-
version = "0.2.0"
-
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
-
edition = "2021"
-

-
[dependencies]
-
anyhow = { version = "1" }
-
bs58 = { version = "0.4.0" }
-
ed25519-compact = { version = "1.0.12", features = ["pem"] }
-
byteorder = { version = "1" }
-
bloomy = { version = "1.2" }
-
chrono = { version = "0.4.0" }
-
colored = { version = "1.9.0" }
-
crossbeam-channel = { version = "0.5.6" }
-
fastrand = { version = "1.8.0" }
-
git-ref-format = { version = "0", features = ["serde", "macro"] }
-
git2 = { version = "0.13" }
-
git-url = { version = "0.3.5", features = ["serde1"] }
-
multibase = { version = "0.9.1" }
-
log = { version = "0.4.17", features = ["std"] }
-
once_cell = { version = "1.13" }
-
olpc-cjson = { version = "0.1.1" }
-
sha2 = { version = "0.10.2" }
-
serde = { version = "1", features = ["derive"] }
-
serde_json = { version = "1", features = ["preserve_order"] }
-
siphasher = { version = "0.3.10" }
-
radicle-git-ext = { version = "0", features = ["serde"] }
-
nonempty = { version = "0.8.0", features = ["serialize"] }
-
nakamoto-net = { version = "0.3.0" }
-
nakamoto-net-poll = { version = "0.3.0" }
-
tempfile = { version = "3.3.0" }
-
thiserror = { version = "1" }
-

-
[dev-dependencies]
-
quickcheck = { version = "1", default-features = false }
-
quickcheck_macros = { version = "1", default-features = false }
deleted node/src/address_book.rs
@@ -1,468 +0,0 @@
-
use std::io::Seek;
-
use std::ops::{Deref, DerefMut};
-
use std::path::Path;
-
use std::{fs, io, net};
-

-
use crate::collections::HashMap;
-
use crate::LocalTime;
-
use nonempty::NonEmpty;
-
use serde::{Deserialize, Serialize};
-

-
/// A map with the ability to randomly select values.
-
#[derive(Debug)]
-
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
-
    }
-
}
-

-
/// A known address.
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct KnownAddress {
-
    /// Network address.
-
    pub addr: net::SocketAddr,
-
    /// Address of the peer who sent us this address.
-
    pub source: Source,
-
    /// Last time this address was used to successfully connect to a peer.
-
    #[serde(with = "local_time")]
-
    pub last_success: Option<LocalTime>,
-
    /// Last time this address was sampled.
-
    #[serde(with = "local_time")]
-
    pub last_sampled: Option<LocalTime>,
-
    /// Last time this address was tried.
-
    #[serde(with = "local_time")]
-
    pub last_attempt: Option<LocalTime>,
-
    /// Last time this peer was seen alive.
-
    #[serde(with = "local_time")]
-
    pub last_active: Option<LocalTime>,
-
}
-

-
impl KnownAddress {
-
    /// Create a new known address.
-
    pub fn new(addr: net::SocketAddr, source: Source, last_active: Option<LocalTime>) -> Self {
-
        Self {
-
            addr,
-
            source,
-
            last_success: None,
-
            last_attempt: None,
-
            last_sampled: None,
-
            last_active,
-
        }
-
    }
-
}
-

-
/// Address source. Specifies where an address originated from.
-
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub enum Source {
-
    /// An address that was shared by another peer.
-
    Peer(net::SocketAddr),
-
    /// 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(addr) => write!(f, "{}", addr),
-
            Self::Dns => write!(f, "DNS"),
-
            Self::Imported => write!(f, "Imported"),
-
        }
-
    }
-
}
-

-
/// A file-backed address cache.
-
#[derive(Debug)]
-
pub struct Cache {
-
    addrs: std::collections::HashMap<net::IpAddr, KnownAddress>,
-
    file: fs::File,
-
}
-

-
impl Cache {
-
    /// Open an existing cache.
-
    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
-
        fs::OpenOptions::new()
-
            .read(true)
-
            .write(true)
-
            .open(path)
-
            .and_then(Self::from)
-
    }
-

-
    /// Create a new cache.
-
    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
-
        use std::collections::HashMap;
-

-
        let file = fs::OpenOptions::new()
-
            .create_new(true)
-
            .write(true)
-
            .open(path)?;
-

-
        Ok(Self {
-
            file,
-
            addrs: HashMap::new(),
-
        })
-
    }
-

-
    /// Create a new cache from a file.
-
    pub fn from(mut file: fs::File) -> io::Result<Self> {
-
        use std::collections::HashMap;
-

-
        let bytes = file.seek(io::SeekFrom::End(0))?;
-
        let addrs = if bytes == 0 {
-
            HashMap::new()
-
        } else {
-
            file.rewind()?;
-
            serde_json::from_reader(&file)?
-
        };
-

-
        Ok(Self { file, addrs })
-
    }
-
}
-

-
impl Store for Cache {
-
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
-
        self.addrs.get_mut(ip)
-
    }
-

-
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
-
        self.addrs.get(ip)
-
    }
-

-
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
-
        self.addrs.remove(ip)
-
    }
-

-
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
-
        <std::collections::HashMap<_, _> as Store>::insert(&mut self.addrs, ip, ka)
-
    }
-

-
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
-
        Box::new(self.addrs.iter())
-
    }
-

-
    fn clear(&mut self) {
-
        self.addrs.clear()
-
    }
-

-
    fn len(&self) -> usize {
-
        self.addrs.len()
-
    }
-

-
    fn flush<'a>(&mut self) -> io::Result<()> {
-
        use io::Write;
-

-
        let peers = serde_json::to_value(&self.addrs)?;
-
        let s = serde_json::to_string(&peers)?;
-

-
        self.file.set_len(0)?;
-
        self.file.seek(io::SeekFrom::Start(0))?;
-
        self.file.write_all(s.as_bytes())?;
-
        self.file.write_all(&[b'\n'])?;
-
        self.file.sync_data()?;
-

-
        Ok(())
-
    }
-
}
-

-
/// Address store.
-
///
-
/// Used to store peer addresses and metadata.
-
pub trait Store {
-
    /// Get a known peer address.
-
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress>;
-

-
    /// Get a known peer address mutably.
-
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress>;
-

-
    /// Insert a *new* address into the store. Returns `true` if the address was inserted,
-
    /// or `false` if it was already known.
-
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool;
-

-
    /// Remove an address from the store.
-
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress>;
-

-
    /// Return an iterator over the known addresses.
-
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a>;
-

-
    /// Returns the number of addresses.
-
    fn len(&self) -> usize;
-

-
    /// Returns true if there are no addresses.
-
    fn is_empty(&self) -> bool {
-
        self.len() == 0
-
    }
-

-
    /// Seed the peer store with addresses.
-
    /// Fails if *none* of the seeds could be resolved to addresses.
-
    fn seed<S: net::ToSocketAddrs>(
-
        &mut self,
-
        seeds: impl Iterator<Item = S>,
-
        source: Source,
-
    ) -> io::Result<()> {
-
        let mut error = None;
-
        let mut success = false;
-

-
        for seed in seeds {
-
            match seed.to_socket_addrs() {
-
                Ok(addrs) => {
-
                    success = true;
-
                    for addr in addrs {
-
                        self.insert(addr.ip(), KnownAddress::new(addr, source, None));
-
                    }
-
                }
-
                Err(err) => error = Some(err),
-
            }
-
        }
-

-
        if success {
-
            return Ok(());
-
        }
-
        if let Some(err) = error {
-
            return Err(io::Error::new(
-
                io::ErrorKind::Other,
-
                format!("seeds failed to resolve: {}", err),
-
            ));
-
        }
-
        Ok(())
-
    }
-

-
    /// Clears the store of all addresses.
-
    fn clear(&mut self);
-

-
    /// Flush data to permanent storage.
-
    fn flush(&mut self) -> io::Result<()>;
-
}
-

-
/// Implementation of [`Store`] for [`std::collections::HashMap`].
-
impl Store for std::collections::HashMap<net::IpAddr, KnownAddress> {
-
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
-
        self.get_mut(ip)
-
    }
-

-
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
-
        self.get(ip)
-
    }
-

-
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
-
        self.remove(ip)
-
    }
-

-
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
-
        use std::collections::hash_map::Entry;
-

-
        match self.entry(ip) {
-
            Entry::Vacant(v) => {
-
                v.insert(ka);
-
            }
-
            Entry::Occupied(_) => return false,
-
        }
-
        true
-
    }
-

-
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
-
        Box::new(self.iter())
-
    }
-

-
    fn clear(&mut self) {
-
        self.clear()
-
    }
-

-
    fn len(&self) -> usize {
-
        self.len()
-
    }
-

-
    fn flush(&mut self) -> std::io::Result<()> {
-
        Ok(())
-
    }
-
}
-

-
/// Implementation of [`Store`] for [`crate::collections::HashMap`].
-
impl Store for crate::collections::HashMap<net::IpAddr, KnownAddress> {
-
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
-
        self.get_mut(ip)
-
    }
-

-
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
-
        self.get(ip)
-
    }
-

-
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
-
        self.remove(ip)
-
    }
-

-
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
-
        use std::collections::hash_map::Entry;
-

-
        match self.entry(ip) {
-
            Entry::Vacant(v) => {
-
                v.insert(ka);
-
            }
-
            Entry::Occupied(_) => return false,
-
        }
-
        true
-
    }
-

-
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
-
        Box::new(self.iter())
-
    }
-

-
    fn clear(&mut self) {
-
        self.clear()
-
    }
-

-
    fn len(&self) -> usize {
-
        self.len()
-
    }
-

-
    fn flush(&mut self) -> std::io::Result<()> {
-
        Ok(())
-
    }
-
}
-

-
mod local_time {
-
    use super::LocalTime;
-
    use serde::{Deserialize, Deserializer, Serializer};
-

-
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<LocalTime>, D::Error>
-
    where
-
        D: Deserializer<'de>,
-
    {
-
        let value: Option<u64> = Deserialize::deserialize(deserializer)?;
-

-
        if let Some(value) = value {
-
            Ok(Some(LocalTime::from_secs(value)))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    pub fn serialize<S>(value: &Option<LocalTime>, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: Serializer,
-
    {
-
        if let Some(local_time) = value {
-
            serializer.serialize_u64(local_time.as_secs())
-
        } else {
-
            serializer.serialize_none()
-
        }
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use super::*;
-

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

-
        Cache::create(&path).unwrap();
-
        let cache = Cache::open(&path).unwrap();
-

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

-
    #[test]
-
    fn test_save_and_load() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let path = tmp.path().join("cache");
-
        let mut expected = Vec::new();
-

-
        {
-
            let mut cache = Cache::create(&path).unwrap();
-

-
            for i in 32..48 {
-
                let ip = net::IpAddr::from([127, 0, 0, i]);
-
                let addr = net::SocketAddr::from((ip, 8333));
-
                let ka = KnownAddress {
-
                    addr,
-
                    source: Source::Dns,
-
                    last_success: Some(LocalTime::from_secs(i as u64)),
-
                    last_sampled: Some(LocalTime::from_secs((i + 1) as u64)),
-
                    last_attempt: None,
-
                    last_active: None,
-
                };
-
                cache.insert(ip, ka);
-
            }
-
            cache.flush().unwrap();
-

-
            for (ip, ka) in cache.iter() {
-
                expected.push((*ip, ka.clone()));
-
            }
-
        }
-

-
        {
-
            let cache = Cache::open(&path).unwrap();
-
            let mut actual = cache
-
                .iter()
-
                .map(|(i, ka)| (*i, ka.clone()))
-
                .collect::<Vec<_>>();
-

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

-
            assert_eq!(actual, expected);
-
        }
-
    }
-
}
deleted node/src/address_manager.rs
@@ -1,12 +0,0 @@
-
use crate::address_book::Store;
-

-
#[derive(Debug)]
-
pub struct AddressManager<S> {
-
    store: S,
-
}
-

-
impl<S: Store> AddressManager<S> {
-
    pub fn new(store: S) -> Self {
-
        Self { store }
-
    }
-
}
deleted node/src/client.rs
@@ -1,126 +0,0 @@
-
use std::net;
-
use std::path::Path;
-

-
use crossbeam_channel as chan;
-
use nakamoto_net::{LocalTime, Reactor};
-

-
use crate::clock::RefClock;
-
use crate::collections::HashMap;
-
use crate::crypto::Signer;
-
use crate::service;
-
use crate::storage::git::Storage;
-
use crate::transport::Transport;
-
use crate::wire::Wire;
-

-
pub mod handle;
-

-
/// Client configuration.
-
#[derive(Debug, Clone)]
-
pub struct Config {
-
    /// Client service configuration.
-
    pub service: service::Config,
-
    /// Client listen addresses.
-
    pub listen: Vec<net::SocketAddr>,
-
}
-

-
impl Config {
-
    /// Create a new configuration for the given network.
-
    pub fn new(network: service::Network) -> Self {
-
        Self {
-
            service: service::Config {
-
                network,
-
                ..service::Config::default()
-
            },
-
            ..Self::default()
-
        }
-
    }
-
}
-

-
impl Default for Config {
-
    fn default() -> Self {
-
        Self {
-
            service: service::Config::default(),
-
            listen: vec![([0, 0, 0, 0], 0).into()],
-
        }
-
    }
-
}
-

-
pub struct Client<R: Reactor, G: Signer> {
-
    reactor: R,
-
    storage: Storage,
-
    signer: G,
-

-
    handle: chan::Sender<service::Command>,
-
    commands: chan::Receiver<service::Command>,
-
    shutdown: chan::Sender<()>,
-
    listening: chan::Receiver<net::SocketAddr>,
-
    events: Events,
-
}
-

-
impl<R: Reactor, G: Signer> Client<R, G> {
-
    pub fn new<P: AsRef<Path>>(path: P, signer: G) -> Result<Self, nakamoto_net::error::Error> {
-
        let (handle, commands) = chan::unbounded::<service::Command>();
-
        let (shutdown, shutdown_recv) = chan::bounded(1);
-
        let (listening_send, listening) = chan::bounded(1);
-
        let reactor = R::new(shutdown_recv, listening_send)?;
-
        let storage = Storage::open(path)?;
-
        let events = Events {};
-

-
        Ok(Self {
-
            storage,
-
            signer,
-
            reactor,
-
            handle,
-
            commands,
-
            listening,
-
            shutdown,
-
            events,
-
        })
-
    }
-

-
    pub fn run(mut self, config: Config) -> Result<(), nakamoto_net::error::Error> {
-
        let network = config.service.network;
-
        let rng = fastrand::Rng::new();
-
        let time = LocalTime::now();
-
        let storage = self.storage;
-
        let signer = self.signer;
-
        let addresses = HashMap::with_hasher(rng.clone().into());
-

-
        log::info!("Initializing client ({:?})..", network);
-

-
        let service = service::Service::new(
-
            config.service,
-
            RefClock::from(time),
-
            storage,
-
            addresses,
-
            signer,
-
            rng,
-
        );
-
        self.reactor.run(
-
            &config.listen,
-
            Transport::new(Wire::new(service)),
-
            self.events,
-
            self.commands,
-
        )?;
-

-
        Ok(())
-
    }
-

-
    /// Create a new handle to communicate with the client.
-
    pub fn handle(&self) -> handle::Handle<R::Waker> {
-
        handle::Handle {
-
            waker: self.reactor.waker(),
-
            commands: self.handle.clone(),
-
            shutdown: self.shutdown.clone(),
-
            listening: self.listening.clone(),
-
        }
-
    }
-
}
-

-
pub struct Events {}
-

-
impl nakamoto_net::Publisher<service::Event> for Events {
-
    fn publish(&mut self, e: service::Event) {
-
        log::info!("Received event {:?}", e);
-
    }
-
}
deleted node/src/client/handle.rs
@@ -1,118 +0,0 @@
-
use std::net;
-

-
use crossbeam_channel as chan;
-
use nakamoto_net::Waker;
-
use thiserror::Error;
-

-
use crate::identity::Id;
-
use crate::service;
-
use crate::service::{CommandError, FetchLookup};
-

-
/// An error resulting from a handle method.
-
#[derive(Error, Debug)]
-
pub enum Error {
-
    /// The command channel is no longer connected.
-
    #[error("command channel is not connected")]
-
    NotConnected,
-
    /// The command returned an error.
-
    #[error("command failed: {0}")]
-
    Command(#[from] CommandError),
-
    /// The operation timed out.
-
    #[error("the operation timed out")]
-
    Timeout,
-
    /// An I/O error occured.
-
    #[error(transparent)]
-
    Io(#[from] std::io::Error),
-
}
-

-
impl From<chan::RecvError> for Error {
-
    fn from(_: chan::RecvError) -> Self {
-
        Self::NotConnected
-
    }
-
}
-

-
impl From<chan::RecvTimeoutError> for Error {
-
    fn from(err: chan::RecvTimeoutError) -> Self {
-
        match err {
-
            chan::RecvTimeoutError::Timeout => Self::Timeout,
-
            chan::RecvTimeoutError::Disconnected => Self::NotConnected,
-
        }
-
    }
-
}
-

-
impl<T> From<chan::SendError<T>> for Error {
-
    fn from(_: chan::SendError<T>) -> Self {
-
        Self::NotConnected
-
    }
-
}
-

-
pub struct Handle<W: Waker> {
-
    pub(crate) commands: chan::Sender<service::Command>,
-
    pub(crate) shutdown: chan::Sender<()>,
-
    pub(crate) listening: chan::Receiver<net::SocketAddr>,
-
    pub(crate) waker: W,
-
}
-

-
impl<W: Waker> traits::Handle for Handle<W> {
-
    /// Retrieve or update the given project from the network.
-
    fn fetch(&self, id: Id) -> Result<FetchLookup, Error> {
-
        let (sender, receiver) = chan::bounded(1);
-
        self.commands.send(service::Command::Fetch(id, sender))?;
-
        receiver.recv().map_err(Error::from)
-
    }
-

-
    /// Start tracking the given project. Doesn't do anything if the project is already tracked.
-
    fn track(&self, id: Id) -> Result<bool, Error> {
-
        let (sender, receiver) = chan::bounded(1);
-
        self.commands.send(service::Command::Track(id, sender))?;
-
        receiver.recv().map_err(Error::from)
-
    }
-

-
    /// Untrack the given project and delete it from storage.
-
    fn untrack(&self, id: Id) -> Result<bool, Error> {
-
        let (sender, receiver) = chan::bounded(1);
-
        self.commands.send(service::Command::Untrack(id, sender))?;
-
        receiver.recv().map_err(Error::from)
-
    }
-

-
    /// Notify the client that a project has been updated.
-
    fn updated(&self, id: Id) -> Result<(), Error> {
-
        self.command(service::Command::AnnounceRefs(id))
-
    }
-

-
    /// Send a command to the command channel, and wake up the event loop.
-
    fn command(&self, cmd: service::Command) -> Result<(), Error> {
-
        self.commands.send(cmd)?;
-
        self.waker.wake()?;
-

-
        Ok(())
-
    }
-

-
    /// Ask the client to shutdown.
-
    fn shutdown(self) -> Result<(), Error> {
-
        self.shutdown.send(())?;
-
        self.waker.wake()?;
-

-
        Ok(())
-
    }
-
}
-

-
pub mod traits {
-
    use super::*;
-

-
    pub trait Handle {
-
        /// Retrieve or update the project from network.
-
        fn fetch(&self, id: Id) -> Result<FetchLookup, Error>;
-
        /// Start tracking the given project. Doesn't do anything if the project is already
-
        /// tracked.
-
        fn track(&self, id: Id) -> Result<bool, Error>;
-
        /// Untrack the given project and delete it from storage.
-
        fn untrack(&self, id: Id) -> Result<bool, Error>;
-
        /// Notify the client that a project has been updated.
-
        fn updated(&self, id: Id) -> Result<(), Error>;
-
        /// Send a command to the command channel, and wake up the event loop.
-
        fn command(&self, cmd: service::Command) -> Result<(), Error>;
-
        /// Ask the client to shutdown.
-
        fn shutdown(self) -> Result<(), Error>;
-
    }
-
}
deleted node/src/clock.rs
@@ -1,37 +0,0 @@
-
use std::cell::RefCell;
-
use std::rc::Rc;
-

-
use crate::{LocalDuration, LocalTime};
-

-
/// Clock with interior mutability.
-
#[derive(Debug, Clone)]
-
pub struct RefClock(Rc<RefCell<LocalTime>>);
-

-
impl std::ops::Deref for RefClock {
-
    type Target = Rc<RefCell<LocalTime>>;
-

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

-
impl RefClock {
-
    /// Elapse time.
-
    pub fn elapse(&self, duration: LocalDuration) {
-
        self.borrow_mut().elapse(duration)
-
    }
-

-
    pub fn local_time(&self) -> LocalTime {
-
        *self.borrow()
-
    }
-

-
    pub fn set(&mut self, time: LocalTime) {
-
        *self.borrow_mut() = time;
-
    }
-
}
-

-
impl From<LocalTime> for RefClock {
-
    fn from(other: LocalTime) -> Self {
-
        Self(Rc::new(RefCell::new(other)))
-
    }
-
}
deleted node/src/collections.rs
@@ -1,44 +0,0 @@
-
//! Useful collections for peer-to-peer networking.
-
use siphasher::sip::SipHasher13;
-

-
/// A `HashMap` which uses [`fastrand::Rng`] for its random state.
-
pub type HashMap<K, V> = std::collections::HashMap<K, V, RandomState>;
-

-
/// A `HashSet` which uses [`fastrand::Rng`] for its random state.
-
pub type HashSet<K> = std::collections::HashSet<K, RandomState>;
-

-
/// Random hasher state.
-
#[derive(Clone)]
-
pub struct RandomState {
-
    key1: u64,
-
    key2: u64,
-
}
-

-
impl Default for RandomState {
-
    fn default() -> Self {
-
        Self::new(fastrand::Rng::new())
-
    }
-
}
-

-
impl RandomState {
-
    fn new(rng: fastrand::Rng) -> Self {
-
        Self {
-
            key1: rng.u64(..),
-
            key2: rng.u64(..),
-
        }
-
    }
-
}
-

-
impl std::hash::BuildHasher for RandomState {
-
    type Hasher = SipHasher13;
-

-
    fn build_hasher(&self) -> Self::Hasher {
-
        SipHasher13::new_with_keys(self.key1, self.key2)
-
    }
-
}
-

-
impl From<fastrand::Rng> for RandomState {
-
    fn from(rng: fastrand::Rng) -> Self {
-
        Self::new(rng)
-
    }
-
}
deleted node/src/control.rs
@@ -1,203 +0,0 @@
-
//! Client control socket implementation.
-
use std::io::prelude::*;
-
use std::io::BufReader;
-
use std::io::LineWriter;
-
use std::os::unix::net::UnixListener;
-
use std::os::unix::net::UnixStream;
-
use std::path::Path;
-
use std::{fs, io, net};
-

-
use crate::client;
-
use crate::client::handle::traits::Handle;
-
use crate::identity::Id;
-
use crate::service::FetchLookup;
-
use crate::service::FetchResult;
-

-
/// Default name for control socket file.
-
pub const DEFAULT_SOCKET_NAME: &str = "radicle.sock";
-

-
#[derive(thiserror::Error, Debug)]
-
pub enum Error {
-
    #[error("failed to bind control socket listener: {0}")]
-
    Bind(io::Error),
-
}
-

-
/// Listen for commands on the control socket, and process them.
-
pub fn listen<P: AsRef<Path>, H: Handle>(path: P, handle: H) -> Result<(), Error> {
-
    // Remove the socket file on startup before rebinding.
-
    fs::remove_file(&path).ok();
-

-
    let listener = UnixListener::bind(path).map_err(Error::Bind)?;
-
    for incoming in listener.incoming() {
-
        match incoming {
-
            Ok(mut stream) => {
-
                if let Err(e) = drain(&stream, &handle) {
-
                    log::error!("Received {} on control socket", e);
-

-
                    writeln!(stream, "error: {}", e).ok();
-

-
                    stream.flush().ok();
-
                    stream.shutdown(net::Shutdown::Both).ok();
-
                } else {
-
                    writeln!(stream, "ok").ok();
-
                }
-
            }
-
            Err(e) => log::error!("Failed to open control socket stream: {}", e),
-
        }
-
    }
-

-
    Ok(())
-
}
-

-
#[derive(thiserror::Error, Debug)]
-
enum DrainError {
-
    #[error("invalid command argument `{0}`")]
-
    InvalidCommandArg(String),
-
    #[error("unknown command `{0}`")]
-
    UnknownCommand(String),
-
    #[error("invalid command")]
-
    InvalidCommand,
-
    #[error("client error: {0}")]
-
    Client(#[from] client::handle::Error),
-
    #[error("i/o error: {0}")]
-
    Io(#[from] io::Error),
-
}
-

-
fn drain<H: Handle>(stream: &UnixStream, handle: &H) -> Result<(), DrainError> {
-
    let mut reader = BufReader::new(stream);
-

-
    // TODO: refactor to include helper
-
    for line in reader.by_ref().lines().flatten() {
-
        match line.split_once(' ') {
-
            Some(("fetch", arg)) => {
-
                if let Ok(id) = arg.parse() {
-
                    fetch(id, LineWriter::new(stream), handle)?;
-
                } else {
-
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
-
                }
-
            }
-
            Some(("track", arg)) => {
-
                if let Ok(id) = arg.parse() {
-
                    if let Err(e) = handle.track(id) {
-
                        return Err(DrainError::Client(e));
-
                    }
-
                } else {
-
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
-
                }
-
            }
-
            Some(("untrack", arg)) => {
-
                if let Ok(id) = arg.parse() {
-
                    if let Err(e) = handle.untrack(id) {
-
                        return Err(DrainError::Client(e));
-
                    }
-
                } else {
-
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
-
                }
-
            }
-
            Some(("update", arg)) => {
-
                if let Ok(id) = arg.parse() {
-
                    if let Err(e) = handle.updated(id) {
-
                        return Err(DrainError::Client(e));
-
                    }
-
                } else {
-
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
-
                }
-
            }
-
            Some((cmd, _)) => return Err(DrainError::UnknownCommand(cmd.to_owned())),
-
            None => return Err(DrainError::InvalidCommand),
-
        }
-
    }
-
    Ok(())
-
}
-

-
fn fetch<W: Write, H: Handle>(id: Id, mut writer: W, handle: &H) -> Result<(), DrainError> {
-
    match handle.fetch(id.clone()) {
-
        Err(e) => {
-
            return Err(DrainError::Client(e));
-
        }
-
        Ok(FetchLookup::Found { seeds, results }) => {
-
            let seeds = Vec::from(seeds);
-

-
            writeln!(
-
                writer,
-
                "ok: found {} seeds for {} ({:?})",
-
                seeds.len(),
-
                &id,
-
                &seeds,
-
            )?;
-

-
            for result in results.iter() {
-
                match result {
-
                    FetchResult::Fetched { from, updated } => {
-
                        writeln!(writer, "ok: {} fetched from {}", &id, from)?;
-

-
                        for update in updated {
-
                            writeln!(writer, "{}", update)?;
-
                        }
-
                    }
-
                    FetchResult::Error { from, error } => {
-
                        writeln!(
-
                            writer,
-
                            "error: {} failed to fetch from {}: {}",
-
                            &id, from, error
-
                        )?;
-
                    }
-
                }
-
            }
-
        }
-
        Ok(FetchLookup::NotFound) => {
-
            writeln!(writer, "error: {} was not found", &id)?;
-
        }
-
        Ok(FetchLookup::NotTracking) => {
-
            writeln!(writer, "error: {} is not tracked", &id)?;
-
        }
-
        Ok(FetchLookup::Error(err)) => {
-
            writeln!(writer, "error: {}", err)?;
-
        }
-
    }
-
    Ok(())
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use std::io::prelude::*;
-
    use std::os::unix::net::UnixStream;
-
    use std::{net, thread};
-

-
    use super::*;
-
    use crate::identity::Id;
-
    use crate::test;
-

-
    #[test]
-
    fn test_control_socket() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let handle = test::handle::Handle::default();
-
        let socket = tmp.path().join("alice.sock");
-
        let projs = test::arbitrary::set::<Id>(1..3);
-

-
        thread::spawn({
-
            let socket = socket.clone();
-
            let handle = handle.clone();
-

-
            move || listen(socket, handle)
-
        });
-

-
        let mut stream = loop {
-
            if let Ok(stream) = UnixStream::connect(&socket) {
-
                break stream;
-
            }
-
        };
-
        for proj in &projs {
-
            writeln!(&stream, "update {}", proj).unwrap();
-
        }
-

-
        let mut buf = [0; 2];
-
        stream.shutdown(net::Shutdown::Write).unwrap();
-
        stream.read_exact(&mut buf).unwrap();
-

-
        assert_eq!(&buf, &[b'o', b'k']);
-
        for proj in &projs {
-
            assert!(handle.updates.lock().unwrap().contains(proj));
-
        }
-
    }
-
}
deleted node/src/crypto.rs
@@ -1,225 +0,0 @@
-
use std::sync::Arc;
-
use std::{fmt, ops::Deref, str::FromStr};
-

-
use ed25519_compact as ed25519;
-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
pub use ed25519::{Error, KeyPair, Seed};
-

-
/// Verified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub struct Verified;
-
/// Unverified (used as type witness).
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-
pub struct Unverified;
-

-
pub trait Signer: Send + Sync {
-
    /// Return this signer's public/verification key.
-
    fn public_key(&self) -> &PublicKey;
-
    /// Sign a message and return the signature.
-
    fn sign(&self, msg: &[u8]) -> Signature;
-
}
-

-
impl<T> Signer for Arc<T>
-
where
-
    T: Signer + ?Sized,
-
{
-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.deref().sign(msg)
-
    }
-

-
    fn public_key(&self) -> &PublicKey {
-
        self.deref().public_key()
-
    }
-
}
-

-
impl<T> Signer for &T
-
where
-
    T: Signer + ?Sized,
-
{
-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.deref().sign(msg)
-
    }
-

-
    fn public_key(&self) -> &PublicKey {
-
        self.deref().public_key()
-
    }
-
}
-

-
/// Cryptographic signature.
-
#[derive(PartialEq, Eq, Copy, Clone)]
-
pub struct Signature(pub ed25519::Signature);
-

-
impl fmt::Display for Signature {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        let base = multibase::Base::Base58Btc;
-
        write!(f, "{}", multibase::encode(base, self.deref()))
-
    }
-
}
-

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

-
#[derive(Error, Debug)]
-
pub enum SignatureError {
-
    #[error("invalid multibase string: {0}")]
-
    Multibase(#[from] multibase::Error),
-
    #[error("invalid signature: {0}")]
-
    Invalid(#[from] ed25519::Error),
-
}
-

-
impl From<ed25519::Signature> for Signature {
-
    fn from(other: ed25519::Signature) -> Self {
-
        Self(other)
-
    }
-
}
-

-
impl FromStr for Signature {
-
    type Err = SignatureError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
-

-
        Ok(Self(sig))
-
    }
-
}
-

-
impl Deref for Signature {
-
    type Target = ed25519::Signature;
-

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

-
impl From<[u8; 64]> for Signature {
-
    fn from(bytes: [u8; 64]) -> Self {
-
        Self(ed25519::Signature::new(bytes))
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Signature {
-
    type Error = ed25519::Error;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
-
        ed25519::Signature::from_slice(bytes).map(Self)
-
    }
-
}
-

-
/// The public/verification key.
-
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
-
#[serde(into = "String", try_from = "String")]
-
pub struct PublicKey(pub ed25519::PublicKey);
-

-
/// The private/signing key.
-
pub type SecretKey = ed25519::SecretKey;
-

-
#[derive(Error, Debug)]
-
pub enum PublicKeyError {
-
    #[error("invalid length {0}")]
-
    InvalidLength(usize),
-
    #[error("invalid multibase string: {0}")]
-
    Multibase(#[from] multibase::Error),
-
    #[error("invalid key: {0}")]
-
    InvalidKey(#[from] ed25519::Error),
-
}
-

-
impl std::hash::Hash for PublicKey {
-
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-
        self.0.deref().hash(state)
-
    }
-
}
-

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

-
impl From<PublicKey> for String {
-
    fn from(other: PublicKey) -> Self {
-
        other.to_human()
-
    }
-
}
-

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

-
impl PartialEq for PublicKey {
-
    fn eq(&self, other: &Self) -> bool {
-
        self.0 == other.0
-
    }
-
}
-

-
impl From<ed25519::PublicKey> for PublicKey {
-
    fn from(other: ed25519::PublicKey) -> Self {
-
        Self(other)
-
    }
-
}
-

-
impl TryFrom<[u8; 32]> for PublicKey {
-
    type Error = ed25519::Error;
-

-
    fn try_from(other: [u8; 32]) -> Result<Self, Self::Error> {
-
        Ok(Self(ed25519::PublicKey::new(other)))
-
    }
-
}
-

-
impl PublicKey {
-
    pub fn to_human(&self) -> String {
-
        multibase::encode(multibase::Base::Base58Btc, self.0.deref())
-
    }
-
}
-

-
impl FromStr for PublicKey {
-
    type Err = PublicKeyError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let array: [u8; 32] = bytes
-
            .try_into()
-
            .map_err(|v: Vec<u8>| PublicKeyError::InvalidLength(v.len()))?;
-
        let key = ed25519::PublicKey::new(array);
-

-
        Ok(Self(key))
-
    }
-
}
-

-
impl TryFrom<String> for PublicKey {
-
    type Error = PublicKeyError;
-

-
    fn try_from(value: String) -> Result<Self, Self::Error> {
-
        Self::from_str(&value)
-
    }
-
}
-

-
impl Deref for PublicKey {
-
    type Target = ed25519::PublicKey;
-

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

-
#[cfg(test)]
-
mod test {
-
    use crate::crypto::PublicKey;
-
    use quickcheck_macros::quickcheck;
-
    use std::str::FromStr;
-

-
    #[quickcheck]
-
    fn prop_encode_decode(input: PublicKey) {
-
        let encoded = input.to_string();
-
        let decoded = PublicKey::from_str(&encoded).unwrap();
-

-
        assert_eq!(input, decoded);
-
    }
-
}
deleted node/src/decoder.rs
@@ -1,108 +0,0 @@
-
use std::io;
-
use std::marker::PhantomData;
-

-
use crate::service::message::Envelope;
-
use crate::wire;
-

-
/// Message stream decoder.
-
///
-
/// Used to for example turn a byte stream into network messages.
-
#[derive(Debug)]
-
pub struct Decoder<D = Envelope> {
-
    unparsed: Vec<u8>,
-
    item: PhantomData<D>,
-
}
-

-
impl<D> From<Vec<u8>> for Decoder<D> {
-
    fn from(unparsed: Vec<u8>) -> Self {
-
        Self {
-
            unparsed,
-
            item: PhantomData,
-
        }
-
    }
-
}
-

-
impl<D: wire::Decode> Decoder<D> {
-
    /// Create a new stream decoder.
-
    pub fn new(capacity: usize) -> Self {
-
        Self {
-
            unparsed: Vec::with_capacity(capacity),
-
            item: PhantomData,
-
        }
-
    }
-

-
    /// Input bytes into the decoder.
-
    pub fn input(&mut self, bytes: &[u8]) {
-
        self.unparsed.extend_from_slice(bytes);
-
    }
-

-
    /// Decode and return the next message. Returns [`None`] if nothing was decoded.
-
    pub fn decode_next(&mut self) -> Result<Option<D>, wire::Error> {
-
        let mut reader = io::Cursor::new(self.unparsed.as_mut_slice());
-

-
        match D::decode(&mut reader) {
-
            Ok(msg) => {
-
                let pos = reader.position() as usize;
-
                self.unparsed.drain(..pos);
-

-
                Ok(Some(msg))
-
            }
-
            Err(err) if err.is_eof() => Ok(None),
-
            Err(err) => Err(err),
-
        }
-
    }
-
}
-

-
impl<D: wire::Decode> io::Write for Decoder<D> {
-
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-
        self.input(buf);
-

-
        Ok(buf.len())
-
    }
-

-
    fn flush(&mut self) -> io::Result<()> {
-
        Ok(())
-
    }
-
}
-

-
impl<D: wire::Decode> Iterator for Decoder<D> {
-
    type Item = Result<D, wire::Error>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.decode_next().transpose()
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    const MSG_HELLO: &[u8] = &[5, b'h', b'e', b'l', b'l', b'o'];
-
    const MSG_BYE: &[u8] = &[3, b'b', b'y', b'e'];
-

-
    #[quickcheck]
-
    fn prop_decode_next(chunk_size: usize) {
-
        let mut bytes = vec![];
-
        let mut msgs = vec![];
-
        let mut decoder = Decoder::<String>::new(8);
-

-
        let chunk_size = 1 + chunk_size % MSG_HELLO.len() + MSG_BYE.len();
-

-
        bytes.extend_from_slice(MSG_HELLO);
-
        bytes.extend_from_slice(MSG_BYE);
-

-
        for chunk in bytes.as_slice().chunks(chunk_size) {
-
            decoder.input(chunk);
-

-
            while let Some(msg) = decoder.decode_next().unwrap() {
-
                msgs.push(msg);
-
            }
-
        }
-

-
        assert_eq!(decoder.unparsed.len(), 0);
-
        assert_eq!(msgs.len(), 2);
-
        assert_eq!(msgs[0], String::from("hello"));
-
        assert_eq!(msgs[1], String::from("bye"));
-
    }
-
}
deleted node/src/git.rs
@@ -1,243 +0,0 @@
-
use std::path::Path;
-
use std::str::FromStr;
-

-
use git_ref_format as format;
-
use once_cell::sync::Lazy;
-

-
use crate::collections::HashMap;
-
use crate::crypto::PublicKey;
-
use crate::storage::refs::Refs;
-
use crate::storage::RemoteId;
-

-
pub use ext::Error;
-
pub use ext::Oid;
-
pub use git_ref_format as fmt;
-
pub use git_ref_format::{refname, RefStr, RefString};
-
pub use git_url as url;
-
pub use git_url::Url;
-
pub use radicle_git_ext as ext;
-

-
/// Default port of the `git` transport protocol.
-
pub const PROTOCOL_PORT: u16 = 9418;
-

-
#[derive(thiserror::Error, Debug)]
-
pub enum RefError {
-
    #[error("invalid ref name '{0}'")]
-
    InvalidName(format::RefString),
-
    #[error("invalid ref format: {0}")]
-
    Format(#[from] format::Error),
-
}
-

-
#[derive(thiserror::Error, Debug)]
-
pub enum ListRefsError {
-
    #[error("git error: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("invalid ref: {0}")]
-
    InvalidRef(#[from] RefError),
-
}
-

-
pub mod refs {
-
    use super::*;
-

-
    /// Where project information is kept.
-
    pub static IDENTITY_BRANCH: Lazy<RefString> = Lazy::new(|| refname!("radicle/id"));
-

-
    pub mod storage {
-
        use super::*;
-

-
        pub fn branch(remote: &RemoteId, branch: &str) -> String {
-
            format!("refs/remotes/{remote}/heads/{branch}")
-
        }
-

-
        /// Get the branch used to track project information.
-
        pub fn id(remote: &RemoteId) -> String {
-
            branch(remote, &IDENTITY_BRANCH)
-
        }
-
    }
-

-
    pub mod workdir {
-
        pub fn branch(branch: &str) -> String {
-
            format!("refs/heads/{branch}")
-
        }
-

-
        pub fn note(name: &str) -> String {
-
            format!("refs/notes/{name}")
-
        }
-

-
        pub fn remote_branch(remote: &str, branch: &str) -> String {
-
            format!("refs/remotes/{remote}/{branch}")
-
        }
-

-
        pub fn tag(name: &str) -> String {
-
            format!("refs/tags/{name}")
-
        }
-
    }
-
}
-

-
/// List remote refs of a project, given the remote URL.
-
pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError> {
-
    let url = url.to_string();
-
    let mut remotes = HashMap::default();
-
    let mut remote = git2::Remote::create_detached(&url)?;
-

-
    remote.connect(git2::Direction::Fetch)?;
-

-
    let refs = remote.list()?;
-
    for r in refs {
-
        let (id, refname) = parse_ref::<PublicKey>(r.name())?;
-
        let entry = remotes.entry(id).or_insert_with(Refs::default);
-

-
        entry.insert(refname, r.oid().into());
-
    }
-

-
    Ok(remotes)
-
}
-

-
/// Parse a ref string.
-
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
-
    let input = format::RefStr::try_from_str(s)?;
-
    let suffix = input
-
        .strip_prefix(format::refname!("refs/remotes"))
-
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
-

-
    let mut components = suffix.components();
-
    let id = components
-
        .next()
-
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
-
    let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
-
    let refstr = components.collect::<format::RefString>();
-

-
    Ok((id, refstr))
-
}
-

-
/// Create an initial empty commit.
-
pub fn initial_commit<'a>(
-
    repo: &'a git2::Repository,
-
    sig: &git2::Signature,
-
) -> Result<git2::Commit<'a>, git2::Error> {
-
    let tree_id = repo.index()?.write_tree()?;
-
    let tree = repo.find_tree(tree_id)?;
-
    let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
-
    let commit = repo.find_commit(oid).unwrap();
-

-
    Ok(commit)
-
}
-

-
/// Create a commit and update the given ref to it.
-
pub fn commit<'a>(
-
    repo: &'a git2::Repository,
-
    parent: &'a git2::Commit,
-
    target: &RefStr,
-
    message: &str,
-
    user: &str,
-
) -> Result<git2::Commit<'a>, git2::Error> {
-
    let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
-
    let tree_id = repo.index()?.write_tree()?;
-
    let tree = repo.find_tree(tree_id)?;
-
    let oid = repo.commit(Some(target.as_str()), &sig, &sig, message, &tree, &[parent])?;
-
    let commit = repo.find_commit(oid).unwrap();
-

-
    Ok(commit)
-
}
-

-
/// Push the refs to the radicle remote.
-
pub fn push(repo: &git2::Repository) -> Result<(), git2::Error> {
-
    let mut remote = repo.find_remote("rad")?;
-
    let refspecs = remote.push_refspecs().unwrap();
-
    let refspec = refspecs.into_iter().next().unwrap().unwrap();
-

-
    // The `git2` crate doesn't seem to support push refspecs with '*' in them,
-
    // so we manually replace it with the current branch.
-
    let head = repo.head().unwrap();
-
    let branch = head.shorthand().unwrap();
-
    let refspec = refspec.replace('*', branch);
-

-
    remote.push::<&str>(&[&refspec], None)
-
}
-

-
/// Get the repository head.
-
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
-
    let head = repo.head()?.peel_to_commit()?;
-

-
    Ok(head)
-
}
-

-
/// Write a tree with the given blob at the given path.
-
pub fn write_tree<'r>(
-
    path: &Path,
-
    bytes: &[u8],
-
    repo: &'r git2::Repository,
-
) -> Result<git2::Tree<'r>, Error> {
-
    let blob_id = repo.blob(bytes)?;
-
    let mut builder = repo.treebuilder(None)?;
-
    builder.insert(path, blob_id, 0o100_644)?;
-

-
    let tree_id = builder.write()?;
-
    let tree = repo.find_tree(tree_id)?;
-

-
    Ok(tree)
-
}
-

-
/// Configure a repository's radicle remote.
-
///
-
/// Takes the repository in which to configure the remote, the name of the remote, the public
-
/// key of the remote, and the path to the remote repository on the filesystem.
-
pub fn configure_remote<'r>(
-
    repo: &'r git2::Repository,
-
    remote_name: &str,
-
    remote_id: &RemoteId,
-
    remote_path: &Path,
-
) -> Result<git2::Remote<'r>, git2::Error> {
-
    let url = Url {
-
        scheme: git_url::Scheme::File,
-
        path: remote_path.to_string_lossy().to_string().into(),
-

-
        ..Url::default()
-
    };
-
    let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*");
-
    let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*");
-
    let remote = repo.remote_with_fetch(remote_name, url.to_string().as_str(), &fetch)?;
-
    repo.remote_add_push(remote_name, &push)?;
-

-
    Ok(remote)
-
}
-

-
/// Set the upstream of the given branch to the given remote.
-
///
-
/// This writes to the `config` directly. The entry will look like the
-
/// following:
-
///
-
/// ```text
-
/// [branch "main"]
-
///     remote = rad
-
///     merge = refs/heads/main
-
/// ```
-
pub fn set_upstream(
-
    repo: &git2::Repository,
-
    remote: &str,
-
    branch: &str,
-
    merge: &str,
-
) -> Result<(), git2::Error> {
-
    let mut config = repo.config()?;
-
    let branch_remote = format!("branch.{}.remote", branch);
-
    let branch_merge = format!("branch.{}.merge", branch);
-

-
    config.remove_multivar(&branch_remote, ".*").or_else(|e| {
-
        if ext::is_not_found_err(&e) {
-
            Ok(())
-
        } else {
-
            Err(e)
-
        }
-
    })?;
-
    config.remove_multivar(&branch_merge, ".*").or_else(|e| {
-
        if ext::is_not_found_err(&e) {
-
            Ok(())
-
        } else {
-
            Err(e)
-
        }
-
    })?;
-
    config.set_multivar(&branch_remote, ".*", remote)?;
-
    config.set_multivar(&branch_merge, ".*", merge)?;
-

-
    Ok(())
-
}
deleted node/src/hash.rs
@@ -1,69 +0,0 @@
-
use std::{convert::TryInto, fmt};
-

-
use serde::{Deserialize, Serialize};
-
use sha2::{
-
    digest::{generic_array::GenericArray, OutputSizeUser},
-
    Digest as _, Sha256,
-
};
-
use thiserror::Error;
-

-
#[derive(Debug, Clone, PartialEq, Eq, Error)]
-
pub enum DecodeError {
-
    #[error("invalid digest length {0}")]
-
    InvalidLength(usize),
-
}
-

-
/// A SHA-256 hash.
-
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Digest([u8; 32]);
-

-
impl Digest {
-
    pub fn new(bytes: impl AsRef<[u8]>) -> Self {
-
        Self::from(Sha256::digest(bytes))
-
    }
-
}
-

-
impl AsRef<[u8; 32]> for Digest {
-
    fn as_ref(&self) -> &[u8; 32] {
-
        &self.0
-
    }
-
}
-

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

-
impl fmt::Display for Digest {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        for byte in &self.0 {
-
            write!(f, "{:02x}", byte)?;
-
        }
-
        Ok(())
-
    }
-
}
-

-
impl From<[u8; 32]> for Digest {
-
    fn from(bytes: [u8; 32]) -> Self {
-
        Self(bytes)
-
    }
-
}
-

-
impl TryFrom<&[u8]> for Digest {
-
    type Error = DecodeError;
-

-
    fn try_from(bytes: &[u8]) -> Result<Self, DecodeError> {
-
        let bytes: [u8; 32] = bytes
-
            .try_into()
-
            .map_err(|_| DecodeError::InvalidLength(bytes.len()))?;
-

-
        Ok(bytes.into())
-
    }
-
}
-

-
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
-
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
-
        Self(array.into())
-
    }
-
}
deleted node/src/identity.rs
@@ -1,246 +0,0 @@
-
pub mod doc;
-

-
use std::ops::Deref;
-
use std::path::PathBuf;
-
use std::{ffi::OsString, fmt, str::FromStr};
-

-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
use crate::crypto;
-
use crate::crypto::Verified;
-
use crate::git;
-
use crate::serde_ext;
-
use crate::storage::Remotes;
-

-
pub use crypto::PublicKey;
-
pub use doc::{Delegate, Doc};
-

-
#[derive(Error, Debug)]
-
pub enum IdError {
-
    #[error("invalid git object id: {0}")]
-
    InvalidOid(#[from] git2::Error),
-
    #[error(transparent)]
-
    Multibase(#[from] multibase::Error),
-
}
-

-
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-
pub struct Id(git::Oid);
-

-
impl fmt::Display for Id {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        f.write_str(&self.to_human())
-
    }
-
}
-

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

-
impl Id {
-
    pub fn to_human(&self) -> String {
-
        multibase::encode(multibase::Base::Base58Btc, self.0.as_bytes())
-
    }
-

-
    pub fn from_human(s: &str) -> Result<Self, IdError> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let array: git::Oid = bytes.as_slice().try_into()?;
-

-
        Ok(Self(array))
-
    }
-
}
-

-
impl FromStr for Id {
-
    type Err = IdError;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        Self::from_human(s)
-
    }
-
}
-

-
impl TryFrom<OsString> for Id {
-
    type Error = IdError;
-

-
    fn try_from(value: OsString) -> Result<Self, Self::Error> {
-
        let string = value.to_string_lossy();
-
        Self::from_str(&string)
-
    }
-
}
-

-
impl From<git::Oid> for Id {
-
    fn from(oid: git::Oid) -> Self {
-
        Self(oid)
-
    }
-
}
-

-
impl From<git2::Oid> for Id {
-
    fn from(oid: git2::Oid) -> Self {
-
        Self(oid.into())
-
    }
-
}
-

-
impl Deref for Id {
-
    type Target = git::Oid;
-

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

-
impl serde::Serialize for Id {
-
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        S: serde::Serializer,
-
    {
-
        serde_ext::string::serialize(self, serializer)
-
    }
-
}
-

-
impl<'de> serde::Deserialize<'de> for Id {
-
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-
    where
-
        D: serde::Deserializer<'de>,
-
    {
-
        serde_ext::string::deserialize(deserializer)
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum DidError {
-
    #[error("invalid did: {0}")]
-
    Did(String),
-
    #[error("invalid public key: {0}")]
-
    PublicKey(#[from] crypto::PublicKeyError),
-
}
-

-
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
-
#[serde(into = "String", try_from = "String")]
-
pub struct Did(crypto::PublicKey);
-

-
impl Did {
-
    pub fn encode(&self) -> String {
-
        format!("did:key:{}", self.0.to_human())
-
    }
-

-
    pub fn decode(input: &str) -> Result<Self, DidError> {
-
        let key = input
-
            .strip_prefix("did:key:")
-
            .ok_or_else(|| DidError::Did(input.to_owned()))?;
-

-
        crypto::PublicKey::from_str(key)
-
            .map(Did)
-
            .map_err(DidError::from)
-
    }
-
}
-

-
impl From<crypto::PublicKey> for Did {
-
    fn from(key: crypto::PublicKey) -> Self {
-
        Self(key)
-
    }
-
}
-

-
impl From<Did> for String {
-
    fn from(other: Did) -> Self {
-
        other.encode()
-
    }
-
}
-

-
impl TryFrom<String> for Did {
-
    type Error = DidError;
-

-
    fn try_from(value: String) -> Result<Self, Self::Error> {
-
        Self::decode(&value)
-
    }
-
}
-

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

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

-
impl Deref for Did {
-
    type Target = PublicKey;
-

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

-
/// A stored and verified project.
-
#[derive(Debug, Clone)]
-
pub struct Project {
-
    /// The project identifier.
-
    pub id: Id,
-
    /// The latest project identity document.
-
    pub doc: Doc<Verified>,
-
    /// The project remotes.
-
    pub remotes: Remotes<Verified>,
-
    /// On-disk file path for this project's repository.
-
    pub path: PathBuf,
-
}
-

-
impl Project {
-
    pub fn delegate(&mut self, name: String, key: crypto::PublicKey) -> bool {
-
        self.doc.delegate(Delegate {
-
            name,
-
            id: Did::from(key),
-
        })
-
    }
-
}
-

-
impl Deref for Project {
-
    type Target = Doc<Verified>;
-

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

-
#[cfg(test)]
-
mod test {
-
    use super::*;
-
    use crate::crypto::PublicKey;
-
    use quickcheck_macros::quickcheck;
-
    use std::collections::HashSet;
-

-
    #[quickcheck]
-
    fn prop_key_equality(a: PublicKey, b: PublicKey) {
-
        assert_ne!(a, b);
-

-
        let mut hm = HashSet::new();
-

-
        assert!(hm.insert(a));
-
        assert!(hm.insert(b));
-
        assert!(!hm.insert(a));
-
        assert!(!hm.insert(b));
-
    }
-

-
    #[quickcheck]
-
    fn prop_from_str(input: Id) {
-
        let encoded = input.to_string();
-
        let decoded = Id::from_str(&encoded).unwrap();
-

-
        assert_eq!(input, decoded);
-
    }
-

-
    #[quickcheck]
-
    fn prop_json_eq_str(pk: PublicKey, proj: Id, did: Did) {
-
        let json = serde_json::to_string(&pk).unwrap();
-
        assert_eq!(format!("\"{}\"", pk), json);
-

-
        let json = serde_json::to_string(&proj).unwrap();
-
        assert_eq!(format!("\"{}\"", proj), json);
-

-
        let json = serde_json::to_string(&did).unwrap();
-
        assert_eq!(format!("\"{}\"", did), json);
-
    }
-
}
deleted node/src/identity/doc.rs
@@ -1,592 +0,0 @@
-
use std::collections::{BTreeMap, HashMap};
-
use std::fmt::Write as _;
-
use std::io;
-
use std::marker::PhantomData;
-
use std::ops::Deref;
-
use std::path::Path;
-

-
use nonempty::NonEmpty;
-
use once_cell::sync::Lazy;
-
use radicle_git_ext::Oid;
-
use serde::{Deserialize, Serialize};
-
use thiserror::Error;
-

-
use crate::crypto;
-
use crate::crypto::{Signature, Unverified, Verified};
-
use crate::git;
-
use crate::identity::{Did, Id};
-
use crate::storage::git::trailers;
-
use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};
-

-
pub use crypto::PublicKey;
-

-
/// Untrusted, well-formed input.
-
#[derive(Clone, Copy, Debug)]
-
pub struct Untrusted;
-
/// Signed by quorum of the previous delegation.
-
#[derive(Clone, Copy, Debug)]
-
pub struct Trusted;
-

-
pub static PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
-

-
pub const MAX_STRING_LENGTH: usize = 255;
-
pub const MAX_DELEGATES: usize = 255;
-

-
#[derive(Error, Debug)]
-
pub enum Error {
-
    #[error("json: {0}")]
-
    Json(#[from] serde_json::Error),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("verification: {0}")]
-
    Verification(#[from] VerificationError),
-
    #[error("git: {0}")]
-
    Git(#[from] git::Error),
-
    #[error("git: {0}")]
-
    RawGit(#[from] git2::Error),
-
}
-

-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
-
pub struct Delegate {
-
    pub name: String,
-
    pub id: Did,
-
}
-

-
impl Delegate {
-
    fn matches(&self, key: &PublicKey) -> bool {
-
        &self.id.0 == key
-
    }
-
}
-

-
impl From<Delegate> for PublicKey {
-
    fn from(delegate: Delegate) -> Self {
-
        delegate.id.0
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
#[serde(rename_all = "kebab-case")]
-
pub struct Payload {
-
    pub name: String,
-
    pub description: String,    // TODO: Make optional.
-
    pub default_branch: String, // TODO: Make optional.
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-
#[serde(transparent)]
-
// TODO: Restrict values.
-
pub struct Namespace(String);
-

-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-
pub struct Doc<V> {
-
    #[serde(rename = "xyz.radicle.project")]
-
    pub payload: Payload,
-
    #[serde(flatten)]
-
    pub extensions: BTreeMap<Namespace, serde_json::Value>,
-
    pub delegates: NonEmpty<Delegate>,
-
    pub threshold: usize,
-

-
    verified: PhantomData<V>,
-
}
-

-
impl Doc<Verified> {
-
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
-
        let mut buf = Vec::new();
-
        let mut serializer =
-
            serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
-

-
        self.serialize(&mut serializer)?;
-
        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
-

-
        Ok((oid.into(), buf))
-
    }
-

-
    /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
-
    pub fn delegate(&mut self, delegate: Delegate) -> bool {
-
        if self.delegates.iter().all(|d| d.id != delegate.id) {
-
            self.delegates.push(delegate);
-
            return true;
-
        }
-
        false
-
    }
-

-
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
-
        let (oid, bytes) = self.encode()?;
-
        let sig = signer.sign(&bytes);
-

-
        Ok((oid, sig))
-
    }
-

-
    pub fn create<'r, S: WriteStorage<'r>>(
-
        &self,
-
        remote: &RemoteId,
-
        msg: &str,
-
        storage: &'r S,
-
    ) -> Result<(Id, git::Oid, S::Repository), Error> {
-
        // You can checkout this branch in your working copy with:
-
        //
-
        //      git fetch rad
-
        //      git checkout -b radicle/id remotes/rad/radicle/id
-
        //
-
        let (doc_oid, doc) = self.encode()?;
-
        let id = Id::from(doc_oid);
-
        let repo = storage.repository(&id).unwrap();
-
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
-
        let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?;
-

-
        drop(tree);
-

-
        Ok((id, oid, repo))
-
    }
-

-
    pub fn update<'r, R: WriteRepository<'r>>(
-
        &self,
-
        remote: &RemoteId,
-
        msg: &str,
-
        signatures: &[(&PublicKey, Signature)],
-
        repo: &R,
-
    ) -> Result<git::Oid, Error> {
-
        let mut msg = format!("{msg}\n\n");
-
        for (key, sig) in signatures {
-
            writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
-
                .expect("in-memory writes don't fail");
-
        }
-

-
        let (_, doc) = self.encode()?;
-
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
-
        let id_ref = git::refs::storage::id(remote);
-
        let head = repo.raw().find_reference(&id_ref)?.peel_to_commit()?;
-
        let oid = Doc::commit(remote, &tree, &msg, &[&head], repo.raw())?;
-

-
        Ok(oid)
-
    }
-

-
    fn commit(
-
        remote: &RemoteId,
-
        tree: &git2::Tree,
-
        msg: &str,
-
        parents: &[&git2::Commit],
-
        repo: &git2::Repository,
-
    ) -> Result<git::Oid, Error> {
-
        let sig = repo
-
            .signature()
-
            .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
-

-
        let id_ref = git::refs::storage::id(remote);
-
        let oid = repo.commit(Some(&id_ref), &sig, &sig, msg, tree, parents)?;
-

-
        Ok(oid.into())
-
    }
-
}
-

-
impl<V> Deref for Doc<V> {
-
    type Target = Payload;
-

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

-
#[derive(Error, Debug)]
-
pub enum VerificationError {
-
    #[error("invalid name: {0}")]
-
    Name(&'static str),
-
    #[error("invalid description: {0}")]
-
    Description(&'static str),
-
    #[error("invalid default branch: {0}")]
-
    DefaultBranch(&'static str),
-
    #[error("invalid delegates: {0}")]
-
    Delegates(&'static str),
-
    #[error("invalid version `{0}`")]
-
    Version(u32),
-
    #[error("invalid parent: {0}")]
-
    Parent(&'static str),
-
    #[error("invalid threshold `{0}`: {1}")]
-
    Threshold(usize, &'static str),
-
}
-

-
impl Doc<Unverified> {
-
    pub fn initial(
-
        name: String,
-
        description: String,
-
        default_branch: BranchName,
-
        delegate: Delegate,
-
    ) -> Self {
-
        Self {
-
            payload: Payload {
-
                name,
-
                description,
-
                default_branch,
-
            },
-
            extensions: BTreeMap::new(),
-
            delegates: NonEmpty::new(delegate),
-
            threshold: 1,
-
            verified: PhantomData,
-
        }
-
    }
-

-
    pub fn new(
-
        name: String,
-
        description: String,
-
        default_branch: BranchName,
-
        delegates: NonEmpty<Delegate>,
-
        threshold: usize,
-
    ) -> Self {
-
        Self {
-
            payload: Payload {
-
                name,
-
                description,
-
                default_branch,
-
            },
-
            extensions: BTreeMap::new(),
-
            delegates,
-
            threshold,
-
            verified: PhantomData,
-
        }
-
    }
-

-
    pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
-
        serde_json::from_slice(bytes)
-
    }
-

-
    pub fn verified(self) -> Result<Doc<Verified>, VerificationError> {
-
        if self.name.is_empty() {
-
            return Err(VerificationError::Name("name cannot be empty"));
-
        }
-
        if self.name.len() > MAX_STRING_LENGTH {
-
            return Err(VerificationError::Name("name cannot exceed 255 bytes"));
-
        }
-
        if self.description.len() > MAX_STRING_LENGTH {
-
            return Err(VerificationError::Description(
-
                "description cannot exceed 255 bytes",
-
            ));
-
        }
-
        if self.delegates.len() > MAX_DELEGATES {
-
            return Err(VerificationError::Delegates(
-
                "number of delegates cannot exceed 255",
-
            ));
-
        }
-
        if self
-
            .delegates
-
            .iter()
-
            .any(|d| d.name.is_empty() || d.name.len() > MAX_STRING_LENGTH)
-
        {
-
            return Err(VerificationError::Delegates(
-
                "delegate name must not be empty and must not exceed 255 bytes",
-
            ));
-
        }
-
        if self.delegates.is_empty() {
-
            return Err(VerificationError::Delegates(
-
                "delegate list cannot be empty",
-
            ));
-
        }
-
        if self.default_branch.is_empty() {
-
            return Err(VerificationError::DefaultBranch(
-
                "default branch cannot be empty",
-
            ));
-
        }
-
        if self.default_branch.len() > MAX_STRING_LENGTH {
-
            return Err(VerificationError::DefaultBranch(
-
                "default branch cannot exceed 255 bytes",
-
            ));
-
        }
-
        if self.threshold > self.delegates.len() {
-
            return Err(VerificationError::Threshold(
-
                self.threshold,
-
                "threshold cannot exceed number of delegates",
-
            ));
-
        }
-
        if self.threshold == 0 {
-
            return Err(VerificationError::Threshold(
-
                self.threshold,
-
                "threshold cannot be zero",
-
            ));
-
        }
-

-
        Ok(Doc {
-
            payload: self.payload,
-
            extensions: self.extensions,
-
            delegates: self.delegates,
-
            threshold: self.threshold,
-
            verified: PhantomData,
-
        })
-
    }
-

-
    pub fn blob_at<'r, R: ReadRepository<'r>>(
-
        commit: Oid,
-
        repo: &R,
-
    ) -> Result<Option<git2::Blob>, git::Error> {
-
        match repo.blob_at(commit, Path::new(&*PATH)) {
-
            Err(git::ext::Error::NotFound(_)) => Ok(None),
-
            Err(e) => Err(e),
-
            Ok(blob) => Ok(Some(blob)),
-
        }
-
    }
-

-
    pub fn load_at<'r, R: ReadRepository<'r>>(
-
        commit: Oid,
-
        repo: &R,
-
    ) -> Result<Option<(Self, Oid)>, git::Error> {
-
        if let Some(blob) = Self::blob_at(commit, repo)? {
-
            let doc = Doc::from_json(blob.content()).unwrap();
-
            return Ok(Some((doc, blob.id().into())));
-
        }
-
        Ok(None)
-
    }
-

-
    pub fn load<'r, R: ReadRepository<'r>>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<(Self, Oid)>, git::Error> {
-
        if let Some(oid) = Self::head(remote, repo)? {
-
            Self::load_at(oid, repo)
-
        } else {
-
            Ok(None)
-
        }
-
    }
-
}
-

-
impl<V> Doc<V> {
-
    pub fn head<'r, R: ReadRepository<'r>>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<Oid>, git::Error> {
-
        let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
-
        if let Some(oid) = repo.reference_oid(remote, head)? {
-
            Ok(Some(oid))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-
}
-

-
#[derive(Error, Debug)]
-
pub enum IdentityError {
-
    #[error("git: {0}")]
-
    GitRaw(#[from] git2::Error),
-
    #[error("git: {0}")]
-
    Git(#[from] git::Error),
-
    #[error("verification: {0}")]
-
    Verification(#[from] VerificationError),
-
    #[error("root hash `{0}` does not match project")]
-
    MismatchedRoot(Oid),
-
    #[error("commit signature for {0} is invalid: {1}")]
-
    InvalidSignature(PublicKey, crypto::Error),
-
    #[error("quorum not reached: {0} signatures for a threshold of {1}")]
-
    QuorumNotReached(usize, usize),
-
}
-

-
#[derive(Clone, Debug, PartialEq, Eq)]
-
pub struct Identity<I> {
-
    /// The head of the identity branch. This points to a commit that
-
    /// contains the current document blob.
-
    pub head: Oid,
-
    /// The canonical identifier for this identity.
-
    /// This is the object id of the initial document blob.
-
    pub root: I,
-
    /// The object id of the current document blob.
-
    pub current: Oid,
-
    /// Revision number. The initial document has a revision of `0`.
-
    pub revision: u32,
-
    /// The current document.
-
    pub doc: Doc<Verified>,
-
    /// Signatures over this identity.
-
    pub signatures: HashMap<PublicKey, Signature>,
-
}
-

-
impl Identity<Oid> {
-
    pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
-
        // The root hash must be equal to the id.
-
        if self.root != *id {
-
            return Err(IdentityError::MismatchedRoot(self.root));
-
        }
-

-
        Ok(Identity {
-
            root: id,
-
            head: self.head,
-
            current: self.current,
-
            revision: self.revision,
-
            doc: self.doc,
-
            signatures: self.signatures,
-
        })
-
    }
-
}
-

-
impl Identity<Untrusted> {
-
    pub fn load<'r, R: ReadRepository<'r>>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<Identity<Oid>>, IdentityError> {
-
        if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
-
            let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
-

-
            // Retrieve root document.
-
            let root_oid = history.pop().unwrap()?.into();
-
            let root_blob = Doc::blob_at(root_oid, repo)?.unwrap();
-
            let root: git::Oid = root_blob.id().into();
-
            let trusted = Doc::from_json(root_blob.content()).unwrap();
-
            let revision = history.len() as u32;
-

-
            let mut trusted = trusted.verified()?;
-
            let mut current = root;
-
            let mut signatures = Vec::new();
-

-
            // Traverse the history chronologically.
-
            for oid in history.into_iter().rev() {
-
                let oid = oid?;
-
                let blob = Doc::blob_at(oid.into(), repo)?.unwrap();
-
                let untrusted = Doc::from_json(blob.content()).unwrap();
-
                let untrusted = untrusted.verified()?;
-
                let commit = repo.commit(oid.into())?.unwrap();
-
                let msg = commit.message_raw().unwrap();
-

-
                // Keys that signed the *current* document version.
-
                signatures = trailers::parse_signatures(msg).unwrap();
-
                for (pk, sig) in &signatures {
-
                    if let Err(err) = pk.verify(blob.content(), sig) {
-
                        return Err(IdentityError::InvalidSignature(*pk, err));
-
                    }
-
                }
-

-
                // Check that enough delegates signed this next version.
-
                let quorum = signatures
-
                    .iter()
-
                    .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
-
                    .count();
-
                if quorum < trusted.threshold {
-
                    return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
-
                }
-

-
                trusted = untrusted;
-
                current = blob.id().into();
-
            }
-

-
            return Ok(Some(Identity {
-
                root,
-
                head,
-
                current,
-
                revision,
-
                doc: trusted,
-
                signatures: signatures.into_iter().collect(),
-
            }));
-
        }
-
        Ok(None)
-
    }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use crate::prelude::Signer;
-
    use crate::rad;
-
    use crate::storage::git::Storage;
-
    use crate::storage::{ReadStorage, WriteStorage};
-
    use crate::test::{crypto, fixtures};
-

-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    #[test]
-
    fn test_valid_identity() {
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let mut rng = fastrand::Rng::new();
-

-
        let alice = crypto::MockSigner::new(&mut rng);
-
        let bob = crypto::MockSigner::new(&mut rng);
-
        let eve = crypto::MockSigner::new(&mut rng);
-

-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (id, _, _, _) =
-
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
-

-
        // Bob and Eve fork the project from Alice.
-
        rad::fork(&id, alice.public_key(), &bob, &storage).unwrap();
-
        rad::fork(&id, alice.public_key(), &eve, &storage).unwrap();
-

-
        // TODO: In some cases we want to get the repo and the project, but don't
-
        // want to have to create a repository object twice. Perhaps there should
-
        // be a way of getting a project from a repo.
-
        let mut proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
-
        let repo = storage.repository(&id).unwrap();
-

-
        // Make a change to the description and sign it.
-
        proj.doc.payload.description += "!";
-
        proj.sign(&alice)
-
            .and_then(|(_, sig)| {
-
                proj.update(
-
                    alice.public_key(),
-
                    "Update description",
-
                    &[(alice.public_key(), sig)],
-
                    &repo,
-
                )
-
            })
-
            .unwrap();
-

-
        // Add Bob as a delegate, and sign it.
-
        proj.delegate("bob".to_owned(), *bob.public_key());
-
        proj.doc.threshold = 2;
-
        proj.sign(&alice)
-
            .and_then(|(_, sig)| {
-
                proj.update(
-
                    alice.public_key(),
-
                    "Add bob",
-
                    &[(alice.public_key(), sig)],
-
                    &repo,
-
                )
-
            })
-
            .unwrap();
-

-
        // Add Eve as a delegate, and sign it.
-
        proj.delegate("eve".to_owned(), *eve.public_key());
-
        proj.sign(&alice)
-
            .and_then(|(_, alice_sig)| {
-
                proj.sign(&bob).and_then(|(_, bob_sig)| {
-
                    proj.update(
-
                        alice.public_key(),
-
                        "Add eve",
-
                        &[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)],
-
                        &repo,
-
                    )
-
                })
-
            })
-
            .unwrap();
-

-
        // Update description again with signatures by Eve and Bob.
-
        proj.doc.payload.description += "?";
-
        let (current, head) = proj
-
            .sign(&bob)
-
            .and_then(|(_, bob_sig)| {
-
                proj.sign(&eve).and_then(|(blob_id, eve_sig)| {
-
                    proj.update(
-
                        alice.public_key(),
-
                        "Update description",
-
                        &[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)],
-
                        &repo,
-
                    )
-
                    .map(|head| (blob_id, head))
-
                })
-
            })
-
            .unwrap();
-

-
        let identity: Identity<Id> = Identity::load(alice.public_key(), &repo)
-
            .unwrap()
-
            .unwrap()
-
            .verified(id.clone())
-
            .unwrap();
-

-
        assert_eq!(identity.signatures.len(), 2);
-
        assert_eq!(identity.revision, 4);
-
        assert_eq!(identity.root, id);
-
        assert_eq!(identity.current, current);
-
        assert_eq!(identity.head, head);
-
        assert_eq!(identity.doc, proj.doc);
-

-
        let proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
-
        assert_eq!(proj.description, "Acme's repository!?");
-
    }
-

-
    #[quickcheck]
-
    fn prop_encode_decode(doc: Doc<Verified>) {
-
        let (_, bytes) = doc.encode().unwrap();
-
        assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
-
    }
-
}
deleted node/src/lib.rs
@@ -1,34 +0,0 @@
-
#![allow(dead_code)]
-
pub use nakamoto_net::{Io, Link, LocalDuration, LocalTime};
-

-
pub mod address_book;
-
pub mod address_manager;
-
pub mod client;
-
pub mod clock;
-
pub mod collections;
-
pub mod control;
-
pub mod crypto;
-
pub mod decoder;
-
pub mod git;
-
pub mod hash;
-
pub mod identity;
-
pub mod logger;
-
pub mod rad;
-
pub mod serde_ext;
-
pub mod service;
-
pub mod storage;
-
#[cfg(test)]
-
pub mod test;
-
pub mod transport;
-
pub mod wire;
-

-
pub mod prelude {
-
    pub use crate::crypto::{PublicKey, Signature, Signer};
-
    pub use crate::decoder::Decoder;
-
    pub use crate::hash::Digest;
-
    pub use crate::identity::{Did, Id};
-
    pub use crate::service::filter::Filter;
-
    pub use crate::service::{NodeId, Timestamp};
-
    pub use crate::storage::refs::Refs;
-
    pub use crate::storage::WriteStorage;
-
}
deleted node/src/logger.rs
@@ -1,61 +0,0 @@
-
//! Logging module.
-
use std::io;
-

-
use chrono::prelude::*;
-
use colored::*;
-
use log::{Level, Log, Metadata, Record, SetLoggerError};
-

-
struct Logger {
-
    level: Level,
-
}
-

-
impl Log for Logger {
-
    fn enabled(&self, metadata: &Metadata) -> bool {
-
        metadata.level() <= self.level
-
    }
-

-
    fn log(&self, record: &Record) {
-
        if self.enabled(record.metadata()) {
-
            let module = record.module_path().unwrap_or_default();
-

-
            if record.level() == Level::Error {
-
                write(record, module, io::stderr());
-
            } else {
-
                write(record, module, io::stdout());
-
            }
-

-
            fn write(record: &log::Record, module: &str, mut stream: impl io::Write) {
-
                let message = format!("{} {} {}", record.level(), module.bold(), record.args());
-
                let message = match record.level() {
-
                    Level::Error => message.red(),
-
                    Level::Warn => message.yellow(),
-
                    Level::Info => message.normal(),
-
                    Level::Debug => message.dimmed(),
-
                    Level::Trace => message.white().dimmed(),
-
                };
-

-
                writeln!(
-
                    stream,
-
                    "{} {}",
-
                    Local::now()
-
                        .to_rfc3339_opts(SecondsFormat::Millis, true)
-
                        .white(),
-
                    message,
-
                )
-
                .expect("write shouldn't fail");
-
            }
-
        }
-
    }
-

-
    fn flush(&self) {}
-
}
-

-
/// Initialize a new logger.
-
pub fn init(level: Level) -> Result<(), SetLoggerError> {
-
    let logger = Logger { level };
-

-
    log::set_boxed_logger(Box::new(logger))?;
-
    log::set_max_level(level.to_level_filter());
-

-
    Ok(())
-
}
deleted node/src/main.rs
@@ -1,36 +0,0 @@
-
use std::path::Path;
-
use std::thread;
-
use std::{env, net};
-

-
use radicle_node::crypto::{PublicKey, Signature, Signer};
-
use radicle_node::{client, control};
-

-
type Reactor = nakamoto_net_poll::Reactor<net::TcpStream>;
-

-
struct FailingSigner {}
-

-
impl Signer for FailingSigner {
-
    fn public_key(&self) -> &PublicKey {
-
        panic!("Failing signer always fails!");
-
    }
-

-
    fn sign(&self, _msg: &[u8]) -> Signature {
-
        panic!("Failing signer always fails!");
-
    }
-
}
-

-
fn main() -> anyhow::Result<()> {
-
    let signer = FailingSigner {};
-
    let client = client::Client::<Reactor, _>::new(Path::new("."), signer)?;
-
    let handle = client.handle();
-
    let config = client::Config::default();
-
    let socket = env::var("RAD_SOCKET").unwrap_or_else(|_| control::DEFAULT_SOCKET_NAME.to_owned());
-

-
    let t1 = thread::spawn(move || control::listen(socket, handle));
-
    let t2 = thread::spawn(move || client.run(config));
-

-
    t1.join().unwrap()?;
-
    t2.join().unwrap()?;
-

-
    Ok(())
-
}
deleted node/src/rad.rs
@@ -1,329 +0,0 @@
-
use std::io;
-
use std::path::Path;
-

-
use thiserror::Error;
-

-
use crate::crypto::{Signer, Verified};
-
use crate::git;
-
use crate::identity::Id;
-
use crate::storage::refs::SignedRefs;
-
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
-
use crate::{identity, storage};
-

-
pub const REMOTE_NAME: &str = "rad";
-

-
#[derive(Error, Debug)]
-
pub enum InitError {
-
    #[error("doc: {0}")]
-
    Doc(#[from] identity::doc::Error),
-
    #[error("doc: {0}")]
-
    DocVerification(#[from] identity::doc::VerificationError),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("storage: {0}")]
-
    Storage(#[from] storage::Error),
-
    #[error("cannot initialize project inside a bare repository")]
-
    BareRepo,
-
    #[error("cannot initialize project from detached head state")]
-
    DetachedHead,
-
    #[error("HEAD reference is not valid UTF-8")]
-
    InvalidHead,
-
}
-

-
/// Initialize a new radicle project from a git repository.
-
pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
-
    repo: &git2::Repository,
-
    name: &str,
-
    description: &str,
-
    default_branch: BranchName,
-
    signer: G,
-
    storage: &'r S,
-
) -> Result<(Id, SignedRefs<Verified>), InitError> {
-
    let pk = signer.public_key();
-
    let delegate = identity::Delegate {
-
        // TODO: Use actual user name.
-
        name: String::from("anonymous"),
-
        id: identity::Did::from(*pk),
-
    };
-
    let doc = identity::Doc::initial(
-
        name.to_owned(),
-
        description.to_owned(),
-
        default_branch.clone(),
-
        delegate,
-
    )
-
    .verified()?;
-

-
    let (id, _, project) = doc.create(pk, "Initialize Radicle", storage)?;
-

-
    git::set_upstream(
-
        repo,
-
        REMOTE_NAME,
-
        &default_branch,
-
        &git::refs::storage::branch(pk, &default_branch),
-
    )?;
-

-
    // TODO: Note that you'll likely want to use `RemoteCallbacks` and set
-
    // `push_update_reference` to test whether all the references were pushed
-
    // successfully.
-
    git::configure_remote(repo, REMOTE_NAME, pk, project.path())?.push::<&str>(
-
        &[&format!(
-
            "{}:{}",
-
            &git::refs::workdir::branch(&default_branch),
-
            &git::refs::storage::branch(pk, &default_branch),
-
        )],
-
        None,
-
    )?;
-
    let signed = storage.sign_refs(&project, signer)?;
-

-
    Ok((id, signed))
-
}
-

-
#[derive(Error, Debug)]
-
pub enum ForkError {
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("storage: {0}")]
-
    Storage(#[from] storage::Error),
-
    #[error("project `{0}` was not found in storage")]
-
    NotFound(Id),
-
    #[error("git: invalid reference")]
-
    InvalidReference,
-
}
-

-
/// Create a local tree for an existing project, from an existing remote.
-
pub fn fork<'r, G: Signer, S: storage::WriteStorage<'r>>(
-
    proj: &Id,
-
    remote: &RemoteId,
-
    signer: G,
-
    storage: S,
-
) -> Result<(), ForkError> {
-
    // TODO: Copy tags over?
-

-
    // Creates or copies the following references:
-
    //
-
    // refs/remotes/<pk>/heads/master
-
    // refs/remotes/<pk>/heads/radicle/id
-
    // refs/remotes/<pk>/tags/*
-
    // refs/remotes/<pk>/rad/signature
-

-
    let me = signer.public_key();
-
    let project = storage
-
        .get(remote, proj)?
-
        .ok_or_else(|| ForkError::NotFound(proj.clone()))?;
-
    let repository = storage.repository(proj)?;
-

-
    let raw = repository.raw();
-
    let remote_head = raw
-
        .find_reference(&git::refs::storage::branch(
-
            remote,
-
            &project.doc.default_branch,
-
        ))?
-
        .target()
-
        .ok_or(ForkError::InvalidReference)?;
-
    raw.reference(
-
        &git::refs::storage::branch(me, &project.doc.default_branch),
-
        remote_head,
-
        false,
-
        &format!("creating default branch for {me}"),
-
    )?;
-

-
    let remote_id = raw
-
        .find_reference(&git::refs::storage::id(remote))?
-
        .target()
-
        .ok_or(ForkError::InvalidReference)?;
-
    raw.reference(
-
        &git::refs::storage::id(me),
-
        remote_id,
-
        false,
-
        &format!("creating identity branch for {me}"),
-
    )?;
-

-
    storage.sign_refs(&repository, &signer)?;
-

-
    Ok(())
-
}
-

-
#[derive(Error, Debug)]
-
pub enum CheckoutError {
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("storage: {0}")]
-
    Storage(#[from] storage::Error),
-
    #[error("project `{0}` was not found in storage")]
-
    NotFound(Id),
-
}
-

-
/// Checkout a project from storage as a working copy.
-
/// This effectively does a `git-clone` from storage.
-
pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
-
    proj: &Id,
-
    remote: &RemoteId,
-
    path: P,
-
    storage: S,
-
) -> Result<git2::Repository, CheckoutError> {
-
    // TODO: Decide on whether we can use `clone_local`
-
    // TODO: Look into sharing object databases.
-
    let project = storage
-
        .get(remote, proj)?
-
        .ok_or_else(|| CheckoutError::NotFound(proj.clone()))?;
-

-
    let mut opts = git2::RepositoryInitOptions::new();
-
    opts.no_reinit(true).description(&project.doc.description);
-

-
    let repo = git2::Repository::init_opts(path, &opts)?;
-
    let default_branch = project.doc.default_branch.as_str();
-

-
    // Configure and fetch all refs from remote.
-
    git::configure_remote(&repo, REMOTE_NAME, remote, &project.path)?.fetch::<&str>(
-
        &[],
-
        None,
-
        None,
-
    )?;
-

-
    {
-
        // Setup default branch.
-
        let remote_head_ref = git::refs::workdir::remote_branch(REMOTE_NAME, default_branch);
-
        let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
-
        let _ = repo.branch(default_branch, &remote_head_commit, true)?;
-

-
        // Setup remote tracking for default branch.
-
        git::set_upstream(
-
            &repo,
-
            REMOTE_NAME,
-
            default_branch,
-
            &git::refs::storage::branch(remote, default_branch),
-
        )?;
-
    }
-

-
    Ok(repo)
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use crate::git::fmt::refname;
-
    use crate::identity::{Delegate, Did};
-
    use crate::storage::git::Storage;
-
    use crate::storage::{ReadStorage, WriteStorage};
-
    use crate::test::{crypto, fixtures};
-

-
    #[test]
-
    fn test_init() {
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let signer = crypto::MockSigner::default();
-
        let public_key = *signer.public_key();
-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
-

-
        let (proj, refs) = init(
-
            &repo,
-
            "acme",
-
            "Acme's repo",
-
            BranchName::from("master"),
-
            &signer,
-
            &storage,
-
        )
-
        .unwrap();
-

-
        let project = storage.get(&public_key, &proj).unwrap().unwrap();
-

-
        assert_eq!(project.remotes[&public_key].refs, refs);
-
        assert_eq!(project.id, proj);
-
        assert_eq!(project.doc.name, "acme");
-
        assert_eq!(project.doc.description, "Acme's repo");
-
        assert_eq!(project.doc.default_branch, BranchName::from("master"));
-
        assert_eq!(
-
            project.doc.delegates.first(),
-
            &Delegate {
-
                name: String::from("anonymous"),
-
                id: Did::from(public_key),
-
            }
-
        );
-
    }
-

-
    #[test]
-
    fn test_fork() {
-
        let mut rng = fastrand::Rng::new();
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let alice = crypto::MockSigner::new(&mut rng);
-
        let alice_id = alice.public_key();
-
        let bob = crypto::MockSigner::new(&mut rng);
-
        let bob_id = bob.public_key();
-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
-

-
        // Alice creates a project.
-
        let (id, alice_refs) = init(
-
            &original,
-
            "acme",
-
            "Acme's repo",
-
            BranchName::from("master"),
-
            &alice,
-
            &storage,
-
        )
-
        .unwrap();
-

-
        // Bob forks it and creates a checkout.
-
        fork(&id, alice_id, &bob, &storage).unwrap();
-
        checkout(&id, bob_id, tempdir.path().join("copy"), &storage).unwrap();
-

-
        let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap();
-

-
        assert_eq!(
-
            bob_remote.refs.get(&refname!("master")),
-
            alice_refs.get(&refname!("master"))
-
        );
-
    }
-

-
    #[test]
-
    fn test_checkout() {
-
        let tempdir = tempfile::tempdir().unwrap();
-
        let signer = crypto::MockSigner::default();
-
        let remote_id = signer.public_key();
-
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
-
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
-

-
        let (id, _) = init(
-
            &original,
-
            "acme",
-
            "Acme's repo",
-
            BranchName::from("master"),
-
            &signer,
-
            &storage,
-
        )
-
        .unwrap();
-

-
        let copy = checkout(&id, remote_id, tempdir.path().join("copy"), &storage).unwrap();
-

-
        assert_eq!(
-
            copy.head().unwrap().target(),
-
            original.head().unwrap().target()
-
        );
-
        assert_eq!(
-
            copy.branch_upstream_name("refs/heads/master")
-
                .unwrap()
-
                .to_vec(),
-
            original
-
                .branch_upstream_name("refs/heads/master")
-
                .unwrap()
-
                .to_vec()
-
        );
-
        assert_eq!(
-
            copy.find_remote(REMOTE_NAME)
-
                .unwrap()
-
                .refspecs()
-
                .into_iter()
-
                .map(|r| r.bytes().to_vec())
-
                .collect::<Vec<_>>(),
-
            original
-
                .find_remote(REMOTE_NAME)
-
                .unwrap()
-
                .refspecs()
-
                .into_iter()
-
                .map(|r| r.bytes().to_vec())
-
                .collect::<Vec<_>>(),
-
        );
-
    }
-
}
deleted node/src/serde_ext.rs
@@ -1,25 +0,0 @@
-
pub mod string {
-
    use std::fmt::Display;
-
    use std::str::FromStr;
-

-
    use serde::{de, Deserialize, Deserializer, Serializer};
-

-
    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
-
    where
-
        T: Display,
-
        S: Serializer,
-
    {
-
        serializer.collect_str(value)
-
    }
-

-
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
-
    where
-
        T: FromStr,
-
        T::Err: Display,
-
        D: Deserializer<'de>,
-
    {
-
        String::deserialize(deserializer)?
-
            .parse()
-
            .map_err(de::Error::custom)
-
    }
-
}
deleted node/src/service.rs
@@ -1,887 +0,0 @@
-
#![allow(dead_code)]
-
pub mod config;
-
pub mod filter;
-
pub mod message;
-
pub mod peer;
-

-
use std::ops::{Deref, DerefMut};
-
use std::{collections::VecDeque, fmt, net, net::IpAddr};
-

-
use crossbeam_channel as chan;
-
use fastrand::Rng;
-
use git_url::Url;
-
use log::*;
-
use nakamoto::{LocalDuration, LocalTime};
-
use nakamoto_net as nakamoto;
-
use nakamoto_net::Link;
-
use nonempty::NonEmpty;
-

-
use crate::address_book;
-
use crate::address_book::AddressBook;
-
use crate::address_manager::AddressManager;
-
use crate::clock::RefClock;
-
use crate::collections::{HashMap, HashSet};
-
use crate::crypto;
-
use crate::identity::{Id, Project};
-
use crate::service::config::ProjectTracking;
-
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
-
use crate::service::peer::{Peer, PeerError, PeerState};
-
use crate::storage;
-
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, WriteStorage};
-

-
pub use crate::service::config::{Config, Network};
-
pub use crate::service::message::{Envelope, Message};
-

-
use self::filter::Filter;
-
use self::message::{InventoryAnnouncement, NodeFeatures};
-

-
pub const DEFAULT_PORT: u16 = 8776;
-
pub const PROTOCOL_VERSION: u32 = 1;
-
pub const TARGET_OUTBOUND_PEERS: usize = 8;
-
pub const IDLE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
-
pub const ANNOUNCE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
-
pub const SYNC_INTERVAL: LocalDuration = LocalDuration::from_secs(60);
-
pub const PRUNE_INTERVAL: LocalDuration = LocalDuration::from_mins(30);
-
pub const MAX_CONNECTION_ATTEMPTS: usize = 3;
-
pub const MAX_TIME_DELTA: LocalDuration = LocalDuration::from_mins(60);
-

-
/// Network node identifier.
-
pub type NodeId = crypto::PublicKey;
-
/// Network routing table. Keeps track of where projects are hosted.
-
pub type Routing = HashMap<Id, HashSet<NodeId>>;
-
/// Seconds since epoch.
-
pub type Timestamp = u64;
-

-
/// Output of a state transition.
-
#[derive(Debug)]
-
pub enum Io {
-
    /// There are some messages ready to be sent to a peer.
-
    Write(net::SocketAddr, Vec<Envelope>),
-
    /// Connect to a peer.
-
    Connect(net::SocketAddr),
-
    /// Disconnect from a peer.
-
    Disconnect(net::SocketAddr, DisconnectReason),
-
    /// Ask for a wakeup in a specified amount of time.
-
    Wakeup(LocalDuration),
-
    /// Emit an event.
-
    Event(Event),
-
}
-

-
/// A service event.
-
#[derive(Debug, Clone)]
-
pub enum Event {
-
    RefsFetched {
-
        from: Url,
-
        project: Id,
-
        updated: Vec<RefUpdate>,
-
    },
-
}
-

-
/// Error returned by [`Command::Fetch`].
-
#[derive(thiserror::Error, Debug)]
-
pub enum FetchError {
-
    #[error(transparent)]
-
    Git(#[from] git2::Error),
-
    #[error(transparent)]
-
    Storage(#[from] storage::Error),
-
    #[error(transparent)]
-
    Fetch(#[from] storage::FetchError),
-
}
-

-
/// Result of looking up seeds in our routing table.
-
#[derive(Debug)]
-
pub enum FetchLookup {
-
    /// Found seeds for the given project.
-
    Found {
-
        seeds: NonEmpty<net::SocketAddr>,
-
        results: chan::Receiver<FetchResult>,
-
    },
-
    /// Can't fetch because no seeds were found for this project.
-
    NotFound,
-
    /// Can't fetch because the project isn't tracked.
-
    NotTracking,
-
    /// Error trying to find seeds.
-
    Error(FetchError),
-
}
-

-
/// Result of a fetch request from a specific seed.
-
#[derive(Debug)]
-
#[allow(clippy::large_enum_variant)]
-
pub enum FetchResult {
-
    /// Successful fetch from a seed.
-
    Fetched {
-
        from: net::SocketAddr,
-
        updated: Vec<RefUpdate>,
-
    },
-
    /// Error fetching the resource from a seed.
-
    Error {
-
        from: net::SocketAddr,
-
        error: FetchError,
-
    },
-
}
-

-
/// Commands sent to the service by the operator.
-
#[derive(Debug)]
-
pub enum Command {
-
    AnnounceRefs(Id),
-
    Connect(net::SocketAddr),
-
    Fetch(Id, chan::Sender<FetchLookup>),
-
    Track(Id, chan::Sender<bool>),
-
    Untrack(Id, chan::Sender<bool>),
-
}
-

-
/// Command-related errors.
-
#[derive(thiserror::Error, Debug)]
-
pub enum CommandError {}
-

-
#[derive(Debug)]
-
pub struct Service<S, T, G> {
-
    /// Peers currently or recently connected.
-
    peers: Peers,
-
    /// Service state that isn't peer-specific.
-
    context: Context<S, T, G>,
-
    /// Whether our local inventory no long represents what we have announced to the network.
-
    out_of_sync: bool,
-
    /// Last time the service was idle.
-
    last_idle: LocalTime,
-
    /// Last time the service synced.
-
    last_sync: LocalTime,
-
    /// Last time the service routing table was pruned.
-
    last_prune: LocalTime,
-
    /// Last time the service announced its inventory.
-
    last_announce: LocalTime,
-
    /// Time when the service was initialized.
-
    start_time: LocalTime,
-
}
-

-
impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Service<S, T, G> {
-
    pub fn new(
-
        config: Config,
-
        clock: RefClock,
-
        storage: T,
-
        addresses: S,
-
        signer: G,
-
        rng: Rng,
-
    ) -> Self {
-
        let addrmgr = AddressManager::new(addresses);
-

-
        Self {
-
            context: Context::new(config, clock, storage, addrmgr, signer, rng.clone()),
-
            peers: Peers::new(rng),
-
            out_of_sync: false,
-
            last_idle: LocalTime::default(),
-
            last_sync: LocalTime::default(),
-
            last_prune: LocalTime::default(),
-
            last_announce: LocalTime::default(),
-
            start_time: LocalTime::default(),
-
        }
-
    }
-

-
    pub fn disconnect(&mut self, remote: &IpAddr, reason: DisconnectReason) {
-
        if let Some(addr) = self.peers.get(remote).map(|p| p.addr) {
-
            self.context.disconnect(addr, reason);
-
        }
-
    }
-

-
    pub fn seeds(&self, id: &Id) -> Box<dyn Iterator<Item = (&NodeId, &Peer)> + '_> {
-
        if let Some(peers) = self.routing.get(id) {
-
            Box::new(
-
                peers
-
                    .iter()
-
                    .filter_map(|id| self.peers.by_id(id).map(|p| (id, p))),
-
            )
-
        } else {
-
            Box::new(std::iter::empty())
-
        }
-
    }
-

-
    pub fn tracked(&self) -> Result<Vec<Id>, storage::Error> {
-
        let tracked = match &self.config.project_tracking {
-
            ProjectTracking::All { blocked } => self
-
                .storage
-
                .inventory()?
-
                .into_iter()
-
                .filter(|id| !blocked.contains(id))
-
                .collect(),
-

-
            ProjectTracking::Allowed(projs) => projs.iter().cloned().collect(),
-
        };
-

-
        Ok(tracked)
-
    }
-

-
    /// Track a project.
-
    /// Returns whether or not the tracking policy was updated.
-
    pub fn track(&mut self, id: Id) -> bool {
-
        self.out_of_sync = self.config.track(id);
-
        self.out_of_sync
-
    }
-

-
    /// Untrack a project.
-
    /// Returns whether or not the tracking policy was updated.
-
    /// Note that when untracking, we don't announce anything to the network. This is because by
-
    /// simply not announcing it anymore, it will eventually be pruned by nodes.
-
    pub fn untrack(&mut self, id: Id) -> bool {
-
        self.config.untrack(id)
-
    }
-

-
    /// Find the closest `n` peers by proximity in tracking graphs.
-
    /// Returns a sorted list from the closest peer to the furthest.
-
    /// Peers with more trackings in common score score higher.
-
    #[allow(unused)]
-
    pub fn closest_peers(&self, n: usize) -> Vec<NodeId> {
-
        todo!()
-
    }
-

-
    /// Get the connected peers.
-
    pub fn peers(&self) -> &Peers {
-
        &self.peers
-
    }
-

-
    /// Get the current inventory.
-
    pub fn inventory(&self) -> Result<Inventory, storage::Error> {
-
        self.context.storage.inventory()
-
    }
-

-
    /// Get the storage instance.
-
    pub fn storage(&self) -> &T {
-
        &self.context.storage
-
    }
-

-
    /// Get the mutable storage instance.
-
    pub fn storage_mut(&mut self) -> &mut T {
-
        &mut self.context.storage
-
    }
-

-
    /// Get a project from storage, using the local node's key.
-
    pub fn get(&self, proj: &Id) -> Result<Option<Project>, storage::Error> {
-
        self.storage.get(&self.node_id(), proj)
-
    }
-

-
    /// Get the local signer.
-
    pub fn signer(&self) -> &G {
-
        &self.context.signer
-
    }
-

-
    /// Get the local service time.
-
    pub fn local_time(&self) -> LocalTime {
-
        self.context.clock.local_time()
-
    }
-

-
    /// Get service configuration.
-
    pub fn config(&self) -> &Config {
-
        &self.context.config
-
    }
-

-
    /// Get reference to routing table.
-
    pub fn routing(&self) -> &Routing {
-
        &self.context.routing
-
    }
-

-
    /// Get I/O outbox.
-
    pub fn outbox(&mut self) -> &mut VecDeque<Io> {
-
        &mut self.context.io
-
    }
-

-
    pub fn lookup(&self, id: &Id) -> Lookup {
-
        Lookup {
-
            local: self.context.storage.get(&self.node_id(), id).unwrap(),
-
            remote: self
-
                .context
-
                .routing
-
                .get(id)
-
                .map_or(vec![], |r| r.iter().cloned().collect()),
-
        }
-
    }
-

-
    pub fn initialize(&mut self, time: LocalTime) {
-
        trace!("Init {}", time.as_secs());
-

-
        self.start_time = time;
-

-
        // Connect to configured peers.
-
        let addrs = self.context.config.connect.clone();
-
        for addr in addrs {
-
            self.context.connect(addr);
-
        }
-
    }
-

-
    pub fn tick(&mut self, now: nakamoto::LocalTime) {
-
        trace!("Tick +{}", now - self.start_time);
-

-
        self.context.clock.set(now);
-
    }
-

-
    pub fn wake(&mut self) {
-
        let now = self.context.clock.local_time();
-

-
        trace!("Wake +{}", now - self.start_time);
-

-
        if now - self.last_idle >= IDLE_INTERVAL {
-
            debug!("Running 'idle' task...");
-

-
            self.maintain_connections();
-
            self.context.io.push_back(Io::Wakeup(IDLE_INTERVAL));
-
            self.last_idle = now;
-
        }
-
        if now - self.last_sync >= SYNC_INTERVAL {
-
            debug!("Running 'sync' task...");
-

-
            // TODO: What do we do here?
-
            self.context.io.push_back(Io::Wakeup(SYNC_INTERVAL));
-
            self.last_sync = now;
-
        }
-
        if now - self.last_announce >= ANNOUNCE_INTERVAL {
-
            if self.out_of_sync {
-
                self.announce_inventory().unwrap();
-
            }
-
            self.context.io.push_back(Io::Wakeup(ANNOUNCE_INTERVAL));
-
            self.last_announce = now;
-
        }
-
        if now - self.last_prune >= PRUNE_INTERVAL {
-
            debug!("Running 'prune' task...");
-

-
            self.prune_routing_entries();
-
            self.context.io.push_back(Io::Wakeup(PRUNE_INTERVAL));
-
            self.last_prune = now;
-
        }
-
    }
-

-
    pub fn command(&mut self, cmd: Command) {
-
        debug!("Command {:?}", cmd);
-

-
        match cmd {
-
            Command::Connect(addr) => self.context.connect(addr),
-
            Command::Fetch(id, resp) => {
-
                if !self.config.is_tracking(&id) {
-
                    resp.send(FetchLookup::NotTracking).ok();
-
                    return;
-
                }
-

-
                let seeds = self.seeds(&id).collect::<Vec<_>>();
-
                let seeds = if let Some(seeds) = NonEmpty::from_vec(seeds) {
-
                    seeds
-
                } else {
-
                    log::error!("No seeds found for {}", id);
-
                    resp.send(FetchLookup::NotFound).ok();
-

-
                    return;
-
                };
-
                log::debug!("Found {} seeds for {}", seeds.len(), id);
-

-
                let mut repo = match self.storage.repository(&id) {
-
                    Ok(repo) => repo,
-
                    Err(err) => {
-
                        log::error!("Error opening repo for {}: {}", id, err);
-
                        resp.send(FetchLookup::Error(err.into())).ok();
-

-
                        return;
-
                    }
-
                };
-

-
                let (results_, results) = chan::bounded(seeds.len());
-
                resp.send(FetchLookup::Found {
-
                    seeds: seeds.clone().map(|(_, peer)| peer.addr),
-
                    results,
-
                })
-
                .ok();
-

-
                // TODO: Limit the number of seeds we fetch from? Randomize?
-
                for (_, peer) in seeds {
-
                    match repo.fetch(&Url {
-
                        scheme: git_url::Scheme::Git,
-
                        host: Some(peer.addr.ip().to_string()),
-
                        port: Some(peer.addr.port()),
-
                        // TODO: Fix upstream crate so that it adds a `/` when needed.
-
                        path: format!("/{}", id).into(),
-
                        ..Url::default()
-
                    }) {
-
                        Ok(updated) => {
-
                            results_
-
                                .send(FetchResult::Fetched {
-
                                    from: peer.addr,
-
                                    updated,
-
                                })
-
                                .ok();
-
                        }
-
                        Err(err) => {
-
                            results_
-
                                .send(FetchResult::Error {
-
                                    from: peer.addr,
-
                                    error: err.into(),
-
                                })
-
                                .ok();
-
                        }
-
                    }
-
                }
-
            }
-
            Command::Track(id, resp) => {
-
                resp.send(self.track(id)).ok();
-
            }
-
            Command::Untrack(id, resp) => {
-
                resp.send(self.untrack(id)).ok();
-
            }
-
            Command::AnnounceRefs(id) => {
-
                let node = self.node_id();
-
                let repo = self.storage.repository(&id).unwrap();
-
                let remote = repo.remote(&node).unwrap();
-
                let peers = self.peers.negotiated().map(|(_, p)| p);
-
                let refs = remote.refs.into();
-
                let message = RefsAnnouncement { id, refs };
-
                let signature = message.sign(&self.signer);
-

-
                self.context.broadcast(
-
                    Message::RefsAnnouncement {
-
                        node,
-
                        message,
-
                        signature,
-
                    },
-
                    peers,
-
                );
-
            }
-
        }
-
    }
-

-
    pub fn attempted(&mut self, addr: &std::net::SocketAddr) {
-
        let ip = addr.ip();
-
        let persistent = self.context.config.is_persistent(addr);
-
        let peer = self
-
            .peers
-
            .entry(ip)
-
            .or_insert_with(|| Peer::new(*addr, Link::Outbound, persistent));
-

-
        peer.attempted();
-
    }
-

-
    pub fn connected(
-
        &mut self,
-
        addr: std::net::SocketAddr,
-
        _local_addr: &std::net::SocketAddr,
-
        link: Link,
-
    ) {
-
        let ip = addr.ip();
-

-
        debug!("Connected to {} ({:?})", ip, link);
-

-
        // For outbound connections, we are the first to say "Hello".
-
        // For inbound connections, we wait for the remote to say "Hello" first.
-
        // TODO: How should we deal with multiple peers connecting from the same IP address?
-
        if link.is_outbound() {
-
            // TODO: Refactor this so that we don't create messages if the peer isn't found.
-
            let messages = self.handshake_messages();
-

-
            if let Some(peer) = self.peers.get_mut(&ip) {
-
                self.context.write_all(peer.addr, messages);
-
                peer.connected();
-
            }
-
        } else {
-
            self.peers.insert(
-
                ip,
-
                Peer::new(
-
                    addr,
-
                    Link::Inbound,
-
                    self.context.config.is_persistent(&addr),
-
                ),
-
            );
-
        }
-
    }
-

-
    pub fn disconnected(
-
        &mut self,
-
        addr: &std::net::SocketAddr,
-
        reason: nakamoto::DisconnectReason<DisconnectReason>,
-
    ) {
-
        let since = self.local_time();
-
        let ip = addr.ip();
-

-
        debug!("Disconnected from {} ({})", ip, reason);
-

-
        if let Some(peer) = self.peers.get_mut(&ip) {
-
            peer.state = PeerState::Disconnected { since };
-

-
            // Attempt to re-connect to persistent peers.
-
            if self.context.config.is_persistent(addr) && peer.attempts() < MAX_CONNECTION_ATTEMPTS
-
            {
-
                if reason.is_dial_err() {
-
                    return;
-
                }
-
                if let nakamoto::DisconnectReason::Protocol(r) = reason {
-
                    if !r.is_transient() {
-
                        return;
-
                    }
-
                }
-
                // TODO: Eventually we want a delay before attempting a reconnection,
-
                // with exponential back-off.
-
                debug!("Reconnecting to {} (attempts={})...", ip, peer.attempts());
-

-
                // TODO: Try to reconnect only if the peer was attempted. A disconnect without
-
                // even a successful attempt means that we're unlikely to be able to reconnect.
-

-
                self.context.connect(*addr);
-
            } else {
-
                // TODO: Non-persistent peers should be removed from the
-
                // map here or at some later point.
-
            }
-
        }
-
    }
-

-
    pub fn received_message(&mut self, addr: &std::net::SocketAddr, msg: Envelope) {
-
        let peer_ip = addr.ip();
-
        let peer = if let Some(peer) = self.peers.get_mut(&peer_ip) {
-
            peer
-
        } else {
-
            return;
-
        };
-

-
        let relay = match peer.received(msg, &mut self.context) {
-
            Ok(msg) => msg,
-
            Err(err) => {
-
                self.context
-
                    .disconnect(peer.addr, DisconnectReason::Error(err));
-
                // If there's an error, stop processing messages from this peer.
-
                // However, we still relay messages returned up to this point.
-
                //
-
                // FIXME: The peer should be set in a state such that we don'that
-
                // process further messages.
-
                return;
-
            }
-
        };
-

-
        if let Some(msg) = relay {
-
            let negotiated = self
-
                .peers
-
                .negotiated()
-
                .filter(|(ip, _)| **ip != peer_ip)
-
                .map(|(_, p)| p);
-

-
            self.context.relay(msg, negotiated.clone());
-
        }
-
    }
-

-
    ////////////////////////////////////////////////////////////////////////////
-
    // Periodic tasks
-
    ////////////////////////////////////////////////////////////////////////////
-

-
    /// Announce our inventory to all connected peers.
-
    fn announce_inventory(&mut self) -> Result<(), storage::Error> {
-
        let inv = Message::inventory(self.context.inventory_announcement()?, &self.context.signer);
-

-
        for addr in self.peers.negotiated().map(|(_, p)| p.addr) {
-
            self.context.write(addr, inv.clone());
-
        }
-
        Ok(())
-
    }
-

-
    fn prune_routing_entries(&mut self) {
-
        // TODO
-
    }
-

-
    fn maintain_connections(&mut self) {
-
        // TODO: Connect to all potential seeds.
-
        if self.peers.len() < TARGET_OUTBOUND_PEERS {
-
            let delta = TARGET_OUTBOUND_PEERS - self.peers.len();
-

-
            for _ in 0..delta {
-
                // TODO: Connect to random peer.
-
            }
-
        }
-
    }
-
}
-

-
impl<S, T, G> Deref for Service<S, T, G> {
-
    type Target = Context<S, T, G>;
-

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

-
impl<S, T, G> DerefMut for Service<S, T, G> {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.context
-
    }
-
}
-

-
#[derive(Debug, Clone)]
-
pub enum DisconnectReason {
-
    User,
-
    Error(PeerError),
-
}
-

-
impl DisconnectReason {
-
    fn is_transient(&self) -> bool {
-
        match self {
-
            Self::User => false,
-
            Self::Error(..) => false,
-
        }
-
    }
-
}
-

-
impl From<DisconnectReason> for nakamoto_net::DisconnectReason<DisconnectReason> {
-
    fn from(reason: DisconnectReason) -> Self {
-
        nakamoto_net::DisconnectReason::Protocol(reason)
-
    }
-
}
-

-
impl fmt::Display for DisconnectReason {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::User => write!(f, "user"),
-
            Self::Error(err) => write!(f, "error: {}", err),
-
        }
-
    }
-
}
-

-
impl<S, T, G> Iterator for Service<S, T, G> {
-
    type Item = Io;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.context.io.pop_front()
-
    }
-
}
-

-
/// Result of a project lookup.
-
#[derive(Debug)]
-
pub struct Lookup {
-
    /// Whether the project was found locally or not.
-
    pub local: Option<Project>,
-
    /// A list of remote peers on which the project is known to exist.
-
    pub remote: Vec<NodeId>,
-
}
-

-
/// Global service state used across peers.
-
#[derive(Debug)]
-
pub struct Context<S, T, G> {
-
    /// Service configuration.
-
    config: Config,
-
    /// Our cryptographic signer and key.
-
    signer: G,
-
    /// Tracks the location of projects.
-
    routing: Routing,
-
    /// Outgoing I/O queue.
-
    io: VecDeque<Io>,
-
    /// Clock. Tells the time.
-
    clock: RefClock,
-
    /// Project storage.
-
    storage: T,
-
    /// Peer address manager.
-
    addrmgr: AddressManager<S>,
-
    /// Source of entropy.
-
    rng: Rng,
-
}
-

-
impl<S, T, G> Context<S, T, G>
-
where
-
    T: storage::ReadStorage,
-
    G: crypto::Signer,
-
{
-
    pub(crate) fn node_id(&self) -> NodeId {
-
        *self.signer.public_key()
-
    }
-
}
-

-
impl<'r, S, T, G> Context<S, T, G>
-
where
-
    T: storage::WriteStorage<'r>,
-
    G: crypto::Signer,
-
{
-
    pub(crate) fn new(
-
        config: Config,
-
        clock: RefClock,
-
        storage: T,
-
        addrmgr: AddressManager<S>,
-
        signer: G,
-
        rng: Rng,
-
    ) -> Self {
-
        Self {
-
            config,
-
            signer,
-
            clock,
-
            routing: HashMap::with_hasher(rng.clone().into()),
-
            io: VecDeque::new(),
-
            storage,
-
            addrmgr,
-
            rng,
-
        }
-
    }
-

-
    fn node_announcement(&self) -> NodeAnnouncement {
-
        let timestamp = self.timestamp();
-
        let features = NodeFeatures::default();
-
        let alias = self.alias();
-
        let addresses = vec![]; // TODO
-

-
        NodeAnnouncement {
-
            features,
-
            timestamp,
-
            alias,
-
            addresses,
-
        }
-
    }
-

-
    fn inventory_announcement(&self) -> Result<InventoryAnnouncement, storage::Error> {
-
        let timestamp = self.timestamp();
-
        let inventory = self.storage.inventory()?;
-

-
        Ok(InventoryAnnouncement {
-
            inventory,
-
            timestamp,
-
        })
-
    }
-

-
    fn filter(&self) -> Filter {
-
        match &self.config.project_tracking {
-
            ProjectTracking::All { .. } => Filter::default(),
-
            ProjectTracking::Allowed(ids) => Filter::new(ids.iter()),
-
        }
-
    }
-

-
    fn handshake_messages(&self) -> [Message; 4] {
-
        let git = self.config.git_url.clone();
-
        [
-
            Message::init(
-
                self.node_id(),
-
                self.timestamp(),
-
                self.config.listen.clone(),
-
                git,
-
            ),
-
            Message::node(self.node_announcement(), &self.signer),
-
            Message::inventory(self.inventory_announcement().unwrap(), &self.signer),
-
            Message::subscribe(self.filter(), self.timestamp(), Timestamp::MAX),
-
        ]
-
    }
-

-
    fn alias(&self) -> [u8; 32] {
-
        let mut alias = [0u8; 32];
-

-
        alias[..9].copy_from_slice("anonymous".as_bytes());
-
        alias
-
    }
-

-
    /// Process a peer inventory announcement by updating our routing table.
-
    fn process_inventory(&mut self, inventory: &Inventory, from: NodeId, remote: &Url) {
-
        for proj_id in inventory {
-
            let inventory = self
-
                .routing
-
                .entry(proj_id.clone())
-
                .or_insert_with(|| HashSet::with_hasher(self.rng.clone().into()));
-

-
            // TODO: Fire an event on routing update.
-
            if inventory.insert(from) && self.config.is_tracking(proj_id) {
-
                self.fetch(proj_id, remote);
-
            }
-
        }
-
    }
-

-
    fn fetch(&mut self, proj_id: &Id, remote: &Url) -> Vec<RefUpdate> {
-
        let mut repo = self.storage.repository(proj_id).unwrap();
-
        let mut path = remote.path.clone();
-

-
        path.push(b'/');
-
        path.extend(proj_id.to_string().into_bytes());
-

-
        repo.fetch(&Url {
-
            path,
-
            ..remote.clone()
-
        })
-
        .unwrap()
-
    }
-

-
    /// Disconnect a peer.
-
    fn disconnect(&mut self, addr: net::SocketAddr, reason: DisconnectReason) {
-
        self.io.push_back(Io::Disconnect(addr, reason));
-
    }
-
}
-

-
impl<S, T, G> Context<S, T, G> {
-
    /// Get current local timestamp.
-
    pub(crate) fn timestamp(&self) -> Timestamp {
-
        self.clock.local_time().as_secs()
-
    }
-

-
    /// Connect to a peer.
-
    fn connect(&mut self, addr: net::SocketAddr) {
-
        // TODO: Make sure we don't try to connect more than once to the same address.
-
        self.io.push_back(Io::Connect(addr));
-
    }
-

-
    fn write_all(&mut self, remote: net::SocketAddr, msgs: impl IntoIterator<Item = Message>) {
-
        let envelopes = msgs
-
            .into_iter()
-
            .map(|msg| self.config.network.envelope(msg))
-
            .collect();
-
        self.io.push_back(Io::Write(remote, envelopes));
-
    }
-

-
    fn write(&mut self, remote: net::SocketAddr, msg: Message) {
-
        debug!("Write {:?} to {}", &msg, remote.ip());
-

-
        let envelope = self.config.network.envelope(msg);
-
        self.io.push_back(Io::Write(remote, vec![envelope]));
-
    }
-

-
    /// Broadcast a message to a list of peers.
-
    fn broadcast<'a>(&mut self, msg: Message, peers: impl IntoIterator<Item = &'a Peer>) {
-
        for peer in peers {
-
            self.write(peer.addr, msg.clone());
-
        }
-
    }
-

-
    /// Relay a message to interested peers.
-
    fn relay<'a>(&mut self, msg: Message, peers: impl IntoIterator<Item = &'a Peer>) {
-
        if let Message::RefsAnnouncement { message, .. } = &msg {
-
            let id = message.id.clone();
-
            let peers = peers.into_iter().filter(|p| {
-
                if let Some(subscribe) = &p.subscribe {
-
                    subscribe.filter.contains(&id)
-
                } else {
-
                    // If the peer did not send us a `subscribe` message, we don'the
-
                    // relay any messages to them.
-
                    false
-
                }
-
            });
-
            self.broadcast(msg, peers);
-
        } else {
-
            self.broadcast(msg, peers);
-
        }
-
    }
-
}
-

-
#[derive(Debug)]
-
/// Holds currently (or recently) connected peers.
-
pub struct Peers(AddressBook<IpAddr, Peer>);
-

-
impl Peers {
-
    pub fn new(rng: Rng) -> Self {
-
        Self(AddressBook::new(rng))
-
    }
-

-
    pub fn by_id(&self, id: &NodeId) -> Option<&Peer> {
-
        self.0.values().find(|p| {
-
            if let PeerState::Negotiated { id: _id, .. } = &p.state {
-
                _id == id
-
            } else {
-
                false
-
            }
-
        })
-
    }
-

-
    /// Iterator over fully negotiated peers.
-
    pub fn negotiated(&self) -> impl Iterator<Item = (&IpAddr, &Peer)> + Clone {
-
        self.0.iter().filter(move |(_, p)| p.is_negotiated())
-
    }
-
}
-

-
impl Deref for Peers {
-
    type Target = AddressBook<IpAddr, Peer>;
-

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

-
impl DerefMut for Peers {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.0
-
    }
-
}
deleted node/src/service/config.rs
@@ -1,128 +0,0 @@
-
use std::net;
-

-
use git_url::Url;
-

-
use crate::collections::HashSet;
-
use crate::git;
-
use crate::identity::{Id, PublicKey};
-
use crate::service::message::{Address, Envelope, Message};
-

-
/// Peer-to-peer network.
-
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
-
pub enum Network {
-
    #[default]
-
    Main,
-
    Test,
-
}
-

-
impl Network {
-
    pub fn magic(&self) -> u32 {
-
        match self {
-
            Self::Main => 0x819b43d9,
-
            Self::Test => 0x717ebaf8,
-
        }
-
    }
-

-
    pub fn envelope(&self, msg: Message) -> Envelope {
-
        Envelope {
-
            magic: self.magic(),
-
            msg,
-
        }
-
    }
-
}
-

-
/// Project tracking policy.
-
#[derive(Debug, Clone)]
-
pub enum ProjectTracking {
-
    /// Track all projects we come across.
-
    All { blocked: HashSet<Id> },
-
    /// Track a static list of projects.
-
    Allowed(HashSet<Id>),
-
}
-

-
impl Default for ProjectTracking {
-
    fn default() -> Self {
-
        Self::All {
-
            blocked: HashSet::default(),
-
        }
-
    }
-
}
-

-
/// Project remote tracking policy.
-
#[derive(Debug, Default, Clone)]
-
pub enum RemoteTracking {
-
    /// Only track remotes of project delegates.
-
    #[default]
-
    DelegatesOnly,
-
    /// Track all remotes.
-
    All { blocked: HashSet<PublicKey> },
-
    /// Track a specific list of users as well as the project delegates.
-
    Allowed(HashSet<PublicKey>),
-
}
-

-
/// Service configuration.
-
#[derive(Debug, Clone)]
-
pub struct Config {
-
    /// Peers to connect to on startup.
-
    /// Connections to these peers will be maintained.
-
    pub connect: Vec<net::SocketAddr>,
-
    /// Peer-to-peer network.
-
    pub network: Network,
-
    /// Project tracking policy.
-
    pub project_tracking: ProjectTracking,
-
    /// Project remote tracking policy.
-
    pub remote_tracking: RemoteTracking,
-
    /// Whether or not our node should relay inventories.
-
    pub relay: bool,
-
    /// List of addresses to listen on for protocol connections.
-
    pub listen: Vec<Address>,
-
    /// Our Git URL for fetching projects.
-
    pub git_url: Url,
-
}
-

-
impl Default for Config {
-
    fn default() -> Self {
-
        Self {
-
            connect: Vec::default(),
-
            network: Network::default(),
-
            project_tracking: ProjectTracking::default(),
-
            remote_tracking: RemoteTracking::default(),
-
            relay: true,
-
            listen: vec![],
-
            git_url: Url {
-
                scheme: git::url::Scheme::File,
-
                path: "/dev/null".to_owned().into(),
-
                ..Url::default()
-
            },
-
        }
-
    }
-
}
-

-
impl Config {
-
    pub fn is_persistent(&self, addr: &net::SocketAddr) -> bool {
-
        self.connect.contains(addr)
-
    }
-

-
    pub fn is_tracking(&self, id: &Id) -> bool {
-
        match &self.project_tracking {
-
            ProjectTracking::All { blocked } => !blocked.contains(id),
-
            ProjectTracking::Allowed(ids) => ids.contains(id),
-
        }
-
    }
-

-
    /// Track a project. Returns whether the policy was updated.
-
    pub fn track(&mut self, id: Id) -> bool {
-
        match &mut self.project_tracking {
-
            ProjectTracking::All { .. } => false,
-
            ProjectTracking::Allowed(ids) => ids.insert(id),
-
        }
-
    }
-

-
    /// Untrack a project. Returns whether the policy was updated.
-
    pub fn untrack(&mut self, id: Id) -> bool {
-
        match &mut self.project_tracking {
-
            ProjectTracking::All { blocked } => blocked.insert(id),
-
            ProjectTracking::Allowed(ids) => ids.remove(&id),
-
        }
-
    }
-
}
deleted node/src/service/filter.rs
@@ -1,56 +0,0 @@
-
use std::ops::{Deref, DerefMut};
-

-
pub use bloomy::BloomFilter;
-

-
use crate::identity::Id;
-

-
/// Size in bytes of subscription bloom filter.
-
pub const FILTER_SIZE: usize = 1024 * 16;
-
/// Number of hashes used for bloom filter.
-
pub const FILTER_HASHES: usize = 7;
-

-
/// Subscription filter.
-
///
-
/// The [`Default`] instance has all bits set to `1`, ie. it will match
-
/// everything.
-
///
-
/// Nb. This filter doesn't currently support inserting public keys.
-
#[derive(Clone, PartialEq, Eq, Debug)]
-
pub struct Filter(BloomFilter<Id>);
-

-
impl Default for Filter {
-
    fn default() -> Self {
-
        Self(BloomFilter::from(vec![0xff; FILTER_SIZE]))
-
    }
-
}
-

-
impl Filter {
-
    pub fn new<'a>(ids: impl IntoIterator<Item = &'a Id>) -> Self {
-
        let mut bloom = BloomFilter::with_size(FILTER_SIZE);
-

-
        for id in ids.into_iter() {
-
            bloom.insert(id);
-
        }
-
        Self(bloom)
-
    }
-
}
-

-
impl Deref for Filter {
-
    type Target = BloomFilter<Id>;
-

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

-
impl DerefMut for Filter {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.0
-
    }
-
}
-

-
impl From<BloomFilter<Id>> for Filter {
-
    fn from(bloom: BloomFilter<Id>) -> Self {
-
        Self(bloom)
-
    }
-
}
deleted node/src/service/message.rs
@@ -1,301 +0,0 @@
-
use std::{fmt, io, net};
-

-
use crate::crypto;
-
use crate::git;
-
use crate::identity::Id;
-
use crate::service::filter::Filter;
-
use crate::service::{NodeId, Timestamp, PROTOCOL_VERSION};
-
use crate::storage::refs::Refs;
-
use crate::wire;
-

-
/// Message envelope. All messages sent over the network are wrapped in this type.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Envelope {
-
    /// Network magic constant. Used to differentiate networks.
-
    pub magic: u32,
-
    /// The message payload.
-
    pub msg: Message,
-
}
-

-
/// Advertized node feature. Signals what services the node supports.
-
pub type NodeFeatures = [u8; 32];
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
// TODO: We should check the length and charset when deserializing.
-
pub struct Hostname(String);
-

-
/// Peer public protocol address.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum Address {
-
    Ipv4 {
-
        ip: net::Ipv4Addr,
-
        port: u16,
-
    },
-
    Ipv6 {
-
        ip: net::Ipv6Addr,
-
        port: u16,
-
    },
-
    Hostname {
-
        host: Hostname,
-
        port: u16,
-
    },
-
    /// Tor V3 onion address.
-
    Onion {
-
        key: crypto::PublicKey,
-
        port: u16,
-
        checksum: u16,
-
        version: u8,
-
    },
-
}
-

-
impl From<net::SocketAddr> for Address {
-
    fn from(other: net::SocketAddr) -> Self {
-
        let port = other.port();
-

-
        match other.ip() {
-
            net::IpAddr::V4(ip) => Self::Ipv4 { ip, port },
-
            net::IpAddr::V6(ip) => Self::Ipv6 { ip, port },
-
        }
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Subscribe {
-
    /// Subscribe to events matching this filter.
-
    pub filter: Filter,
-
    /// Request messages since this time.
-
    pub since: Timestamp,
-
    /// Request messages until this time.
-
    pub until: Timestamp,
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct NodeAnnouncement {
-
    /// Advertized features.
-
    pub features: NodeFeatures,
-
    /// Monotonic timestamp.
-
    pub timestamp: Timestamp,
-
    /// Non-unique alias. Must be valid UTF-8.
-
    pub alias: [u8; 32],
-
    /// Announced addresses.
-
    pub addresses: Vec<Address>,
-
}
-

-
impl NodeAnnouncement {
-
    /// Verify a signature on this message.
-
    pub fn verify(&self, signer: &NodeId, signature: &crypto::Signature) -> bool {
-
        let msg = wire::serialize(self);
-
        signer.verify(&msg, signature).is_ok()
-
    }
-
}
-

-
impl wire::Encode for NodeAnnouncement {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = 0;
-

-
        n += self.features.encode(writer)?;
-
        n += self.timestamp.encode(writer)?;
-
        n += self.alias.encode(writer)?;
-
        n += self.addresses.as_slice().encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for NodeAnnouncement {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let features = NodeFeatures::decode(reader)?;
-
        let timestamp = Timestamp::decode(reader)?;
-
        let alias = wire::Decode::decode(reader)?;
-
        let addresses = Vec::<Address>::decode(reader)?;
-

-
        Ok(Self {
-
            features,
-
            timestamp,
-
            alias,
-
            addresses,
-
        })
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct RefsAnnouncement {
-
    /// Repository identifier.
-
    pub id: Id,
-
    /// Updated refs.
-
    pub refs: Refs,
-
}
-

-
impl RefsAnnouncement {
-
    /// Verify a signature on this message.
-
    pub fn verify(&self, signer: &NodeId, signature: &crypto::Signature) -> bool {
-
        let msg = wire::serialize(self);
-
        signer.verify(&msg, signature).is_ok()
-
    }
-

-
    /// Sign this announcement.
-
    pub fn sign<S: crypto::Signer>(&self, signer: S) -> crypto::Signature {
-
        let msg = wire::serialize(self);
-
        signer.sign(&msg)
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct InventoryAnnouncement {
-
    pub inventory: Vec<Id>,
-
    pub timestamp: Timestamp,
-
}
-

-
impl InventoryAnnouncement {
-
    /// Verify a signature on this message.
-
    pub fn verify(&self, signer: NodeId, signature: &crypto::Signature) -> bool {
-
        let msg = wire::serialize(self);
-
        signer.verify(&msg, signature).is_ok()
-
    }
-
}
-

-
/// Message payload.
-
/// These are the messages peers send to each other.
-
#[derive(Clone, PartialEq, Eq)]
-
pub enum Message {
-
    /// The first message sent to a peer after connection.
-
    Initialize {
-
        // TODO: This is currently untrusted.
-
        id: NodeId,
-
        timestamp: Timestamp,
-
        version: u32,
-
        addrs: Vec<Address>,
-
        git: git::Url,
-
    },
-

-
    /// Subscribe to gossip messages matching the filter and time range.
-
    /// timestamp.
-
    Subscribe(Subscribe),
-

-
    /// Node announcing its inventory to the network.
-
    /// This should be the whole inventory every time.
-
    InventoryAnnouncement {
-
        /// Node identifier.
-
        node: NodeId,
-
        /// Unsigned node inventory.
-
        message: InventoryAnnouncement,
-
        /// Signature over the announcement.
-
        signature: crypto::Signature,
-
    },
-

-
    /// Node announcing itself to the network.
-
    NodeAnnouncement {
-
        /// Node identifier.
-
        node: NodeId,
-
        /// Unsigned node announcement.
-
        message: NodeAnnouncement,
-
        /// Signature over the announcement, by the node being announced.
-
        signature: crypto::Signature,
-
    },
-

-
    /// Node announcing project refs being created or updated.
-
    RefsAnnouncement {
-
        /// Node identifier.
-
        node: NodeId,
-
        /// Unsigned refs announcement.
-
        message: RefsAnnouncement,
-
        /// Signature over the announcement, by the node that updated the refs.
-
        signature: crypto::Signature,
-
    },
-
}
-

-
impl Message {
-
    pub fn init(id: NodeId, timestamp: Timestamp, addrs: Vec<Address>, git: git::Url) -> Self {
-
        Self::Initialize {
-
            id,
-
            timestamp,
-
            version: PROTOCOL_VERSION,
-
            addrs,
-
            git,
-
        }
-
    }
-

-
    pub fn node<S: crypto::Signer>(message: NodeAnnouncement, signer: S) -> Self {
-
        let msg = wire::serialize(&message);
-
        let signature = signer.sign(&msg);
-
        let node = *signer.public_key();
-

-
        Self::NodeAnnouncement {
-
            node,
-
            signature,
-
            message,
-
        }
-
    }
-

-
    pub fn inventory<S: crypto::Signer>(message: InventoryAnnouncement, signer: S) -> Self {
-
        let msg = wire::serialize(&message);
-
        let signature = signer.sign(&msg);
-
        let node = *signer.public_key();
-

-
        Self::InventoryAnnouncement {
-
            node,
-
            signature,
-
            message,
-
        }
-
    }
-

-
    pub fn subscribe(filter: Filter, since: Timestamp, until: Timestamp) -> Self {
-
        Self::Subscribe(Subscribe {
-
            filter,
-
            since,
-
            until,
-
        })
-
    }
-
}
-

-
impl fmt::Debug for Message {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::Initialize { id, .. } => write!(f, "Initialize({})", id),
-
            Self::Subscribe(Subscribe { since, until, .. }) => {
-
                write!(f, "Subscribe({}..{})", since, until)
-
            }
-

-
            Self::NodeAnnouncement { node, .. } => write!(f, "NodeAnnouncement({})", node),
-
            Self::InventoryAnnouncement { node, message, .. } => {
-
                write!(
-
                    f,
-
                    "InventoryAnnouncement({}, [{}], {})",
-
                    node,
-
                    message
-
                        .inventory
-
                        .iter()
-
                        .map(|i| i.to_string())
-
                        .collect::<Vec<String>>()
-
                        .join(", "),
-
                    message.timestamp
-
                )
-
            }
-
            Self::RefsAnnouncement { node, message, .. } => {
-
                write!(
-
                    f,
-
                    "RefsAnnouncement({}, {}, {:?})",
-
                    node, message.id, message.refs
-
                )
-
            }
-
        }
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    use crate::crypto::Signer;
-
    use crate::test::crypto::MockSigner;
-

-
    #[quickcheck]
-
    fn prop_refs_announcement_signing(id: Id, refs: Refs) {
-
        let signer = MockSigner::new(&mut fastrand::Rng::new());
-
        let message = RefsAnnouncement { id, refs };
-
        let signature = message.sign(&signer);
-

-
        assert!(message.verify(signer.public_key(), &signature));
-
    }
-
}
deleted node/src/service/peer.rs
@@ -1,244 +0,0 @@
-
use crate::service::message::*;
-
use crate::service::*;
-

-
#[derive(Debug, Default)]
-
#[allow(clippy::large_enum_variant)]
-
pub enum PeerState {
-
    /// Initial peer state. For outgoing peers this
-
    /// means we've attempted a connection. For incoming
-
    /// peers, this means they've successfully connected
-
    /// to us.
-
    #[default]
-
    Initial,
-
    /// State after successful handshake.
-
    Negotiated {
-
        /// The peer's unique identifier.
-
        id: NodeId,
-
        since: LocalTime,
-
        /// Addresses this peer is reachable on.
-
        addrs: Vec<Address>,
-
        git: Url,
-
    },
-
    /// When a peer is disconnected.
-
    Disconnected { since: LocalTime },
-
}
-

-
#[derive(thiserror::Error, Debug, Clone)]
-
pub enum PeerError {
-
    #[error("wrong network constant in message: {0}")]
-
    WrongMagic(u32),
-
    #[error("wrong protocol version in message: {0}")]
-
    WrongVersion(u32),
-
    #[error("invalid inventory timestamp: {0}")]
-
    InvalidTimestamp(u64),
-
    #[error("peer misbehaved")]
-
    Misbehavior,
-
}
-

-
#[derive(Debug)]
-
pub struct Peer {
-
    /// Peer address.
-
    pub addr: net::SocketAddr,
-
    /// Connection direction.
-
    pub link: Link,
-
    /// Whether we should attempt to re-connect
-
    /// to this peer upon disconnection.
-
    pub persistent: bool,
-
    /// Peer connection state.
-
    pub state: PeerState,
-
    /// Last known peer time.
-
    pub timestamp: Timestamp,
-
    /// Peer subscription.
-
    pub subscribe: Option<Subscribe>,
-

-
    /// Connection attempts. For persistent peers, Tracks
-
    /// how many times we've attempted to connect. We reset this to zero
-
    /// upon successful connection.
-
    attempts: usize,
-
}
-

-
impl Peer {
-
    pub fn new(addr: net::SocketAddr, link: Link, persistent: bool) -> Self {
-
        Self {
-
            addr,
-
            state: PeerState::default(),
-
            link,
-
            timestamp: Timestamp::default(),
-
            subscribe: None,
-
            persistent,
-
            attempts: 0,
-
        }
-
    }
-

-
    pub fn ip(&self) -> IpAddr {
-
        self.addr.ip()
-
    }
-

-
    pub fn is_negotiated(&self) -> bool {
-
        matches!(self.state, PeerState::Negotiated { .. })
-
    }
-

-
    pub fn attempts(&self) -> usize {
-
        self.attempts
-
    }
-

-
    pub fn attempted(&mut self) {
-
        self.attempts += 1;
-
    }
-

-
    pub fn connected(&mut self) {
-
        self.attempts = 0;
-
    }
-

-
    pub fn received<'r, S, T, G>(
-
        &mut self,
-
        envelope: Envelope,
-
        ctx: &mut Context<S, T, G>,
-
    ) -> Result<Option<Message>, PeerError>
-
    where
-
        T: storage::WriteStorage<'r>,
-
        G: crypto::Signer,
-
    {
-
        if envelope.magic != ctx.config.network.magic() {
-
            return Err(PeerError::WrongMagic(envelope.magic));
-
        }
-
        debug!("Received {:?} from {}", &envelope.msg, self.ip());
-

-
        match (&self.state, envelope.msg) {
-
            (
-
                PeerState::Initial,
-
                Message::Initialize {
-
                    id,
-
                    timestamp,
-
                    version,
-
                    addrs,
-
                    git,
-
                },
-
            ) => {
-
                let now = ctx.timestamp();
-

-
                if timestamp.abs_diff(now) > MAX_TIME_DELTA.as_secs() {
-
                    return Err(PeerError::InvalidTimestamp(timestamp));
-
                }
-
                if version != PROTOCOL_VERSION {
-
                    return Err(PeerError::WrongVersion(version));
-
                }
-
                // Nb. This is a very primitive handshake. Eventually we should have anyhow
-
                // extra "acknowledgment" message sent when the `Initialize` is well received.
-
                if self.link.is_inbound() {
-
                    ctx.write_all(self.addr, ctx.handshake_messages());
-
                }
-
                // Nb. we don't set the peer timestamp here, since it is going to be
-
                // set after the first message is received only. Setting it here would
-
                // mean that messages received right after the handshake could be ignored.
-
                self.state = PeerState::Negotiated {
-
                    id,
-
                    since: ctx.clock.local_time(),
-
                    addrs,
-
                    git,
-
                };
-
            }
-
            (PeerState::Initial, _) => {
-
                debug!(
-
                    "Disconnecting peer {} for sending us a message before handshake",
-
                    self.ip()
-
                );
-
                return Err(PeerError::Misbehavior);
-
            }
-
            (
-
                PeerState::Negotiated { git, .. },
-
                Message::InventoryAnnouncement {
-
                    node,
-
                    message,
-
                    signature,
-
                },
-
            ) => {
-
                let now = ctx.clock.local_time();
-
                let last = self.timestamp;
-

-
                // Don't allow messages from too far in the past or future.
-
                if message.timestamp.abs_diff(now.as_secs()) > MAX_TIME_DELTA.as_secs() {
-
                    return Err(PeerError::InvalidTimestamp(message.timestamp));
-
                }
-
                // Discard inventory messages we've already seen, otherwise update
-
                // out last seen time.
-
                if message.timestamp > last {
-
                    self.timestamp = message.timestamp;
-
                } else {
-
                    return Ok(None);
-
                }
-
                ctx.process_inventory(&message.inventory, node, git);
-

-
                if ctx.config.relay {
-
                    return Ok(Some(Message::InventoryAnnouncement {
-
                        node,
-
                        message,
-
                        signature,
-
                    }));
-
                }
-
            }
-
            // Process a peer inventory update announcement by (maybe) fetching.
-
            (
-
                PeerState::Negotiated { git, .. },
-
                Message::RefsAnnouncement {
-
                    node,
-
                    message,
-
                    signature,
-
                },
-
            ) => {
-
                if message.verify(&node, &signature) {
-
                    // TODO: Buffer/throttle fetches.
-
                    // TODO: Check that we're tracking this user as well.
-
                    if ctx.config.is_tracking(&message.id) {
-
                        // TODO: Check refs to see if we should try to fetch or not.
-
                        let updated_refs = ctx.fetch(&message.id, git);
-
                        let is_updated = !updated_refs.is_empty();
-

-
                        ctx.io.push_back(Io::Event(Event::RefsFetched {
-
                            from: git.clone(),
-
                            project: message.id.clone(),
-
                            updated: updated_refs,
-
                        }));
-

-
                        if is_updated {
-
                            return Ok(Some(Message::RefsAnnouncement {
-
                                node,
-
                                message,
-
                                signature,
-
                            }));
-
                        }
-
                    }
-
                } else {
-
                    return Err(PeerError::Misbehavior);
-
                }
-
            }
-
            (
-
                PeerState::Negotiated { .. },
-
                Message::NodeAnnouncement {
-
                    node,
-
                    message,
-
                    signature,
-
                },
-
            ) => {
-
                if !message.verify(&node, &signature) {
-
                    return Err(PeerError::Misbehavior);
-
                }
-
                log::warn!("Node announcement handling is not implemented");
-
            }
-
            (PeerState::Negotiated { .. }, Message::Subscribe(subscribe)) => {
-
                self.subscribe = Some(subscribe);
-
            }
-
            (PeerState::Negotiated { .. }, Message::Initialize { .. }) => {
-
                debug!(
-
                    "Disconnecting peer {} for sending us a redundant handshake message",
-
                    self.ip()
-
                );
-
                return Err(PeerError::Misbehavior);
-
            }
-
            (PeerState::Disconnected { .. }, msg) => {
-
                debug!("Ignoring {:?} from disconnected peer {}", msg, self.ip());
-
            }
-
        }
-
        Ok(None)
-
    }
-
}
deleted node/src/storage.rs
@@ -1,306 +0,0 @@
-
pub mod git;
-
pub mod refs;
-

-
use std::collections::hash_map;
-
use std::marker::PhantomData;
-
use std::ops::Deref;
-
use std::path::Path;
-
use std::{fmt, io};
-

-
use thiserror::Error;
-

-
pub use radicle_git_ext::Oid;
-

-
use crate::collections::HashMap;
-
use crate::crypto;
-
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
-
use crate::git::ext as git_ext;
-
use crate::git::Url;
-
use crate::git::{RefError, RefStr, RefString};
-
use crate::identity;
-
use crate::identity::{Id, IdError, Project};
-
use crate::storage::refs::Refs;
-

-
use self::refs::SignedRefs;
-

-
pub type BranchName = String;
-
pub type Inventory = Vec<Id>;
-

-
/// Storage error.
-
#[derive(Error, Debug)]
-
pub enum Error {
-
    #[error("invalid git reference")]
-
    InvalidRef,
-
    #[error("git reference error: {0}")]
-
    Ref(#[from] RefError),
-
    #[error(transparent)]
-
    Refs(#[from] refs::Error),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("id: {0}")]
-
    Id(#[from] IdError),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("doc: {0}")]
-
    Doc(#[from] identity::doc::Error),
-
    #[error("invalid repository head")]
-
    InvalidHead,
-
}
-

-
/// Fetch error.
-
#[derive(Error, Debug)]
-
#[allow(clippy::large_enum_variant)]
-
pub enum FetchError {
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("verify: {0}")]
-
    Verify(#[from] git::VerifyError),
-
}
-

-
pub type RemoteId = PublicKey;
-

-
/// An update to a reference.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum RefUpdate {
-
    Updated { name: RefString, old: Oid, new: Oid },
-
    Created { name: RefString, oid: Oid },
-
    Deleted { name: RefString, oid: Oid },
-
    Skipped { name: RefString, oid: Oid },
-
}
-

-
impl RefUpdate {
-
    pub fn from(name: RefString, old: impl Into<Oid>, new: impl Into<Oid>) -> Self {
-
        let old = old.into();
-
        let new = new.into();
-

-
        if old.is_zero() {
-
            Self::Created { name, oid: new }
-
        } else if new.is_zero() {
-
            Self::Deleted { name, oid: old }
-
        } else if old != new {
-
            Self::Updated { name, old, new }
-
        } else {
-
            Self::Skipped { name, oid: old }
-
        }
-
    }
-
}
-

-
impl fmt::Display for RefUpdate {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match self {
-
            Self::Updated { name, old, new } => {
-
                write!(f, "~ {:.7}..{:.7} {}", old, new, name)
-
            }
-
            Self::Created { name, oid } => {
-
                write!(f, "* 0000000..{:.7} {}", oid, name)
-
            }
-
            Self::Deleted { name, oid } => {
-
                write!(f, "- {:.7}..0000000 {}", oid, name)
-
            }
-
            Self::Skipped { name, oid } => {
-
                write!(f, "= {:.7}..{:.7} {}", oid, oid, name)
-
            }
-
        }
-
    }
-
}
-

-
/// Project remotes. Tracks the git state of a project.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
-

-
impl<V> FromIterator<(RemoteId, Remote<V>)> for Remotes<V> {
-
    fn from_iter<T: IntoIterator<Item = (RemoteId, Remote<V>)>>(iter: T) -> Self {
-
        Self(iter.into_iter().collect())
-
    }
-
}
-

-
impl<V> Deref for Remotes<V> {
-
    type Target = HashMap<RemoteId, Remote<V>>;
-

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

-
impl<V> Remotes<V> {
-
    pub fn new(remotes: HashMap<RemoteId, Remote<V>>) -> Self {
-
        Self(remotes)
-
    }
-
}
-

-
impl Remotes<Verified> {
-
    pub fn unverified(self) -> Remotes<Unverified> {
-
        Remotes(
-
            self.into_iter()
-
                .map(|(id, r)| (id, r.unverified()))
-
                .collect(),
-
        )
-
    }
-
}
-

-
impl<V> Default for Remotes<V> {
-
    fn default() -> Self {
-
        Self(HashMap::default())
-
    }
-
}
-

-
impl<V> IntoIterator for Remotes<V> {
-
    type Item = (RemoteId, Remote<V>);
-
    type IntoIter = hash_map::IntoIter<RemoteId, Remote<V>>;
-

-
    fn into_iter(self) -> Self::IntoIter {
-
        self.0.into_iter()
-
    }
-
}
-

-
impl<V> From<Remotes<V>> for HashMap<RemoteId, Refs> {
-
    fn from(other: Remotes<V>) -> Self {
-
        let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into());
-

-
        for (k, v) in other.into_iter() {
-
            remotes.insert(k, v.refs.into());
-
        }
-
        remotes
-
    }
-
}
-

-
/// A project remote.
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct Remote<V> {
-
    /// ID of remote.
-
    pub id: PublicKey,
-
    /// Git references published under this remote, and their hashes.
-
    pub refs: SignedRefs<V>,
-
    /// Whether this remote is of a project delegate.
-
    pub delegate: bool,
-
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
-
    verified: PhantomData<V>,
-
}
-

-
impl<V> Remote<V> {
-
    pub fn new(id: PublicKey, refs: impl Into<SignedRefs<V>>) -> Self {
-
        Self {
-
            id,
-
            refs: refs.into(),
-
            delegate: false,
-
            verified: PhantomData,
-
        }
-
    }
-
}
-

-
impl Remote<Unverified> {
-
    pub fn verified(self) -> Result<Remote<Verified>, crypto::Error> {
-
        let refs = self.refs.verified(&self.id)?;
-

-
        Ok(Remote {
-
            id: self.id,
-
            refs,
-
            delegate: self.delegate,
-
            verified: PhantomData,
-
        })
-
    }
-
}
-

-
impl Remote<Verified> {
-
    pub fn unverified(self) -> Remote<Unverified> {
-
        Remote {
-
            id: self.id,
-
            refs: self.refs.unverified(),
-
            delegate: self.delegate,
-
            verified: PhantomData,
-
        }
-
    }
-
}
-

-
pub trait ReadStorage {
-
    fn url(&self) -> Url;
-
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error>;
-
    fn inventory(&self) -> Result<Inventory, Error>;
-
}
-

-
pub trait WriteStorage<'r>: ReadStorage {
-
    type Repository: WriteRepository<'r>;
-

-
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error>;
-
    fn sign_refs<G: Signer>(
-
        &self,
-
        repository: &Self::Repository,
-
        signer: G,
-
    ) -> Result<SignedRefs<Verified>, Error>;
-
}
-

-
pub trait ReadRepository<'r> {
-
    type Remotes: Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r;
-

-
    fn is_empty(&self) -> Result<bool, git2::Error>;
-
    fn path(&self) -> &Path;
-
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
-
    fn reference(
-
        &self,
-
        remote: &RemoteId,
-
        reference: &RefStr,
-
    ) -> Result<Option<git2::Reference>, git2::Error>;
-
    fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error>;
-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
-
    fn reference_oid(
-
        &self,
-
        remote: &RemoteId,
-
        reference: &RefStr,
-
    ) -> Result<Option<Oid>, git2::Error>;
-
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
-
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
-
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error>;
-
    /// Return the project associated with this repository.
-
    fn project(&self) -> Result<Project, Error>;
-
}
-

-
pub trait WriteRepository<'r>: ReadRepository<'r> {
-
    fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
-
    fn raw(&self) -> &git2::Repository;
-
}
-

-
impl<T, S> ReadStorage for T
-
where
-
    T: Deref<Target = S>,
-
    S: ReadStorage + 'static,
-
{
-
    fn url(&self) -> Url {
-
        self.deref().url()
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        self.deref().inventory()
-
    }
-

-
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
-
        self.deref().get(remote, proj)
-
    }
-
}
-

-
impl<'r, T, S> WriteStorage<'r> for T
-
where
-
    T: Deref<Target = S>,
-
    S: WriteStorage<'r> + 'static,
-
{
-
    type Repository = S::Repository;
-

-
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
-
        self.deref().repository(proj)
-
    }
-

-
    fn sign_refs<G: Signer>(
-
        &self,
-
        repository: &S::Repository,
-
        signer: G,
-
    ) -> Result<SignedRefs<Verified>, Error> {
-
        self.deref().sign_refs(repository, signer)
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    #[test]
-
    fn test_storage() {}
-
}
deleted node/src/storage/git.rs
@@ -1,759 +0,0 @@
-
use std::collections::{BTreeMap, HashMap};
-
use std::path::{Path, PathBuf};
-
use std::{fmt, fs, io};
-

-
use git_ref_format::refspec;
-
use once_cell::sync::Lazy;
-

-
pub use radicle_git_ext::Oid;
-

-
use crate::crypto::{Signer, Unverified, Verified};
-
use crate::git;
-
use crate::identity::{self, Doc};
-
use crate::identity::{Id, Project};
-
use crate::storage::refs;
-
use crate::storage::refs::{Refs, SignedRefs};
-
use crate::storage::{
-
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, WriteRepository,
-
    WriteStorage,
-
};
-

-
use super::{RefUpdate, RemoteId};
-

-
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/remotes/*"));
-
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
-
    Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
-

-
#[derive(Error, Debug)]
-
pub enum IdentityError {
-
    #[error("identity branches diverge from each other")]
-
    BranchesDiverge,
-
    #[error("identity branches are in an invalid state")]
-
    InvalidState,
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
    #[error("git: {0}")]
-
    GitExt(#[from] git::Error),
-
    #[error("refs: {0}")]
-
    Refs(#[from] refs::Error),
-
}
-

-
pub struct Storage {
-
    path: PathBuf,
-
}
-

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

-
impl ReadStorage for Storage {
-
    fn url(&self) -> git::Url {
-
        git::Url {
-
            scheme: git_url::Scheme::File,
-
            host: None,
-
            path: self.path.to_string_lossy().to_string().into(),
-
            ..git::Url::default()
-
        }
-
    }
-

-
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
-
        // TODO: Don't create a repo here if it doesn't exist?
-
        // Perhaps for checking we could have a `contains` method?
-
        let repo = self.repository(proj)?;
-

-
        if let Some(doc) = repo.identity_of(remote)? {
-
            let remotes = repo.remotes()?.collect::<Result<_, _>>()?;
-
            let path = repo.path().to_path_buf();
-

-
            // TODO: We should check that there is at least one remote, which is
-
            // the one of the local user, otherwise it means the project is in
-
            // an corrupted state.
-

-
            Ok(Some(Project {
-
                id: proj.clone(),
-
                doc,
-
                remotes,
-
                path,
-
            }))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        self.projects()
-
    }
-
}
-

-
impl<'r> WriteStorage<'r> for Storage {
-
    type Repository = Repository;
-

-
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
-
        Repository::open(self.path.join(proj.to_string()))
-
    }
-

-
    fn sign_refs<G: Signer>(
-
        &self,
-
        repository: &Repository,
-
        signer: G,
-
    ) -> Result<SignedRefs<Verified>, Error> {
-
        let remote = signer.public_key();
-
        let refs = repository.references(remote)?;
-
        let signed = refs.signed(&signer)?;
-

-
        signed.save(remote, repository)?;
-

-
        Ok(signed)
-
    }
-
}
-

-
impl Storage {
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
-
        let path = path.as_ref().to_path_buf();
-

-
        match fs::create_dir_all(&path) {
-
            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
-
            Err(err) => return Err(err),
-
            Ok(()) => {}
-
        }
-

-
        Ok(Self { path })
-
    }
-

-
    pub fn path(&self) -> &Path {
-
        self.path.as_path()
-
    }
-

-
    pub fn projects(&self) -> Result<Vec<Id>, Error> {
-
        let mut projects = Vec::new();
-

-
        for result in fs::read_dir(&self.path)? {
-
            let path = result?;
-
            let id = Id::try_from(path.file_name())?;
-

-
            projects.push(id);
-
        }
-
        Ok(projects)
-
    }
-

-
    pub fn inspect(&self) -> Result<(), Error> {
-
        for proj in self.projects()? {
-
            let repo = self.repository(&proj)?;
-

-
            for r in repo.raw().references()? {
-
                let r = r?;
-
                let name = r.name().ok_or(Error::InvalidRef)?;
-
                let oid = r.target().ok_or(Error::InvalidRef)?;
-

-
                println!("{} {} {}", proj, oid, name);
-
            }
-
        }
-
        Ok(())
-
    }
-
}
-

-
pub struct Repository {
-
    pub(crate) backend: git2::Repository,
-
    // TODO: Add project id here so we can refer to it
-
    // in a bunch of places. We could write it to the
-
    // git config for later.
-
}
-

-
#[derive(Debug, Error)]
-
pub enum VerifyError {
-
    #[error("invalid remote `{0}`")]
-
    InvalidRemote(RemoteId),
-
    #[error("invalid target `{2}` for reference `{1}` of remote `{0}`")]
-
    InvalidRefTarget(RemoteId, git::RefString, git2::Oid),
-
    #[error("invalid reference")]
-
    InvalidRef,
-
    #[error("ref error: {0}")]
-
    Ref(#[from] git::RefError),
-
    #[error("refs error: {0}")]
-
    Refs(#[from] refs::Error),
-
    #[error("unknown reference `{1}` in remote `{0}`")]
-
    UnknownRef(RemoteId, git::RefString),
-
    #[error("missing reference `{1}` in remote `{0}`")]
-
    MissingRef(RemoteId, git::RefString),
-
    #[error("git: {0}")]
-
    Git(#[from] git2::Error),
-
}
-

-
impl Repository {
-
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-
        let backend = match git2::Repository::open_bare(path.as_ref()) {
-
            Err(e) if git::ext::is_not_found_err(&e) => {
-
                let backend = git2::Repository::init_opts(
-
                    path,
-
                    git2::RepositoryInitOptions::new()
-
                        .bare(true)
-
                        .no_reinit(true)
-
                        .external_template(false),
-
                )?;
-
                let mut config = backend.config()?;
-

-
                // TODO: Get ahold of user name and/or key.
-
                config.set_str("user.name", "radicle")?;
-
                config.set_str("user.email", "radicle@localhost")?;
-

-
                Ok(backend)
-
            }
-
            Ok(repo) => Ok(repo),
-
            Err(e) => Err(e),
-
        }?;
-

-
        Ok(Self { backend })
-
    }
-

-
    pub fn head(&self) -> Result<git2::Commit, git2::Error> {
-
        // TODO: Find longest history, get document and get head.
-
        // Perhaps we should even set a local `HEAD` or at least `refs/heads/master`
-
        todo!();
-
    }
-

-
    pub fn verify(&self) -> Result<(), VerifyError> {
-
        let mut remotes: HashMap<RemoteId, Refs> = self
-
            .remotes()?
-
            .map(|remote| {
-
                let (id, remote) = remote?;
-
                Ok((id, remote.refs.into()))
-
            })
-
            .collect::<Result<_, VerifyError>>()?;
-

-
        for r in self.backend.references()? {
-
            let r = r?;
-
            let name = r.name().ok_or(VerifyError::InvalidRef)?;
-
            let oid = r.target().ok_or(VerifyError::InvalidRef)?;
-
            let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
-

-
            if refname == *refs::SIGNATURE_REF {
-
                continue;
-
            }
-
            let remote = remotes
-
                .get_mut(&remote_id)
-
                .ok_or(VerifyError::InvalidRemote(remote_id))?;
-
            let signed_oid = remote
-
                .remove(&refname)
-
                .ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
-

-
            if git::Oid::from(oid) != signed_oid {
-
                return Err(VerifyError::InvalidRefTarget(remote_id, refname, oid));
-
            }
-
        }
-

-
        // The refs that are left in the map, are ones that were signed, but are not
-
        // in the repository.
-
        for (id, refs) in remotes.into_iter() {
-
            if let Some((name, _)) = refs.into_iter().next() {
-
                return Err(VerifyError::MissingRef(id, name));
-
            }
-
        }
-

-
        Ok(())
-
    }
-

-
    pub fn inspect(&self) -> Result<(), Error> {
-
        for r in self.backend.references()? {
-
            let r = r?;
-
            let name = r.name().ok_or(Error::InvalidRef)?;
-
            let oid = r.target().ok_or(Error::InvalidRef)?;
-

-
            println!("{} {}", oid, name);
-
        }
-
        Ok(())
-
    }
-

-
    pub fn identity_of(
-
        &self,
-
        remote: &RemoteId,
-
    ) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
-
        if let Some((doc, _)) = identity::Doc::load(remote, self)? {
-
            Ok(Some(doc.verified().unwrap()))
-
        } else {
-
            Ok(None)
-
        }
-
    }
-

-
    /// Return the canonical identity [`git::Oid`] and document.
-
    pub fn identity(&self) -> Result<(git::Oid, identity::Doc<Unverified>), IdentityError> {
-
        let mut heads = Vec::new();
-
        for remote in self.remote_ids()? {
-
            let remote = remote?;
-
            let oid = Doc::<Unverified>::head(&remote, self)?.unwrap();
-

-
            heads.push(oid.into());
-
        }
-
        // Keep track of the longest identity branch.
-
        let mut longest = heads.pop().ok_or(IdentityError::InvalidState)?;
-

-
        for head in &heads {
-
            let base = self.raw().merge_base(*head, longest)?;
-

-
            if base == longest {
-
                // `head` is a successor of `longest`. Update `longest`.
-
                //
-
                //   o head
-
                //   |
-
                //   o longest (base)
-
                //   |
-
                //
-
                longest = *head;
-
            } else if base == *head || *head == longest {
-
                // `head` is an ancestor of `longest`, or equal to it. Do nothing.
-
                //
-
                //   o longest             o longest, head (base)
-
                //   |                     |
-
                //   o head (base)   OR    o
-
                //   |                     |
-
                //
-
            } else {
-
                // The merge base between `head` and `longest` (`base`)
-
                // is neither `head` nor `longest`. Therefore, the branches have
-
                // diverged.
-
                //
-
                //    longest   head
-
                //           \ /
-
                //            o (base)
-
                //            |
-
                //
-
                return Err(IdentityError::BranchesDiverge);
-
            }
-
        }
-

-
        Doc::load_at(longest.into(), self)?
-
            .ok_or(refs::Error::NotFound)
-
            .map(|(doc, _)| (longest.into(), doc))
-
            .map_err(IdentityError::from)
-
    }
-

-
    pub fn remote_ids(
-
        &self,
-
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git2::Error> {
-
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
-
            |reference| -> Result<RemoteId, refs::Error> {
-
                let r = reference?;
-
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
-
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
-

-
                Ok(id)
-
            },
-
        );
-
        Ok(iter)
-
    }
-
}
-

-
impl<'r> ReadRepository<'r> for Repository {
-
    type Remotes = Box<dyn Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r>;
-

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
-
        let some = self.remotes()?.next().is_some();
-
        Ok(!some)
-
    }
-

-
    fn path(&self) -> &Path {
-
        self.backend.path()
-
    }
-

-
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::Error> {
-
        git::ext::Blob::At {
-
            object: oid.into(),
-
            path,
-
        }
-
        .get(&self.backend)
-
    }
-

-
    fn reference(
-
        &self,
-
        remote: &RemoteId,
-
        name: &git::RefStr,
-
    ) -> Result<Option<git2::Reference>, git2::Error> {
-
        let name = name.strip_prefix(git::refname!("refs")).unwrap_or(name);
-
        let name = format!("refs/remotes/{remote}/{name}");
-
        self.backend.find_reference(&name).map(Some).or_else(|e| {
-
            if git::ext::is_not_found_err(&e) {
-
                Ok(None)
-
            } else {
-
                Err(e)
-
            }
-
        })
-
    }
-

-
    fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
-
        self.backend.find_commit(oid.into()).map(Some).or_else(|e| {
-
            if git::ext::is_not_found_err(&e) {
-
                Ok(None)
-
            } else {
-
                Err(e)
-
            }
-
        })
-
    }
-

-
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
-
        let mut revwalk = self.backend.revwalk()?;
-
        revwalk.push(head.into())?;
-

-
        Ok(revwalk)
-
    }
-

-
    fn reference_oid(
-
        &self,
-
        remote: &RemoteId,
-
        reference: &git::RefStr,
-
    ) -> Result<Option<Oid>, git2::Error> {
-
        let reference = self.reference(remote, reference)?;
-
        Ok(reference.and_then(|r| r.target().map(|o| o.into())))
-
    }
-

-
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
-
        let refs = SignedRefs::load(remote, self)?;
-
        Ok(Remote::new(*remote, refs))
-
    }
-

-
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error> {
-
        // TODO: Only return known refs, eg. heads/ rad/ tags/ etc..
-
        let entries = self
-
            .backend
-
            .references_glob(format!("refs/remotes/{remote}/*").as_str())?;
-
        let mut refs = BTreeMap::new();
-

-
        for e in entries {
-
            let e = e?;
-
            let name = e.name().ok_or(Error::InvalidRef)?;
-
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
-
            let oid = e.target().ok_or(Error::InvalidRef)?;
-

-
            refs.insert(refname, oid.into());
-
        }
-
        Ok(refs.into())
-
    }
-

-
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error> {
-
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
-
            |reference| -> Result<(RemoteId, Remote<Verified>), refs::Error> {
-
                let r = reference?;
-
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
-
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
-
                let remote = self.remote(&id)?;
-

-
                Ok((id, remote))
-
            },
-
        );
-

-
        Ok(Box::new(iter))
-
    }
-

-
    fn project(&self) -> Result<Project, Error> {
-
        todo!()
-
    }
-
}
-

-
impl<'r> WriteRepository<'r> for Repository {
-
    /// Fetch all remotes of a project from the given URL.
-
    fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, FetchError> {
-
        // TODO: Have function to fetch specific remotes.
-
        //
-
        // Repository layout should look like this:
-
        //
-
        //   /refs/remotes/<remote>
-
        //         /heads
-
        //           /master
-
        //         /tags
-
        //         ...
-
        //
-
        let url = url.to_string();
-
        let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
-
        let mut updates = Vec::new();
-
        let mut callbacks = git2::RemoteCallbacks::new();
-
        let tempdir = tempfile::tempdir()?;
-
        // TODO: Comment
-
        let staging = {
-
            let mut builder = git2::build::RepoBuilder::new();
-
            let path = tempdir.path().join("git");
-
            let staging_repo = builder
-
                .bare(true)
-
                // TODO: Comment
-
                // TODO: Due to this, I think we'll have to run GC when there is a failure.
-
                .clone_local(git2::build::CloneLocal::Local)
-
                .clone(
-
                    &git::Url {
-
                        scheme: git::url::Scheme::File,
-
                        path: self.backend.path().to_string_lossy().to_string().into(),
-
                        ..git::Url::default()
-
                    }
-
                    .to_string(),
-
                    &path,
-
                )?;
-

-
            // In case we fetch an invalid update, we want to make sure nothing is deleted.
-
            let mut opts = git2::FetchOptions::default();
-
            opts.prune(git2::FetchPrune::Off);
-

-
            staging_repo
-
                .remote_anonymous(&url)?
-
                .fetch(refs, Some(&mut opts), None)?;
-
            // TODO: Comment
-
            Repository::from(staging_repo).verify()?;
-

-
            path
-
        };
-

-
        callbacks.update_tips(|name, old, new| {
-
            if let Ok(name) = git::RefString::try_from(name) {
-
                updates.push(RefUpdate::from(name, old, new));
-
            } else {
-
                log::warn!("Invalid ref `{}` detected; aborting fetch", name);
-
                return false;
-
            }
-
            // Returning `true` ensures the process is not aborted.
-
            true
-
        });
-

-
        {
-
            let mut remote = self.backend.remote_anonymous(
-
                &git::Url {
-
                    scheme: git::url::Scheme::File,
-
                    path: staging.to_string_lossy().to_string().into(),
-
                    ..git::Url::default()
-
                }
-
                .to_string(),
-
            )?;
-
            let mut opts = git2::FetchOptions::default();
-
            opts.remote_callbacks(callbacks);
-

-
            // TODO: Make sure we verify before pruning, as pruning may get us into
-
            // a state we can't roll back.
-
            opts.prune(git2::FetchPrune::On);
-
            remote.fetch(refs, Some(&mut opts), None)?;
-
        }
-

-
        Ok(updates)
-
    }
-

-
    fn raw(&self) -> &git2::Repository {
-
        &self.backend
-
    }
-
}
-

-
impl From<git2::Repository> for Repository {
-
    fn from(backend: git2::Repository) -> Self {
-
        Self { backend }
-
    }
-
}
-

-
pub mod trailers {
-
    use std::str::FromStr;
-

-
    use super::*;
-
    use crate::crypto::{PublicKey, PublicKeyError};
-
    use crate::crypto::{Signature, SignatureError};
-

-
    pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
-

-
    #[derive(Error, Debug)]
-
    pub enum Error {
-
        #[error("invalid format for signature trailer")]
-
        SignatureTrailerFormat,
-
        #[error("invalid public key in signature trailer")]
-
        PublicKey(#[from] PublicKeyError),
-
        #[error("invalid signature in trailer")]
-
        Signature(#[from] SignatureError),
-
    }
-

-
    pub fn parse_signatures(msg: &str) -> Result<Vec<(PublicKey, Signature)>, Error> {
-
        let trailers =
-
            git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
-
        let mut signatures = Vec::with_capacity(trailers.len());
-

-
        for (key, val) in trailers.iter() {
-
            if key == SIGNATURE_TRAILER {
-
                if let Some((pk, sig)) = val.split_once(' ') {
-
                    let pk = PublicKey::from_str(pk)?;
-
                    let sig = Signature::from_str(sig)?;
-

-
                    signatures.push((pk, sig));
-
                } else {
-
                    return Err(Error::SignatureTrailerFormat);
-
                }
-
            }
-
        }
-
        Ok(signatures)
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use crate::assert_matches;
-
    use crate::git;
-
    use crate::storage::refs::SIGNATURE_REF;
-
    use crate::storage::{ReadStorage, RefUpdate, WriteRepository};
-
    use crate::test::arbitrary;
-
    use crate::test::crypto::MockSigner;
-
    use crate::test::fixtures;
-

-
    #[test]
-
    fn test_remote_refs() {
-
        let dir = tempfile::tempdir().unwrap();
-
        let storage = fixtures::storage(dir.path());
-
        let inv = storage.inventory().unwrap();
-
        let proj = inv.first().unwrap();
-
        let mut refs = git::remote_refs(&git::Url {
-
            host: Some(dir.path().to_string_lossy().to_string()),
-
            scheme: git_url::Scheme::File,
-
            path: format!("/{}", proj).into(),
-
            ..git::Url::default()
-
        })
-
        .unwrap();
-

-
        let project = storage.repository(proj).unwrap();
-
        let remotes = project.remotes().unwrap();
-

-
        // Strip the remote refs of sigrefs so we can compare them.
-
        for remote in refs.values_mut() {
-
            remote.remove(&*SIGNATURE_REF).unwrap();
-
        }
-

-
        let remotes = remotes
-
            .map(|remote| remote.map(|(id, r): (RemoteId, Remote<Verified>)| (id, r.refs.into())))
-
            .collect::<Result<_, _>>()
-
            .unwrap();
-

-
        assert_eq!(refs, remotes);
-
    }
-

-
    #[test]
-
    fn test_fetch() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let alice = fixtures::storage(tmp.path().join("alice"));
-
        let bob = Storage::open(tmp.path().join("bob")).unwrap();
-
        let inventory = alice.inventory().unwrap();
-
        let proj = inventory.first().unwrap();
-
        let repo = alice.repository(proj).unwrap();
-
        let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
-
        let refname = git::refname!("heads/master");
-

-
        // Have Bob fetch Alice's refs.
-
        let updates = bob
-
            .repository(proj)
-
            .unwrap()
-
            .fetch(&git::Url {
-
                scheme: git_url::Scheme::File,
-
                path: alice
-
                    .path()
-
                    .join(proj.to_string())
-
                    .to_string_lossy()
-
                    .into_owned()
-
                    .into(),
-
                ..git::Url::default()
-
            })
-
            .unwrap();
-

-
        // Four refs are created for each remote.
-
        assert_eq!(updates.len(), remotes.len() * 4);
-

-
        for update in updates {
-
            assert_matches!(
-
                update,
-
                RefUpdate::Created { name, .. } if name.starts_with("refs/remotes")
-
            );
-
        }
-

-
        for remote in remotes {
-
            let (id, _) = remote.unwrap();
-
            let alice_repo = alice.repository(proj).unwrap();
-
            let alice_oid = alice_repo.reference(&id, &refname).unwrap().unwrap();
-

-
            let bob_repo = bob.repository(proj).unwrap();
-
            let bob_oid = bob_repo.reference(&id, &refname).unwrap().unwrap();
-

-
            assert_eq!(alice_oid.target(), bob_oid.target());
-
        }
-
    }
-

-
    #[test]
-
    fn test_fetch_update() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let alice = Storage::open(tmp.path().join("alice/storage")).unwrap();
-
        let bob = Storage::open(tmp.path().join("bob/storage")).unwrap();
-

-
        let alice_signer = MockSigner::new(&mut fastrand::Rng::new());
-
        let alice_id = alice_signer.public_key();
-
        let (proj_id, _, proj_repo, alice_head) =
-
            fixtures::project(tmp.path().join("alice/project"), &alice, &alice_signer).unwrap();
-

-
        let refname = git::refname!("refs/heads/master");
-
        let alice_url = git::Url {
-
            scheme: git_url::Scheme::File,
-
            path: alice
-
                .path()
-
                .join(proj_id.to_string())
-
                .to_string_lossy()
-
                .into_owned()
-
                .into(),
-
            ..git::Url::default()
-
        };
-

-
        // Have Bob fetch Alice's refs.
-
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
-
        // Three refs are created: the branch, the signature and the id.
-
        assert_eq!(updates.len(), 3);
-

-
        let alice_proj_storage = alice.repository(&proj_id).unwrap();
-
        let alice_head = proj_repo.find_commit(alice_head).unwrap();
-
        let alice_head = git::commit(&proj_repo, &alice_head, &refname, "Making changes", "Alice")
-
            .unwrap()
-
            .id();
-
        git::push(&proj_repo).unwrap();
-
        alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
-

-
        // Have Bob fetch Alice's new commit.
-
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
-
        // The branch and signature refs are updated.
-
        assert_matches!(
-
            updates.as_slice(),
-
            &[RefUpdate::Updated { .. }, RefUpdate::Updated { .. }]
-
        );
-

-
        // Bob's storage is updated.
-
        let bob_repo = bob.repository(&proj_id).unwrap();
-
        let bob_master = bob_repo.reference(alice_id, &refname).unwrap().unwrap();
-

-
        assert_eq!(bob_master.target().unwrap(), alice_head);
-
    }
-

-
    #[test]
-
    fn test_sign_refs() {
-
        let tmp = tempfile::tempdir().unwrap();
-
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
-
        let storage = Storage::open(tmp.path()).unwrap();
-
        let proj_id = arbitrary::gen::<Id>(1);
-
        let alice = *signer.public_key();
-
        let project = storage.repository(&proj_id).unwrap();
-
        let backend = &project.backend;
-
        let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
-
        let head = git::initial_commit(backend, &sig).unwrap();
-

-
        git::commit(
-
            backend,
-
            &head,
-
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
-
            "Second commit",
-
            &alice.to_string(),
-
        )
-
        .unwrap();
-

-
        let signed = storage.sign_refs(&project, &signer).unwrap();
-
        let remote = project.remote(&alice).unwrap();
-
        let mut unsigned = project.references(&alice).unwrap();
-

-
        // The signed refs doesn't contain the signature ref itself.
-
        unsigned.remove(&*SIGNATURE_REF).unwrap();
-

-
        assert_eq!(remote.refs, signed);
-
        assert_eq!(*remote.refs, unsigned);
-
    }
-
}
deleted node/src/storage/refs.rs
@@ -1,378 +0,0 @@
-
use std::collections::BTreeMap;
-
use std::fmt::Debug;
-
use std::io;
-
use std::io::{BufRead, BufReader};
-
use std::marker::PhantomData;
-
use std::ops::{Deref, DerefMut};
-
use std::path::Path;
-
use std::str::FromStr;
-

-
use once_cell::sync::Lazy;
-
use radicle_git_ext as git_ext;
-
use thiserror::Error;
-

-
use crate::crypto;
-
use crate::crypto::{PublicKey, Signature, Signer, Unverified, Verified};
-
use crate::git;
-
use crate::git::Oid;
-
use crate::storage;
-
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
-
use crate::wire;
-

-
pub static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("radicle/signature"));
-
pub const REFS_BLOB_PATH: &str = "refs";
-
pub const SIGNATURE_BLOB_PATH: &str = "signature";
-

-
#[derive(Debug)]
-
pub enum Updated {
-
    /// The computed [`Refs`] were stored as a new commit.
-
    Updated { oid: Oid },
-
    /// The stored [`Refs`] were the same as the computed ones, so no new commit
-
    /// was created.
-
    Unchanged { oid: Oid },
-
}
-

-
#[derive(Debug, Error)]
-
pub enum Error {
-
    #[error("invalid signature: {0}")]
-
    InvalidSignature(#[from] crypto::Error),
-
    #[error("canonical refs: {0}")]
-
    Canonical(#[from] canonical::Error),
-
    #[error("invalid reference")]
-
    InvalidRef,
-
    #[error("invalid reference: {0}")]
-
    Ref(#[from] git::RefError),
-
    #[error(transparent)]
-
    Git(#[from] git2::Error),
-
    #[error(transparent)]
-
    GitExt(#[from] git_ext::Error),
-
    #[error("refs were not found")]
-
    NotFound,
-
}
-

-
/// The published state of a local repository.
-
#[derive(Default, Clone, Debug, PartialEq, Eq)]
-
pub struct Refs(BTreeMap<git::RefString, Oid>);
-

-
impl Refs {
-
    /// Verify the given signature on these refs, and return [`SignedRefs`] on success.
-
    pub fn verified(
-
        self,
-
        signer: &PublicKey,
-
        signature: Signature,
-
    ) -> Result<SignedRefs<Verified>, Error> {
-
        let refs = self;
-
        let msg = refs.canonical();
-

-
        match signer.verify(&msg, &signature) {
-
            Ok(()) => Ok(SignedRefs {
-
                refs,
-
                signature,
-
                _verified: PhantomData,
-
            }),
-
            Err(e) => Err(e.into()),
-
        }
-
    }
-

-
    /// Sign these refs with the given signer and return [`SignedRefs`].
-
    pub fn signed<S>(self, signer: S) -> Result<SignedRefs<Verified>, Error>
-
    where
-
        S: Signer,
-
    {
-
        let refs = self;
-
        let msg = refs.canonical();
-
        let signature = signer.sign(&msg);
-

-
        Ok(SignedRefs {
-
            refs,
-
            signature,
-
            _verified: PhantomData,
-
        })
-
    }
-

-
    /// Create refs from a canonical representation.
-
    pub fn from_canonical(bytes: &[u8]) -> Result<Self, canonical::Error> {
-
        let reader = BufReader::new(bytes);
-
        let mut refs = BTreeMap::new();
-

-
        for line in reader.lines() {
-
            let line = line?;
-
            let (oid, name) = line
-
                .split_once(' ')
-
                .ok_or(canonical::Error::InvalidFormat)?;
-

-
            let name = git::RefString::try_from(name)?;
-
            let oid = Oid::from_str(oid)?;
-

-
            if oid.is_zero() {
-
                continue;
-
            }
-
            refs.insert(name, oid);
-
        }
-
        Ok(Self(refs))
-
    }
-

-
    pub fn canonical(&self) -> Vec<u8> {
-
        let mut buf = String::new();
-
        let refs = self
-
            .iter()
-
            .filter(|(name, oid)| *name != &*SIGNATURE_REF && !oid.is_zero());
-

-
        for (name, oid) in refs {
-
            buf.push_str(&oid.to_string());
-
            buf.push(' ');
-
            buf.push_str(name);
-
            buf.push('\n');
-
        }
-
        buf.into_bytes()
-
    }
-
}
-

-
impl IntoIterator for Refs {
-
    type Item = (git::RefString, Oid);
-
    type IntoIter = std::collections::btree_map::IntoIter<git::RefString, Oid>;
-

-
    fn into_iter(self) -> Self::IntoIter {
-
        self.0.into_iter()
-
    }
-
}
-

-
impl From<Refs> for BTreeMap<git::RefString, Oid> {
-
    fn from(refs: Refs) -> Self {
-
        refs.0
-
    }
-
}
-

-
impl<V> From<SignedRefs<V>> for Refs {
-
    fn from(signed: SignedRefs<V>) -> Self {
-
        signed.refs
-
    }
-
}
-

-
impl From<BTreeMap<git::RefString, Oid>> for Refs {
-
    fn from(refs: BTreeMap<git::RefString, Oid>) -> Self {
-
        Self(refs)
-
    }
-
}
-

-
impl Deref for Refs {
-
    type Target = BTreeMap<git::RefString, Oid>;
-

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

-
impl DerefMut for Refs {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.0
-
    }
-
}
-

-
/// Combination of [`Refs`] and a [`Signature`]. The signature is a cryptographic
-
/// signature over the refs. This allows us to easily verify if a set of refs
-
/// came from a particular key.
-
///
-
/// The type parameter keeps track of whether the signature was [`Verified`] or
-
/// [`Unverified`].
-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub struct SignedRefs<V> {
-
    refs: Refs,
-
    signature: Signature,
-
    _verified: PhantomData<V>,
-
}
-

-
impl SignedRefs<Unverified> {
-
    pub fn new(refs: Refs, signature: Signature) -> Self {
-
        Self {
-
            refs,
-
            signature,
-
            _verified: PhantomData,
-
        }
-
    }
-

-
    pub fn verified(self, signer: &PublicKey) -> Result<SignedRefs<Verified>, crypto::Error> {
-
        match self.verify(signer) {
-
            Ok(()) => Ok(SignedRefs {
-
                refs: self.refs,
-
                signature: self.signature,
-
                _verified: PhantomData,
-
            }),
-
            Err(e) => Err(e),
-
        }
-
    }
-

-
    pub fn verify(&self, signer: &PublicKey) -> Result<(), crypto::Error> {
-
        let canonical = self.refs.canonical();
-

-
        match signer.verify(&canonical, &self.signature) {
-
            Ok(()) => Ok(()),
-
            Err(e) => Err(e),
-
        }
-
    }
-
}
-

-
impl SignedRefs<Verified> {
-
    pub fn load<'r, S>(remote: &RemoteId, repo: &S) -> Result<Self, Error>
-
    where
-
        S: ReadRepository<'r>,
-
    {
-
        if let Some(oid) = repo.reference_oid(remote, &SIGNATURE_REF)? {
-
            Self::load_at(oid, remote, repo)
-
        } else {
-
            Err(Error::NotFound)
-
        }
-
    }
-

-
    pub fn load_at<'r, S>(oid: Oid, remote: &RemoteId, repo: &S) -> Result<Self, Error>
-
    where
-
        S: storage::ReadRepository<'r>,
-
    {
-
        let refs = repo.blob_at(oid, Path::new(REFS_BLOB_PATH))?;
-
        let signature = repo.blob_at(oid, Path::new(SIGNATURE_BLOB_PATH))?;
-
        let signature: crypto::Signature = signature.content().try_into()?;
-

-
        match remote.verify(refs.content(), &signature) {
-
            Ok(()) => {
-
                let refs = Refs::from_canonical(refs.content())?;
-

-
                Ok(Self {
-
                    refs,
-
                    signature,
-
                    _verified: PhantomData,
-
                })
-
            }
-
            Err(e) => Err(e.into()),
-
        }
-
    }
-

-
    /// Save the signed refs to disk.
-
    /// This creates a new commit on the signed refs branch, and updates the branch pointer.
-
    pub fn save<'r, S: WriteRepository<'r>>(
-
        &self,
-
        // TODO: This should be part of the signed refs.
-
        remote: &RemoteId,
-
        repo: &S,
-
    ) -> Result<Updated, Error> {
-
        let sigref = &*SIGNATURE_REF;
-
        let parent: Option<git2::Commit> = repo
-
            .reference(remote, sigref)?
-
            .map(|r| r.peel_to_commit())
-
            .transpose()?;
-

-
        let tree = {
-
            let raw = repo.raw();
-
            let refs_blob_oid = raw.blob(&self.canonical())?;
-
            let sig_blob_oid = raw.blob(self.signature.as_ref())?;
-

-
            let mut builder = raw.treebuilder(None)?;
-
            builder.insert(REFS_BLOB_PATH, refs_blob_oid, 0o100_644)?;
-
            builder.insert(SIGNATURE_BLOB_PATH, sig_blob_oid, 0o100_644)?;
-

-
            let oid = builder.write()?;
-

-
            raw.find_tree(oid)
-
        }?;
-

-
        if let Some(ref parent) = parent {
-
            if parent.tree()?.id() == tree.id() {
-
                return Ok(Updated::Unchanged {
-
                    oid: parent.id().into(),
-
                });
-
            }
-
        }
-

-
        let sigref = format!("refs/remotes/{remote}/{sigref}");
-
        let author = repo.raw().signature()?;
-
        let commit = repo.raw().commit(
-
            Some(&sigref),
-
            &author,
-
            &author,
-
            &format!("Update {} for {}", sigref, remote),
-
            &tree,
-
            &parent.iter().collect::<Vec<&git2::Commit>>(),
-
        );
-

-
        match commit {
-
            Ok(oid) => Ok(Updated::Updated { oid: oid.into() }),
-
            Err(e) => match (e.class(), e.code()) {
-
                (git2::ErrorClass::Object, git2::ErrorCode::Modified) => {
-
                    log::warn!("Concurrent modification of refs: {:?}", e);
-

-
                    Err(Error::Git(e))
-
                }
-
                _ => Err(e.into()),
-
            },
-
        }
-
    }
-

-
    pub fn unverified(self) -> SignedRefs<Unverified> {
-
        SignedRefs {
-
            refs: self.refs,
-
            signature: self.signature,
-
            _verified: PhantomData,
-
        }
-
    }
-
}
-

-
impl<V> wire::Encode for SignedRefs<V> {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = 0;
-

-
        n += self.refs.encode(writer)?;
-
        n += self.signature.encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for SignedRefs<Unverified> {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let refs = Refs::decode(reader)?;
-
        let signature = Signature::decode(reader)?;
-

-
        Ok(Self {
-
            refs,
-
            signature,
-
            _verified: PhantomData,
-
        })
-
    }
-
}
-

-
impl<V> Deref for SignedRefs<V> {
-
    type Target = Refs;
-

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

-
pub mod canonical {
-
    use super::*;
-

-
    #[derive(Debug, thiserror::Error)]
-
    pub enum Error {
-
        #[error(transparent)]
-
        InvalidRef(#[from] git_ref_format::Error),
-
        #[error("invalid canonical format")]
-
        InvalidFormat,
-
        #[error(transparent)]
-
        Io(#[from] io::Error),
-
        #[error(transparent)]
-
        Git(#[from] git2::Error),
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    #[quickcheck]
-
    fn prop_canonical_roundtrip(refs: Refs) {
-
        let encoded = refs.canonical();
-
        let decoded = Refs::from_canonical(&encoded).unwrap();
-

-
        assert_eq!(refs, decoded);
-
    }
-
}
deleted node/src/test.rs
@@ -1,10 +0,0 @@
-
pub(crate) mod arbitrary;
-
pub(crate) mod assert;
-
pub(crate) mod crypto;
-
pub(crate) mod fixtures;
-
pub(crate) mod handle;
-
pub(crate) mod logger;
-
pub(crate) mod peer;
-
pub(crate) mod simulator;
-
pub(crate) mod storage;
-
pub(crate) mod tests;
deleted node/src/test/arbitrary.rs
@@ -1,336 +0,0 @@
-
use std::collections::{BTreeMap, HashSet};
-
use std::hash::Hash;
-
use std::iter;
-
use std::net;
-
use std::ops::RangeBounds;
-
use std::path::PathBuf;
-

-
use bloomy::BloomFilter;
-
use nonempty::NonEmpty;
-
use quickcheck::Arbitrary;
-

-
use crate::collections::HashMap;
-
use crate::crypto;
-
use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified};
-
use crate::git;
-
use crate::hash;
-
use crate::identity::{doc::Delegate, doc::Doc, Did, Id, Project};
-
use crate::service::filter::{Filter, FILTER_SIZE};
-
use crate::service::message::{
-
    Address, Envelope, InventoryAnnouncement, Message, NodeAnnouncement, RefsAnnouncement,
-
    Subscribe,
-
};
-
use crate::service::{NodeId, Timestamp};
-
use crate::storage;
-
use crate::storage::refs::{Refs, SignedRefs};
-
use crate::test::storage::MockStorage;
-
use crate::wire::message::MessageType;
-

-
use super::crypto::MockSigner;
-

-
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
-
    let size = fastrand::usize(range);
-
    let mut set = HashSet::with_capacity(size);
-
    let mut g = quickcheck::Gen::new(size);
-

-
    while set.len() < size {
-
        set.insert(T::arbitrary(&mut g));
-
    }
-
    set
-
}
-

-
pub fn gen<T: Arbitrary>(size: usize) -> T {
-
    let mut gen = quickcheck::Gen::new(size);
-

-
    T::arbitrary(&mut gen)
-
}
-

-
#[derive(Clone, Debug)]
-
pub struct ByteArray<const N: usize>([u8; N]);
-

-
impl<const N: usize> ByteArray<N> {
-
    pub fn into_inner(self) -> [u8; N] {
-
        self.0
-
    }
-

-
    pub fn as_slice(&self) -> &[u8] {
-
        self.0.as_slice()
-
    }
-
}
-

-
impl<const N: usize> Arbitrary for ByteArray<N> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let mut bytes: [u8; N] = [0; N];
-
        for byte in &mut bytes {
-
            *byte = u8::arbitrary(g);
-
        }
-
        Self(bytes)
-
    }
-
}
-

-
impl Arbitrary for Filter {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let mut bytes = vec![0; FILTER_SIZE];
-
        for _ in 0..64 {
-
            let index = usize::arbitrary(g) % bytes.len();
-
            bytes[index] = u8::arbitrary(g);
-
        }
-
        Self::from(BloomFilter::from(bytes))
-
    }
-
}
-

-
impl Arbitrary for Envelope {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        Self {
-
            magic: u32::arbitrary(g),
-
            msg: Message::arbitrary(g),
-
        }
-
    }
-
}
-

-
impl Arbitrary for Message {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let type_id = g
-
            .choose(&[
-
                MessageType::InventoryAnnouncement,
-
                MessageType::NodeAnnouncement,
-
                MessageType::RefsAnnouncement,
-
                MessageType::Subscribe,
-
            ])
-
            .unwrap();
-

-
        match type_id {
-
            MessageType::InventoryAnnouncement => Self::InventoryAnnouncement {
-
                node: NodeId::arbitrary(g),
-
                message: InventoryAnnouncement {
-
                    inventory: Vec::<Id>::arbitrary(g),
-
                    timestamp: Timestamp::arbitrary(g),
-
                },
-
                signature: crypto::Signature::from(ByteArray::<64>::arbitrary(g).into_inner()),
-
            },
-
            MessageType::RefsAnnouncement => Self::RefsAnnouncement {
-
                node: NodeId::arbitrary(g),
-
                message: RefsAnnouncement {
-
                    id: Id::arbitrary(g),
-
                    refs: Refs::arbitrary(g),
-
                },
-
                signature: crypto::Signature::from(ByteArray::<64>::arbitrary(g).into_inner()),
-
            },
-
            MessageType::NodeAnnouncement => {
-
                let message = NodeAnnouncement {
-
                    features: ByteArray::<32>::arbitrary(g).into_inner(),
-
                    timestamp: Timestamp::arbitrary(g),
-
                    alias: ByteArray::<32>::arbitrary(g).into_inner(),
-
                    addresses: Arbitrary::arbitrary(g),
-
                };
-
                let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
-
                let signature = crypto::Signature::from(bytes.into_inner());
-

-
                Self::NodeAnnouncement {
-
                    node: NodeId::arbitrary(g),
-
                    signature,
-
                    message,
-
                }
-
            }
-
            MessageType::Subscribe => Self::Subscribe(Subscribe {
-
                filter: Filter::arbitrary(g),
-
                since: Timestamp::arbitrary(g),
-
                until: Timestamp::arbitrary(g),
-
            }),
-
            _ => unreachable!(),
-
        }
-
    }
-
}
-

-
impl Arbitrary for Address {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        if bool::arbitrary(g) {
-
            Address::Ipv4 {
-
                ip: net::Ipv4Addr::from(u32::arbitrary(g)),
-
                port: u16::arbitrary(g),
-
            }
-
        } else {
-
            let octets: [u8; 16] = ByteArray::<16>::arbitrary(g).into_inner();
-

-
            Address::Ipv6 {
-
                ip: net::Ipv6Addr::from(octets),
-
                port: u16::arbitrary(g),
-
            }
-
        }
-
    }
-
}
-

-
impl Arbitrary for storage::Remotes<crypto::Verified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
-
            Arbitrary::arbitrary(g);
-

-
        storage::Remotes::new(remotes)
-
    }
-
}
-

-
impl Arbitrary for MockStorage {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let inventory = Arbitrary::arbitrary(g);
-
        MockStorage::new(inventory)
-
    }
-
}
-

-
impl Arbitrary for Project {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let doc = Doc::<Verified>::arbitrary(g);
-
        let (oid, _) = doc.encode().unwrap();
-
        let id = Id::from(oid);
-
        let remotes = storage::Remotes::arbitrary(g);
-
        let path = PathBuf::arbitrary(g);
-

-
        Self {
-
            id,
-
            doc,
-
            remotes,
-
            path,
-
        }
-
    }
-
}
-

-
impl Arbitrary for Did {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        Self::from(PublicKey::arbitrary(g))
-
    }
-
}
-

-
impl Arbitrary for Delegate {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        Self {
-
            name: String::arbitrary(g),
-
            id: Did::arbitrary(g),
-
        }
-
    }
-
}
-

-
impl Arbitrary for Doc<Unverified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let name = String::arbitrary(g);
-
        let description = String::arbitrary(g);
-
        let default_branch = String::arbitrary(g);
-
        let delegate = Delegate::arbitrary(g);
-

-
        Self::initial(name, description, default_branch, delegate)
-
    }
-
}
-

-
impl Arbitrary for Doc<Verified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
-
        let name = iter::repeat_with(|| rng.alphanumeric())
-
            .take(rng.usize(1..16))
-
            .collect();
-
        let description = iter::repeat_with(|| rng.alphanumeric())
-
            .take(rng.usize(0..32))
-
            .collect();
-
        let default_branch = iter::repeat_with(|| rng.alphanumeric())
-
            .take(rng.usize(1..16))
-
            .collect();
-
        let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
-
            name: iter::repeat_with(|| rng.alphanumeric())
-
                .take(rng.usize(1..16))
-
                .collect(),
-
            id: Did::arbitrary(g),
-
        })
-
        .take(rng.usize(1..6))
-
        .collect::<Vec<_>>()
-
        .try_into()
-
        .unwrap();
-
        let threshold = delegates.len() / 2 + 1;
-
        let doc: Doc<Unverified> =
-
            Doc::new(name, description, default_branch, delegates, threshold);
-

-
        doc.verified().unwrap()
-
    }
-
}
-

-
impl Arbitrary for SignedRefs<Unverified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
-
        let signature = crypto::Signature::from(bytes.into_inner());
-
        let refs = Refs::arbitrary(g);
-

-
        Self::new(refs, signature)
-
    }
-
}
-

-
impl Arbitrary for Refs {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
-
        let mut bytes: [u8; 20] = [0; 20];
-
        let names = &[
-
            "heads/master",
-
            "heads/feature/1",
-
            "heads/feature/2",
-
            "heads/feature/3",
-
            "heads/radicle/id",
-
            "tags/v1.0",
-
            "tags/v2.0",
-
            "notes/1",
-
        ];
-

-
        for _ in 0..g.size().min(names.len()) {
-
            if let Some(name) = g.choose(names) {
-
                for byte in &mut bytes {
-
                    *byte = u8::arbitrary(g);
-
                }
-
                let oid = storage::Oid::try_from(&bytes[..]).unwrap();
-
                let name = git::RefString::try_from(*name).unwrap();
-

-
                refs.insert(name, oid);
-
            }
-
        }
-
        Self::from(refs)
-
    }
-
}
-

-
impl Arbitrary for storage::Remote<crypto::Verified> {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let refs = Refs::arbitrary(g);
-
        let signer = MockSigner::arbitrary(g);
-
        let signed = refs.signed(&signer).unwrap();
-

-
        storage::Remote::new(*signer.public_key(), signed)
-
    }
-
}
-

-
impl Arbitrary for MockSigner {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
-
        let seed = Seed::new(bytes.into_inner());
-
        let sk = KeyPair::from_seed(seed).sk;
-

-
        MockSigner::from(sk)
-
    }
-
}
-

-
impl Arbitrary for Id {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes = ByteArray::<20>::arbitrary(g);
-
        let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
-

-
        Id::from(oid)
-
    }
-
}
-

-
impl Arbitrary for hash::Digest {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: Vec<u8> = Arbitrary::arbitrary(g);
-
        hash::Digest::new(&bytes)
-
    }
-
}
-

-
impl Arbitrary for PublicKey {
-
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
-
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
-
        let seed = Seed::new(bytes.into_inner());
-
        let keypair = KeyPair::from_seed(seed);
-

-
        PublicKey(keypair.pk)
-
    }
-
}
deleted node/src/test/assert.rs
@@ -1,296 +0,0 @@
-
// Copyright (c) 2016 Murarth
-
//
-
// 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.
-

-
//! Provides a macro, `assert_matches!`, which tests whether a value
-
//! matches a given pattern, causing a panic if the match fails.
-
//!
-
//! See the macro [`assert_matches!`] documentation for more information.
-
//!
-
//! Also provides a debug-only counterpart, [`debug_assert_matches!`].
-
//!
-
//! See the macro [`debug_assert_matches!`] documentation for more information
-
//! about this macro.
-
//!
-
//! [`assert_matches!`]: macro.assert_matches.html
-
//! [`debug_assert_matches!`]: macro.debug_assert_matches.html
-

-
#![deny(missing_docs)]
-

-
/// Asserts that an expression matches a given pattern.
-
///
-
/// A guard expression may be supplied to add further restrictions to the
-
/// expected value of the expression.
-
///
-
/// A `match` arm may be supplied to perform additional assertions or to yield
-
/// a value from the macro invocation.
-
///
-
#[macro_export]
-
macro_rules! assert_matches {
-
    ( $e:expr , $($pat:pat_param)|+ ) => {
-
        match $e {
-
            $($pat)|+ => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr ) => {
-
        match $e {
-
            $($pat)|+ if $cond => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+ if $cond))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr ) => {
-
        match $e {
-
            $($pat)|+ => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr ) => {
-
        match $e {
-
            $($pat)|+ if $cond => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
-
                e, stringify!($($pat)|+ if $cond))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+), format_args!($($arg)*))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ if $cond => (),
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+), format_args!($($arg)*))
-
        }
-
    };
-
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr , $($arg:tt)* ) => {
-
        match $e {
-
            $($pat)|+ if $cond => $arm,
-
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
-
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
-
        }
-
    };
-
}
-

-
/// Asserts that an expression matches a given pattern.
-
///
-
/// Unlike [`assert_matches!`], `debug_assert_matches!` statements are only enabled
-
/// in non-optimized builds by default. An optimized build will omit all
-
/// `debug_assert_matches!` statements unless `-C debug-assertions` is passed
-
/// to the compiler.
-
///
-
/// See the macro [`assert_matches!`] documentation for more information.
-
///
-
/// [`assert_matches!`]: macro.assert_matches.html
-
#[macro_export(local_inner_macros)]
-
macro_rules! debug_assert_matches {
-
    ( $($tt:tt)* ) => { {
-
        if _assert_matches_cfg!(debug_assertions) {
-
            assert_matches!($($tt)*);
-
        }
-
    } }
-
}
-

-
#[doc(hidden)]
-
#[macro_export]
-
macro_rules! _assert_matches_cfg {
-
    ( $($tt:tt)* ) => { cfg!($($tt)*) }
-
}
-

-
#[cfg(test)]
-
mod test {
-
    use std::panic::{catch_unwind, UnwindSafe};
-

-
    #[derive(Debug)]
-
    enum Foo {
-
        A(i32),
-
        B(&'static str),
-
        C(&'static str),
-
    }
-

-
    #[test]
-
    fn test_assert_succeed() {
-
        let a = Foo::A(123);
-

-
        assert_matches!(a, Foo::A(_));
-
        assert_matches!(a, Foo::A(123));
-
        assert_matches!(a, Foo::A(i) if i == 123);
-
        assert_matches!(a, Foo::A(42) | Foo::A(123));
-

-
        let b = Foo::B("foo");
-

-
        assert_matches!(b, Foo::B(_));
-
        assert_matches!(b, Foo::B("foo"));
-
        assert_matches!(b, Foo::B(s) if s == "foo");
-
        assert_matches!(b, Foo::B(s) => assert_eq!(s, "foo"));
-
        assert_matches!(b, Foo::B(s) => { assert_eq!(s, "foo") });
-
        assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "foo"));
-
        assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo") });
-

-
        let c = Foo::C("foo");
-

-
        assert_matches!(c, Foo::B(_) | Foo::C(_));
-
        assert_matches!(c, Foo::B("foo") | Foo::C("foo"));
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo");
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) => assert_eq!(s, "foo"));
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) => { assert_eq!(s, "foo") });
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => assert_eq!(s, "foo"));
-
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => { assert_eq!(s, "foo") });
-
    }
-

-
    #[test]
-
    #[should_panic]
-
    fn test_assert_panic_0() {
-
        let a = Foo::A(123);
-

-
        assert_matches!(a, Foo::B(_));
-
    }
-

-
    #[test]
-
    #[should_panic]
-
    fn test_assert_panic_1() {
-
        let b = Foo::B("foo");
-

-
        assert_matches!(b, Foo::B("bar"));
-
    }
-

-
    #[test]
-
    #[should_panic]
-
    fn test_assert_panic_2() {
-
        let b = Foo::B("foo");
-

-
        assert_matches!(b, Foo::B(s) if s == "bar");
-
    }
-

-
    #[test]
-
    fn test_assert_no_move() {
-
        let b = &mut Foo::A(0);
-
        assert_matches!(*b, Foo::A(0));
-
    }
-

-
    #[test]
-
    fn assert_with_message() {
-
        let a = Foo::A(0);
-

-
        assert_matches!(a, Foo::A(_), "o noes");
-
        assert_matches!(a, Foo::A(n) if n == 0, "o noes");
-
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes");
-
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
-
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes");
-
        assert_matches!(a, Foo::A(n) if n == 0 => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
-
        assert_matches!(a, Foo::A(_), "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {:?}", a);
-
        assert_matches!(a, Foo::A(_), "o noes {value:?}", value = a);
-
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {value:?}", value=a);
-
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {value:?}", value=a);
-
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {value:?}", value=a);
-
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes {value:?}", value=a);
-
    }
-

-
    fn panic_message<F>(f: F) -> String
-
    where
-
        F: FnOnce() + UnwindSafe,
-
    {
-
        let err = catch_unwind(f).expect_err("function did not panic");
-

-
        *err.downcast::<String>()
-
            .expect("function panicked with non-String value")
-
    }
-

-
    #[test]
-
    fn test_panic_message() {
-
        let a = Foo::A(1);
-

-
        // expr, pat
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_));
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
-
        );
-

-
        // expr, pat if cond
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
-
        );
-

-
        // expr, pat => arm
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_) => {});
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
-
        );
-

-
        // expr, pat if cond => arm
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo" => {});
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
-
        );
-

-
        // expr, pat, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_), "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
-
        );
-

-
        // expr, pat if cond, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo", "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
-
        );
-

-
        // expr, pat => arm, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(_) => {}, "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
-
        );
-

-
        // expr, pat if cond => arm, args
-
        assert_eq!(
-
            panic_message(|| {
-
                assert_matches!(a, Foo::B(s) if s == "foo" => {}, "msg");
-
            }),
-
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
-
        );
-
    }
-
}
deleted node/src/test/crypto.rs
@@ -1,65 +0,0 @@
-
use crate::crypto::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer};
-

-
#[derive(Debug, Clone)]
-
pub struct MockSigner {
-
    pk: PublicKey,
-
    sk: SecretKey,
-
}
-

-
impl MockSigner {
-
    pub fn new(rng: &mut fastrand::Rng) -> Self {
-
        let mut bytes: [u8; 32] = [0; 32];
-

-
        for byte in &mut bytes {
-
            *byte = rng.u8(..);
-
        }
-
        let seed = Seed::new(bytes);
-
        let keypair = KeyPair::from_seed(seed);
-

-
        Self::from(keypair.sk)
-
    }
-
}
-

-
impl From<SecretKey> for MockSigner {
-
    fn from(sk: SecretKey) -> Self {
-
        let pk = sk.public_key().into();
-
        Self { sk, pk }
-
    }
-
}
-

-
impl Default for MockSigner {
-
    fn default() -> Self {
-
        let seed = Seed::generate();
-
        let keypair = KeyPair::from_seed(seed);
-
        let sk = keypair.sk;
-

-
        Self {
-
            pk: sk.public_key().into(),
-
            sk,
-
        }
-
    }
-
}
-

-
impl PartialEq for MockSigner {
-
    fn eq(&self, other: &Self) -> bool {
-
        self.pk == other.pk
-
    }
-
}
-

-
impl Eq for MockSigner {}
-

-
impl std::hash::Hash for MockSigner {
-
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-
        self.pk.hash(state)
-
    }
-
}
-

-
impl Signer for MockSigner {
-
    fn public_key(&self) -> &PublicKey {
-
        &self.pk
-
    }
-

-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        self.sk.sign(msg, None).into()
-
    }
-
}
deleted node/src/test/fixtures.rs
@@ -1,116 +0,0 @@
-
use std::path::Path;
-

-
use crate::crypto::{Signer, Verified};
-
use crate::git;
-
use crate::identity::Id;
-
use crate::rad;
-
use crate::storage::git::Storage;
-
use crate::storage::refs::SignedRefs;
-
use crate::storage::{BranchName, WriteStorage};
-
use crate::test::arbitrary;
-
use crate::test::crypto::MockSigner;
-

-
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
-
    let path = path.as_ref();
-
    let proj_ids = arbitrary::set::<Id>(3..=3);
-
    let signers = arbitrary::set::<MockSigner>(3..=3);
-
    let storage = Storage::open(path).unwrap();
-

-
    crate::test::logger::init(log::Level::Debug);
-

-
    for signer in signers {
-
        let remote = signer.public_key();
-

-
        log::debug!("signer {}...", remote);
-

-
        for proj in proj_ids.iter() {
-
            let repo = storage.repository(proj).unwrap();
-
            let raw = &repo.backend;
-
            let sig = git2::Signature::now(&remote.to_string(), "anonymous@radicle.xyz").unwrap();
-
            let head = git::initial_commit(raw, &sig).unwrap();
-

-
            log::debug!("{}: creating {}...", remote, proj);
-

-
            raw.reference(
-
                &format!("refs/remotes/{remote}/heads/radicle/id"),
-
                head.id(),
-
                false,
-
                "test",
-
            )
-
            .unwrap();
-

-
            let head = git::commit(
-
                raw,
-
                &head,
-
                &git::RefString::try_from(format!("refs/remotes/{remote}/heads/master")).unwrap(),
-
                "Second commit",
-
                &remote.to_string(),
-
            )
-
            .unwrap();
-

-
            git::commit(
-
                raw,
-
                &head,
-
                &git::RefString::try_from(format!("refs/remotes/{remote}/heads/patch/3")).unwrap(),
-
                "Third commit",
-
                &remote.to_string(),
-
            )
-
            .unwrap();
-

-
            storage.sign_refs(&repo, &signer).unwrap();
-
        }
-
    }
-
    storage
-
}
-

-
/// Create a new repository at the given path, and initialize it into a project.
-
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
-
    path: P,
-
    storage: &'r S,
-
    signer: G,
-
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
-
    let (repo, head) = repository(path);
-
    let (id, refs) = rad::init(
-
        &repo,
-
        "acme",
-
        "Acme's repository",
-
        BranchName::from("master"),
-
        signer,
-
        storage,
-
    )?;
-

-
    Ok((id, refs, repo, head))
-
}
-

-
/// Creates a regular repository at the given path with a couple of commits.
-
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
-
    let repo = git2::Repository::init(path).unwrap();
-
    let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
-
    let head = git::initial_commit(&repo, &sig).unwrap();
-
    let oid = git::commit(
-
        &repo,
-
        &head,
-
        git::refname!("refs/heads/master").as_refstr(),
-
        "Second commit",
-
        "anonymous",
-
    )
-
    .unwrap()
-
    .id();
-

-
    // Look, I don't really understand why we have to do this, but we do.
-
    drop(head);
-

-
    (repo, oid)
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-

-
    #[test]
-
    fn smoke() {
-
        let tmp = tempfile::tempdir().unwrap();
-

-
        storage(&tmp.path());
-
    }
-
}
deleted node/src/test/handle.rs
@@ -1,40 +0,0 @@
-
use std::sync::{Arc, Mutex};
-

-
use crate::client::handle::traits;
-
use crate::client::handle::Error;
-
use crate::identity::Id;
-
use crate::service;
-
use crate::service::FetchLookup;
-

-
#[derive(Default, Clone)]
-
pub struct Handle {
-
    pub updates: Arc<Mutex<Vec<Id>>>,
-
}
-

-
impl traits::Handle for Handle {
-
    fn fetch(&self, _id: Id) -> Result<FetchLookup, Error> {
-
        Ok(FetchLookup::NotFound)
-
    }
-

-
    fn track(&self, _id: Id) -> Result<bool, Error> {
-
        Ok(true)
-
    }
-

-
    fn untrack(&self, _id: Id) -> Result<bool, Error> {
-
        Ok(true)
-
    }
-

-
    fn updated(&self, id: Id) -> Result<(), Error> {
-
        self.updates.lock().unwrap().push(id);
-

-
        Ok(())
-
    }
-

-
    fn command(&self, _cmd: service::Command) -> Result<(), Error> {
-
        Ok(())
-
    }
-

-
    fn shutdown(self) -> Result<(), Error> {
-
        Ok(())
-
    }
-
}
deleted node/src/test/logger.rs
@@ -1,49 +0,0 @@
-
use log::*;
-

-
struct Logger {
-
    level: Level,
-
}
-

-
impl Log for Logger {
-
    fn enabled(&self, metadata: &Metadata) -> bool {
-
        metadata.level() <= self.level
-
    }
-

-
    fn log(&self, record: &Record) {
-
        use colored::Colorize;
-

-
        match record.target() {
-
            "test" => {
-
                println!("{} {}", "test:".cyan(), record.args().to_string().yellow())
-
            }
-
            "sim" => {
-
                println!("{}  {}", "sim:".bold(), record.args().to_string().bold())
-
            }
-
            target => {
-
                if self.enabled(record.metadata()) {
-
                    let s = format!("{:<8} {}", format!("{}:", target), record.args());
-
                    match record.level() {
-
                        log::Level::Warn => {
-
                            println!("{}", s.yellow());
-
                        }
-
                        log::Level::Error => {
-
                            println!("{}", s.red());
-
                        }
-
                        _ => {
-
                            println!("{}", s.dimmed());
-
                        }
-
                    }
-
                }
-
            }
-
        }
-
    }
-

-
    fn flush(&self) {}
-
}
-

-
pub fn init(level: Level) {
-
    let logger = Logger { level };
-

-
    log::set_boxed_logger(Box::new(logger)).ok();
-
    log::set_max_level(level.to_level_filter());
-
}
deleted node/src/test/peer.rs
@@ -1,211 +0,0 @@
-
use std::net;
-
use std::ops::{Deref, DerefMut};
-

-
use git_url::Url;
-
use log::*;
-

-
use crate::address_book::{KnownAddress, Source};
-
use crate::clock::RefClock;
-
use crate::collections::HashMap;
-
use crate::service;
-
use crate::service::config::*;
-
use crate::service::message::*;
-
use crate::service::*;
-
use crate::storage::WriteStorage;
-
use crate::test::crypto::MockSigner;
-
use crate::test::simulator;
-
use crate::{Link, LocalTime};
-

-
/// Service instantiation used for testing.
-
pub type Service<S> = service::Service<HashMap<net::IpAddr, KnownAddress>, S, MockSigner>;
-

-
#[derive(Debug)]
-
pub struct Peer<S> {
-
    pub name: &'static str,
-
    pub service: Service<S>,
-
    pub ip: net::IpAddr,
-
    pub rng: fastrand::Rng,
-
    pub local_time: LocalTime,
-
    pub local_addr: net::SocketAddr,
-

-
    initialized: bool,
-
}
-

-
impl<'r, S> simulator::Peer<S> for Peer<S>
-
where
-
    S: WriteStorage<'r> + 'static,
-
{
-
    fn init(&mut self) {
-
        self.initialize()
-
    }
-

-
    fn addr(&self) -> net::SocketAddr {
-
        net::SocketAddr::new(self.ip, DEFAULT_PORT)
-
    }
-
}
-

-
impl<S> Deref for Peer<S> {
-
    type Target = Service<S>;
-

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

-
impl<S> DerefMut for Peer<S> {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.service
-
    }
-
}
-

-
impl<'r, S> Peer<S>
-
where
-
    S: WriteStorage<'r> + 'static,
-
{
-
    pub fn new(name: &'static str, ip: impl Into<net::IpAddr>, storage: S) -> Self {
-
        Self::config(
-
            name,
-
            Config {
-
                git_url: storage.url(),
-
                ..Config::default()
-
            },
-
            ip,
-
            vec![],
-
            storage,
-
            fastrand::Rng::new(),
-
        )
-
    }
-

-
    pub fn config(
-
        name: &'static str,
-
        config: Config,
-
        ip: impl Into<net::IpAddr>,
-
        addrs: Vec<(net::SocketAddr, Source)>,
-
        storage: S,
-
        mut rng: fastrand::Rng,
-
    ) -> Self {
-
        let addrs = addrs
-
            .into_iter()
-
            .map(|(addr, src)| (addr.ip(), KnownAddress::new(addr, src, None)))
-
            .collect();
-
        let local_time = LocalTime::now();
-
        let clock = RefClock::from(local_time);
-
        let signer = MockSigner::new(&mut rng);
-
        let service = Service::new(config, clock, storage, addrs, signer, rng.clone());
-
        let ip = ip.into();
-
        let local_addr = net::SocketAddr::new(ip, rng.u16(..));
-

-
        Self {
-
            name,
-
            service,
-
            ip,
-
            local_addr,
-
            rng,
-
            local_time,
-
            initialized: false,
-
        }
-
    }
-

-
    pub fn initialize(&mut self) {
-
        if !self.initialized {
-
            info!("{}: Initializing: address = {}", self.name, self.ip);
-

-
            self.initialized = true;
-
            self.service.initialize(LocalTime::now());
-
        }
-
    }
-

-
    pub fn timestamp(&self) -> Timestamp {
-
        self.service.timestamp()
-
    }
-

-
    pub fn git_url(&self) -> Url {
-
        self.config().git_url.clone()
-
    }
-

-
    pub fn node_id(&self) -> NodeId {
-
        self.service.node_id()
-
    }
-

-
    pub fn receive(&mut self, peer: &net::SocketAddr, msg: Message) {
-
        self.service
-
            .received_message(peer, self.config().network.envelope(msg));
-
    }
-

-
    pub fn connect_from(&mut self, peer: &Self) {
-
        let remote = simulator::Peer::<S>::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();
-

-
        self.initialize();
-
        self.service.connected(remote, &local, Link::Inbound);
-
        self.receive(
-
            &remote,
-
            Message::init(
-
                peer.node_id(),
-
                self.local_time().as_secs(),
-
                vec![Address::from(remote)],
-
                git,
-
            ),
-
        );
-

-
        let mut msgs = self.messages(&remote);
-
        msgs.find(|m| matches!(m, Message::Initialize { .. }))
-
            .expect("`initialize` is sent");
-
        msgs.find(|m| matches!(m, Message::InventoryAnnouncement { .. }))
-
            .expect("`inventory-announcement` is sent");
-
    }
-

-
    pub fn connect_to(&mut self, peer: &Self) {
-
        let remote = simulator::Peer::<S>::addr(peer);
-

-
        self.initialize();
-
        self.service.attempted(&remote);
-
        self.service
-
            .connected(remote, &self.local_addr, Link::Outbound);
-

-
        let mut msgs = self.messages(&remote);
-
        msgs.find(|m| matches!(m, Message::Initialize { .. }))
-
            .expect("`initialize` is sent");
-
        msgs.find(|m| matches!(m, Message::InventoryAnnouncement { .. }))
-
            .expect("`inventory-announcement` is sent");
-

-
        let git = peer.config().git_url.clone();
-
        self.receive(
-
            &remote,
-
            Message::init(
-
                peer.node_id(),
-
                self.local_time().as_secs(),
-
                peer.config().listen.clone(),
-
                git,
-
            ),
-
        );
-
    }
-

-
    /// Drain outgoing messages sent from this peer to the remote address.
-
    pub fn messages(&mut self, remote: &net::SocketAddr) -> impl Iterator<Item = Message> {
-
        let mut msgs = Vec::new();
-

-
        self.service.outbox().retain(|o| match o {
-
            service::Io::Write(a, envelopes) if a == remote => {
-
                msgs.extend(envelopes.iter().map(|e| e.msg.clone()));
-
                false
-
            }
-
            _ => true,
-
        });
-

-
        msgs.into_iter()
-
    }
-

-
    /// Get a draining iterator over the peer's emitted events.
-
    pub fn events(&mut self) -> impl Iterator<Item = Event> + '_ {
-
        self.outbox()
-
            .filter_map(|io| if let Io::Event(e) = io { Some(e) } else { None })
-
    }
-

-
    /// Get a draining iterator over the peer's I/O outbox.
-
    pub fn outbox(&mut self) -> impl Iterator<Item = Io> + '_ {
-
        self.service.outbox().drain(..)
-
    }
-
}
deleted node/src/test/simulator.rs
@@ -1,619 +0,0 @@
-
//! A simple P2P network simulator. Acts as the _reactor_, but without doing any I/O.
-
#![allow(clippy::collapsible_if)]
-

-
#[cfg(feature = "quickcheck")]
-
pub mod arbitrary;
-

-
use std::collections::{BTreeMap, BTreeSet, VecDeque};
-
use std::marker::PhantomData;
-
use std::ops::{Deref, DerefMut, Range};
-
use std::{fmt, io, net};
-

-
use log::*;
-
use nakamoto_net as nakamoto;
-
use nakamoto_net::{Link, LocalDuration, LocalTime};
-

-
use crate::service::{DisconnectReason, Envelope, Event, Io};
-
use crate::storage::WriteStorage;
-
use crate::test::peer::Service;
-

-
/// Minimum latency between peers.
-
pub const MIN_LATENCY: LocalDuration = LocalDuration::from_millis(1);
-
/// Maximum number of events buffered per peer.
-
pub const MAX_EVENTS: usize = 2048;
-

-
/// Identifier for a simulated node/peer.
-
/// The simulator requires each peer to have a distinct IP address.
-
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 {
-
    /// Initialize the peer. This should at minimum initialize the service with the
-
    /// current time.
-
    fn init(&mut self);
-
    /// Get the peer address.
-
    fn addr(&self) -> net::SocketAddr;
-
}
-

-
/// Simulated service input.
-
#[derive(Debug, Clone)]
-
pub enum Input {
-
    /// Connection attempt underway.
-
    Connecting {
-
        /// Remote peer address.
-
        addr: net::SocketAddr,
-
    },
-
    /// New connection with a peer.
-
    Connected {
-
        /// Remote peer id.
-
        addr: net::SocketAddr,
-
        /// Local peer id.
-
        local_addr: net::SocketAddr,
-
        /// Link direction.
-
        link: Link,
-
    },
-
    /// Disconnected from peer.
-
    Disconnected(
-
        net::SocketAddr,
-
        nakamoto::DisconnectReason<DisconnectReason>,
-
    ),
-
    /// Received a message from a remote peer.
-
    Received(net::SocketAddr, Vec<Envelope>),
-
    /// Used to advance the state machine after some wall time has passed.
-
    Wake,
-
}
-

-
/// A scheduled service input.
-
#[derive(Debug, Clone)]
-
pub struct Scheduled {
-
    /// The node for which this input is scheduled.
-
    pub node: NodeId,
-
    /// The remote peer from which this input originates.
-
    /// If the input originates from the local node, this should be set to the zero address.
-
    pub remote: net::SocketAddr,
-
    /// The input being scheduled.
-
    pub input: Input,
-
}
-

-
impl fmt::Display for Scheduled {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        match &self.input {
-
            Input::Received(from, msgs) => {
-
                for msg in msgs {
-
                    write!(f, "{} <- {} ({:?})", self.node, from, msg)?;
-
                }
-
                Ok(())
-
            }
-
            Input::Connected {
-
                addr,
-
                local_addr,
-
                link: Link::Inbound,
-
                ..
-
            } => write!(f, "{} <== {}: Connected", local_addr, addr),
-
            Input::Connected {
-
                local_addr,
-
                addr,
-
                link: Link::Outbound,
-
                ..
-
            } => write!(f, "{} ==> {}: Connected", local_addr, addr),
-
            Input::Connecting { addr } => {
-
                write!(f, "{} => {}: Connecting", self.node, addr)
-
            }
-
            Input::Disconnected(addr, reason) => {
-
                write!(f, "{} =/= {}: Disconnected: {}", self.node, addr, reason)
-
            }
-
            Input::Wake => {
-
                write!(f, "{}: Tock", self.node)
-
            }
-
        }
-
    }
-
}
-

-
/// Inbox of scheduled state machine inputs to be delivered to the simulated nodes.
-
#[derive(Debug)]
-
pub struct Inbox {
-
    /// The set of scheduled inputs. We use a `BTreeMap` to ensure inputs are always
-
    /// ordered by scheduled delivery time.
-
    messages: BTreeMap<LocalTime, Scheduled>,
-
}
-

-
impl Inbox {
-
    /// Add a scheduled input to the inbox.
-
    fn insert(&mut self, mut time: LocalTime, msg: Scheduled) {
-
        // Make sure we don't overwrite an existing message by using the same time slot.
-
        while self.messages.contains_key(&time) {
-
            time = time + MIN_LATENCY;
-
        }
-
        self.messages.insert(time, msg);
-
    }
-

-
    /// Get the next scheduled input to be delivered.
-
    fn next(&mut self) -> Option<(LocalTime, Scheduled)> {
-
        self.messages
-
            .iter()
-
            .next()
-
            .map(|(time, scheduled)| (*time, scheduled.clone()))
-
    }
-

-
    /// Get the last message sent between two peers. Only checks one direction.
-
    fn last(&self, node: &NodeId, remote: &net::SocketAddr) -> Option<(&LocalTime, &Scheduled)> {
-
        self.messages
-
            .iter()
-
            .rev()
-
            .find(|(_, v)| &v.node == node && &v.remote == remote)
-
    }
-
}
-

-
/// Simulation options.
-
#[derive(Debug, Clone)]
-
pub struct Options {
-
    /// Minimum and maximum latency between nodes, in seconds.
-
    pub latency: Range<u64>,
-
    /// Probability that network I/O fails.
-
    /// A rate of `1.0` means 100% of I/O fails.
-
    pub failure_rate: f64,
-
}
-

-
impl Default for Options {
-
    fn default() -> Self {
-
        Self {
-
            latency: Range::default(),
-
            failure_rate: 0.,
-
        }
-
    }
-
}
-

-
/// A peer-to-peer node simulation.
-
pub struct Simulation<S> {
-
    /// Inbox of inputs to be delivered by the simulation.
-
    inbox: Inbox,
-
    /// Events emitted during simulation.
-
    events: BTreeMap<NodeId, VecDeque<Event>>,
-
    /// Priority events that should happen immediately.
-
    priority: VecDeque<Scheduled>,
-
    /// Simulated latencies between nodes.
-
    latencies: BTreeMap<(NodeId, NodeId), LocalDuration>,
-
    /// Network partitions between two nodes.
-
    partitions: BTreeSet<(NodeId, NodeId)>,
-
    /// Set of existing connections between nodes.
-
    connections: BTreeMap<(NodeId, NodeId), u16>,
-
    /// Set of connection attempts.
-
    attempts: BTreeSet<(NodeId, NodeId)>,
-
    /// Simulation options.
-
    opts: Options,
-
    /// Start time of simulation.
-
    start_time: LocalTime,
-
    /// Current simulation time. Updated when a scheduled message is processed.
-
    time: LocalTime,
-
    /// RNG.
-
    rng: fastrand::Rng,
-
    /// Storage type.
-
    storage: PhantomData<S>,
-
}
-

-
impl<'r, S: WriteStorage<'r>> Simulation<S> {
-
    /// Create a new simulation.
-
    pub fn new(time: LocalTime, rng: fastrand::Rng, opts: Options) -> Self {
-
        Self {
-
            inbox: Inbox {
-
                messages: BTreeMap::new(),
-
            },
-
            events: BTreeMap::new(),
-
            priority: VecDeque::new(),
-
            partitions: BTreeSet::new(),
-
            latencies: BTreeMap::new(),
-
            connections: BTreeMap::new(),
-
            attempts: BTreeSet::new(),
-
            opts,
-
            start_time: time,
-
            time,
-
            rng,
-
            storage: PhantomData,
-
        }
-
    }
-

-
    /// Check whether the simulation is done, ie. there are no more messages to process.
-
    pub fn is_done(&self) -> bool {
-
        self.inbox.messages.is_empty()
-
    }
-

-
    /// Total amount of simulated time elapsed.
-
    #[allow(dead_code)]
-
    pub fn elapsed(&self) -> LocalDuration {
-
        self.time - self.start_time
-
    }
-

-
    /// Check whether the simulation has settled, ie. the only messages left to process
-
    /// are (periodic) timeouts.
-
    pub fn is_settled(&self) -> bool {
-
        self.inbox
-
            .messages
-
            .iter()
-
            .all(|(_, s)| matches!(s.input, Input::Wake))
-
    }
-

-
    /// Get a node's emitted events.
-
    pub fn events(&mut self, node: &NodeId) -> impl Iterator<Item = Event> + '_ {
-
        self.events.entry(*node).or_default().drain(..)
-
    }
-

-
    /// Get the latency between two nodes. The minimum latency between nodes is 1 millisecond.
-
    pub fn latency(&self, from: NodeId, to: NodeId) -> LocalDuration {
-
        self.latencies
-
            .get(&(from, to))
-
            .cloned()
-
            .map(|l| {
-
                if l <= MIN_LATENCY {
-
                    l
-
                } else {
-
                    // Create variance in the latency. The resulting latency
-
                    // will be between half, and two times the base latency.
-
                    let millis = l.as_millis();
-

-
                    if self.rng.bool() {
-
                        // More latency.
-
                        LocalDuration::from_millis(millis + self.rng.u128(0..millis))
-
                    } else {
-
                        // Less latency.
-
                        LocalDuration::from_millis(millis - self.rng.u128(0..millis / 2))
-
                    }
-
                }
-
            })
-
            .unwrap_or_else(|| MIN_LATENCY)
-
    }
-

-
    /// Initialize peers.
-
    pub fn initialize<'a, P>(self, peers: impl IntoIterator<Item = &'a mut P>) -> Self
-
    where
-
        P: Peer<S>,
-
    {
-
        for peer in peers.into_iter() {
-
            peer.init();
-
        }
-
        self
-
    }
-

-
    /// Run the simulation while the given predicate holds.
-
    pub fn run_while<'a, P>(
-
        &mut self,
-
        peers: impl IntoIterator<Item = &'a mut P>,
-
        pred: impl Fn(&Self) -> bool,
-
    ) where
-
        P: Peer<S>,
-
    {
-
        let mut nodes: BTreeMap<_, _> = peers.into_iter().map(|p| (p.addr().ip(), p)).collect();
-

-
        while self.step_(&mut nodes) {
-
            if !pred(self) {
-
                break;
-
            }
-
        }
-
    }
-

-
    /// 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 {
-
        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 {
-
        if !self.opts.latency.is_empty() {
-
            // Configure latencies.
-
            for (i, from) in nodes.keys().enumerate() {
-
                for to in nodes.keys().skip(i + 1) {
-
                    let range = self.opts.latency.clone();
-
                    let latency = LocalDuration::from_millis(
-
                        self.rng
-
                            .u128(range.start as u128 * 1_000..range.end as u128 * 1_000),
-
                    );
-

-
                    self.latencies.entry((*from, *to)).or_insert(latency);
-
                    self.latencies.entry((*to, *from)).or_insert(latency);
-
                }
-
            }
-
        }
-

-
        // Create and heal partitions.
-
        // TODO: These aren't really "network" partitions, as they are only
-
        // between individual nodes. We need to think about more realistic
-
        // scenarios. We should also think about creating various network
-
        // topologies.
-
        if self.time.as_secs() % 10 == 0 {
-
            for (i, x) in nodes.keys().enumerate() {
-
                for y in nodes.keys().skip(i + 1) {
-
                    if self.is_fallible() {
-
                        self.partitions.insert((*x, *y));
-
                    } else {
-
                        self.partitions.remove(&(*x, *y));
-
                    }
-
                }
-
            }
-
        }
-

-
        // Schedule any messages in the pipes.
-
        for peer in nodes.values_mut() {
-
            let ip = peer.addr().ip();
-

-
            for o in peer.by_ref() {
-
                self.schedule(&ip, o);
-
            }
-
        }
-
        // Next high-priority message.
-
        let priority = self.priority.pop_front().map(|s| (self.time, s));
-

-
        if let Some((time, next)) = priority.or_else(|| self.inbox.next()) {
-
            let elapsed = (time - self.start_time).as_millis();
-
            if matches!(next.input, Input::Wake) {
-
                trace!(target: "sim", "{:05} {}", elapsed, next);
-
            } else {
-
                // TODO: This can be confusing, since this event may not actually be passed to
-
                // the service. It would be best to only log the events that are being sent
-
                // to the service, or to log when an input is being dropped.
-
                info!(target: "sim", "{:05} {} ({})", elapsed, next, self.inbox.messages.len());
-
            }
-
            assert!(time >= self.time, "Time only moves forwards!");
-

-
            self.time = time;
-
            self.inbox.messages.remove(&time);
-

-
            let Scheduled { input, node, .. } = next;
-

-
            if let Some(ref mut p) = nodes.get_mut(&node) {
-
                p.tick(time);
-

-
                match input {
-
                    Input::Connecting { addr } => {
-
                        if self.attempts.insert((node, addr.ip())) {
-
                            p.attempted(&addr);
-
                        }
-
                    }
-
                    Input::Connected {
-
                        addr,
-
                        local_addr,
-
                        link,
-
                    } => {
-
                        let conn = (node, addr.ip());
-

-
                        let attempted = link.is_outbound() && self.attempts.remove(&conn);
-
                        if attempted || link.is_inbound() {
-
                            if self.connections.insert(conn, local_addr.port()).is_none() {
-
                                p.connected(addr, &local_addr, link);
-
                            }
-
                        }
-
                    }
-
                    Input::Disconnected(addr, reason) => {
-
                        let conn = (node, addr.ip());
-
                        let attempt = self.attempts.remove(&conn);
-
                        let connection = self.connections.remove(&conn).is_some();
-

-
                        // Can't be both attempting and connected.
-
                        assert!(!(attempt && connection));
-

-
                        if attempt || connection {
-
                            p.disconnected(&addr, reason);
-
                        }
-
                    }
-
                    Input::Wake => p.wake(),
-
                    Input::Received(addr, msgs) => {
-
                        for msg in msgs {
-
                            p.received_message(&addr, msg);
-
                        }
-
                    }
-
                }
-
                for o in p.by_ref() {
-
                    self.schedule(&node, o);
-
                }
-
            } else {
-
                panic!(
-
                    "Node {} not found when attempting to schedule {:?}",
-
                    node, input
-
                );
-
            }
-
        }
-
        !self.is_done()
-
    }
-

-
    /// Process a service output event from a node.
-
    pub fn schedule(&mut self, node: &NodeId, out: Io) {
-
        let node = *node;
-

-
        match out {
-
            Io::Write(receiver, msgs) => {
-
                if msgs.is_empty() {
-
                    return;
-
                }
-
                // If the other end has disconnected the sender with some latency, there may not be
-
                // a connection remaining to use.
-
                let port = if let Some(port) = self.connections.get(&(node, receiver.ip())) {
-
                    *port
-
                } else {
-
                    return;
-
                };
-

-
                let sender: net::SocketAddr = (node, port).into();
-
                if self.is_partitioned(sender.ip(), receiver.ip()) {
-
                    // Drop message if nodes are partitioned.
-
                    info!(
-
                        target: "sim",
-
                        "{} -> {} (DROPPED)",
-
                         sender, receiver,
-
                    );
-
                    return;
-
                }
-

-
                // Schedule message in the future, ensuring messages don't arrive out-of-order
-
                // between two peers.
-
                let latency = self.latency(node, receiver.ip());
-
                let time = self
-
                    .inbox
-
                    .last(&receiver.ip(), &sender)
-
                    .map(|(k, _)| *k)
-
                    .unwrap_or_else(|| self.time);
-
                let time = time + latency;
-
                let elapsed = (time - self.start_time).as_millis();
-

-
                for msg in &msgs {
-
                    info!(
-
                        target: "sim",
-
                        "{:05} {} -> {} ({:?}) (+{})",
-
                        elapsed, sender, receiver, msg, latency
-
                    );
-
                }
-

-
                self.inbox.insert(
-
                    time,
-
                    Scheduled {
-
                        remote: sender,
-
                        node: receiver.ip(),
-
                        input: Input::Received(sender, msgs),
-
                    },
-
                );
-
            }
-
            Io::Connect(remote) => {
-
                assert!(remote.ip() != node, "self-connections are not allowed");
-

-
                // Create an ephemeral sockaddr for the connecting (local) node.
-
                let local_addr: net::SocketAddr = net::SocketAddr::new(node, self.rng.u16(8192..));
-
                let latency = self.latency(node, remote.ip());
-

-
                self.inbox.insert(
-
                    self.time + MIN_LATENCY,
-
                    Scheduled {
-
                        node,
-
                        remote,
-
                        input: Input::Connecting { addr: remote },
-
                    },
-
                );
-

-
                // Fail to connect if the nodes are partitioned.
-
                if self.is_partitioned(node, remote.ip()) {
-
                    log::info!(target: "sim", "{} -/-> {} (partitioned)", node, remote.ip());
-

-
                    // Sometimes, the service gets a failure input, other times it just hangs.
-
                    if self.rng.bool() {
-
                        self.inbox.insert(
-
                            self.time + MIN_LATENCY,
-
                            Scheduled {
-
                                node,
-
                                remote,
-
                                input: Input::Disconnected(
-
                                    remote,
-
                                    nakamoto::DisconnectReason::ConnectionError(
-
                                        io::Error::from(io::ErrorKind::UnexpectedEof).into(),
-
                                    ),
-
                                ),
-
                            },
-
                        );
-
                    }
-
                    return;
-
                }
-

-
                self.inbox.insert(
-
                    // The remote will get the connection attempt with some latency.
-
                    self.time + latency,
-
                    Scheduled {
-
                        node: remote.ip(),
-
                        remote: local_addr,
-
                        input: Input::Connected {
-
                            addr: local_addr,
-
                            local_addr: remote,
-
                            link: Link::Inbound,
-
                        },
-
                    },
-
                );
-
                self.inbox.insert(
-
                    // The local node will have established the connection after some latency.
-
                    self.time + latency,
-
                    Scheduled {
-
                        remote,
-
                        node,
-
                        input: Input::Connected {
-
                            addr: remote,
-
                            local_addr,
-
                            link: Link::Outbound,
-
                        },
-
                    },
-
                );
-
            }
-
            Io::Disconnect(remote, reason) => {
-
                // The local node is immediately disconnected.
-
                self.priority.push_back(Scheduled {
-
                    remote,
-
                    node,
-
                    input: Input::Disconnected(remote, reason.into()),
-
                });
-

-
                // Nb. It's possible for disconnects to happen simultaneously from both ends, hence
-
                // it can be that a node will try to disconnect a remote that is already
-
                // disconnected from the other side.
-
                //
-
                // It's also possible that the connection was only attempted and never succeeded,
-
                // in which case we would return here.
-
                let port = if let Some(port) = self.connections.get(&(node, remote.ip())) {
-
                    *port
-
                } else {
-
                    debug!(target: "sim", "Ignoring disconnect of {remote} from {node}");
-
                    return;
-
                };
-
                let local_addr: net::SocketAddr = (node, port).into();
-
                let latency = self.latency(node, remote.ip());
-

-
                // The remote node receives the disconnection with some delay.
-
                self.inbox.insert(
-
                    self.time + latency,
-
                    Scheduled {
-
                        node: remote.ip(),
-
                        remote: local_addr,
-
                        input: Input::Disconnected(
-
                            local_addr,
-
                            nakamoto::DisconnectReason::ConnectionError(
-
                                io::Error::from(io::ErrorKind::ConnectionReset).into(),
-
                            ),
-
                        ),
-
                    },
-
                );
-
            }
-
            Io::Wakeup(duration) => {
-
                let time = self.time + duration;
-

-
                if !matches!(
-
                    self.inbox.messages.get(&time),
-
                    Some(Scheduled {
-
                        input: Input::Wake,
-
                        ..
-
                    })
-
                ) {
-
                    self.inbox.insert(
-
                        time,
-
                        Scheduled {
-
                            node,
-
                            // The remote is not applicable for this type of output.
-
                            remote: ([0, 0, 0, 0], 0).into(),
-
                            input: Input::Wake,
-
                        },
-
                    );
-
                }
-
            }
-
            Io::Event(event) => {
-
                let events = self.events.entry(node).or_insert_with(VecDeque::new);
-
                if events.len() >= MAX_EVENTS {
-
                    warn!(target: "sim", "Dropping event: buffer is full");
-
                } else {
-
                    events.push_back(event);
-
                }
-
            }
-
        }
-
    }
-

-
    /// Check whether we should fail the next operation.
-
    fn is_fallible(&self) -> bool {
-
        self.rng.f64() % 1.0 < self.opts.failure_rate
-
    }
-

-
    /// Check whether two nodes are partitioned.
-
    fn is_partitioned(&self, a: NodeId, b: NodeId) -> bool {
-
        self.partitions.contains(&(a, b)) || self.partitions.contains(&(b, a))
-
    }
-
}
deleted node/src/test/storage.rs
@@ -1,142 +0,0 @@
-
use git_url::Url;
-

-
use crate::crypto::{Signer, Verified};
-
use crate::git;
-
use crate::identity::{Id, Project};
-
use crate::storage::{refs, RefUpdate};
-
use crate::storage::{
-
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, RemoteId, WriteRepository,
-
    WriteStorage,
-
};
-

-
#[derive(Clone, Debug)]
-
pub struct MockStorage {
-
    pub inventory: Vec<Project>,
-
}
-

-
impl MockStorage {
-
    pub fn new(inventory: Vec<Project>) -> Self {
-
        Self { inventory }
-
    }
-

-
    pub fn empty() -> Self {
-
        Self {
-
            inventory: Vec::new(),
-
        }
-
    }
-
}
-

-
impl ReadStorage for MockStorage {
-
    fn url(&self) -> Url {
-
        Url {
-
            scheme: git_url::Scheme::Radicle,
-
            host: Some("mock".to_string()),
-
            ..Url::default()
-
        }
-
    }
-

-
    fn get(&self, _remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
-
        if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) {
-
            return Ok(Some(proj.clone()));
-
        }
-
        Ok(None)
-
    }
-

-
    fn inventory(&self) -> Result<Inventory, Error> {
-
        let inventory = self
-
            .inventory
-
            .iter()
-
            .map(|proj| proj.id.clone())
-
            .collect::<Vec<_>>();
-

-
        Ok(inventory)
-
    }
-
}
-

-
impl WriteStorage<'_> for MockStorage {
-
    type Repository = MockRepository;
-

-
    fn repository(&self, _proj: &Id) -> Result<Self::Repository, Error> {
-
        Ok(MockRepository {})
-
    }
-

-
    fn sign_refs<G: Signer>(
-
        &self,
-
        _repository: &Self::Repository,
-
        _signer: G,
-
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
-
        todo!()
-
    }
-
}
-

-
pub struct MockRepository {}
-

-
impl ReadRepository<'_> for MockRepository {
-
    type Remotes = std::iter::Empty<Result<(RemoteId, Remote<Verified>), refs::Error>>;
-

-
    fn is_empty(&self) -> Result<bool, git2::Error> {
-
        Ok(true)
-
    }
-

-
    fn path(&self) -> &std::path::Path {
-
        todo!()
-
    }
-

-
    fn remote(&self, _remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
-
        todo!()
-
    }
-

-
    fn remotes(&self) -> Result<Self::Remotes, git2::Error> {
-
        todo!()
-
    }
-

-
    fn commit(&self, _oid: git::Oid) -> Result<Option<git2::Commit>, git2::Error> {
-
        todo!()
-
    }
-

-
    fn revwalk(&self, _head: git::Oid) -> Result<git2::Revwalk, git2::Error> {
-
        todo!()
-
    }
-

-
    fn blob_at<'a>(
-
        &'a self,
-
        _oid: radicle_git_ext::Oid,
-
        _path: &'a std::path::Path,
-
    ) -> Result<git2::Blob<'a>, radicle_git_ext::Error> {
-
        todo!()
-
    }
-

-
    fn reference(
-
        &self,
-
        _remote: &RemoteId,
-
        _reference: &git::RefStr,
-
    ) -> Result<Option<git2::Reference>, git2::Error> {
-
        todo!()
-
    }
-

-
    fn reference_oid(
-
        &self,
-
        _remote: &RemoteId,
-
        _reference: &git::RefStr,
-
    ) -> Result<Option<radicle_git_ext::Oid>, git2::Error> {
-
        todo!()
-
    }
-

-
    fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> {
-
        todo!()
-
    }
-

-
    fn project(&self) -> Result<Project, Error> {
-
        todo!()
-
    }
-
}
-

-
impl WriteRepository<'_> for MockRepository {
-
    fn fetch(&mut self, _url: &Url) -> Result<Vec<RefUpdate>, FetchError> {
-
        Ok(vec![])
-
    }
-

-
    fn raw(&self) -> &git2::Repository {
-
        todo!()
-
    }
-
}
deleted node/src/test/tests.rs
@@ -1,530 +0,0 @@
-
use std::io;
-
use std::sync::Arc;
-

-
use crossbeam_channel as chan;
-
use nakamoto_net as nakamoto;
-

-
use crate::collections::{HashMap, HashSet};
-
use crate::service::config::*;
-
use crate::service::message::*;
-
use crate::service::peer::*;
-
use crate::service::*;
-
use crate::storage::git::Storage;
-
use crate::storage::ReadStorage;
-
use crate::test::fixtures;
-
#[allow(unused)]
-
use crate::test::logger;
-
use crate::test::peer::Peer;
-
use crate::test::simulator;
-
use crate::test::simulator::{Peer as _, Simulation};
-
use crate::test::storage::MockStorage;
-
use crate::{assert_matches, Link, LocalTime};
-
use crate::{client, identity, rad, service, storage, test};
-

-
// NOTE
-
//
-
// If you wish to see the logs for a running test, simply add the following line to your test:
-
//
-
//      logger::init(log::Level::Debug);
-
//
-
// You may then run the test with eg. `cargo test -- --nocapture` to always show output.
-

-
#[test]
-
fn test_outbound_connection() {
-
    let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
-
    let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
-
    let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
-

-
    alice.connect_to(&bob);
-
    alice.connect_to(&eve);
-

-
    let peers = alice
-
        .service
-
        .peers()
-
        .negotiated()
-
        .map(|(ip, _)| *ip)
-
        .collect::<Vec<_>>();
-

-
    assert!(peers.contains(&eve.ip));
-
    assert!(peers.contains(&bob.ip));
-
}
-

-
#[test]
-
fn test_inbound_connection() {
-
    let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
-
    let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
-
    let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
-

-
    alice.connect_from(&bob);
-
    alice.connect_from(&eve);
-

-
    let peers = alice
-
        .service
-
        .peers()
-
        .negotiated()
-
        .map(|(ip, _)| *ip)
-
        .collect::<Vec<_>>();
-

-
    assert!(peers.contains(&eve.ip));
-
    assert!(peers.contains(&bob.ip));
-
}
-

-
#[test]
-
fn test_persistent_peer_connect() {
-
    let 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.addr(), eve.addr()],
-
        ..Config::default()
-
    };
-
    let mut alice = Peer::config(
-
        "alice",
-
        config,
-
        [7, 7, 7, 7],
-
        vec![],
-
        MockStorage::empty(),
-
        rng,
-
    );
-

-
    alice.initialize();
-

-
    let mut outbox = alice.outbox();
-
    assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == bob.addr());
-
    assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == eve.addr());
-
    assert_matches!(outbox.next(), None);
-
}
-

-
#[test]
-
#[ignore]
-
fn test_wrong_peer_version() {
-
    // TODO
-
}
-

-
#[test]
-
fn test_handshake_invalid_timestamp() {
-
    let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
-
    let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
-
    let time_delta = MAX_TIME_DELTA.as_secs() + 1;
-
    let local = std::net::SocketAddr::new(bob.ip, bob.rng.u16(..));
-

-
    alice.initialize();
-
    alice.connected(bob.addr(), &local, Link::Inbound);
-
    alice.receive(
-
        &bob.addr(),
-
        Message::init(
-
            bob.node_id(),
-
            alice.timestamp() - time_delta,
-
            vec![],
-
            bob.git_url(),
-
        ),
-
    );
-
    assert_matches!(alice.outbox().next(), Some(Io::Disconnect(addr, _)) if addr == bob.addr());
-
}
-

-
#[test]
-
#[ignore]
-
fn test_wrong_peer_magic() {
-
    // TODO
-
}
-

-
#[test]
-
fn test_inventory_sync() {
-
    let tmp = tempfile::tempdir().unwrap();
-
    let mut alice = Peer::new(
-
        "alice",
-
        [7, 7, 7, 7],
-
        Storage::open(tmp.path().join("alice")).unwrap(),
-
    );
-
    let bob_storage = fixtures::storage(tmp.path().join("bob"));
-
    let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
-
    let now = LocalTime::now().as_secs();
-
    let projs = bob.storage().inventory().unwrap();
-

-
    alice.connect_to(&bob);
-
    alice.receive(
-
        &bob.addr(),
-
        Message::inventory(
-
            InventoryAnnouncement {
-
                inventory: projs.clone(),
-
                timestamp: now,
-
            },
-
            bob.signer(),
-
        ),
-
    );
-

-
    for proj in &projs {
-
        let seeds = alice.routing().get(proj).unwrap();
-
        assert!(seeds.contains(&bob.node_id()));
-
    }
-

-
    let a = alice
-
        .storage()
-
        .inventory()
-
        .unwrap()
-
        .into_iter()
-
        .collect::<HashSet<_>>();
-
    let b = projs.into_iter().collect::<HashSet<_>>();
-

-
    assert_eq!(a, b);
-
}
-

-
#[test]
-
fn test_tracking() {
-
    let mut alice = Peer::config(
-
        "alice",
-
        Config {
-
            project_tracking: ProjectTracking::Allowed(HashSet::default()),
-
            ..Config::default()
-
        },
-
        [7, 7, 7, 7],
-
        vec![],
-
        MockStorage::empty(),
-
        fastrand::Rng::new(),
-
    );
-
    let proj_id: identity::Id = test::arbitrary::gen(1);
-

-
    let (sender, receiver) = chan::bounded(1);
-
    alice.command(Command::Track(proj_id.clone(), sender));
-
    let policy_change = receiver
-
        .recv()
-
        .map_err(client::handle::Error::from)
-
        .unwrap();
-
    assert!(policy_change);
-
    assert!(alice.config().is_tracking(&proj_id));
-

-
    let (sender, receiver) = chan::bounded(1);
-
    alice.command(Command::Untrack(proj_id.clone(), sender));
-
    let policy_change = receiver
-
        .recv()
-
        .map_err(client::handle::Error::from)
-
        .unwrap();
-
    assert!(policy_change);
-
    assert!(!alice.config().is_tracking(&proj_id));
-
}
-

-
#[test]
-
fn test_inventory_relay_bad_timestamp() {
-
    let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
-
    let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
-
    let two_hours = 3600 * 2;
-
    let timestamp = alice.local_time.as_secs() - two_hours;
-

-
    alice.connect_to(&bob);
-
    alice.receive(
-
        &bob.addr(),
-
        Message::inventory(
-
            InventoryAnnouncement {
-
                inventory: vec![],
-
                timestamp,
-
            },
-
            bob.signer(),
-
        ),
-
    );
-
    assert_matches!(
-
        alice.outbox().next(),
-
        Some(Io::Disconnect(addr, DisconnectReason::Error(PeerError::InvalidTimestamp(t))))
-
        if addr == bob.addr() && t == timestamp
-
    );
-
}
-

-
#[test]
-
fn test_inventory_relay() {
-
    // Topology is eve <-> alice <-> bob
-
    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 inv = vec![];
-
    let now = LocalTime::now().as_secs();
-

-
    // Inventory from Bob relayed to Eve.
-
    alice.connect_to(&bob);
-
    alice.connect_from(&eve);
-
    alice.receive(
-
        &bob.addr(),
-
        Message::inventory(
-
            InventoryAnnouncement {
-
                inventory: inv.clone(),
-
                timestamp: now,
-
            },
-
            bob.signer(),
-
        ),
-
    );
-
    assert_matches!(
-
        alice.messages(&eve.addr()).next(),
-
        Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement { timestamp, .. }, .. })
-
        if node == bob.node_id() && timestamp == now
-
    );
-
    assert_matches!(
-
        alice.messages(&bob.addr()).next(),
-
        None,
-
        "The inventory is not sent back to Bob"
-
    );
-

-
    alice.receive(
-
        &bob.addr(),
-
        Message::inventory(
-
            InventoryAnnouncement {
-
                inventory: inv.clone(),
-
                timestamp: now,
-
            },
-
            bob.signer(),
-
        ),
-
    );
-
    assert_matches!(
-
        alice.messages(&eve.addr()).next(),
-
        None,
-
        "Sending the same inventory again doesn't trigger a relay"
-
    );
-

-
    alice.receive(
-
        &bob.addr(),
-
        Message::inventory(
-
            InventoryAnnouncement {
-
                inventory: inv.clone(),
-
                timestamp: now + 1,
-
            },
-
            bob.signer(),
-
        ),
-
    );
-
    assert_matches!(
-
        alice.messages(&eve.addr()).next(),
-
        Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement{ timestamp, .. }, .. })
-
        if node == bob.node_id() && timestamp == now + 1,
-
        "Sending a new inventory does trigger the relay"
-
    );
-

-
    // Inventory from Eve relayed to Bob.
-
    alice.receive(
-
        &eve.addr(),
-
        Message::inventory(
-
            InventoryAnnouncement {
-
                inventory: inv,
-
                timestamp: now,
-
            },
-
            eve.signer(),
-
        ),
-
    );
-
    assert_matches!(
-
        alice.messages(&bob.addr()).next(),
-
        Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement { timestamp, .. }, .. })
-
        if node == eve.node_id() && timestamp == now
-
    );
-
}
-

-
#[test]
-
fn test_persistent_peer_reconnect() {
-
    let mut bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
-
    let mut eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
-
    let mut alice = Peer::config(
-
        "alice",
-
        Config {
-
            connect: vec![bob.addr(), eve.addr()],
-
            ..Config::default()
-
        },
-
        [7, 7, 7, 7],
-
        vec![],
-
        MockStorage::empty(),
-
        fastrand::Rng::new(),
-
    );
-

-
    let mut sim = Simulation::new(
-
        LocalTime::now(),
-
        alice.rng.clone(),
-
        simulator::Options::default(),
-
    )
-
    .initialize([&mut alice, &mut bob, &mut eve]);
-

-
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
-

-
    let ips = alice
-
        .peers()
-
        .negotiated()
-
        .map(|(ip, _)| *ip)
-
        .collect::<Vec<_>>();
-
    assert!(ips.contains(&bob.ip));
-
    assert!(ips.contains(&eve.ip));
-

-
    // ... Negotiated ...
-
    //
-
    // Now let's disconnect a peer.
-

-
    // A transient error such as this will cause Alice to attempt a reconnection.
-
    let error = Arc::new(io::Error::from(io::ErrorKind::ConnectionReset));
-

-
    // A non-transient disconnect, such as one requested by the user will not trigger
-
    // a reconnection.
-
    alice.disconnected(
-
        &eve.addr(),
-
        nakamoto::DisconnectReason::DialError(error.clone()),
-
    );
-
    assert_matches!(alice.outbox().next(), None);
-

-
    for _ in 0..MAX_CONNECTION_ATTEMPTS {
-
        alice.disconnected(
-
            &bob.addr(),
-
            nakamoto::DisconnectReason::ConnectionError(error.clone()),
-
        );
-
        assert_matches!(alice.outbox().next(), Some(Io::Connect(a)) if a == bob.addr());
-
        assert_matches!(alice.outbox().next(), None);
-

-
        alice.attempted(&bob.addr());
-
    }
-

-
    // After the max connection attempts, a disconnect doesn't trigger a reconnect.
-
    alice.disconnected(
-
        &bob.addr(),
-
        nakamoto::DisconnectReason::ConnectionError(error),
-
    );
-
    assert_matches!(alice.outbox().next(), None);
-
}
-

-
#[test]
-
fn test_push_and_pull() {
-
    logger::init(log::Level::Debug);
-

-
    let tempdir = tempfile::tempdir().unwrap();
-

-
    let storage_alice = Storage::open(tempdir.path().join("alice").join("storage")).unwrap();
-
    let (repo, _) = fixtures::repository(tempdir.path().join("working"));
-
    let mut alice = Peer::new("alice", [7, 7, 7, 7], storage_alice);
-

-
    let storage_bob = Storage::open(tempdir.path().join("bob").join("storage")).unwrap();
-
    let mut bob = Peer::new("bob", [8, 8, 8, 8], storage_bob);
-

-
    let storage_eve = Storage::open(tempdir.path().join("eve").join("storage")).unwrap();
-
    let mut eve = Peer::new("eve", [9, 9, 9, 9], storage_eve);
-

-
    // Alice and Bob connect to Eve.
-
    alice.command(service::Command::Connect(eve.addr()));
-
    bob.command(service::Command::Connect(eve.addr()));
-

-
    let mut sim = Simulation::new(
-
        LocalTime::now(),
-
        alice.rng.clone(),
-
        simulator::Options::default(),
-
    )
-
    .initialize([&mut alice, &mut bob, &mut eve]);
-

-
    // Here we expect Alice to connect to Eve.
-
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
-

-
    // Alice creates a new project.
-
    let (proj_id, _) = rad::init(
-
        &repo,
-
        "alice",
-
        "alice's repo",
-
        storage::BranchName::from("master"),
-
        alice.signer(),
-
        alice.storage(),
-
    )
-
    .unwrap();
-

-
    // Bob tracks Alice's project.
-
    let (sender, _) = chan::bounded(1);
-
    bob.command(service::Command::Track(proj_id.clone(), sender));
-

-
    // Eve tracks Alice's project.
-
    let (sender, _) = chan::bounded(1);
-
    eve.command(service::Command::Track(proj_id.clone(), sender));
-

-
    // Neither of them have it in the beginning.
-
    assert!(eve.get(&proj_id).unwrap().is_none());
-
    assert!(bob.get(&proj_id).unwrap().is_none());
-

-
    // Alice announces her refs.
-
    // We now expect Eve to fetch Alice's project from Alice.
-
    // Then we expect Bob to fetch Alice's project from Eve.
-
    alice.command(service::Command::AnnounceRefs(proj_id.clone()));
-
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
-

-
    assert!(eve
-
        .storage()
-
        .get(&alice.node_id(), &proj_id)
-
        .unwrap()
-
        .is_some());
-
    assert!(bob
-
        .storage()
-
        .get(&alice.node_id(), &proj_id)
-
        .unwrap()
-
        .is_some());
-
    assert_matches!(
-
        sim.events(&bob.ip).next(),
-
        Some(service::Event::RefsFetched { from, .. })
-
        if from == eve.git_url(),
-
        "Bob fetched from Eve"
-
    );
-
}
-

-
#[test]
-
fn prop_inventory_exchange_dense() {
-
    fn property(alice_inv: MockStorage, bob_inv: MockStorage, eve_inv: MockStorage) {
-
        let rng = fastrand::Rng::new();
-
        let alice = Peer::new("alice", [7, 7, 7, 7], alice_inv.clone());
-
        let mut bob = Peer::new("bob", [8, 8, 8, 8], bob_inv.clone());
-
        let mut eve = Peer::new("eve", [9, 9, 9, 9], eve_inv.clone());
-
        let mut routing = Routing::with_hasher(rng.clone().into());
-

-
        for (inv, peer) in &[
-
            (alice_inv.inventory, alice.node_id()),
-
            (bob_inv.inventory, bob.node_id()),
-
            (eve_inv.inventory, eve.node_id()),
-
        ] {
-
            for proj in inv {
-
                routing
-
                    .entry(proj.id.clone())
-
                    .or_insert_with(|| HashSet::with_hasher(rng.clone().into()))
-
                    .insert(*peer);
-
            }
-
        }
-

-
        // Fully-connected.
-
        bob.command(Command::Connect(alice.addr()));
-
        bob.command(Command::Connect(eve.addr()));
-
        eve.command(Command::Connect(alice.addr()));
-
        eve.command(Command::Connect(bob.addr()));
-

-
        let mut peers: HashMap<_, _> = [
-
            (alice.node_id(), alice),
-
            (bob.node_id(), bob),
-
            (eve.node_id(), eve),
-
        ]
-
        .into_iter()
-
        .collect();
-
        let mut simulator = Simulation::new(LocalTime::now(), rng, simulator::Options::default())
-
            .initialize(peers.values_mut());
-

-
        simulator.run_while(peers.values_mut(), |s| !s.is_settled());
-

-
        for (proj_id, remotes) in &routing {
-
            for peer in peers.values() {
-
                let lookup = peer.lookup(proj_id);
-

-
                if lookup.local.is_some() {
-
                    peer.get(proj_id)
-
                        .expect("There are no errors querying storage")
-
                        .expect("The project is available locally");
-
                } else {
-
                    for remote in &lookup.remote {
-
                        peers[remote]
-
                            .get(proj_id)
-
                            .expect("There are no errors querying storage")
-
                            .expect("The project is available remotely");
-
                    }
-
                    assert!(
-
                        !lookup.remote.is_empty(),
-
                        "There are remote locations for the project"
-
                    );
-
                    assert_eq!(
-
                        &lookup.remote.into_iter().collect::<HashSet<_>>(),
-
                        remotes,
-
                        "The remotes match the global routing table"
-
                    );
-
                }
-
            }
-
        }
-
    }
-
    quickcheck::QuickCheck::new()
-
        .gen(quickcheck::Gen::new(8))
-
        .quickcheck(property as fn(MockStorage, MockStorage, MockStorage));
-
}
deleted node/src/transport.rs
@@ -1,107 +0,0 @@
-
use std::net;
-
use std::ops::{Deref, DerefMut};
-

-
use nakamoto::LocalTime;
-
use nakamoto_net as nakamoto;
-
use nakamoto_net::{Io, Link};
-

-
use crate::address_book;
-
use crate::collections::HashMap;
-
use crate::crypto;
-
use crate::service::{Command, DisconnectReason, Event, Service};
-
use crate::storage::WriteStorage;
-
use crate::wire::Wire;
-

-
#[derive(Debug)]
-
struct Peer {
-
    addr: net::SocketAddr,
-
}
-

-
#[derive(Debug)]
-
pub struct Transport<S, T, G> {
-
    peers: HashMap<net::IpAddr, Peer>,
-
    inner: Wire<S, T, G>,
-
}
-

-
impl<S, T, G> Transport<S, T, G> {
-
    pub fn new(inner: Wire<S, T, G>) -> Self {
-
        Self {
-
            peers: HashMap::default(),
-
            inner,
-
        }
-
    }
-
}
-

-
impl<'r, S, T, G> nakamoto::Protocol for Transport<S, T, G>
-
where
-
    T: WriteStorage<'r> + 'static,
-
    S: address_book::Store,
-
    G: crypto::Signer,
-
{
-
    type Event = Event;
-
    type Command = Command;
-
    type DisconnectReason = DisconnectReason;
-

-
    fn initialize(&mut self, time: LocalTime) {
-
        self.inner.initialize(time)
-
    }
-

-
    fn tick(&mut self, now: nakamoto::LocalTime) {
-
        self.inner.tick(now)
-
    }
-

-
    fn wake(&mut self) {
-
        self.inner.wake()
-
    }
-

-
    fn command(&mut self, cmd: Self::Command) {
-
        self.inner.command(cmd)
-
    }
-

-
    fn attempted(&mut self, addr: &std::net::SocketAddr) {
-
        self.inner.attempted(addr)
-
    }
-

-
    fn connected(
-
        &mut self,
-
        addr: std::net::SocketAddr,
-
        local_addr: &std::net::SocketAddr,
-
        link: Link,
-
    ) {
-
        self.inner.connected(addr, local_addr, link)
-
    }
-

-
    fn disconnected(
-
        &mut self,
-
        addr: &std::net::SocketAddr,
-
        reason: nakamoto::DisconnectReason<Self::DisconnectReason>,
-
    ) {
-
        self.inner.disconnected(addr, reason)
-
    }
-

-
    fn received_bytes(&mut self, addr: &std::net::SocketAddr, bytes: &[u8]) {
-
        self.inner.received_bytes(addr, bytes)
-
    }
-
}
-

-
impl<S, T, G> Iterator for Transport<S, T, G> {
-
    type Item = Io<Event, DisconnectReason>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        self.inner.next()
-
    }
-
}
-

-
impl<S, T, G> Deref for Transport<S, T, G> {
-
    type Target = Service<S, T, G>;
-

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

-
impl<S, T, G> DerefMut for Transport<S, T, G> {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.inner
-
    }
-
}
deleted node/src/wire.rs
@@ -1,642 +0,0 @@
-
pub mod message;
-

-
use std::collections::{BTreeMap, HashMap};
-
use std::convert::TryFrom;
-
use std::net::IpAddr;
-
use std::ops::{Deref, DerefMut};
-
use std::string::FromUtf8Error;
-
use std::{io, mem};
-

-
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
-
use nakamoto_net as nakamoto;
-
use nakamoto_net::Link;
-

-
use crate::address_book;
-
use crate::crypto::{PublicKey, Signature, Signer};
-
use crate::decoder::Decoder;
-
use crate::git;
-
use crate::git::fmt;
-
use crate::hash::Digest;
-
use crate::identity::Id;
-
use crate::service;
-
use crate::service::filter;
-
use crate::storage::refs::Refs;
-
use crate::storage::WriteStorage;
-

-
/// The default type we use to represent sizes.
-
/// Four bytes is more than enough for anything sent over the wire.
-
/// Note that in certain cases, we may use only one or two byte types.
-
pub type Size = u32;
-

-
#[derive(thiserror::Error, Debug)]
-
pub enum Error {
-
    #[error("i/o: {0}")]
-
    Io(#[from] io::Error),
-
    #[error("UTF-8 error: {0}")]
-
    FromUtf8(#[from] FromUtf8Error),
-
    #[error("invalid size: expected {expected}, got {actual}")]
-
    InvalidSize { expected: usize, actual: usize },
-
    #[error("invalid filter size: {0}")]
-
    InvalidFilterSize(usize),
-
    #[error(transparent)]
-
    InvalidRefName(#[from] fmt::Error),
-
    #[error("invalid git url `{url}`: {error}")]
-
    InvalidGitUrl {
-
        url: String,
-
        error: git::url::parse::Error,
-
    },
-
    #[error("unknown address type `{0}`")]
-
    UnknownAddressType(u8),
-
    #[error("unknown message type `{0}`")]
-
    UnknownMessageType(u16),
-
}
-

-
impl Error {
-
    /// Whether we've reached the end of file. This will be true when we fail to decode
-
    /// a message because there's not enough data in the stream.
-
    pub fn is_eof(&self) -> bool {
-
        matches!(self, Self::Io(err) if err.kind() == io::ErrorKind::UnexpectedEof)
-
    }
-
}
-

-
/// Things that can be encoded as binary.
-
pub trait Encode {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error>;
-
}
-

-
/// Things that can be decoded from binary.
-
pub trait Decode: Sized {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error>;
-
}
-

-
/// Encode an object into a vector.
-
pub fn serialize<T: Encode + ?Sized>(data: &T) -> Vec<u8> {
-
    let mut buffer = Vec::new();
-
    let len = data
-
        .encode(&mut buffer)
-
        .expect("in-memory writes don't error");
-

-
    debug_assert_eq!(len, buffer.len());
-

-
    buffer
-
}
-

-
/// Decode an object from a vector.
-
pub fn deserialize<T: Decode>(data: &[u8]) -> Result<T, Error> {
-
    let mut cursor = io::Cursor::new(data);
-

-
    T::decode(&mut cursor)
-
}
-

-
impl Encode for u8 {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        writer.write_u8(*self)?;
-

-
        Ok(mem::size_of::<Self>())
-
    }
-
}
-

-
impl Encode for u16 {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        writer.write_u16::<NetworkEndian>(*self)?;
-

-
        Ok(mem::size_of::<Self>())
-
    }
-
}
-

-
impl Encode for u32 {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        writer.write_u32::<NetworkEndian>(*self)?;
-

-
        Ok(mem::size_of::<Self>())
-
    }
-
}
-

-
impl Encode for u64 {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        writer.write_u64::<NetworkEndian>(*self)?;
-

-
        Ok(mem::size_of::<Self>())
-
    }
-
}
-

-
impl Encode for usize {
-
    /// We encode this type to a [`u32`], since there's no need to send larger messages
-
    /// over the network.
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        assert!(
-
            *self <= u32::MAX as usize,
-
            "Cannot encode sizes larger than {}",
-
            u32::MAX
-
        );
-
        writer.write_u32::<NetworkEndian>(*self as u32)?;
-

-
        Ok(mem::size_of::<u32>())
-
    }
-
}
-

-
impl Encode for PublicKey {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        self.deref().encode(writer)
-
    }
-
}
-

-
impl<const T: usize> Encode for &[u8; T] {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        writer.write_all(*self)?;
-

-
        Ok(mem::size_of::<Self>())
-
    }
-
}
-

-
impl<const T: usize> Encode for [u8; T] {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        writer.write_all(self)?;
-

-
        Ok(mem::size_of::<Self>())
-
    }
-
}
-

-
impl<T> Encode for &[T]
-
where
-
    T: Encode,
-
{
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = (self.len() as Size).encode(writer)?;
-

-
        for item in self.iter() {
-
            n += item.encode(writer)?;
-
        }
-
        Ok(n)
-
    }
-
}
-

-
impl Encode for &str {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        assert!(self.len() <= u8::MAX as usize);
-

-
        let n = (self.len() as u8).encode(writer)?;
-
        let bytes = self.as_bytes();
-

-
        // Nb. Don't use the [`Encode`] instance here for &[u8], because we are prefixing the
-
        // length ourselves.
-
        writer.write_all(bytes)?;
-

-
        Ok(n + bytes.len())
-
    }
-
}
-

-
impl Encode for String {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        self.as_str().encode(writer)
-
    }
-
}
-

-
impl Encode for git::Url {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        self.to_string().encode(writer)
-
    }
-
}
-

-
impl Encode for Digest {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        self.as_ref().encode(writer)
-
    }
-
}
-

-
impl Encode for Id {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        self.deref().encode(writer)
-
    }
-
}
-

-
impl Encode for Refs {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = self.len().encode(writer)?;
-

-
        for (name, oid) in self.iter() {
-
            n += name.as_str().encode(writer)?;
-
            n += oid.encode(writer)?;
-
        }
-
        Ok(n)
-
    }
-
}
-

-
impl Encode for Signature {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        self.deref().encode(writer)
-
    }
-
}
-

-
impl Encode for git::Oid {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        // Nb. We use length-encoding here to support future SHA-2 object ids.
-
        self.as_bytes().encode(writer)
-
    }
-
}
-

-
////////////////////////////////////////////////////////////////////////////////
-

-
impl Decode for PublicKey {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let buf: [u8; 32] = Decode::decode(reader)?;
-

-
        PublicKey::try_from(buf)
-
            .map_err(|e| Error::Io(io::Error::new(io::ErrorKind::InvalidInput, e.to_string())))
-
    }
-
}
-

-
impl Decode for Refs {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let len = usize::decode(reader)?;
-
        let mut refs = BTreeMap::new();
-

-
        for _ in 0..len {
-
            let name = String::decode(reader)?;
-
            let name = git::RefString::try_from(name).map_err(Error::from)?;
-
            let oid = git::Oid::decode(reader)?;
-

-
            refs.insert(name, oid);
-
        }
-
        Ok(refs.into())
-
    }
-
}
-

-
impl Decode for git::Oid {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let len = usize::decode(reader)?;
-
        #[allow(non_upper_case_globals)]
-
        const expected: usize = mem::size_of::<git2::Oid>();
-

-
        if len != expected {
-
            return Err(Error::InvalidSize {
-
                expected,
-
                actual: len,
-
            });
-
        }
-

-
        let buf: [u8; expected] = Decode::decode(reader)?;
-
        let oid = git2::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
-
        let oid = git::Oid::from(oid);
-

-
        Ok(oid)
-
    }
-
}
-

-
impl Decode for Signature {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let bytes: [u8; 64] = Decode::decode(reader)?;
-

-
        Ok(Signature::from(bytes))
-
    }
-
}
-

-
impl Decode for u8 {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        reader.read_u8().map_err(Error::from)
-
    }
-
}
-

-
impl Decode for u16 {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        reader.read_u16::<NetworkEndian>().map_err(Error::from)
-
    }
-
}
-

-
impl Decode for u32 {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        reader.read_u32::<NetworkEndian>().map_err(Error::from)
-
    }
-
}
-

-
impl Decode for u64 {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        reader.read_u64::<NetworkEndian>().map_err(Error::from)
-
    }
-
}
-

-
impl Decode for usize {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let size: usize = u32::decode(reader)?
-
            .try_into()
-
            .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
-

-
        Ok(size)
-
    }
-
}
-

-
impl<const N: usize> Decode for [u8; N] {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let mut ary = [0; N];
-
        reader.read_exact(&mut ary)?;
-

-
        Ok(ary)
-
    }
-
}
-

-
impl<T> Decode for Vec<T>
-
where
-
    T: Decode,
-
{
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let len: Size = Size::decode(reader)?;
-
        let mut vec = Vec::with_capacity(len as usize);
-

-
        for _ in 0..len {
-
            let item = T::decode(reader)?;
-
            vec.push(item);
-
        }
-
        Ok(vec)
-
    }
-
}
-

-
impl Decode for String {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let len = u8::decode(reader)?;
-
        let mut bytes = vec![0; len as usize];
-

-
        reader.read_exact(&mut bytes)?;
-

-
        let string = String::from_utf8(bytes)?;
-

-
        Ok(string)
-
    }
-
}
-

-
impl Decode for git::Url {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let url = String::decode(reader)?;
-
        let url = Self::from_bytes(url.as_bytes())
-
            .map_err(|error| Error::InvalidGitUrl { url, error })?;
-

-
        Ok(url)
-
    }
-
}
-

-
impl Decode for Id {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let oid: git::Oid = Decode::decode(reader)?;
-

-
        Ok(Self::from(oid))
-
    }
-
}
-

-
impl Decode for Digest {
-
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let bytes: [u8; 32] = Decode::decode(reader)?;
-

-
        Ok(Self::from(bytes))
-
    }
-
}
-

-
impl Encode for filter::Filter {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = 0;
-

-
        n += self.deref().as_bytes().encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl Decode for filter::Filter {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
-
        let size: Size = Decode::decode(reader)?;
-
        if size as usize != filter::FILTER_SIZE {
-
            return Err(Error::InvalidFilterSize(size as usize));
-
        }
-
        let bytes: [u8; filter::FILTER_SIZE] = Decode::decode(reader)?;
-
        let bf = filter::BloomFilter::from(Vec::from(bytes));
-

-
        debug_assert_eq!(bf.hashes(), filter::FILTER_HASHES);
-

-
        Ok(Self::from(bf))
-
    }
-
}
-

-
#[derive(Debug)]
-
pub struct Wire<S, T, G> {
-
    inboxes: HashMap<IpAddr, Decoder>,
-
    inner: service::Service<S, T, G>,
-
}
-

-
impl<S, T, G> Wire<S, T, G> {
-
    pub fn new(inner: service::Service<S, T, G>) -> Self {
-
        Self {
-
            inboxes: HashMap::new(),
-
            inner,
-
        }
-
    }
-
}
-

-
impl<'r, S, T, G> Wire<S, T, G>
-
where
-
    S: address_book::Store,
-
    T: WriteStorage<'r> + 'static,
-
    G: Signer,
-
{
-
    pub fn connected(
-
        &mut self,
-
        addr: std::net::SocketAddr,
-
        local_addr: &std::net::SocketAddr,
-
        link: Link,
-
    ) {
-
        self.inboxes.insert(addr.ip(), Decoder::new(256));
-
        self.inner.connected(addr, local_addr, link)
-
    }
-

-
    pub fn disconnected(
-
        &mut self,
-
        addr: &std::net::SocketAddr,
-
        reason: nakamoto::DisconnectReason<service::DisconnectReason>,
-
    ) {
-
        self.inboxes.remove(&addr.ip());
-
        self.inner.disconnected(addr, reason)
-
    }
-

-
    pub fn received_bytes(&mut self, addr: &std::net::SocketAddr, bytes: &[u8]) {
-
        let peer_ip = addr.ip();
-

-
        if let Some(inbox) = self.inboxes.get_mut(&peer_ip) {
-
            inbox.input(bytes);
-

-
            loop {
-
                match inbox.decode_next() {
-
                    Ok(Some(msg)) => self.inner.received_message(addr, msg),
-
                    Ok(None) => break,
-

-
                    Err(err) => {
-
                        // TODO: Disconnect peer.
-
                        log::error!("Invalid message received from {}: {}", peer_ip, err);
-

-
                        return;
-
                    }
-
                }
-
            }
-
        } else {
-
            log::debug!("Received message from unknown peer {}", peer_ip);
-
        }
-
    }
-
}
-

-
impl<S, T, G> Iterator for Wire<S, T, G> {
-
    type Item = nakamoto::Io<service::Event, service::DisconnectReason>;
-

-
    fn next(&mut self) -> Option<Self::Item> {
-
        match self.inner.next() {
-
            Some(service::Io::Write(addr, msgs)) => {
-
                let mut buf = Vec::new();
-
                for msg in msgs {
-
                    log::debug!("Write {:?} to {}", &msg, addr.ip());
-

-
                    msg.encode(&mut buf)
-
                        .expect("writing to an in-memory buffer doesn't fail");
-
                }
-
                Some(nakamoto::Io::Write(addr, buf))
-
            }
-
            Some(service::Io::Event(e)) => Some(nakamoto::Io::Event(e)),
-
            Some(service::Io::Connect(a)) => Some(nakamoto::Io::Connect(a)),
-
            Some(service::Io::Disconnect(a, r)) => Some(nakamoto::Io::Disconnect(a, r)),
-
            Some(service::Io::Wakeup(d)) => Some(nakamoto::Io::Wakeup(d)),
-

-
            None => None,
-
        }
-
    }
-
}
-

-
impl<S, T, G> Deref for Wire<S, T, G> {
-
    type Target = service::Service<S, T, G>;
-

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

-
impl<S, T, G> DerefMut for Wire<S, T, G> {
-
    fn deref_mut(&mut self) -> &mut Self::Target {
-
        &mut self.inner
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    use crate::crypto::Unverified;
-
    use crate::storage::refs::SignedRefs;
-
    use crate::test::arbitrary;
-

-
    #[quickcheck]
-
    fn prop_u8(input: u8) {
-
        assert_eq!(deserialize::<u8>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_u16(input: u16) {
-
        assert_eq!(deserialize::<u16>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_u32(input: u32) {
-
        assert_eq!(deserialize::<u32>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_u64(input: u64) {
-
        assert_eq!(deserialize::<u64>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_usize(input: usize) -> quickcheck::TestResult {
-
        if input > u32::MAX as usize {
-
            return quickcheck::TestResult::discard();
-
        }
-
        assert_eq!(deserialize::<usize>(&serialize(&input)).unwrap(), input);
-

-
        quickcheck::TestResult::passed()
-
    }
-

-
    #[quickcheck]
-
    fn prop_string(input: String) -> quickcheck::TestResult {
-
        if input.len() > u8::MAX as usize {
-
            return quickcheck::TestResult::discard();
-
        }
-
        assert_eq!(deserialize::<String>(&serialize(&input)).unwrap(), input);
-

-
        quickcheck::TestResult::passed()
-
    }
-

-
    #[quickcheck]
-
    fn prop_vec(input: Vec<String>) {
-
        assert_eq!(
-
            deserialize::<Vec<String>>(&serialize(&input.as_slice())).unwrap(),
-
            input
-
        );
-
    }
-

-
    #[quickcheck]
-
    fn prop_pubkey(input: PublicKey) {
-
        assert_eq!(deserialize::<PublicKey>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_id(input: Id) {
-
        assert_eq!(deserialize::<Id>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_digest(input: Digest) {
-
        assert_eq!(deserialize::<Digest>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_refs(input: Refs) {
-
        assert_eq!(deserialize::<Refs>(&serialize(&input)).unwrap(), input);
-
    }
-

-
    #[quickcheck]
-
    fn prop_signature(input: arbitrary::ByteArray<64>) {
-
        let signature = Signature::from(input.into_inner());
-

-
        assert_eq!(
-
            deserialize::<Signature>(&serialize(&signature)).unwrap(),
-
            signature
-
        );
-
    }
-

-
    #[quickcheck]
-
    fn prop_oid(input: arbitrary::ByteArray<20>) {
-
        let oid = git::Oid::try_from(input.into_inner().as_slice()).unwrap();
-

-
        assert_eq!(deserialize::<git::Oid>(&serialize(&oid)).unwrap(), oid);
-
    }
-

-
    #[quickcheck]
-
    fn prop_signed_refs(input: SignedRefs<Unverified>) {
-
        assert_eq!(
-
            deserialize::<SignedRefs<Unverified>>(&serialize(&input)).unwrap(),
-
            input
-
        );
-
    }
-

-
    #[test]
-
    fn test_string() {
-
        assert_eq!(
-
            serialize(&String::from("hello")),
-
            vec![5, b'h', b'e', b'l', b'l', b'o']
-
        );
-
    }
-

-
    #[test]
-
    fn test_git_url() {
-
        let url = git::Url {
-
            scheme: git::url::Scheme::Https,
-
            path: "/git".to_owned().into(),
-
            host: Some("seed.radicle.xyz".to_owned()),
-
            port: Some(8888),
-
            ..git::Url::default()
-
        };
-
        assert_eq!(deserialize::<git::Url>(&serialize(&url)).unwrap(), url);
-
    }
-
}
deleted node/src/wire/message.rs
@@ -1,377 +0,0 @@
-
use std::{io, net};
-

-
use byteorder::{NetworkEndian, ReadBytesExt};
-

-
use crate::git;
-
use crate::prelude::*;
-
use crate::service::message::*;
-
use crate::wire;
-

-
/// Message type.
-
#[repr(u16)]
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-
pub enum MessageType {
-
    Initialize = 0,
-
    NodeAnnouncement = 2,
-
    InventoryAnnouncement = 4,
-
    RefsAnnouncement = 6,
-
    Subscribe = 8,
-
}
-

-
impl From<MessageType> for u16 {
-
    fn from(other: MessageType) -> Self {
-
        other as u16
-
    }
-
}
-

-
impl TryFrom<u16> for MessageType {
-
    type Error = u16;
-

-
    fn try_from(other: u16) -> Result<Self, Self::Error> {
-
        match other {
-
            0 => Ok(MessageType::Initialize),
-
            2 => Ok(MessageType::NodeAnnouncement),
-
            4 => Ok(MessageType::InventoryAnnouncement),
-
            6 => Ok(MessageType::RefsAnnouncement),
-
            8 => Ok(MessageType::Subscribe),
-
            _ => Err(other),
-
        }
-
    }
-
}
-

-
impl Message {
-
    pub fn type_id(&self) -> u16 {
-
        match self {
-
            Self::Initialize { .. } => MessageType::Initialize,
-
            Self::Subscribe { .. } => MessageType::Subscribe,
-
            Self::NodeAnnouncement { .. } => MessageType::NodeAnnouncement,
-
            Self::InventoryAnnouncement { .. } => MessageType::InventoryAnnouncement,
-
            Self::RefsAnnouncement { .. } => MessageType::RefsAnnouncement,
-
        }
-
        .into()
-
    }
-
}
-

-
/// 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 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),
-
        }
-
    }
-
}
-

-
impl wire::Encode for RefsAnnouncement {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = 0;
-

-
        n += self.id.encode(writer)?;
-
        n += self.refs.encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for RefsAnnouncement {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let id = Id::decode(reader)?;
-
        let refs = Refs::decode(reader)?;
-

-
        Ok(Self { id, refs })
-
    }
-
}
-

-
impl wire::Encode for InventoryAnnouncement {
-
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
-
        let mut n = 0;
-

-
        n += self.inventory.as_slice().encode(writer)?;
-
        n += self.timestamp.encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for InventoryAnnouncement {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let inventory = Vec::<Id>::decode(reader)?;
-
        let timestamp = Timestamp::decode(reader)?;
-

-
        Ok(Self {
-
            inventory,
-
            timestamp,
-
        })
-
    }
-
}
-

-
impl wire::Encode for Message {
-
    fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
-
        let mut n = self.type_id().encode(writer)?;
-

-
        match self {
-
            Self::Initialize {
-
                id,
-
                timestamp,
-
                version,
-
                addrs,
-
                git,
-
            } => {
-
                n += id.encode(writer)?;
-
                n += timestamp.encode(writer)?;
-
                n += version.encode(writer)?;
-
                n += addrs.as_slice().encode(writer)?;
-
                n += git.encode(writer)?;
-
            }
-
            Self::Subscribe(Subscribe {
-
                filter,
-
                since,
-
                until,
-
            }) => {
-
                n += filter.encode(writer)?;
-
                n += since.encode(writer)?;
-
                n += until.encode(writer)?;
-
            }
-
            Self::RefsAnnouncement {
-
                node,
-
                message,
-
                signature,
-
            } => {
-
                n += node.encode(writer)?;
-
                n += message.encode(writer)?;
-
                n += signature.encode(writer)?;
-
            }
-
            Self::InventoryAnnouncement {
-
                node,
-
                message,
-
                signature,
-
            } => {
-
                n += node.encode(writer)?;
-
                n += message.encode(writer)?;
-
                n += signature.encode(writer)?;
-
            }
-
            Self::NodeAnnouncement {
-
                node,
-
                message,
-
                signature,
-
            } => {
-
                n += node.encode(writer)?;
-
                n += message.encode(writer)?;
-
                n += signature.encode(writer)?;
-
            }
-
        }
-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for Message {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let type_id = reader.read_u16::<NetworkEndian>()?;
-

-
        match MessageType::try_from(type_id) {
-
            Ok(MessageType::Initialize) => {
-
                let id = NodeId::decode(reader)?;
-
                let timestamp = Timestamp::decode(reader)?;
-
                let version = u32::decode(reader)?;
-
                let addrs = Vec::<Address>::decode(reader)?;
-
                let git = git::Url::decode(reader)?;
-

-
                Ok(Self::Initialize {
-
                    id,
-
                    timestamp,
-
                    version,
-
                    addrs,
-
                    git,
-
                })
-
            }
-
            Ok(MessageType::Subscribe) => {
-
                let filter = Filter::decode(reader)?;
-
                let since = Timestamp::decode(reader)?;
-
                let until = Timestamp::decode(reader)?;
-

-
                Ok(Self::Subscribe(Subscribe {
-
                    filter,
-
                    since,
-
                    until,
-
                }))
-
            }
-
            Ok(MessageType::NodeAnnouncement) => {
-
                let node = NodeId::decode(reader)?;
-
                let message = NodeAnnouncement::decode(reader)?;
-
                let signature = Signature::decode(reader)?;
-

-
                Ok(Self::NodeAnnouncement {
-
                    node,
-
                    message,
-
                    signature,
-
                })
-
            }
-
            Ok(MessageType::InventoryAnnouncement) => {
-
                let node = NodeId::decode(reader)?;
-
                let message = InventoryAnnouncement::decode(reader)?;
-
                let signature = Signature::decode(reader)?;
-

-
                Ok(Self::InventoryAnnouncement {
-
                    node,
-
                    message,
-
                    signature,
-
                })
-
            }
-
            Ok(MessageType::RefsAnnouncement) => {
-
                let node = NodeId::decode(reader)?;
-
                let message = RefsAnnouncement::decode(reader)?;
-
                let signature = Signature::decode(reader)?;
-

-
                Ok(Self::RefsAnnouncement {
-
                    node,
-
                    message,
-
                    signature,
-
                })
-
            }
-
            Err(other) => Err(wire::Error::UnknownMessageType(other)),
-
        }
-
    }
-
}
-

-
impl wire::Encode for Envelope {
-
    fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
-
        let mut n = 0;
-

-
        n += self.magic.encode(writer)?;
-
        n += self.msg.encode(writer)?;
-

-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for Envelope {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let magic = u32::decode(reader)?;
-
        let msg = Message::decode(reader)?;
-

-
        Ok(Self { magic, msg })
-
    }
-
}
-

-
impl wire::Encode for Address {
-
    fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
-
        let mut n = 0;
-

-
        match self {
-
            Self::Ipv4 { ip, port } => {
-
                n += u8::from(AddressType::Ipv4).encode(writer)?;
-
                n += ip.octets().encode(writer)?;
-
                n += port.encode(writer)?;
-
            }
-
            Self::Ipv6 { ip, port } => {
-
                n += u8::from(AddressType::Ipv6).encode(writer)?;
-
                n += ip.octets().encode(writer)?;
-
                n += port.encode(writer)?;
-
            }
-
            Self::Hostname { .. } => todo!(),
-
            Self::Onion { .. } => todo!(),
-
        }
-
        Ok(n)
-
    }
-
}
-

-
impl wire::Decode for Address {
-
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
-
        let addrtype = reader.read_u8()?;
-

-
        match AddressType::try_from(addrtype) {
-
            Ok(AddressType::Ipv4) => {
-
                let octets: [u8; 4] = wire::Decode::decode(reader)?;
-
                let ip = net::Ipv4Addr::from(octets);
-
                let port = u16::decode(reader)?;
-

-
                Ok(Self::Ipv4 { ip, port })
-
            }
-
            Ok(AddressType::Ipv6) => {
-
                let octets: [u8; 16] = wire::Decode::decode(reader)?;
-
                let ip = net::Ipv6Addr::from(octets);
-
                let port = u16::decode(reader)?;
-

-
                Ok(Self::Ipv6 { ip, port })
-
            }
-
            Ok(AddressType::Hostname) => {
-
                todo!();
-
            }
-
            Ok(AddressType::Onion) => {
-
                todo!();
-
            }
-
            Err(other) => Err(wire::Error::UnknownAddressType(other)),
-
        }
-
    }
-
}
-

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
-

-
    use crate::decoder::Decoder;
-
    use crate::wire::{self, Encode};
-

-
    #[quickcheck]
-
    fn prop_message_encode_decode(message: Message) {
-
        assert_eq!(
-
            wire::deserialize::<Message>(&wire::serialize(&message)).unwrap(),
-
            message
-
        );
-
    }
-

-
    #[quickcheck]
-
    fn prop_envelope_encode_decode(envelope: Envelope) {
-
        assert_eq!(
-
            wire::deserialize::<Envelope>(&wire::serialize(&envelope)).unwrap(),
-
            envelope
-
        );
-
    }
-

-
    #[test]
-
    fn prop_envelope_decoder() {
-
        fn property(items: Vec<Envelope>) {
-
            let mut decoder = Decoder::<Envelope>::new(8);
-

-
            for item in &items {
-
                item.encode(&mut decoder).unwrap();
-
            }
-
            for item in items {
-
                assert_eq!(decoder.next().unwrap().unwrap(), item);
-
            }
-
        }
-

-
        quickcheck::QuickCheck::new()
-
            .gen(quickcheck::Gen::new(16))
-
            .quickcheck(property as fn(items: Vec<Envelope>));
-
    }
-

-
    #[quickcheck]
-
    fn prop_addr(addr: Address) {
-
        assert_eq!(
-
            wire::deserialize::<Address>(&wire::serialize(&addr)).unwrap(),
-
            addr
-
        );
-
    }
-
}
added radicle-node/Cargo.toml
@@ -0,0 +1,38 @@
+
[package]
+
name = "radicle-node"
+
license = "MIT OR Apache-2.0"
+
version = "0.2.0"
+
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
+
edition = "2021"
+

+
[dependencies]
+
anyhow = { version = "1" }
+
bs58 = { version = "0.4.0" }
+
ed25519-compact = { version = "1.0.12", features = ["pem"] }
+
byteorder = { version = "1" }
+
bloomy = { version = "1.2" }
+
chrono = { version = "0.4.0" }
+
colored = { version = "1.9.0" }
+
crossbeam-channel = { version = "0.5.6" }
+
fastrand = { version = "1.8.0" }
+
git-ref-format = { version = "0", features = ["serde", "macro"] }
+
git2 = { version = "0.13" }
+
git-url = { version = "0.3.5", features = ["serde1"] }
+
multibase = { version = "0.9.1" }
+
log = { version = "0.4.17", features = ["std"] }
+
once_cell = { version = "1.13" }
+
olpc-cjson = { version = "0.1.1" }
+
sha2 = { version = "0.10.2" }
+
serde = { version = "1", features = ["derive"] }
+
serde_json = { version = "1", features = ["preserve_order"] }
+
siphasher = { version = "0.3.10" }
+
radicle-git-ext = { version = "0", features = ["serde"] }
+
nonempty = { version = "0.8.0", features = ["serialize"] }
+
nakamoto-net = { version = "0.3.0" }
+
nakamoto-net-poll = { version = "0.3.0" }
+
tempfile = { version = "3.3.0" }
+
thiserror = { version = "1" }
+

+
[dev-dependencies]
+
quickcheck = { version = "1", default-features = false }
+
quickcheck_macros = { version = "1", default-features = false }
added radicle-node/src/address_book.rs
@@ -0,0 +1,468 @@
+
use std::io::Seek;
+
use std::ops::{Deref, DerefMut};
+
use std::path::Path;
+
use std::{fs, io, net};
+

+
use crate::collections::HashMap;
+
use crate::LocalTime;
+
use nonempty::NonEmpty;
+
use serde::{Deserialize, Serialize};
+

+
/// A map with the ability to randomly select values.
+
#[derive(Debug)]
+
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
+
    }
+
}
+

+
/// A known address.
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct KnownAddress {
+
    /// Network address.
+
    pub addr: net::SocketAddr,
+
    /// Address of the peer who sent us this address.
+
    pub source: Source,
+
    /// Last time this address was used to successfully connect to a peer.
+
    #[serde(with = "local_time")]
+
    pub last_success: Option<LocalTime>,
+
    /// Last time this address was sampled.
+
    #[serde(with = "local_time")]
+
    pub last_sampled: Option<LocalTime>,
+
    /// Last time this address was tried.
+
    #[serde(with = "local_time")]
+
    pub last_attempt: Option<LocalTime>,
+
    /// Last time this peer was seen alive.
+
    #[serde(with = "local_time")]
+
    pub last_active: Option<LocalTime>,
+
}
+

+
impl KnownAddress {
+
    /// Create a new known address.
+
    pub fn new(addr: net::SocketAddr, source: Source, last_active: Option<LocalTime>) -> Self {
+
        Self {
+
            addr,
+
            source,
+
            last_success: None,
+
            last_attempt: None,
+
            last_sampled: None,
+
            last_active,
+
        }
+
    }
+
}
+

+
/// Address source. Specifies where an address originated from.
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub enum Source {
+
    /// An address that was shared by another peer.
+
    Peer(net::SocketAddr),
+
    /// 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(addr) => write!(f, "{}", addr),
+
            Self::Dns => write!(f, "DNS"),
+
            Self::Imported => write!(f, "Imported"),
+
        }
+
    }
+
}
+

+
/// A file-backed address cache.
+
#[derive(Debug)]
+
pub struct Cache {
+
    addrs: std::collections::HashMap<net::IpAddr, KnownAddress>,
+
    file: fs::File,
+
}
+

+
impl Cache {
+
    /// Open an existing cache.
+
    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
+
        fs::OpenOptions::new()
+
            .read(true)
+
            .write(true)
+
            .open(path)
+
            .and_then(Self::from)
+
    }
+

+
    /// Create a new cache.
+
    pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
+
        use std::collections::HashMap;
+

+
        let file = fs::OpenOptions::new()
+
            .create_new(true)
+
            .write(true)
+
            .open(path)?;
+

+
        Ok(Self {
+
            file,
+
            addrs: HashMap::new(),
+
        })
+
    }
+

+
    /// Create a new cache from a file.
+
    pub fn from(mut file: fs::File) -> io::Result<Self> {
+
        use std::collections::HashMap;
+

+
        let bytes = file.seek(io::SeekFrom::End(0))?;
+
        let addrs = if bytes == 0 {
+
            HashMap::new()
+
        } else {
+
            file.rewind()?;
+
            serde_json::from_reader(&file)?
+
        };
+

+
        Ok(Self { file, addrs })
+
    }
+
}
+

+
impl Store for Cache {
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
+
        self.addrs.get_mut(ip)
+
    }
+

+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
+
        self.addrs.get(ip)
+
    }
+

+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
+
        self.addrs.remove(ip)
+
    }
+

+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
+
        <std::collections::HashMap<_, _> as Store>::insert(&mut self.addrs, ip, ka)
+
    }
+

+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
+
        Box::new(self.addrs.iter())
+
    }
+

+
    fn clear(&mut self) {
+
        self.addrs.clear()
+
    }
+

+
    fn len(&self) -> usize {
+
        self.addrs.len()
+
    }
+

+
    fn flush<'a>(&mut self) -> io::Result<()> {
+
        use io::Write;
+

+
        let peers = serde_json::to_value(&self.addrs)?;
+
        let s = serde_json::to_string(&peers)?;
+

+
        self.file.set_len(0)?;
+
        self.file.seek(io::SeekFrom::Start(0))?;
+
        self.file.write_all(s.as_bytes())?;
+
        self.file.write_all(&[b'\n'])?;
+
        self.file.sync_data()?;
+

+
        Ok(())
+
    }
+
}
+

+
/// Address store.
+
///
+
/// Used to store peer addresses and metadata.
+
pub trait Store {
+
    /// Get a known peer address.
+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress>;
+

+
    /// Get a known peer address mutably.
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress>;
+

+
    /// Insert a *new* address into the store. Returns `true` if the address was inserted,
+
    /// or `false` if it was already known.
+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool;
+

+
    /// Remove an address from the store.
+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress>;
+

+
    /// Return an iterator over the known addresses.
+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a>;
+

+
    /// Returns the number of addresses.
+
    fn len(&self) -> usize;
+

+
    /// Returns true if there are no addresses.
+
    fn is_empty(&self) -> bool {
+
        self.len() == 0
+
    }
+

+
    /// Seed the peer store with addresses.
+
    /// Fails if *none* of the seeds could be resolved to addresses.
+
    fn seed<S: net::ToSocketAddrs>(
+
        &mut self,
+
        seeds: impl Iterator<Item = S>,
+
        source: Source,
+
    ) -> io::Result<()> {
+
        let mut error = None;
+
        let mut success = false;
+

+
        for seed in seeds {
+
            match seed.to_socket_addrs() {
+
                Ok(addrs) => {
+
                    success = true;
+
                    for addr in addrs {
+
                        self.insert(addr.ip(), KnownAddress::new(addr, source, None));
+
                    }
+
                }
+
                Err(err) => error = Some(err),
+
            }
+
        }
+

+
        if success {
+
            return Ok(());
+
        }
+
        if let Some(err) = error {
+
            return Err(io::Error::new(
+
                io::ErrorKind::Other,
+
                format!("seeds failed to resolve: {}", err),
+
            ));
+
        }
+
        Ok(())
+
    }
+

+
    /// Clears the store of all addresses.
+
    fn clear(&mut self);
+

+
    /// Flush data to permanent storage.
+
    fn flush(&mut self) -> io::Result<()>;
+
}
+

+
/// Implementation of [`Store`] for [`std::collections::HashMap`].
+
impl Store for std::collections::HashMap<net::IpAddr, KnownAddress> {
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
+
        self.get_mut(ip)
+
    }
+

+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
+
        self.get(ip)
+
    }
+

+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
+
        self.remove(ip)
+
    }
+

+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
+
        use std::collections::hash_map::Entry;
+

+
        match self.entry(ip) {
+
            Entry::Vacant(v) => {
+
                v.insert(ka);
+
            }
+
            Entry::Occupied(_) => return false,
+
        }
+
        true
+
    }
+

+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
+
        Box::new(self.iter())
+
    }
+

+
    fn clear(&mut self) {
+
        self.clear()
+
    }
+

+
    fn len(&self) -> usize {
+
        self.len()
+
    }
+

+
    fn flush(&mut self) -> std::io::Result<()> {
+
        Ok(())
+
    }
+
}
+

+
/// Implementation of [`Store`] for [`crate::collections::HashMap`].
+
impl Store for crate::collections::HashMap<net::IpAddr, KnownAddress> {
+
    fn get_mut(&mut self, ip: &net::IpAddr) -> Option<&mut KnownAddress> {
+
        self.get_mut(ip)
+
    }
+

+
    fn get(&self, ip: &net::IpAddr) -> Option<&KnownAddress> {
+
        self.get(ip)
+
    }
+

+
    fn remove(&mut self, ip: &net::IpAddr) -> Option<KnownAddress> {
+
        self.remove(ip)
+
    }
+

+
    fn insert(&mut self, ip: net::IpAddr, ka: KnownAddress) -> bool {
+
        use std::collections::hash_map::Entry;
+

+
        match self.entry(ip) {
+
            Entry::Vacant(v) => {
+
                v.insert(ka);
+
            }
+
            Entry::Occupied(_) => return false,
+
        }
+
        true
+
    }
+

+
    fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&net::IpAddr, &KnownAddress)> + 'a> {
+
        Box::new(self.iter())
+
    }
+

+
    fn clear(&mut self) {
+
        self.clear()
+
    }
+

+
    fn len(&self) -> usize {
+
        self.len()
+
    }
+

+
    fn flush(&mut self) -> std::io::Result<()> {
+
        Ok(())
+
    }
+
}
+

+
mod local_time {
+
    use super::LocalTime;
+
    use serde::{Deserialize, Deserializer, Serializer};
+

+
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<LocalTime>, D::Error>
+
    where
+
        D: Deserializer<'de>,
+
    {
+
        let value: Option<u64> = Deserialize::deserialize(deserializer)?;
+

+
        if let Some(value) = value {
+
            Ok(Some(LocalTime::from_secs(value)))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    pub fn serialize<S>(value: &Option<LocalTime>, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: Serializer,
+
    {
+
        if let Some(local_time) = value {
+
            serializer.serialize_u64(local_time.as_secs())
+
        } else {
+
            serializer.serialize_none()
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+

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

+
        Cache::create(&path).unwrap();
+
        let cache = Cache::open(&path).unwrap();
+

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

+
    #[test]
+
    fn test_save_and_load() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let path = tmp.path().join("cache");
+
        let mut expected = Vec::new();
+

+
        {
+
            let mut cache = Cache::create(&path).unwrap();
+

+
            for i in 32..48 {
+
                let ip = net::IpAddr::from([127, 0, 0, i]);
+
                let addr = net::SocketAddr::from((ip, 8333));
+
                let ka = KnownAddress {
+
                    addr,
+
                    source: Source::Dns,
+
                    last_success: Some(LocalTime::from_secs(i as u64)),
+
                    last_sampled: Some(LocalTime::from_secs((i + 1) as u64)),
+
                    last_attempt: None,
+
                    last_active: None,
+
                };
+
                cache.insert(ip, ka);
+
            }
+
            cache.flush().unwrap();
+

+
            for (ip, ka) in cache.iter() {
+
                expected.push((*ip, ka.clone()));
+
            }
+
        }
+

+
        {
+
            let cache = Cache::open(&path).unwrap();
+
            let mut actual = cache
+
                .iter()
+
                .map(|(i, ka)| (*i, ka.clone()))
+
                .collect::<Vec<_>>();
+

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

+
            assert_eq!(actual, expected);
+
        }
+
    }
+
}
added radicle-node/src/address_manager.rs
@@ -0,0 +1,12 @@
+
use crate::address_book::Store;
+

+
#[derive(Debug)]
+
pub struct AddressManager<S> {
+
    store: S,
+
}
+

+
impl<S: Store> AddressManager<S> {
+
    pub fn new(store: S) -> Self {
+
        Self { store }
+
    }
+
}
added radicle-node/src/client.rs
@@ -0,0 +1,126 @@
+
use std::net;
+
use std::path::Path;
+

+
use crossbeam_channel as chan;
+
use nakamoto_net::{LocalTime, Reactor};
+

+
use crate::clock::RefClock;
+
use crate::collections::HashMap;
+
use crate::crypto::Signer;
+
use crate::service;
+
use crate::storage::git::Storage;
+
use crate::transport::Transport;
+
use crate::wire::Wire;
+

+
pub mod handle;
+

+
/// Client configuration.
+
#[derive(Debug, Clone)]
+
pub struct Config {
+
    /// Client service configuration.
+
    pub service: service::Config,
+
    /// Client listen addresses.
+
    pub listen: Vec<net::SocketAddr>,
+
}
+

+
impl Config {
+
    /// Create a new configuration for the given network.
+
    pub fn new(network: service::Network) -> Self {
+
        Self {
+
            service: service::Config {
+
                network,
+
                ..service::Config::default()
+
            },
+
            ..Self::default()
+
        }
+
    }
+
}
+

+
impl Default for Config {
+
    fn default() -> Self {
+
        Self {
+
            service: service::Config::default(),
+
            listen: vec![([0, 0, 0, 0], 0).into()],
+
        }
+
    }
+
}
+

+
pub struct Client<R: Reactor, G: Signer> {
+
    reactor: R,
+
    storage: Storage,
+
    signer: G,
+

+
    handle: chan::Sender<service::Command>,
+
    commands: chan::Receiver<service::Command>,
+
    shutdown: chan::Sender<()>,
+
    listening: chan::Receiver<net::SocketAddr>,
+
    events: Events,
+
}
+

+
impl<R: Reactor, G: Signer> Client<R, G> {
+
    pub fn new<P: AsRef<Path>>(path: P, signer: G) -> Result<Self, nakamoto_net::error::Error> {
+
        let (handle, commands) = chan::unbounded::<service::Command>();
+
        let (shutdown, shutdown_recv) = chan::bounded(1);
+
        let (listening_send, listening) = chan::bounded(1);
+
        let reactor = R::new(shutdown_recv, listening_send)?;
+
        let storage = Storage::open(path)?;
+
        let events = Events {};
+

+
        Ok(Self {
+
            storage,
+
            signer,
+
            reactor,
+
            handle,
+
            commands,
+
            listening,
+
            shutdown,
+
            events,
+
        })
+
    }
+

+
    pub fn run(mut self, config: Config) -> Result<(), nakamoto_net::error::Error> {
+
        let network = config.service.network;
+
        let rng = fastrand::Rng::new();
+
        let time = LocalTime::now();
+
        let storage = self.storage;
+
        let signer = self.signer;
+
        let addresses = HashMap::with_hasher(rng.clone().into());
+

+
        log::info!("Initializing client ({:?})..", network);
+

+
        let service = service::Service::new(
+
            config.service,
+
            RefClock::from(time),
+
            storage,
+
            addresses,
+
            signer,
+
            rng,
+
        );
+
        self.reactor.run(
+
            &config.listen,
+
            Transport::new(Wire::new(service)),
+
            self.events,
+
            self.commands,
+
        )?;
+

+
        Ok(())
+
    }
+

+
    /// Create a new handle to communicate with the client.
+
    pub fn handle(&self) -> handle::Handle<R::Waker> {
+
        handle::Handle {
+
            waker: self.reactor.waker(),
+
            commands: self.handle.clone(),
+
            shutdown: self.shutdown.clone(),
+
            listening: self.listening.clone(),
+
        }
+
    }
+
}
+

+
pub struct Events {}
+

+
impl nakamoto_net::Publisher<service::Event> for Events {
+
    fn publish(&mut self, e: service::Event) {
+
        log::info!("Received event {:?}", e);
+
    }
+
}
added radicle-node/src/client/handle.rs
@@ -0,0 +1,118 @@
+
use std::net;
+

+
use crossbeam_channel as chan;
+
use nakamoto_net::Waker;
+
use thiserror::Error;
+

+
use crate::identity::Id;
+
use crate::service;
+
use crate::service::{CommandError, FetchLookup};
+

+
/// An error resulting from a handle method.
+
#[derive(Error, Debug)]
+
pub enum Error {
+
    /// The command channel is no longer connected.
+
    #[error("command channel is not connected")]
+
    NotConnected,
+
    /// The command returned an error.
+
    #[error("command failed: {0}")]
+
    Command(#[from] CommandError),
+
    /// The operation timed out.
+
    #[error("the operation timed out")]
+
    Timeout,
+
    /// An I/O error occured.
+
    #[error(transparent)]
+
    Io(#[from] std::io::Error),
+
}
+

+
impl From<chan::RecvError> for Error {
+
    fn from(_: chan::RecvError) -> Self {
+
        Self::NotConnected
+
    }
+
}
+

+
impl From<chan::RecvTimeoutError> for Error {
+
    fn from(err: chan::RecvTimeoutError) -> Self {
+
        match err {
+
            chan::RecvTimeoutError::Timeout => Self::Timeout,
+
            chan::RecvTimeoutError::Disconnected => Self::NotConnected,
+
        }
+
    }
+
}
+

+
impl<T> From<chan::SendError<T>> for Error {
+
    fn from(_: chan::SendError<T>) -> Self {
+
        Self::NotConnected
+
    }
+
}
+

+
pub struct Handle<W: Waker> {
+
    pub(crate) commands: chan::Sender<service::Command>,
+
    pub(crate) shutdown: chan::Sender<()>,
+
    pub(crate) listening: chan::Receiver<net::SocketAddr>,
+
    pub(crate) waker: W,
+
}
+

+
impl<W: Waker> traits::Handle for Handle<W> {
+
    /// Retrieve or update the given project from the network.
+
    fn fetch(&self, id: Id) -> Result<FetchLookup, Error> {
+
        let (sender, receiver) = chan::bounded(1);
+
        self.commands.send(service::Command::Fetch(id, sender))?;
+
        receiver.recv().map_err(Error::from)
+
    }
+

+
    /// Start tracking the given project. Doesn't do anything if the project is already tracked.
+
    fn track(&self, id: Id) -> Result<bool, Error> {
+
        let (sender, receiver) = chan::bounded(1);
+
        self.commands.send(service::Command::Track(id, sender))?;
+
        receiver.recv().map_err(Error::from)
+
    }
+

+
    /// Untrack the given project and delete it from storage.
+
    fn untrack(&self, id: Id) -> Result<bool, Error> {
+
        let (sender, receiver) = chan::bounded(1);
+
        self.commands.send(service::Command::Untrack(id, sender))?;
+
        receiver.recv().map_err(Error::from)
+
    }
+

+
    /// Notify the client that a project has been updated.
+
    fn updated(&self, id: Id) -> Result<(), Error> {
+
        self.command(service::Command::AnnounceRefs(id))
+
    }
+

+
    /// Send a command to the command channel, and wake up the event loop.
+
    fn command(&self, cmd: service::Command) -> Result<(), Error> {
+
        self.commands.send(cmd)?;
+
        self.waker.wake()?;
+

+
        Ok(())
+
    }
+

+
    /// Ask the client to shutdown.
+
    fn shutdown(self) -> Result<(), Error> {
+
        self.shutdown.send(())?;
+
        self.waker.wake()?;
+

+
        Ok(())
+
    }
+
}
+

+
pub mod traits {
+
    use super::*;
+

+
    pub trait Handle {
+
        /// Retrieve or update the project from network.
+
        fn fetch(&self, id: Id) -> Result<FetchLookup, Error>;
+
        /// Start tracking the given project. Doesn't do anything if the project is already
+
        /// tracked.
+
        fn track(&self, id: Id) -> Result<bool, Error>;
+
        /// Untrack the given project and delete it from storage.
+
        fn untrack(&self, id: Id) -> Result<bool, Error>;
+
        /// Notify the client that a project has been updated.
+
        fn updated(&self, id: Id) -> Result<(), Error>;
+
        /// Send a command to the command channel, and wake up the event loop.
+
        fn command(&self, cmd: service::Command) -> Result<(), Error>;
+
        /// Ask the client to shutdown.
+
        fn shutdown(self) -> Result<(), Error>;
+
    }
+
}
added radicle-node/src/clock.rs
@@ -0,0 +1,37 @@
+
use std::cell::RefCell;
+
use std::rc::Rc;
+

+
use crate::{LocalDuration, LocalTime};
+

+
/// Clock with interior mutability.
+
#[derive(Debug, Clone)]
+
pub struct RefClock(Rc<RefCell<LocalTime>>);
+

+
impl std::ops::Deref for RefClock {
+
    type Target = Rc<RefCell<LocalTime>>;
+

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

+
impl RefClock {
+
    /// Elapse time.
+
    pub fn elapse(&self, duration: LocalDuration) {
+
        self.borrow_mut().elapse(duration)
+
    }
+

+
    pub fn local_time(&self) -> LocalTime {
+
        *self.borrow()
+
    }
+

+
    pub fn set(&mut self, time: LocalTime) {
+
        *self.borrow_mut() = time;
+
    }
+
}
+

+
impl From<LocalTime> for RefClock {
+
    fn from(other: LocalTime) -> Self {
+
        Self(Rc::new(RefCell::new(other)))
+
    }
+
}
added radicle-node/src/collections.rs
@@ -0,0 +1,44 @@
+
//! Useful collections for peer-to-peer networking.
+
use siphasher::sip::SipHasher13;
+

+
/// A `HashMap` which uses [`fastrand::Rng`] for its random state.
+
pub type HashMap<K, V> = std::collections::HashMap<K, V, RandomState>;
+

+
/// A `HashSet` which uses [`fastrand::Rng`] for its random state.
+
pub type HashSet<K> = std::collections::HashSet<K, RandomState>;
+

+
/// Random hasher state.
+
#[derive(Clone)]
+
pub struct RandomState {
+
    key1: u64,
+
    key2: u64,
+
}
+

+
impl Default for RandomState {
+
    fn default() -> Self {
+
        Self::new(fastrand::Rng::new())
+
    }
+
}
+

+
impl RandomState {
+
    fn new(rng: fastrand::Rng) -> Self {
+
        Self {
+
            key1: rng.u64(..),
+
            key2: rng.u64(..),
+
        }
+
    }
+
}
+

+
impl std::hash::BuildHasher for RandomState {
+
    type Hasher = SipHasher13;
+

+
    fn build_hasher(&self) -> Self::Hasher {
+
        SipHasher13::new_with_keys(self.key1, self.key2)
+
    }
+
}
+

+
impl From<fastrand::Rng> for RandomState {
+
    fn from(rng: fastrand::Rng) -> Self {
+
        Self::new(rng)
+
    }
+
}
added radicle-node/src/control.rs
@@ -0,0 +1,203 @@
+
//! Client control socket implementation.
+
use std::io::prelude::*;
+
use std::io::BufReader;
+
use std::io::LineWriter;
+
use std::os::unix::net::UnixListener;
+
use std::os::unix::net::UnixStream;
+
use std::path::Path;
+
use std::{fs, io, net};
+

+
use crate::client;
+
use crate::client::handle::traits::Handle;
+
use crate::identity::Id;
+
use crate::service::FetchLookup;
+
use crate::service::FetchResult;
+

+
/// Default name for control socket file.
+
pub const DEFAULT_SOCKET_NAME: &str = "radicle.sock";
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum Error {
+
    #[error("failed to bind control socket listener: {0}")]
+
    Bind(io::Error),
+
}
+

+
/// Listen for commands on the control socket, and process them.
+
pub fn listen<P: AsRef<Path>, H: Handle>(path: P, handle: H) -> Result<(), Error> {
+
    // Remove the socket file on startup before rebinding.
+
    fs::remove_file(&path).ok();
+

+
    let listener = UnixListener::bind(path).map_err(Error::Bind)?;
+
    for incoming in listener.incoming() {
+
        match incoming {
+
            Ok(mut stream) => {
+
                if let Err(e) = drain(&stream, &handle) {
+
                    log::error!("Received {} on control socket", e);
+

+
                    writeln!(stream, "error: {}", e).ok();
+

+
                    stream.flush().ok();
+
                    stream.shutdown(net::Shutdown::Both).ok();
+
                } else {
+
                    writeln!(stream, "ok").ok();
+
                }
+
            }
+
            Err(e) => log::error!("Failed to open control socket stream: {}", e),
+
        }
+
    }
+

+
    Ok(())
+
}
+

+
#[derive(thiserror::Error, Debug)]
+
enum DrainError {
+
    #[error("invalid command argument `{0}`")]
+
    InvalidCommandArg(String),
+
    #[error("unknown command `{0}`")]
+
    UnknownCommand(String),
+
    #[error("invalid command")]
+
    InvalidCommand,
+
    #[error("client error: {0}")]
+
    Client(#[from] client::handle::Error),
+
    #[error("i/o error: {0}")]
+
    Io(#[from] io::Error),
+
}
+

+
fn drain<H: Handle>(stream: &UnixStream, handle: &H) -> Result<(), DrainError> {
+
    let mut reader = BufReader::new(stream);
+

+
    // TODO: refactor to include helper
+
    for line in reader.by_ref().lines().flatten() {
+
        match line.split_once(' ') {
+
            Some(("fetch", arg)) => {
+
                if let Ok(id) = arg.parse() {
+
                    fetch(id, LineWriter::new(stream), handle)?;
+
                } else {
+
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
+
                }
+
            }
+
            Some(("track", arg)) => {
+
                if let Ok(id) = arg.parse() {
+
                    if let Err(e) = handle.track(id) {
+
                        return Err(DrainError::Client(e));
+
                    }
+
                } else {
+
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
+
                }
+
            }
+
            Some(("untrack", arg)) => {
+
                if let Ok(id) = arg.parse() {
+
                    if let Err(e) = handle.untrack(id) {
+
                        return Err(DrainError::Client(e));
+
                    }
+
                } else {
+
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
+
                }
+
            }
+
            Some(("update", arg)) => {
+
                if let Ok(id) = arg.parse() {
+
                    if let Err(e) = handle.updated(id) {
+
                        return Err(DrainError::Client(e));
+
                    }
+
                } else {
+
                    return Err(DrainError::InvalidCommandArg(arg.to_owned()));
+
                }
+
            }
+
            Some((cmd, _)) => return Err(DrainError::UnknownCommand(cmd.to_owned())),
+
            None => return Err(DrainError::InvalidCommand),
+
        }
+
    }
+
    Ok(())
+
}
+

+
fn fetch<W: Write, H: Handle>(id: Id, mut writer: W, handle: &H) -> Result<(), DrainError> {
+
    match handle.fetch(id.clone()) {
+
        Err(e) => {
+
            return Err(DrainError::Client(e));
+
        }
+
        Ok(FetchLookup::Found { seeds, results }) => {
+
            let seeds = Vec::from(seeds);
+

+
            writeln!(
+
                writer,
+
                "ok: found {} seeds for {} ({:?})",
+
                seeds.len(),
+
                &id,
+
                &seeds,
+
            )?;
+

+
            for result in results.iter() {
+
                match result {
+
                    FetchResult::Fetched { from, updated } => {
+
                        writeln!(writer, "ok: {} fetched from {}", &id, from)?;
+

+
                        for update in updated {
+
                            writeln!(writer, "{}", update)?;
+
                        }
+
                    }
+
                    FetchResult::Error { from, error } => {
+
                        writeln!(
+
                            writer,
+
                            "error: {} failed to fetch from {}: {}",
+
                            &id, from, error
+
                        )?;
+
                    }
+
                }
+
            }
+
        }
+
        Ok(FetchLookup::NotFound) => {
+
            writeln!(writer, "error: {} was not found", &id)?;
+
        }
+
        Ok(FetchLookup::NotTracking) => {
+
            writeln!(writer, "error: {} is not tracked", &id)?;
+
        }
+
        Ok(FetchLookup::Error(err)) => {
+
            writeln!(writer, "error: {}", err)?;
+
        }
+
    }
+
    Ok(())
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use std::io::prelude::*;
+
    use std::os::unix::net::UnixStream;
+
    use std::{net, thread};
+

+
    use super::*;
+
    use crate::identity::Id;
+
    use crate::test;
+

+
    #[test]
+
    fn test_control_socket() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let handle = test::handle::Handle::default();
+
        let socket = tmp.path().join("alice.sock");
+
        let projs = test::arbitrary::set::<Id>(1..3);
+

+
        thread::spawn({
+
            let socket = socket.clone();
+
            let handle = handle.clone();
+

+
            move || listen(socket, handle)
+
        });
+

+
        let mut stream = loop {
+
            if let Ok(stream) = UnixStream::connect(&socket) {
+
                break stream;
+
            }
+
        };
+
        for proj in &projs {
+
            writeln!(&stream, "update {}", proj).unwrap();
+
        }
+

+
        let mut buf = [0; 2];
+
        stream.shutdown(net::Shutdown::Write).unwrap();
+
        stream.read_exact(&mut buf).unwrap();
+

+
        assert_eq!(&buf, &[b'o', b'k']);
+
        for proj in &projs {
+
            assert!(handle.updates.lock().unwrap().contains(proj));
+
        }
+
    }
+
}
added radicle-node/src/crypto.rs
@@ -0,0 +1,225 @@
+
use std::sync::Arc;
+
use std::{fmt, ops::Deref, str::FromStr};
+

+
use ed25519_compact as ed25519;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
pub use ed25519::{Error, KeyPair, Seed};
+

+
/// Verified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub struct Verified;
+
/// Unverified (used as type witness).
+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+
pub struct Unverified;
+

+
pub trait Signer: Send + Sync {
+
    /// Return this signer's public/verification key.
+
    fn public_key(&self) -> &PublicKey;
+
    /// Sign a message and return the signature.
+
    fn sign(&self, msg: &[u8]) -> Signature;
+
}
+

+
impl<T> Signer for Arc<T>
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+
}
+

+
impl<T> Signer for &T
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+
}
+

+
/// Cryptographic signature.
+
#[derive(PartialEq, Eq, Copy, Clone)]
+
pub struct Signature(pub ed25519::Signature);
+

+
impl fmt::Display for Signature {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        let base = multibase::Base::Base58Btc;
+
        write!(f, "{}", multibase::encode(base, self.deref()))
+
    }
+
}
+

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

+
#[derive(Error, Debug)]
+
pub enum SignatureError {
+
    #[error("invalid multibase string: {0}")]
+
    Multibase(#[from] multibase::Error),
+
    #[error("invalid signature: {0}")]
+
    Invalid(#[from] ed25519::Error),
+
}
+

+
impl From<ed25519::Signature> for Signature {
+
    fn from(other: ed25519::Signature) -> Self {
+
        Self(other)
+
    }
+
}
+

+
impl FromStr for Signature {
+
    type Err = SignatureError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let sig = ed25519::Signature::from_slice(bytes.as_slice())?;
+

+
        Ok(Self(sig))
+
    }
+
}
+

+
impl Deref for Signature {
+
    type Target = ed25519::Signature;
+

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

+
impl From<[u8; 64]> for Signature {
+
    fn from(bytes: [u8; 64]) -> Self {
+
        Self(ed25519::Signature::new(bytes))
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Signature {
+
    type Error = ed25519::Error;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
+
        ed25519::Signature::from_slice(bytes).map(Self)
+
    }
+
}
+

+
/// The public/verification key.
+
#[derive(Serialize, Deserialize, Eq, Copy, Clone)]
+
#[serde(into = "String", try_from = "String")]
+
pub struct PublicKey(pub ed25519::PublicKey);
+

+
/// The private/signing key.
+
pub type SecretKey = ed25519::SecretKey;
+

+
#[derive(Error, Debug)]
+
pub enum PublicKeyError {
+
    #[error("invalid length {0}")]
+
    InvalidLength(usize),
+
    #[error("invalid multibase string: {0}")]
+
    Multibase(#[from] multibase::Error),
+
    #[error("invalid key: {0}")]
+
    InvalidKey(#[from] ed25519::Error),
+
}
+

+
impl std::hash::Hash for PublicKey {
+
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
        self.0.deref().hash(state)
+
    }
+
}
+

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

+
impl From<PublicKey> for String {
+
    fn from(other: PublicKey) -> Self {
+
        other.to_human()
+
    }
+
}
+

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

+
impl PartialEq for PublicKey {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.0 == other.0
+
    }
+
}
+

+
impl From<ed25519::PublicKey> for PublicKey {
+
    fn from(other: ed25519::PublicKey) -> Self {
+
        Self(other)
+
    }
+
}
+

+
impl TryFrom<[u8; 32]> for PublicKey {
+
    type Error = ed25519::Error;
+

+
    fn try_from(other: [u8; 32]) -> Result<Self, Self::Error> {
+
        Ok(Self(ed25519::PublicKey::new(other)))
+
    }
+
}
+

+
impl PublicKey {
+
    pub fn to_human(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, self.0.deref())
+
    }
+
}
+

+
impl FromStr for PublicKey {
+
    type Err = PublicKeyError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let array: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|v: Vec<u8>| PublicKeyError::InvalidLength(v.len()))?;
+
        let key = ed25519::PublicKey::new(array);
+

+
        Ok(Self(key))
+
    }
+
}
+

+
impl TryFrom<String> for PublicKey {
+
    type Error = PublicKeyError;
+

+
    fn try_from(value: String) -> Result<Self, Self::Error> {
+
        Self::from_str(&value)
+
    }
+
}
+

+
impl Deref for PublicKey {
+
    type Target = ed25519::PublicKey;
+

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

+
#[cfg(test)]
+
mod test {
+
    use crate::crypto::PublicKey;
+
    use quickcheck_macros::quickcheck;
+
    use std::str::FromStr;
+

+
    #[quickcheck]
+
    fn prop_encode_decode(input: PublicKey) {
+
        let encoded = input.to_string();
+
        let decoded = PublicKey::from_str(&encoded).unwrap();
+

+
        assert_eq!(input, decoded);
+
    }
+
}
added radicle-node/src/decoder.rs
@@ -0,0 +1,108 @@
+
use std::io;
+
use std::marker::PhantomData;
+

+
use crate::service::message::Envelope;
+
use crate::wire;
+

+
/// Message stream decoder.
+
///
+
/// Used to for example turn a byte stream into network messages.
+
#[derive(Debug)]
+
pub struct Decoder<D = Envelope> {
+
    unparsed: Vec<u8>,
+
    item: PhantomData<D>,
+
}
+

+
impl<D> From<Vec<u8>> for Decoder<D> {
+
    fn from(unparsed: Vec<u8>) -> Self {
+
        Self {
+
            unparsed,
+
            item: PhantomData,
+
        }
+
    }
+
}
+

+
impl<D: wire::Decode> Decoder<D> {
+
    /// Create a new stream decoder.
+
    pub fn new(capacity: usize) -> Self {
+
        Self {
+
            unparsed: Vec::with_capacity(capacity),
+
            item: PhantomData,
+
        }
+
    }
+

+
    /// Input bytes into the decoder.
+
    pub fn input(&mut self, bytes: &[u8]) {
+
        self.unparsed.extend_from_slice(bytes);
+
    }
+

+
    /// Decode and return the next message. Returns [`None`] if nothing was decoded.
+
    pub fn decode_next(&mut self) -> Result<Option<D>, wire::Error> {
+
        let mut reader = io::Cursor::new(self.unparsed.as_mut_slice());
+

+
        match D::decode(&mut reader) {
+
            Ok(msg) => {
+
                let pos = reader.position() as usize;
+
                self.unparsed.drain(..pos);
+

+
                Ok(Some(msg))
+
            }
+
            Err(err) if err.is_eof() => Ok(None),
+
            Err(err) => Err(err),
+
        }
+
    }
+
}
+

+
impl<D: wire::Decode> io::Write for Decoder<D> {
+
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+
        self.input(buf);
+

+
        Ok(buf.len())
+
    }
+

+
    fn flush(&mut self) -> io::Result<()> {
+
        Ok(())
+
    }
+
}
+

+
impl<D: wire::Decode> Iterator for Decoder<D> {
+
    type Item = Result<D, wire::Error>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.decode_next().transpose()
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    const MSG_HELLO: &[u8] = &[5, b'h', b'e', b'l', b'l', b'o'];
+
    const MSG_BYE: &[u8] = &[3, b'b', b'y', b'e'];
+

+
    #[quickcheck]
+
    fn prop_decode_next(chunk_size: usize) {
+
        let mut bytes = vec![];
+
        let mut msgs = vec![];
+
        let mut decoder = Decoder::<String>::new(8);
+

+
        let chunk_size = 1 + chunk_size % MSG_HELLO.len() + MSG_BYE.len();
+

+
        bytes.extend_from_slice(MSG_HELLO);
+
        bytes.extend_from_slice(MSG_BYE);
+

+
        for chunk in bytes.as_slice().chunks(chunk_size) {
+
            decoder.input(chunk);
+

+
            while let Some(msg) = decoder.decode_next().unwrap() {
+
                msgs.push(msg);
+
            }
+
        }
+

+
        assert_eq!(decoder.unparsed.len(), 0);
+
        assert_eq!(msgs.len(), 2);
+
        assert_eq!(msgs[0], String::from("hello"));
+
        assert_eq!(msgs[1], String::from("bye"));
+
    }
+
}
added radicle-node/src/git.rs
@@ -0,0 +1,243 @@
+
use std::path::Path;
+
use std::str::FromStr;
+

+
use git_ref_format as format;
+
use once_cell::sync::Lazy;
+

+
use crate::collections::HashMap;
+
use crate::crypto::PublicKey;
+
use crate::storage::refs::Refs;
+
use crate::storage::RemoteId;
+

+
pub use ext::Error;
+
pub use ext::Oid;
+
pub use git_ref_format as fmt;
+
pub use git_ref_format::{refname, RefStr, RefString};
+
pub use git_url as url;
+
pub use git_url::Url;
+
pub use radicle_git_ext as ext;
+

+
/// Default port of the `git` transport protocol.
+
pub const PROTOCOL_PORT: u16 = 9418;
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum RefError {
+
    #[error("invalid ref name '{0}'")]
+
    InvalidName(format::RefString),
+
    #[error("invalid ref format: {0}")]
+
    Format(#[from] format::Error),
+
}
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum ListRefsError {
+
    #[error("git error: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("invalid ref: {0}")]
+
    InvalidRef(#[from] RefError),
+
}
+

+
pub mod refs {
+
    use super::*;
+

+
    /// Where project information is kept.
+
    pub static IDENTITY_BRANCH: Lazy<RefString> = Lazy::new(|| refname!("radicle/id"));
+

+
    pub mod storage {
+
        use super::*;
+

+
        pub fn branch(remote: &RemoteId, branch: &str) -> String {
+
            format!("refs/remotes/{remote}/heads/{branch}")
+
        }
+

+
        /// Get the branch used to track project information.
+
        pub fn id(remote: &RemoteId) -> String {
+
            branch(remote, &IDENTITY_BRANCH)
+
        }
+
    }
+

+
    pub mod workdir {
+
        pub fn branch(branch: &str) -> String {
+
            format!("refs/heads/{branch}")
+
        }
+

+
        pub fn note(name: &str) -> String {
+
            format!("refs/notes/{name}")
+
        }
+

+
        pub fn remote_branch(remote: &str, branch: &str) -> String {
+
            format!("refs/remotes/{remote}/{branch}")
+
        }
+

+
        pub fn tag(name: &str) -> String {
+
            format!("refs/tags/{name}")
+
        }
+
    }
+
}
+

+
/// List remote refs of a project, given the remote URL.
+
pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError> {
+
    let url = url.to_string();
+
    let mut remotes = HashMap::default();
+
    let mut remote = git2::Remote::create_detached(&url)?;
+

+
    remote.connect(git2::Direction::Fetch)?;
+

+
    let refs = remote.list()?;
+
    for r in refs {
+
        let (id, refname) = parse_ref::<PublicKey>(r.name())?;
+
        let entry = remotes.entry(id).or_insert_with(Refs::default);
+

+
        entry.insert(refname, r.oid().into());
+
    }
+

+
    Ok(remotes)
+
}
+

+
/// Parse a ref string.
+
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
+
    let input = format::RefStr::try_from_str(s)?;
+
    let suffix = input
+
        .strip_prefix(format::refname!("refs/remotes"))
+
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
+

+
    let mut components = suffix.components();
+
    let id = components
+
        .next()
+
        .ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
+
    let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
+
    let refstr = components.collect::<format::RefString>();
+

+
    Ok((id, refstr))
+
}
+

+
/// Create an initial empty commit.
+
pub fn initial_commit<'a>(
+
    repo: &'a git2::Repository,
+
    sig: &git2::Signature,
+
) -> Result<git2::Commit<'a>, git2::Error> {
+
    let tree_id = repo.index()?.write_tree()?;
+
    let tree = repo.find_tree(tree_id)?;
+
    let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
+
    let commit = repo.find_commit(oid).unwrap();
+

+
    Ok(commit)
+
}
+

+
/// Create a commit and update the given ref to it.
+
pub fn commit<'a>(
+
    repo: &'a git2::Repository,
+
    parent: &'a git2::Commit,
+
    target: &RefStr,
+
    message: &str,
+
    user: &str,
+
) -> Result<git2::Commit<'a>, git2::Error> {
+
    let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?;
+
    let tree_id = repo.index()?.write_tree()?;
+
    let tree = repo.find_tree(tree_id)?;
+
    let oid = repo.commit(Some(target.as_str()), &sig, &sig, message, &tree, &[parent])?;
+
    let commit = repo.find_commit(oid).unwrap();
+

+
    Ok(commit)
+
}
+

+
/// Push the refs to the radicle remote.
+
pub fn push(repo: &git2::Repository) -> Result<(), git2::Error> {
+
    let mut remote = repo.find_remote("rad")?;
+
    let refspecs = remote.push_refspecs().unwrap();
+
    let refspec = refspecs.into_iter().next().unwrap().unwrap();
+

+
    // The `git2` crate doesn't seem to support push refspecs with '*' in them,
+
    // so we manually replace it with the current branch.
+
    let head = repo.head().unwrap();
+
    let branch = head.shorthand().unwrap();
+
    let refspec = refspec.replace('*', branch);
+

+
    remote.push::<&str>(&[&refspec], None)
+
}
+

+
/// Get the repository head.
+
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
+
    let head = repo.head()?.peel_to_commit()?;
+

+
    Ok(head)
+
}
+

+
/// Write a tree with the given blob at the given path.
+
pub fn write_tree<'r>(
+
    path: &Path,
+
    bytes: &[u8],
+
    repo: &'r git2::Repository,
+
) -> Result<git2::Tree<'r>, Error> {
+
    let blob_id = repo.blob(bytes)?;
+
    let mut builder = repo.treebuilder(None)?;
+
    builder.insert(path, blob_id, 0o100_644)?;
+

+
    let tree_id = builder.write()?;
+
    let tree = repo.find_tree(tree_id)?;
+

+
    Ok(tree)
+
}
+

+
/// Configure a repository's radicle remote.
+
///
+
/// Takes the repository in which to configure the remote, the name of the remote, the public
+
/// key of the remote, and the path to the remote repository on the filesystem.
+
pub fn configure_remote<'r>(
+
    repo: &'r git2::Repository,
+
    remote_name: &str,
+
    remote_id: &RemoteId,
+
    remote_path: &Path,
+
) -> Result<git2::Remote<'r>, git2::Error> {
+
    let url = Url {
+
        scheme: git_url::Scheme::File,
+
        path: remote_path.to_string_lossy().to_string().into(),
+

+
        ..Url::default()
+
    };
+
    let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*");
+
    let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*");
+
    let remote = repo.remote_with_fetch(remote_name, url.to_string().as_str(), &fetch)?;
+
    repo.remote_add_push(remote_name, &push)?;
+

+
    Ok(remote)
+
}
+

+
/// Set the upstream of the given branch to the given remote.
+
///
+
/// This writes to the `config` directly. The entry will look like the
+
/// following:
+
///
+
/// ```text
+
/// [branch "main"]
+
///     remote = rad
+
///     merge = refs/heads/main
+
/// ```
+
pub fn set_upstream(
+
    repo: &git2::Repository,
+
    remote: &str,
+
    branch: &str,
+
    merge: &str,
+
) -> Result<(), git2::Error> {
+
    let mut config = repo.config()?;
+
    let branch_remote = format!("branch.{}.remote", branch);
+
    let branch_merge = format!("branch.{}.merge", branch);
+

+
    config.remove_multivar(&branch_remote, ".*").or_else(|e| {
+
        if ext::is_not_found_err(&e) {
+
            Ok(())
+
        } else {
+
            Err(e)
+
        }
+
    })?;
+
    config.remove_multivar(&branch_merge, ".*").or_else(|e| {
+
        if ext::is_not_found_err(&e) {
+
            Ok(())
+
        } else {
+
            Err(e)
+
        }
+
    })?;
+
    config.set_multivar(&branch_remote, ".*", remote)?;
+
    config.set_multivar(&branch_merge, ".*", merge)?;
+

+
    Ok(())
+
}
added radicle-node/src/hash.rs
@@ -0,0 +1,69 @@
+
use std::{convert::TryInto, fmt};
+

+
use serde::{Deserialize, Serialize};
+
use sha2::{
+
    digest::{generic_array::GenericArray, OutputSizeUser},
+
    Digest as _, Sha256,
+
};
+
use thiserror::Error;
+

+
#[derive(Debug, Clone, PartialEq, Eq, Error)]
+
pub enum DecodeError {
+
    #[error("invalid digest length {0}")]
+
    InvalidLength(usize),
+
}
+

+
/// A SHA-256 hash.
+
#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Digest([u8; 32]);
+

+
impl Digest {
+
    pub fn new(bytes: impl AsRef<[u8]>) -> Self {
+
        Self::from(Sha256::digest(bytes))
+
    }
+
}
+

+
impl AsRef<[u8; 32]> for Digest {
+
    fn as_ref(&self) -> &[u8; 32] {
+
        &self.0
+
    }
+
}
+

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

+
impl fmt::Display for Digest {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        for byte in &self.0 {
+
            write!(f, "{:02x}", byte)?;
+
        }
+
        Ok(())
+
    }
+
}
+

+
impl From<[u8; 32]> for Digest {
+
    fn from(bytes: [u8; 32]) -> Self {
+
        Self(bytes)
+
    }
+
}
+

+
impl TryFrom<&[u8]> for Digest {
+
    type Error = DecodeError;
+

+
    fn try_from(bytes: &[u8]) -> Result<Self, DecodeError> {
+
        let bytes: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|_| DecodeError::InvalidLength(bytes.len()))?;
+

+
        Ok(bytes.into())
+
    }
+
}
+

+
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
+
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
+
        Self(array.into())
+
    }
+
}
added radicle-node/src/identity.rs
@@ -0,0 +1,246 @@
+
pub mod doc;
+

+
use std::ops::Deref;
+
use std::path::PathBuf;
+
use std::{ffi::OsString, fmt, str::FromStr};
+

+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::crypto;
+
use crate::crypto::Verified;
+
use crate::git;
+
use crate::serde_ext;
+
use crate::storage::Remotes;
+

+
pub use crypto::PublicKey;
+
pub use doc::{Delegate, Doc};
+

+
#[derive(Error, Debug)]
+
pub enum IdError {
+
    #[error("invalid git object id: {0}")]
+
    InvalidOid(#[from] git2::Error),
+
    #[error(transparent)]
+
    Multibase(#[from] multibase::Error),
+
}
+

+
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+
pub struct Id(git::Oid);
+

+
impl fmt::Display for Id {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        f.write_str(&self.to_human())
+
    }
+
}
+

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

+
impl Id {
+
    pub fn to_human(&self) -> String {
+
        multibase::encode(multibase::Base::Base58Btc, self.0.as_bytes())
+
    }
+

+
    pub fn from_human(s: &str) -> Result<Self, IdError> {
+
        let (_, bytes) = multibase::decode(s)?;
+
        let array: git::Oid = bytes.as_slice().try_into()?;
+

+
        Ok(Self(array))
+
    }
+
}
+

+
impl FromStr for Id {
+
    type Err = IdError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Self::from_human(s)
+
    }
+
}
+

+
impl TryFrom<OsString> for Id {
+
    type Error = IdError;
+

+
    fn try_from(value: OsString) -> Result<Self, Self::Error> {
+
        let string = value.to_string_lossy();
+
        Self::from_str(&string)
+
    }
+
}
+

+
impl From<git::Oid> for Id {
+
    fn from(oid: git::Oid) -> Self {
+
        Self(oid)
+
    }
+
}
+

+
impl From<git2::Oid> for Id {
+
    fn from(oid: git2::Oid) -> Self {
+
        Self(oid.into())
+
    }
+
}
+

+
impl Deref for Id {
+
    type Target = git::Oid;
+

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

+
impl serde::Serialize for Id {
+
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        S: serde::Serializer,
+
    {
+
        serde_ext::string::serialize(self, serializer)
+
    }
+
}
+

+
impl<'de> serde::Deserialize<'de> for Id {
+
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+
    where
+
        D: serde::Deserializer<'de>,
+
    {
+
        serde_ext::string::deserialize(deserializer)
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum DidError {
+
    #[error("invalid did: {0}")]
+
    Did(String),
+
    #[error("invalid public key: {0}")]
+
    PublicKey(#[from] crypto::PublicKeyError),
+
}
+

+
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
+
#[serde(into = "String", try_from = "String")]
+
pub struct Did(crypto::PublicKey);
+

+
impl Did {
+
    pub fn encode(&self) -> String {
+
        format!("did:key:{}", self.0.to_human())
+
    }
+

+
    pub fn decode(input: &str) -> Result<Self, DidError> {
+
        let key = input
+
            .strip_prefix("did:key:")
+
            .ok_or_else(|| DidError::Did(input.to_owned()))?;
+

+
        crypto::PublicKey::from_str(key)
+
            .map(Did)
+
            .map_err(DidError::from)
+
    }
+
}
+

+
impl From<crypto::PublicKey> for Did {
+
    fn from(key: crypto::PublicKey) -> Self {
+
        Self(key)
+
    }
+
}
+

+
impl From<Did> for String {
+
    fn from(other: Did) -> Self {
+
        other.encode()
+
    }
+
}
+

+
impl TryFrom<String> for Did {
+
    type Error = DidError;
+

+
    fn try_from(value: String) -> Result<Self, Self::Error> {
+
        Self::decode(&value)
+
    }
+
}
+

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

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

+
impl Deref for Did {
+
    type Target = PublicKey;
+

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

+
/// A stored and verified project.
+
#[derive(Debug, Clone)]
+
pub struct Project {
+
    /// The project identifier.
+
    pub id: Id,
+
    /// The latest project identity document.
+
    pub doc: Doc<Verified>,
+
    /// The project remotes.
+
    pub remotes: Remotes<Verified>,
+
    /// On-disk file path for this project's repository.
+
    pub path: PathBuf,
+
}
+

+
impl Project {
+
    pub fn delegate(&mut self, name: String, key: crypto::PublicKey) -> bool {
+
        self.doc.delegate(Delegate {
+
            name,
+
            id: Did::from(key),
+
        })
+
    }
+
}
+

+
impl Deref for Project {
+
    type Target = Doc<Verified>;
+

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

+
#[cfg(test)]
+
mod test {
+
    use super::*;
+
    use crate::crypto::PublicKey;
+
    use quickcheck_macros::quickcheck;
+
    use std::collections::HashSet;
+

+
    #[quickcheck]
+
    fn prop_key_equality(a: PublicKey, b: PublicKey) {
+
        assert_ne!(a, b);
+

+
        let mut hm = HashSet::new();
+

+
        assert!(hm.insert(a));
+
        assert!(hm.insert(b));
+
        assert!(!hm.insert(a));
+
        assert!(!hm.insert(b));
+
    }
+

+
    #[quickcheck]
+
    fn prop_from_str(input: Id) {
+
        let encoded = input.to_string();
+
        let decoded = Id::from_str(&encoded).unwrap();
+

+
        assert_eq!(input, decoded);
+
    }
+

+
    #[quickcheck]
+
    fn prop_json_eq_str(pk: PublicKey, proj: Id, did: Did) {
+
        let json = serde_json::to_string(&pk).unwrap();
+
        assert_eq!(format!("\"{}\"", pk), json);
+

+
        let json = serde_json::to_string(&proj).unwrap();
+
        assert_eq!(format!("\"{}\"", proj), json);
+

+
        let json = serde_json::to_string(&did).unwrap();
+
        assert_eq!(format!("\"{}\"", did), json);
+
    }
+
}
added radicle-node/src/identity/doc.rs
@@ -0,0 +1,592 @@
+
use std::collections::{BTreeMap, HashMap};
+
use std::fmt::Write as _;
+
use std::io;
+
use std::marker::PhantomData;
+
use std::ops::Deref;
+
use std::path::Path;
+

+
use nonempty::NonEmpty;
+
use once_cell::sync::Lazy;
+
use radicle_git_ext::Oid;
+
use serde::{Deserialize, Serialize};
+
use thiserror::Error;
+

+
use crate::crypto;
+
use crate::crypto::{Signature, Unverified, Verified};
+
use crate::git;
+
use crate::identity::{Did, Id};
+
use crate::storage::git::trailers;
+
use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage};
+

+
pub use crypto::PublicKey;
+

+
/// Untrusted, well-formed input.
+
#[derive(Clone, Copy, Debug)]
+
pub struct Untrusted;
+
/// Signed by quorum of the previous delegation.
+
#[derive(Clone, Copy, Debug)]
+
pub struct Trusted;
+

+
pub static PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
+

+
pub const MAX_STRING_LENGTH: usize = 255;
+
pub const MAX_DELEGATES: usize = 255;
+

+
#[derive(Error, Debug)]
+
pub enum Error {
+
    #[error("json: {0}")]
+
    Json(#[from] serde_json::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("verification: {0}")]
+
    Verification(#[from] VerificationError),
+
    #[error("git: {0}")]
+
    Git(#[from] git::Error),
+
    #[error("git: {0}")]
+
    RawGit(#[from] git2::Error),
+
}
+

+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+
pub struct Delegate {
+
    pub name: String,
+
    pub id: Did,
+
}
+

+
impl Delegate {
+
    fn matches(&self, key: &PublicKey) -> bool {
+
        &self.id.0 == key
+
    }
+
}
+

+
impl From<Delegate> for PublicKey {
+
    fn from(delegate: Delegate) -> Self {
+
        delegate.id.0
+
    }
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "kebab-case")]
+
pub struct Payload {
+
    pub name: String,
+
    pub description: String,    // TODO: Make optional.
+
    pub default_branch: String, // TODO: Make optional.
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
+
#[serde(transparent)]
+
// TODO: Restrict values.
+
pub struct Namespace(String);
+

+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+
pub struct Doc<V> {
+
    #[serde(rename = "xyz.radicle.project")]
+
    pub payload: Payload,
+
    #[serde(flatten)]
+
    pub extensions: BTreeMap<Namespace, serde_json::Value>,
+
    pub delegates: NonEmpty<Delegate>,
+
    pub threshold: usize,
+

+
    verified: PhantomData<V>,
+
}
+

+
impl Doc<Verified> {
+
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
+
        let mut buf = Vec::new();
+
        let mut serializer =
+
            serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new());
+

+
        self.serialize(&mut serializer)?;
+
        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
+

+
        Ok((oid.into(), buf))
+
    }
+

+
    /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
+
    pub fn delegate(&mut self, delegate: Delegate) -> bool {
+
        if self.delegates.iter().all(|d| d.id != delegate.id) {
+
            self.delegates.push(delegate);
+
            return true;
+
        }
+
        false
+
    }
+

+
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
+
        let (oid, bytes) = self.encode()?;
+
        let sig = signer.sign(&bytes);
+

+
        Ok((oid, sig))
+
    }
+

+
    pub fn create<'r, S: WriteStorage<'r>>(
+
        &self,
+
        remote: &RemoteId,
+
        msg: &str,
+
        storage: &'r S,
+
    ) -> Result<(Id, git::Oid, S::Repository), Error> {
+
        // You can checkout this branch in your working copy with:
+
        //
+
        //      git fetch rad
+
        //      git checkout -b radicle/id remotes/rad/radicle/id
+
        //
+
        let (doc_oid, doc) = self.encode()?;
+
        let id = Id::from(doc_oid);
+
        let repo = storage.repository(&id).unwrap();
+
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
+
        let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?;
+

+
        drop(tree);
+

+
        Ok((id, oid, repo))
+
    }
+

+
    pub fn update<'r, R: WriteRepository<'r>>(
+
        &self,
+
        remote: &RemoteId,
+
        msg: &str,
+
        signatures: &[(&PublicKey, Signature)],
+
        repo: &R,
+
    ) -> Result<git::Oid, Error> {
+
        let mut msg = format!("{msg}\n\n");
+
        for (key, sig) in signatures {
+
            writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
+
                .expect("in-memory writes don't fail");
+
        }
+

+
        let (_, doc) = self.encode()?;
+
        let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?;
+
        let id_ref = git::refs::storage::id(remote);
+
        let head = repo.raw().find_reference(&id_ref)?.peel_to_commit()?;
+
        let oid = Doc::commit(remote, &tree, &msg, &[&head], repo.raw())?;
+

+
        Ok(oid)
+
    }
+

+
    fn commit(
+
        remote: &RemoteId,
+
        tree: &git2::Tree,
+
        msg: &str,
+
        parents: &[&git2::Commit],
+
        repo: &git2::Repository,
+
    ) -> Result<git::Oid, Error> {
+
        let sig = repo
+
            .signature()
+
            .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
+

+
        let id_ref = git::refs::storage::id(remote);
+
        let oid = repo.commit(Some(&id_ref), &sig, &sig, msg, tree, parents)?;
+

+
        Ok(oid.into())
+
    }
+
}
+

+
impl<V> Deref for Doc<V> {
+
    type Target = Payload;
+

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

+
#[derive(Error, Debug)]
+
pub enum VerificationError {
+
    #[error("invalid name: {0}")]
+
    Name(&'static str),
+
    #[error("invalid description: {0}")]
+
    Description(&'static str),
+
    #[error("invalid default branch: {0}")]
+
    DefaultBranch(&'static str),
+
    #[error("invalid delegates: {0}")]
+
    Delegates(&'static str),
+
    #[error("invalid version `{0}`")]
+
    Version(u32),
+
    #[error("invalid parent: {0}")]
+
    Parent(&'static str),
+
    #[error("invalid threshold `{0}`: {1}")]
+
    Threshold(usize, &'static str),
+
}
+

+
impl Doc<Unverified> {
+
    pub fn initial(
+
        name: String,
+
        description: String,
+
        default_branch: BranchName,
+
        delegate: Delegate,
+
    ) -> Self {
+
        Self {
+
            payload: Payload {
+
                name,
+
                description,
+
                default_branch,
+
            },
+
            extensions: BTreeMap::new(),
+
            delegates: NonEmpty::new(delegate),
+
            threshold: 1,
+
            verified: PhantomData,
+
        }
+
    }
+

+
    pub fn new(
+
        name: String,
+
        description: String,
+
        default_branch: BranchName,
+
        delegates: NonEmpty<Delegate>,
+
        threshold: usize,
+
    ) -> Self {
+
        Self {
+
            payload: Payload {
+
                name,
+
                description,
+
                default_branch,
+
            },
+
            extensions: BTreeMap::new(),
+
            delegates,
+
            threshold,
+
            verified: PhantomData,
+
        }
+
    }
+

+
    pub fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
+
        serde_json::from_slice(bytes)
+
    }
+

+
    pub fn verified(self) -> Result<Doc<Verified>, VerificationError> {
+
        if self.name.is_empty() {
+
            return Err(VerificationError::Name("name cannot be empty"));
+
        }
+
        if self.name.len() > MAX_STRING_LENGTH {
+
            return Err(VerificationError::Name("name cannot exceed 255 bytes"));
+
        }
+
        if self.description.len() > MAX_STRING_LENGTH {
+
            return Err(VerificationError::Description(
+
                "description cannot exceed 255 bytes",
+
            ));
+
        }
+
        if self.delegates.len() > MAX_DELEGATES {
+
            return Err(VerificationError::Delegates(
+
                "number of delegates cannot exceed 255",
+
            ));
+
        }
+
        if self
+
            .delegates
+
            .iter()
+
            .any(|d| d.name.is_empty() || d.name.len() > MAX_STRING_LENGTH)
+
        {
+
            return Err(VerificationError::Delegates(
+
                "delegate name must not be empty and must not exceed 255 bytes",
+
            ));
+
        }
+
        if self.delegates.is_empty() {
+
            return Err(VerificationError::Delegates(
+
                "delegate list cannot be empty",
+
            ));
+
        }
+
        if self.default_branch.is_empty() {
+
            return Err(VerificationError::DefaultBranch(
+
                "default branch cannot be empty",
+
            ));
+
        }
+
        if self.default_branch.len() > MAX_STRING_LENGTH {
+
            return Err(VerificationError::DefaultBranch(
+
                "default branch cannot exceed 255 bytes",
+
            ));
+
        }
+
        if self.threshold > self.delegates.len() {
+
            return Err(VerificationError::Threshold(
+
                self.threshold,
+
                "threshold cannot exceed number of delegates",
+
            ));
+
        }
+
        if self.threshold == 0 {
+
            return Err(VerificationError::Threshold(
+
                self.threshold,
+
                "threshold cannot be zero",
+
            ));
+
        }
+

+
        Ok(Doc {
+
            payload: self.payload,
+
            extensions: self.extensions,
+
            delegates: self.delegates,
+
            threshold: self.threshold,
+
            verified: PhantomData,
+
        })
+
    }
+

+
    pub fn blob_at<'r, R: ReadRepository<'r>>(
+
        commit: Oid,
+
        repo: &R,
+
    ) -> Result<Option<git2::Blob>, git::Error> {
+
        match repo.blob_at(commit, Path::new(&*PATH)) {
+
            Err(git::ext::Error::NotFound(_)) => Ok(None),
+
            Err(e) => Err(e),
+
            Ok(blob) => Ok(Some(blob)),
+
        }
+
    }
+

+
    pub fn load_at<'r, R: ReadRepository<'r>>(
+
        commit: Oid,
+
        repo: &R,
+
    ) -> Result<Option<(Self, Oid)>, git::Error> {
+
        if let Some(blob) = Self::blob_at(commit, repo)? {
+
            let doc = Doc::from_json(blob.content()).unwrap();
+
            return Ok(Some((doc, blob.id().into())));
+
        }
+
        Ok(None)
+
    }
+

+
    pub fn load<'r, R: ReadRepository<'r>>(
+
        remote: &RemoteId,
+
        repo: &R,
+
    ) -> Result<Option<(Self, Oid)>, git::Error> {
+
        if let Some(oid) = Self::head(remote, repo)? {
+
            Self::load_at(oid, repo)
+
        } else {
+
            Ok(None)
+
        }
+
    }
+
}
+

+
impl<V> Doc<V> {
+
    pub fn head<'r, R: ReadRepository<'r>>(
+
        remote: &RemoteId,
+
        repo: &R,
+
    ) -> Result<Option<Oid>, git::Error> {
+
        let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
+
        if let Some(oid) = repo.reference_oid(remote, head)? {
+
            Ok(Some(oid))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+
}
+

+
#[derive(Error, Debug)]
+
pub enum IdentityError {
+
    #[error("git: {0}")]
+
    GitRaw(#[from] git2::Error),
+
    #[error("git: {0}")]
+
    Git(#[from] git::Error),
+
    #[error("verification: {0}")]
+
    Verification(#[from] VerificationError),
+
    #[error("root hash `{0}` does not match project")]
+
    MismatchedRoot(Oid),
+
    #[error("commit signature for {0} is invalid: {1}")]
+
    InvalidSignature(PublicKey, crypto::Error),
+
    #[error("quorum not reached: {0} signatures for a threshold of {1}")]
+
    QuorumNotReached(usize, usize),
+
}
+

+
#[derive(Clone, Debug, PartialEq, Eq)]
+
pub struct Identity<I> {
+
    /// The head of the identity branch. This points to a commit that
+
    /// contains the current document blob.
+
    pub head: Oid,
+
    /// The canonical identifier for this identity.
+
    /// This is the object id of the initial document blob.
+
    pub root: I,
+
    /// The object id of the current document blob.
+
    pub current: Oid,
+
    /// Revision number. The initial document has a revision of `0`.
+
    pub revision: u32,
+
    /// The current document.
+
    pub doc: Doc<Verified>,
+
    /// Signatures over this identity.
+
    pub signatures: HashMap<PublicKey, Signature>,
+
}
+

+
impl Identity<Oid> {
+
    pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
+
        // The root hash must be equal to the id.
+
        if self.root != *id {
+
            return Err(IdentityError::MismatchedRoot(self.root));
+
        }
+

+
        Ok(Identity {
+
            root: id,
+
            head: self.head,
+
            current: self.current,
+
            revision: self.revision,
+
            doc: self.doc,
+
            signatures: self.signatures,
+
        })
+
    }
+
}
+

+
impl Identity<Untrusted> {
+
    pub fn load<'r, R: ReadRepository<'r>>(
+
        remote: &RemoteId,
+
        repo: &R,
+
    ) -> Result<Option<Identity<Oid>>, IdentityError> {
+
        if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
+
            let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
+

+
            // Retrieve root document.
+
            let root_oid = history.pop().unwrap()?.into();
+
            let root_blob = Doc::blob_at(root_oid, repo)?.unwrap();
+
            let root: git::Oid = root_blob.id().into();
+
            let trusted = Doc::from_json(root_blob.content()).unwrap();
+
            let revision = history.len() as u32;
+

+
            let mut trusted = trusted.verified()?;
+
            let mut current = root;
+
            let mut signatures = Vec::new();
+

+
            // Traverse the history chronologically.
+
            for oid in history.into_iter().rev() {
+
                let oid = oid?;
+
                let blob = Doc::blob_at(oid.into(), repo)?.unwrap();
+
                let untrusted = Doc::from_json(blob.content()).unwrap();
+
                let untrusted = untrusted.verified()?;
+
                let commit = repo.commit(oid.into())?.unwrap();
+
                let msg = commit.message_raw().unwrap();
+

+
                // Keys that signed the *current* document version.
+
                signatures = trailers::parse_signatures(msg).unwrap();
+
                for (pk, sig) in &signatures {
+
                    if let Err(err) = pk.verify(blob.content(), sig) {
+
                        return Err(IdentityError::InvalidSignature(*pk, err));
+
                    }
+
                }
+

+
                // Check that enough delegates signed this next version.
+
                let quorum = signatures
+
                    .iter()
+
                    .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key)))
+
                    .count();
+
                if quorum < trusted.threshold {
+
                    return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
+
                }
+

+
                trusted = untrusted;
+
                current = blob.id().into();
+
            }
+

+
            return Ok(Some(Identity {
+
                root,
+
                head,
+
                current,
+
                revision,
+
                doc: trusted,
+
                signatures: signatures.into_iter().collect(),
+
            }));
+
        }
+
        Ok(None)
+
    }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use crate::prelude::Signer;
+
    use crate::rad;
+
    use crate::storage::git::Storage;
+
    use crate::storage::{ReadStorage, WriteStorage};
+
    use crate::test::{crypto, fixtures};
+

+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    #[test]
+
    fn test_valid_identity() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let mut rng = fastrand::Rng::new();
+

+
        let alice = crypto::MockSigner::new(&mut rng);
+
        let bob = crypto::MockSigner::new(&mut rng);
+
        let eve = crypto::MockSigner::new(&mut rng);
+

+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (id, _, _, _) =
+
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
+

+
        // Bob and Eve fork the project from Alice.
+
        rad::fork(&id, alice.public_key(), &bob, &storage).unwrap();
+
        rad::fork(&id, alice.public_key(), &eve, &storage).unwrap();
+

+
        // TODO: In some cases we want to get the repo and the project, but don't
+
        // want to have to create a repository object twice. Perhaps there should
+
        // be a way of getting a project from a repo.
+
        let mut proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
+
        let repo = storage.repository(&id).unwrap();
+

+
        // Make a change to the description and sign it.
+
        proj.doc.payload.description += "!";
+
        proj.sign(&alice)
+
            .and_then(|(_, sig)| {
+
                proj.update(
+
                    alice.public_key(),
+
                    "Update description",
+
                    &[(alice.public_key(), sig)],
+
                    &repo,
+
                )
+
            })
+
            .unwrap();
+

+
        // Add Bob as a delegate, and sign it.
+
        proj.delegate("bob".to_owned(), *bob.public_key());
+
        proj.doc.threshold = 2;
+
        proj.sign(&alice)
+
            .and_then(|(_, sig)| {
+
                proj.update(
+
                    alice.public_key(),
+
                    "Add bob",
+
                    &[(alice.public_key(), sig)],
+
                    &repo,
+
                )
+
            })
+
            .unwrap();
+

+
        // Add Eve as a delegate, and sign it.
+
        proj.delegate("eve".to_owned(), *eve.public_key());
+
        proj.sign(&alice)
+
            .and_then(|(_, alice_sig)| {
+
                proj.sign(&bob).and_then(|(_, bob_sig)| {
+
                    proj.update(
+
                        alice.public_key(),
+
                        "Add eve",
+
                        &[(alice.public_key(), alice_sig), (bob.public_key(), bob_sig)],
+
                        &repo,
+
                    )
+
                })
+
            })
+
            .unwrap();
+

+
        // Update description again with signatures by Eve and Bob.
+
        proj.doc.payload.description += "?";
+
        let (current, head) = proj
+
            .sign(&bob)
+
            .and_then(|(_, bob_sig)| {
+
                proj.sign(&eve).and_then(|(blob_id, eve_sig)| {
+
                    proj.update(
+
                        alice.public_key(),
+
                        "Update description",
+
                        &[(bob.public_key(), bob_sig), (eve.public_key(), eve_sig)],
+
                        &repo,
+
                    )
+
                    .map(|head| (blob_id, head))
+
                })
+
            })
+
            .unwrap();
+

+
        let identity: Identity<Id> = Identity::load(alice.public_key(), &repo)
+
            .unwrap()
+
            .unwrap()
+
            .verified(id.clone())
+
            .unwrap();
+

+
        assert_eq!(identity.signatures.len(), 2);
+
        assert_eq!(identity.revision, 4);
+
        assert_eq!(identity.root, id);
+
        assert_eq!(identity.current, current);
+
        assert_eq!(identity.head, head);
+
        assert_eq!(identity.doc, proj.doc);
+

+
        let proj = storage.get(alice.public_key(), &id).unwrap().unwrap();
+
        assert_eq!(proj.description, "Acme's repository!?");
+
    }
+

+
    #[quickcheck]
+
    fn prop_encode_decode(doc: Doc<Verified>) {
+
        let (_, bytes) = doc.encode().unwrap();
+
        assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
+
    }
+
}
added radicle-node/src/lib.rs
@@ -0,0 +1,34 @@
+
#![allow(dead_code)]
+
pub use nakamoto_net::{Io, Link, LocalDuration, LocalTime};
+

+
pub mod address_book;
+
pub mod address_manager;
+
pub mod client;
+
pub mod clock;
+
pub mod collections;
+
pub mod control;
+
pub mod crypto;
+
pub mod decoder;
+
pub mod git;
+
pub mod hash;
+
pub mod identity;
+
pub mod logger;
+
pub mod rad;
+
pub mod serde_ext;
+
pub mod service;
+
pub mod storage;
+
#[cfg(test)]
+
pub mod test;
+
pub mod transport;
+
pub mod wire;
+

+
pub mod prelude {
+
    pub use crate::crypto::{PublicKey, Signature, Signer};
+
    pub use crate::decoder::Decoder;
+
    pub use crate::hash::Digest;
+
    pub use crate::identity::{Did, Id};
+
    pub use crate::service::filter::Filter;
+
    pub use crate::service::{NodeId, Timestamp};
+
    pub use crate::storage::refs::Refs;
+
    pub use crate::storage::WriteStorage;
+
}
added radicle-node/src/logger.rs
@@ -0,0 +1,61 @@
+
//! Logging module.
+
use std::io;
+

+
use chrono::prelude::*;
+
use colored::*;
+
use log::{Level, Log, Metadata, Record, SetLoggerError};
+

+
struct Logger {
+
    level: Level,
+
}
+

+
impl Log for Logger {
+
    fn enabled(&self, metadata: &Metadata) -> bool {
+
        metadata.level() <= self.level
+
    }
+

+
    fn log(&self, record: &Record) {
+
        if self.enabled(record.metadata()) {
+
            let module = record.module_path().unwrap_or_default();
+

+
            if record.level() == Level::Error {
+
                write(record, module, io::stderr());
+
            } else {
+
                write(record, module, io::stdout());
+
            }
+

+
            fn write(record: &log::Record, module: &str, mut stream: impl io::Write) {
+
                let message = format!("{} {} {}", record.level(), module.bold(), record.args());
+
                let message = match record.level() {
+
                    Level::Error => message.red(),
+
                    Level::Warn => message.yellow(),
+
                    Level::Info => message.normal(),
+
                    Level::Debug => message.dimmed(),
+
                    Level::Trace => message.white().dimmed(),
+
                };
+

+
                writeln!(
+
                    stream,
+
                    "{} {}",
+
                    Local::now()
+
                        .to_rfc3339_opts(SecondsFormat::Millis, true)
+
                        .white(),
+
                    message,
+
                )
+
                .expect("write shouldn't fail");
+
            }
+
        }
+
    }
+

+
    fn flush(&self) {}
+
}
+

+
/// Initialize a new logger.
+
pub fn init(level: Level) -> Result<(), SetLoggerError> {
+
    let logger = Logger { level };
+

+
    log::set_boxed_logger(Box::new(logger))?;
+
    log::set_max_level(level.to_level_filter());
+

+
    Ok(())
+
}
added radicle-node/src/main.rs
@@ -0,0 +1,36 @@
+
use std::path::Path;
+
use std::thread;
+
use std::{env, net};
+

+
use radicle_node::crypto::{PublicKey, Signature, Signer};
+
use radicle_node::{client, control};
+

+
type Reactor = nakamoto_net_poll::Reactor<net::TcpStream>;
+

+
struct FailingSigner {}
+

+
impl Signer for FailingSigner {
+
    fn public_key(&self) -> &PublicKey {
+
        panic!("Failing signer always fails!");
+
    }
+

+
    fn sign(&self, _msg: &[u8]) -> Signature {
+
        panic!("Failing signer always fails!");
+
    }
+
}
+

+
fn main() -> anyhow::Result<()> {
+
    let signer = FailingSigner {};
+
    let client = client::Client::<Reactor, _>::new(Path::new("."), signer)?;
+
    let handle = client.handle();
+
    let config = client::Config::default();
+
    let socket = env::var("RAD_SOCKET").unwrap_or_else(|_| control::DEFAULT_SOCKET_NAME.to_owned());
+

+
    let t1 = thread::spawn(move || control::listen(socket, handle));
+
    let t2 = thread::spawn(move || client.run(config));
+

+
    t1.join().unwrap()?;
+
    t2.join().unwrap()?;
+

+
    Ok(())
+
}
added radicle-node/src/rad.rs
@@ -0,0 +1,329 @@
+
use std::io;
+
use std::path::Path;
+

+
use thiserror::Error;
+

+
use crate::crypto::{Signer, Verified};
+
use crate::git;
+
use crate::identity::Id;
+
use crate::storage::refs::SignedRefs;
+
use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
+
use crate::{identity, storage};
+

+
pub const REMOTE_NAME: &str = "rad";
+

+
#[derive(Error, Debug)]
+
pub enum InitError {
+
    #[error("doc: {0}")]
+
    Doc(#[from] identity::doc::Error),
+
    #[error("doc: {0}")]
+
    DocVerification(#[from] identity::doc::VerificationError),
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("storage: {0}")]
+
    Storage(#[from] storage::Error),
+
    #[error("cannot initialize project inside a bare repository")]
+
    BareRepo,
+
    #[error("cannot initialize project from detached head state")]
+
    DetachedHead,
+
    #[error("HEAD reference is not valid UTF-8")]
+
    InvalidHead,
+
}
+

+
/// Initialize a new radicle project from a git repository.
+
pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
+
    repo: &git2::Repository,
+
    name: &str,
+
    description: &str,
+
    default_branch: BranchName,
+
    signer: G,
+
    storage: &'r S,
+
) -> Result<(Id, SignedRefs<Verified>), InitError> {
+
    let pk = signer.public_key();
+
    let delegate = identity::Delegate {
+
        // TODO: Use actual user name.
+
        name: String::from("anonymous"),
+
        id: identity::Did::from(*pk),
+
    };
+
    let doc = identity::Doc::initial(
+
        name.to_owned(),
+
        description.to_owned(),
+
        default_branch.clone(),
+
        delegate,
+
    )
+
    .verified()?;
+

+
    let (id, _, project) = doc.create(pk, "Initialize Radicle", storage)?;
+

+
    git::set_upstream(
+
        repo,
+
        REMOTE_NAME,
+
        &default_branch,
+
        &git::refs::storage::branch(pk, &default_branch),
+
    )?;
+

+
    // TODO: Note that you'll likely want to use `RemoteCallbacks` and set
+
    // `push_update_reference` to test whether all the references were pushed
+
    // successfully.
+
    git::configure_remote(repo, REMOTE_NAME, pk, project.path())?.push::<&str>(
+
        &[&format!(
+
            "{}:{}",
+
            &git::refs::workdir::branch(&default_branch),
+
            &git::refs::storage::branch(pk, &default_branch),
+
        )],
+
        None,
+
    )?;
+
    let signed = storage.sign_refs(&project, signer)?;
+

+
    Ok((id, signed))
+
}
+

+
#[derive(Error, Debug)]
+
pub enum ForkError {
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("storage: {0}")]
+
    Storage(#[from] storage::Error),
+
    #[error("project `{0}` was not found in storage")]
+
    NotFound(Id),
+
    #[error("git: invalid reference")]
+
    InvalidReference,
+
}
+

+
/// Create a local tree for an existing project, from an existing remote.
+
pub fn fork<'r, G: Signer, S: storage::WriteStorage<'r>>(
+
    proj: &Id,
+
    remote: &RemoteId,
+
    signer: G,
+
    storage: S,
+
) -> Result<(), ForkError> {
+
    // TODO: Copy tags over?
+

+
    // Creates or copies the following references:
+
    //
+
    // refs/remotes/<pk>/heads/master
+
    // refs/remotes/<pk>/heads/radicle/id
+
    // refs/remotes/<pk>/tags/*
+
    // refs/remotes/<pk>/rad/signature
+

+
    let me = signer.public_key();
+
    let project = storage
+
        .get(remote, proj)?
+
        .ok_or_else(|| ForkError::NotFound(proj.clone()))?;
+
    let repository = storage.repository(proj)?;
+

+
    let raw = repository.raw();
+
    let remote_head = raw
+
        .find_reference(&git::refs::storage::branch(
+
            remote,
+
            &project.doc.default_branch,
+
        ))?
+
        .target()
+
        .ok_or(ForkError::InvalidReference)?;
+
    raw.reference(
+
        &git::refs::storage::branch(me, &project.doc.default_branch),
+
        remote_head,
+
        false,
+
        &format!("creating default branch for {me}"),
+
    )?;
+

+
    let remote_id = raw
+
        .find_reference(&git::refs::storage::id(remote))?
+
        .target()
+
        .ok_or(ForkError::InvalidReference)?;
+
    raw.reference(
+
        &git::refs::storage::id(me),
+
        remote_id,
+
        false,
+
        &format!("creating identity branch for {me}"),
+
    )?;
+

+
    storage.sign_refs(&repository, &signer)?;
+

+
    Ok(())
+
}
+

+
#[derive(Error, Debug)]
+
pub enum CheckoutError {
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("storage: {0}")]
+
    Storage(#[from] storage::Error),
+
    #[error("project `{0}` was not found in storage")]
+
    NotFound(Id),
+
}
+

+
/// Checkout a project from storage as a working copy.
+
/// This effectively does a `git-clone` from storage.
+
pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
+
    proj: &Id,
+
    remote: &RemoteId,
+
    path: P,
+
    storage: S,
+
) -> Result<git2::Repository, CheckoutError> {
+
    // TODO: Decide on whether we can use `clone_local`
+
    // TODO: Look into sharing object databases.
+
    let project = storage
+
        .get(remote, proj)?
+
        .ok_or_else(|| CheckoutError::NotFound(proj.clone()))?;
+

+
    let mut opts = git2::RepositoryInitOptions::new();
+
    opts.no_reinit(true).description(&project.doc.description);
+

+
    let repo = git2::Repository::init_opts(path, &opts)?;
+
    let default_branch = project.doc.default_branch.as_str();
+

+
    // Configure and fetch all refs from remote.
+
    git::configure_remote(&repo, REMOTE_NAME, remote, &project.path)?.fetch::<&str>(
+
        &[],
+
        None,
+
        None,
+
    )?;
+

+
    {
+
        // Setup default branch.
+
        let remote_head_ref = git::refs::workdir::remote_branch(REMOTE_NAME, default_branch);
+
        let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
+
        let _ = repo.branch(default_branch, &remote_head_commit, true)?;
+

+
        // Setup remote tracking for default branch.
+
        git::set_upstream(
+
            &repo,
+
            REMOTE_NAME,
+
            default_branch,
+
            &git::refs::storage::branch(remote, default_branch),
+
        )?;
+
    }
+

+
    Ok(repo)
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use crate::git::fmt::refname;
+
    use crate::identity::{Delegate, Did};
+
    use crate::storage::git::Storage;
+
    use crate::storage::{ReadStorage, WriteStorage};
+
    use crate::test::{crypto, fixtures};
+

+
    #[test]
+
    fn test_init() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let signer = crypto::MockSigner::default();
+
        let public_key = *signer.public_key();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
+

+
        let (proj, refs) = init(
+
            &repo,
+
            "acme",
+
            "Acme's repo",
+
            BranchName::from("master"),
+
            &signer,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        let project = storage.get(&public_key, &proj).unwrap().unwrap();
+

+
        assert_eq!(project.remotes[&public_key].refs, refs);
+
        assert_eq!(project.id, proj);
+
        assert_eq!(project.doc.name, "acme");
+
        assert_eq!(project.doc.description, "Acme's repo");
+
        assert_eq!(project.doc.default_branch, BranchName::from("master"));
+
        assert_eq!(
+
            project.doc.delegates.first(),
+
            &Delegate {
+
                name: String::from("anonymous"),
+
                id: Did::from(public_key),
+
            }
+
        );
+
    }
+

+
    #[test]
+
    fn test_fork() {
+
        let mut rng = fastrand::Rng::new();
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let alice = crypto::MockSigner::new(&mut rng);
+
        let alice_id = alice.public_key();
+
        let bob = crypto::MockSigner::new(&mut rng);
+
        let bob_id = bob.public_key();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
+

+
        // Alice creates a project.
+
        let (id, alice_refs) = init(
+
            &original,
+
            "acme",
+
            "Acme's repo",
+
            BranchName::from("master"),
+
            &alice,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        // Bob forks it and creates a checkout.
+
        fork(&id, alice_id, &bob, &storage).unwrap();
+
        checkout(&id, bob_id, tempdir.path().join("copy"), &storage).unwrap();
+

+
        let bob_remote = storage.repository(&id).unwrap().remote(bob_id).unwrap();
+

+
        assert_eq!(
+
            bob_remote.refs.get(&refname!("master")),
+
            alice_refs.get(&refname!("master"))
+
        );
+
    }
+

+
    #[test]
+
    fn test_checkout() {
+
        let tempdir = tempfile::tempdir().unwrap();
+
        let signer = crypto::MockSigner::default();
+
        let remote_id = signer.public_key();
+
        let storage = Storage::open(tempdir.path().join("storage")).unwrap();
+
        let (original, _) = fixtures::repository(tempdir.path().join("original"));
+

+
        let (id, _) = init(
+
            &original,
+
            "acme",
+
            "Acme's repo",
+
            BranchName::from("master"),
+
            &signer,
+
            &storage,
+
        )
+
        .unwrap();
+

+
        let copy = checkout(&id, remote_id, tempdir.path().join("copy"), &storage).unwrap();
+

+
        assert_eq!(
+
            copy.head().unwrap().target(),
+
            original.head().unwrap().target()
+
        );
+
        assert_eq!(
+
            copy.branch_upstream_name("refs/heads/master")
+
                .unwrap()
+
                .to_vec(),
+
            original
+
                .branch_upstream_name("refs/heads/master")
+
                .unwrap()
+
                .to_vec()
+
        );
+
        assert_eq!(
+
            copy.find_remote(REMOTE_NAME)
+
                .unwrap()
+
                .refspecs()
+
                .into_iter()
+
                .map(|r| r.bytes().to_vec())
+
                .collect::<Vec<_>>(),
+
            original
+
                .find_remote(REMOTE_NAME)
+
                .unwrap()
+
                .refspecs()
+
                .into_iter()
+
                .map(|r| r.bytes().to_vec())
+
                .collect::<Vec<_>>(),
+
        );
+
    }
+
}
added radicle-node/src/serde_ext.rs
@@ -0,0 +1,25 @@
+
pub mod string {
+
    use std::fmt::Display;
+
    use std::str::FromStr;
+

+
    use serde::{de, Deserialize, Deserializer, Serializer};
+

+
    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
+
    where
+
        T: Display,
+
        S: Serializer,
+
    {
+
        serializer.collect_str(value)
+
    }
+

+
    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
+
    where
+
        T: FromStr,
+
        T::Err: Display,
+
        D: Deserializer<'de>,
+
    {
+
        String::deserialize(deserializer)?
+
            .parse()
+
            .map_err(de::Error::custom)
+
    }
+
}
added radicle-node/src/service.rs
@@ -0,0 +1,887 @@
+
#![allow(dead_code)]
+
pub mod config;
+
pub mod filter;
+
pub mod message;
+
pub mod peer;
+

+
use std::ops::{Deref, DerefMut};
+
use std::{collections::VecDeque, fmt, net, net::IpAddr};
+

+
use crossbeam_channel as chan;
+
use fastrand::Rng;
+
use git_url::Url;
+
use log::*;
+
use nakamoto::{LocalDuration, LocalTime};
+
use nakamoto_net as nakamoto;
+
use nakamoto_net::Link;
+
use nonempty::NonEmpty;
+

+
use crate::address_book;
+
use crate::address_book::AddressBook;
+
use crate::address_manager::AddressManager;
+
use crate::clock::RefClock;
+
use crate::collections::{HashMap, HashSet};
+
use crate::crypto;
+
use crate::identity::{Id, Project};
+
use crate::service::config::ProjectTracking;
+
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
+
use crate::service::peer::{Peer, PeerError, PeerState};
+
use crate::storage;
+
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, WriteStorage};
+

+
pub use crate::service::config::{Config, Network};
+
pub use crate::service::message::{Envelope, Message};
+

+
use self::filter::Filter;
+
use self::message::{InventoryAnnouncement, NodeFeatures};
+

+
pub const DEFAULT_PORT: u16 = 8776;
+
pub const PROTOCOL_VERSION: u32 = 1;
+
pub const TARGET_OUTBOUND_PEERS: usize = 8;
+
pub const IDLE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
+
pub const ANNOUNCE_INTERVAL: LocalDuration = LocalDuration::from_secs(30);
+
pub const SYNC_INTERVAL: LocalDuration = LocalDuration::from_secs(60);
+
pub const PRUNE_INTERVAL: LocalDuration = LocalDuration::from_mins(30);
+
pub const MAX_CONNECTION_ATTEMPTS: usize = 3;
+
pub const MAX_TIME_DELTA: LocalDuration = LocalDuration::from_mins(60);
+

+
/// Network node identifier.
+
pub type NodeId = crypto::PublicKey;
+
/// Network routing table. Keeps track of where projects are hosted.
+
pub type Routing = HashMap<Id, HashSet<NodeId>>;
+
/// Seconds since epoch.
+
pub type Timestamp = u64;
+

+
/// Output of a state transition.
+
#[derive(Debug)]
+
pub enum Io {
+
    /// There are some messages ready to be sent to a peer.
+
    Write(net::SocketAddr, Vec<Envelope>),
+
    /// Connect to a peer.
+
    Connect(net::SocketAddr),
+
    /// Disconnect from a peer.
+
    Disconnect(net::SocketAddr, DisconnectReason),
+
    /// Ask for a wakeup in a specified amount of time.
+
    Wakeup(LocalDuration),
+
    /// Emit an event.
+
    Event(Event),
+
}
+

+
/// A service event.
+
#[derive(Debug, Clone)]
+
pub enum Event {
+
    RefsFetched {
+
        from: Url,
+
        project: Id,
+
        updated: Vec<RefUpdate>,
+
    },
+
}
+

+
/// Error returned by [`Command::Fetch`].
+
#[derive(thiserror::Error, Debug)]
+
pub enum FetchError {
+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
    #[error(transparent)]
+
    Storage(#[from] storage::Error),
+
    #[error(transparent)]
+
    Fetch(#[from] storage::FetchError),
+
}
+

+
/// Result of looking up seeds in our routing table.
+
#[derive(Debug)]
+
pub enum FetchLookup {
+
    /// Found seeds for the given project.
+
    Found {
+
        seeds: NonEmpty<net::SocketAddr>,
+
        results: chan::Receiver<FetchResult>,
+
    },
+
    /// Can't fetch because no seeds were found for this project.
+
    NotFound,
+
    /// Can't fetch because the project isn't tracked.
+
    NotTracking,
+
    /// Error trying to find seeds.
+
    Error(FetchError),
+
}
+

+
/// Result of a fetch request from a specific seed.
+
#[derive(Debug)]
+
#[allow(clippy::large_enum_variant)]
+
pub enum FetchResult {
+
    /// Successful fetch from a seed.
+
    Fetched {
+
        from: net::SocketAddr,
+
        updated: Vec<RefUpdate>,
+
    },
+
    /// Error fetching the resource from a seed.
+
    Error {
+
        from: net::SocketAddr,
+
        error: FetchError,
+
    },
+
}
+

+
/// Commands sent to the service by the operator.
+
#[derive(Debug)]
+
pub enum Command {
+
    AnnounceRefs(Id),
+
    Connect(net::SocketAddr),
+
    Fetch(Id, chan::Sender<FetchLookup>),
+
    Track(Id, chan::Sender<bool>),
+
    Untrack(Id, chan::Sender<bool>),
+
}
+

+
/// Command-related errors.
+
#[derive(thiserror::Error, Debug)]
+
pub enum CommandError {}
+

+
#[derive(Debug)]
+
pub struct Service<S, T, G> {
+
    /// Peers currently or recently connected.
+
    peers: Peers,
+
    /// Service state that isn't peer-specific.
+
    context: Context<S, T, G>,
+
    /// Whether our local inventory no long represents what we have announced to the network.
+
    out_of_sync: bool,
+
    /// Last time the service was idle.
+
    last_idle: LocalTime,
+
    /// Last time the service synced.
+
    last_sync: LocalTime,
+
    /// Last time the service routing table was pruned.
+
    last_prune: LocalTime,
+
    /// Last time the service announced its inventory.
+
    last_announce: LocalTime,
+
    /// Time when the service was initialized.
+
    start_time: LocalTime,
+
}
+

+
impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Service<S, T, G> {
+
    pub fn new(
+
        config: Config,
+
        clock: RefClock,
+
        storage: T,
+
        addresses: S,
+
        signer: G,
+
        rng: Rng,
+
    ) -> Self {
+
        let addrmgr = AddressManager::new(addresses);
+

+
        Self {
+
            context: Context::new(config, clock, storage, addrmgr, signer, rng.clone()),
+
            peers: Peers::new(rng),
+
            out_of_sync: false,
+
            last_idle: LocalTime::default(),
+
            last_sync: LocalTime::default(),
+
            last_prune: LocalTime::default(),
+
            last_announce: LocalTime::default(),
+
            start_time: LocalTime::default(),
+
        }
+
    }
+

+
    pub fn disconnect(&mut self, remote: &IpAddr, reason: DisconnectReason) {
+
        if let Some(addr) = self.peers.get(remote).map(|p| p.addr) {
+
            self.context.disconnect(addr, reason);
+
        }
+
    }
+

+
    pub fn seeds(&self, id: &Id) -> Box<dyn Iterator<Item = (&NodeId, &Peer)> + '_> {
+
        if let Some(peers) = self.routing.get(id) {
+
            Box::new(
+
                peers
+
                    .iter()
+
                    .filter_map(|id| self.peers.by_id(id).map(|p| (id, p))),
+
            )
+
        } else {
+
            Box::new(std::iter::empty())
+
        }
+
    }
+

+
    pub fn tracked(&self) -> Result<Vec<Id>, storage::Error> {
+
        let tracked = match &self.config.project_tracking {
+
            ProjectTracking::All { blocked } => self
+
                .storage
+
                .inventory()?
+
                .into_iter()
+
                .filter(|id| !blocked.contains(id))
+
                .collect(),
+

+
            ProjectTracking::Allowed(projs) => projs.iter().cloned().collect(),
+
        };
+

+
        Ok(tracked)
+
    }
+

+
    /// Track a project.
+
    /// Returns whether or not the tracking policy was updated.
+
    pub fn track(&mut self, id: Id) -> bool {
+
        self.out_of_sync = self.config.track(id);
+
        self.out_of_sync
+
    }
+

+
    /// Untrack a project.
+
    /// Returns whether or not the tracking policy was updated.
+
    /// Note that when untracking, we don't announce anything to the network. This is because by
+
    /// simply not announcing it anymore, it will eventually be pruned by nodes.
+
    pub fn untrack(&mut self, id: Id) -> bool {
+
        self.config.untrack(id)
+
    }
+

+
    /// Find the closest `n` peers by proximity in tracking graphs.
+
    /// Returns a sorted list from the closest peer to the furthest.
+
    /// Peers with more trackings in common score score higher.
+
    #[allow(unused)]
+
    pub fn closest_peers(&self, n: usize) -> Vec<NodeId> {
+
        todo!()
+
    }
+

+
    /// Get the connected peers.
+
    pub fn peers(&self) -> &Peers {
+
        &self.peers
+
    }
+

+
    /// Get the current inventory.
+
    pub fn inventory(&self) -> Result<Inventory, storage::Error> {
+
        self.context.storage.inventory()
+
    }
+

+
    /// Get the storage instance.
+
    pub fn storage(&self) -> &T {
+
        &self.context.storage
+
    }
+

+
    /// Get the mutable storage instance.
+
    pub fn storage_mut(&mut self) -> &mut T {
+
        &mut self.context.storage
+
    }
+

+
    /// Get a project from storage, using the local node's key.
+
    pub fn get(&self, proj: &Id) -> Result<Option<Project>, storage::Error> {
+
        self.storage.get(&self.node_id(), proj)
+
    }
+

+
    /// Get the local signer.
+
    pub fn signer(&self) -> &G {
+
        &self.context.signer
+
    }
+

+
    /// Get the local service time.
+
    pub fn local_time(&self) -> LocalTime {
+
        self.context.clock.local_time()
+
    }
+

+
    /// Get service configuration.
+
    pub fn config(&self) -> &Config {
+
        &self.context.config
+
    }
+

+
    /// Get reference to routing table.
+
    pub fn routing(&self) -> &Routing {
+
        &self.context.routing
+
    }
+

+
    /// Get I/O outbox.
+
    pub fn outbox(&mut self) -> &mut VecDeque<Io> {
+
        &mut self.context.io
+
    }
+

+
    pub fn lookup(&self, id: &Id) -> Lookup {
+
        Lookup {
+
            local: self.context.storage.get(&self.node_id(), id).unwrap(),
+
            remote: self
+
                .context
+
                .routing
+
                .get(id)
+
                .map_or(vec![], |r| r.iter().cloned().collect()),
+
        }
+
    }
+

+
    pub fn initialize(&mut self, time: LocalTime) {
+
        trace!("Init {}", time.as_secs());
+

+
        self.start_time = time;
+

+
        // Connect to configured peers.
+
        let addrs = self.context.config.connect.clone();
+
        for addr in addrs {
+
            self.context.connect(addr);
+
        }
+
    }
+

+
    pub fn tick(&mut self, now: nakamoto::LocalTime) {
+
        trace!("Tick +{}", now - self.start_time);
+

+
        self.context.clock.set(now);
+
    }
+

+
    pub fn wake(&mut self) {
+
        let now = self.context.clock.local_time();
+

+
        trace!("Wake +{}", now - self.start_time);
+

+
        if now - self.last_idle >= IDLE_INTERVAL {
+
            debug!("Running 'idle' task...");
+

+
            self.maintain_connections();
+
            self.context.io.push_back(Io::Wakeup(IDLE_INTERVAL));
+
            self.last_idle = now;
+
        }
+
        if now - self.last_sync >= SYNC_INTERVAL {
+
            debug!("Running 'sync' task...");
+

+
            // TODO: What do we do here?
+
            self.context.io.push_back(Io::Wakeup(SYNC_INTERVAL));
+
            self.last_sync = now;
+
        }
+
        if now - self.last_announce >= ANNOUNCE_INTERVAL {
+
            if self.out_of_sync {
+
                self.announce_inventory().unwrap();
+
            }
+
            self.context.io.push_back(Io::Wakeup(ANNOUNCE_INTERVAL));
+
            self.last_announce = now;
+
        }
+
        if now - self.last_prune >= PRUNE_INTERVAL {
+
            debug!("Running 'prune' task...");
+

+
            self.prune_routing_entries();
+
            self.context.io.push_back(Io::Wakeup(PRUNE_INTERVAL));
+
            self.last_prune = now;
+
        }
+
    }
+

+
    pub fn command(&mut self, cmd: Command) {
+
        debug!("Command {:?}", cmd);
+

+
        match cmd {
+
            Command::Connect(addr) => self.context.connect(addr),
+
            Command::Fetch(id, resp) => {
+
                if !self.config.is_tracking(&id) {
+
                    resp.send(FetchLookup::NotTracking).ok();
+
                    return;
+
                }
+

+
                let seeds = self.seeds(&id).collect::<Vec<_>>();
+
                let seeds = if let Some(seeds) = NonEmpty::from_vec(seeds) {
+
                    seeds
+
                } else {
+
                    log::error!("No seeds found for {}", id);
+
                    resp.send(FetchLookup::NotFound).ok();
+

+
                    return;
+
                };
+
                log::debug!("Found {} seeds for {}", seeds.len(), id);
+

+
                let mut repo = match self.storage.repository(&id) {
+
                    Ok(repo) => repo,
+
                    Err(err) => {
+
                        log::error!("Error opening repo for {}: {}", id, err);
+
                        resp.send(FetchLookup::Error(err.into())).ok();
+

+
                        return;
+
                    }
+
                };
+

+
                let (results_, results) = chan::bounded(seeds.len());
+
                resp.send(FetchLookup::Found {
+
                    seeds: seeds.clone().map(|(_, peer)| peer.addr),
+
                    results,
+
                })
+
                .ok();
+

+
                // TODO: Limit the number of seeds we fetch from? Randomize?
+
                for (_, peer) in seeds {
+
                    match repo.fetch(&Url {
+
                        scheme: git_url::Scheme::Git,
+
                        host: Some(peer.addr.ip().to_string()),
+
                        port: Some(peer.addr.port()),
+
                        // TODO: Fix upstream crate so that it adds a `/` when needed.
+
                        path: format!("/{}", id).into(),
+
                        ..Url::default()
+
                    }) {
+
                        Ok(updated) => {
+
                            results_
+
                                .send(FetchResult::Fetched {
+
                                    from: peer.addr,
+
                                    updated,
+
                                })
+
                                .ok();
+
                        }
+
                        Err(err) => {
+
                            results_
+
                                .send(FetchResult::Error {
+
                                    from: peer.addr,
+
                                    error: err.into(),
+
                                })
+
                                .ok();
+
                        }
+
                    }
+
                }
+
            }
+
            Command::Track(id, resp) => {
+
                resp.send(self.track(id)).ok();
+
            }
+
            Command::Untrack(id, resp) => {
+
                resp.send(self.untrack(id)).ok();
+
            }
+
            Command::AnnounceRefs(id) => {
+
                let node = self.node_id();
+
                let repo = self.storage.repository(&id).unwrap();
+
                let remote = repo.remote(&node).unwrap();
+
                let peers = self.peers.negotiated().map(|(_, p)| p);
+
                let refs = remote.refs.into();
+
                let message = RefsAnnouncement { id, refs };
+
                let signature = message.sign(&self.signer);
+

+
                self.context.broadcast(
+
                    Message::RefsAnnouncement {
+
                        node,
+
                        message,
+
                        signature,
+
                    },
+
                    peers,
+
                );
+
            }
+
        }
+
    }
+

+
    pub fn attempted(&mut self, addr: &std::net::SocketAddr) {
+
        let ip = addr.ip();
+
        let persistent = self.context.config.is_persistent(addr);
+
        let peer = self
+
            .peers
+
            .entry(ip)
+
            .or_insert_with(|| Peer::new(*addr, Link::Outbound, persistent));
+

+
        peer.attempted();
+
    }
+

+
    pub fn connected(
+
        &mut self,
+
        addr: std::net::SocketAddr,
+
        _local_addr: &std::net::SocketAddr,
+
        link: Link,
+
    ) {
+
        let ip = addr.ip();
+

+
        debug!("Connected to {} ({:?})", ip, link);
+

+
        // For outbound connections, we are the first to say "Hello".
+
        // For inbound connections, we wait for the remote to say "Hello" first.
+
        // TODO: How should we deal with multiple peers connecting from the same IP address?
+
        if link.is_outbound() {
+
            // TODO: Refactor this so that we don't create messages if the peer isn't found.
+
            let messages = self.handshake_messages();
+

+
            if let Some(peer) = self.peers.get_mut(&ip) {
+
                self.context.write_all(peer.addr, messages);
+
                peer.connected();
+
            }
+
        } else {
+
            self.peers.insert(
+
                ip,
+
                Peer::new(
+
                    addr,
+
                    Link::Inbound,
+
                    self.context.config.is_persistent(&addr),
+
                ),
+
            );
+
        }
+
    }
+

+
    pub fn disconnected(
+
        &mut self,
+
        addr: &std::net::SocketAddr,
+
        reason: nakamoto::DisconnectReason<DisconnectReason>,
+
    ) {
+
        let since = self.local_time();
+
        let ip = addr.ip();
+

+
        debug!("Disconnected from {} ({})", ip, reason);
+

+
        if let Some(peer) = self.peers.get_mut(&ip) {
+
            peer.state = PeerState::Disconnected { since };
+

+
            // Attempt to re-connect to persistent peers.
+
            if self.context.config.is_persistent(addr) && peer.attempts() < MAX_CONNECTION_ATTEMPTS
+
            {
+
                if reason.is_dial_err() {
+
                    return;
+
                }
+
                if let nakamoto::DisconnectReason::Protocol(r) = reason {
+
                    if !r.is_transient() {
+
                        return;
+
                    }
+
                }
+
                // TODO: Eventually we want a delay before attempting a reconnection,
+
                // with exponential back-off.
+
                debug!("Reconnecting to {} (attempts={})...", ip, peer.attempts());
+

+
                // TODO: Try to reconnect only if the peer was attempted. A disconnect without
+
                // even a successful attempt means that we're unlikely to be able to reconnect.
+

+
                self.context.connect(*addr);
+
            } else {
+
                // TODO: Non-persistent peers should be removed from the
+
                // map here or at some later point.
+
            }
+
        }
+
    }
+

+
    pub fn received_message(&mut self, addr: &std::net::SocketAddr, msg: Envelope) {
+
        let peer_ip = addr.ip();
+
        let peer = if let Some(peer) = self.peers.get_mut(&peer_ip) {
+
            peer
+
        } else {
+
            return;
+
        };
+

+
        let relay = match peer.received(msg, &mut self.context) {
+
            Ok(msg) => msg,
+
            Err(err) => {
+
                self.context
+
                    .disconnect(peer.addr, DisconnectReason::Error(err));
+
                // If there's an error, stop processing messages from this peer.
+
                // However, we still relay messages returned up to this point.
+
                //
+
                // FIXME: The peer should be set in a state such that we don'that
+
                // process further messages.
+
                return;
+
            }
+
        };
+

+
        if let Some(msg) = relay {
+
            let negotiated = self
+
                .peers
+
                .negotiated()
+
                .filter(|(ip, _)| **ip != peer_ip)
+
                .map(|(_, p)| p);
+

+
            self.context.relay(msg, negotiated.clone());
+
        }
+
    }
+

+
    ////////////////////////////////////////////////////////////////////////////
+
    // Periodic tasks
+
    ////////////////////////////////////////////////////////////////////////////
+

+
    /// Announce our inventory to all connected peers.
+
    fn announce_inventory(&mut self) -> Result<(), storage::Error> {
+
        let inv = Message::inventory(self.context.inventory_announcement()?, &self.context.signer);
+

+
        for addr in self.peers.negotiated().map(|(_, p)| p.addr) {
+
            self.context.write(addr, inv.clone());
+
        }
+
        Ok(())
+
    }
+

+
    fn prune_routing_entries(&mut self) {
+
        // TODO
+
    }
+

+
    fn maintain_connections(&mut self) {
+
        // TODO: Connect to all potential seeds.
+
        if self.peers.len() < TARGET_OUTBOUND_PEERS {
+
            let delta = TARGET_OUTBOUND_PEERS - self.peers.len();
+

+
            for _ in 0..delta {
+
                // TODO: Connect to random peer.
+
            }
+
        }
+
    }
+
}
+

+
impl<S, T, G> Deref for Service<S, T, G> {
+
    type Target = Context<S, T, G>;
+

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

+
impl<S, T, G> DerefMut for Service<S, T, G> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.context
+
    }
+
}
+

+
#[derive(Debug, Clone)]
+
pub enum DisconnectReason {
+
    User,
+
    Error(PeerError),
+
}
+

+
impl DisconnectReason {
+
    fn is_transient(&self) -> bool {
+
        match self {
+
            Self::User => false,
+
            Self::Error(..) => false,
+
        }
+
    }
+
}
+

+
impl From<DisconnectReason> for nakamoto_net::DisconnectReason<DisconnectReason> {
+
    fn from(reason: DisconnectReason) -> Self {
+
        nakamoto_net::DisconnectReason::Protocol(reason)
+
    }
+
}
+

+
impl fmt::Display for DisconnectReason {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::User => write!(f, "user"),
+
            Self::Error(err) => write!(f, "error: {}", err),
+
        }
+
    }
+
}
+

+
impl<S, T, G> Iterator for Service<S, T, G> {
+
    type Item = Io;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.context.io.pop_front()
+
    }
+
}
+

+
/// Result of a project lookup.
+
#[derive(Debug)]
+
pub struct Lookup {
+
    /// Whether the project was found locally or not.
+
    pub local: Option<Project>,
+
    /// A list of remote peers on which the project is known to exist.
+
    pub remote: Vec<NodeId>,
+
}
+

+
/// Global service state used across peers.
+
#[derive(Debug)]
+
pub struct Context<S, T, G> {
+
    /// Service configuration.
+
    config: Config,
+
    /// Our cryptographic signer and key.
+
    signer: G,
+
    /// Tracks the location of projects.
+
    routing: Routing,
+
    /// Outgoing I/O queue.
+
    io: VecDeque<Io>,
+
    /// Clock. Tells the time.
+
    clock: RefClock,
+
    /// Project storage.
+
    storage: T,
+
    /// Peer address manager.
+
    addrmgr: AddressManager<S>,
+
    /// Source of entropy.
+
    rng: Rng,
+
}
+

+
impl<S, T, G> Context<S, T, G>
+
where
+
    T: storage::ReadStorage,
+
    G: crypto::Signer,
+
{
+
    pub(crate) fn node_id(&self) -> NodeId {
+
        *self.signer.public_key()
+
    }
+
}
+

+
impl<'r, S, T, G> Context<S, T, G>
+
where
+
    T: storage::WriteStorage<'r>,
+
    G: crypto::Signer,
+
{
+
    pub(crate) fn new(
+
        config: Config,
+
        clock: RefClock,
+
        storage: T,
+
        addrmgr: AddressManager<S>,
+
        signer: G,
+
        rng: Rng,
+
    ) -> Self {
+
        Self {
+
            config,
+
            signer,
+
            clock,
+
            routing: HashMap::with_hasher(rng.clone().into()),
+
            io: VecDeque::new(),
+
            storage,
+
            addrmgr,
+
            rng,
+
        }
+
    }
+

+
    fn node_announcement(&self) -> NodeAnnouncement {
+
        let timestamp = self.timestamp();
+
        let features = NodeFeatures::default();
+
        let alias = self.alias();
+
        let addresses = vec![]; // TODO
+

+
        NodeAnnouncement {
+
            features,
+
            timestamp,
+
            alias,
+
            addresses,
+
        }
+
    }
+

+
    fn inventory_announcement(&self) -> Result<InventoryAnnouncement, storage::Error> {
+
        let timestamp = self.timestamp();
+
        let inventory = self.storage.inventory()?;
+

+
        Ok(InventoryAnnouncement {
+
            inventory,
+
            timestamp,
+
        })
+
    }
+

+
    fn filter(&self) -> Filter {
+
        match &self.config.project_tracking {
+
            ProjectTracking::All { .. } => Filter::default(),
+
            ProjectTracking::Allowed(ids) => Filter::new(ids.iter()),
+
        }
+
    }
+

+
    fn handshake_messages(&self) -> [Message; 4] {
+
        let git = self.config.git_url.clone();
+
        [
+
            Message::init(
+
                self.node_id(),
+
                self.timestamp(),
+
                self.config.listen.clone(),
+
                git,
+
            ),
+
            Message::node(self.node_announcement(), &self.signer),
+
            Message::inventory(self.inventory_announcement().unwrap(), &self.signer),
+
            Message::subscribe(self.filter(), self.timestamp(), Timestamp::MAX),
+
        ]
+
    }
+

+
    fn alias(&self) -> [u8; 32] {
+
        let mut alias = [0u8; 32];
+

+
        alias[..9].copy_from_slice("anonymous".as_bytes());
+
        alias
+
    }
+

+
    /// Process a peer inventory announcement by updating our routing table.
+
    fn process_inventory(&mut self, inventory: &Inventory, from: NodeId, remote: &Url) {
+
        for proj_id in inventory {
+
            let inventory = self
+
                .routing
+
                .entry(proj_id.clone())
+
                .or_insert_with(|| HashSet::with_hasher(self.rng.clone().into()));
+

+
            // TODO: Fire an event on routing update.
+
            if inventory.insert(from) && self.config.is_tracking(proj_id) {
+
                self.fetch(proj_id, remote);
+
            }
+
        }
+
    }
+

+
    fn fetch(&mut self, proj_id: &Id, remote: &Url) -> Vec<RefUpdate> {
+
        let mut repo = self.storage.repository(proj_id).unwrap();
+
        let mut path = remote.path.clone();
+

+
        path.push(b'/');
+
        path.extend(proj_id.to_string().into_bytes());
+

+
        repo.fetch(&Url {
+
            path,
+
            ..remote.clone()
+
        })
+
        .unwrap()
+
    }
+

+
    /// Disconnect a peer.
+
    fn disconnect(&mut self, addr: net::SocketAddr, reason: DisconnectReason) {
+
        self.io.push_back(Io::Disconnect(addr, reason));
+
    }
+
}
+

+
impl<S, T, G> Context<S, T, G> {
+
    /// Get current local timestamp.
+
    pub(crate) fn timestamp(&self) -> Timestamp {
+
        self.clock.local_time().as_secs()
+
    }
+

+
    /// Connect to a peer.
+
    fn connect(&mut self, addr: net::SocketAddr) {
+
        // TODO: Make sure we don't try to connect more than once to the same address.
+
        self.io.push_back(Io::Connect(addr));
+
    }
+

+
    fn write_all(&mut self, remote: net::SocketAddr, msgs: impl IntoIterator<Item = Message>) {
+
        let envelopes = msgs
+
            .into_iter()
+
            .map(|msg| self.config.network.envelope(msg))
+
            .collect();
+
        self.io.push_back(Io::Write(remote, envelopes));
+
    }
+

+
    fn write(&mut self, remote: net::SocketAddr, msg: Message) {
+
        debug!("Write {:?} to {}", &msg, remote.ip());
+

+
        let envelope = self.config.network.envelope(msg);
+
        self.io.push_back(Io::Write(remote, vec![envelope]));
+
    }
+

+
    /// Broadcast a message to a list of peers.
+
    fn broadcast<'a>(&mut self, msg: Message, peers: impl IntoIterator<Item = &'a Peer>) {
+
        for peer in peers {
+
            self.write(peer.addr, msg.clone());
+
        }
+
    }
+

+
    /// Relay a message to interested peers.
+
    fn relay<'a>(&mut self, msg: Message, peers: impl IntoIterator<Item = &'a Peer>) {
+
        if let Message::RefsAnnouncement { message, .. } = &msg {
+
            let id = message.id.clone();
+
            let peers = peers.into_iter().filter(|p| {
+
                if let Some(subscribe) = &p.subscribe {
+
                    subscribe.filter.contains(&id)
+
                } else {
+
                    // If the peer did not send us a `subscribe` message, we don'the
+
                    // relay any messages to them.
+
                    false
+
                }
+
            });
+
            self.broadcast(msg, peers);
+
        } else {
+
            self.broadcast(msg, peers);
+
        }
+
    }
+
}
+

+
#[derive(Debug)]
+
/// Holds currently (or recently) connected peers.
+
pub struct Peers(AddressBook<IpAddr, Peer>);
+

+
impl Peers {
+
    pub fn new(rng: Rng) -> Self {
+
        Self(AddressBook::new(rng))
+
    }
+

+
    pub fn by_id(&self, id: &NodeId) -> Option<&Peer> {
+
        self.0.values().find(|p| {
+
            if let PeerState::Negotiated { id: _id, .. } = &p.state {
+
                _id == id
+
            } else {
+
                false
+
            }
+
        })
+
    }
+

+
    /// Iterator over fully negotiated peers.
+
    pub fn negotiated(&self) -> impl Iterator<Item = (&IpAddr, &Peer)> + Clone {
+
        self.0.iter().filter(move |(_, p)| p.is_negotiated())
+
    }
+
}
+

+
impl Deref for Peers {
+
    type Target = AddressBook<IpAddr, Peer>;
+

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

+
impl DerefMut for Peers {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.0
+
    }
+
}
added radicle-node/src/service/config.rs
@@ -0,0 +1,128 @@
+
use std::net;
+

+
use git_url::Url;
+

+
use crate::collections::HashSet;
+
use crate::git;
+
use crate::identity::{Id, PublicKey};
+
use crate::service::message::{Address, Envelope, Message};
+

+
/// Peer-to-peer network.
+
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+
pub enum Network {
+
    #[default]
+
    Main,
+
    Test,
+
}
+

+
impl Network {
+
    pub fn magic(&self) -> u32 {
+
        match self {
+
            Self::Main => 0x819b43d9,
+
            Self::Test => 0x717ebaf8,
+
        }
+
    }
+

+
    pub fn envelope(&self, msg: Message) -> Envelope {
+
        Envelope {
+
            magic: self.magic(),
+
            msg,
+
        }
+
    }
+
}
+

+
/// Project tracking policy.
+
#[derive(Debug, Clone)]
+
pub enum ProjectTracking {
+
    /// Track all projects we come across.
+
    All { blocked: HashSet<Id> },
+
    /// Track a static list of projects.
+
    Allowed(HashSet<Id>),
+
}
+

+
impl Default for ProjectTracking {
+
    fn default() -> Self {
+
        Self::All {
+
            blocked: HashSet::default(),
+
        }
+
    }
+
}
+

+
/// Project remote tracking policy.
+
#[derive(Debug, Default, Clone)]
+
pub enum RemoteTracking {
+
    /// Only track remotes of project delegates.
+
    #[default]
+
    DelegatesOnly,
+
    /// Track all remotes.
+
    All { blocked: HashSet<PublicKey> },
+
    /// Track a specific list of users as well as the project delegates.
+
    Allowed(HashSet<PublicKey>),
+
}
+

+
/// Service configuration.
+
#[derive(Debug, Clone)]
+
pub struct Config {
+
    /// Peers to connect to on startup.
+
    /// Connections to these peers will be maintained.
+
    pub connect: Vec<net::SocketAddr>,
+
    /// Peer-to-peer network.
+
    pub network: Network,
+
    /// Project tracking policy.
+
    pub project_tracking: ProjectTracking,
+
    /// Project remote tracking policy.
+
    pub remote_tracking: RemoteTracking,
+
    /// Whether or not our node should relay inventories.
+
    pub relay: bool,
+
    /// List of addresses to listen on for protocol connections.
+
    pub listen: Vec<Address>,
+
    /// Our Git URL for fetching projects.
+
    pub git_url: Url,
+
}
+

+
impl Default for Config {
+
    fn default() -> Self {
+
        Self {
+
            connect: Vec::default(),
+
            network: Network::default(),
+
            project_tracking: ProjectTracking::default(),
+
            remote_tracking: RemoteTracking::default(),
+
            relay: true,
+
            listen: vec![],
+
            git_url: Url {
+
                scheme: git::url::Scheme::File,
+
                path: "/dev/null".to_owned().into(),
+
                ..Url::default()
+
            },
+
        }
+
    }
+
}
+

+
impl Config {
+
    pub fn is_persistent(&self, addr: &net::SocketAddr) -> bool {
+
        self.connect.contains(addr)
+
    }
+

+
    pub fn is_tracking(&self, id: &Id) -> bool {
+
        match &self.project_tracking {
+
            ProjectTracking::All { blocked } => !blocked.contains(id),
+
            ProjectTracking::Allowed(ids) => ids.contains(id),
+
        }
+
    }
+

+
    /// Track a project. Returns whether the policy was updated.
+
    pub fn track(&mut self, id: Id) -> bool {
+
        match &mut self.project_tracking {
+
            ProjectTracking::All { .. } => false,
+
            ProjectTracking::Allowed(ids) => ids.insert(id),
+
        }
+
    }
+

+
    /// Untrack a project. Returns whether the policy was updated.
+
    pub fn untrack(&mut self, id: Id) -> bool {
+
        match &mut self.project_tracking {
+
            ProjectTracking::All { blocked } => blocked.insert(id),
+
            ProjectTracking::Allowed(ids) => ids.remove(&id),
+
        }
+
    }
+
}
added radicle-node/src/service/filter.rs
@@ -0,0 +1,56 @@
+
use std::ops::{Deref, DerefMut};
+

+
pub use bloomy::BloomFilter;
+

+
use crate::identity::Id;
+

+
/// Size in bytes of subscription bloom filter.
+
pub const FILTER_SIZE: usize = 1024 * 16;
+
/// Number of hashes used for bloom filter.
+
pub const FILTER_HASHES: usize = 7;
+

+
/// Subscription filter.
+
///
+
/// The [`Default`] instance has all bits set to `1`, ie. it will match
+
/// everything.
+
///
+
/// Nb. This filter doesn't currently support inserting public keys.
+
#[derive(Clone, PartialEq, Eq, Debug)]
+
pub struct Filter(BloomFilter<Id>);
+

+
impl Default for Filter {
+
    fn default() -> Self {
+
        Self(BloomFilter::from(vec![0xff; FILTER_SIZE]))
+
    }
+
}
+

+
impl Filter {
+
    pub fn new<'a>(ids: impl IntoIterator<Item = &'a Id>) -> Self {
+
        let mut bloom = BloomFilter::with_size(FILTER_SIZE);
+

+
        for id in ids.into_iter() {
+
            bloom.insert(id);
+
        }
+
        Self(bloom)
+
    }
+
}
+

+
impl Deref for Filter {
+
    type Target = BloomFilter<Id>;
+

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

+
impl DerefMut for Filter {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.0
+
    }
+
}
+

+
impl From<BloomFilter<Id>> for Filter {
+
    fn from(bloom: BloomFilter<Id>) -> Self {
+
        Self(bloom)
+
    }
+
}
added radicle-node/src/service/message.rs
@@ -0,0 +1,301 @@
+
use std::{fmt, io, net};
+

+
use crate::crypto;
+
use crate::git;
+
use crate::identity::Id;
+
use crate::service::filter::Filter;
+
use crate::service::{NodeId, Timestamp, PROTOCOL_VERSION};
+
use crate::storage::refs::Refs;
+
use crate::wire;
+

+
/// Message envelope. All messages sent over the network are wrapped in this type.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Envelope {
+
    /// Network magic constant. Used to differentiate networks.
+
    pub magic: u32,
+
    /// The message payload.
+
    pub msg: Message,
+
}
+

+
/// Advertized node feature. Signals what services the node supports.
+
pub type NodeFeatures = [u8; 32];
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
// TODO: We should check the length and charset when deserializing.
+
pub struct Hostname(String);
+

+
/// Peer public protocol address.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub enum Address {
+
    Ipv4 {
+
        ip: net::Ipv4Addr,
+
        port: u16,
+
    },
+
    Ipv6 {
+
        ip: net::Ipv6Addr,
+
        port: u16,
+
    },
+
    Hostname {
+
        host: Hostname,
+
        port: u16,
+
    },
+
    /// Tor V3 onion address.
+
    Onion {
+
        key: crypto::PublicKey,
+
        port: u16,
+
        checksum: u16,
+
        version: u8,
+
    },
+
}
+

+
impl From<net::SocketAddr> for Address {
+
    fn from(other: net::SocketAddr) -> Self {
+
        let port = other.port();
+

+
        match other.ip() {
+
            net::IpAddr::V4(ip) => Self::Ipv4 { ip, port },
+
            net::IpAddr::V6(ip) => Self::Ipv6 { ip, port },
+
        }
+
    }
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Subscribe {
+
    /// Subscribe to events matching this filter.
+
    pub filter: Filter,
+
    /// Request messages since this time.
+
    pub since: Timestamp,
+
    /// Request messages until this time.
+
    pub until: Timestamp,
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct NodeAnnouncement {
+
    /// Advertized features.
+
    pub features: NodeFeatures,
+
    /// Monotonic timestamp.
+
    pub timestamp: Timestamp,
+
    /// Non-unique alias. Must be valid UTF-8.
+
    pub alias: [u8; 32],
+
    /// Announced addresses.
+
    pub addresses: Vec<Address>,
+
}
+

+
impl NodeAnnouncement {
+
    /// Verify a signature on this message.
+
    pub fn verify(&self, signer: &NodeId, signature: &crypto::Signature) -> bool {
+
        let msg = wire::serialize(self);
+
        signer.verify(&msg, signature).is_ok()
+
    }
+
}
+

+
impl wire::Encode for NodeAnnouncement {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = 0;
+

+
        n += self.features.encode(writer)?;
+
        n += self.timestamp.encode(writer)?;
+
        n += self.alias.encode(writer)?;
+
        n += self.addresses.as_slice().encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for NodeAnnouncement {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let features = NodeFeatures::decode(reader)?;
+
        let timestamp = Timestamp::decode(reader)?;
+
        let alias = wire::Decode::decode(reader)?;
+
        let addresses = Vec::<Address>::decode(reader)?;
+

+
        Ok(Self {
+
            features,
+
            timestamp,
+
            alias,
+
            addresses,
+
        })
+
    }
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct RefsAnnouncement {
+
    /// Repository identifier.
+
    pub id: Id,
+
    /// Updated refs.
+
    pub refs: Refs,
+
}
+

+
impl RefsAnnouncement {
+
    /// Verify a signature on this message.
+
    pub fn verify(&self, signer: &NodeId, signature: &crypto::Signature) -> bool {
+
        let msg = wire::serialize(self);
+
        signer.verify(&msg, signature).is_ok()
+
    }
+

+
    /// Sign this announcement.
+
    pub fn sign<S: crypto::Signer>(&self, signer: S) -> crypto::Signature {
+
        let msg = wire::serialize(self);
+
        signer.sign(&msg)
+
    }
+
}
+

+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct InventoryAnnouncement {
+
    pub inventory: Vec<Id>,
+
    pub timestamp: Timestamp,
+
}
+

+
impl InventoryAnnouncement {
+
    /// Verify a signature on this message.
+
    pub fn verify(&self, signer: NodeId, signature: &crypto::Signature) -> bool {
+
        let msg = wire::serialize(self);
+
        signer.verify(&msg, signature).is_ok()
+
    }
+
}
+

+
/// Message payload.
+
/// These are the messages peers send to each other.
+
#[derive(Clone, PartialEq, Eq)]
+
pub enum Message {
+
    /// The first message sent to a peer after connection.
+
    Initialize {
+
        // TODO: This is currently untrusted.
+
        id: NodeId,
+
        timestamp: Timestamp,
+
        version: u32,
+
        addrs: Vec<Address>,
+
        git: git::Url,
+
    },
+

+
    /// Subscribe to gossip messages matching the filter and time range.
+
    /// timestamp.
+
    Subscribe(Subscribe),
+

+
    /// Node announcing its inventory to the network.
+
    /// This should be the whole inventory every time.
+
    InventoryAnnouncement {
+
        /// Node identifier.
+
        node: NodeId,
+
        /// Unsigned node inventory.
+
        message: InventoryAnnouncement,
+
        /// Signature over the announcement.
+
        signature: crypto::Signature,
+
    },
+

+
    /// Node announcing itself to the network.
+
    NodeAnnouncement {
+
        /// Node identifier.
+
        node: NodeId,
+
        /// Unsigned node announcement.
+
        message: NodeAnnouncement,
+
        /// Signature over the announcement, by the node being announced.
+
        signature: crypto::Signature,
+
    },
+

+
    /// Node announcing project refs being created or updated.
+
    RefsAnnouncement {
+
        /// Node identifier.
+
        node: NodeId,
+
        /// Unsigned refs announcement.
+
        message: RefsAnnouncement,
+
        /// Signature over the announcement, by the node that updated the refs.
+
        signature: crypto::Signature,
+
    },
+
}
+

+
impl Message {
+
    pub fn init(id: NodeId, timestamp: Timestamp, addrs: Vec<Address>, git: git::Url) -> Self {
+
        Self::Initialize {
+
            id,
+
            timestamp,
+
            version: PROTOCOL_VERSION,
+
            addrs,
+
            git,
+
        }
+
    }
+

+
    pub fn node<S: crypto::Signer>(message: NodeAnnouncement, signer: S) -> Self {
+
        let msg = wire::serialize(&message);
+
        let signature = signer.sign(&msg);
+
        let node = *signer.public_key();
+

+
        Self::NodeAnnouncement {
+
            node,
+
            signature,
+
            message,
+
        }
+
    }
+

+
    pub fn inventory<S: crypto::Signer>(message: InventoryAnnouncement, signer: S) -> Self {
+
        let msg = wire::serialize(&message);
+
        let signature = signer.sign(&msg);
+
        let node = *signer.public_key();
+

+
        Self::InventoryAnnouncement {
+
            node,
+
            signature,
+
            message,
+
        }
+
    }
+

+
    pub fn subscribe(filter: Filter, since: Timestamp, until: Timestamp) -> Self {
+
        Self::Subscribe(Subscribe {
+
            filter,
+
            since,
+
            until,
+
        })
+
    }
+
}
+

+
impl fmt::Debug for Message {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::Initialize { id, .. } => write!(f, "Initialize({})", id),
+
            Self::Subscribe(Subscribe { since, until, .. }) => {
+
                write!(f, "Subscribe({}..{})", since, until)
+
            }
+

+
            Self::NodeAnnouncement { node, .. } => write!(f, "NodeAnnouncement({})", node),
+
            Self::InventoryAnnouncement { node, message, .. } => {
+
                write!(
+
                    f,
+
                    "InventoryAnnouncement({}, [{}], {})",
+
                    node,
+
                    message
+
                        .inventory
+
                        .iter()
+
                        .map(|i| i.to_string())
+
                        .collect::<Vec<String>>()
+
                        .join(", "),
+
                    message.timestamp
+
                )
+
            }
+
            Self::RefsAnnouncement { node, message, .. } => {
+
                write!(
+
                    f,
+
                    "RefsAnnouncement({}, {}, {:?})",
+
                    node, message.id, message.refs
+
                )
+
            }
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    use crate::crypto::Signer;
+
    use crate::test::crypto::MockSigner;
+

+
    #[quickcheck]
+
    fn prop_refs_announcement_signing(id: Id, refs: Refs) {
+
        let signer = MockSigner::new(&mut fastrand::Rng::new());
+
        let message = RefsAnnouncement { id, refs };
+
        let signature = message.sign(&signer);
+

+
        assert!(message.verify(signer.public_key(), &signature));
+
    }
+
}
added radicle-node/src/service/peer.rs
@@ -0,0 +1,244 @@
+
use crate::service::message::*;
+
use crate::service::*;
+

+
#[derive(Debug, Default)]
+
#[allow(clippy::large_enum_variant)]
+
pub enum PeerState {
+
    /// Initial peer state. For outgoing peers this
+
    /// means we've attempted a connection. For incoming
+
    /// peers, this means they've successfully connected
+
    /// to us.
+
    #[default]
+
    Initial,
+
    /// State after successful handshake.
+
    Negotiated {
+
        /// The peer's unique identifier.
+
        id: NodeId,
+
        since: LocalTime,
+
        /// Addresses this peer is reachable on.
+
        addrs: Vec<Address>,
+
        git: Url,
+
    },
+
    /// When a peer is disconnected.
+
    Disconnected { since: LocalTime },
+
}
+

+
#[derive(thiserror::Error, Debug, Clone)]
+
pub enum PeerError {
+
    #[error("wrong network constant in message: {0}")]
+
    WrongMagic(u32),
+
    #[error("wrong protocol version in message: {0}")]
+
    WrongVersion(u32),
+
    #[error("invalid inventory timestamp: {0}")]
+
    InvalidTimestamp(u64),
+
    #[error("peer misbehaved")]
+
    Misbehavior,
+
}
+

+
#[derive(Debug)]
+
pub struct Peer {
+
    /// Peer address.
+
    pub addr: net::SocketAddr,
+
    /// Connection direction.
+
    pub link: Link,
+
    /// Whether we should attempt to re-connect
+
    /// to this peer upon disconnection.
+
    pub persistent: bool,
+
    /// Peer connection state.
+
    pub state: PeerState,
+
    /// Last known peer time.
+
    pub timestamp: Timestamp,
+
    /// Peer subscription.
+
    pub subscribe: Option<Subscribe>,
+

+
    /// Connection attempts. For persistent peers, Tracks
+
    /// how many times we've attempted to connect. We reset this to zero
+
    /// upon successful connection.
+
    attempts: usize,
+
}
+

+
impl Peer {
+
    pub fn new(addr: net::SocketAddr, link: Link, persistent: bool) -> Self {
+
        Self {
+
            addr,
+
            state: PeerState::default(),
+
            link,
+
            timestamp: Timestamp::default(),
+
            subscribe: None,
+
            persistent,
+
            attempts: 0,
+
        }
+
    }
+

+
    pub fn ip(&self) -> IpAddr {
+
        self.addr.ip()
+
    }
+

+
    pub fn is_negotiated(&self) -> bool {
+
        matches!(self.state, PeerState::Negotiated { .. })
+
    }
+

+
    pub fn attempts(&self) -> usize {
+
        self.attempts
+
    }
+

+
    pub fn attempted(&mut self) {
+
        self.attempts += 1;
+
    }
+

+
    pub fn connected(&mut self) {
+
        self.attempts = 0;
+
    }
+

+
    pub fn received<'r, S, T, G>(
+
        &mut self,
+
        envelope: Envelope,
+
        ctx: &mut Context<S, T, G>,
+
    ) -> Result<Option<Message>, PeerError>
+
    where
+
        T: storage::WriteStorage<'r>,
+
        G: crypto::Signer,
+
    {
+
        if envelope.magic != ctx.config.network.magic() {
+
            return Err(PeerError::WrongMagic(envelope.magic));
+
        }
+
        debug!("Received {:?} from {}", &envelope.msg, self.ip());
+

+
        match (&self.state, envelope.msg) {
+
            (
+
                PeerState::Initial,
+
                Message::Initialize {
+
                    id,
+
                    timestamp,
+
                    version,
+
                    addrs,
+
                    git,
+
                },
+
            ) => {
+
                let now = ctx.timestamp();
+

+
                if timestamp.abs_diff(now) > MAX_TIME_DELTA.as_secs() {
+
                    return Err(PeerError::InvalidTimestamp(timestamp));
+
                }
+
                if version != PROTOCOL_VERSION {
+
                    return Err(PeerError::WrongVersion(version));
+
                }
+
                // Nb. This is a very primitive handshake. Eventually we should have anyhow
+
                // extra "acknowledgment" message sent when the `Initialize` is well received.
+
                if self.link.is_inbound() {
+
                    ctx.write_all(self.addr, ctx.handshake_messages());
+
                }
+
                // Nb. we don't set the peer timestamp here, since it is going to be
+
                // set after the first message is received only. Setting it here would
+
                // mean that messages received right after the handshake could be ignored.
+
                self.state = PeerState::Negotiated {
+
                    id,
+
                    since: ctx.clock.local_time(),
+
                    addrs,
+
                    git,
+
                };
+
            }
+
            (PeerState::Initial, _) => {
+
                debug!(
+
                    "Disconnecting peer {} for sending us a message before handshake",
+
                    self.ip()
+
                );
+
                return Err(PeerError::Misbehavior);
+
            }
+
            (
+
                PeerState::Negotiated { git, .. },
+
                Message::InventoryAnnouncement {
+
                    node,
+
                    message,
+
                    signature,
+
                },
+
            ) => {
+
                let now = ctx.clock.local_time();
+
                let last = self.timestamp;
+

+
                // Don't allow messages from too far in the past or future.
+
                if message.timestamp.abs_diff(now.as_secs()) > MAX_TIME_DELTA.as_secs() {
+
                    return Err(PeerError::InvalidTimestamp(message.timestamp));
+
                }
+
                // Discard inventory messages we've already seen, otherwise update
+
                // out last seen time.
+
                if message.timestamp > last {
+
                    self.timestamp = message.timestamp;
+
                } else {
+
                    return Ok(None);
+
                }
+
                ctx.process_inventory(&message.inventory, node, git);
+

+
                if ctx.config.relay {
+
                    return Ok(Some(Message::InventoryAnnouncement {
+
                        node,
+
                        message,
+
                        signature,
+
                    }));
+
                }
+
            }
+
            // Process a peer inventory update announcement by (maybe) fetching.
+
            (
+
                PeerState::Negotiated { git, .. },
+
                Message::RefsAnnouncement {
+
                    node,
+
                    message,
+
                    signature,
+
                },
+
            ) => {
+
                if message.verify(&node, &signature) {
+
                    // TODO: Buffer/throttle fetches.
+
                    // TODO: Check that we're tracking this user as well.
+
                    if ctx.config.is_tracking(&message.id) {
+
                        // TODO: Check refs to see if we should try to fetch or not.
+
                        let updated_refs = ctx.fetch(&message.id, git);
+
                        let is_updated = !updated_refs.is_empty();
+

+
                        ctx.io.push_back(Io::Event(Event::RefsFetched {
+
                            from: git.clone(),
+
                            project: message.id.clone(),
+
                            updated: updated_refs,
+
                        }));
+

+
                        if is_updated {
+
                            return Ok(Some(Message::RefsAnnouncement {
+
                                node,
+
                                message,
+
                                signature,
+
                            }));
+
                        }
+
                    }
+
                } else {
+
                    return Err(PeerError::Misbehavior);
+
                }
+
            }
+
            (
+
                PeerState::Negotiated { .. },
+
                Message::NodeAnnouncement {
+
                    node,
+
                    message,
+
                    signature,
+
                },
+
            ) => {
+
                if !message.verify(&node, &signature) {
+
                    return Err(PeerError::Misbehavior);
+
                }
+
                log::warn!("Node announcement handling is not implemented");
+
            }
+
            (PeerState::Negotiated { .. }, Message::Subscribe(subscribe)) => {
+
                self.subscribe = Some(subscribe);
+
            }
+
            (PeerState::Negotiated { .. }, Message::Initialize { .. }) => {
+
                debug!(
+
                    "Disconnecting peer {} for sending us a redundant handshake message",
+
                    self.ip()
+
                );
+
                return Err(PeerError::Misbehavior);
+
            }
+
            (PeerState::Disconnected { .. }, msg) => {
+
                debug!("Ignoring {:?} from disconnected peer {}", msg, self.ip());
+
            }
+
        }
+
        Ok(None)
+
    }
+
}
added radicle-node/src/storage.rs
@@ -0,0 +1,306 @@
+
pub mod git;
+
pub mod refs;
+

+
use std::collections::hash_map;
+
use std::marker::PhantomData;
+
use std::ops::Deref;
+
use std::path::Path;
+
use std::{fmt, io};
+

+
use thiserror::Error;
+

+
pub use radicle_git_ext::Oid;
+

+
use crate::collections::HashMap;
+
use crate::crypto;
+
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
+
use crate::git::ext as git_ext;
+
use crate::git::Url;
+
use crate::git::{RefError, RefStr, RefString};
+
use crate::identity;
+
use crate::identity::{Id, IdError, Project};
+
use crate::storage::refs::Refs;
+

+
use self::refs::SignedRefs;
+

+
pub type BranchName = String;
+
pub type Inventory = Vec<Id>;
+

+
/// Storage error.
+
#[derive(Error, Debug)]
+
pub enum Error {
+
    #[error("invalid git reference")]
+
    InvalidRef,
+
    #[error("git reference error: {0}")]
+
    Ref(#[from] RefError),
+
    #[error(transparent)]
+
    Refs(#[from] refs::Error),
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("id: {0}")]
+
    Id(#[from] IdError),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("doc: {0}")]
+
    Doc(#[from] identity::doc::Error),
+
    #[error("invalid repository head")]
+
    InvalidHead,
+
}
+

+
/// Fetch error.
+
#[derive(Error, Debug)]
+
#[allow(clippy::large_enum_variant)]
+
pub enum FetchError {
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("verify: {0}")]
+
    Verify(#[from] git::VerifyError),
+
}
+

+
pub type RemoteId = PublicKey;
+

+
/// An update to a reference.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub enum RefUpdate {
+
    Updated { name: RefString, old: Oid, new: Oid },
+
    Created { name: RefString, oid: Oid },
+
    Deleted { name: RefString, oid: Oid },
+
    Skipped { name: RefString, oid: Oid },
+
}
+

+
impl RefUpdate {
+
    pub fn from(name: RefString, old: impl Into<Oid>, new: impl Into<Oid>) -> Self {
+
        let old = old.into();
+
        let new = new.into();
+

+
        if old.is_zero() {
+
            Self::Created { name, oid: new }
+
        } else if new.is_zero() {
+
            Self::Deleted { name, oid: old }
+
        } else if old != new {
+
            Self::Updated { name, old, new }
+
        } else {
+
            Self::Skipped { name, oid: old }
+
        }
+
    }
+
}
+

+
impl fmt::Display for RefUpdate {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::Updated { name, old, new } => {
+
                write!(f, "~ {:.7}..{:.7} {}", old, new, name)
+
            }
+
            Self::Created { name, oid } => {
+
                write!(f, "* 0000000..{:.7} {}", oid, name)
+
            }
+
            Self::Deleted { name, oid } => {
+
                write!(f, "- {:.7}..0000000 {}", oid, name)
+
            }
+
            Self::Skipped { name, oid } => {
+
                write!(f, "= {:.7}..{:.7} {}", oid, oid, name)
+
            }
+
        }
+
    }
+
}
+

+
/// Project remotes. Tracks the git state of a project.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
+

+
impl<V> FromIterator<(RemoteId, Remote<V>)> for Remotes<V> {
+
    fn from_iter<T: IntoIterator<Item = (RemoteId, Remote<V>)>>(iter: T) -> Self {
+
        Self(iter.into_iter().collect())
+
    }
+
}
+

+
impl<V> Deref for Remotes<V> {
+
    type Target = HashMap<RemoteId, Remote<V>>;
+

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

+
impl<V> Remotes<V> {
+
    pub fn new(remotes: HashMap<RemoteId, Remote<V>>) -> Self {
+
        Self(remotes)
+
    }
+
}
+

+
impl Remotes<Verified> {
+
    pub fn unverified(self) -> Remotes<Unverified> {
+
        Remotes(
+
            self.into_iter()
+
                .map(|(id, r)| (id, r.unverified()))
+
                .collect(),
+
        )
+
    }
+
}
+

+
impl<V> Default for Remotes<V> {
+
    fn default() -> Self {
+
        Self(HashMap::default())
+
    }
+
}
+

+
impl<V> IntoIterator for Remotes<V> {
+
    type Item = (RemoteId, Remote<V>);
+
    type IntoIter = hash_map::IntoIter<RemoteId, Remote<V>>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.0.into_iter()
+
    }
+
}
+

+
impl<V> From<Remotes<V>> for HashMap<RemoteId, Refs> {
+
    fn from(other: Remotes<V>) -> Self {
+
        let mut remotes = HashMap::with_hasher(fastrand::Rng::new().into());
+

+
        for (k, v) in other.into_iter() {
+
            remotes.insert(k, v.refs.into());
+
        }
+
        remotes
+
    }
+
}
+

+
/// A project remote.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct Remote<V> {
+
    /// ID of remote.
+
    pub id: PublicKey,
+
    /// Git references published under this remote, and their hashes.
+
    pub refs: SignedRefs<V>,
+
    /// Whether this remote is of a project delegate.
+
    pub delegate: bool,
+
    /// Whether the remote is verified or not, ie. whether its signed refs were checked.
+
    verified: PhantomData<V>,
+
}
+

+
impl<V> Remote<V> {
+
    pub fn new(id: PublicKey, refs: impl Into<SignedRefs<V>>) -> Self {
+
        Self {
+
            id,
+
            refs: refs.into(),
+
            delegate: false,
+
            verified: PhantomData,
+
        }
+
    }
+
}
+

+
impl Remote<Unverified> {
+
    pub fn verified(self) -> Result<Remote<Verified>, crypto::Error> {
+
        let refs = self.refs.verified(&self.id)?;
+

+
        Ok(Remote {
+
            id: self.id,
+
            refs,
+
            delegate: self.delegate,
+
            verified: PhantomData,
+
        })
+
    }
+
}
+

+
impl Remote<Verified> {
+
    pub fn unverified(self) -> Remote<Unverified> {
+
        Remote {
+
            id: self.id,
+
            refs: self.refs.unverified(),
+
            delegate: self.delegate,
+
            verified: PhantomData,
+
        }
+
    }
+
}
+

+
pub trait ReadStorage {
+
    fn url(&self) -> Url;
+
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error>;
+
    fn inventory(&self) -> Result<Inventory, Error>;
+
}
+

+
pub trait WriteStorage<'r>: ReadStorage {
+
    type Repository: WriteRepository<'r>;
+

+
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error>;
+
    fn sign_refs<G: Signer>(
+
        &self,
+
        repository: &Self::Repository,
+
        signer: G,
+
    ) -> Result<SignedRefs<Verified>, Error>;
+
}
+

+
pub trait ReadRepository<'r> {
+
    type Remotes: Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r;
+

+
    fn is_empty(&self) -> Result<bool, git2::Error>;
+
    fn path(&self) -> &Path;
+
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
+
    fn reference(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &RefStr,
+
    ) -> Result<Option<git2::Reference>, git2::Error>;
+
    fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error>;
+
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
+
    fn reference_oid(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &RefStr,
+
    ) -> Result<Option<Oid>, git2::Error>;
+
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
+
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
+
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error>;
+
    /// Return the project associated with this repository.
+
    fn project(&self) -> Result<Project, Error>;
+
}
+

+
pub trait WriteRepository<'r>: ReadRepository<'r> {
+
    fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, FetchError>;
+
    fn raw(&self) -> &git2::Repository;
+
}
+

+
impl<T, S> ReadStorage for T
+
where
+
    T: Deref<Target = S>,
+
    S: ReadStorage + 'static,
+
{
+
    fn url(&self) -> Url {
+
        self.deref().url()
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        self.deref().inventory()
+
    }
+

+
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
+
        self.deref().get(remote, proj)
+
    }
+
}
+

+
impl<'r, T, S> WriteStorage<'r> for T
+
where
+
    T: Deref<Target = S>,
+
    S: WriteStorage<'r> + 'static,
+
{
+
    type Repository = S::Repository;
+

+
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
+
        self.deref().repository(proj)
+
    }
+

+
    fn sign_refs<G: Signer>(
+
        &self,
+
        repository: &S::Repository,
+
        signer: G,
+
    ) -> Result<SignedRefs<Verified>, Error> {
+
        self.deref().sign_refs(repository, signer)
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    #[test]
+
    fn test_storage() {}
+
}
added radicle-node/src/storage/git.rs
@@ -0,0 +1,759 @@
+
use std::collections::{BTreeMap, HashMap};
+
use std::path::{Path, PathBuf};
+
use std::{fmt, fs, io};
+

+
use git_ref_format::refspec;
+
use once_cell::sync::Lazy;
+

+
pub use radicle_git_ext::Oid;
+

+
use crate::crypto::{Signer, Unverified, Verified};
+
use crate::git;
+
use crate::identity::{self, Doc};
+
use crate::identity::{Id, Project};
+
use crate::storage::refs;
+
use crate::storage::refs::{Refs, SignedRefs};
+
use crate::storage::{
+
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, WriteRepository,
+
    WriteStorage,
+
};
+

+
use super::{RefUpdate, RemoteId};
+

+
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
+
    Lazy::new(|| refspec::pattern!("refs/remotes/*"));
+
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
+
    Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
+

+
#[derive(Error, Debug)]
+
pub enum IdentityError {
+
    #[error("identity branches diverge from each other")]
+
    BranchesDiverge,
+
    #[error("identity branches are in an invalid state")]
+
    InvalidState,
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
    #[error("git: {0}")]
+
    GitExt(#[from] git::Error),
+
    #[error("refs: {0}")]
+
    Refs(#[from] refs::Error),
+
}
+

+
pub struct Storage {
+
    path: PathBuf,
+
}
+

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

+
impl ReadStorage for Storage {
+
    fn url(&self) -> git::Url {
+
        git::Url {
+
            scheme: git_url::Scheme::File,
+
            host: None,
+
            path: self.path.to_string_lossy().to_string().into(),
+
            ..git::Url::default()
+
        }
+
    }
+

+
    fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
+
        // TODO: Don't create a repo here if it doesn't exist?
+
        // Perhaps for checking we could have a `contains` method?
+
        let repo = self.repository(proj)?;
+

+
        if let Some(doc) = repo.identity_of(remote)? {
+
            let remotes = repo.remotes()?.collect::<Result<_, _>>()?;
+
            let path = repo.path().to_path_buf();
+

+
            // TODO: We should check that there is at least one remote, which is
+
            // the one of the local user, otherwise it means the project is in
+
            // an corrupted state.
+

+
            Ok(Some(Project {
+
                id: proj.clone(),
+
                doc,
+
                remotes,
+
                path,
+
            }))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        self.projects()
+
    }
+
}
+

+
impl<'r> WriteStorage<'r> for Storage {
+
    type Repository = Repository;
+

+
    fn repository(&self, proj: &Id) -> Result<Self::Repository, Error> {
+
        Repository::open(self.path.join(proj.to_string()))
+
    }
+

+
    fn sign_refs<G: Signer>(
+
        &self,
+
        repository: &Repository,
+
        signer: G,
+
    ) -> Result<SignedRefs<Verified>, Error> {
+
        let remote = signer.public_key();
+
        let refs = repository.references(remote)?;
+
        let signed = refs.signed(&signer)?;
+

+
        signed.save(remote, repository)?;
+

+
        Ok(signed)
+
    }
+
}
+

+
impl Storage {
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
+
        let path = path.as_ref().to_path_buf();
+

+
        match fs::create_dir_all(&path) {
+
            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
+
            Err(err) => return Err(err),
+
            Ok(()) => {}
+
        }
+

+
        Ok(Self { path })
+
    }
+

+
    pub fn path(&self) -> &Path {
+
        self.path.as_path()
+
    }
+

+
    pub fn projects(&self) -> Result<Vec<Id>, Error> {
+
        let mut projects = Vec::new();
+

+
        for result in fs::read_dir(&self.path)? {
+
            let path = result?;
+
            let id = Id::try_from(path.file_name())?;
+

+
            projects.push(id);
+
        }
+
        Ok(projects)
+
    }
+

+
    pub fn inspect(&self) -> Result<(), Error> {
+
        for proj in self.projects()? {
+
            let repo = self.repository(&proj)?;
+

+
            for r in repo.raw().references()? {
+
                let r = r?;
+
                let name = r.name().ok_or(Error::InvalidRef)?;
+
                let oid = r.target().ok_or(Error::InvalidRef)?;
+

+
                println!("{} {} {}", proj, oid, name);
+
            }
+
        }
+
        Ok(())
+
    }
+
}
+

+
pub struct Repository {
+
    pub(crate) backend: git2::Repository,
+
    // TODO: Add project id here so we can refer to it
+
    // in a bunch of places. We could write it to the
+
    // git config for later.
+
}
+

+
#[derive(Debug, Error)]
+
pub enum VerifyError {
+
    #[error("invalid remote `{0}`")]
+
    InvalidRemote(RemoteId),
+
    #[error("invalid target `{2}` for reference `{1}` of remote `{0}`")]
+
    InvalidRefTarget(RemoteId, git::RefString, git2::Oid),
+
    #[error("invalid reference")]
+
    InvalidRef,
+
    #[error("ref error: {0}")]
+
    Ref(#[from] git::RefError),
+
    #[error("refs error: {0}")]
+
    Refs(#[from] refs::Error),
+
    #[error("unknown reference `{1}` in remote `{0}`")]
+
    UnknownRef(RemoteId, git::RefString),
+
    #[error("missing reference `{1}` in remote `{0}`")]
+
    MissingRef(RemoteId, git::RefString),
+
    #[error("git: {0}")]
+
    Git(#[from] git2::Error),
+
}
+

+
impl Repository {
+
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
+
        let backend = match git2::Repository::open_bare(path.as_ref()) {
+
            Err(e) if git::ext::is_not_found_err(&e) => {
+
                let backend = git2::Repository::init_opts(
+
                    path,
+
                    git2::RepositoryInitOptions::new()
+
                        .bare(true)
+
                        .no_reinit(true)
+
                        .external_template(false),
+
                )?;
+
                let mut config = backend.config()?;
+

+
                // TODO: Get ahold of user name and/or key.
+
                config.set_str("user.name", "radicle")?;
+
                config.set_str("user.email", "radicle@localhost")?;
+

+
                Ok(backend)
+
            }
+
            Ok(repo) => Ok(repo),
+
            Err(e) => Err(e),
+
        }?;
+

+
        Ok(Self { backend })
+
    }
+

+
    pub fn head(&self) -> Result<git2::Commit, git2::Error> {
+
        // TODO: Find longest history, get document and get head.
+
        // Perhaps we should even set a local `HEAD` or at least `refs/heads/master`
+
        todo!();
+
    }
+

+
    pub fn verify(&self) -> Result<(), VerifyError> {
+
        let mut remotes: HashMap<RemoteId, Refs> = self
+
            .remotes()?
+
            .map(|remote| {
+
                let (id, remote) = remote?;
+
                Ok((id, remote.refs.into()))
+
            })
+
            .collect::<Result<_, VerifyError>>()?;
+

+
        for r in self.backend.references()? {
+
            let r = r?;
+
            let name = r.name().ok_or(VerifyError::InvalidRef)?;
+
            let oid = r.target().ok_or(VerifyError::InvalidRef)?;
+
            let (remote_id, refname) = git::parse_ref::<RemoteId>(name)?;
+

+
            if refname == *refs::SIGNATURE_REF {
+
                continue;
+
            }
+
            let remote = remotes
+
                .get_mut(&remote_id)
+
                .ok_or(VerifyError::InvalidRemote(remote_id))?;
+
            let signed_oid = remote
+
                .remove(&refname)
+
                .ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
+

+
            if git::Oid::from(oid) != signed_oid {
+
                return Err(VerifyError::InvalidRefTarget(remote_id, refname, oid));
+
            }
+
        }
+

+
        // The refs that are left in the map, are ones that were signed, but are not
+
        // in the repository.
+
        for (id, refs) in remotes.into_iter() {
+
            if let Some((name, _)) = refs.into_iter().next() {
+
                return Err(VerifyError::MissingRef(id, name));
+
            }
+
        }
+

+
        Ok(())
+
    }
+

+
    pub fn inspect(&self) -> Result<(), Error> {
+
        for r in self.backend.references()? {
+
            let r = r?;
+
            let name = r.name().ok_or(Error::InvalidRef)?;
+
            let oid = r.target().ok_or(Error::InvalidRef)?;
+

+
            println!("{} {}", oid, name);
+
        }
+
        Ok(())
+
    }
+

+
    pub fn identity_of(
+
        &self,
+
        remote: &RemoteId,
+
    ) -> Result<Option<identity::Doc<Verified>>, refs::Error> {
+
        if let Some((doc, _)) = identity::Doc::load(remote, self)? {
+
            Ok(Some(doc.verified().unwrap()))
+
        } else {
+
            Ok(None)
+
        }
+
    }
+

+
    /// Return the canonical identity [`git::Oid`] and document.
+
    pub fn identity(&self) -> Result<(git::Oid, identity::Doc<Unverified>), IdentityError> {
+
        let mut heads = Vec::new();
+
        for remote in self.remote_ids()? {
+
            let remote = remote?;
+
            let oid = Doc::<Unverified>::head(&remote, self)?.unwrap();
+

+
            heads.push(oid.into());
+
        }
+
        // Keep track of the longest identity branch.
+
        let mut longest = heads.pop().ok_or(IdentityError::InvalidState)?;
+

+
        for head in &heads {
+
            let base = self.raw().merge_base(*head, longest)?;
+

+
            if base == longest {
+
                // `head` is a successor of `longest`. Update `longest`.
+
                //
+
                //   o head
+
                //   |
+
                //   o longest (base)
+
                //   |
+
                //
+
                longest = *head;
+
            } else if base == *head || *head == longest {
+
                // `head` is an ancestor of `longest`, or equal to it. Do nothing.
+
                //
+
                //   o longest             o longest, head (base)
+
                //   |                     |
+
                //   o head (base)   OR    o
+
                //   |                     |
+
                //
+
            } else {
+
                // The merge base between `head` and `longest` (`base`)
+
                // is neither `head` nor `longest`. Therefore, the branches have
+
                // diverged.
+
                //
+
                //    longest   head
+
                //           \ /
+
                //            o (base)
+
                //            |
+
                //
+
                return Err(IdentityError::BranchesDiverge);
+
            }
+
        }
+

+
        Doc::load_at(longest.into(), self)?
+
            .ok_or(refs::Error::NotFound)
+
            .map(|(doc, _)| (longest.into(), doc))
+
            .map_err(IdentityError::from)
+
    }
+

+
    pub fn remote_ids(
+
        &self,
+
    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git2::Error> {
+
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
+
            |reference| -> Result<RemoteId, refs::Error> {
+
                let r = reference?;
+
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
+
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
+

+
                Ok(id)
+
            },
+
        );
+
        Ok(iter)
+
    }
+
}
+

+
impl<'r> ReadRepository<'r> for Repository {
+
    type Remotes = Box<dyn Iterator<Item = Result<(RemoteId, Remote<Verified>), refs::Error>> + 'r>;
+

+
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
        let some = self.remotes()?.next().is_some();
+
        Ok(!some)
+
    }
+

+
    fn path(&self) -> &Path {
+
        self.backend.path()
+
    }
+

+
    fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git::Error> {
+
        git::ext::Blob::At {
+
            object: oid.into(),
+
            path,
+
        }
+
        .get(&self.backend)
+
    }
+

+
    fn reference(
+
        &self,
+
        remote: &RemoteId,
+
        name: &git::RefStr,
+
    ) -> Result<Option<git2::Reference>, git2::Error> {
+
        let name = name.strip_prefix(git::refname!("refs")).unwrap_or(name);
+
        let name = format!("refs/remotes/{remote}/{name}");
+
        self.backend.find_reference(&name).map(Some).or_else(|e| {
+
            if git::ext::is_not_found_err(&e) {
+
                Ok(None)
+
            } else {
+
                Err(e)
+
            }
+
        })
+
    }
+

+
    fn commit(&self, oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
+
        self.backend.find_commit(oid.into()).map(Some).or_else(|e| {
+
            if git::ext::is_not_found_err(&e) {
+
                Ok(None)
+
            } else {
+
                Err(e)
+
            }
+
        })
+
    }
+

+
    fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error> {
+
        let mut revwalk = self.backend.revwalk()?;
+
        revwalk.push(head.into())?;
+

+
        Ok(revwalk)
+
    }
+

+
    fn reference_oid(
+
        &self,
+
        remote: &RemoteId,
+
        reference: &git::RefStr,
+
    ) -> Result<Option<Oid>, git2::Error> {
+
        let reference = self.reference(remote, reference)?;
+
        Ok(reference.and_then(|r| r.target().map(|o| o.into())))
+
    }
+

+
    fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
+
        let refs = SignedRefs::load(remote, self)?;
+
        Ok(Remote::new(*remote, refs))
+
    }
+

+
    fn references(&self, remote: &RemoteId) -> Result<Refs, Error> {
+
        // TODO: Only return known refs, eg. heads/ rad/ tags/ etc..
+
        let entries = self
+
            .backend
+
            .references_glob(format!("refs/remotes/{remote}/*").as_str())?;
+
        let mut refs = BTreeMap::new();
+

+
        for e in entries {
+
            let e = e?;
+
            let name = e.name().ok_or(Error::InvalidRef)?;
+
            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
+
            let oid = e.target().ok_or(Error::InvalidRef)?;
+

+
            refs.insert(refname, oid.into());
+
        }
+
        Ok(refs.into())
+
    }
+

+
    fn remotes(&'r self) -> Result<Self::Remotes, git2::Error> {
+
        let iter = self.backend.references_glob(SIGNATURES_GLOB.as_str())?.map(
+
            |reference| -> Result<(RemoteId, Remote<Verified>), refs::Error> {
+
                let r = reference?;
+
                let name = r.name().ok_or(refs::Error::InvalidRef)?;
+
                let (id, _) = git::parse_ref::<RemoteId>(name)?;
+
                let remote = self.remote(&id)?;
+

+
                Ok((id, remote))
+
            },
+
        );
+

+
        Ok(Box::new(iter))
+
    }
+

+
    fn project(&self) -> Result<Project, Error> {
+
        todo!()
+
    }
+
}
+

+
impl<'r> WriteRepository<'r> for Repository {
+
    /// Fetch all remotes of a project from the given URL.
+
    fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, FetchError> {
+
        // TODO: Have function to fetch specific remotes.
+
        //
+
        // Repository layout should look like this:
+
        //
+
        //   /refs/remotes/<remote>
+
        //         /heads
+
        //           /master
+
        //         /tags
+
        //         ...
+
        //
+
        let url = url.to_string();
+
        let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
+
        let mut updates = Vec::new();
+
        let mut callbacks = git2::RemoteCallbacks::new();
+
        let tempdir = tempfile::tempdir()?;
+
        // TODO: Comment
+
        let staging = {
+
            let mut builder = git2::build::RepoBuilder::new();
+
            let path = tempdir.path().join("git");
+
            let staging_repo = builder
+
                .bare(true)
+
                // TODO: Comment
+
                // TODO: Due to this, I think we'll have to run GC when there is a failure.
+
                .clone_local(git2::build::CloneLocal::Local)
+
                .clone(
+
                    &git::Url {
+
                        scheme: git::url::Scheme::File,
+
                        path: self.backend.path().to_string_lossy().to_string().into(),
+
                        ..git::Url::default()
+
                    }
+
                    .to_string(),
+
                    &path,
+
                )?;
+

+
            // In case we fetch an invalid update, we want to make sure nothing is deleted.
+
            let mut opts = git2::FetchOptions::default();
+
            opts.prune(git2::FetchPrune::Off);
+

+
            staging_repo
+
                .remote_anonymous(&url)?
+
                .fetch(refs, Some(&mut opts), None)?;
+
            // TODO: Comment
+
            Repository::from(staging_repo).verify()?;
+

+
            path
+
        };
+

+
        callbacks.update_tips(|name, old, new| {
+
            if let Ok(name) = git::RefString::try_from(name) {
+
                updates.push(RefUpdate::from(name, old, new));
+
            } else {
+
                log::warn!("Invalid ref `{}` detected; aborting fetch", name);
+
                return false;
+
            }
+
            // Returning `true` ensures the process is not aborted.
+
            true
+
        });
+

+
        {
+
            let mut remote = self.backend.remote_anonymous(
+
                &git::Url {
+
                    scheme: git::url::Scheme::File,
+
                    path: staging.to_string_lossy().to_string().into(),
+
                    ..git::Url::default()
+
                }
+
                .to_string(),
+
            )?;
+
            let mut opts = git2::FetchOptions::default();
+
            opts.remote_callbacks(callbacks);
+

+
            // TODO: Make sure we verify before pruning, as pruning may get us into
+
            // a state we can't roll back.
+
            opts.prune(git2::FetchPrune::On);
+
            remote.fetch(refs, Some(&mut opts), None)?;
+
        }
+

+
        Ok(updates)
+
    }
+

+
    fn raw(&self) -> &git2::Repository {
+
        &self.backend
+
    }
+
}
+

+
impl From<git2::Repository> for Repository {
+
    fn from(backend: git2::Repository) -> Self {
+
        Self { backend }
+
    }
+
}
+

+
pub mod trailers {
+
    use std::str::FromStr;
+

+
    use super::*;
+
    use crate::crypto::{PublicKey, PublicKeyError};
+
    use crate::crypto::{Signature, SignatureError};
+

+
    pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
+

+
    #[derive(Error, Debug)]
+
    pub enum Error {
+
        #[error("invalid format for signature trailer")]
+
        SignatureTrailerFormat,
+
        #[error("invalid public key in signature trailer")]
+
        PublicKey(#[from] PublicKeyError),
+
        #[error("invalid signature in trailer")]
+
        Signature(#[from] SignatureError),
+
    }
+

+
    pub fn parse_signatures(msg: &str) -> Result<Vec<(PublicKey, Signature)>, Error> {
+
        let trailers =
+
            git2::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
+
        let mut signatures = Vec::with_capacity(trailers.len());
+

+
        for (key, val) in trailers.iter() {
+
            if key == SIGNATURE_TRAILER {
+
                if let Some((pk, sig)) = val.split_once(' ') {
+
                    let pk = PublicKey::from_str(pk)?;
+
                    let sig = Signature::from_str(sig)?;
+

+
                    signatures.push((pk, sig));
+
                } else {
+
                    return Err(Error::SignatureTrailerFormat);
+
                }
+
            }
+
        }
+
        Ok(signatures)
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use crate::assert_matches;
+
    use crate::git;
+
    use crate::storage::refs::SIGNATURE_REF;
+
    use crate::storage::{ReadStorage, RefUpdate, WriteRepository};
+
    use crate::test::arbitrary;
+
    use crate::test::crypto::MockSigner;
+
    use crate::test::fixtures;
+

+
    #[test]
+
    fn test_remote_refs() {
+
        let dir = tempfile::tempdir().unwrap();
+
        let storage = fixtures::storage(dir.path());
+
        let inv = storage.inventory().unwrap();
+
        let proj = inv.first().unwrap();
+
        let mut refs = git::remote_refs(&git::Url {
+
            host: Some(dir.path().to_string_lossy().to_string()),
+
            scheme: git_url::Scheme::File,
+
            path: format!("/{}", proj).into(),
+
            ..git::Url::default()
+
        })
+
        .unwrap();
+

+
        let project = storage.repository(proj).unwrap();
+
        let remotes = project.remotes().unwrap();
+

+
        // Strip the remote refs of sigrefs so we can compare them.
+
        for remote in refs.values_mut() {
+
            remote.remove(&*SIGNATURE_REF).unwrap();
+
        }
+

+
        let remotes = remotes
+
            .map(|remote| remote.map(|(id, r): (RemoteId, Remote<Verified>)| (id, r.refs.into())))
+
            .collect::<Result<_, _>>()
+
            .unwrap();
+

+
        assert_eq!(refs, remotes);
+
    }
+

+
    #[test]
+
    fn test_fetch() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let alice = fixtures::storage(tmp.path().join("alice"));
+
        let bob = Storage::open(tmp.path().join("bob")).unwrap();
+
        let inventory = alice.inventory().unwrap();
+
        let proj = inventory.first().unwrap();
+
        let repo = alice.repository(proj).unwrap();
+
        let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
+
        let refname = git::refname!("heads/master");
+

+
        // Have Bob fetch Alice's refs.
+
        let updates = bob
+
            .repository(proj)
+
            .unwrap()
+
            .fetch(&git::Url {
+
                scheme: git_url::Scheme::File,
+
                path: alice
+
                    .path()
+
                    .join(proj.to_string())
+
                    .to_string_lossy()
+
                    .into_owned()
+
                    .into(),
+
                ..git::Url::default()
+
            })
+
            .unwrap();
+

+
        // Four refs are created for each remote.
+
        assert_eq!(updates.len(), remotes.len() * 4);
+

+
        for update in updates {
+
            assert_matches!(
+
                update,
+
                RefUpdate::Created { name, .. } if name.starts_with("refs/remotes")
+
            );
+
        }
+

+
        for remote in remotes {
+
            let (id, _) = remote.unwrap();
+
            let alice_repo = alice.repository(proj).unwrap();
+
            let alice_oid = alice_repo.reference(&id, &refname).unwrap().unwrap();
+

+
            let bob_repo = bob.repository(proj).unwrap();
+
            let bob_oid = bob_repo.reference(&id, &refname).unwrap().unwrap();
+

+
            assert_eq!(alice_oid.target(), bob_oid.target());
+
        }
+
    }
+

+
    #[test]
+
    fn test_fetch_update() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let alice = Storage::open(tmp.path().join("alice/storage")).unwrap();
+
        let bob = Storage::open(tmp.path().join("bob/storage")).unwrap();
+

+
        let alice_signer = MockSigner::new(&mut fastrand::Rng::new());
+
        let alice_id = alice_signer.public_key();
+
        let (proj_id, _, proj_repo, alice_head) =
+
            fixtures::project(tmp.path().join("alice/project"), &alice, &alice_signer).unwrap();
+

+
        let refname = git::refname!("refs/heads/master");
+
        let alice_url = git::Url {
+
            scheme: git_url::Scheme::File,
+
            path: alice
+
                .path()
+
                .join(proj_id.to_string())
+
                .to_string_lossy()
+
                .into_owned()
+
                .into(),
+
            ..git::Url::default()
+
        };
+

+
        // Have Bob fetch Alice's refs.
+
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
+
        // Three refs are created: the branch, the signature and the id.
+
        assert_eq!(updates.len(), 3);
+

+
        let alice_proj_storage = alice.repository(&proj_id).unwrap();
+
        let alice_head = proj_repo.find_commit(alice_head).unwrap();
+
        let alice_head = git::commit(&proj_repo, &alice_head, &refname, "Making changes", "Alice")
+
            .unwrap()
+
            .id();
+
        git::push(&proj_repo).unwrap();
+
        alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
+

+
        // Have Bob fetch Alice's new commit.
+
        let updates = bob.repository(&proj_id).unwrap().fetch(&alice_url).unwrap();
+
        // The branch and signature refs are updated.
+
        assert_matches!(
+
            updates.as_slice(),
+
            &[RefUpdate::Updated { .. }, RefUpdate::Updated { .. }]
+
        );
+

+
        // Bob's storage is updated.
+
        let bob_repo = bob.repository(&proj_id).unwrap();
+
        let bob_master = bob_repo.reference(alice_id, &refname).unwrap().unwrap();
+

+
        assert_eq!(bob_master.target().unwrap(), alice_head);
+
    }
+

+
    #[test]
+
    fn test_sign_refs() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let mut rng = fastrand::Rng::new();
+
        let signer = MockSigner::new(&mut rng);
+
        let storage = Storage::open(tmp.path()).unwrap();
+
        let proj_id = arbitrary::gen::<Id>(1);
+
        let alice = *signer.public_key();
+
        let project = storage.repository(&proj_id).unwrap();
+
        let backend = &project.backend;
+
        let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
+
        let head = git::initial_commit(backend, &sig).unwrap();
+

+
        git::commit(
+
            backend,
+
            &head,
+
            &git::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
+
            "Second commit",
+
            &alice.to_string(),
+
        )
+
        .unwrap();
+

+
        let signed = storage.sign_refs(&project, &signer).unwrap();
+
        let remote = project.remote(&alice).unwrap();
+
        let mut unsigned = project.references(&alice).unwrap();
+

+
        // The signed refs doesn't contain the signature ref itself.
+
        unsigned.remove(&*SIGNATURE_REF).unwrap();
+

+
        assert_eq!(remote.refs, signed);
+
        assert_eq!(*remote.refs, unsigned);
+
    }
+
}
added radicle-node/src/storage/refs.rs
@@ -0,0 +1,378 @@
+
use std::collections::BTreeMap;
+
use std::fmt::Debug;
+
use std::io;
+
use std::io::{BufRead, BufReader};
+
use std::marker::PhantomData;
+
use std::ops::{Deref, DerefMut};
+
use std::path::Path;
+
use std::str::FromStr;
+

+
use once_cell::sync::Lazy;
+
use radicle_git_ext as git_ext;
+
use thiserror::Error;
+

+
use crate::crypto;
+
use crate::crypto::{PublicKey, Signature, Signer, Unverified, Verified};
+
use crate::git;
+
use crate::git::Oid;
+
use crate::storage;
+
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
+
use crate::wire;
+

+
pub static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("radicle/signature"));
+
pub const REFS_BLOB_PATH: &str = "refs";
+
pub const SIGNATURE_BLOB_PATH: &str = "signature";
+

+
#[derive(Debug)]
+
pub enum Updated {
+
    /// The computed [`Refs`] were stored as a new commit.
+
    Updated { oid: Oid },
+
    /// The stored [`Refs`] were the same as the computed ones, so no new commit
+
    /// was created.
+
    Unchanged { oid: Oid },
+
}
+

+
#[derive(Debug, Error)]
+
pub enum Error {
+
    #[error("invalid signature: {0}")]
+
    InvalidSignature(#[from] crypto::Error),
+
    #[error("canonical refs: {0}")]
+
    Canonical(#[from] canonical::Error),
+
    #[error("invalid reference")]
+
    InvalidRef,
+
    #[error("invalid reference: {0}")]
+
    Ref(#[from] git::RefError),
+
    #[error(transparent)]
+
    Git(#[from] git2::Error),
+
    #[error(transparent)]
+
    GitExt(#[from] git_ext::Error),
+
    #[error("refs were not found")]
+
    NotFound,
+
}
+

+
/// The published state of a local repository.
+
#[derive(Default, Clone, Debug, PartialEq, Eq)]
+
pub struct Refs(BTreeMap<git::RefString, Oid>);
+

+
impl Refs {
+
    /// Verify the given signature on these refs, and return [`SignedRefs`] on success.
+
    pub fn verified(
+
        self,
+
        signer: &PublicKey,
+
        signature: Signature,
+
    ) -> Result<SignedRefs<Verified>, Error> {
+
        let refs = self;
+
        let msg = refs.canonical();
+

+
        match signer.verify(&msg, &signature) {
+
            Ok(()) => Ok(SignedRefs {
+
                refs,
+
                signature,
+
                _verified: PhantomData,
+
            }),
+
            Err(e) => Err(e.into()),
+
        }
+
    }
+

+
    /// Sign these refs with the given signer and return [`SignedRefs`].
+
    pub fn signed<S>(self, signer: S) -> Result<SignedRefs<Verified>, Error>
+
    where
+
        S: Signer,
+
    {
+
        let refs = self;
+
        let msg = refs.canonical();
+
        let signature = signer.sign(&msg);
+

+
        Ok(SignedRefs {
+
            refs,
+
            signature,
+
            _verified: PhantomData,
+
        })
+
    }
+

+
    /// Create refs from a canonical representation.
+
    pub fn from_canonical(bytes: &[u8]) -> Result<Self, canonical::Error> {
+
        let reader = BufReader::new(bytes);
+
        let mut refs = BTreeMap::new();
+

+
        for line in reader.lines() {
+
            let line = line?;
+
            let (oid, name) = line
+
                .split_once(' ')
+
                .ok_or(canonical::Error::InvalidFormat)?;
+

+
            let name = git::RefString::try_from(name)?;
+
            let oid = Oid::from_str(oid)?;
+

+
            if oid.is_zero() {
+
                continue;
+
            }
+
            refs.insert(name, oid);
+
        }
+
        Ok(Self(refs))
+
    }
+

+
    pub fn canonical(&self) -> Vec<u8> {
+
        let mut buf = String::new();
+
        let refs = self
+
            .iter()
+
            .filter(|(name, oid)| *name != &*SIGNATURE_REF && !oid.is_zero());
+

+
        for (name, oid) in refs {
+
            buf.push_str(&oid.to_string());
+
            buf.push(' ');
+
            buf.push_str(name);
+
            buf.push('\n');
+
        }
+
        buf.into_bytes()
+
    }
+
}
+

+
impl IntoIterator for Refs {
+
    type Item = (git::RefString, Oid);
+
    type IntoIter = std::collections::btree_map::IntoIter<git::RefString, Oid>;
+

+
    fn into_iter(self) -> Self::IntoIter {
+
        self.0.into_iter()
+
    }
+
}
+

+
impl From<Refs> for BTreeMap<git::RefString, Oid> {
+
    fn from(refs: Refs) -> Self {
+
        refs.0
+
    }
+
}
+

+
impl<V> From<SignedRefs<V>> for Refs {
+
    fn from(signed: SignedRefs<V>) -> Self {
+
        signed.refs
+
    }
+
}
+

+
impl From<BTreeMap<git::RefString, Oid>> for Refs {
+
    fn from(refs: BTreeMap<git::RefString, Oid>) -> Self {
+
        Self(refs)
+
    }
+
}
+

+
impl Deref for Refs {
+
    type Target = BTreeMap<git::RefString, Oid>;
+

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

+
impl DerefMut for Refs {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.0
+
    }
+
}
+

+
/// Combination of [`Refs`] and a [`Signature`]. The signature is a cryptographic
+
/// signature over the refs. This allows us to easily verify if a set of refs
+
/// came from a particular key.
+
///
+
/// The type parameter keeps track of whether the signature was [`Verified`] or
+
/// [`Unverified`].
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub struct SignedRefs<V> {
+
    refs: Refs,
+
    signature: Signature,
+
    _verified: PhantomData<V>,
+
}
+

+
impl SignedRefs<Unverified> {
+
    pub fn new(refs: Refs, signature: Signature) -> Self {
+
        Self {
+
            refs,
+
            signature,
+
            _verified: PhantomData,
+
        }
+
    }
+

+
    pub fn verified(self, signer: &PublicKey) -> Result<SignedRefs<Verified>, crypto::Error> {
+
        match self.verify(signer) {
+
            Ok(()) => Ok(SignedRefs {
+
                refs: self.refs,
+
                signature: self.signature,
+
                _verified: PhantomData,
+
            }),
+
            Err(e) => Err(e),
+
        }
+
    }
+

+
    pub fn verify(&self, signer: &PublicKey) -> Result<(), crypto::Error> {
+
        let canonical = self.refs.canonical();
+

+
        match signer.verify(&canonical, &self.signature) {
+
            Ok(()) => Ok(()),
+
            Err(e) => Err(e),
+
        }
+
    }
+
}
+

+
impl SignedRefs<Verified> {
+
    pub fn load<'r, S>(remote: &RemoteId, repo: &S) -> Result<Self, Error>
+
    where
+
        S: ReadRepository<'r>,
+
    {
+
        if let Some(oid) = repo.reference_oid(remote, &SIGNATURE_REF)? {
+
            Self::load_at(oid, remote, repo)
+
        } else {
+
            Err(Error::NotFound)
+
        }
+
    }
+

+
    pub fn load_at<'r, S>(oid: Oid, remote: &RemoteId, repo: &S) -> Result<Self, Error>
+
    where
+
        S: storage::ReadRepository<'r>,
+
    {
+
        let refs = repo.blob_at(oid, Path::new(REFS_BLOB_PATH))?;
+
        let signature = repo.blob_at(oid, Path::new(SIGNATURE_BLOB_PATH))?;
+
        let signature: crypto::Signature = signature.content().try_into()?;
+

+
        match remote.verify(refs.content(), &signature) {
+
            Ok(()) => {
+
                let refs = Refs::from_canonical(refs.content())?;
+

+
                Ok(Self {
+
                    refs,
+
                    signature,
+
                    _verified: PhantomData,
+
                })
+
            }
+
            Err(e) => Err(e.into()),
+
        }
+
    }
+

+
    /// Save the signed refs to disk.
+
    /// This creates a new commit on the signed refs branch, and updates the branch pointer.
+
    pub fn save<'r, S: WriteRepository<'r>>(
+
        &self,
+
        // TODO: This should be part of the signed refs.
+
        remote: &RemoteId,
+
        repo: &S,
+
    ) -> Result<Updated, Error> {
+
        let sigref = &*SIGNATURE_REF;
+
        let parent: Option<git2::Commit> = repo
+
            .reference(remote, sigref)?
+
            .map(|r| r.peel_to_commit())
+
            .transpose()?;
+

+
        let tree = {
+
            let raw = repo.raw();
+
            let refs_blob_oid = raw.blob(&self.canonical())?;
+
            let sig_blob_oid = raw.blob(self.signature.as_ref())?;
+

+
            let mut builder = raw.treebuilder(None)?;
+
            builder.insert(REFS_BLOB_PATH, refs_blob_oid, 0o100_644)?;
+
            builder.insert(SIGNATURE_BLOB_PATH, sig_blob_oid, 0o100_644)?;
+

+
            let oid = builder.write()?;
+

+
            raw.find_tree(oid)
+
        }?;
+

+
        if let Some(ref parent) = parent {
+
            if parent.tree()?.id() == tree.id() {
+
                return Ok(Updated::Unchanged {
+
                    oid: parent.id().into(),
+
                });
+
            }
+
        }
+

+
        let sigref = format!("refs/remotes/{remote}/{sigref}");
+
        let author = repo.raw().signature()?;
+
        let commit = repo.raw().commit(
+
            Some(&sigref),
+
            &author,
+
            &author,
+
            &format!("Update {} for {}", sigref, remote),
+
            &tree,
+
            &parent.iter().collect::<Vec<&git2::Commit>>(),
+
        );
+

+
        match commit {
+
            Ok(oid) => Ok(Updated::Updated { oid: oid.into() }),
+
            Err(e) => match (e.class(), e.code()) {
+
                (git2::ErrorClass::Object, git2::ErrorCode::Modified) => {
+
                    log::warn!("Concurrent modification of refs: {:?}", e);
+

+
                    Err(Error::Git(e))
+
                }
+
                _ => Err(e.into()),
+
            },
+
        }
+
    }
+

+
    pub fn unverified(self) -> SignedRefs<Unverified> {
+
        SignedRefs {
+
            refs: self.refs,
+
            signature: self.signature,
+
            _verified: PhantomData,
+
        }
+
    }
+
}
+

+
impl<V> wire::Encode for SignedRefs<V> {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = 0;
+

+
        n += self.refs.encode(writer)?;
+
        n += self.signature.encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for SignedRefs<Unverified> {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let refs = Refs::decode(reader)?;
+
        let signature = Signature::decode(reader)?;
+

+
        Ok(Self {
+
            refs,
+
            signature,
+
            _verified: PhantomData,
+
        })
+
    }
+
}
+

+
impl<V> Deref for SignedRefs<V> {
+
    type Target = Refs;
+

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

+
pub mod canonical {
+
    use super::*;
+

+
    #[derive(Debug, thiserror::Error)]
+
    pub enum Error {
+
        #[error(transparent)]
+
        InvalidRef(#[from] git_ref_format::Error),
+
        #[error("invalid canonical format")]
+
        InvalidFormat,
+
        #[error(transparent)]
+
        Io(#[from] io::Error),
+
        #[error(transparent)]
+
        Git(#[from] git2::Error),
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    #[quickcheck]
+
    fn prop_canonical_roundtrip(refs: Refs) {
+
        let encoded = refs.canonical();
+
        let decoded = Refs::from_canonical(&encoded).unwrap();
+

+
        assert_eq!(refs, decoded);
+
    }
+
}
added radicle-node/src/test.rs
@@ -0,0 +1,10 @@
+
pub(crate) mod arbitrary;
+
pub(crate) mod assert;
+
pub(crate) mod crypto;
+
pub(crate) mod fixtures;
+
pub(crate) mod handle;
+
pub(crate) mod logger;
+
pub(crate) mod peer;
+
pub(crate) mod simulator;
+
pub(crate) mod storage;
+
pub(crate) mod tests;
added radicle-node/src/test/arbitrary.rs
@@ -0,0 +1,336 @@
+
use std::collections::{BTreeMap, HashSet};
+
use std::hash::Hash;
+
use std::iter;
+
use std::net;
+
use std::ops::RangeBounds;
+
use std::path::PathBuf;
+

+
use bloomy::BloomFilter;
+
use nonempty::NonEmpty;
+
use quickcheck::Arbitrary;
+

+
use crate::collections::HashMap;
+
use crate::crypto;
+
use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified};
+
use crate::git;
+
use crate::hash;
+
use crate::identity::{doc::Delegate, doc::Doc, Did, Id, Project};
+
use crate::service::filter::{Filter, FILTER_SIZE};
+
use crate::service::message::{
+
    Address, Envelope, InventoryAnnouncement, Message, NodeAnnouncement, RefsAnnouncement,
+
    Subscribe,
+
};
+
use crate::service::{NodeId, Timestamp};
+
use crate::storage;
+
use crate::storage::refs::{Refs, SignedRefs};
+
use crate::test::storage::MockStorage;
+
use crate::wire::message::MessageType;
+

+
use super::crypto::MockSigner;
+

+
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
+
    let size = fastrand::usize(range);
+
    let mut set = HashSet::with_capacity(size);
+
    let mut g = quickcheck::Gen::new(size);
+

+
    while set.len() < size {
+
        set.insert(T::arbitrary(&mut g));
+
    }
+
    set
+
}
+

+
pub fn gen<T: Arbitrary>(size: usize) -> T {
+
    let mut gen = quickcheck::Gen::new(size);
+

+
    T::arbitrary(&mut gen)
+
}
+

+
#[derive(Clone, Debug)]
+
pub struct ByteArray<const N: usize>([u8; N]);
+

+
impl<const N: usize> ByteArray<N> {
+
    pub fn into_inner(self) -> [u8; N] {
+
        self.0
+
    }
+

+
    pub fn as_slice(&self) -> &[u8] {
+
        self.0.as_slice()
+
    }
+
}
+

+
impl<const N: usize> Arbitrary for ByteArray<N> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut bytes: [u8; N] = [0; N];
+
        for byte in &mut bytes {
+
            *byte = u8::arbitrary(g);
+
        }
+
        Self(bytes)
+
    }
+
}
+

+
impl Arbitrary for Filter {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut bytes = vec![0; FILTER_SIZE];
+
        for _ in 0..64 {
+
            let index = usize::arbitrary(g) % bytes.len();
+
            bytes[index] = u8::arbitrary(g);
+
        }
+
        Self::from(BloomFilter::from(bytes))
+
    }
+
}
+

+
impl Arbitrary for Envelope {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        Self {
+
            magic: u32::arbitrary(g),
+
            msg: Message::arbitrary(g),
+
        }
+
    }
+
}
+

+
impl Arbitrary for Message {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let type_id = g
+
            .choose(&[
+
                MessageType::InventoryAnnouncement,
+
                MessageType::NodeAnnouncement,
+
                MessageType::RefsAnnouncement,
+
                MessageType::Subscribe,
+
            ])
+
            .unwrap();
+

+
        match type_id {
+
            MessageType::InventoryAnnouncement => Self::InventoryAnnouncement {
+
                node: NodeId::arbitrary(g),
+
                message: InventoryAnnouncement {
+
                    inventory: Vec::<Id>::arbitrary(g),
+
                    timestamp: Timestamp::arbitrary(g),
+
                },
+
                signature: crypto::Signature::from(ByteArray::<64>::arbitrary(g).into_inner()),
+
            },
+
            MessageType::RefsAnnouncement => Self::RefsAnnouncement {
+
                node: NodeId::arbitrary(g),
+
                message: RefsAnnouncement {
+
                    id: Id::arbitrary(g),
+
                    refs: Refs::arbitrary(g),
+
                },
+
                signature: crypto::Signature::from(ByteArray::<64>::arbitrary(g).into_inner()),
+
            },
+
            MessageType::NodeAnnouncement => {
+
                let message = NodeAnnouncement {
+
                    features: ByteArray::<32>::arbitrary(g).into_inner(),
+
                    timestamp: Timestamp::arbitrary(g),
+
                    alias: ByteArray::<32>::arbitrary(g).into_inner(),
+
                    addresses: Arbitrary::arbitrary(g),
+
                };
+
                let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
+
                let signature = crypto::Signature::from(bytes.into_inner());
+

+
                Self::NodeAnnouncement {
+
                    node: NodeId::arbitrary(g),
+
                    signature,
+
                    message,
+
                }
+
            }
+
            MessageType::Subscribe => Self::Subscribe(Subscribe {
+
                filter: Filter::arbitrary(g),
+
                since: Timestamp::arbitrary(g),
+
                until: Timestamp::arbitrary(g),
+
            }),
+
            _ => unreachable!(),
+
        }
+
    }
+
}
+

+
impl Arbitrary for Address {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        if bool::arbitrary(g) {
+
            Address::Ipv4 {
+
                ip: net::Ipv4Addr::from(u32::arbitrary(g)),
+
                port: u16::arbitrary(g),
+
            }
+
        } else {
+
            let octets: [u8; 16] = ByteArray::<16>::arbitrary(g).into_inner();
+

+
            Address::Ipv6 {
+
                ip: net::Ipv6Addr::from(octets),
+
                port: u16::arbitrary(g),
+
            }
+
        }
+
    }
+
}
+

+
impl Arbitrary for storage::Remotes<crypto::Verified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
+
            Arbitrary::arbitrary(g);
+

+
        storage::Remotes::new(remotes)
+
    }
+
}
+

+
impl Arbitrary for MockStorage {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let inventory = Arbitrary::arbitrary(g);
+
        MockStorage::new(inventory)
+
    }
+
}
+

+
impl Arbitrary for Project {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let doc = Doc::<Verified>::arbitrary(g);
+
        let (oid, _) = doc.encode().unwrap();
+
        let id = Id::from(oid);
+
        let remotes = storage::Remotes::arbitrary(g);
+
        let path = PathBuf::arbitrary(g);
+

+
        Self {
+
            id,
+
            doc,
+
            remotes,
+
            path,
+
        }
+
    }
+
}
+

+
impl Arbitrary for Did {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        Self::from(PublicKey::arbitrary(g))
+
    }
+
}
+

+
impl Arbitrary for Delegate {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        Self {
+
            name: String::arbitrary(g),
+
            id: Did::arbitrary(g),
+
        }
+
    }
+
}
+

+
impl Arbitrary for Doc<Unverified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let name = String::arbitrary(g);
+
        let description = String::arbitrary(g);
+
        let default_branch = String::arbitrary(g);
+
        let delegate = Delegate::arbitrary(g);
+

+
        Self::initial(name, description, default_branch, delegate)
+
    }
+
}
+

+
impl Arbitrary for Doc<Verified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
+
        let name = iter::repeat_with(|| rng.alphanumeric())
+
            .take(rng.usize(1..16))
+
            .collect();
+
        let description = iter::repeat_with(|| rng.alphanumeric())
+
            .take(rng.usize(0..32))
+
            .collect();
+
        let default_branch = iter::repeat_with(|| rng.alphanumeric())
+
            .take(rng.usize(1..16))
+
            .collect();
+
        let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
+
            name: iter::repeat_with(|| rng.alphanumeric())
+
                .take(rng.usize(1..16))
+
                .collect(),
+
            id: Did::arbitrary(g),
+
        })
+
        .take(rng.usize(1..6))
+
        .collect::<Vec<_>>()
+
        .try_into()
+
        .unwrap();
+
        let threshold = delegates.len() / 2 + 1;
+
        let doc: Doc<Unverified> =
+
            Doc::new(name, description, default_branch, delegates, threshold);
+

+
        doc.verified().unwrap()
+
    }
+
}
+

+
impl Arbitrary for SignedRefs<Unverified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
+
        let signature = crypto::Signature::from(bytes.into_inner());
+
        let refs = Refs::arbitrary(g);
+

+
        Self::new(refs, signature)
+
    }
+
}
+

+
impl Arbitrary for Refs {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
+
        let mut bytes: [u8; 20] = [0; 20];
+
        let names = &[
+
            "heads/master",
+
            "heads/feature/1",
+
            "heads/feature/2",
+
            "heads/feature/3",
+
            "heads/radicle/id",
+
            "tags/v1.0",
+
            "tags/v2.0",
+
            "notes/1",
+
        ];
+

+
        for _ in 0..g.size().min(names.len()) {
+
            if let Some(name) = g.choose(names) {
+
                for byte in &mut bytes {
+
                    *byte = u8::arbitrary(g);
+
                }
+
                let oid = storage::Oid::try_from(&bytes[..]).unwrap();
+
                let name = git::RefString::try_from(*name).unwrap();
+

+
                refs.insert(name, oid);
+
            }
+
        }
+
        Self::from(refs)
+
    }
+
}
+

+
impl Arbitrary for storage::Remote<crypto::Verified> {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let refs = Refs::arbitrary(g);
+
        let signer = MockSigner::arbitrary(g);
+
        let signed = refs.signed(&signer).unwrap();
+

+
        storage::Remote::new(*signer.public_key(), signed)
+
    }
+
}
+

+
impl Arbitrary for MockSigner {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
+
        let seed = Seed::new(bytes.into_inner());
+
        let sk = KeyPair::from_seed(seed).sk;
+

+
        MockSigner::from(sk)
+
    }
+
}
+

+
impl Arbitrary for Id {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes = ByteArray::<20>::arbitrary(g);
+
        let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
+

+
        Id::from(oid)
+
    }
+
}
+

+
impl Arbitrary for hash::Digest {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: Vec<u8> = Arbitrary::arbitrary(g);
+
        hash::Digest::new(&bytes)
+
    }
+
}
+

+
impl Arbitrary for PublicKey {
+
    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
+
        let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
+
        let seed = Seed::new(bytes.into_inner());
+
        let keypair = KeyPair::from_seed(seed);
+

+
        PublicKey(keypair.pk)
+
    }
+
}
added radicle-node/src/test/assert.rs
@@ -0,0 +1,296 @@
+
// Copyright (c) 2016 Murarth
+
//
+
// 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.
+

+
//! Provides a macro, `assert_matches!`, which tests whether a value
+
//! matches a given pattern, causing a panic if the match fails.
+
//!
+
//! See the macro [`assert_matches!`] documentation for more information.
+
//!
+
//! Also provides a debug-only counterpart, [`debug_assert_matches!`].
+
//!
+
//! See the macro [`debug_assert_matches!`] documentation for more information
+
//! about this macro.
+
//!
+
//! [`assert_matches!`]: macro.assert_matches.html
+
//! [`debug_assert_matches!`]: macro.debug_assert_matches.html
+

+
#![deny(missing_docs)]
+

+
/// Asserts that an expression matches a given pattern.
+
///
+
/// A guard expression may be supplied to add further restrictions to the
+
/// expected value of the expression.
+
///
+
/// A `match` arm may be supplied to perform additional assertions or to yield
+
/// a value from the macro invocation.
+
///
+
#[macro_export]
+
macro_rules! assert_matches {
+
    ( $e:expr , $($pat:pat_param)|+ ) => {
+
        match $e {
+
            $($pat)|+ => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr ) => {
+
        match $e {
+
            $($pat)|+ if $cond => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+ if $cond))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr ) => {
+
        match $e {
+
            $($pat)|+ => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr ) => {
+
        match $e {
+
            $($pat)|+ if $cond => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`",
+
                e, stringify!($($pat)|+ if $cond))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ if $cond => (),
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ => $arm:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+), format_args!($($arg)*))
+
        }
+
    };
+
    ( $e:expr , $($pat:pat_param)|+ if $cond:expr => $arm:expr , $($arg:tt)* ) => {
+
        match $e {
+
            $($pat)|+ if $cond => $arm,
+
            ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}",
+
                e, stringify!($($pat)|+ if $cond), format_args!($($arg)*))
+
        }
+
    };
+
}
+

+
/// Asserts that an expression matches a given pattern.
+
///
+
/// Unlike [`assert_matches!`], `debug_assert_matches!` statements are only enabled
+
/// in non-optimized builds by default. An optimized build will omit all
+
/// `debug_assert_matches!` statements unless `-C debug-assertions` is passed
+
/// to the compiler.
+
///
+
/// See the macro [`assert_matches!`] documentation for more information.
+
///
+
/// [`assert_matches!`]: macro.assert_matches.html
+
#[macro_export(local_inner_macros)]
+
macro_rules! debug_assert_matches {
+
    ( $($tt:tt)* ) => { {
+
        if _assert_matches_cfg!(debug_assertions) {
+
            assert_matches!($($tt)*);
+
        }
+
    } }
+
}
+

+
#[doc(hidden)]
+
#[macro_export]
+
macro_rules! _assert_matches_cfg {
+
    ( $($tt:tt)* ) => { cfg!($($tt)*) }
+
}
+

+
#[cfg(test)]
+
mod test {
+
    use std::panic::{catch_unwind, UnwindSafe};
+

+
    #[derive(Debug)]
+
    enum Foo {
+
        A(i32),
+
        B(&'static str),
+
        C(&'static str),
+
    }
+

+
    #[test]
+
    fn test_assert_succeed() {
+
        let a = Foo::A(123);
+

+
        assert_matches!(a, Foo::A(_));
+
        assert_matches!(a, Foo::A(123));
+
        assert_matches!(a, Foo::A(i) if i == 123);
+
        assert_matches!(a, Foo::A(42) | Foo::A(123));
+

+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B(_));
+
        assert_matches!(b, Foo::B("foo"));
+
        assert_matches!(b, Foo::B(s) if s == "foo");
+
        assert_matches!(b, Foo::B(s) => assert_eq!(s, "foo"));
+
        assert_matches!(b, Foo::B(s) => { assert_eq!(s, "foo") });
+
        assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "foo"));
+
        assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo") });
+

+
        let c = Foo::C("foo");
+

+
        assert_matches!(c, Foo::B(_) | Foo::C(_));
+
        assert_matches!(c, Foo::B("foo") | Foo::C("foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo");
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) => assert_eq!(s, "foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) => { assert_eq!(s, "foo") });
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => assert_eq!(s, "foo"));
+
        assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => { assert_eq!(s, "foo") });
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_0() {
+
        let a = Foo::A(123);
+

+
        assert_matches!(a, Foo::B(_));
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_1() {
+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B("bar"));
+
    }
+

+
    #[test]
+
    #[should_panic]
+
    fn test_assert_panic_2() {
+
        let b = Foo::B("foo");
+

+
        assert_matches!(b, Foo::B(s) if s == "bar");
+
    }
+

+
    #[test]
+
    fn test_assert_no_move() {
+
        let b = &mut Foo::A(0);
+
        assert_matches!(*b, Foo::A(0));
+
    }
+

+
    #[test]
+
    fn assert_with_message() {
+
        let a = Foo::A(0);
+

+
        assert_matches!(a, Foo::A(_), "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes");
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes");
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes");
+
        assert_matches!(a, Foo::A(n) if n == 0 => { assert_eq!(n, 0); assert!(n < 1) }, "o noes");
+
        assert_matches!(a, Foo::A(_), "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {:?}", a);
+
        assert_matches!(a, Foo::A(_), "o noes {value:?}", value = a);
+
        assert_matches!(a, Foo::A(n) if n == 0, "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {value:?}", value=a);
+
        assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes {value:?}", value=a);
+
    }
+

+
    fn panic_message<F>(f: F) -> String
+
    where
+
        F: FnOnce() + UnwindSafe,
+
    {
+
        let err = catch_unwind(f).expect_err("function did not panic");
+

+
        *err.downcast::<String>()
+
            .expect("function panicked with non-String value")
+
    }
+

+
    #[test]
+
    fn test_panic_message() {
+
        let a = Foo::A(1);
+

+
        // expr, pat
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_));
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
+
        );
+

+
        // expr, pat if cond
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
+
        );
+

+
        // expr, pat => arm
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_) => {});
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#
+
        );
+

+
        // expr, pat if cond => arm
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo" => {});
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#
+
        );
+

+
        // expr, pat, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_), "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
+
        );
+

+
        // expr, pat if cond, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo", "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
+
        );
+

+
        // expr, pat => arm, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(_) => {}, "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#
+
        );
+

+
        // expr, pat if cond => arm, args
+
        assert_eq!(
+
            panic_message(|| {
+
                assert_matches!(a, Foo::B(s) if s == "foo" => {}, "msg");
+
            }),
+
            r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#
+
        );
+
    }
+
}
added radicle-node/src/test/crypto.rs
@@ -0,0 +1,65 @@
+
use crate::crypto::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer};
+

+
#[derive(Debug, Clone)]
+
pub struct MockSigner {
+
    pk: PublicKey,
+
    sk: SecretKey,
+
}
+

+
impl MockSigner {
+
    pub fn new(rng: &mut fastrand::Rng) -> Self {
+
        let mut bytes: [u8; 32] = [0; 32];
+

+
        for byte in &mut bytes {
+
            *byte = rng.u8(..);
+
        }
+
        let seed = Seed::new(bytes);
+
        let keypair = KeyPair::from_seed(seed);
+

+
        Self::from(keypair.sk)
+
    }
+
}
+

+
impl From<SecretKey> for MockSigner {
+
    fn from(sk: SecretKey) -> Self {
+
        let pk = sk.public_key().into();
+
        Self { sk, pk }
+
    }
+
}
+

+
impl Default for MockSigner {
+
    fn default() -> Self {
+
        let seed = Seed::generate();
+
        let keypair = KeyPair::from_seed(seed);
+
        let sk = keypair.sk;
+

+
        Self {
+
            pk: sk.public_key().into(),
+
            sk,
+
        }
+
    }
+
}
+

+
impl PartialEq for MockSigner {
+
    fn eq(&self, other: &Self) -> bool {
+
        self.pk == other.pk
+
    }
+
}
+

+
impl Eq for MockSigner {}
+

+
impl std::hash::Hash for MockSigner {
+
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+
        self.pk.hash(state)
+
    }
+
}
+

+
impl Signer for MockSigner {
+
    fn public_key(&self) -> &PublicKey {
+
        &self.pk
+
    }
+

+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.sk.sign(msg, None).into()
+
    }
+
}
added radicle-node/src/test/fixtures.rs
@@ -0,0 +1,116 @@
+
use std::path::Path;
+

+
use crate::crypto::{Signer, Verified};
+
use crate::git;
+
use crate::identity::Id;
+
use crate::rad;
+
use crate::storage::git::Storage;
+
use crate::storage::refs::SignedRefs;
+
use crate::storage::{BranchName, WriteStorage};
+
use crate::test::arbitrary;
+
use crate::test::crypto::MockSigner;
+

+
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
+
    let path = path.as_ref();
+
    let proj_ids = arbitrary::set::<Id>(3..=3);
+
    let signers = arbitrary::set::<MockSigner>(3..=3);
+
    let storage = Storage::open(path).unwrap();
+

+
    crate::test::logger::init(log::Level::Debug);
+

+
    for signer in signers {
+
        let remote = signer.public_key();
+

+
        log::debug!("signer {}...", remote);
+

+
        for proj in proj_ids.iter() {
+
            let repo = storage.repository(proj).unwrap();
+
            let raw = &repo.backend;
+
            let sig = git2::Signature::now(&remote.to_string(), "anonymous@radicle.xyz").unwrap();
+
            let head = git::initial_commit(raw, &sig).unwrap();
+

+
            log::debug!("{}: creating {}...", remote, proj);
+

+
            raw.reference(
+
                &format!("refs/remotes/{remote}/heads/radicle/id"),
+
                head.id(),
+
                false,
+
                "test",
+
            )
+
            .unwrap();
+

+
            let head = git::commit(
+
                raw,
+
                &head,
+
                &git::RefString::try_from(format!("refs/remotes/{remote}/heads/master")).unwrap(),
+
                "Second commit",
+
                &remote.to_string(),
+
            )
+
            .unwrap();
+

+
            git::commit(
+
                raw,
+
                &head,
+
                &git::RefString::try_from(format!("refs/remotes/{remote}/heads/patch/3")).unwrap(),
+
                "Third commit",
+
                &remote.to_string(),
+
            )
+
            .unwrap();
+

+
            storage.sign_refs(&repo, &signer).unwrap();
+
        }
+
    }
+
    storage
+
}
+

+
/// Create a new repository at the given path, and initialize it into a project.
+
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
+
    path: P,
+
    storage: &'r S,
+
    signer: G,
+
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
+
    let (repo, head) = repository(path);
+
    let (id, refs) = rad::init(
+
        &repo,
+
        "acme",
+
        "Acme's repository",
+
        BranchName::from("master"),
+
        signer,
+
        storage,
+
    )?;
+

+
    Ok((id, refs, repo, head))
+
}
+

+
/// Creates a regular repository at the given path with a couple of commits.
+
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
+
    let repo = git2::Repository::init(path).unwrap();
+
    let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
+
    let head = git::initial_commit(&repo, &sig).unwrap();
+
    let oid = git::commit(
+
        &repo,
+
        &head,
+
        git::refname!("refs/heads/master").as_refstr(),
+
        "Second commit",
+
        "anonymous",
+
    )
+
    .unwrap()
+
    .id();
+

+
    // Look, I don't really understand why we have to do this, but we do.
+
    drop(head);
+

+
    (repo, oid)
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+

+
    #[test]
+
    fn smoke() {
+
        let tmp = tempfile::tempdir().unwrap();
+

+
        storage(&tmp.path());
+
    }
+
}
added radicle-node/src/test/handle.rs
@@ -0,0 +1,40 @@
+
use std::sync::{Arc, Mutex};
+

+
use crate::client::handle::traits;
+
use crate::client::handle::Error;
+
use crate::identity::Id;
+
use crate::service;
+
use crate::service::FetchLookup;
+

+
#[derive(Default, Clone)]
+
pub struct Handle {
+
    pub updates: Arc<Mutex<Vec<Id>>>,
+
}
+

+
impl traits::Handle for Handle {
+
    fn fetch(&self, _id: Id) -> Result<FetchLookup, Error> {
+
        Ok(FetchLookup::NotFound)
+
    }
+

+
    fn track(&self, _id: Id) -> Result<bool, Error> {
+
        Ok(true)
+
    }
+

+
    fn untrack(&self, _id: Id) -> Result<bool, Error> {
+
        Ok(true)
+
    }
+

+
    fn updated(&self, id: Id) -> Result<(), Error> {
+
        self.updates.lock().unwrap().push(id);
+

+
        Ok(())
+
    }
+

+
    fn command(&self, _cmd: service::Command) -> Result<(), Error> {
+
        Ok(())
+
    }
+

+
    fn shutdown(self) -> Result<(), Error> {
+
        Ok(())
+
    }
+
}
added radicle-node/src/test/logger.rs
@@ -0,0 +1,49 @@
+
use log::*;
+

+
struct Logger {
+
    level: Level,
+
}
+

+
impl Log for Logger {
+
    fn enabled(&self, metadata: &Metadata) -> bool {
+
        metadata.level() <= self.level
+
    }
+

+
    fn log(&self, record: &Record) {
+
        use colored::Colorize;
+

+
        match record.target() {
+
            "test" => {
+
                println!("{} {}", "test:".cyan(), record.args().to_string().yellow())
+
            }
+
            "sim" => {
+
                println!("{}  {}", "sim:".bold(), record.args().to_string().bold())
+
            }
+
            target => {
+
                if self.enabled(record.metadata()) {
+
                    let s = format!("{:<8} {}", format!("{}:", target), record.args());
+
                    match record.level() {
+
                        log::Level::Warn => {
+
                            println!("{}", s.yellow());
+
                        }
+
                        log::Level::Error => {
+
                            println!("{}", s.red());
+
                        }
+
                        _ => {
+
                            println!("{}", s.dimmed());
+
                        }
+
                    }
+
                }
+
            }
+
        }
+
    }
+

+
    fn flush(&self) {}
+
}
+

+
pub fn init(level: Level) {
+
    let logger = Logger { level };
+

+
    log::set_boxed_logger(Box::new(logger)).ok();
+
    log::set_max_level(level.to_level_filter());
+
}
added radicle-node/src/test/peer.rs
@@ -0,0 +1,211 @@
+
use std::net;
+
use std::ops::{Deref, DerefMut};
+

+
use git_url::Url;
+
use log::*;
+

+
use crate::address_book::{KnownAddress, Source};
+
use crate::clock::RefClock;
+
use crate::collections::HashMap;
+
use crate::service;
+
use crate::service::config::*;
+
use crate::service::message::*;
+
use crate::service::*;
+
use crate::storage::WriteStorage;
+
use crate::test::crypto::MockSigner;
+
use crate::test::simulator;
+
use crate::{Link, LocalTime};
+

+
/// Service instantiation used for testing.
+
pub type Service<S> = service::Service<HashMap<net::IpAddr, KnownAddress>, S, MockSigner>;
+

+
#[derive(Debug)]
+
pub struct Peer<S> {
+
    pub name: &'static str,
+
    pub service: Service<S>,
+
    pub ip: net::IpAddr,
+
    pub rng: fastrand::Rng,
+
    pub local_time: LocalTime,
+
    pub local_addr: net::SocketAddr,
+

+
    initialized: bool,
+
}
+

+
impl<'r, S> simulator::Peer<S> for Peer<S>
+
where
+
    S: WriteStorage<'r> + 'static,
+
{
+
    fn init(&mut self) {
+
        self.initialize()
+
    }
+

+
    fn addr(&self) -> net::SocketAddr {
+
        net::SocketAddr::new(self.ip, DEFAULT_PORT)
+
    }
+
}
+

+
impl<S> Deref for Peer<S> {
+
    type Target = Service<S>;
+

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

+
impl<S> DerefMut for Peer<S> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.service
+
    }
+
}
+

+
impl<'r, S> Peer<S>
+
where
+
    S: WriteStorage<'r> + 'static,
+
{
+
    pub fn new(name: &'static str, ip: impl Into<net::IpAddr>, storage: S) -> Self {
+
        Self::config(
+
            name,
+
            Config {
+
                git_url: storage.url(),
+
                ..Config::default()
+
            },
+
            ip,
+
            vec![],
+
            storage,
+
            fastrand::Rng::new(),
+
        )
+
    }
+

+
    pub fn config(
+
        name: &'static str,
+
        config: Config,
+
        ip: impl Into<net::IpAddr>,
+
        addrs: Vec<(net::SocketAddr, Source)>,
+
        storage: S,
+
        mut rng: fastrand::Rng,
+
    ) -> Self {
+
        let addrs = addrs
+
            .into_iter()
+
            .map(|(addr, src)| (addr.ip(), KnownAddress::new(addr, src, None)))
+
            .collect();
+
        let local_time = LocalTime::now();
+
        let clock = RefClock::from(local_time);
+
        let signer = MockSigner::new(&mut rng);
+
        let service = Service::new(config, clock, storage, addrs, signer, rng.clone());
+
        let ip = ip.into();
+
        let local_addr = net::SocketAddr::new(ip, rng.u16(..));
+

+
        Self {
+
            name,
+
            service,
+
            ip,
+
            local_addr,
+
            rng,
+
            local_time,
+
            initialized: false,
+
        }
+
    }
+

+
    pub fn initialize(&mut self) {
+
        if !self.initialized {
+
            info!("{}: Initializing: address = {}", self.name, self.ip);
+

+
            self.initialized = true;
+
            self.service.initialize(LocalTime::now());
+
        }
+
    }
+

+
    pub fn timestamp(&self) -> Timestamp {
+
        self.service.timestamp()
+
    }
+

+
    pub fn git_url(&self) -> Url {
+
        self.config().git_url.clone()
+
    }
+

+
    pub fn node_id(&self) -> NodeId {
+
        self.service.node_id()
+
    }
+

+
    pub fn receive(&mut self, peer: &net::SocketAddr, msg: Message) {
+
        self.service
+
            .received_message(peer, self.config().network.envelope(msg));
+
    }
+

+
    pub fn connect_from(&mut self, peer: &Self) {
+
        let remote = simulator::Peer::<S>::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();
+

+
        self.initialize();
+
        self.service.connected(remote, &local, Link::Inbound);
+
        self.receive(
+
            &remote,
+
            Message::init(
+
                peer.node_id(),
+
                self.local_time().as_secs(),
+
                vec![Address::from(remote)],
+
                git,
+
            ),
+
        );
+

+
        let mut msgs = self.messages(&remote);
+
        msgs.find(|m| matches!(m, Message::Initialize { .. }))
+
            .expect("`initialize` is sent");
+
        msgs.find(|m| matches!(m, Message::InventoryAnnouncement { .. }))
+
            .expect("`inventory-announcement` is sent");
+
    }
+

+
    pub fn connect_to(&mut self, peer: &Self) {
+
        let remote = simulator::Peer::<S>::addr(peer);
+

+
        self.initialize();
+
        self.service.attempted(&remote);
+
        self.service
+
            .connected(remote, &self.local_addr, Link::Outbound);
+

+
        let mut msgs = self.messages(&remote);
+
        msgs.find(|m| matches!(m, Message::Initialize { .. }))
+
            .expect("`initialize` is sent");
+
        msgs.find(|m| matches!(m, Message::InventoryAnnouncement { .. }))
+
            .expect("`inventory-announcement` is sent");
+

+
        let git = peer.config().git_url.clone();
+
        self.receive(
+
            &remote,
+
            Message::init(
+
                peer.node_id(),
+
                self.local_time().as_secs(),
+
                peer.config().listen.clone(),
+
                git,
+
            ),
+
        );
+
    }
+

+
    /// Drain outgoing messages sent from this peer to the remote address.
+
    pub fn messages(&mut self, remote: &net::SocketAddr) -> impl Iterator<Item = Message> {
+
        let mut msgs = Vec::new();
+

+
        self.service.outbox().retain(|o| match o {
+
            service::Io::Write(a, envelopes) if a == remote => {
+
                msgs.extend(envelopes.iter().map(|e| e.msg.clone()));
+
                false
+
            }
+
            _ => true,
+
        });
+

+
        msgs.into_iter()
+
    }
+

+
    /// Get a draining iterator over the peer's emitted events.
+
    pub fn events(&mut self) -> impl Iterator<Item = Event> + '_ {
+
        self.outbox()
+
            .filter_map(|io| if let Io::Event(e) = io { Some(e) } else { None })
+
    }
+

+
    /// Get a draining iterator over the peer's I/O outbox.
+
    pub fn outbox(&mut self) -> impl Iterator<Item = Io> + '_ {
+
        self.service.outbox().drain(..)
+
    }
+
}
added radicle-node/src/test/simulator.rs
@@ -0,0 +1,619 @@
+
//! A simple P2P network simulator. Acts as the _reactor_, but without doing any I/O.
+
#![allow(clippy::collapsible_if)]
+

+
#[cfg(feature = "quickcheck")]
+
pub mod arbitrary;
+

+
use std::collections::{BTreeMap, BTreeSet, VecDeque};
+
use std::marker::PhantomData;
+
use std::ops::{Deref, DerefMut, Range};
+
use std::{fmt, io, net};
+

+
use log::*;
+
use nakamoto_net as nakamoto;
+
use nakamoto_net::{Link, LocalDuration, LocalTime};
+

+
use crate::service::{DisconnectReason, Envelope, Event, Io};
+
use crate::storage::WriteStorage;
+
use crate::test::peer::Service;
+

+
/// Minimum latency between peers.
+
pub const MIN_LATENCY: LocalDuration = LocalDuration::from_millis(1);
+
/// Maximum number of events buffered per peer.
+
pub const MAX_EVENTS: usize = 2048;
+

+
/// Identifier for a simulated node/peer.
+
/// The simulator requires each peer to have a distinct IP address.
+
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 {
+
    /// Initialize the peer. This should at minimum initialize the service with the
+
    /// current time.
+
    fn init(&mut self);
+
    /// Get the peer address.
+
    fn addr(&self) -> net::SocketAddr;
+
}
+

+
/// Simulated service input.
+
#[derive(Debug, Clone)]
+
pub enum Input {
+
    /// Connection attempt underway.
+
    Connecting {
+
        /// Remote peer address.
+
        addr: net::SocketAddr,
+
    },
+
    /// New connection with a peer.
+
    Connected {
+
        /// Remote peer id.
+
        addr: net::SocketAddr,
+
        /// Local peer id.
+
        local_addr: net::SocketAddr,
+
        /// Link direction.
+
        link: Link,
+
    },
+
    /// Disconnected from peer.
+
    Disconnected(
+
        net::SocketAddr,
+
        nakamoto::DisconnectReason<DisconnectReason>,
+
    ),
+
    /// Received a message from a remote peer.
+
    Received(net::SocketAddr, Vec<Envelope>),
+
    /// Used to advance the state machine after some wall time has passed.
+
    Wake,
+
}
+

+
/// A scheduled service input.
+
#[derive(Debug, Clone)]
+
pub struct Scheduled {
+
    /// The node for which this input is scheduled.
+
    pub node: NodeId,
+
    /// The remote peer from which this input originates.
+
    /// If the input originates from the local node, this should be set to the zero address.
+
    pub remote: net::SocketAddr,
+
    /// The input being scheduled.
+
    pub input: Input,
+
}
+

+
impl fmt::Display for Scheduled {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match &self.input {
+
            Input::Received(from, msgs) => {
+
                for msg in msgs {
+
                    write!(f, "{} <- {} ({:?})", self.node, from, msg)?;
+
                }
+
                Ok(())
+
            }
+
            Input::Connected {
+
                addr,
+
                local_addr,
+
                link: Link::Inbound,
+
                ..
+
            } => write!(f, "{} <== {}: Connected", local_addr, addr),
+
            Input::Connected {
+
                local_addr,
+
                addr,
+
                link: Link::Outbound,
+
                ..
+
            } => write!(f, "{} ==> {}: Connected", local_addr, addr),
+
            Input::Connecting { addr } => {
+
                write!(f, "{} => {}: Connecting", self.node, addr)
+
            }
+
            Input::Disconnected(addr, reason) => {
+
                write!(f, "{} =/= {}: Disconnected: {}", self.node, addr, reason)
+
            }
+
            Input::Wake => {
+
                write!(f, "{}: Tock", self.node)
+
            }
+
        }
+
    }
+
}
+

+
/// Inbox of scheduled state machine inputs to be delivered to the simulated nodes.
+
#[derive(Debug)]
+
pub struct Inbox {
+
    /// The set of scheduled inputs. We use a `BTreeMap` to ensure inputs are always
+
    /// ordered by scheduled delivery time.
+
    messages: BTreeMap<LocalTime, Scheduled>,
+
}
+

+
impl Inbox {
+
    /// Add a scheduled input to the inbox.
+
    fn insert(&mut self, mut time: LocalTime, msg: Scheduled) {
+
        // Make sure we don't overwrite an existing message by using the same time slot.
+
        while self.messages.contains_key(&time) {
+
            time = time + MIN_LATENCY;
+
        }
+
        self.messages.insert(time, msg);
+
    }
+

+
    /// Get the next scheduled input to be delivered.
+
    fn next(&mut self) -> Option<(LocalTime, Scheduled)> {
+
        self.messages
+
            .iter()
+
            .next()
+
            .map(|(time, scheduled)| (*time, scheduled.clone()))
+
    }
+

+
    /// Get the last message sent between two peers. Only checks one direction.
+
    fn last(&self, node: &NodeId, remote: &net::SocketAddr) -> Option<(&LocalTime, &Scheduled)> {
+
        self.messages
+
            .iter()
+
            .rev()
+
            .find(|(_, v)| &v.node == node && &v.remote == remote)
+
    }
+
}
+

+
/// Simulation options.
+
#[derive(Debug, Clone)]
+
pub struct Options {
+
    /// Minimum and maximum latency between nodes, in seconds.
+
    pub latency: Range<u64>,
+
    /// Probability that network I/O fails.
+
    /// A rate of `1.0` means 100% of I/O fails.
+
    pub failure_rate: f64,
+
}
+

+
impl Default for Options {
+
    fn default() -> Self {
+
        Self {
+
            latency: Range::default(),
+
            failure_rate: 0.,
+
        }
+
    }
+
}
+

+
/// A peer-to-peer node simulation.
+
pub struct Simulation<S> {
+
    /// Inbox of inputs to be delivered by the simulation.
+
    inbox: Inbox,
+
    /// Events emitted during simulation.
+
    events: BTreeMap<NodeId, VecDeque<Event>>,
+
    /// Priority events that should happen immediately.
+
    priority: VecDeque<Scheduled>,
+
    /// Simulated latencies between nodes.
+
    latencies: BTreeMap<(NodeId, NodeId), LocalDuration>,
+
    /// Network partitions between two nodes.
+
    partitions: BTreeSet<(NodeId, NodeId)>,
+
    /// Set of existing connections between nodes.
+
    connections: BTreeMap<(NodeId, NodeId), u16>,
+
    /// Set of connection attempts.
+
    attempts: BTreeSet<(NodeId, NodeId)>,
+
    /// Simulation options.
+
    opts: Options,
+
    /// Start time of simulation.
+
    start_time: LocalTime,
+
    /// Current simulation time. Updated when a scheduled message is processed.
+
    time: LocalTime,
+
    /// RNG.
+
    rng: fastrand::Rng,
+
    /// Storage type.
+
    storage: PhantomData<S>,
+
}
+

+
impl<'r, S: WriteStorage<'r>> Simulation<S> {
+
    /// Create a new simulation.
+
    pub fn new(time: LocalTime, rng: fastrand::Rng, opts: Options) -> Self {
+
        Self {
+
            inbox: Inbox {
+
                messages: BTreeMap::new(),
+
            },
+
            events: BTreeMap::new(),
+
            priority: VecDeque::new(),
+
            partitions: BTreeSet::new(),
+
            latencies: BTreeMap::new(),
+
            connections: BTreeMap::new(),
+
            attempts: BTreeSet::new(),
+
            opts,
+
            start_time: time,
+
            time,
+
            rng,
+
            storage: PhantomData,
+
        }
+
    }
+

+
    /// Check whether the simulation is done, ie. there are no more messages to process.
+
    pub fn is_done(&self) -> bool {
+
        self.inbox.messages.is_empty()
+
    }
+

+
    /// Total amount of simulated time elapsed.
+
    #[allow(dead_code)]
+
    pub fn elapsed(&self) -> LocalDuration {
+
        self.time - self.start_time
+
    }
+

+
    /// Check whether the simulation has settled, ie. the only messages left to process
+
    /// are (periodic) timeouts.
+
    pub fn is_settled(&self) -> bool {
+
        self.inbox
+
            .messages
+
            .iter()
+
            .all(|(_, s)| matches!(s.input, Input::Wake))
+
    }
+

+
    /// Get a node's emitted events.
+
    pub fn events(&mut self, node: &NodeId) -> impl Iterator<Item = Event> + '_ {
+
        self.events.entry(*node).or_default().drain(..)
+
    }
+

+
    /// Get the latency between two nodes. The minimum latency between nodes is 1 millisecond.
+
    pub fn latency(&self, from: NodeId, to: NodeId) -> LocalDuration {
+
        self.latencies
+
            .get(&(from, to))
+
            .cloned()
+
            .map(|l| {
+
                if l <= MIN_LATENCY {
+
                    l
+
                } else {
+
                    // Create variance in the latency. The resulting latency
+
                    // will be between half, and two times the base latency.
+
                    let millis = l.as_millis();
+

+
                    if self.rng.bool() {
+
                        // More latency.
+
                        LocalDuration::from_millis(millis + self.rng.u128(0..millis))
+
                    } else {
+
                        // Less latency.
+
                        LocalDuration::from_millis(millis - self.rng.u128(0..millis / 2))
+
                    }
+
                }
+
            })
+
            .unwrap_or_else(|| MIN_LATENCY)
+
    }
+

+
    /// Initialize peers.
+
    pub fn initialize<'a, P>(self, peers: impl IntoIterator<Item = &'a mut P>) -> Self
+
    where
+
        P: Peer<S>,
+
    {
+
        for peer in peers.into_iter() {
+
            peer.init();
+
        }
+
        self
+
    }
+

+
    /// Run the simulation while the given predicate holds.
+
    pub fn run_while<'a, P>(
+
        &mut self,
+
        peers: impl IntoIterator<Item = &'a mut P>,
+
        pred: impl Fn(&Self) -> bool,
+
    ) where
+
        P: Peer<S>,
+
    {
+
        let mut nodes: BTreeMap<_, _> = peers.into_iter().map(|p| (p.addr().ip(), p)).collect();
+

+
        while self.step_(&mut nodes) {
+
            if !pred(self) {
+
                break;
+
            }
+
        }
+
    }
+

+
    /// 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 {
+
        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 {
+
        if !self.opts.latency.is_empty() {
+
            // Configure latencies.
+
            for (i, from) in nodes.keys().enumerate() {
+
                for to in nodes.keys().skip(i + 1) {
+
                    let range = self.opts.latency.clone();
+
                    let latency = LocalDuration::from_millis(
+
                        self.rng
+
                            .u128(range.start as u128 * 1_000..range.end as u128 * 1_000),
+
                    );
+

+
                    self.latencies.entry((*from, *to)).or_insert(latency);
+
                    self.latencies.entry((*to, *from)).or_insert(latency);
+
                }
+
            }
+
        }
+

+
        // Create and heal partitions.
+
        // TODO: These aren't really "network" partitions, as they are only
+
        // between individual nodes. We need to think about more realistic
+
        // scenarios. We should also think about creating various network
+
        // topologies.
+
        if self.time.as_secs() % 10 == 0 {
+
            for (i, x) in nodes.keys().enumerate() {
+
                for y in nodes.keys().skip(i + 1) {
+
                    if self.is_fallible() {
+
                        self.partitions.insert((*x, *y));
+
                    } else {
+
                        self.partitions.remove(&(*x, *y));
+
                    }
+
                }
+
            }
+
        }
+

+
        // Schedule any messages in the pipes.
+
        for peer in nodes.values_mut() {
+
            let ip = peer.addr().ip();
+

+
            for o in peer.by_ref() {
+
                self.schedule(&ip, o);
+
            }
+
        }
+
        // Next high-priority message.
+
        let priority = self.priority.pop_front().map(|s| (self.time, s));
+

+
        if let Some((time, next)) = priority.or_else(|| self.inbox.next()) {
+
            let elapsed = (time - self.start_time).as_millis();
+
            if matches!(next.input, Input::Wake) {
+
                trace!(target: "sim", "{:05} {}", elapsed, next);
+
            } else {
+
                // TODO: This can be confusing, since this event may not actually be passed to
+
                // the service. It would be best to only log the events that are being sent
+
                // to the service, or to log when an input is being dropped.
+
                info!(target: "sim", "{:05} {} ({})", elapsed, next, self.inbox.messages.len());
+
            }
+
            assert!(time >= self.time, "Time only moves forwards!");
+

+
            self.time = time;
+
            self.inbox.messages.remove(&time);
+

+
            let Scheduled { input, node, .. } = next;
+

+
            if let Some(ref mut p) = nodes.get_mut(&node) {
+
                p.tick(time);
+

+
                match input {
+
                    Input::Connecting { addr } => {
+
                        if self.attempts.insert((node, addr.ip())) {
+
                            p.attempted(&addr);
+
                        }
+
                    }
+
                    Input::Connected {
+
                        addr,
+
                        local_addr,
+
                        link,
+
                    } => {
+
                        let conn = (node, addr.ip());
+

+
                        let attempted = link.is_outbound() && self.attempts.remove(&conn);
+
                        if attempted || link.is_inbound() {
+
                            if self.connections.insert(conn, local_addr.port()).is_none() {
+
                                p.connected(addr, &local_addr, link);
+
                            }
+
                        }
+
                    }
+
                    Input::Disconnected(addr, reason) => {
+
                        let conn = (node, addr.ip());
+
                        let attempt = self.attempts.remove(&conn);
+
                        let connection = self.connections.remove(&conn).is_some();
+

+
                        // Can't be both attempting and connected.
+
                        assert!(!(attempt && connection));
+

+
                        if attempt || connection {
+
                            p.disconnected(&addr, reason);
+
                        }
+
                    }
+
                    Input::Wake => p.wake(),
+
                    Input::Received(addr, msgs) => {
+
                        for msg in msgs {
+
                            p.received_message(&addr, msg);
+
                        }
+
                    }
+
                }
+
                for o in p.by_ref() {
+
                    self.schedule(&node, o);
+
                }
+
            } else {
+
                panic!(
+
                    "Node {} not found when attempting to schedule {:?}",
+
                    node, input
+
                );
+
            }
+
        }
+
        !self.is_done()
+
    }
+

+
    /// Process a service output event from a node.
+
    pub fn schedule(&mut self, node: &NodeId, out: Io) {
+
        let node = *node;
+

+
        match out {
+
            Io::Write(receiver, msgs) => {
+
                if msgs.is_empty() {
+
                    return;
+
                }
+
                // If the other end has disconnected the sender with some latency, there may not be
+
                // a connection remaining to use.
+
                let port = if let Some(port) = self.connections.get(&(node, receiver.ip())) {
+
                    *port
+
                } else {
+
                    return;
+
                };
+

+
                let sender: net::SocketAddr = (node, port).into();
+
                if self.is_partitioned(sender.ip(), receiver.ip()) {
+
                    // Drop message if nodes are partitioned.
+
                    info!(
+
                        target: "sim",
+
                        "{} -> {} (DROPPED)",
+
                         sender, receiver,
+
                    );
+
                    return;
+
                }
+

+
                // Schedule message in the future, ensuring messages don't arrive out-of-order
+
                // between two peers.
+
                let latency = self.latency(node, receiver.ip());
+
                let time = self
+
                    .inbox
+
                    .last(&receiver.ip(), &sender)
+
                    .map(|(k, _)| *k)
+
                    .unwrap_or_else(|| self.time);
+
                let time = time + latency;
+
                let elapsed = (time - self.start_time).as_millis();
+

+
                for msg in &msgs {
+
                    info!(
+
                        target: "sim",
+
                        "{:05} {} -> {} ({:?}) (+{})",
+
                        elapsed, sender, receiver, msg, latency
+
                    );
+
                }
+

+
                self.inbox.insert(
+
                    time,
+
                    Scheduled {
+
                        remote: sender,
+
                        node: receiver.ip(),
+
                        input: Input::Received(sender, msgs),
+
                    },
+
                );
+
            }
+
            Io::Connect(remote) => {
+
                assert!(remote.ip() != node, "self-connections are not allowed");
+

+
                // Create an ephemeral sockaddr for the connecting (local) node.
+
                let local_addr: net::SocketAddr = net::SocketAddr::new(node, self.rng.u16(8192..));
+
                let latency = self.latency(node, remote.ip());
+

+
                self.inbox.insert(
+
                    self.time + MIN_LATENCY,
+
                    Scheduled {
+
                        node,
+
                        remote,
+
                        input: Input::Connecting { addr: remote },
+
                    },
+
                );
+

+
                // Fail to connect if the nodes are partitioned.
+
                if self.is_partitioned(node, remote.ip()) {
+
                    log::info!(target: "sim", "{} -/-> {} (partitioned)", node, remote.ip());
+

+
                    // Sometimes, the service gets a failure input, other times it just hangs.
+
                    if self.rng.bool() {
+
                        self.inbox.insert(
+
                            self.time + MIN_LATENCY,
+
                            Scheduled {
+
                                node,
+
                                remote,
+
                                input: Input::Disconnected(
+
                                    remote,
+
                                    nakamoto::DisconnectReason::ConnectionError(
+
                                        io::Error::from(io::ErrorKind::UnexpectedEof).into(),
+
                                    ),
+
                                ),
+
                            },
+
                        );
+
                    }
+
                    return;
+
                }
+

+
                self.inbox.insert(
+
                    // The remote will get the connection attempt with some latency.
+
                    self.time + latency,
+
                    Scheduled {
+
                        node: remote.ip(),
+
                        remote: local_addr,
+
                        input: Input::Connected {
+
                            addr: local_addr,
+
                            local_addr: remote,
+
                            link: Link::Inbound,
+
                        },
+
                    },
+
                );
+
                self.inbox.insert(
+
                    // The local node will have established the connection after some latency.
+
                    self.time + latency,
+
                    Scheduled {
+
                        remote,
+
                        node,
+
                        input: Input::Connected {
+
                            addr: remote,
+
                            local_addr,
+
                            link: Link::Outbound,
+
                        },
+
                    },
+
                );
+
            }
+
            Io::Disconnect(remote, reason) => {
+
                // The local node is immediately disconnected.
+
                self.priority.push_back(Scheduled {
+
                    remote,
+
                    node,
+
                    input: Input::Disconnected(remote, reason.into()),
+
                });
+

+
                // Nb. It's possible for disconnects to happen simultaneously from both ends, hence
+
                // it can be that a node will try to disconnect a remote that is already
+
                // disconnected from the other side.
+
                //
+
                // It's also possible that the connection was only attempted and never succeeded,
+
                // in which case we would return here.
+
                let port = if let Some(port) = self.connections.get(&(node, remote.ip())) {
+
                    *port
+
                } else {
+
                    debug!(target: "sim", "Ignoring disconnect of {remote} from {node}");
+
                    return;
+
                };
+
                let local_addr: net::SocketAddr = (node, port).into();
+
                let latency = self.latency(node, remote.ip());
+

+
                // The remote node receives the disconnection with some delay.
+
                self.inbox.insert(
+
                    self.time + latency,
+
                    Scheduled {
+
                        node: remote.ip(),
+
                        remote: local_addr,
+
                        input: Input::Disconnected(
+
                            local_addr,
+
                            nakamoto::DisconnectReason::ConnectionError(
+
                                io::Error::from(io::ErrorKind::ConnectionReset).into(),
+
                            ),
+
                        ),
+
                    },
+
                );
+
            }
+
            Io::Wakeup(duration) => {
+
                let time = self.time + duration;
+

+
                if !matches!(
+
                    self.inbox.messages.get(&time),
+
                    Some(Scheduled {
+
                        input: Input::Wake,
+
                        ..
+
                    })
+
                ) {
+
                    self.inbox.insert(
+
                        time,
+
                        Scheduled {
+
                            node,
+
                            // The remote is not applicable for this type of output.
+
                            remote: ([0, 0, 0, 0], 0).into(),
+
                            input: Input::Wake,
+
                        },
+
                    );
+
                }
+
            }
+
            Io::Event(event) => {
+
                let events = self.events.entry(node).or_insert_with(VecDeque::new);
+
                if events.len() >= MAX_EVENTS {
+
                    warn!(target: "sim", "Dropping event: buffer is full");
+
                } else {
+
                    events.push_back(event);
+
                }
+
            }
+
        }
+
    }
+

+
    /// Check whether we should fail the next operation.
+
    fn is_fallible(&self) -> bool {
+
        self.rng.f64() % 1.0 < self.opts.failure_rate
+
    }
+

+
    /// Check whether two nodes are partitioned.
+
    fn is_partitioned(&self, a: NodeId, b: NodeId) -> bool {
+
        self.partitions.contains(&(a, b)) || self.partitions.contains(&(b, a))
+
    }
+
}
added radicle-node/src/test/storage.rs
@@ -0,0 +1,142 @@
+
use git_url::Url;
+

+
use crate::crypto::{Signer, Verified};
+
use crate::git;
+
use crate::identity::{Id, Project};
+
use crate::storage::{refs, RefUpdate};
+
use crate::storage::{
+
    Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, RemoteId, WriteRepository,
+
    WriteStorage,
+
};
+

+
#[derive(Clone, Debug)]
+
pub struct MockStorage {
+
    pub inventory: Vec<Project>,
+
}
+

+
impl MockStorage {
+
    pub fn new(inventory: Vec<Project>) -> Self {
+
        Self { inventory }
+
    }
+

+
    pub fn empty() -> Self {
+
        Self {
+
            inventory: Vec::new(),
+
        }
+
    }
+
}
+

+
impl ReadStorage for MockStorage {
+
    fn url(&self) -> Url {
+
        Url {
+
            scheme: git_url::Scheme::Radicle,
+
            host: Some("mock".to_string()),
+
            ..Url::default()
+
        }
+
    }
+

+
    fn get(&self, _remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
+
        if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) {
+
            return Ok(Some(proj.clone()));
+
        }
+
        Ok(None)
+
    }
+

+
    fn inventory(&self) -> Result<Inventory, Error> {
+
        let inventory = self
+
            .inventory
+
            .iter()
+
            .map(|proj| proj.id.clone())
+
            .collect::<Vec<_>>();
+

+
        Ok(inventory)
+
    }
+
}
+

+
impl WriteStorage<'_> for MockStorage {
+
    type Repository = MockRepository;
+

+
    fn repository(&self, _proj: &Id) -> Result<Self::Repository, Error> {
+
        Ok(MockRepository {})
+
    }
+

+
    fn sign_refs<G: Signer>(
+
        &self,
+
        _repository: &Self::Repository,
+
        _signer: G,
+
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
+
        todo!()
+
    }
+
}
+

+
pub struct MockRepository {}
+

+
impl ReadRepository<'_> for MockRepository {
+
    type Remotes = std::iter::Empty<Result<(RemoteId, Remote<Verified>), refs::Error>>;
+

+
    fn is_empty(&self) -> Result<bool, git2::Error> {
+
        Ok(true)
+
    }
+

+
    fn path(&self) -> &std::path::Path {
+
        todo!()
+
    }
+

+
    fn remote(&self, _remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
+
        todo!()
+
    }
+

+
    fn remotes(&self) -> Result<Self::Remotes, git2::Error> {
+
        todo!()
+
    }
+

+
    fn commit(&self, _oid: git::Oid) -> Result<Option<git2::Commit>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn revwalk(&self, _head: git::Oid) -> Result<git2::Revwalk, git2::Error> {
+
        todo!()
+
    }
+

+
    fn blob_at<'a>(
+
        &'a self,
+
        _oid: radicle_git_ext::Oid,
+
        _path: &'a std::path::Path,
+
    ) -> Result<git2::Blob<'a>, radicle_git_ext::Error> {
+
        todo!()
+
    }
+

+
    fn reference(
+
        &self,
+
        _remote: &RemoteId,
+
        _reference: &git::RefStr,
+
    ) -> Result<Option<git2::Reference>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn reference_oid(
+
        &self,
+
        _remote: &RemoteId,
+
        _reference: &git::RefStr,
+
    ) -> Result<Option<radicle_git_ext::Oid>, git2::Error> {
+
        todo!()
+
    }
+

+
    fn references(&self, _remote: &RemoteId) -> Result<crate::storage::refs::Refs, Error> {
+
        todo!()
+
    }
+

+
    fn project(&self) -> Result<Project, Error> {
+
        todo!()
+
    }
+
}
+

+
impl WriteRepository<'_> for MockRepository {
+
    fn fetch(&mut self, _url: &Url) -> Result<Vec<RefUpdate>, FetchError> {
+
        Ok(vec![])
+
    }
+

+
    fn raw(&self) -> &git2::Repository {
+
        todo!()
+
    }
+
}
added radicle-node/src/test/tests.rs
@@ -0,0 +1,530 @@
+
use std::io;
+
use std::sync::Arc;
+

+
use crossbeam_channel as chan;
+
use nakamoto_net as nakamoto;
+

+
use crate::collections::{HashMap, HashSet};
+
use crate::service::config::*;
+
use crate::service::message::*;
+
use crate::service::peer::*;
+
use crate::service::*;
+
use crate::storage::git::Storage;
+
use crate::storage::ReadStorage;
+
use crate::test::fixtures;
+
#[allow(unused)]
+
use crate::test::logger;
+
use crate::test::peer::Peer;
+
use crate::test::simulator;
+
use crate::test::simulator::{Peer as _, Simulation};
+
use crate::test::storage::MockStorage;
+
use crate::{assert_matches, Link, LocalTime};
+
use crate::{client, identity, rad, service, storage, test};
+

+
// NOTE
+
//
+
// If you wish to see the logs for a running test, simply add the following line to your test:
+
//
+
//      logger::init(log::Level::Debug);
+
//
+
// You may then run the test with eg. `cargo test -- --nocapture` to always show output.
+

+
#[test]
+
fn test_outbound_connection() {
+
    let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
+
    let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
+
    let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
+

+
    alice.connect_to(&bob);
+
    alice.connect_to(&eve);
+

+
    let peers = alice
+
        .service
+
        .peers()
+
        .negotiated()
+
        .map(|(ip, _)| *ip)
+
        .collect::<Vec<_>>();
+

+
    assert!(peers.contains(&eve.ip));
+
    assert!(peers.contains(&bob.ip));
+
}
+

+
#[test]
+
fn test_inbound_connection() {
+
    let mut alice = Peer::new("alice", [8, 8, 8, 8], MockStorage::empty());
+
    let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
+
    let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
+

+
    alice.connect_from(&bob);
+
    alice.connect_from(&eve);
+

+
    let peers = alice
+
        .service
+
        .peers()
+
        .negotiated()
+
        .map(|(ip, _)| *ip)
+
        .collect::<Vec<_>>();
+

+
    assert!(peers.contains(&eve.ip));
+
    assert!(peers.contains(&bob.ip));
+
}
+

+
#[test]
+
fn test_persistent_peer_connect() {
+
    let 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.addr(), eve.addr()],
+
        ..Config::default()
+
    };
+
    let mut alice = Peer::config(
+
        "alice",
+
        config,
+
        [7, 7, 7, 7],
+
        vec![],
+
        MockStorage::empty(),
+
        rng,
+
    );
+

+
    alice.initialize();
+

+
    let mut outbox = alice.outbox();
+
    assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == bob.addr());
+
    assert_matches!(outbox.next(), Some(Io::Connect(a)) if a == eve.addr());
+
    assert_matches!(outbox.next(), None);
+
}
+

+
#[test]
+
#[ignore]
+
fn test_wrong_peer_version() {
+
    // TODO
+
}
+

+
#[test]
+
fn test_handshake_invalid_timestamp() {
+
    let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
+
    let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
+
    let time_delta = MAX_TIME_DELTA.as_secs() + 1;
+
    let local = std::net::SocketAddr::new(bob.ip, bob.rng.u16(..));
+

+
    alice.initialize();
+
    alice.connected(bob.addr(), &local, Link::Inbound);
+
    alice.receive(
+
        &bob.addr(),
+
        Message::init(
+
            bob.node_id(),
+
            alice.timestamp() - time_delta,
+
            vec![],
+
            bob.git_url(),
+
        ),
+
    );
+
    assert_matches!(alice.outbox().next(), Some(Io::Disconnect(addr, _)) if addr == bob.addr());
+
}
+

+
#[test]
+
#[ignore]
+
fn test_wrong_peer_magic() {
+
    // TODO
+
}
+

+
#[test]
+
fn test_inventory_sync() {
+
    let tmp = tempfile::tempdir().unwrap();
+
    let mut alice = Peer::new(
+
        "alice",
+
        [7, 7, 7, 7],
+
        Storage::open(tmp.path().join("alice")).unwrap(),
+
    );
+
    let bob_storage = fixtures::storage(tmp.path().join("bob"));
+
    let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
+
    let now = LocalTime::now().as_secs();
+
    let projs = bob.storage().inventory().unwrap();
+

+
    alice.connect_to(&bob);
+
    alice.receive(
+
        &bob.addr(),
+
        Message::inventory(
+
            InventoryAnnouncement {
+
                inventory: projs.clone(),
+
                timestamp: now,
+
            },
+
            bob.signer(),
+
        ),
+
    );
+

+
    for proj in &projs {
+
        let seeds = alice.routing().get(proj).unwrap();
+
        assert!(seeds.contains(&bob.node_id()));
+
    }
+

+
    let a = alice
+
        .storage()
+
        .inventory()
+
        .unwrap()
+
        .into_iter()
+
        .collect::<HashSet<_>>();
+
    let b = projs.into_iter().collect::<HashSet<_>>();
+

+
    assert_eq!(a, b);
+
}
+

+
#[test]
+
fn test_tracking() {
+
    let mut alice = Peer::config(
+
        "alice",
+
        Config {
+
            project_tracking: ProjectTracking::Allowed(HashSet::default()),
+
            ..Config::default()
+
        },
+
        [7, 7, 7, 7],
+
        vec![],
+
        MockStorage::empty(),
+
        fastrand::Rng::new(),
+
    );
+
    let proj_id: identity::Id = test::arbitrary::gen(1);
+

+
    let (sender, receiver) = chan::bounded(1);
+
    alice.command(Command::Track(proj_id.clone(), sender));
+
    let policy_change = receiver
+
        .recv()
+
        .map_err(client::handle::Error::from)
+
        .unwrap();
+
    assert!(policy_change);
+
    assert!(alice.config().is_tracking(&proj_id));
+

+
    let (sender, receiver) = chan::bounded(1);
+
    alice.command(Command::Untrack(proj_id.clone(), sender));
+
    let policy_change = receiver
+
        .recv()
+
        .map_err(client::handle::Error::from)
+
        .unwrap();
+
    assert!(policy_change);
+
    assert!(!alice.config().is_tracking(&proj_id));
+
}
+

+
#[test]
+
fn test_inventory_relay_bad_timestamp() {
+
    let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
+
    let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
+
    let two_hours = 3600 * 2;
+
    let timestamp = alice.local_time.as_secs() - two_hours;
+

+
    alice.connect_to(&bob);
+
    alice.receive(
+
        &bob.addr(),
+
        Message::inventory(
+
            InventoryAnnouncement {
+
                inventory: vec![],
+
                timestamp,
+
            },
+
            bob.signer(),
+
        ),
+
    );
+
    assert_matches!(
+
        alice.outbox().next(),
+
        Some(Io::Disconnect(addr, DisconnectReason::Error(PeerError::InvalidTimestamp(t))))
+
        if addr == bob.addr() && t == timestamp
+
    );
+
}
+

+
#[test]
+
fn test_inventory_relay() {
+
    // Topology is eve <-> alice <-> bob
+
    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 inv = vec![];
+
    let now = LocalTime::now().as_secs();
+

+
    // Inventory from Bob relayed to Eve.
+
    alice.connect_to(&bob);
+
    alice.connect_from(&eve);
+
    alice.receive(
+
        &bob.addr(),
+
        Message::inventory(
+
            InventoryAnnouncement {
+
                inventory: inv.clone(),
+
                timestamp: now,
+
            },
+
            bob.signer(),
+
        ),
+
    );
+
    assert_matches!(
+
        alice.messages(&eve.addr()).next(),
+
        Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement { timestamp, .. }, .. })
+
        if node == bob.node_id() && timestamp == now
+
    );
+
    assert_matches!(
+
        alice.messages(&bob.addr()).next(),
+
        None,
+
        "The inventory is not sent back to Bob"
+
    );
+

+
    alice.receive(
+
        &bob.addr(),
+
        Message::inventory(
+
            InventoryAnnouncement {
+
                inventory: inv.clone(),
+
                timestamp: now,
+
            },
+
            bob.signer(),
+
        ),
+
    );
+
    assert_matches!(
+
        alice.messages(&eve.addr()).next(),
+
        None,
+
        "Sending the same inventory again doesn't trigger a relay"
+
    );
+

+
    alice.receive(
+
        &bob.addr(),
+
        Message::inventory(
+
            InventoryAnnouncement {
+
                inventory: inv.clone(),
+
                timestamp: now + 1,
+
            },
+
            bob.signer(),
+
        ),
+
    );
+
    assert_matches!(
+
        alice.messages(&eve.addr()).next(),
+
        Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement{ timestamp, .. }, .. })
+
        if node == bob.node_id() && timestamp == now + 1,
+
        "Sending a new inventory does trigger the relay"
+
    );
+

+
    // Inventory from Eve relayed to Bob.
+
    alice.receive(
+
        &eve.addr(),
+
        Message::inventory(
+
            InventoryAnnouncement {
+
                inventory: inv,
+
                timestamp: now,
+
            },
+
            eve.signer(),
+
        ),
+
    );
+
    assert_matches!(
+
        alice.messages(&bob.addr()).next(),
+
        Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement { timestamp, .. }, .. })
+
        if node == eve.node_id() && timestamp == now
+
    );
+
}
+

+
#[test]
+
fn test_persistent_peer_reconnect() {
+
    let mut bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
+
    let mut eve = Peer::new("eve", [9, 9, 9, 9], MockStorage::empty());
+
    let mut alice = Peer::config(
+
        "alice",
+
        Config {
+
            connect: vec![bob.addr(), eve.addr()],
+
            ..Config::default()
+
        },
+
        [7, 7, 7, 7],
+
        vec![],
+
        MockStorage::empty(),
+
        fastrand::Rng::new(),
+
    );
+

+
    let mut sim = Simulation::new(
+
        LocalTime::now(),
+
        alice.rng.clone(),
+
        simulator::Options::default(),
+
    )
+
    .initialize([&mut alice, &mut bob, &mut eve]);
+

+
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
+

+
    let ips = alice
+
        .peers()
+
        .negotiated()
+
        .map(|(ip, _)| *ip)
+
        .collect::<Vec<_>>();
+
    assert!(ips.contains(&bob.ip));
+
    assert!(ips.contains(&eve.ip));
+

+
    // ... Negotiated ...
+
    //
+
    // Now let's disconnect a peer.
+

+
    // A transient error such as this will cause Alice to attempt a reconnection.
+
    let error = Arc::new(io::Error::from(io::ErrorKind::ConnectionReset));
+

+
    // A non-transient disconnect, such as one requested by the user will not trigger
+
    // a reconnection.
+
    alice.disconnected(
+
        &eve.addr(),
+
        nakamoto::DisconnectReason::DialError(error.clone()),
+
    );
+
    assert_matches!(alice.outbox().next(), None);
+

+
    for _ in 0..MAX_CONNECTION_ATTEMPTS {
+
        alice.disconnected(
+
            &bob.addr(),
+
            nakamoto::DisconnectReason::ConnectionError(error.clone()),
+
        );
+
        assert_matches!(alice.outbox().next(), Some(Io::Connect(a)) if a == bob.addr());
+
        assert_matches!(alice.outbox().next(), None);
+

+
        alice.attempted(&bob.addr());
+
    }
+

+
    // After the max connection attempts, a disconnect doesn't trigger a reconnect.
+
    alice.disconnected(
+
        &bob.addr(),
+
        nakamoto::DisconnectReason::ConnectionError(error),
+
    );
+
    assert_matches!(alice.outbox().next(), None);
+
}
+

+
#[test]
+
fn test_push_and_pull() {
+
    logger::init(log::Level::Debug);
+

+
    let tempdir = tempfile::tempdir().unwrap();
+

+
    let storage_alice = Storage::open(tempdir.path().join("alice").join("storage")).unwrap();
+
    let (repo, _) = fixtures::repository(tempdir.path().join("working"));
+
    let mut alice = Peer::new("alice", [7, 7, 7, 7], storage_alice);
+

+
    let storage_bob = Storage::open(tempdir.path().join("bob").join("storage")).unwrap();
+
    let mut bob = Peer::new("bob", [8, 8, 8, 8], storage_bob);
+

+
    let storage_eve = Storage::open(tempdir.path().join("eve").join("storage")).unwrap();
+
    let mut eve = Peer::new("eve", [9, 9, 9, 9], storage_eve);
+

+
    // Alice and Bob connect to Eve.
+
    alice.command(service::Command::Connect(eve.addr()));
+
    bob.command(service::Command::Connect(eve.addr()));
+

+
    let mut sim = Simulation::new(
+
        LocalTime::now(),
+
        alice.rng.clone(),
+
        simulator::Options::default(),
+
    )
+
    .initialize([&mut alice, &mut bob, &mut eve]);
+

+
    // Here we expect Alice to connect to Eve.
+
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
+

+
    // Alice creates a new project.
+
    let (proj_id, _) = rad::init(
+
        &repo,
+
        "alice",
+
        "alice's repo",
+
        storage::BranchName::from("master"),
+
        alice.signer(),
+
        alice.storage(),
+
    )
+
    .unwrap();
+

+
    // Bob tracks Alice's project.
+
    let (sender, _) = chan::bounded(1);
+
    bob.command(service::Command::Track(proj_id.clone(), sender));
+

+
    // Eve tracks Alice's project.
+
    let (sender, _) = chan::bounded(1);
+
    eve.command(service::Command::Track(proj_id.clone(), sender));
+

+
    // Neither of them have it in the beginning.
+
    assert!(eve.get(&proj_id).unwrap().is_none());
+
    assert!(bob.get(&proj_id).unwrap().is_none());
+

+
    // Alice announces her refs.
+
    // We now expect Eve to fetch Alice's project from Alice.
+
    // Then we expect Bob to fetch Alice's project from Eve.
+
    alice.command(service::Command::AnnounceRefs(proj_id.clone()));
+
    sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
+

+
    assert!(eve
+
        .storage()
+
        .get(&alice.node_id(), &proj_id)
+
        .unwrap()
+
        .is_some());
+
    assert!(bob
+
        .storage()
+
        .get(&alice.node_id(), &proj_id)
+
        .unwrap()
+
        .is_some());
+
    assert_matches!(
+
        sim.events(&bob.ip).next(),
+
        Some(service::Event::RefsFetched { from, .. })
+
        if from == eve.git_url(),
+
        "Bob fetched from Eve"
+
    );
+
}
+

+
#[test]
+
fn prop_inventory_exchange_dense() {
+
    fn property(alice_inv: MockStorage, bob_inv: MockStorage, eve_inv: MockStorage) {
+
        let rng = fastrand::Rng::new();
+
        let alice = Peer::new("alice", [7, 7, 7, 7], alice_inv.clone());
+
        let mut bob = Peer::new("bob", [8, 8, 8, 8], bob_inv.clone());
+
        let mut eve = Peer::new("eve", [9, 9, 9, 9], eve_inv.clone());
+
        let mut routing = Routing::with_hasher(rng.clone().into());
+

+
        for (inv, peer) in &[
+
            (alice_inv.inventory, alice.node_id()),
+
            (bob_inv.inventory, bob.node_id()),
+
            (eve_inv.inventory, eve.node_id()),
+
        ] {
+
            for proj in inv {
+
                routing
+
                    .entry(proj.id.clone())
+
                    .or_insert_with(|| HashSet::with_hasher(rng.clone().into()))
+
                    .insert(*peer);
+
            }
+
        }
+

+
        // Fully-connected.
+
        bob.command(Command::Connect(alice.addr()));
+
        bob.command(Command::Connect(eve.addr()));
+
        eve.command(Command::Connect(alice.addr()));
+
        eve.command(Command::Connect(bob.addr()));
+

+
        let mut peers: HashMap<_, _> = [
+
            (alice.node_id(), alice),
+
            (bob.node_id(), bob),
+
            (eve.node_id(), eve),
+
        ]
+
        .into_iter()
+
        .collect();
+
        let mut simulator = Simulation::new(LocalTime::now(), rng, simulator::Options::default())
+
            .initialize(peers.values_mut());
+

+
        simulator.run_while(peers.values_mut(), |s| !s.is_settled());
+

+
        for (proj_id, remotes) in &routing {
+
            for peer in peers.values() {
+
                let lookup = peer.lookup(proj_id);
+

+
                if lookup.local.is_some() {
+
                    peer.get(proj_id)
+
                        .expect("There are no errors querying storage")
+
                        .expect("The project is available locally");
+
                } else {
+
                    for remote in &lookup.remote {
+
                        peers[remote]
+
                            .get(proj_id)
+
                            .expect("There are no errors querying storage")
+
                            .expect("The project is available remotely");
+
                    }
+
                    assert!(
+
                        !lookup.remote.is_empty(),
+
                        "There are remote locations for the project"
+
                    );
+
                    assert_eq!(
+
                        &lookup.remote.into_iter().collect::<HashSet<_>>(),
+
                        remotes,
+
                        "The remotes match the global routing table"
+
                    );
+
                }
+
            }
+
        }
+
    }
+
    quickcheck::QuickCheck::new()
+
        .gen(quickcheck::Gen::new(8))
+
        .quickcheck(property as fn(MockStorage, MockStorage, MockStorage));
+
}
added radicle-node/src/transport.rs
@@ -0,0 +1,107 @@
+
use std::net;
+
use std::ops::{Deref, DerefMut};
+

+
use nakamoto::LocalTime;
+
use nakamoto_net as nakamoto;
+
use nakamoto_net::{Io, Link};
+

+
use crate::address_book;
+
use crate::collections::HashMap;
+
use crate::crypto;
+
use crate::service::{Command, DisconnectReason, Event, Service};
+
use crate::storage::WriteStorage;
+
use crate::wire::Wire;
+

+
#[derive(Debug)]
+
struct Peer {
+
    addr: net::SocketAddr,
+
}
+

+
#[derive(Debug)]
+
pub struct Transport<S, T, G> {
+
    peers: HashMap<net::IpAddr, Peer>,
+
    inner: Wire<S, T, G>,
+
}
+

+
impl<S, T, G> Transport<S, T, G> {
+
    pub fn new(inner: Wire<S, T, G>) -> Self {
+
        Self {
+
            peers: HashMap::default(),
+
            inner,
+
        }
+
    }
+
}
+

+
impl<'r, S, T, G> nakamoto::Protocol for Transport<S, T, G>
+
where
+
    T: WriteStorage<'r> + 'static,
+
    S: address_book::Store,
+
    G: crypto::Signer,
+
{
+
    type Event = Event;
+
    type Command = Command;
+
    type DisconnectReason = DisconnectReason;
+

+
    fn initialize(&mut self, time: LocalTime) {
+
        self.inner.initialize(time)
+
    }
+

+
    fn tick(&mut self, now: nakamoto::LocalTime) {
+
        self.inner.tick(now)
+
    }
+

+
    fn wake(&mut self) {
+
        self.inner.wake()
+
    }
+

+
    fn command(&mut self, cmd: Self::Command) {
+
        self.inner.command(cmd)
+
    }
+

+
    fn attempted(&mut self, addr: &std::net::SocketAddr) {
+
        self.inner.attempted(addr)
+
    }
+

+
    fn connected(
+
        &mut self,
+
        addr: std::net::SocketAddr,
+
        local_addr: &std::net::SocketAddr,
+
        link: Link,
+
    ) {
+
        self.inner.connected(addr, local_addr, link)
+
    }
+

+
    fn disconnected(
+
        &mut self,
+
        addr: &std::net::SocketAddr,
+
        reason: nakamoto::DisconnectReason<Self::DisconnectReason>,
+
    ) {
+
        self.inner.disconnected(addr, reason)
+
    }
+

+
    fn received_bytes(&mut self, addr: &std::net::SocketAddr, bytes: &[u8]) {
+
        self.inner.received_bytes(addr, bytes)
+
    }
+
}
+

+
impl<S, T, G> Iterator for Transport<S, T, G> {
+
    type Item = Io<Event, DisconnectReason>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner.next()
+
    }
+
}
+

+
impl<S, T, G> Deref for Transport<S, T, G> {
+
    type Target = Service<S, T, G>;
+

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

+
impl<S, T, G> DerefMut for Transport<S, T, G> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.inner
+
    }
+
}
added radicle-node/src/wire.rs
@@ -0,0 +1,642 @@
+
pub mod message;
+

+
use std::collections::{BTreeMap, HashMap};
+
use std::convert::TryFrom;
+
use std::net::IpAddr;
+
use std::ops::{Deref, DerefMut};
+
use std::string::FromUtf8Error;
+
use std::{io, mem};
+

+
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
+
use nakamoto_net as nakamoto;
+
use nakamoto_net::Link;
+

+
use crate::address_book;
+
use crate::crypto::{PublicKey, Signature, Signer};
+
use crate::decoder::Decoder;
+
use crate::git;
+
use crate::git::fmt;
+
use crate::hash::Digest;
+
use crate::identity::Id;
+
use crate::service;
+
use crate::service::filter;
+
use crate::storage::refs::Refs;
+
use crate::storage::WriteStorage;
+

+
/// The default type we use to represent sizes.
+
/// Four bytes is more than enough for anything sent over the wire.
+
/// Note that in certain cases, we may use only one or two byte types.
+
pub type Size = u32;
+

+
#[derive(thiserror::Error, Debug)]
+
pub enum Error {
+
    #[error("i/o: {0}")]
+
    Io(#[from] io::Error),
+
    #[error("UTF-8 error: {0}")]
+
    FromUtf8(#[from] FromUtf8Error),
+
    #[error("invalid size: expected {expected}, got {actual}")]
+
    InvalidSize { expected: usize, actual: usize },
+
    #[error("invalid filter size: {0}")]
+
    InvalidFilterSize(usize),
+
    #[error(transparent)]
+
    InvalidRefName(#[from] fmt::Error),
+
    #[error("invalid git url `{url}`: {error}")]
+
    InvalidGitUrl {
+
        url: String,
+
        error: git::url::parse::Error,
+
    },
+
    #[error("unknown address type `{0}`")]
+
    UnknownAddressType(u8),
+
    #[error("unknown message type `{0}`")]
+
    UnknownMessageType(u16),
+
}
+

+
impl Error {
+
    /// Whether we've reached the end of file. This will be true when we fail to decode
+
    /// a message because there's not enough data in the stream.
+
    pub fn is_eof(&self) -> bool {
+
        matches!(self, Self::Io(err) if err.kind() == io::ErrorKind::UnexpectedEof)
+
    }
+
}
+

+
/// Things that can be encoded as binary.
+
pub trait Encode {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error>;
+
}
+

+
/// Things that can be decoded from binary.
+
pub trait Decode: Sized {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error>;
+
}
+

+
/// Encode an object into a vector.
+
pub fn serialize<T: Encode + ?Sized>(data: &T) -> Vec<u8> {
+
    let mut buffer = Vec::new();
+
    let len = data
+
        .encode(&mut buffer)
+
        .expect("in-memory writes don't error");
+

+
    debug_assert_eq!(len, buffer.len());
+

+
    buffer
+
}
+

+
/// Decode an object from a vector.
+
pub fn deserialize<T: Decode>(data: &[u8]) -> Result<T, Error> {
+
    let mut cursor = io::Cursor::new(data);
+

+
    T::decode(&mut cursor)
+
}
+

+
impl Encode for u8 {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        writer.write_u8(*self)?;
+

+
        Ok(mem::size_of::<Self>())
+
    }
+
}
+

+
impl Encode for u16 {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        writer.write_u16::<NetworkEndian>(*self)?;
+

+
        Ok(mem::size_of::<Self>())
+
    }
+
}
+

+
impl Encode for u32 {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        writer.write_u32::<NetworkEndian>(*self)?;
+

+
        Ok(mem::size_of::<Self>())
+
    }
+
}
+

+
impl Encode for u64 {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        writer.write_u64::<NetworkEndian>(*self)?;
+

+
        Ok(mem::size_of::<Self>())
+
    }
+
}
+

+
impl Encode for usize {
+
    /// We encode this type to a [`u32`], since there's no need to send larger messages
+
    /// over the network.
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        assert!(
+
            *self <= u32::MAX as usize,
+
            "Cannot encode sizes larger than {}",
+
            u32::MAX
+
        );
+
        writer.write_u32::<NetworkEndian>(*self as u32)?;
+

+
        Ok(mem::size_of::<u32>())
+
    }
+
}
+

+
impl Encode for PublicKey {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.deref().encode(writer)
+
    }
+
}
+

+
impl<const T: usize> Encode for &[u8; T] {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        writer.write_all(*self)?;
+

+
        Ok(mem::size_of::<Self>())
+
    }
+
}
+

+
impl<const T: usize> Encode for [u8; T] {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        writer.write_all(self)?;
+

+
        Ok(mem::size_of::<Self>())
+
    }
+
}
+

+
impl<T> Encode for &[T]
+
where
+
    T: Encode,
+
{
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = (self.len() as Size).encode(writer)?;
+

+
        for item in self.iter() {
+
            n += item.encode(writer)?;
+
        }
+
        Ok(n)
+
    }
+
}
+

+
impl Encode for &str {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        assert!(self.len() <= u8::MAX as usize);
+

+
        let n = (self.len() as u8).encode(writer)?;
+
        let bytes = self.as_bytes();
+

+
        // Nb. Don't use the [`Encode`] instance here for &[u8], because we are prefixing the
+
        // length ourselves.
+
        writer.write_all(bytes)?;
+

+
        Ok(n + bytes.len())
+
    }
+
}
+

+
impl Encode for String {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.as_str().encode(writer)
+
    }
+
}
+

+
impl Encode for git::Url {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.to_string().encode(writer)
+
    }
+
}
+

+
impl Encode for Digest {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.as_ref().encode(writer)
+
    }
+
}
+

+
impl Encode for Id {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.deref().encode(writer)
+
    }
+
}
+

+
impl Encode for Refs {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = self.len().encode(writer)?;
+

+
        for (name, oid) in self.iter() {
+
            n += name.as_str().encode(writer)?;
+
            n += oid.encode(writer)?;
+
        }
+
        Ok(n)
+
    }
+
}
+

+
impl Encode for Signature {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.deref().encode(writer)
+
    }
+
}
+

+
impl Encode for git::Oid {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        // Nb. We use length-encoding here to support future SHA-2 object ids.
+
        self.as_bytes().encode(writer)
+
    }
+
}
+

+
////////////////////////////////////////////////////////////////////////////////
+

+
impl Decode for PublicKey {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let buf: [u8; 32] = Decode::decode(reader)?;
+

+
        PublicKey::try_from(buf)
+
            .map_err(|e| Error::Io(io::Error::new(io::ErrorKind::InvalidInput, e.to_string())))
+
    }
+
}
+

+
impl Decode for Refs {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let len = usize::decode(reader)?;
+
        let mut refs = BTreeMap::new();
+

+
        for _ in 0..len {
+
            let name = String::decode(reader)?;
+
            let name = git::RefString::try_from(name).map_err(Error::from)?;
+
            let oid = git::Oid::decode(reader)?;
+

+
            refs.insert(name, oid);
+
        }
+
        Ok(refs.into())
+
    }
+
}
+

+
impl Decode for git::Oid {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let len = usize::decode(reader)?;
+
        #[allow(non_upper_case_globals)]
+
        const expected: usize = mem::size_of::<git2::Oid>();
+

+
        if len != expected {
+
            return Err(Error::InvalidSize {
+
                expected,
+
                actual: len,
+
            });
+
        }
+

+
        let buf: [u8; expected] = Decode::decode(reader)?;
+
        let oid = git2::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
+
        let oid = git::Oid::from(oid);
+

+
        Ok(oid)
+
    }
+
}
+

+
impl Decode for Signature {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let bytes: [u8; 64] = Decode::decode(reader)?;
+

+
        Ok(Signature::from(bytes))
+
    }
+
}
+

+
impl Decode for u8 {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        reader.read_u8().map_err(Error::from)
+
    }
+
}
+

+
impl Decode for u16 {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        reader.read_u16::<NetworkEndian>().map_err(Error::from)
+
    }
+
}
+

+
impl Decode for u32 {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        reader.read_u32::<NetworkEndian>().map_err(Error::from)
+
    }
+
}
+

+
impl Decode for u64 {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        reader.read_u64::<NetworkEndian>().map_err(Error::from)
+
    }
+
}
+

+
impl Decode for usize {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let size: usize = u32::decode(reader)?
+
            .try_into()
+
            .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
+

+
        Ok(size)
+
    }
+
}
+

+
impl<const N: usize> Decode for [u8; N] {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let mut ary = [0; N];
+
        reader.read_exact(&mut ary)?;
+

+
        Ok(ary)
+
    }
+
}
+

+
impl<T> Decode for Vec<T>
+
where
+
    T: Decode,
+
{
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let len: Size = Size::decode(reader)?;
+
        let mut vec = Vec::with_capacity(len as usize);
+

+
        for _ in 0..len {
+
            let item = T::decode(reader)?;
+
            vec.push(item);
+
        }
+
        Ok(vec)
+
    }
+
}
+

+
impl Decode for String {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let len = u8::decode(reader)?;
+
        let mut bytes = vec![0; len as usize];
+

+
        reader.read_exact(&mut bytes)?;
+

+
        let string = String::from_utf8(bytes)?;
+

+
        Ok(string)
+
    }
+
}
+

+
impl Decode for git::Url {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let url = String::decode(reader)?;
+
        let url = Self::from_bytes(url.as_bytes())
+
            .map_err(|error| Error::InvalidGitUrl { url, error })?;
+

+
        Ok(url)
+
    }
+
}
+

+
impl Decode for Id {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let oid: git::Oid = Decode::decode(reader)?;
+

+
        Ok(Self::from(oid))
+
    }
+
}
+

+
impl Decode for Digest {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let bytes: [u8; 32] = Decode::decode(reader)?;
+

+
        Ok(Self::from(bytes))
+
    }
+
}
+

+
impl Encode for filter::Filter {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = 0;
+

+
        n += self.deref().as_bytes().encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl Decode for filter::Filter {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let size: Size = Decode::decode(reader)?;
+
        if size as usize != filter::FILTER_SIZE {
+
            return Err(Error::InvalidFilterSize(size as usize));
+
        }
+
        let bytes: [u8; filter::FILTER_SIZE] = Decode::decode(reader)?;
+
        let bf = filter::BloomFilter::from(Vec::from(bytes));
+

+
        debug_assert_eq!(bf.hashes(), filter::FILTER_HASHES);
+

+
        Ok(Self::from(bf))
+
    }
+
}
+

+
#[derive(Debug)]
+
pub struct Wire<S, T, G> {
+
    inboxes: HashMap<IpAddr, Decoder>,
+
    inner: service::Service<S, T, G>,
+
}
+

+
impl<S, T, G> Wire<S, T, G> {
+
    pub fn new(inner: service::Service<S, T, G>) -> Self {
+
        Self {
+
            inboxes: HashMap::new(),
+
            inner,
+
        }
+
    }
+
}
+

+
impl<'r, S, T, G> Wire<S, T, G>
+
where
+
    S: address_book::Store,
+
    T: WriteStorage<'r> + 'static,
+
    G: Signer,
+
{
+
    pub fn connected(
+
        &mut self,
+
        addr: std::net::SocketAddr,
+
        local_addr: &std::net::SocketAddr,
+
        link: Link,
+
    ) {
+
        self.inboxes.insert(addr.ip(), Decoder::new(256));
+
        self.inner.connected(addr, local_addr, link)
+
    }
+

+
    pub fn disconnected(
+
        &mut self,
+
        addr: &std::net::SocketAddr,
+
        reason: nakamoto::DisconnectReason<service::DisconnectReason>,
+
    ) {
+
        self.inboxes.remove(&addr.ip());
+
        self.inner.disconnected(addr, reason)
+
    }
+

+
    pub fn received_bytes(&mut self, addr: &std::net::SocketAddr, bytes: &[u8]) {
+
        let peer_ip = addr.ip();
+

+
        if let Some(inbox) = self.inboxes.get_mut(&peer_ip) {
+
            inbox.input(bytes);
+

+
            loop {
+
                match inbox.decode_next() {
+
                    Ok(Some(msg)) => self.inner.received_message(addr, msg),
+
                    Ok(None) => break,
+

+
                    Err(err) => {
+
                        // TODO: Disconnect peer.
+
                        log::error!("Invalid message received from {}: {}", peer_ip, err);
+

+
                        return;
+
                    }
+
                }
+
            }
+
        } else {
+
            log::debug!("Received message from unknown peer {}", peer_ip);
+
        }
+
    }
+
}
+

+
impl<S, T, G> Iterator for Wire<S, T, G> {
+
    type Item = nakamoto::Io<service::Event, service::DisconnectReason>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        match self.inner.next() {
+
            Some(service::Io::Write(addr, msgs)) => {
+
                let mut buf = Vec::new();
+
                for msg in msgs {
+
                    log::debug!("Write {:?} to {}", &msg, addr.ip());
+

+
                    msg.encode(&mut buf)
+
                        .expect("writing to an in-memory buffer doesn't fail");
+
                }
+
                Some(nakamoto::Io::Write(addr, buf))
+
            }
+
            Some(service::Io::Event(e)) => Some(nakamoto::Io::Event(e)),
+
            Some(service::Io::Connect(a)) => Some(nakamoto::Io::Connect(a)),
+
            Some(service::Io::Disconnect(a, r)) => Some(nakamoto::Io::Disconnect(a, r)),
+
            Some(service::Io::Wakeup(d)) => Some(nakamoto::Io::Wakeup(d)),
+

+
            None => None,
+
        }
+
    }
+
}
+

+
impl<S, T, G> Deref for Wire<S, T, G> {
+
    type Target = service::Service<S, T, G>;
+

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

+
impl<S, T, G> DerefMut for Wire<S, T, G> {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.inner
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    use crate::crypto::Unverified;
+
    use crate::storage::refs::SignedRefs;
+
    use crate::test::arbitrary;
+

+
    #[quickcheck]
+
    fn prop_u8(input: u8) {
+
        assert_eq!(deserialize::<u8>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_u16(input: u16) {
+
        assert_eq!(deserialize::<u16>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_u32(input: u32) {
+
        assert_eq!(deserialize::<u32>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_u64(input: u64) {
+
        assert_eq!(deserialize::<u64>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_usize(input: usize) -> quickcheck::TestResult {
+
        if input > u32::MAX as usize {
+
            return quickcheck::TestResult::discard();
+
        }
+
        assert_eq!(deserialize::<usize>(&serialize(&input)).unwrap(), input);
+

+
        quickcheck::TestResult::passed()
+
    }
+

+
    #[quickcheck]
+
    fn prop_string(input: String) -> quickcheck::TestResult {
+
        if input.len() > u8::MAX as usize {
+
            return quickcheck::TestResult::discard();
+
        }
+
        assert_eq!(deserialize::<String>(&serialize(&input)).unwrap(), input);
+

+
        quickcheck::TestResult::passed()
+
    }
+

+
    #[quickcheck]
+
    fn prop_vec(input: Vec<String>) {
+
        assert_eq!(
+
            deserialize::<Vec<String>>(&serialize(&input.as_slice())).unwrap(),
+
            input
+
        );
+
    }
+

+
    #[quickcheck]
+
    fn prop_pubkey(input: PublicKey) {
+
        assert_eq!(deserialize::<PublicKey>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_id(input: Id) {
+
        assert_eq!(deserialize::<Id>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_digest(input: Digest) {
+
        assert_eq!(deserialize::<Digest>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_refs(input: Refs) {
+
        assert_eq!(deserialize::<Refs>(&serialize(&input)).unwrap(), input);
+
    }
+

+
    #[quickcheck]
+
    fn prop_signature(input: arbitrary::ByteArray<64>) {
+
        let signature = Signature::from(input.into_inner());
+

+
        assert_eq!(
+
            deserialize::<Signature>(&serialize(&signature)).unwrap(),
+
            signature
+
        );
+
    }
+

+
    #[quickcheck]
+
    fn prop_oid(input: arbitrary::ByteArray<20>) {
+
        let oid = git::Oid::try_from(input.into_inner().as_slice()).unwrap();
+

+
        assert_eq!(deserialize::<git::Oid>(&serialize(&oid)).unwrap(), oid);
+
    }
+

+
    #[quickcheck]
+
    fn prop_signed_refs(input: SignedRefs<Unverified>) {
+
        assert_eq!(
+
            deserialize::<SignedRefs<Unverified>>(&serialize(&input)).unwrap(),
+
            input
+
        );
+
    }
+

+
    #[test]
+
    fn test_string() {
+
        assert_eq!(
+
            serialize(&String::from("hello")),
+
            vec![5, b'h', b'e', b'l', b'l', b'o']
+
        );
+
    }
+

+
    #[test]
+
    fn test_git_url() {
+
        let url = git::Url {
+
            scheme: git::url::Scheme::Https,
+
            path: "/git".to_owned().into(),
+
            host: Some("seed.radicle.xyz".to_owned()),
+
            port: Some(8888),
+
            ..git::Url::default()
+
        };
+
        assert_eq!(deserialize::<git::Url>(&serialize(&url)).unwrap(), url);
+
    }
+
}
added radicle-node/src/wire/message.rs
@@ -0,0 +1,377 @@
+
use std::{io, net};
+

+
use byteorder::{NetworkEndian, ReadBytesExt};
+

+
use crate::git;
+
use crate::prelude::*;
+
use crate::service::message::*;
+
use crate::wire;
+

+
/// Message type.
+
#[repr(u16)]
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+
pub enum MessageType {
+
    Initialize = 0,
+
    NodeAnnouncement = 2,
+
    InventoryAnnouncement = 4,
+
    RefsAnnouncement = 6,
+
    Subscribe = 8,
+
}
+

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

+
impl TryFrom<u16> for MessageType {
+
    type Error = u16;
+

+
    fn try_from(other: u16) -> Result<Self, Self::Error> {
+
        match other {
+
            0 => Ok(MessageType::Initialize),
+
            2 => Ok(MessageType::NodeAnnouncement),
+
            4 => Ok(MessageType::InventoryAnnouncement),
+
            6 => Ok(MessageType::RefsAnnouncement),
+
            8 => Ok(MessageType::Subscribe),
+
            _ => Err(other),
+
        }
+
    }
+
}
+

+
impl Message {
+
    pub fn type_id(&self) -> u16 {
+
        match self {
+
            Self::Initialize { .. } => MessageType::Initialize,
+
            Self::Subscribe { .. } => MessageType::Subscribe,
+
            Self::NodeAnnouncement { .. } => MessageType::NodeAnnouncement,
+
            Self::InventoryAnnouncement { .. } => MessageType::InventoryAnnouncement,
+
            Self::RefsAnnouncement { .. } => MessageType::RefsAnnouncement,
+
        }
+
        .into()
+
    }
+
}
+

+
/// 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 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),
+
        }
+
    }
+
}
+

+
impl wire::Encode for RefsAnnouncement {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = 0;
+

+
        n += self.id.encode(writer)?;
+
        n += self.refs.encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for RefsAnnouncement {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let id = Id::decode(reader)?;
+
        let refs = Refs::decode(reader)?;
+

+
        Ok(Self { id, refs })
+
    }
+
}
+

+
impl wire::Encode for InventoryAnnouncement {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let mut n = 0;
+

+
        n += self.inventory.as_slice().encode(writer)?;
+
        n += self.timestamp.encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for InventoryAnnouncement {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let inventory = Vec::<Id>::decode(reader)?;
+
        let timestamp = Timestamp::decode(reader)?;
+

+
        Ok(Self {
+
            inventory,
+
            timestamp,
+
        })
+
    }
+
}
+

+
impl wire::Encode for Message {
+
    fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
+
        let mut n = self.type_id().encode(writer)?;
+

+
        match self {
+
            Self::Initialize {
+
                id,
+
                timestamp,
+
                version,
+
                addrs,
+
                git,
+
            } => {
+
                n += id.encode(writer)?;
+
                n += timestamp.encode(writer)?;
+
                n += version.encode(writer)?;
+
                n += addrs.as_slice().encode(writer)?;
+
                n += git.encode(writer)?;
+
            }
+
            Self::Subscribe(Subscribe {
+
                filter,
+
                since,
+
                until,
+
            }) => {
+
                n += filter.encode(writer)?;
+
                n += since.encode(writer)?;
+
                n += until.encode(writer)?;
+
            }
+
            Self::RefsAnnouncement {
+
                node,
+
                message,
+
                signature,
+
            } => {
+
                n += node.encode(writer)?;
+
                n += message.encode(writer)?;
+
                n += signature.encode(writer)?;
+
            }
+
            Self::InventoryAnnouncement {
+
                node,
+
                message,
+
                signature,
+
            } => {
+
                n += node.encode(writer)?;
+
                n += message.encode(writer)?;
+
                n += signature.encode(writer)?;
+
            }
+
            Self::NodeAnnouncement {
+
                node,
+
                message,
+
                signature,
+
            } => {
+
                n += node.encode(writer)?;
+
                n += message.encode(writer)?;
+
                n += signature.encode(writer)?;
+
            }
+
        }
+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for Message {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let type_id = reader.read_u16::<NetworkEndian>()?;
+

+
        match MessageType::try_from(type_id) {
+
            Ok(MessageType::Initialize) => {
+
                let id = NodeId::decode(reader)?;
+
                let timestamp = Timestamp::decode(reader)?;
+
                let version = u32::decode(reader)?;
+
                let addrs = Vec::<Address>::decode(reader)?;
+
                let git = git::Url::decode(reader)?;
+

+
                Ok(Self::Initialize {
+
                    id,
+
                    timestamp,
+
                    version,
+
                    addrs,
+
                    git,
+
                })
+
            }
+
            Ok(MessageType::Subscribe) => {
+
                let filter = Filter::decode(reader)?;
+
                let since = Timestamp::decode(reader)?;
+
                let until = Timestamp::decode(reader)?;
+

+
                Ok(Self::Subscribe(Subscribe {
+
                    filter,
+
                    since,
+
                    until,
+
                }))
+
            }
+
            Ok(MessageType::NodeAnnouncement) => {
+
                let node = NodeId::decode(reader)?;
+
                let message = NodeAnnouncement::decode(reader)?;
+
                let signature = Signature::decode(reader)?;
+

+
                Ok(Self::NodeAnnouncement {
+
                    node,
+
                    message,
+
                    signature,
+
                })
+
            }
+
            Ok(MessageType::InventoryAnnouncement) => {
+
                let node = NodeId::decode(reader)?;
+
                let message = InventoryAnnouncement::decode(reader)?;
+
                let signature = Signature::decode(reader)?;
+

+
                Ok(Self::InventoryAnnouncement {
+
                    node,
+
                    message,
+
                    signature,
+
                })
+
            }
+
            Ok(MessageType::RefsAnnouncement) => {
+
                let node = NodeId::decode(reader)?;
+
                let message = RefsAnnouncement::decode(reader)?;
+
                let signature = Signature::decode(reader)?;
+

+
                Ok(Self::RefsAnnouncement {
+
                    node,
+
                    message,
+
                    signature,
+
                })
+
            }
+
            Err(other) => Err(wire::Error::UnknownMessageType(other)),
+
        }
+
    }
+
}
+

+
impl wire::Encode for Envelope {
+
    fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
+
        let mut n = 0;
+

+
        n += self.magic.encode(writer)?;
+
        n += self.msg.encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for Envelope {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let magic = u32::decode(reader)?;
+
        let msg = Message::decode(reader)?;
+

+
        Ok(Self { magic, msg })
+
    }
+
}
+

+
impl wire::Encode for Address {
+
    fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
+
        let mut n = 0;
+

+
        match self {
+
            Self::Ipv4 { ip, port } => {
+
                n += u8::from(AddressType::Ipv4).encode(writer)?;
+
                n += ip.octets().encode(writer)?;
+
                n += port.encode(writer)?;
+
            }
+
            Self::Ipv6 { ip, port } => {
+
                n += u8::from(AddressType::Ipv6).encode(writer)?;
+
                n += ip.octets().encode(writer)?;
+
                n += port.encode(writer)?;
+
            }
+
            Self::Hostname { .. } => todo!(),
+
            Self::Onion { .. } => todo!(),
+
        }
+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for Address {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        let addrtype = reader.read_u8()?;
+

+
        match AddressType::try_from(addrtype) {
+
            Ok(AddressType::Ipv4) => {
+
                let octets: [u8; 4] = wire::Decode::decode(reader)?;
+
                let ip = net::Ipv4Addr::from(octets);
+
                let port = u16::decode(reader)?;
+

+
                Ok(Self::Ipv4 { ip, port })
+
            }
+
            Ok(AddressType::Ipv6) => {
+
                let octets: [u8; 16] = wire::Decode::decode(reader)?;
+
                let ip = net::Ipv6Addr::from(octets);
+
                let port = u16::decode(reader)?;
+

+
                Ok(Self::Ipv6 { ip, port })
+
            }
+
            Ok(AddressType::Hostname) => {
+
                todo!();
+
            }
+
            Ok(AddressType::Onion) => {
+
                todo!();
+
            }
+
            Err(other) => Err(wire::Error::UnknownAddressType(other)),
+
        }
+
    }
+
}
+

+
#[cfg(test)]
+
mod tests {
+
    use super::*;
+
    use quickcheck_macros::quickcheck;
+

+
    use crate::decoder::Decoder;
+
    use crate::wire::{self, Encode};
+

+
    #[quickcheck]
+
    fn prop_message_encode_decode(message: Message) {
+
        assert_eq!(
+
            wire::deserialize::<Message>(&wire::serialize(&message)).unwrap(),
+
            message
+
        );
+
    }
+

+
    #[quickcheck]
+
    fn prop_envelope_encode_decode(envelope: Envelope) {
+
        assert_eq!(
+
            wire::deserialize::<Envelope>(&wire::serialize(&envelope)).unwrap(),
+
            envelope
+
        );
+
    }
+

+
    #[test]
+
    fn prop_envelope_decoder() {
+
        fn property(items: Vec<Envelope>) {
+
            let mut decoder = Decoder::<Envelope>::new(8);
+

+
            for item in &items {
+
                item.encode(&mut decoder).unwrap();
+
            }
+
            for item in items {
+
                assert_eq!(decoder.next().unwrap().unwrap(), item);
+
            }
+
        }
+

+
        quickcheck::QuickCheck::new()
+
            .gen(quickcheck::Gen::new(16))
+
            .quickcheck(property as fn(items: Vec<Envelope>));
+
    }
+

+
    #[quickcheck]
+
    fn prop_addr(addr: Address) {
+
        assert_eq!(
+
            wire::deserialize::<Address>(&wire::serialize(&addr)).unwrap(),
+
            addr
+
        );
+
    }
+
}