Split repository into two creates

* `radicle`: core library
* `radicle-node`: node

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-09-21 15:11:17 +02:00
parent d1ccf1b807
commit 46501507be
No known key found for this signature in database
32 changed files with 499 additions and 485 deletions

89
Cargo.lock generated
View File

@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "arrayref"
@ -114,12 +114,6 @@ dependencies = [
"siphasher",
]
[[package]]
name = "bs58"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
[[package]]
name = "bstr"
version = "0.2.17"
@ -277,9 +271,9 @@ dependencies = [
[[package]]
name = "digest"
version = "0.10.3"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [
"block-buffer 0.10.3",
"crypto-common",
@ -496,9 +490,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.132"
version = "0.2.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
[[package]]
name = "libgit2-sys"
@ -646,9 +640,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "opaque-debug"
@ -764,6 +758,31 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radicle"
version = "0.2.0"
dependencies = [
"ed25519-compact",
"fastrand",
"git-ref-format",
"git-url",
"git2",
"log",
"multibase",
"nonempty",
"olpc-cjson",
"once_cell",
"quickcheck",
"quickcheck_macros",
"radicle-git-ext",
"serde",
"serde_json",
"sha2 0.10.6",
"siphasher",
"tempfile",
"thiserror",
]
[[package]]
name = "radicle-git-ext"
version = "0.1.0"
@ -784,30 +803,20 @@ version = "0.2.0"
dependencies = [
"anyhow",
"bloomy",
"bs58",
"byteorder",
"chrono",
"colored",
"crossbeam-channel",
"ed25519-compact",
"fastrand",
"git-ref-format",
"git-url",
"git2",
"log",
"multibase",
"nakamoto-net",
"nakamoto-net-poll",
"nonempty",
"olpc-cjson",
"once_cell",
"quickcheck",
"quickcheck_macros",
"radicle-git-ext",
"radicle",
"serde",
"serde_json",
"sha2 0.10.5",
"siphasher",
"tempfile",
"thiserror",
]
@ -828,9 +837,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
@ -919,13 +928,13 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.3",
"digest 0.10.5",
]
[[package]]
@ -958,9 +967,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
dependencies = [
"proc-macro2",
"quote",
@ -983,18 +992,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.34"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.34"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
dependencies = [
"proc-macro2",
"quote",
@ -1041,15 +1050,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "unicode-normalization"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]

View File

@ -1,5 +1,5 @@
[workspace]
members = ["radicle-node"]
members = ["radicle", "radicle-node"]
[patch.crates-io.radicle-git-ext]
git = "https://github.com/radicle-dev/radicle-link"

View File

@ -7,32 +7,26 @@ edition = "2021"
[dependencies]
anyhow = { version = "1" }
bs58 = { version = "0.4.0" }
ed25519-compact = { version = "1.0.12", features = ["pem"] }
byteorder = { version = "1" }
bloomy = { version = "1.2" }
chrono = { version = "0.4.0" }
colored = { version = "1.9.0" }
crossbeam-channel = { version = "0.5.6" }
fastrand = { version = "1.8.0" }
git-ref-format = { version = "0", features = ["serde", "macro"] }
git2 = { version = "0.13" }
git-url = { version = "0.3.5", features = ["serde1"] }
multibase = { version = "0.9.1" }
log = { version = "0.4.17", features = ["std"] }
once_cell = { version = "1.13" }
olpc-cjson = { version = "0.1.1" }
sha2 = { version = "0.10.2" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
siphasher = { version = "0.3.10" }
radicle-git-ext = { version = "0", features = ["serde"] }
nonempty = { version = "0.8.0", features = ["serialize"] }
nakamoto-net = { version = "0.3.0" }
nakamoto-net-poll = { version = "0.3.0" }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
[dependencies.radicle]
path = "../radicle"
version = "0.2.0"
[dev-dependencies]
radicle = { path = "../radicle", version = "*", features = ["test"] }
quickcheck = { version = "1", default-features = false }
quickcheck_macros = { version = "1", default-features = false }

View File

@ -5,23 +5,17 @@ pub mod address_book;
pub mod address_manager;
pub mod client;
pub mod clock;
pub mod collections;
pub mod control;
pub mod crypto;
pub mod decoder;
pub mod git;
pub mod hash;
pub mod identity;
pub mod logger;
pub mod rad;
pub mod serde_ext;
pub mod service;
pub mod storage;
#[cfg(test)]
pub mod test;
pub mod transport;
pub mod wire;
pub use radicle::{collections, crypto, git, hash, identity, rad, storage};
pub mod prelude {
pub use crate::crypto::{PublicKey, Signature, Signer};
pub use crate::decoder::Decoder;

View File

@ -9,7 +9,6 @@ use std::{collections::VecDeque, fmt, net, net::IpAddr};
use crossbeam_channel as chan;
use fastrand::Rng;
use git_url::Url;
use log::*;
use nakamoto::{LocalDuration, LocalTime};
use nakamoto_net as nakamoto;
@ -22,6 +21,8 @@ use crate::address_manager::AddressManager;
use crate::clock::RefClock;
use crate::collections::{HashMap, HashSet};
use crate::crypto;
use crate::git;
use crate::git::Url;
use crate::identity::{Id, Project};
use crate::service::config::ProjectTracking;
use crate::service::message::{NodeAnnouncement, RefsAnnouncement};
@ -81,7 +82,7 @@ pub enum Event {
#[derive(thiserror::Error, Debug)]
pub enum FetchError {
#[error(transparent)]
Git(#[from] git2::Error),
Git(#[from] git::raw::Error),
#[error(transparent)]
Storage(#[from] storage::Error),
#[error(transparent)]
@ -389,7 +390,7 @@ impl<'r, T: WriteStorage<'r>, S: address_book::Store, G: crypto::Signer> Service
// TODO: Limit the number of seeds we fetch from? Randomize?
for (_, peer) in seeds {
match repo.fetch(&Url {
scheme: git_url::Scheme::Git,
scheme: git::url::Scheme::Git,
host: Some(peer.addr.ip().to_string()),
port: Some(peer.addr.port()),
// TODO: Fix upstream crate so that it adds a `/` when needed.

View File

@ -1,9 +1,8 @@
use std::net;
use git_url::Url;
use crate::collections::HashSet;
use crate::git;
use crate::git::Url;
use crate::identity::{Id, PublicKey};
use crate::service::message::{Address, Envelope, Message};

View File

@ -288,7 +288,7 @@ mod tests {
use quickcheck_macros::quickcheck;
use crate::crypto::Signer;
use crate::test::crypto::MockSigner;
use crate::test::signer::MockSigner;
#[quickcheck]
fn prop_refs_announcement_signing(id: Id, refs: Refs) {

View File

@ -1,10 +1,11 @@
pub(crate) mod arbitrary;
pub(crate) mod assert;
pub(crate) mod crypto;
pub(crate) mod fixtures;
pub(crate) mod handle;
pub(crate) mod logger;
pub(crate) mod peer;
pub(crate) mod simulator;
pub(crate) mod storage;
pub(crate) mod tests;
#[cfg(test)]
pub use radicle::assert_matches;
#[cfg(test)]
pub use radicle::test::*;

View File

@ -1,72 +1,20 @@
use std::collections::{BTreeMap, HashSet};
use std::hash::Hash;
use std::iter;
use std::net;
use std::ops::RangeBounds;
use std::path::PathBuf;
use bloomy::BloomFilter;
use nonempty::NonEmpty;
use quickcheck::Arbitrary;
use crate::collections::HashMap;
use crate::crypto;
use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified};
use crate::git;
use crate::hash;
use crate::identity::{doc::Delegate, doc::Doc, Did, Id, Project};
use crate::identity::Id;
use crate::service::filter::{Filter, FILTER_SIZE};
use crate::service::message::{
Address, Envelope, InventoryAnnouncement, Message, NodeAnnouncement, RefsAnnouncement,
Subscribe,
};
use crate::service::{NodeId, Timestamp};
use crate::storage;
use crate::storage::refs::{Refs, SignedRefs};
use crate::test::storage::MockStorage;
use crate::storage::refs::Refs;
use crate::wire::message::MessageType;
use super::crypto::MockSigner;
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
let size = fastrand::usize(range);
let mut set = HashSet::with_capacity(size);
let mut g = quickcheck::Gen::new(size);
while set.len() < size {
set.insert(T::arbitrary(&mut g));
}
set
}
pub fn gen<T: Arbitrary>(size: usize) -> T {
let mut gen = quickcheck::Gen::new(size);
T::arbitrary(&mut gen)
}
#[derive(Clone, Debug)]
pub struct ByteArray<const N: usize>([u8; N]);
impl<const N: usize> ByteArray<N> {
pub fn into_inner(self) -> [u8; N] {
self.0
}
pub fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
}
impl<const N: usize> Arbitrary for ByteArray<N> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut bytes: [u8; N] = [0; N];
for byte in &mut bytes {
*byte = u8::arbitrary(g);
}
Self(bytes)
}
}
pub use radicle::test::arbitrary::*;
impl Arbitrary for Filter {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
@ -159,178 +107,3 @@ impl Arbitrary for Address {
}
}
}
impl Arbitrary for storage::Remotes<crypto::Verified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
Arbitrary::arbitrary(g);
storage::Remotes::new(remotes)
}
}
impl Arbitrary for MockStorage {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let inventory = Arbitrary::arbitrary(g);
MockStorage::new(inventory)
}
}
impl Arbitrary for Project {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let doc = Doc::<Verified>::arbitrary(g);
let (oid, _) = doc.encode().unwrap();
let id = Id::from(oid);
let remotes = storage::Remotes::arbitrary(g);
let path = PathBuf::arbitrary(g);
Self {
id,
doc,
remotes,
path,
}
}
}
impl Arbitrary for Did {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self::from(PublicKey::arbitrary(g))
}
}
impl Arbitrary for Delegate {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self {
name: String::arbitrary(g),
id: Did::arbitrary(g),
}
}
}
impl Arbitrary for Doc<Unverified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let name = String::arbitrary(g);
let description = String::arbitrary(g);
let default_branch = String::arbitrary(g);
let delegate = Delegate::arbitrary(g);
Self::initial(name, description, default_branch, delegate)
}
}
impl Arbitrary for Doc<Verified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let name = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16))
.collect();
let description = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(0..32))
.collect();
let default_branch = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16))
.collect();
let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
name: iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16))
.collect(),
id: Did::arbitrary(g),
})
.take(rng.usize(1..6))
.collect::<Vec<_>>()
.try_into()
.unwrap();
let threshold = delegates.len() / 2 + 1;
let doc: Doc<Unverified> =
Doc::new(name, description, default_branch, delegates, threshold);
doc.verified().unwrap()
}
}
impl Arbitrary for SignedRefs<Unverified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
let signature = crypto::Signature::from(bytes.into_inner());
let refs = Refs::arbitrary(g);
Self::new(refs, signature)
}
}
impl Arbitrary for Refs {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
let mut bytes: [u8; 20] = [0; 20];
let names = &[
"heads/master",
"heads/feature/1",
"heads/feature/2",
"heads/feature/3",
"heads/radicle/id",
"tags/v1.0",
"tags/v2.0",
"notes/1",
];
for _ in 0..g.size().min(names.len()) {
if let Some(name) = g.choose(names) {
for byte in &mut bytes {
*byte = u8::arbitrary(g);
}
let oid = storage::Oid::try_from(&bytes[..]).unwrap();
let name = git::RefString::try_from(*name).unwrap();
refs.insert(name, oid);
}
}
Self::from(refs)
}
}
impl Arbitrary for storage::Remote<crypto::Verified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let refs = Refs::arbitrary(g);
let signer = MockSigner::arbitrary(g);
let signed = refs.signed(&signer).unwrap();
storage::Remote::new(*signer.public_key(), signed)
}
}
impl Arbitrary for MockSigner {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
let seed = Seed::new(bytes.into_inner());
let sk = KeyPair::from_seed(seed).sk;
MockSigner::from(sk)
}
}
impl Arbitrary for Id {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes = ByteArray::<20>::arbitrary(g);
let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
Id::from(oid)
}
}
impl Arbitrary for hash::Digest {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: Vec<u8> = Arbitrary::arbitrary(g);
hash::Digest::new(&bytes)
}
}
impl Arbitrary for PublicKey {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
let seed = Seed::new(bytes.into_inner());
let keypair = KeyPair::from_seed(seed);
PublicKey(keypair.pk)
}
}

