node: Re-think gossip messages and signing

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier 2022-09-12 18:01:16 +02:00
parent b265f1b456
commit 651513c757
No known key found for this signature in database
16 changed files with 536 additions and 385 deletions

View File

@ -43,9 +43,10 @@ impl Default for Config {
} }
} }
pub struct Client<R: Reactor> { pub struct Client<R: Reactor, G: Signer> {
reactor: R, reactor: R,
storage: Storage, storage: Storage,
signer: G,
handle: chan::Sender<protocol::Command>, handle: chan::Sender<protocol::Command>,
commands: chan::Receiver<protocol::Command>, commands: chan::Receiver<protocol::Command>,
@ -54,20 +55,18 @@ pub struct Client<R: Reactor> {
events: Events, events: Events,
} }
impl<R: Reactor> Client<R> { impl<R: Reactor, G: Signer> Client<R, G> {
pub fn new<P: AsRef<Path>, S: Signer + 'static>( pub fn new<P: AsRef<Path>>(path: P, signer: G) -> Result<Self, nakamoto_net::error::Error> {
path: P,
signer: S,
) -> Result<Self, nakamoto_net::error::Error> {
let (handle, commands) = chan::unbounded::<protocol::Command>(); let (handle, commands) = chan::unbounded::<protocol::Command>();
let (shutdown, shutdown_recv) = chan::bounded(1); let (shutdown, shutdown_recv) = chan::bounded(1);
let (listening_send, listening) = chan::bounded(1); let (listening_send, listening) = chan::bounded(1);
let reactor = R::new(shutdown_recv, listening_send)?; let reactor = R::new(shutdown_recv, listening_send)?;
let storage = Storage::open(path, signer)?; let storage = Storage::open(path)?;
let events = Events {}; let events = Events {};
Ok(Self { Ok(Self {
storage, storage,
signer,
reactor, reactor,
handle, handle,
commands, commands,
@ -82,7 +81,7 @@ impl<R: Reactor> Client<R> {
let rng = fastrand::Rng::new(); let rng = fastrand::Rng::new();
let time = LocalTime::now(); let time = LocalTime::now();
let storage = self.storage; let storage = self.storage;
let signer = storage.signer(); let signer = self.signer;
let addresses = HashMap::with_hasher(rng.clone().into()); let addresses = HashMap::with_hasher(rng.clone().into());
log::info!("Initializing client ({:?})..", network); log::info!("Initializing client ({:?})..", network);

View File

@ -77,7 +77,7 @@ impl<W: Waker> traits::Handle for Handle<W> {
/// Notify the client that a project has been updated. /// Notify the client that a project has been updated.
fn updated(&self, id: Id) -> Result<(), Error> { fn updated(&self, id: Id) -> Result<(), Error> {
self.command(protocol::Command::AnnounceRefsUpdate(id)) self.command(protocol::Command::AnnounceRefs(id))
} }
/// Send a command to the command channel, and wake up the event loop. /// Send a command to the command channel, and wake up the event loop.

View File

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

View File

@ -24,7 +24,7 @@ use crate::collections::{HashMap, HashSet};
use crate::crypto; use crate::crypto;
use crate::identity::{Id, Project}; use crate::identity::{Id, Project};
use crate::protocol::config::ProjectTracking; use crate::protocol::config::ProjectTracking;
use crate::protocol::message::Message; use crate::protocol::message::{Message, NodeAnnouncement, RefsAnnouncement};
use crate::protocol::peer::{Peer, PeerError, PeerState}; use crate::protocol::peer::{Peer, PeerError, PeerState};
use crate::protocol::wire::Encode; use crate::protocol::wire::Encode;
use crate::storage; use crate::storage;
@ -32,6 +32,8 @@ use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, Writ
pub use crate::protocol::config::{Config, Network}; pub use crate::protocol::config::{Config, Network};
use self::message::{InventoryAnnouncement, NodeFeatures};
pub const DEFAULT_PORT: u16 = 8776; pub const DEFAULT_PORT: u16 = 8776;
pub const PROTOCOL_VERSION: u32 = 1; pub const PROTOCOL_VERSION: u32 = 1;
pub const TARGET_OUTBOUND_PEERS: usize = 8; pub const TARGET_OUTBOUND_PEERS: usize = 8;
@ -102,7 +104,7 @@ pub enum FetchResult {
/// Commands sent to the protocol by the operator. /// Commands sent to the protocol by the operator.
#[derive(Debug)] #[derive(Debug)]
pub enum Command { pub enum Command {
AnnounceRefsUpdate(Id), AnnounceRefs(Id),
Connect(net::SocketAddr), Connect(net::SocketAddr),
Fetch(Id, chan::Sender<FetchLookup>), Fetch(Id, chan::Sender<FetchLookup>),
Track(Id, chan::Sender<bool>), Track(Id, chan::Sender<bool>),
@ -232,6 +234,16 @@ impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Protoco
&mut self.context.storage &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 protocol time. /// Get the local protocol time.
pub fn local_time(&self) -> LocalTime { pub fn local_time(&self) -> LocalTime {
self.context.clock.local_time() self.context.clock.local_time()
@ -254,7 +266,7 @@ impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Protoco
pub fn lookup(&self, id: &Id) -> Lookup { pub fn lookup(&self, id: &Id) -> Lookup {
Lookup { Lookup {
local: self.context.storage.get(id).unwrap(), local: self.context.storage.get(&self.node_id(), id).unwrap(),
remote: self remote: self
.context .context
.routing .routing
@ -269,7 +281,7 @@ impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Protoco
/// Announce our inventory to all connected peers. /// Announce our inventory to all connected peers.
fn announce_inventory(&mut self) -> Result<(), storage::Error> { fn announce_inventory(&mut self) -> Result<(), storage::Error> {
let inv = Message::inventory(&self.context)?; let inv = Message::inventory(self.context.inventory_announcement()?, &self.context.signer);
for addr in self.peers.negotiated().map(|(_, p)| p.addr) { for addr in self.peers.negotiated().map(|(_, p)| p.addr) {
self.context.write(addr, inv.clone()); self.context.write(addr, inv.clone());
@ -277,27 +289,6 @@ impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Protoco
Ok(()) Ok(())
} }
fn get_inventories(&mut self) -> Result<(), storage::Error> {
let mut msgs = Vec::new();
for id in self.tracked()? {
for (_, peer) in self.seeds(&id) {
if peer.is_negotiated() {
msgs.push((
peer.addr,
Message::GetInventory {
ids: vec![id.clone()],
},
));
}
}
}
for (remote, msg) in msgs {
self.write(remote, msg);
}
Ok(())
}
fn prune_routing_entries(&mut self) { fn prune_routing_entries(&mut self) {
// TODO // TODO
} }
@ -357,7 +348,7 @@ where
if now - self.last_sync >= SYNC_INTERVAL { if now - self.last_sync >= SYNC_INTERVAL {
debug!("Running 'sync' task..."); debug!("Running 'sync' task...");
self.get_inventories().unwrap(); // TODO: What do we do here?
self.context.io.push_back(Io::Wakeup(SYNC_INTERVAL)); self.context.io.push_back(Io::Wakeup(SYNC_INTERVAL));
self.last_sync = now; self.last_sync = now;
} }
@ -451,15 +442,23 @@ where
Command::Untrack(id, resp) => { Command::Untrack(id, resp) => {
resp.send(self.untrack(id)).ok(); resp.send(self.untrack(id)).ok();
} }
Command::AnnounceRefsUpdate(id) => { Command::AnnounceRefs(id) => {
let signer = *self.storage.public_key(); let node = self.node_id();
let repo = self.storage.repository(&id).unwrap(); let repo = self.storage.repository(&id).unwrap();
let remote = repo.remote(&signer).unwrap(); let remote = repo.remote(&node).unwrap();
let peers = self.peers.negotiated().map(|(_, p)| p.addr); let peers = self.peers.negotiated().map(|(_, p)| p.addr);
let refs = remote.refs.unverified(); let refs = remote.refs.into();
let message = RefsAnnouncement { id, refs };
let signature = message.sign(&self.signer);
self.context self.context.broadcast(
.broadcast(Message::RefsUpdate { id, signer, refs }, peers); Message::RefsAnnouncement {
node,
message,
signature,
},
peers,
);
} }
} }
} }
@ -489,21 +488,11 @@ where
// For inbound connections, we wait for the remote to say "Hello" first. // 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? // TODO: How should we deal with multiple peers connecting from the same IP address?
if link.is_outbound() { if link.is_outbound() {
let git = self.config.git_url.clone(); // 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) { if let Some(peer) = self.peers.get_mut(&ip) {
self.context.write_all( self.context.write_all(peer.addr, messages);
peer.addr,
[
Message::hello(
self.context.id(),
self.context.timestamp(),
self.context.config.listen.clone(),
git,
),
Message::get_inventory([]),
],
);
peer.connected(); peer.connected();
} }
} else { } else {
@ -695,7 +684,7 @@ where
T: storage::ReadStorage, T: storage::ReadStorage,
G: crypto::Signer, G: crypto::Signer,
{ {
pub(crate) fn id(&self) -> NodeId { pub(crate) fn node_id(&self) -> NodeId {
*self.signer.public_key() *self.signer.public_key()
} }
} }
@ -725,6 +714,51 @@ where
} }
} }
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 handshake_messages(&self) -> [Message; 3] {
let git = self.config.git_url.clone();
[
Message::hello(
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),
]
}
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. /// Process a peer inventory announcement by updating our routing table.
fn process_inventory(&mut self, inventory: &Inventory, from: NodeId, remote: &Url) { fn process_inventory(&mut self, inventory: &Inventory, from: NodeId, remote: &Url) {
for proj_id in inventory { for proj_id in inventory {

View File

@ -1,4 +1,4 @@
use std::{io, net}; use std::{fmt, io, net};
use byteorder::{NetworkEndian, ReadBytesExt}; use byteorder::{NetworkEndian, ReadBytesExt};
@ -6,9 +6,8 @@ use crate::crypto;
use crate::git; use crate::git;
use crate::identity::Id; use crate::identity::Id;
use crate::protocol::wire; use crate::protocol::wire;
use crate::protocol::{Context, NodeId, Timestamp, PROTOCOL_VERSION}; use crate::protocol::{NodeId, Timestamp, PROTOCOL_VERSION};
use crate::storage; use crate::storage::refs::Refs;
use crate::storage::refs::SignedRefs;
/// Message envelope. All messages sent over the network are wrapped in this type. /// Message envelope. All messages sent over the network are wrapped in this type.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -31,10 +30,9 @@ pub struct Hostname(String);
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageType { pub enum MessageType {
Hello = 0, Hello = 0,
Node = 2, NodeAnnouncement = 2,
GetInventory = 4, InventoryAnnouncement = 4,
Inventory = 6, RefsAnnouncement = 6,
RefsUpdate = 8,
} }
impl From<MessageType> for u16 { impl From<MessageType> for u16 {
@ -49,10 +47,9 @@ impl TryFrom<u16> for MessageType {
fn try_from(other: u16) -> Result<Self, Self::Error> { fn try_from(other: u16) -> Result<Self, Self::Error> {
match other { match other {
0 => Ok(MessageType::Hello), 0 => Ok(MessageType::Hello),
2 => Ok(MessageType::Node), 2 => Ok(MessageType::NodeAnnouncement),
4 => Ok(MessageType::GetInventory), 4 => Ok(MessageType::InventoryAnnouncement),
6 => Ok(MessageType::Inventory), 6 => Ok(MessageType::RefsAnnouncement),
8 => Ok(MessageType::RefsUpdate),
_ => Err(other), _ => Err(other),
} }
} }
@ -197,8 +194,6 @@ impl wire::Decode for Address {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeAnnouncement { pub struct NodeAnnouncement {
/// Node identifier.
pub id: NodeId,
/// Advertized features. /// Advertized features.
pub features: NodeFeatures, pub features: NodeFeatures,
/// Monotonic timestamp. /// Monotonic timestamp.
@ -211,9 +206,9 @@ pub struct NodeAnnouncement {
impl NodeAnnouncement { impl NodeAnnouncement {
/// Verify a signature on this message. /// Verify a signature on this message.
pub fn verify(&self, signature: &crypto::Signature) -> bool { pub fn verify(&self, signer: &NodeId, signature: &crypto::Signature) -> bool {
let msg = wire::serialize(self); let msg = wire::serialize(self);
self.id.verify(signature, &msg).is_ok() signer.verify(signature, &msg).is_ok()
} }
} }
@ -221,7 +216,6 @@ impl wire::Encode for NodeAnnouncement {
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> { fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
let mut n = 0; let mut n = 0;
n += self.id.encode(writer)?;
n += self.features.encode(writer)?; n += self.features.encode(writer)?;
n += self.timestamp.encode(writer)?; n += self.timestamp.encode(writer)?;
n += self.alias.encode(writer)?; n += self.alias.encode(writer)?;
@ -233,14 +227,12 @@ impl wire::Encode for NodeAnnouncement {
impl wire::Decode for NodeAnnouncement { impl wire::Decode for NodeAnnouncement {
fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> { fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
let id = NodeId::decode(reader)?;
let features = NodeFeatures::decode(reader)?; let features = NodeFeatures::decode(reader)?;
let timestamp = Timestamp::decode(reader)?; let timestamp = Timestamp::decode(reader)?;
let alias = wire::Decode::decode(reader)?; let alias = wire::Decode::decode(reader)?;
let addresses = Vec::<Address>::decode(reader)?; let addresses = Vec::<Address>::decode(reader)?;
Ok(Self { Ok(Self {
id,
features, features,
timestamp, timestamp,
alias, alias,
@ -249,9 +241,88 @@ impl wire::Decode for NodeAnnouncement {
} }
} }
#[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(signature, &msg).is_ok()
}
/// Sign this announcement.
pub fn sign<S: crypto::Signer>(&self, signer: S) -> crypto::Signature {
let msg = wire::serialize(self);
signer.sign(&msg)
}
}
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 })
}
}
#[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(signature, &msg).is_ok()
}
}
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,
})
}
}
/// Message payload. /// Message payload.
/// These are the messages peers send to each other. /// These are the messages peers send to each other.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Clone, PartialEq, Eq)]
pub enum Message { pub enum Message {
/// Say hello to a peer. This is the first message sent to a peer after connection. /// Say hello to a peer. This is the first message sent to a peer after connection.
Hello { Hello {
@ -262,30 +333,36 @@ pub enum Message {
addrs: Vec<Address>, addrs: Vec<Address>,
git: git::Url, git: git::Url,
}, },
Node {
/// 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. /// Unsigned node announcement.
announcement: NodeAnnouncement, message: NodeAnnouncement,
/// Signature over the announcement, by the node being announced. /// Signature over the announcement, by the node being announced.
signature: crypto::Signature, signature: crypto::Signature,
}, },
/// Get a peer's inventory.
GetInventory { ids: Vec<Id> }, /// Node announcing project refs being created or updated.
/// Send our inventory to a peer. Sent in response to [`Message::GetInventory`]. RefsAnnouncement {
/// Nb. This should be the whole inventory, not a partial update. /// Node identifier.
Inventory {
node: NodeId, node: NodeId,
inv: Vec<Id>, /// Unsigned refs announcement.
timestamp: Timestamp, message: RefsAnnouncement,
}, /// Signature over the announcement, by the node that updated the refs.
/// Project refs were updated. Includes the signature of the user who updated signature: crypto::Signature,
/// their view of the project.
RefsUpdate {
/// Project under which the refs were updated.
id: Id,
/// Signing key.
signer: crypto::PublicKey,
/// Updated refs.
refs: SignedRefs<crypto::Unverified>,
}, },
} }
@ -300,47 +377,71 @@ impl Message {
} }
} }
pub fn node<S: crypto::Signer>(announcement: NodeAnnouncement, signer: S) -> Self { pub fn node<S: crypto::Signer>(message: NodeAnnouncement, signer: S) -> Self {
let msg = wire::serialize(&announcement); let msg = wire::serialize(&message);
let signature = signer.sign(&msg); let signature = signer.sign(&msg);
let node = *signer.public_key();
Self::Node { Self::NodeAnnouncement {
node,
signature, signature,
announcement, message,
} }
} }
pub fn inventory<S, T, G>(ctx: &Context<S, T, G>) -> Result<Self, storage::Error> pub fn inventory<S: crypto::Signer>(message: InventoryAnnouncement, signer: S) -> Self {
where let msg = wire::serialize(&message);
T: storage::ReadStorage, let signature = signer.sign(&msg);
G: crypto::Signer, let node = *signer.public_key();
{
let timestamp = ctx.timestamp();
let inv = ctx.storage.inventory()?;
Ok(Self::Inventory { Self::InventoryAnnouncement {
node: ctx.id(), node,
inv, signature,
timestamp, message,
})
} }
pub fn get_inventory(ids: impl Into<Vec<Id>>) -> Self {
Self::GetInventory { ids: ids.into() }
} }
pub fn type_id(&self) -> u16 { pub fn type_id(&self) -> u16 {
match self { match self {
Self::Hello { .. } => MessageType::Hello, Self::Hello { .. } => MessageType::Hello,
Self::Node { .. } => MessageType::Node, Self::NodeAnnouncement { .. } => MessageType::NodeAnnouncement,
Self::GetInventory { .. } => MessageType::GetInventory, Self::InventoryAnnouncement { .. } => MessageType::InventoryAnnouncement,
Self::Inventory { .. } => MessageType::Inventory, Self::RefsAnnouncement { .. } => MessageType::RefsAnnouncement,
Self::RefsUpdate { .. } => MessageType::RefsUpdate,
} }
.into() .into()
} }
} }
impl fmt::Debug for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Hello { id, .. } => write!(f, "Hello({})", id),
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
)
}
}
}
}
impl wire::Encode for Message { impl wire::Encode for Message {
fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> { fn encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
let mut n = self.type_id().encode(writer)?; let mut n = self.type_id().encode(writer)?;
@ -359,28 +460,31 @@ impl wire::Encode for Message {
n += addrs.as_slice().encode(writer)?; n += addrs.as_slice().encode(writer)?;
n += git.encode(writer)?; n += git.encode(writer)?;
} }
Self::RefsUpdate { id, signer, refs } => { Self::RefsAnnouncement {
n += id.encode(writer)?;
n += signer.encode(writer)?;
n += refs.encode(writer)?;
}
Self::GetInventory { ids } => {
n += ids.as_slice().encode(writer)?;
}
Self::Inventory {
node, node,
inv, message,
timestamp,
} => {
n += node.encode(writer)?;
n += inv.as_slice().encode(writer)?;
n += timestamp.encode(writer)?;
}
Self::Node {
announcement,
signature, signature,
} => { } => {
n += announcement.encode(writer)?; 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)?; n += signature.encode(writer)?;
} }
} }
@ -408,37 +512,38 @@ impl wire::Decode for Message {
git, git,
}) })
} }
Ok(MessageType::Node) => { Ok(MessageType::NodeAnnouncement) => {
let announcement = NodeAnnouncement::decode(reader)?; let node = NodeId::decode(reader)?;
let message = NodeAnnouncement::decode(reader)?;
let signature = crypto::Signature::decode(reader)?; let signature = crypto::Signature::decode(reader)?;
Ok(Self::Node { Ok(Self::NodeAnnouncement {
announcement, node,
message,
signature, signature,
}) })
} }
Ok(MessageType::GetInventory) => { Ok(MessageType::InventoryAnnouncement) => {
let ids = Vec::<Id>::decode(reader)?;
Ok(Self::GetInventory { ids })
}
Ok(MessageType::Inventory) => {
let node = NodeId::decode(reader)?; let node = NodeId::decode(reader)?;
let inv = Vec::<Id>::decode(reader)?; let message = InventoryAnnouncement::decode(reader)?;
let timestamp = Timestamp::decode(reader)?; let signature = crypto::Signature::decode(reader)?;
Ok(Self::Inventory { Ok(Self::InventoryAnnouncement {
node, node,
inv, message,
timestamp, signature,
}) })
} }
Ok(MessageType::RefsUpdate) => { Ok(MessageType::RefsAnnouncement) => {
let id = Id::decode(reader)?; let node = NodeId::decode(reader)?;
let signer = crypto::PublicKey::decode(reader)?; let message = RefsAnnouncement::decode(reader)?;
let refs = SignedRefs::decode(reader)?; let signature = crypto::Signature::decode(reader)?;
Ok(Self::RefsUpdate { id, signer, refs }) Ok(Self::RefsAnnouncement {
node,
message,
signature,
})
} }
Err(other) => Err(wire::Error::UnknownMessageType(other)), Err(other) => Err(wire::Error::UnknownMessageType(other)),
} }
@ -450,8 +555,10 @@ mod tests {
use super::*; use super::*;
use quickcheck_macros::quickcheck; use quickcheck_macros::quickcheck;
use crate::crypto::Signer;
use crate::decoder::Decoder; use crate::decoder::Decoder;
use crate::protocol::wire::{self, Encode}; use crate::protocol::wire::{self, Encode};
use crate::test::crypto::MockSigner;
#[quickcheck] #[quickcheck]
fn prop_message_encode_decode(message: Message) { fn prop_message_encode_decode(message: Message) {
@ -494,4 +601,13 @@ mod tests {
addr addr
); );
} }
#[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));
}
} }

