Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Implement binary encoding for messages
Alexis Sellier committed 3 years ago
commit b3d3077ccc23cb994fd84b2d148fd12ead2c2679
parent c7f78eafb6218dbb53e45f7cd327e0fdb42b1140
11 files changed +638 -88
modified node/Cargo.toml
@@ -9,6 +9,7 @@ edition = "2021"
anyhow = { version = "1" }
bs58 = { version = "0.4.0" }
ed25519-consensus = { version = "2.0.1" }
+
byteorder = { version = "1" }
chrono = { version = "0.4.0" }
colored = { version = "1.9.0" }
crossbeam-channel = { version = "0.5.6" }
modified node/src/crypto.rs
@@ -74,13 +74,13 @@ impl std::hash::Hash for PublicKey {

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

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

@@ -96,8 +96,16 @@ impl From<ed25519::VerificationKey> for PublicKey {
    }
}

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

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

impl PublicKey {
-
    pub fn encode(&self) -> String {
+
    pub fn to_human(&self) -> String {
        multibase::encode(multibase::Base::Base58Btc, &self.0)
    }
}
@@ -131,3 +139,18 @@ impl Deref for PublicKey {
        &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);
+
    }
+
}
modified node/src/git.rs
@@ -9,6 +9,7 @@ use crate::storage::refs::Refs;
use crate::storage::RemoteId;

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;
modified node/src/hash.rs
@@ -1,5 +1,4 @@
-
use std::fmt;
-
use std::str::FromStr;
+
use std::{convert::TryInto, fmt};

use serde::{Deserialize, Serialize};
use sha2::{
@@ -10,8 +9,6 @@ use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum DecodeError {
-
    #[error(transparent)]
-
    Multibase(#[from] multibase::Error),
    #[error("invalid digest length {0}")]
    InvalidLength(usize),
}
@@ -24,38 +21,17 @@ impl Digest {
    pub fn new(bytes: impl AsRef<[u8]>) -> Self {
        Self::from(Sha256::digest(bytes))
    }
-

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

-
    pub fn decode(s: &str) -> Result<Self, DecodeError> {
-
        let (_, bytes) = multibase::decode(s)?;
-
        let array = bytes
-
            .try_into()
-
            .map_err(|v: Vec<u8>| DecodeError::InvalidLength(v.len()))?;
-

-
        Ok(Self(array))
-
    }
}

-
impl FromStr for Digest {
-
    type Err = DecodeError;
-

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

-
impl AsRef<[u8]> for Digest {
-
    fn as_ref(&self) -> &[u8] {
+
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.encode())
+
        write!(f, "Hash({})", self)
    }
}

@@ -74,22 +50,20 @@ impl From<[u8; 32]> for Digest {
    }
}

-
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
-
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
-
        Self(array.into())
-
    }
-
}
+
impl TryFrom<&[u8]> for Digest {
+
    type Error = DecodeError;

-
#[cfg(test)]
-
mod tests {
-
    use super::*;
-
    use quickcheck_macros::quickcheck;
+
    fn try_from(bytes: &[u8]) -> Result<Self, DecodeError> {
+
        let bytes: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|_| DecodeError::InvalidLength(bytes.len()))?;

-
    #[quickcheck]
-
    fn prop_encode_decode(input: Digest) {
-
        let encoded = input.encode();
-
        let decoded = Digest::decode(&encoded).unwrap();
+
        Ok(bytes.into())
+
    }
+
}

-
        assert_eq!(input, decoded);
+
impl From<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>> for Digest {
+
    fn from(array: GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>) -> Self {
+
        Self(array.into())
    }
}
modified node/src/identity.rs
@@ -1,3 +1,4 @@
+
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{ffi::OsString, fmt, io, str::FromStr};

@@ -20,6 +21,8 @@ pub static IDENTITY_PATH: Lazy<&Path> = Lazy::new(|| Path::new("radicle.json"));
pub enum IdError {
    #[error("invalid digest: {0}")]
    InvalidDigest(#[from] hash::DecodeError),
+
    #[error(transparent)]
+
    Multibase(#[from] multibase::Error),
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -27,27 +30,34 @@ pub struct Id(hash::Digest);

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

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

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

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

+
        Ok(Self(array))
+
    }
}

impl FromStr for Id {
    type Err = IdError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        Ok(Self(hash::Digest::from_str(s)?))
+
        Self::from_human(s)
    }
}

@@ -66,6 +76,14 @@ impl From<hash::Digest> for Id {
    }
}