View File

@ -1,116 +0,0 @@
use std::path::Path;
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::Id;
use crate::rad;
use crate::storage::git::Storage;
use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, WriteStorage};
use crate::test::arbitrary;
use crate::test::crypto::MockSigner;
pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
let path = path.as_ref();
let proj_ids = arbitrary::set::<Id>(3..=3);
let signers = arbitrary::set::<MockSigner>(3..=3);
let storage = Storage::open(path).unwrap();
crate::test::logger::init(log::Level::Debug);
for signer in signers {
let remote = signer.public_key();
log::debug!("signer {}...", remote);
for proj in proj_ids.iter() {
let repo = storage.repository(proj).unwrap();
let raw = &repo.backend;
let sig = git2::Signature::now(&remote.to_string(), "anonymous@radicle.xyz").unwrap();
let head = git::initial_commit(raw, &sig).unwrap();
log::debug!("{}: creating {}...", remote, proj);
raw.reference(
&format!("refs/remotes/{remote}/heads/radicle/id"),
head.id(),
false,
"test",
)
.unwrap();
let head = git::commit(
raw,
&head,
&git::RefString::try_from(format!("refs/remotes/{remote}/heads/master")).unwrap(),
"Second commit",
&remote.to_string(),
)
.unwrap();
git::commit(
raw,
&head,
&git::RefString::try_from(format!("refs/remotes/{remote}/heads/patch/3")).unwrap(),
"Third commit",
&remote.to_string(),
)
.unwrap();
storage.sign_refs(&repo, &signer).unwrap();
}
}
storage
}
/// Create a new repository at the given path, and initialize it into a project.
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
path: P,
storage: &'r S,
signer: G,
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
let (repo, head) = repository(path);
let (id, refs) = rad::init(
&repo,
"acme",
"Acme's repository",
BranchName::from("master"),
signer,
storage,
)?;
Ok((id, refs, repo, head))
}
/// Creates a regular repository at the given path with a couple of commits.
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
let repo = git2::Repository::init(path).unwrap();
let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
let head = git::initial_commit(&repo, &sig).unwrap();
let oid = git::commit(
&repo,
&head,
git::refname!("refs/heads/master").as_refstr(),
"Second commit",
"anonymous",
)
.unwrap()
.id();
// Look, I don't really understand why we have to do this, but we do.
drop(head);
(repo, oid)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smoke() {
let tmp = tempfile::tempdir().unwrap();
storage(&tmp.path());
}
}