View File

@ -131,14 +131,7 @@ impl Peer {
// Nb. This is a very primitive handshake. Eventually we should have anyhow // Nb. This is a very primitive handshake. Eventually we should have anyhow
// extra "acknowledgment" message sent when the `Hello` is well received. // extra "acknowledgment" message sent when the `Hello` is well received.
if self.link.is_inbound() { if self.link.is_inbound() {
let git = ctx.config.git_url.clone(); ctx.write_all(self.addr, ctx.handshake_messages());
ctx.write_all(
self.addr,
[
Message::hello(ctx.id(), now, ctx.config.listen.clone(), git),
Message::get_inventory([]),
],
);
} }
// Nb. we don't set the peer timestamp here, since it is going to be // 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 // set after the first message is received only. Setting it here would
@ -157,64 +150,66 @@ impl Peer {
); );
return Err(PeerError::Misbehavior); return Err(PeerError::Misbehavior);
} }
(PeerState::Negotiated { .. }, Message::GetInventory { .. }) => {
// TODO: Handle partial inventory requests.
let inventory = Message::inventory(ctx).unwrap();
ctx.write(self.addr, inventory);
}
( (
PeerState::Negotiated { git, .. }, PeerState::Negotiated { git, .. },
Message::Inventory { Message::InventoryAnnouncement {
node, node,
timestamp, message,
inv, signature,
}, },
) => { ) => {
let now = ctx.clock.local_time(); let now = ctx.clock.local_time();
let last = self.timestamp; let last = self.timestamp;
// Don't allow messages from too far in the past or future. // Don't allow messages from too far in the past or future.
if timestamp.abs_diff(now.as_secs()) > MAX_TIME_DELTA.as_secs() { if message.timestamp.abs_diff(now.as_secs()) > MAX_TIME_DELTA.as_secs() {
return Err(PeerError::InvalidTimestamp(timestamp)); return Err(PeerError::InvalidTimestamp(message.timestamp));
} }
// Discard inventory messages we've already seen, otherwise update // Discard inventory messages we've already seen, otherwise update
// out last seen time. // out last seen time.
if timestamp > last { if message.timestamp > last {
self.timestamp = timestamp; self.timestamp = message.timestamp;
} else { } else {
return Ok(None); return Ok(None);
} }
ctx.process_inventory(&inv, node, git); ctx.process_inventory(&message.inventory, node, git);
if ctx.config.relay { if ctx.config.relay {
return Ok(Some(Message::Inventory { return Ok(Some(Message::InventoryAnnouncement {
node, node,
inv, message,
timestamp, signature,
})); }));
} }
} }
// Process a peer inventory update announcement by (maybe) fetching. // Process a peer inventory update announcement by (maybe) fetching.
(PeerState::Negotiated { git, .. }, Message::RefsUpdate { id, signer, refs }) => { (
if let Ok(refs) = refs.verified(&signer) { PeerState::Negotiated { git, .. },
Message::RefsAnnouncement {
node,
message,
signature,
},
) => {
if message.verify(&node, &signature) {
// TODO: Buffer/throttle fetches. // TODO: Buffer/throttle fetches.
// TODO: Check that we're tracking this user as well. // TODO: Check that we're tracking this user as well.
if ctx.config.is_tracking(&id) { if ctx.config.is_tracking(&message.id) {
// TODO: Check refs to see if we should try to fetch or not. // TODO: Check refs to see if we should try to fetch or not.
let updated_refs = ctx.fetch(&id, git); let updated_refs = ctx.fetch(&message.id, git);
let is_updated = !updated_refs.is_empty(); let is_updated = !updated_refs.is_empty();
ctx.io.push_back(Io::Event(Event::RefsFetched { ctx.io.push_back(Io::Event(Event::RefsFetched {
from: git.clone(), from: git.clone(),
project: id.clone(), project: message.id.clone(),
updated: updated_refs, updated: updated_refs,
})); }));
if is_updated { if is_updated {
return Ok(Some(Message::RefsUpdate { return Ok(Some(Message::RefsAnnouncement {
id, node,
signer, message,
refs: refs.unverified(), signature,
})); }));
} }
} }
@ -224,15 +219,16 @@ impl Peer {
} }
( (
PeerState::Negotiated { .. }, PeerState::Negotiated { .. },
Message::Node { Message::NodeAnnouncement {
announcement, node,
message,
signature, signature,
}, },
) => { ) => {
if !announcement.verify(&signature) { if !message.verify(&node, &signature) {
return Err(PeerError::Misbehavior); return Err(PeerError::Misbehavior);
} }
todo!(); log::warn!("Node announcement handling is not implemented");
} }
(PeerState::Negotiated { .. }, Message::Hello { .. }) => { (PeerState::Negotiated { .. }, Message::Hello { .. }) => {
debug!( debug!(

View File

@ -4,12 +4,12 @@ use std::path::Path;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use thiserror::Error; use thiserror::Error;
use crate::crypto::Verified; use crate::crypto::{Signer, Verified};
use crate::git; use crate::git;
use crate::identity::Id; use crate::identity::Id;
use crate::storage::git::RADICLE_ID_REF; use crate::storage::git::RADICLE_ID_REF;
use crate::storage::refs::SignedRefs; use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, ReadRepository as _, WriteRepository as _}; use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _};
use crate::{identity, storage}; use crate::{identity, storage};
pub const REMOTE_NAME: &str = "rad"; pub const REMOTE_NAME: &str = "rad";
@ -33,14 +33,15 @@ pub enum InitError {
} }
/// Initialize a new radicle project from a git repository. /// Initialize a new radicle project from a git repository.
pub fn init<'r, S: storage::WriteStorage<'r>>( pub fn init<'r, G: Signer, S: storage::WriteStorage<'r>>(
repo: &git2::Repository, repo: &git2::Repository,
name: &str, name: &str,
description: &str, description: &str,
default_branch: BranchName, default_branch: BranchName,
signer: G,
storage: S, storage: S,
) -> Result<(Id, SignedRefs<Verified>), InitError> { ) -> Result<(Id, SignedRefs<Verified>), InitError> {
let pk = storage.public_key(); let pk = signer.public_key();
let delegate = identity::Delegate { let delegate = identity::Delegate {
// TODO: Use actual user name. // TODO: Use actual user name.
name: String::from("anonymous"), name: String::from("anonymous"),
@ -102,7 +103,7 @@ pub fn init<'r, S: storage::WriteStorage<'r>>(
)], )],
None, None,
)?; )?;
let signed = storage.sign_refs(&project)?; let signed = storage.sign_refs(&project, signer)?;
Ok((id, signed)) Ok((id, signed))
} }
@ -121,24 +122,24 @@ pub enum CheckoutError {
/// This effectively does a `git-clone` from storage. /// This effectively does a `git-clone` from storage.
pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>( pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
proj: &Id, proj: &Id,
remote: &RemoteId,
path: P, path: P,
storage: S, storage: S,
) -> Result<git2::Repository, CheckoutError> { ) -> Result<git2::Repository, CheckoutError> {
// TODO: Decide on whether we can use `clone_local` // TODO: Decide on whether we can use `clone_local`
// TODO: Look into sharing object databases. // TODO: Look into sharing object databases.
let project = storage let project = storage
.get(proj)? .get(remote, proj)?
.ok_or_else(|| CheckoutError::NotFound(proj.clone()))?; .ok_or_else(|| CheckoutError::NotFound(proj.clone()))?;
let mut opts = git2::RepositoryInitOptions::new(); let mut opts = git2::RepositoryInitOptions::new();
opts.no_reinit(true).description(&project.doc.description); opts.no_reinit(true).description(&project.doc.description);
let repo = git2::Repository::init_opts(path, &opts)?; let repo = git2::Repository::init_opts(path, &opts)?;
let remote_id = storage.public_key();
let default_branch = project.doc.default_branch.as_str(); let default_branch = project.doc.default_branch.as_str();
// Configure and fetch all refs from remote. // Configure and fetch all refs from remote.
git::configure_remote(&repo, REMOTE_NAME, remote_id, &project.path)?.fetch::<&str>( git::configure_remote(&repo, REMOTE_NAME, remote, &project.path)?.fetch::<&str>(
&[], &[],
None, None,
None, None,
@ -155,7 +156,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
&repo, &repo,
REMOTE_NAME, REMOTE_NAME,
default_branch, default_branch,
&format!("refs/remotes/{remote_id}/heads/{default_branch}"), &format!("refs/remotes/{remote}/heads/{default_branch}"),
)?; )?;
} }
@ -174,22 +175,24 @@ mod tests {
fn test_init() { fn test_init() {
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default(); let signer = crypto::MockSigner::default();
let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap(); let public_key = *signer.public_key();
let mut storage = Storage::open(tempdir.path().join("storage")).unwrap();
let repo = fixtures::repository(tempdir.path().join("working")); let repo = fixtures::repository(tempdir.path().join("working"));
let (id, refs) = init( let (proj, refs) = init(
&repo, &repo,
"acme", "acme",
"Acme's repo", "Acme's repo",
BranchName::from("master"), BranchName::from("master"),
&signer,
&mut storage, &mut storage,
) )
.unwrap(); .unwrap();
let project = storage.get(&id).unwrap().unwrap(); let project = storage.get(&public_key, &proj).unwrap().unwrap();
assert_eq!(project.remotes[storage.public_key()].refs, refs); assert_eq!(project.remotes[&public_key].refs, refs);
assert_eq!(project.id, id); assert_eq!(project.id, proj);
assert_eq!(project.doc.name, "acme"); assert_eq!(project.doc.name, "acme");
assert_eq!(project.doc.description, "Acme's repo"); assert_eq!(project.doc.description, "Acme's repo");
assert_eq!(project.doc.default_branch, BranchName::from("master")); assert_eq!(project.doc.default_branch, BranchName::from("master"));
@ -197,7 +200,7 @@ mod tests {
project.doc.delegates.first(), project.doc.delegates.first(),
&Delegate { &Delegate {
name: String::from("anonymous"), name: String::from("anonymous"),
id: Did::from(*storage.public_key()), id: Did::from(public_key),
} }
); );
} }
@ -206,7 +209,8 @@ mod tests {
fn test_checkout() { fn test_checkout() {
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default(); let signer = crypto::MockSigner::default();
let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap(); let remote_id = signer.public_key();
let mut storage = Storage::open(tempdir.path().join("storage")).unwrap();
let original = fixtures::repository(tempdir.path().join("original")); let original = fixtures::repository(tempdir.path().join("original"));
let (id, _) = init( let (id, _) = init(
@ -214,11 +218,12 @@ mod tests {
"acme", "acme",
"Acme's repo", "Acme's repo",
BranchName::from("master"), BranchName::from("master"),
&signer,
&mut storage, &mut storage,
) )
.unwrap(); .unwrap();
let copy = checkout(&id, tempdir.path().join("copy"), &mut storage).unwrap(); let copy = checkout(&id, remote_id, tempdir.path().join("copy"), &mut storage).unwrap();
assert_eq!( assert_eq!(
copy.head().unwrap().target(), copy.head().unwrap().target(),

View File

@ -3,7 +3,7 @@ pub mod refs;
use std::collections::hash_map; use std::collections::hash_map;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Deref, DerefMut}; use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::{fmt, io}; use std::{fmt, io};
@ -13,7 +13,8 @@ use thiserror::Error;
pub use radicle_git_ext::Oid; pub use radicle_git_ext::Oid;
use crate::collections::HashMap; use crate::collections::HashMap;
use crate::crypto::{self, PublicKey, Unverified, Verified}; use crate::crypto;
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
use crate::git::Url; use crate::git::Url;
use crate::git::{RefError, RefStr, RefString}; use crate::git::{RefError, RefStr, RefString};
use crate::identity; use crate::identity;
@ -202,9 +203,8 @@ impl Remote<Verified> {
} }
pub trait ReadStorage { pub trait ReadStorage {
fn public_key(&self) -> &PublicKey;
fn url(&self) -> Url; fn url(&self) -> Url;
fn get(&self, proj: &Id) -> Result<Option<Project>, Error>; fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error>;
fn inventory(&self) -> Result<Inventory, Error>; fn inventory(&self) -> Result<Inventory, Error>;
} }
@ -212,7 +212,11 @@ pub trait WriteStorage<'r>: ReadStorage {
type Repository: WriteRepository<'r>; type Repository: WriteRepository<'r>;
fn repository(&self, proj: &Id) -> Result<Self::Repository, Error>; fn repository(&self, proj: &Id) -> Result<Self::Repository, Error>;
fn sign_refs(&self, repository: &Self::Repository) -> Result<SignedRefs<Verified>, Error>; fn sign_refs<G: Signer>(
&self,
repository: &Self::Repository,
signer: G,
) -> Result<SignedRefs<Verified>, Error>;
} }
pub trait ReadRepository<'r> { pub trait ReadRepository<'r> {
@ -246,10 +250,6 @@ where
T: Deref<Target = S>, T: Deref<Target = S>,
S: ReadStorage + 'static, S: ReadStorage + 'static,
{ {
fn public_key(&self) -> &PublicKey {
self.deref().public_key()
}
fn url(&self) -> Url { fn url(&self) -> Url {
self.deref().url() self.deref().url()
} }
@ -258,14 +258,14 @@ where
self.deref().inventory() self.deref().inventory()
} }
fn get(&self, proj: &Id) -> Result<Option<Project>, Error> { fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
self.deref().get(proj) self.deref().get(remote, proj)
} }
} }
impl<'r, T, S> WriteStorage<'r> for T impl<'r, T, S> WriteStorage<'r> for T
where where
T: DerefMut<Target = S>, T: Deref<Target = S>,
S: WriteStorage<'r> + 'static, S: WriteStorage<'r> + 'static,
{ {
type Repository = S::Repository; type Repository = S::Repository;
@ -274,8 +274,12 @@ where
self.deref().repository(proj) self.deref().repository(proj)
} }
fn sign_refs(&self, repository: &S::Repository) -> Result<SignedRefs<Verified>, Error> { fn sign_refs<G: Signer>(
self.deref().sign_refs(repository) &self,
repository: &S::Repository,
signer: G,
) -> Result<SignedRefs<Verified>, Error> {
self.deref().sign_refs(repository, signer)
} }
} }