+
impl Deref for Id {
+
    type Target = hash::Digest;
+

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

impl serde::Serialize for Id {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
@@ -98,7 +116,7 @@ pub struct Did(crypto::PublicKey);

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

    pub fn decode(input: &str) -> Result<Self, DidError> {
@@ -219,9 +237,9 @@ mod test {
    }

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

        assert_eq!(input, decoded);
    }
modified node/src/protocol.rs
@@ -2,6 +2,7 @@
pub mod config;
pub mod message;
pub mod peer;
+
pub mod wire;

use std::ops::{Deref, DerefMut};
use std::{collections::VecDeque, fmt, io, net, net::IpAddr};
@@ -667,6 +668,16 @@ pub struct Context<S, T, G> {
    rng: Rng,
}

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

impl<'r, S, T, G> Context<S, T, G>
where
    T: storage::WriteStorage<'r>,
@@ -692,10 +703,6 @@ where
        }
    }

-
    pub(crate) fn id(&self) -> NodeId {
-
        *self.signer.public_key()
-
    }
-

    /// 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 {
modified node/src/protocol/message.rs
@@ -1,10 +1,12 @@
use std::net;

-
use git_url::Url;
+
use byteorder::NetworkEndian;
use serde::{Deserialize, Serialize};

use crate::crypto;
+
use crate::git;
use crate::identity::Id;
+
use crate::protocol::wire;
use crate::protocol::{Context, NodeId, Timestamp, PROTOCOL_VERSION};
use crate::storage;
use crate::storage::refs::SignedRefs;
@@ -28,13 +30,6 @@ pub struct Hostname(String);
/// Peer public protocol address.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum Address {
-
    /// Tor V3 onion address.
-
    Onion {
-
        key: crypto::PublicKey,
-
        port: u16,
-
        checksum: u16,
-
        version: u8,
-
    },
    Ip {
        ip: net::IpAddr,
        port: u16,
@@ -43,6 +38,57 @@ pub enum Address {
        host: Hostname,
        port: u16,
    },
+
    /// Tor V3 onion address.
+
    Onion {
+
        key: crypto::PublicKey,
+
        port: u16,
+
        checksum: u16,
+
        version: u8,
+
    },
+
}
+