View File

@ -1,18 +1,18 @@
use std::net;
use std::ops::{Deref, DerefMut};
use git_url::Url;
use log::*;
use crate::address_book::{KnownAddress, Source};
use crate::clock::RefClock;
use crate::collections::HashMap;
use crate::git::Url;
use crate::service;
use crate::service::config::*;
use crate::service::message::*;
use crate::service::*;
use crate::storage::WriteStorage;
use crate::test::crypto::MockSigner;
use crate::test::signer::MockSigner;
use crate::test::simulator;
use crate::{Link, LocalTime};

View File

@ -11,15 +11,17 @@ use crate::service::peer::*;
use crate::service::*;
use crate::storage::git::Storage;
use crate::storage::ReadStorage;
use crate::test::assert_matches;
use crate::test::fixtures;
#[allow(unused)]
use crate::test::logger;
use crate::test::peer::Peer;
use crate::test::signer::MockSigner;
use crate::test::simulator;
use crate::test::simulator::{Peer as _, Simulation};
use crate::test::storage::MockStorage;
use crate::{assert_matches, Link, LocalTime};
use crate::{client, identity, rad, service, storage, test};
use crate::{Link, LocalTime};
// NOTE
//
@ -136,7 +138,8 @@ fn test_inventory_sync() {
[7, 7, 7, 7],
Storage::open(tmp.path().join("alice")).unwrap(),
);
let bob_storage = fixtures::storage(tmp.path().join("bob"));
let bob_signer = MockSigner::default();
let bob_storage = fixtures::storage(tmp.path().join("bob"), bob_signer).unwrap();
let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
let now = LocalTime::now().as_secs();
let projs = bob.storage().inventory().unwrap();