View File

@ -1,6 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{fmt, fs, io}; use std::{fmt, fs, io};
use git_ref_format::refspec; use git_ref_format::refspec;
@ -11,7 +10,7 @@ pub use radicle_git_ext::Oid;
use crate::crypto::{Signer, Verified}; use crate::crypto::{Signer, Verified};
use crate::git; use crate::git;
use crate::identity::{self, IDENTITY_PATH}; use crate::identity::{self, IDENTITY_PATH};
use crate::identity::{Id, Project, PublicKey}; use crate::identity::{Id, Project};
use crate::storage::refs; use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs}; use crate::storage::refs::{Refs, SignedRefs};
use crate::storage::{ use crate::storage::{
@ -28,7 +27,6 @@ pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
pub struct Storage { pub struct Storage {
path: PathBuf, path: PathBuf,
signer: Arc<dyn Signer>,
} }
impl fmt::Debug for Storage { impl fmt::Debug for Storage {
@ -38,10 +36,6 @@ impl fmt::Debug for Storage {
} }
impl ReadStorage for Storage { impl ReadStorage for Storage {
fn public_key(&self) -> &PublicKey {
self.signer.public_key()
}
fn url(&self) -> git::Url { fn url(&self) -> git::Url {
git::Url { git::Url {
scheme: git_url::Scheme::File, scheme: git_url::Scheme::File,
@ -51,13 +45,12 @@ impl ReadStorage for Storage {
} }
} }
fn get(&self, id: &Id) -> Result<Option<Project>, Error> { fn get(&self, remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
// TODO: Don't create a repo here if it doesn't exist? // TODO: Don't create a repo here if it doesn't exist?
// Perhaps for checking we could have a `contains` method? // Perhaps for checking we could have a `contains` method?
let local = self.public_key(); let repo = self.repository(proj)?;
let repo = self.repository(id)?;
if let Some(doc) = repo.identity(local)? { if let Some(doc) = repo.identity(remote)? {
let remotes = repo.remotes()?.collect::<Result<_, _>>()?; let remotes = repo.remotes()?.collect::<Result<_, _>>()?;
let path = repo.path().to_path_buf(); let path = repo.path().to_path_buf();
@ -66,7 +59,7 @@ impl ReadStorage for Storage {
// an corrupted state. // an corrupted state.
Ok(Some(Project { Ok(Some(Project {
id: id.clone(), id: proj.clone(),
doc, doc,
remotes, remotes,
path, path,
@ -88,10 +81,14 @@ impl<'r> WriteStorage<'r> for Storage {
Repository::open(self.path.join(proj.to_string())) Repository::open(self.path.join(proj.to_string()))
} }
fn sign_refs(&self, repository: &Repository) -> Result<SignedRefs<Verified>, Error> { fn sign_refs<G: Signer>(
let remote = self.signer.public_key(); &self,
repository: &Repository,
signer: G,
) -> Result<SignedRefs<Verified>, Error> {
let remote = signer.public_key();
let refs = repository.references(remote)?; let refs = repository.references(remote)?;
let signed = refs.signed(self.signer.clone())?; let signed = refs.signed(&signer)?;
signed.save(remote, repository)?; signed.save(remote, repository)?;
@ -100,10 +97,7 @@ impl<'r> WriteStorage<'r> for Storage {
} }
impl Storage { impl Storage {
pub fn open<P: AsRef<Path>, S: Signer + 'static>( pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
path: P,
signer: S,
) -> Result<Self, io::Error> {
let path = path.as_ref().to_path_buf(); let path = path.as_ref().to_path_buf();
match fs::create_dir_all(&path) { match fs::create_dir_all(&path) {
@ -112,27 +106,13 @@ impl Storage {
Ok(()) => {} Ok(()) => {}
} }
Ok(Self { Ok(Self { path })
path,
signer: Arc::new(signer),
})
} }
pub fn path(&self) -> &Path { pub fn path(&self) -> &Path {
self.path.as_path() self.path.as_path()
} }
pub fn signer(&self) -> Arc<dyn Signer> {
self.signer.clone()
}
pub fn with_signer(&self, signer: impl Signer + 'static) -> Self {
Self {
path: self.path.clone(),
signer: Arc::new(signer),
}
}
pub fn projects(&self) -> Result<Vec<Id>, Error> { pub fn projects(&self) -> Result<Vec<Id>, Error> {
let mut projects = Vec::new(); let mut projects = Vec::new();
@ -404,7 +384,7 @@ mod tests {
fn test_fetch() { fn test_fetch() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let alice = fixtures::storage(tmp.path().join("alice")); let alice = fixtures::storage(tmp.path().join("alice"));
let bob = Storage::open(tmp.path().join("bob"), MockSigner::default()).unwrap(); let bob = Storage::open(tmp.path().join("bob")).unwrap();
let inventory = alice.inventory().unwrap(); let inventory = alice.inventory().unwrap();
let proj = inventory.first().unwrap(); let proj = inventory.first().unwrap();
let repo = alice.repository(proj).unwrap(); let repo = alice.repository(proj).unwrap();
@ -447,9 +427,9 @@ mod tests {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let mut rng = fastrand::Rng::new(); let mut rng = fastrand::Rng::new();
let signer = MockSigner::new(&mut rng); let signer = MockSigner::new(&mut rng);
let storage = Storage::open(tmp.path(), signer).unwrap(); let storage = Storage::open(tmp.path()).unwrap();
let proj_id = arbitrary::gen::<Id>(1); let proj_id = arbitrary::gen::<Id>(1);
let alice = *storage.public_key(); let alice = *signer.public_key();
let project = storage.repository(&proj_id).unwrap(); let project = storage.repository(&proj_id).unwrap();
let backend = &project.backend; let backend = &project.backend;
let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap(); let sig = git2::Signature::now(&alice.to_string(), "anonymous@radicle.xyz").unwrap();
@ -465,7 +445,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let signed = storage.sign_refs(&project).unwrap(); let signed = storage.sign_refs(&project, &signer).unwrap();
let remote = project.remote(&alice).unwrap(); let remote = project.remote(&alice).unwrap();
let mut unsigned = project.references(&alice).unwrap(); let mut unsigned = project.references(&alice).unwrap();

View File

@ -157,7 +157,7 @@ impl DerefMut for Refs {
/// Combination of [`Refs`] and a [`Signature`]. The signature is a cryptographic /// 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 /// signature over the refs. This allows us to easily verify if a set of refs
/// came from a particular user. /// came from a particular key.
/// ///
/// The type parameter keeps track of whether the signature was [`Verified`] or /// The type parameter keeps track of whether the signature was [`Verified`] or
/// [`Unverified`]. /// [`Unverified`].

View File

@ -13,7 +13,10 @@ use crate::crypto::{PublicKey, SecretKey};
use crate::git; use crate::git;
use crate::hash; use crate::hash;
use crate::identity::{Delegate, Did, Doc, Id, Project}; use crate::identity::{Delegate, Did, Doc, Id, Project};
use crate::protocol::message::{Address, Envelope, Message, MessageType, NodeAnnouncement}; use crate::protocol::message::{
Address, Envelope, InventoryAnnouncement, Message, MessageType, NodeAnnouncement,
RefsAnnouncement,
};
use crate::protocol::{NodeId, Timestamp}; use crate::protocol::{NodeId, Timestamp};
use crate::storage; use crate::storage;
use crate::storage::refs::{Refs, SignedRefs}; use crate::storage::refs::{Refs, SignedRefs};
@ -70,30 +73,31 @@ impl Arbitrary for Message {
fn arbitrary(g: &mut quickcheck::Gen) -> Self { fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let type_id = g let type_id = g
.choose(&[ .choose(&[
MessageType::GetInventory, MessageType::InventoryAnnouncement,
MessageType::Inventory, MessageType::NodeAnnouncement,
MessageType::Node, MessageType::RefsAnnouncement,
MessageType::RefsUpdate,
]) ])
.unwrap(); .unwrap();
match type_id { match type_id {
MessageType::GetInventory => Self::GetInventory { MessageType::InventoryAnnouncement => Self::InventoryAnnouncement {
ids: Vec::<Id>::arbitrary(g),
},
MessageType::Inventory => Self::Inventory {
node: NodeId::arbitrary(g), node: NodeId::arbitrary(g),
inv: Vec::<Id>::arbitrary(g), message: InventoryAnnouncement {
inventory: Vec::<Id>::arbitrary(g),
timestamp: Timestamp::arbitrary(g), timestamp: Timestamp::arbitrary(g),
}, },
MessageType::RefsUpdate => Self::RefsUpdate { signature: crypto::Signature::from(ByteArray::<64>::arbitrary(g).into_inner()),
id: Id::arbitrary(g),
signer: PublicKey::arbitrary(g),
refs: SignedRefs::<Unverified>::arbitrary(g),
}, },
MessageType::Node => { MessageType::RefsAnnouncement => Self::RefsAnnouncement {
let announcement = NodeAnnouncement { node: NodeId::arbitrary(g),
id: 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(), features: ByteArray::<32>::arbitrary(g).into_inner(),
timestamp: Timestamp::arbitrary(g), timestamp: Timestamp::arbitrary(g),
alias: ByteArray::<32>::arbitrary(g).into_inner(), alias: ByteArray::<32>::arbitrary(g).into_inner(),
@ -102,9 +106,10 @@ impl Arbitrary for Message {
let bytes: ByteArray<64> = Arbitrary::arbitrary(g); let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
let signature = crypto::Signature::from(bytes.into_inner()); let signature = crypto::Signature::from(bytes.into_inner());
Self::Node { Self::NodeAnnouncement {
node: NodeId::arbitrary(g),
signature, signature,
announcement, message,
} }
} }
_ => unreachable!(), _ => unreachable!(),

View File

@ -1,9 +1,10 @@
use std::path::Path; use std::path::Path;
use crate::crypto::Signer as _;
use crate::git; use crate::git;
use crate::identity::Id; use crate::identity::Id;
use crate::storage::git::Storage; use crate::storage::git::Storage;
use crate::storage::{ReadStorage, WriteStorage}; use crate::storage::WriteStorage;
use crate::test::arbitrary; use crate::test::arbitrary;
use crate::test::crypto::MockSigner; use crate::test::crypto::MockSigner;
@ -11,15 +12,12 @@ pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
let path = path.as_ref(); let path = path.as_ref();
let proj_ids = arbitrary::set::<Id>(3..=3); let proj_ids = arbitrary::set::<Id>(3..=3);
let signers = arbitrary::set::<MockSigner>(3..=3); let signers = arbitrary::set::<MockSigner>(3..=3);
let mut storages = signers let storage = Storage::open(path).unwrap();
.into_iter()
.map(|s| Storage::open(path, s).unwrap())
.collect::<Vec<_>>();
crate::test::logger::init(log::Level::Debug); crate::test::logger::init(log::Level::Debug);
for storage in &storages { for signer in signers {
let remote = storage.public_key(); let remote = signer.public_key();
log::debug!("signer {}...", remote); log::debug!("signer {}...", remote);
@ -57,10 +55,10 @@ pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
) )
.unwrap(); .unwrap();
storage.sign_refs(&repo).unwrap(); storage.sign_refs(&repo, &signer).unwrap();
} }
} }
storages.pop().unwrap() storage
} }
/// Creates a regular repository at the given path with a couple of commits. /// Creates a regular repository at the given path with a couple of commits.

View File

@ -14,11 +14,7 @@ impl Log for Logger {
match record.target() { match record.target() {
"test" => { "test" => {
println!( println!("{} {}", "test:".cyan(), record.args().to_string().yellow())
"{} {}",
"test:".yellow(),
record.args().to_string().yellow()
)
} }
"sim" => { "sim" => {
println!("{} {}", "sim:".bold(), record.args().to_string().bold()) println!("{} {}", "sim:".bold(), record.args().to_string().bold())
@ -26,11 +22,21 @@ impl Log for Logger {
target => { target => {
if self.enabled(record.metadata()) { if self.enabled(record.metadata()) {
let s = format!("{:<8} {}", format!("{}:", target), record.args()); 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()); println!("{}", s.dimmed());
} }
} }
} }
} }
}
}
fn flush(&self) {} fn flush(&self) {}
} }

View File

@ -124,8 +124,8 @@ where
self.config().git_url.clone() self.config().git_url.clone()
} }
pub fn id(&self) -> NodeId { pub fn node_id(&self) -> NodeId {
self.protocol.id() self.protocol.node_id()
} }
pub fn receive(&mut self, peer: &net::SocketAddr, msg: Message) { pub fn receive(&mut self, peer: &net::SocketAddr, msg: Message) {
@ -145,7 +145,7 @@ where
self.receive( self.receive(
&remote, &remote,
Message::hello( Message::hello(
peer.id(), peer.node_id(),
self.local_time().as_secs(), self.local_time().as_secs(),
vec![Address::from(remote)], vec![Address::from(remote)],
git, git,
@ -155,8 +155,8 @@ where
let mut msgs = self.messages(&remote); let mut msgs = self.messages(&remote);
msgs.find(|m| matches!(m, Message::Hello { .. })) msgs.find(|m| matches!(m, Message::Hello { .. }))
.expect("`hello` is sent"); .expect("`hello` is sent");
msgs.find(|m| matches!(m, Message::GetInventory { .. })) msgs.find(|m| matches!(m, Message::InventoryAnnouncement { .. }))
.expect("`get-inventory` is sent"); .expect("`inventory-announcement` is sent");
} }
pub fn connect_to(&mut self, peer: &Self) { pub fn connect_to(&mut self, peer: &Self) {
@ -170,14 +170,14 @@ where
let mut msgs = self.messages(&remote); let mut msgs = self.messages(&remote);
msgs.find(|m| matches!(m, Message::Hello { .. })) msgs.find(|m| matches!(m, Message::Hello { .. }))
.expect("`hello` is sent"); .expect("`hello` is sent");
msgs.find(|m| matches!(m, Message::GetInventory { .. })) msgs.find(|m| matches!(m, Message::InventoryAnnouncement { .. }))
.expect("`get-inventory` is sent"); .expect("`inventory-announcement` is sent");
let git = peer.config().git_url.clone(); let git = peer.config().git_url.clone();
self.receive( self.receive(
&remote, &remote,
Message::hello( Message::hello(
peer.id(), peer.node_id(),
self.local_time().as_secs(), self.local_time().as_secs(),
peer.config().listen.clone(), peer.config().listen.clone(),
git, git,

View File

@ -1,6 +1,6 @@
use git_url::Url; use git_url::Url;
use crate::crypto::{PublicKey, Verified}; use crate::crypto::{Signer, Verified};
use crate::git; use crate::git;
use crate::identity::{Id, Project}; use crate::identity::{Id, Project};
use crate::storage::{refs, RefUpdate}; use crate::storage::{refs, RefUpdate};
@ -26,10 +26,6 @@ impl MockStorage {
} }
impl ReadStorage for MockStorage { impl ReadStorage for MockStorage {
fn public_key(&self) -> &PublicKey {
todo!()
}
fn url(&self) -> Url { fn url(&self) -> Url {
Url { Url {
scheme: git_url::Scheme::Radicle, scheme: git_url::Scheme::Radicle,
@ -38,7 +34,7 @@ impl ReadStorage for MockStorage {
} }
} }
fn get(&self, proj: &Id) -> Result<Option<Project>, Error> { fn get(&self, _remote: &RemoteId, proj: &Id) -> Result<Option<Project>, Error> {
if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) { if let Some(proj) = self.inventory.iter().find(|p| p.id == *proj) {
return Ok(Some(proj.clone())); return Ok(Some(proj.clone()));
} }
@ -63,9 +59,10 @@ impl WriteStorage<'_> for MockStorage {
Ok(MockRepository {}) Ok(MockRepository {})
} }
fn sign_refs( fn sign_refs<G: Signer>(
&self, &self,
_repository: &Self::Repository, _repository: &Self::Repository,
_signer: G,
) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> { ) -> Result<crate::storage::refs::SignedRefs<Verified>, Error> {
todo!() todo!()
} }

View File

@ -14,7 +14,6 @@ use crate::protocol::peer::*;
use crate::protocol::*; use crate::protocol::*;
use crate::storage::git::Storage; use crate::storage::git::Storage;
use crate::storage::ReadStorage; use crate::storage::ReadStorage;
use crate::test::crypto::MockSigner;
use crate::test::fixtures; use crate::test::fixtures;
#[allow(unused)] #[allow(unused)]
use crate::test::logger; use crate::test::logger;
@ -114,7 +113,7 @@ fn test_handshake_invalid_timestamp() {
alice.receive( alice.receive(
&bob.addr(), &bob.addr(),
Message::hello( Message::hello(
bob.id(), bob.node_id(),
alice.timestamp() - time_delta, alice.timestamp() - time_delta,
vec![], vec![],
bob.git_url(), bob.git_url(),
@ -135,7 +134,7 @@ fn test_inventory_sync() {
let mut alice = Peer::new( let mut alice = Peer::new(
"alice", "alice",
[7, 7, 7, 7], [7, 7, 7, 7],
Storage::open(tmp.path().join("alice"), MockSigner::default()).unwrap(), Storage::open(tmp.path().join("alice")).unwrap(),
); );
let bob_storage = fixtures::storage(tmp.path().join("bob")); let bob_storage = fixtures::storage(tmp.path().join("bob"));
let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage); let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
@ -145,16 +144,18 @@ fn test_inventory_sync() {
alice.connect_to(&bob); alice.connect_to(&bob);
alice.receive( alice.receive(
&bob.addr(), &bob.addr(),
Message::Inventory { Message::inventory(
node: bob.id(), InventoryAnnouncement {
inventory: projs.clone(),
timestamp: now, timestamp: now,
inv: projs.clone(),
}, },
bob.signer(),
),
); );
for proj in &projs { for proj in &projs {
let seeds = alice.routing().get(proj).unwrap(); let seeds = alice.routing().get(proj).unwrap();
assert!(seeds.contains(&bob.id())); assert!(seeds.contains(&bob.node_id()));
} }
let a = alice let a = alice
@ -212,11 +213,13 @@ fn test_inventory_relay_bad_timestamp() {
alice.connect_to(&bob); alice.connect_to(&bob);
alice.receive( alice.receive(
&bob.addr(), &bob.addr(),
Message::Inventory { Message::inventory(
node: bob.id(), InventoryAnnouncement {
inventory: vec![],
timestamp, timestamp,
inv: vec![],
}, },
bob.signer(),
),
); );
assert_matches!( assert_matches!(
alice.outbox().next(), alice.outbox().next(),
@ -239,16 +242,18 @@ fn test_inventory_relay() {
alice.connect_from(&eve); alice.connect_from(&eve);
alice.receive( alice.receive(
&bob.addr(), &bob.addr(),
Message::Inventory { Message::inventory(
node: bob.id(), InventoryAnnouncement {
inventory: inv.clone(),
timestamp: now, timestamp: now,
inv: inv.clone(),
}, },
bob.signer(),
),
); );
assert_matches!( assert_matches!(
alice.messages(&eve.addr()).next(), alice.messages(&eve.addr()).next(),
Some(Message::Inventory { node, timestamp, .. }) Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement { timestamp, .. }, .. })
if node == bob.id() && timestamp == now if node == bob.node_id() && timestamp == now
); );
assert_matches!( assert_matches!(
alice.messages(&bob.addr()).next(), alice.messages(&bob.addr()).next(),
@ -258,11 +263,13 @@ fn test_inventory_relay() {
alice.receive( alice.receive(
&bob.addr(), &bob.addr(),
Message::Inventory { Message::inventory(
node: bob.id(), InventoryAnnouncement {
inventory: inv.clone(),
timestamp: now, timestamp: now,
inv: inv.clone(),
}, },
bob.signer(),
),
); );
assert_matches!( assert_matches!(
alice.messages(&eve.addr()).next(), alice.messages(&eve.addr()).next(),
@ -272,32 +279,36 @@ fn test_inventory_relay() {
alice.receive( alice.receive(
&bob.addr(), &bob.addr(),
Message::Inventory { Message::inventory(
node: bob.id(), InventoryAnnouncement {
inventory: inv.clone(),
timestamp: now + 1, timestamp: now + 1,
inv: inv.clone(),
}, },
bob.signer(),
),
); );
assert_matches!( assert_matches!(
alice.messages(&eve.addr()).next(), alice.messages(&eve.addr()).next(),
Some(Message::Inventory { node, timestamp, .. }) Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement{ timestamp, .. }, .. })
if node == bob.id() && timestamp == now + 1, if node == bob.node_id() && timestamp == now + 1,
"Sending a new inventory does trigger the relay" "Sending a new inventory does trigger the relay"
); );
// Inventory from Eve relayed to Bob. // Inventory from Eve relayed to Bob.
alice.receive( alice.receive(
&eve.addr(), &eve.addr(),
Message::Inventory { Message::inventory(
node: eve.id(), InventoryAnnouncement {
inventory: inv,
timestamp: now, timestamp: now,
inv,
}, },
eve.signer(),
),
); );
assert_matches!( assert_matches!(
alice.messages(&bob.addr()).next(), alice.messages(&bob.addr()).next(),
Some(Message::Inventory { node, timestamp, .. }) Some(Message::InventoryAnnouncement { node, message: InventoryAnnouncement { timestamp, .. }, .. })
if node == eve.id() && timestamp == now if node == eve.node_id() && timestamp == now
); );
} }
@ -374,26 +385,14 @@ fn test_push_and_pull() {
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let storage_alice = Storage::open( let storage_alice = Storage::open(tempdir.path().join("alice").join("storage")).unwrap();
tempdir.path().join("alice").join("storage"),
MockSigner::default(),
)
.unwrap();
let repo = fixtures::repository(tempdir.path().join("working")); let repo = fixtures::repository(tempdir.path().join("working"));
let mut alice = Peer::new("alice", [7, 7, 7, 7], storage_alice); let mut alice = Peer::new("alice", [7, 7, 7, 7], storage_alice);
let storage_bob = Storage::open( let storage_bob = Storage::open(tempdir.path().join("bob").join("storage")).unwrap();
tempdir.path().join("bob").join("storage"),
MockSigner::default(),
)
.unwrap();
let mut bob = Peer::new("bob", [8, 8, 8, 8], storage_bob); let mut bob = Peer::new("bob", [8, 8, 8, 8], storage_bob);
let storage_eve = Storage::open( let storage_eve = Storage::open(tempdir.path().join("eve").join("storage")).unwrap();
tempdir.path().join("eve").join("storage"),
MockSigner::default(),
)
.unwrap();
let mut eve = Peer::new("eve", [9, 9, 9, 9], storage_eve); let mut eve = Peer::new("eve", [9, 9, 9, 9], storage_eve);
// Alice and Bob connect to Eve. // Alice and Bob connect to Eve.
@ -416,7 +415,8 @@ fn test_push_and_pull() {
"alice", "alice",
"alice's repo", "alice's repo",
storage::BranchName::from("master"), storage::BranchName::from("master"),
alice.storage_mut(), alice.signer(),
alice.storage(),
) )
.unwrap(); .unwrap();
@ -429,16 +429,25 @@ fn test_push_and_pull() {
eve.command(protocol::Command::Track(proj_id.clone(), sender)); eve.command(protocol::Command::Track(proj_id.clone(), sender));
// Neither of them have it in the beginning. // Neither of them have it in the beginning.
assert!(eve.storage().get(&proj_id).unwrap().is_none()); assert!(eve.get(&proj_id).unwrap().is_none());
assert!(bob.storage().get(&proj_id).unwrap().is_none()); assert!(bob.get(&proj_id).unwrap().is_none());
// Alice announces her refs. // Alice announces her refs.
// We now expect Eve to fetch Alice's project from Alice. // We now expect Eve to fetch Alice's project from Alice.
// Then we expect Bob to fetch Alice's project from Eve. // Then we expect Bob to fetch Alice's project from Eve.
alice.command(protocol::Command::AnnounceRefsUpdate(proj_id.clone())); alice.command(protocol::Command::AnnounceRefs(proj_id.clone()));
sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled()); sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
assert!(eve.storage().get(&proj_id).unwrap().is_some());
assert!(bob.storage().get(&proj_id).unwrap().is_some()); 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!( assert_matches!(
sim.events(&bob.ip).next(), sim.events(&bob.ip).next(),
Some(protocol::Event::RefsFetched { from, .. }) Some(protocol::Event::RefsFetched { from, .. })
@ -457,9 +466,9 @@ fn prop_inventory_exchange_dense() {
let mut routing = Routing::with_hasher(rng.clone().into()); let mut routing = Routing::with_hasher(rng.clone().into());
for (inv, peer) in &[ for (inv, peer) in &[
(alice_inv.inventory, alice.id()), (alice_inv.inventory, alice.node_id()),
(bob_inv.inventory, bob.id()), (bob_inv.inventory, bob.node_id()),
(eve_inv.inventory, eve.id()), (eve_inv.inventory, eve.node_id()),
] { ] {
for proj in inv { for proj in inv {
routing routing
@ -475,7 +484,11 @@ fn prop_inventory_exchange_dense() {
eve.command(Command::Connect(alice.addr())); eve.command(Command::Connect(alice.addr()));
eve.command(Command::Connect(bob.addr())); eve.command(Command::Connect(bob.addr()));
let mut peers: HashMap<_, _> = [(alice.id(), alice), (bob.id(), bob), (eve.id(), eve)] let mut peers: HashMap<_, _> = [
(alice.node_id(), alice),
(bob.node_id(), bob),
(eve.node_id(), eve),
]
.into_iter() .into_iter()
.collect(); .collect();
let mut simulator = Simulation::new(LocalTime::now(), rng, simulator::Options::default()) let mut simulator = Simulation::new(LocalTime::now(), rng, simulator::Options::default())
@ -488,14 +501,12 @@ fn prop_inventory_exchange_dense() {
let lookup = peer.lookup(proj_id); let lookup = peer.lookup(proj_id);
if lookup.local.is_some() { if lookup.local.is_some() {
peer.storage() peer.get(proj_id)
.get(proj_id)
.expect("There are no errors querying storage") .expect("There are no errors querying storage")
.expect("The project is available locally"); .expect("The project is available locally");
} else { } else {
for remote in &lookup.remote { for remote in &lookup.remote {
peers[remote] peers[remote]
.storage()
.get(proj_id) .get(proj_id)
.expect("There are no errors querying storage") .expect("There are no errors querying storage")
.expect("The project is available remotely"); .expect("The project is available remotely");