diff --git a/radicle-cli/examples/rad-init.md b/radicle-cli/examples/rad-init.md index ad9653a3..bcf9b400 100644 --- a/radicle-cli/examples/rad-init.md +++ b/radicle-cli/examples/rad-init.md @@ -3,19 +3,19 @@ To create your first radicle project, navigate to a git repository, and run the `init` command: ``` -$ rad init --name acme --description "Acme's repository" --no-confirm +$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm Initializing local 🌱 project in . -ok Project acme created +ok Project heartwood created { - "name": "acme", - "description": "Acme's repository", + "name": "heartwood", + "description": "Radicle Heartwood Protocol & Stack", "default-branch": "master" } -Your project id is rad:z4EXAgDJk11uFThfVir5FivKhiAeK. You can show it any time by running: +Your project id is rad:zb2rNRYmmz7SLZ7xMjM7dddswC3K. You can show it any time by running: rad . To publish your project to the network, run: @@ -27,5 +27,5 @@ Projects can be listed with the `ls` command: ``` $ rad ls -acme rad:z4EXAgDJk11uFThfVir5FivKhiAeK cdf76ce Acme's repository +heartwood rad:zb2rNRYmmz7SLZ7xMjM7dddswC3K cdf76ce Radicle Heartwood Protocol & Stack ``` diff --git a/radicle-cli/src/commands/checkout.rs b/radicle-cli/src/commands/checkout.rs index 847ef636..09e5e222 100644 --- a/radicle-cli/src/commands/checkout.rs +++ b/radicle-cli/src/commands/checkout.rs @@ -110,8 +110,8 @@ pub fn execute(options: Options, profile: &Profile) -> anyhow::Result { spinner.finish(); let remotes = delegates - .iter() - .map(|d| *d.id) + .into_iter() + .map(|did| *did) .filter(|id| id != profile.id()) .collect::>(); diff --git a/radicle-cli/src/commands/clone.rs b/radicle-cli/src/commands/clone.rs index b0ddd7ed..5c4f5dfd 100644 --- a/radicle-cli/src/commands/clone.rs +++ b/radicle-cli/src/commands/clone.rs @@ -101,7 +101,7 @@ pub fn clone(id: Id, _interactive: Interactive, ctx: impl term::Context) -> anyh let delegates = doc .delegates .iter() - .map(|d| *d.id) + .map(|d| **d) .filter(|id| id != profile.id()) .collect::>(); let default_branch = doc.payload.default_branch.clone(); diff --git a/radicle-crypto/src/test/signer.rs b/radicle-crypto/src/test/signer.rs index f28e83ad..c315e757 100644 --- a/radicle-crypto/src/test/signer.rs +++ b/radicle-crypto/src/test/signer.rs @@ -8,12 +8,16 @@ pub struct MockSigner { impl MockSigner { pub fn new(rng: &mut fastrand::Rng) -> Self { - let mut bytes: [u8; 32] = [0; 32]; + let mut seed: [u8; 32] = [0; 32]; - for byte in &mut bytes { + for byte in &mut seed { *byte = rng.u8(..); } - let seed = Seed::new(bytes); + Self::from_seed(seed) + } + + pub fn from_seed(seed: [u8; 32]) -> Self { + let seed = Seed::new(seed); let keypair = KeyPair::from_seed(seed); Self::from(SecretKey::from(keypair.sk)) diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index c28385b0..89364523 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -40,7 +40,7 @@ async fn delegates_projects_handler( let Ok((_, head)) = repo.head() else { return None }; let Ok(Doc { payload, delegates, .. }) = repo.project_of(ctx.profile.id()) else { return None }; - if !delegates.iter().any(|d| d.id == delegate) { + if !delegates.iter().any(|d| *d == delegate) { return None; } diff --git a/radicle/src/identity.rs b/radicle/src/identity.rs index c0ae59f2..1df881d6 100644 --- a/radicle/src/identity.rs +++ b/radicle/src/identity.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::crypto; pub use crypto::PublicKey; -pub use project::{Delegate, Doc, Id, IdError}; +pub use project::{Doc, Id, IdError}; #[derive(Error, Debug)] pub enum DidError { diff --git a/radicle/src/identity/project.rs b/radicle/src/identity/project.rs index 9ffcf765..38755ea3 100644 --- a/radicle/src/identity/project.rs +++ b/radicle/src/identity/project.rs @@ -68,24 +68,6 @@ impl DocError { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct Delegate { - pub name: String, - pub id: Did, -} - -impl Delegate { - fn matches(&self, key: &PublicKey) -> bool { - &self.id.0 == key - } -} - -impl From for PublicKey { - fn from(delegate: Delegate) -> Self { - delegate.id.0 - } -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Payload { @@ -105,7 +87,7 @@ pub struct Doc { pub payload: Payload, #[serde(flatten)] pub extensions: BTreeMap, - pub delegates: NonEmpty, + pub delegates: NonEmpty, pub threshold: usize, #[serde(skip)] @@ -125,13 +107,9 @@ impl Doc { } /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before. - pub fn delegate(&mut self, name: String, key: crypto::PublicKey) -> bool { - let delegate = Delegate { - name, - id: Did::from(key), - }; - - if self.delegates.iter().all(|d| d.id != delegate.id) { + pub fn delegate(&mut self, key: crypto::PublicKey) -> bool { + let delegate = Did::from(key); + if self.delegates.iter().all(|id| id != &delegate) { self.delegates.push(delegate); return true; } @@ -233,7 +211,7 @@ impl Doc { name: String, description: String, default_branch: BranchName, - delegate: Delegate, + delegate: Did, ) -> Self { Self { payload: Payload { @@ -252,7 +230,7 @@ impl Doc { name: String, description: String, default_branch: BranchName, - delegates: NonEmpty, + delegates: NonEmpty, threshold: usize, ) -> Self { Self { @@ -289,15 +267,6 @@ impl Doc { "number of delegates cannot exceed 255", )); } - if self - .delegates - .iter() - .any(|d| d.name.is_empty() || d.name.len() > MAX_STRING_LENGTH) - { - return Err(VerificationError::Delegates( - "delegate name must not be empty and must not exceed 255 bytes", - )); - } if self.delegates.is_empty() { return Err(VerificationError::Delegates( "delegate list cannot be empty", @@ -471,7 +440,7 @@ impl Identity { // Check that enough delegates signed this next version. let quorum = signatures .iter() - .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key))) + .filter(|(key, _)| trusted.delegates.iter().any(|d| &**d == key)) .count(); if quorum < trusted.threshold { return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold)); @@ -498,6 +467,7 @@ mod test { use radicle_crypto::Signer as _; use crate::rad; + use crate::storage::git::transport; use crate::storage::git::Storage; use crate::storage::{ReadStorage, WriteStorage}; use crate::test::arbitrary; @@ -506,6 +476,35 @@ mod test { use super::*; use qcheck_macros::quickcheck; + #[test] + fn test_canonical_example() { + let tempdir = tempfile::tempdir().unwrap(); + let storage = Storage::open(tempdir.path().join("storage")).unwrap(); + + transport::local::register(storage.clone()); + + let delegate = MockSigner::from_seed([0xff; 32]); + let (repo, _) = fixtures::repository(tempdir.path().join("working")); + let (id, _, _) = rad::init( + &repo, + "heartwood", + "Radicle Heartwood Protocol & Stack", + git::refname!("master"), + &delegate, + &storage, + ) + .unwrap(); + + assert_eq!( + delegate.public_key().to_human(), + String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi") + ); + assert_eq!( + id.to_human(), + String::from("rad:zb2rNRYmmz7SLZ7xMjM7dddswC3K") + ); + } + #[test] fn test_not_found() { let tempdir = tempfile::tempdir().unwrap(); @@ -559,7 +558,7 @@ mod test { .unwrap(); // Add Bob as a delegate, and sign it. - proj.delegate("bob".to_owned(), *bob.public_key()); + proj.delegate(*bob.public_key()); proj.threshold = 2; proj.sign(&alice) .and_then(|(_, sig)| { @@ -573,7 +572,7 @@ mod test { .unwrap(); // Add Eve as a delegate, and sign it. - proj.delegate("eve".to_owned(), *eve.public_key()); + proj.delegate(*eve.public_key()); proj.sign(&alice) .and_then(|(_, alice_sig)| { proj.sign(&bob).and_then(|(_, bob_sig)| { diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 3f3a91da..b33248fb 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -59,11 +59,7 @@ pub fn init( ) -> Result<(Id, identity::Doc, SignedRefs), InitError> { // TODO: Better error when project id already exists in storage, but remote doesn't. let pk = signer.public_key(); - let delegate = identity::Delegate { - // TODO: Use actual user name. - name: String::from("anonymous"), - id: identity::Did::from(*pk), - }; + let delegate = identity::Did::from(*pk); let doc = identity::Doc::initial( name.to_owned(), description.to_owned(), @@ -348,7 +344,7 @@ mod tests { use radicle_crypto::test::signer::MockSigner; use crate::git::{name::component, qualified, Qualified}; - use crate::identity::{Delegate, Did}; + use crate::identity::Did; use crate::storage::git::transport; use crate::storage::git::Storage; use crate::storage::{ReadStorage, WriteStorage}; @@ -403,13 +399,7 @@ mod tests { assert_eq!(project.name, "acme"); assert_eq!(project.description, "Acme's repo"); assert_eq!(project.default_branch, git::refname!("master")); - assert_eq!( - project.delegates.first(), - &Delegate { - name: String::from("anonymous"), - id: Did::from(public_key), - } - ); + assert_eq!(project.delegates.first(), &Did::from(public_key)); } #[test] diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 77a6d305..44dc22c2 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -500,7 +500,7 @@ impl ReadRepository for Repository { let mut heads = Vec::new(); for delegate in project.delegates.iter() { - let r = self.reference_oid(&delegate.id, &branch_ref)?.into(); + let r = self.reference_oid(delegate, &branch_ref)?.into(); heads.push(r); } diff --git a/radicle/src/test/arbitrary.rs b/radicle/src/test/arbitrary.rs index 65d53f85..80f2fce5 100644 --- a/radicle/src/test/arbitrary.rs +++ b/radicle/src/test/arbitrary.rs @@ -10,7 +10,7 @@ use qcheck::Arbitrary; use crate::collections::HashMap; use crate::git; -use crate::identity::{project::Delegate, project::Doc, Did, Id}; +use crate::identity::{project::Doc, Did, Id}; use crate::storage; use crate::storage::refs::{Refs, SignedRefs}; use crate::test::storage::MockStorage; @@ -57,21 +57,12 @@ impl Arbitrary for Did { } } -impl Arbitrary for Delegate { - fn arbitrary(g: &mut qcheck::Gen) -> Self { - Self { - name: String::arbitrary(g), - id: Did::arbitrary(g), - } - } -} - impl Arbitrary for Doc { fn arbitrary(g: &mut qcheck::Gen) -> Self { let name = String::arbitrary(g); let description = String::arbitrary(g); let default_branch = git::RefString::try_from(String::arbitrary(g)).unwrap(); - let delegate = Delegate::arbitrary(g); + let delegate = Did::arbitrary(g); Self::initial(name, description, default_branch, delegate) } @@ -91,16 +82,11 @@ impl Arbitrary for Doc { .collect::() .try_into() .unwrap(); - 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::>() - .try_into() - .unwrap(); + let delegates: NonEmpty<_> = iter::repeat_with(|| Did::arbitrary(g)) + .take(rng.usize(1..6)) + .collect::>() + .try_into() + .unwrap(); let threshold = delegates.len() / 2 + 1; let doc: Doc = Doc::new(name, description, default_branch, delegates, threshold);