View File

@ -12,7 +12,7 @@ use nakamoto_net as nakamoto;
use nakamoto_net::Link;
use crate::address_book;
use crate::crypto::{PublicKey, Signature, Signer};
use crate::crypto::{PublicKey, Signature, Signer, Unverified};
use crate::decoder::Decoder;
use crate::git;
use crate::git::fmt;
@ -21,6 +21,7 @@ use crate::identity::Id;
use crate::service;
use crate::service::filter;
use crate::storage::refs::Refs;
use crate::storage::refs::SignedRefs;
use crate::storage::WriteStorage;
/// The default type we use to represent sizes.
@ -266,7 +267,7 @@ 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>();
const expected: usize = mem::size_of::<git::raw::Oid>();
if len != expected {
return Err(Error::InvalidSize {
@ -276,7 +277,7 @@ impl Decode for git::Oid {
}
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::raw::Oid::from_bytes(&buf).expect("the buffer is exactly the right size");
let oid = git::Oid::from(oid);
Ok(oid)
@ -414,6 +415,26 @@ impl Decode for filter::Filter {
}
}
impl<V> 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 Decode for SignedRefs<Unverified> {
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
let refs = Refs::decode(reader)?;
let signature = Signature::decode(reader)?;
Ok(Self::new(refs, signature))
}
}
#[derive(Debug)]
pub struct Wire<S, T, G> {
inboxes: HashMap<IpAddr, Decoder>,

37
radicle/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "radicle"
license = "MIT OR Apache-2.0"
version = "0.2.0"
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
edition = "2021"
[features]
default = []
test = ["quickcheck"]
[dependencies]
ed25519-compact = { version = "1.0.12", features = ["pem"] }
fastrand = { version = "1.8.0" }
git-ref-format = { version = "0", features = ["serde", "macro"] }
git2 = { version = "0.13" }
git-url = { version = "0.3.5", features = ["serde1"] }
multibase = { version = "0.9.1" }
log = { version = "0.4.17", features = ["std"] }
once_cell = { version = "1.13" }
olpc-cjson = { version = "0.1.1" }
sha2 = { version = "0.10.2" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
siphasher = { version = "0.3.10" }
radicle-git-ext = { version = "0", features = ["serde"] }
nonempty = { version = "0.8.0", features = ["serialize"] }
tempfile = { version = "3.3.0" }
thiserror = { version = "1" }
[dependencies.quickcheck]
version = "1"
default-features = false
optional = true
[dev-dependencies]
quickcheck_macros = { version = "1", default-features = false }

View File

@ -11,6 +11,7 @@ use crate::storage::RemoteId;
pub use ext::Error;
pub use ext::Oid;
pub use git2 as raw;
pub use git_ref_format as fmt;
pub use git_ref_format::{refname, RefStr, RefString};
pub use git_url as url;

View File

@ -476,11 +476,12 @@ impl Identity<Untrusted> {
#[cfg(test)]
mod test {
use crate::prelude::Signer;
use crate::crypto::Signer;
use crate::rad;
use crate::storage::git::Storage;
use crate::storage::{ReadStorage, WriteStorage};
use crate::test::{crypto, fixtures};
use crate::test::fixtures;
use crate::test::signer::MockSigner;
use super::*;
use quickcheck_macros::quickcheck;
@ -490,9 +491,9 @@ mod test {
let tempdir = tempfile::tempdir().unwrap();
let mut rng = fastrand::Rng::new();
let alice = crypto::MockSigner::new(&mut rng);
let bob = crypto::MockSigner::new(&mut rng);
let eve = crypto::MockSigner::new(&mut rng);
let alice = MockSigner::new(&mut rng);
let bob = MockSigner::new(&mut rng);
let eve = MockSigner::new(&mut rng);
let storage = Storage::open(tempdir.path().join("storage")).unwrap();
let (id, _, _, _) =

10
radicle/src/lib.rs Normal file
View File

@ -0,0 +1,10 @@
pub mod collections;
pub mod crypto;
pub mod git;
pub mod hash;
pub mod identity;
pub mod rad;
pub mod serde_ext;
pub mod storage;
#[cfg(feature = "test")]
pub mod test;

View File

@ -207,12 +207,12 @@ mod tests {
use crate::identity::{Delegate, Did};
use crate::storage::git::Storage;
use crate::storage::{ReadStorage, WriteStorage};
use crate::test::{crypto, fixtures};
use crate::test::{fixtures, signer::MockSigner};
#[test]
fn test_init() {
let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default();
let signer = MockSigner::default();
let public_key = *signer.public_key();
let storage = Storage::open(tempdir.path().join("storage")).unwrap();
let (repo, _) = fixtures::repository(tempdir.path().join("working"));
@ -247,9 +247,9 @@ mod tests {
fn test_fork() {
let mut rng = fastrand::Rng::new();
let tempdir = tempfile::tempdir().unwrap();
let alice = crypto::MockSigner::new(&mut rng);
let alice = MockSigner::new(&mut rng);
let alice_id = alice.public_key();
let bob = crypto::MockSigner::new(&mut rng);
let bob = MockSigner::new(&mut rng);
let bob_id = bob.public_key();
let storage = Storage::open(tempdir.path().join("storage")).unwrap();
let (original, _) = fixtures::repository(tempdir.path().join("original"));
@ -280,7 +280,7 @@ mod tests {
#[test]
fn test_checkout() {
let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default();
let signer = MockSigner::default();
let remote_id = signer.public_key();
let storage = Storage::open(tempdir.path().join("storage")).unwrap();
let (original, _) = fixtures::repository(tempdir.path().join("original"));

View File

@ -5,12 +5,10 @@ use std::{fmt, fs, io};
use git_ref_format::refspec;
use once_cell::sync::Lazy;
pub use radicle_git_ext::Oid;
use crate::crypto::{Signer, Unverified, Verified};
use crate::git;
use crate::identity::{self, Doc};
use crate::identity::{Id, Project};
use crate::identity;
use crate::identity::{Doc, Id, Project};
use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs};
use crate::storage::{
@ -18,6 +16,8 @@ use crate::storage::{
WriteStorage,
};
pub use crate::git::*;
use super::{RefUpdate, RemoteId};
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
@ -50,12 +50,12 @@ impl fmt::Debug for Storage {
}
impl ReadStorage for Storage {
fn url(&self) -> git::Url {
git::Url {
fn url(&self) -> Url {
Url {
scheme: git_url::Scheme::File,
host: None,
path: self.path.to_string_lossy().to_string().into(),
..git::Url::default()
..Url::default()
}
}
@ -167,7 +167,7 @@ pub enum VerifyError {
#[error("invalid remote `{0}`")]
InvalidRemote(RemoteId),
#[error("invalid target `{2}` for reference `{1}` of remote `{0}`")]
InvalidRefTarget(RemoteId, git::RefString, git2::Oid),
InvalidRefTarget(RemoteId, RefString, git2::Oid),
#[error("invalid reference")]
InvalidRef,
#[error("ref error: {0}")]
@ -185,7 +185,7 @@ pub enum VerifyError {
impl Repository {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let backend = match git2::Repository::open_bare(path.as_ref()) {
Err(e) if git::ext::is_not_found_err(&e) => {
Err(e) if ext::is_not_found_err(&e) => {
let backend = git2::Repository::init_opts(
path,
git2::RepositoryInitOptions::new()
@ -239,7 +239,7 @@ impl Repository {
.remove(&refname)
.ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
if git::Oid::from(oid) != signed_oid {
if Oid::from(oid) != signed_oid {
return Err(VerifyError::InvalidRefTarget(remote_id, refname, oid));
}
}
@ -278,7 +278,7 @@ impl Repository {
}
/// Return the canonical identity [`git::Oid`] and document.
pub fn identity(&self) -> Result<(git::Oid, identity::Doc<Unverified>), IdentityError> {
pub fn identity(&self) -> Result<(Oid, identity::Doc<Unverified>), IdentityError> {
let mut heads = Vec::new();
for remote in self.remote_ids()? {
let remote = remote?;
@ -589,19 +589,20 @@ mod tests {
use crate::assert_matches;
use crate::git;
use crate::storage::refs::SIGNATURE_REF;
use crate::storage::{ReadStorage, RefUpdate, WriteRepository};
use crate::storage::{ReadRepository, ReadStorage, RefUpdate, WriteRepository};
use crate::test::arbitrary;
use crate::test::crypto::MockSigner;
use crate::test::fixtures;
use crate::test::signer::MockSigner;
#[test]
fn test_remote_refs() {
let dir = tempfile::tempdir().unwrap();
let storage = fixtures::storage(dir.path());
let signer = MockSigner::default();
let storage = fixtures::storage(dir.path(), &signer).unwrap();
let inv = storage.inventory().unwrap();
let proj = inv.first().unwrap();
let mut refs = git::remote_refs(&git::Url {
host: Some(dir.path().to_string_lossy().to_string()),
host: Some(storage.path().to_string_lossy().to_string()),
scheme: git_url::Scheme::File,
path: format!("/{}", proj).into(),
..git::Url::default()
@ -627,7 +628,8 @@ mod tests {
#[test]
fn test_fetch() {
let tmp = tempfile::tempdir().unwrap();
let alice = fixtures::storage(tmp.path().join("alice"));
let alice_signer = MockSigner::default();
let alice = fixtures::storage(tmp.path().join("alice"), alice_signer).unwrap();
let bob = Storage::open(tmp.path().join("bob")).unwrap();
let inventory = alice.inventory().unwrap();
let proj = inventory.first().unwrap();
@ -652,7 +654,7 @@ mod tests {
.unwrap();
// Four refs are created for each remote.
assert_eq!(updates.len(), remotes.len() * 4);
assert_eq!(updates.len(), remotes.len() * 3);
for update in updates {
assert_matches!(

View File

@ -17,7 +17,6 @@ use crate::git;
use crate::git::Oid;
use crate::storage;
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
use crate::wire;
pub static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("radicle/signature"));
pub const REFS_BLOB_PATH: &str = "refs";
@ -177,8 +176,9 @@ impl DerefMut for Refs {
/// [`Unverified`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedRefs<V> {
refs: Refs,
signature: Signature,
pub refs: Refs,
pub signature: Signature,
_verified: PhantomData<V>,
}
@ -315,30 +315,6 @@ 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;

5
radicle/src/test.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod arbitrary;
pub mod assert;
pub mod fixtures;
pub mod signer;
pub mod storage;

View File

@ -0,0 +1,234 @@
use std::collections::{BTreeMap, HashSet};
use std::hash::Hash;
use std::iter;
use std::ops::RangeBounds;
use std::path::PathBuf;
use nonempty::NonEmpty;
use quickcheck::Arbitrary;
use crate::collections::HashMap;
use crate::crypto;
use crate::crypto::{KeyPair, PublicKey, Seed, Signer, Unverified, Verified};
use crate::git;
use crate::hash;
use crate::identity::{doc::Delegate, doc::Doc, Did, Id, Project};
use crate::storage;
use crate::storage::refs::{Refs, SignedRefs};
use crate::test::signer::MockSigner;
use crate::test::storage::MockStorage;
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
let size = fastrand::usize(range);
let mut set = HashSet::with_capacity(size);
let mut g = quickcheck::Gen::new(size);
while set.len() < size {
set.insert(T::arbitrary(&mut g));
}
set
}
pub fn gen<T: Arbitrary>(size: usize) -> T {
let mut gen = quickcheck::Gen::new(size);
T::arbitrary(&mut gen)
}
#[derive(Clone, Debug)]
pub struct ByteArray<const N: usize>([u8; N]);
impl<const N: usize> ByteArray<N> {
pub fn into_inner(self) -> [u8; N] {
self.0
}
pub fn as_slice(&self) -> &[u8] {
self.0.as_slice()
}
}
impl<const N: usize> Arbitrary for ByteArray<N> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut bytes: [u8; N] = [0; N];
for byte in &mut bytes {
*byte = u8::arbitrary(g);
}
Self(bytes)
}
}
impl Arbitrary for storage::Remotes<crypto::Verified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let remotes: HashMap<storage::RemoteId, storage::Remote<crypto::Verified>> =
Arbitrary::arbitrary(g);
storage::Remotes::new(remotes)
}
}
impl Arbitrary for Project {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let doc = Doc::<Verified>::arbitrary(g);
let (oid, _) = doc.encode().unwrap();
let id = Id::from(oid);
let remotes = storage::Remotes::arbitrary(g);
let path = PathBuf::arbitrary(g);
Self {
id,
doc,
remotes,
path,
}
}
}
impl Arbitrary for Did {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self::from(PublicKey::arbitrary(g))
}
}
impl Arbitrary for Delegate {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self {
name: String::arbitrary(g),
id: Did::arbitrary(g),
}
}
}
impl Arbitrary for Doc<Unverified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let name = String::arbitrary(g);
let description = String::arbitrary(g);
let default_branch = String::arbitrary(g);
let delegate = Delegate::arbitrary(g);
Self::initial(name, description, default_branch, delegate)
}
}
impl Arbitrary for Doc<Verified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let name = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16))
.collect();
let description = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(0..32))
.collect();
let default_branch = iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16))
.collect();
let delegates: NonEmpty<_> = iter::repeat_with(|| Delegate {
name: iter::repeat_with(|| rng.alphanumeric())
.take(rng.usize(1..16))
.collect(),
id: Did::arbitrary(g),
})
.take(rng.usize(1..6))
.collect::<Vec<_>>()
.try_into()
.unwrap();
let threshold = delegates.len() / 2 + 1;
let doc: Doc<Unverified> =
Doc::new(name, description, default_branch, delegates, threshold);
doc.verified().unwrap()
}
}
impl Arbitrary for SignedRefs<Unverified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: ByteArray<64> = Arbitrary::arbitrary(g);
let signature = crypto::Signature::from(bytes.into_inner());
let refs = Refs::arbitrary(g);
Self::new(refs, signature)
}
}
impl Arbitrary for Refs {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let mut refs: BTreeMap<git::RefString, storage::Oid> = BTreeMap::new();
let mut bytes: [u8; 20] = [0; 20];
let names = &[
"heads/master",
"heads/feature/1",
"heads/feature/2",
"heads/feature/3",
"heads/radicle/id",
"tags/v1.0",
"tags/v2.0",
"notes/1",
];
for _ in 0..g.size().min(names.len()) {
if let Some(name) = g.choose(names) {
for byte in &mut bytes {
*byte = u8::arbitrary(g);
}
let oid = storage::Oid::try_from(&bytes[..]).unwrap();
let name = git::RefString::try_from(*name).unwrap();
refs.insert(name, oid);
}
}
Self::from(refs)
}
}
impl Arbitrary for MockSigner {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
let seed = Seed::new(bytes.into_inner());
let sk = KeyPair::from_seed(seed).sk;
MockSigner::from(sk)
}
}
impl Arbitrary for MockStorage {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let inventory = Arbitrary::arbitrary(g);
MockStorage::new(inventory)
}
}
impl Arbitrary for storage::Remote<crypto::Verified> {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let refs = Refs::arbitrary(g);
let signer = MockSigner::arbitrary(g);
let signed = refs.signed(&signer).unwrap();
storage::Remote::new(*signer.public_key(), signed)
}
}
impl Arbitrary for Id {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes = ByteArray::<20>::arbitrary(g);
let oid = git::Oid::try_from(bytes.as_slice()).unwrap();
Id::from(oid)
}
}
impl Arbitrary for hash::Digest {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: Vec<u8> = Arbitrary::arbitrary(g);
hash::Digest::new(&bytes)
}
}
impl Arbitrary for PublicKey {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let bytes: ByteArray<32> = Arbitrary::arbitrary(g);
let seed = Seed::new(bytes.into_inner());
let keypair = KeyPair::from_seed(seed);
PublicKey(keypair.pk)
}
}

View File

@ -0,0 +1,73 @@
use std::path::Path;
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::Id;
use crate::rad;
use crate::storage::git::Storage;
use crate::storage::refs::SignedRefs;
use crate::storage::{BranchName, WriteStorage};
/// Create a new storage with a project.
pub fn storage<P: AsRef<Path>, G: Signer>(path: P, signer: G) -> Result<Storage, rad::InitError> {
let path = path.as_ref();
let storage = Storage::open(path.join("storage"))?;
for (name, desc) in [
("acme", "Acme's repository"),
("vim", "A text editor"),
("rx", "A pixel editor"),
] {
let (repo, _) = repository(path.join("workdir").join(name));
rad::init(
&repo,
name,
desc,
BranchName::from("master"),
&signer,
&storage,
)?;
}
Ok(storage)
}
/// Create a new repository at the given path, and initialize it into a project.
pub fn project<'r, P: AsRef<Path>, S: WriteStorage<'r>, G: Signer>(
path: P,
storage: &'r S,
signer: G,
) -> Result<(Id, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
let (repo, head) = repository(path);
let (id, refs) = rad::init(
&repo,
"acme",
"Acme's repository",
BranchName::from("master"),
signer,
storage,
)?;
Ok((id, refs, repo, head))
}
/// Creates a regular repository at the given path with a couple of commits.
pub fn repository<P: AsRef<Path>>(path: P) -> (git2::Repository, git2::Oid) {
let repo = git2::Repository::init(path).unwrap();
let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap();
let head = git::initial_commit(&repo, &sig).unwrap();
let oid = git::commit(
&repo,
&head,
git::refname!("refs/heads/master").as_refstr(),
"Second commit",
"anonymous",
)
.unwrap()
.id();
// Look, I don't really understand why we have to do this, but we do.
drop(head);
(repo, oid)
}

View File

@ -1,13 +1,9 @@
use git_url::Url;
use crate::crypto::{Signer, Verified};
use crate::git;
use crate::identity::{Id, Project};
use crate::storage::{refs, RefUpdate};
use crate::storage::{
Error, FetchError, Inventory, ReadRepository, ReadStorage, Remote, RemoteId, WriteRepository,
WriteStorage,
};
pub use crate::storage::*;
#[derive(Clone, Debug)]
pub struct MockStorage {
@ -90,11 +86,11 @@ impl ReadRepository<'_> for MockRepository {
todo!()
}
fn commit(&self, _oid: git::Oid) -> Result<Option<git2::Commit>, git2::Error> {
fn commit(&self, _oid: Oid) -> Result<Option<git2::Commit>, git2::Error> {
todo!()
}
fn revwalk(&self, _head: git::Oid) -> Result<git2::Revwalk, git2::Error> {
fn revwalk(&self, _head: Oid) -> Result<git2::Revwalk, git2::Error> {
todo!()
}