+
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::Ip { ip, port } => {
+
                match ip {
+
                    net::IpAddr::V4(addr) => {
+
                        n += 1u8.encode(writer)?;
+
                        n += addr.octets().encode(writer)?;
+
                    }
+
                    net::IpAddr::V6(addr) => {
+
                        n += 2u8.encode(writer)?;
+
                        n += addr.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> {
+
        use byteorder::ReadBytesExt;
+

+
        match reader.read_u8()? {
+
            1 => {
+
                let octets: [u8; 4] = wire::Decode::decode(reader)?;
+
                let ip = net::IpAddr::from(net::Ipv4Addr::from(octets));
+
                let port = u16::decode(reader)?;
+

+
                Ok(Self::Ip { ip, port })
+
            }
+
            _ => {
+
                todo!();
+
            }
+
        }
+
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -78,7 +124,7 @@ pub enum Message {
        timestamp: Timestamp,
        version: u32,
        addrs: Vec<Address>,
-
        git: Url,
+
        git: git::Url,
    },
    Node {
        /// Signature over the announcement, by the node being announced.
@@ -91,11 +137,9 @@ pub enum Message {
    /// Send our inventory to a peer. Sent in response to [`Message::GetInventory`].
    /// Nb. This should be the whole inventory, not a partial update.
    Inventory {
+
        node: NodeId,
        inv: Vec<Id>,
        timestamp: Timestamp,
-
        /// Original peer this inventory came from. We don't set this when we
-
        /// are the originator, only when relaying.
-
        origin: Option<NodeId>,
    },
    /// Project refs were updated. Includes the signature of the user who updated
    /// their view of the project.
@@ -110,7 +154,7 @@ pub enum Message {
}

impl Message {
-
    pub fn hello(id: NodeId, timestamp: Timestamp, addrs: Vec<Address>, git: Url) -> Self {
+
    pub fn hello(id: NodeId, timestamp: Timestamp, addrs: Vec<Address>, git: git::Url) -> Self {
        Self::Hello {
            id,
            timestamp,
@@ -133,18 +177,127 @@ impl Message {
    pub fn inventory<S, T, G>(ctx: &Context<S, T, G>) -> Result<Self, storage::Error>
    where
        T: storage::ReadStorage,
+
        G: crypto::Signer,
    {
        let timestamp = ctx.timestamp();
        let inv = ctx.storage.inventory()?;

        Ok(Self::Inventory {
-
            timestamp,
+
            node: ctx.id(),
            inv,
-
            origin: None,
+
            timestamp,
        })
    }

    pub fn get_inventory(ids: impl Into<Vec<Id>>) -> Self {
        Self::GetInventory { ids: ids.into() }
    }
+

+
    pub fn type_id(&self) -> u16 {
+
        match self {
+
            Self::Hello { .. } => 0,
+
            Self::Node { .. } => 2,
+
            Self::GetInventory { .. } => 4,
+
            Self::Inventory { .. } => 6,
+
            Self::RefsUpdate { .. } => 8,
+
        }
+
    }
+
}
+

+
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::Hello {
+
                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::RefsUpdate { id, signer, refs } => {
+
                n += id.encode(writer)?;
+
                n += signer.encode(writer)?;
+
                n += refs.encode(writer)?;
+
            }
+
            Self::GetInventory { ids } => {
+
                n += ids.as_slice().encode(writer)?;
+
            }
+
            Self::Inventory {
+
                node,
+
                inv,
+
                timestamp,
+
            } => {
+
                n += node.encode(writer)?;
+
                n += inv.as_slice().encode(writer)?;
+
                n += timestamp.encode(writer)?;
+
            }
+
            Self::Node { .. } => {
+
                todo!();
+
            }
+
        }
+
        Ok(n)
+
    }
+
}
+

+
impl wire::Decode for Message {
+
    fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
+
        use byteorder::ReadBytesExt;
+

+
        let type_id = reader.read_u16::<NetworkEndian>()?;
+

+
        match type_id {
+
            0 => {
+
                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::Hello {
+
                    id,
+
                    timestamp,
+
                    version,
+
                    addrs,
+
                    git,
+
                })
+
            }
+
            2 => {
+
                todo!();
+
            }
+
            4 => {
+
                let ids = Vec::<Id>::decode(reader)?;
+

+
                Ok(Self::GetInventory { ids })
+
            }
+
            6 => {
+
                let node = NodeId::decode(reader)?;
+
                let inv = Vec::<Id>::decode(reader)?;
+
                let timestamp = Timestamp::decode(reader)?;
+

+
                Ok(Self::Inventory {
+
                    node,
+
                    inv,
+
                    timestamp,
+
                })
+
            }
+
            8 => {
+
                let id = Id::decode(reader)?;
+
                let signer = crypto::PublicKey::decode(reader)?;
+
                let refs = SignedRefs::decode(reader)?;
+

+
                Ok(Self::RefsUpdate { id, signer, refs })
+
            }
+
            _ => {
+
                todo!();
+
            }
+
        }
+
    }
}
modified node/src/protocol/peer.rs
@@ -163,11 +163,11 @@ impl Peer {
                ctx.write(self.addr, inventory);
            }
            (
-
                PeerState::Negotiated { id, git, .. },
+
                PeerState::Negotiated { git, .. },
                Message::Inventory {
+
                    node,
                    timestamp,
                    inv,
-
                    origin,
                },
            ) => {
                let now = ctx.clock.local_time();
@@ -184,13 +184,13 @@ impl Peer {
                } else {
                    return Ok(None);
                }
-
                ctx.process_inventory(&inv, origin.unwrap_or(*id), git);
+
                ctx.process_inventory(&inv, node, git);

                if ctx.config.relay {
                    return Ok(Some(Message::Inventory {
-
                        timestamp,
+
                        node,
                        inv,
-
                        origin: origin.or(Some(*id)),
+
                        timestamp,
                    }));
                }
            }
added node/src/protocol/wire.rs
@@ -0,0 +1,348 @@
+
use std::collections::BTreeMap;
+
use std::convert::TryFrom;
+
use std::ops::Deref;
+
use std::string::FromUtf8Error;
+
use std::{io, mem, net};
+

+
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
+

+
use crate::crypto::{PublicKey, Signature};
+
use crate::git;
+
use crate::git::fmt;
+
use crate::hash::Digest;
+
use crate::identity::Id;
+
use crate::storage::refs::Refs;
+

+
#[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(transparent)]
+
    InvalidRefName(#[from] fmt::Error),
+
}
+

+
pub trait Encode {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error>;
+
}
+

+
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
+
}
+

+
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 {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        let u = (*self)
+
            .try_into()
+
            .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
+

+
        writer.write_u32::<NetworkEndian>(u)?;
+

+
        Ok(mem::size_of_val(&u))
+
    }
+
}
+

+
impl Encode for PublicKey {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        self.as_bytes().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().encode(writer)?;
+

+
        for item in self.iter() {
+
            n += item.encode(writer)?;
+
        }
+
        Ok(n)
+
    }
+
}
+

+
impl Encode for net::IpAddr {
+
    fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
+
        match self {
+
            net::IpAddr::V4(addr) => addr.octets().encode(writer),
+
            net::IpAddr::V6(addr) => addr.octets().encode(writer),
+
        }
+
    }
+
}
+

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

+
        n += self.len().encode(writer)?;
+
        n += self.as_bytes().encode(writer)?;
+

+
        Ok(n)
+
    }
+
}
+

+
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.to_bytes().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 = u64::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: usize = usize::decode(reader)?;
+
        let mut vec = Vec::with_capacity(len);
+

+
        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 = usize::decode(reader)?;
+
        let mut bytes = vec![0; len];
+

+
        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 string = String::decode(reader)?;
+

+
        Self::from_bytes(string.as_bytes())
+
            .map_err(|e| Error::Io(io::Error::new(io::ErrorKind::InvalidInput, e.to_string())))
+
    }
+
}
+

+
impl Decode for Id {
+
    fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
+
        let digest: Digest = Decode::decode(reader)?;
+

+
        Ok(Self::from(digest))
+
    }
+
}
+

+
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))
+
    }
+
}
modified node/src/storage/refs.rs
@@ -16,6 +16,7 @@ use crate::crypto;
use crate::crypto::{PublicKey, Signature, Signer, Unverified, Verified};
use crate::git;
use crate::git::Oid;
+
use crate::protocol::wire;
use crate::storage;
use crate::storage::{ReadRepository, RemoteId, WriteRepository};

@@ -295,6 +296,30 @@ impl SignedRefs<Verified> {
    }
}

+
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;

modified node/src/test/tests.rs
@@ -146,9 +146,9 @@ fn test_inventory_sync() {
    alice.receive(
        &bob.addr(),
        Message::Inventory {
+
            node: bob.id(),
            timestamp: now,
            inv: projs.clone(),
-
            origin: None,
        },
    );

@@ -213,9 +213,9 @@ fn test_inventory_relay_bad_timestamp() {
    alice.receive(
        &bob.addr(),
        Message::Inventory {
+
            node: bob.id(),
            timestamp,
            inv: vec![],
-
            origin: None,
        },
    );
    assert_matches!(
@@ -240,15 +240,15 @@ fn test_inventory_relay() {
    alice.receive(
        &bob.addr(),
        Message::Inventory {
+
            node: bob.id(),
            timestamp: now,
            inv: inv.clone(),
-
            origin: None,
        },
    );
    assert_matches!(
        alice.messages(&eve.addr()).next(),
-
        Some(Message::Inventory { timestamp, origin, .. })
-
        if origin == Some(bob.id()) && timestamp == now
+
        Some(Message::Inventory { node, timestamp, .. })
+
        if node == bob.id() && timestamp == now
    );
    assert_matches!(
        alice.messages(&bob.addr()).next(),
@@ -259,9 +259,9 @@ fn test_inventory_relay() {
    alice.receive(
        &bob.addr(),
        Message::Inventory {
+
            node: bob.id(),
            timestamp: now,
            inv: inv.clone(),
-
            origin: None,
        },
    );
    assert_matches!(
@@ -273,15 +273,15 @@ fn test_inventory_relay() {
    alice.receive(
        &bob.addr(),
        Message::Inventory {
+
            node: bob.id(),
            timestamp: now + 1,
            inv: inv.clone(),
-
            origin: None,
        },
    );
    assert_matches!(
        alice.messages(&eve.addr()).next(),
-
        Some(Message::Inventory { timestamp, origin, .. })
-
        if origin == Some(bob.id()) && timestamp == now + 1,
+
        Some(Message::Inventory { node, timestamp, .. })
+
        if node == bob.id() && timestamp == now + 1,
        "Sending a new inventory does trigger the relay"
    );

@@ -289,15 +289,15 @@ fn test_inventory_relay() {
    alice.receive(
        &eve.addr(),
        Message::Inventory {
+
            node: eve.id(),
            timestamp: now,
            inv,
-
            origin: None,
        },
    );
    assert_matches!(
        alice.messages(&bob.addr()).next(),
-
        Some(Message::Inventory { timestamp, origin, .. })
-
        if origin == Some(eve.id()) && timestamp == now
+
        Some(Message::Inventory { node, timestamp, .. })
+
        if node == eve.id() && timestamp == now
    );
}