rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodde1958fab04a95f73b7d7dcc62d66775d0854fc1
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "f67ef748bfb0f3bb680bb4db0aba4e4b44f1b605",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"title": "radicle: refactor doc",
"state": {
"status": "merged",
"conflicts": []
},
"before": "f83c11674287d4f56c43daac74d7461abfc0ddde",
"after": "de1958fab04a95f73b7d7dcc62d66775d0854fc1",
"commits": [
"de1958fab04a95f73b7d7dcc62d66775d0854fc1"
],
"target": "3fc92f83fe089263c470b328414d923227cf0e4a",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "f67ef748bfb0f3bb680bb4db0aba4e4b44f1b605",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "The aim of this change is to make the `Doc` type more safe to use by approaching\nthe design via [Parse don't validate][[0]] approach.\n\nThe problem with the previous approach was that all field were `pub` and thus a\n`Doc<Verified>` could easily be mutated and serialized. Granted, the code that\nused the serialization would tend to verify the `Doc` first, however, this\napproach *ensures* that only a verified `Doc` can be serialized. It also meant\nthat trying to add new data that would follow the parse approach would require\nmore generic parameters on top of the existing `PhantomData` parameter, i.e. we\nneed to do something like: `Doc<RawField, V> -> Doc<ValidField, V>`.\n\nThe new approach splits the type into two separate types: `DocMut` and `Doc`.\nThe former is allowed to be mutated at will, and uses types that are less\nstrict. The latter is the valid type that can only be constructed by validating\na `DocMut` (or the `initial` constructor). The `Doc` type's fields can then only\nbe accessed by read-only methods.\n\nSolves the problems above by only allowing mutations to `DocMut`, as well as,\nnew fields being added to `DocMut` which are then validated via\n`DocMut::verified`.\n\n[0]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/",
"base": "855327d303566aac1d3e065d4ba7e16d9ccd18e6",
"oid": "c8faf96c2be9865af9949c0c9dbffddc0c9a0fe6",
"timestamp": 1728992474
},
{
"id": "f58adbe03a3a198d676f5b0d66a9b6b83bfffe44",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Renamed DocMut to RawDoc\n- Renamed `into_mut` to `edit`\n- Use `Doc::edit` everywhere and remove `From<Doc> for RawDoc` impl\n- Introduce `Doc::with_edits` to for a slightly nicer API in some\n sections\n- `Threshold::new` takes `Delegates` to ensure it is always\n constructed safely\n- Changed `Delegates` constructor to remove `HashSet` and use counter\n for checking number of delegates.\n- Added a test for checking verifying a Doc de-deuplicates DIDs\n- Fix documentation indentation",
"base": "f83c11674287d4f56c43daac74d7461abfc0ddde",
"oid": "4c46221ba17595292b002d27a770a009f5ed67a6",
"timestamp": 1729588569
},
{
"id": "3d477afe22b9fc236f191559c32fffc8e62462d4",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Simplify Delegates::new constructor to use `Vec` and use the `len`\n for checking when it exceeds the max.\n- Fix documentation links\n- Change `blob_at` from `pub` to `pub(crate)`",
"base": "f83c11674287d4f56c43daac74d7461abfc0ddde",
"oid": "6de8261c7434aa78f12027b0422c85a5c386a5ad",
"timestamp": 1729596697
},
{
"id": "33614c5881843ef7359638caaa90d7950189af03",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Fix length check to go after we have pushed\n- Add test case for maximum number of delegates",
"base": "f83c11674287d4f56c43daac74d7461abfc0ddde",
"oid": "69cf86b5291827f88bac4dbafc9b708992ce8905",
"timestamp": 1729599916
},
{
"id": "3ee4e47f2d8bad69dcc00d8c61180ac62777a43f",
"author": {
"id": "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"alias": null
},
"description": "Changes:\n- Check length before pushing, and do so after `contains` check",
"base": "f83c11674287d4f56c43daac74d7461abfc0ddde",
"oid": "de1958fab04a95f73b7d7dcc62d66775d0854fc1",
"timestamp": 1729605535
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "c82c6374-9a12-4f16-9a0c-ec50fac9244d"
},
"info_url": "https://cci.rad.levitte.org//c82c6374-9a12-4f16-9a0c-ec50fac9244d.html"
}
Started at: 2026-09-08 05:46:16.800991+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/c82c6374-9a12-4f16-9a0c-ec50fac9244d/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 38 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout f67ef748bfb0f3bb680bb4db0aba4e4b44f1b605
✓ Switched to branch patch/f67ef74 at revision 3ee4e47
✓ Branch patch/f67ef74 setup to track rad/patches/f67ef748bfb0f3bb680bb4db0aba4e4b44f1b605
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout de1958fab04a95f73b7d7dcc62d66775d0854fc1
HEAD is now at de1958fa radicle: refactor doc
Exit code: 0
$ rad patch show f67ef748bfb0f3bb680bb4db0aba4e4b44f1b605 -p
╭──────────────────────────────────────────────────────────────────────────────────╮
│ Title radicle: refactor doc │
│ Patch f67ef748bfb0f3bb680bb4db0aba4e4b44f1b605 │
│ Author fintohaps z6Mkire…SQZ3voM │
│ Head de1958fab04a95f73b7d7dcc62d66775d0854fc1 │
│ Base f83c11674287d4f56c43daac74d7461abfc0ddde │
│ Target master │
│ Branches patch/f67ef74 │
│ Commits ahead 0, behind 1180 │
│ Status merged │
│ │
│ The aim of this change is to make the `Doc` type more safe to use by approaching │
│ the design via [Parse don't validate][[0]] approach. │
│ │
│ The problem with the previous approach was that all field were `pub` and thus a │
│ `Doc<Verified>` could easily be mutated and serialized. Granted, the code that │
│ used the serialization would tend to verify the `Doc` first, however, this │
│ approach *ensures* that only a verified `Doc` can be serialized. It also meant │
│ that trying to add new data that would follow the parse approach would require │
│ more generic parameters on top of the existing `PhantomData` parameter, i.e. we │
│ need to do something like: `Doc<RawField, V> -> Doc<ValidField, V>`. │
│ │
│ The new approach splits the type into two separate types: `DocMut` and `Doc`. │
│ The former is allowed to be mutated at will, and uses types that are less │
│ strict. The latter is the valid type that can only be constructed by validating │
│ a `DocMut` (or the `initial` constructor). The `Doc` type's fields can then only │
│ be accessed by read-only methods. │
│ │
│ Solves the problems above by only allowing mutations to `DocMut`, as well as, │
│ new fields being added to `DocMut` which are then validated via │
│ `DocMut::verified`. │
│ │
│ [0]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ │
├──────────────────────────────────────────────────────────────────────────────────┤
│ de1958f radicle: refactor doc │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision f67ef74 @ 855327d..c8faf96 by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ ↑ Revision f58adbe @ f83c116..4c46221 by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ ↑ Revision 3d477af @ f83c116..6de8261 by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ ↑ Revision 33614c5 @ f83c116..69cf86b by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ ↑ Revision 3ee4e47 @ f83c116..de1958f by fintohaps z6Mkire…SQZ3voM 1 year ago │
│ └─ ✓ merged by cloudhead z6MksFq…bS9wzpT │
╰──────────────────────────────────────────────────────────────────────────────────╯
commit de1958fab04a95f73b7d7dcc62d66775d0854fc1
Author: Fintan Halpenny <fintan.halpenny@gmail.com>
Date: Tue Oct 15 12:26:51 2024 +0100
radicle: refactor doc
The aim of this change is to make the `Doc` type more safe to use by approaching
the design via [Parse don't validate][[0]] approach.
The problem with the previous approach was that all field were `pub` and thus a
`Doc<Verified>` could easily be mutated and serialized. Granted, the code that
used the serialization would tend to verify the `Doc` first, however, this
approach *ensures* that only a verified `Doc` can be serialized. It also meant
that trying to add new data that would follow the parse approach would require
more generic parameters on top of the existing `PhantomData` parameter, i.e. we
need to do something like: `Doc<RawField, V> -> Doc<ValidField, V>`.
The new approach splits the type into two separate types: `RawDoc` and `Doc`.
The former is allowed to be mutated at will, and uses types that are less
strict. The latter is the valid type that can only be constructed by validating
a `RawDoc` (or the `initial` constructor). The `Doc` type's fields can then only
be accessed by read-only methods.
Solves the problems above by only allowing mutations to `RawDoc`, as well as,
new fields being added to `RawDoc` which are then validated via
`RawDoc::verified`.
[0]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
diff --git a/radicle-cli/src/commands/checkout.rs b/radicle-cli/src/commands/checkout.rs
index 0554279f8..1e77243ed 100644
--- a/radicle-cli/src/commands/checkout.rs
+++ b/radicle-cli/src/commands/checkout.rs
@@ -84,11 +84,10 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
let id = options.id;
let storage = &profile.storage;
let remote = options.remote.unwrap_or(profile.did());
- let doc: Doc<_> = storage
+ let doc = storage
.repository(id)?
.identity_doc()
- .context("repository could not be found in local storage")?
- .into();
+ .context("repository could not be found in local storage")?;
let payload = doc.project()?;
let path = PathBuf::from(payload.name());
@@ -115,7 +114,8 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
spinner.finish();
let remotes = doc
- .delegates
+ .delegates()
+ .clone()
.into_iter()
.map(|did| *did)
.filter(|id| id != profile.id())
diff --git a/radicle-cli/src/commands/clone.rs b/radicle-cli/src/commands/clone.rs
index 7c2b9215c..9f39a7a5e 100644
--- a/radicle-cli/src/commands/clone.rs
+++ b/radicle-cli/src/commands/clone.rs
@@ -153,7 +153,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
&profile.storage,
)?;
let delegates = doc
- .delegates
+ .delegates()
.iter()
.map(|d| **d)
.filter(|id| id != profile.id())
@@ -240,7 +240,7 @@ pub fn clone<G: Signer>(
(
raw::Repository,
storage::git::Repository,
- Doc<Verified>,
+ Doc,
Project,
),
CloneError,
diff --git a/radicle-cli/src/commands/id.rs b/radicle-cli/src/commands/id.rs
index ba8dcbd1d..694ae23eb 100644
--- a/radicle-cli/src/commands/id.rs
+++ b/radicle-cli/src/commands/id.rs
@@ -4,14 +4,12 @@ use std::{ffi::OsString, io};
use anyhow::{anyhow, Context};
-use nonempty::NonEmpty;
use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
-use radicle::identity::{doc, Identity, Visibility};
-use radicle::prelude::{Did, Doc, RepoId, Signer};
+use radicle::identity::{doc, Doc, Identity, RawDoc, Visibility};
+use radicle::prelude::{Did, RepoId, Signer};
use radicle::storage::refs;
use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
use radicle::{cob, Profile};
-use radicle_crypto::Verified;
use radicle_surf::diff::Diff;
use radicle_term::Element;
use serde_json as json;
@@ -389,7 +387,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
payload,
} => {
let proposal = {
- let mut proposal = current.doc.clone();
+ let mut proposal = current.doc.clone().edit();
proposal.threshold = threshold.unwrap_or(proposal.threshold);
if !allow.is_disjoint(&disallow) {
@@ -429,18 +427,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
proposal.visibility = Visibility::Public;
}
}
- proposal.delegates = NonEmpty::from_vec(
- proposal
- .delegates
- .into_iter()
- .chain(delegates)
- .filter(|d| !rescind.contains(d))
- .collect::<Vec<_>>(),
- )
- .ok_or(anyhow!(
- "at lease one delegate must be present for the identity to be valid"
- ))?;
-
+ proposal.delegates = proposal
+ .delegates
+ .into_iter()
+ .chain(delegates)
+ .filter(|d| !rescind.contains(d))
+ .collect::<Vec<_>>();
if let Some(errs) = verify_delegates(&proposal, &repo)? {
term::error(format!("failed to verify delegates for {rid}"));
term::error(format!(
@@ -475,6 +467,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
proposal
};
+ let proposal = proposal.verified()?;
if proposal == current.doc {
if !options.quiet {
term::print(term::format::italic(
@@ -578,11 +571,7 @@ fn get<'a>(
Ok(revision)
}
-fn print_meta(
- revision: &Revision,
- previous: &Doc<Verified>,
- profile: &Profile,
-) -> anyhow::Result<()> {
+fn print_meta(revision: &Revision, previous: &Doc, profile: &Profile) -> anyhow::Result<()> {
let mut attrs = term::Table::<2, term::Label>::new(Default::default());
attrs.push([
@@ -630,7 +619,7 @@ fn print_meta(
let accepted = revision.accepted().collect::<Vec<_>>();
let rejected = revision.rejected().collect::<Vec<_>>();
let unknown = previous
- .delegates
+ .delegates()
.iter()
.filter(|id| !accepted.contains(id) && !rejected.contains(id))
.collect::<Vec<_>>();
@@ -712,7 +701,7 @@ and description.
fn update<R: WriteRepository + cob::Store, G: Signer>(
title: Option<String>,
description: Option<String>,
- doc: Doc<Verified>,
+ doc: Doc,
current: &mut IdentityMut<R>,
signer: &G,
) -> anyhow::Result<Revision> {
@@ -734,14 +723,14 @@ fn print_diff(
repo: &radicle::storage::git::Repository,
) -> anyhow::Result<()> {
let previous = if let Some(previous) = previous {
- let previous = Doc::<Verified>::load_at(*previous, repo)?;
+ let previous = Doc::load_at(*previous, repo)?;
let previous = serde_json::to_string_pretty(&previous.doc)?;
Some(previous)
} else {
None
};
- let current = Doc::<Verified>::load_at(*current, repo)?;
+ let current = Doc::load_at(*current, repo)?;
let current = serde_json::to_string_pretty(¤t.doc)?;
let tmp = tempfile::tempdir()?;
@@ -798,8 +787,8 @@ impl VerificationError {
}
}
-fn verify_delegates<S, V>(
- proposal: &Doc<V>,
+fn verify_delegates<S>(
+ proposal: &RawDoc,
repo: &S,
) -> anyhow::Result<Option<Vec<VerificationError>>>
where
diff --git a/radicle-cli/src/commands/init.rs b/radicle-cli/src/commands/init.rs
index 59113c19a..6c903bc1b 100644
--- a/radicle-cli/src/commands/init.rs
+++ b/radicle-cli/src/commands/init.rs
@@ -10,15 +10,14 @@ use std::str::FromStr;
use anyhow::{anyhow, bail, Context as _};
use serde_json as json;
-use radicle::crypto::{ssh, Verified};
+use radicle::crypto::ssh;
use radicle::explorer::ExplorerUrl;
use radicle::git::RefString;
use radicle::identity::project::ProjectName;
-use radicle::identity::{RepoId, Visibility};
+use radicle::identity::{RepoId, Doc, Visibility};
use radicle::node::events::UploadPack;
use radicle::node::policy::Scope;
use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
-use radicle::prelude::Doc;
use radicle::storage::ReadStorage as _;
use radicle::{profile, Node};
@@ -302,7 +301,7 @@ pub fn init(
if options.seed {
profile.seed(rid, options.scope, &mut node)?;
- if doc.visibility.is_public() {
+ if doc.is_public() {
profile.add_inventory(rid, &mut node)?;
}
}
@@ -529,11 +528,11 @@ fn sync(
pub fn announce(
rid: RepoId,
- doc: Doc<Verified>,
+ doc: Doc,
node: &mut Node,
config: &profile::Config,
) -> anyhow::Result<()> {
- if doc.visibility.is_public() {
+ if doc.is_public() {
match sync(rid, node, config) {
Ok(SyncResult::Synced {
result: Some(url), ..
@@ -597,7 +596,7 @@ pub fn announce(
} else {
term::info!(
"You have created a {} repository.",
- term::format::visibility(&doc.visibility)
+ term::format::visibility(doc.visibility())
);
term::info!(
"This repository will only be visible to you, \
diff --git a/radicle-cli/src/commands/inspect.rs b/radicle-cli/src/commands/inspect.rs
index dabd81e6d..84ba3cbd4 100644
--- a/radicle-cli/src/commands/inspect.rs
+++ b/radicle-cli/src/commands/inspect.rs
@@ -152,7 +152,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
Target::Payload => {
let (_, doc) = repo(rid, storage)?;
- json::to_pretty(&doc.payload, Path::new("radicle.json"))?.print();
+ json::to_pretty(&doc.payload(), Path::new("radicle.json"))?.print();
}
Target::Identity => {
let (_, doc) = repo(rid, storage)?;
@@ -195,7 +195,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
Target::Delegates => {
let (_, doc) = repo(rid, storage)?;
let aliases = profile.aliases();
- for did in &doc.delegates {
+ for did in doc.delegates().iter() {
if let Some(alias) = aliases.alias(did) {
println!(
"{} {}",
@@ -209,7 +209,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
Target::Visibility => {
let (_, doc) = repo(rid, storage)?;
- println!("{}", term::format::visibility(&doc.visibility));
+ println!("{}", term::format::visibility(doc.visibility()));
}
Target::History => {
let (repo, _) = repo(rid, storage)?;
diff --git a/radicle-cli/src/commands/ls.rs b/radicle-cli/src/commands/ls.rs
index c26cd8400..8d8a2d3f2 100644
--- a/radicle-cli/src/commands/ls.rs
+++ b/radicle-cli/src/commands/ls.rs
@@ -105,10 +105,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
..
} in repos
{
- if doc.visibility.is_public() && options.private && !options.public {
+ if doc.is_public() && options.private && !options.public {
continue;
}
- if !doc.visibility.is_public() && !options.private && options.public {
+ if !doc.is_public() && !options.private && options.public {
continue;
}
if refs.is_none() && !options.all && !options.seeded {
@@ -135,7 +135,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
term::format::bold(proj.name().to_owned()),
term::format::tertiary(rid.urn()),
if seeded {
- term::format::visibility(&doc.visibility).into()
+ term::format::visibility(doc.visibility()).into()
} else {
term::format::dim("local").into()
},
diff --git a/radicle-cli/src/commands/publish.rs b/radicle-cli/src/commands/publish.rs
index 01d81d3ba..146f80b98 100644
--- a/radicle-cli/src/commands/publish.rs
+++ b/radicle-cli/src/commands/publish.rs
@@ -77,19 +77,19 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let repo = profile.storage.repository_mut(rid)?;
let mut identity = Identity::load_mut(&repo)?;
- let mut doc = identity.doc().clone();
+ let doc = identity.doc();
- if doc.visibility.is_public() {
+ if doc.is_public() {
return Err(Error::WithHint {
err: anyhow!("repository is already public"),
hint: "to announce the repository to the network, run `rad sync --inventory`",
}
.into());
}
- if !doc.is_delegate(profile.id()) {
+ if !doc.is_delegate(&profile.id().into()) {
return Err(anyhow!("only the repository delegate can publish it"));
}
- if doc.delegates.len() > 1 {
+ if doc.delegates().len() > 1 {
return Err(Error::WithHint {
err: anyhow!(
"only repositories with a single delegate can be published with this command"
@@ -101,7 +101,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let signer = profile.signer()?;
// Update identity document.
- doc.visibility = Visibility::Public;
+ let doc = doc.clone().with_edits(|doc| {
+ doc.visibility = Visibility::Public;
+ })?;
identity.update("Publish repository", "", &doc, &signer)?;
repo.sign_refs(&signer)?;
@@ -123,7 +125,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
term::success!(
"Repository is now {}",
- term::format::visibility(&doc.visibility)
+ term::format::visibility(doc.visibility())
);
if !node.is_running() {
diff --git a/radicle-cli/src/commands/seed.rs b/radicle-cli/src/commands/seed.rs
index 6c8b6c4fa..c40333261 100644
--- a/radicle-cli/src/commands/seed.rs
+++ b/radicle-cli/src/commands/seed.rs
@@ -169,7 +169,7 @@ pub fn update(
let outcome = if updated { "updated" } else { "exists" };
if let Ok(repo) = profile.storage.repository(rid) {
- if repo.identity_doc()?.visibility.is_public() {
+ if repo.identity_doc()?.is_public() {
profile.add_inventory(rid, node)?;
term::success!("Inventory updated with {}", term::format::tertiary(rid));
}
diff --git a/radicle-cli/src/node.rs b/radicle-cli/src/node.rs
index 3c4ed4fd0..3bd4f7adc 100644
--- a/radicle-cli/src/node.rs
+++ b/radicle-cli/src/node.rs
@@ -180,7 +180,7 @@ fn announce_<R: ReadRepository>(
let rid = repo.id();
let doc = repo.identity_doc()?;
let mut settings = settings.with_profile(profile);
- let unsynced: Vec<_> = if doc.visibility.is_public() {
+ let unsynced: Vec<_> = if doc.is_public() {
// All seeds.
let all = node.seeds(rid)?;
if all.is_empty() {
@@ -218,7 +218,7 @@ fn announce_<R: ReadRepository>(
} else {
node.sessions()?
.into_iter()
- .filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid))
+ .filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid.into()))
.map(|s| s.nid)
.collect()
};
diff --git a/radicle-fetch/src/handle.rs b/radicle-fetch/src/handle.rs
index ebc86d818..5916af7d9 100644
--- a/radicle-fetch/src/handle.rs
+++ b/radicle-fetch/src/handle.rs
@@ -2,10 +2,9 @@ use std::sync::atomic::{self, AtomicBool};
use std::sync::Arc;
use bstr::BString;
-use radicle::crypto::{PublicKey, Verified};
+use radicle::crypto::PublicKey;
use radicle::git::Oid;
-use radicle::identity::DocError;
-use radicle::prelude::Doc;
+use radicle::identity::{DocError, Doc};
use radicle::storage::git::Repository;
use radicle::storage::ReadRepository;
@@ -74,7 +73,7 @@ impl<S> Handle<S> {
self.interrupt.store(true, atomic::Ordering::Relaxed);
}
- pub fn verified(&self, head: Oid) -> Result<Doc<Verified>, DocError> {
+ pub fn verified(&self, head: Oid) -> Result<Doc, DocError> {
Ok(self.repo.identity_doc_at(head)?.doc)
}
diff --git a/radicle-fetch/src/stage.rs b/radicle-fetch/src/stage.rs
index d41fc12fd..be2422055 100644
--- a/radicle-fetch/src/stage.rs
+++ b/radicle-fetch/src/stage.rs
@@ -207,8 +207,8 @@ impl ProtocolStage for CanonicalId {
remote: self.remote,
err: Box::new(err),
})?;
- if verified.delegates.contains(&self.remote.into()) {
- let is_delegate = |remote: &PublicKey| verified.is_delegate(remote);
+ if verified.is_delegate(&self.remote.into()) {
+ let is_delegate = |remote: &PublicKey| verified.is_delegate(&remote.into());
Ok(Updates::build(
refs.iter()
.filter_map(|r| r.as_special_ref_update(is_delegate)),
diff --git a/radicle-fetch/src/state.rs b/radicle-fetch/src/state.rs
index 489cbb832..86e5f39dd 100644
--- a/radicle-fetch/src/state.rs
+++ b/radicle-fetch/src/state.rs
@@ -4,7 +4,7 @@ use std::time::Instant;
use gix_protocol::handshake;
use radicle::crypto::PublicKey;
use radicle::git::{Oid, Qualified};
-use radicle::identity::{Did, Doc, DocError};
+use radicle::identity::{Did, DocError, Doc};
use radicle::prelude::Verified;
use radicle::storage;
@@ -389,11 +389,11 @@ impl FetchState {
.canonical()?
.ok_or(error::Protocol::MissingRadId)?;
- let is_delegate = anchor.delegates.contains(&Did::from(handle.local()));
+ let is_delegate = anchor.is_delegate(&Did::from(handle.local()));
// TODO: not sure we should allow to block *any* peer from the
// delegate set. We could end up ignoring delegates.
let delegates = anchor
- .delegates
+ .delegates()
.iter()
.filter(|id| !handle.is_blocked(id))
.map(|did| PublicKey::from(*did))
@@ -404,9 +404,9 @@ impl FetchState {
// The local peer does not need to count towards the threshold
// since they must be valid already.
let threshold = if is_delegate {
- anchor.threshold - 1
+ anchor.threshold() - 1
} else {
- anchor.threshold
+ anchor.threshold()
};
let signed_refs = self.run_special_refs(
handle,
@@ -637,11 +637,11 @@ impl<'a, S> Cached<'a, S> {
self.state.canonical_rad_id().copied()
}
- pub fn verified(&self, head: Oid) -> Result<Doc<Verified>, DocError> {
+ pub fn verified(&self, head: Oid) -> Result<Doc, DocError> {
self.handle.verified(head)
}
- pub fn canonical(&self) -> Result<Option<Doc<Verified>>, error::Canonical> {
+ pub fn canonical(&self) -> Result<Option<Doc>, error::Canonical> {
let tip = self.refname_to_id(refs::REFS_RAD_ID.clone())?;
let cached_tip = self.canonical_rad_id();
@@ -736,7 +736,7 @@ impl<'a, S> ValidateRepository for Cached<'a, S> {
/// N.b. if the repository does not have the project payload or a
/// deserialization error occurs, then this will return `None`.
fn validate_project_default_branch(
- anchor: &Doc<Verified>,
+ anchor: &Doc,
sigrefs: &SignedRefs<Verified>,
) -> Option<Validation> {
let proj = anchor.project().ok()?;
diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs
index 0b2bf7ffc..dde23a780 100644
--- a/radicle-node/src/service.rs
+++ b/radicle-node/src/service.rs
@@ -22,6 +22,7 @@ use localtime::{LocalDuration, LocalTime};
use log::*;
use nonempty::NonEmpty;
+use radicle::identity::Doc;
use radicle::node;
use radicle::node::address;
use radicle::node::address::Store as _;
@@ -36,8 +37,8 @@ use radicle::storage::refs::SIGREFS_BRANCH;
use radicle::storage::RepositoryError;
use radicle_fetch::policy::SeedingPolicy;
-use crate::crypto::{Signer, Verified};
-use crate::identity::{Doc, RepoId};
+use crate::crypto::Signer;
+use crate::identity::RepoId;
use crate::node::routing;
use crate::node::routing::InsertResult;
use crate::node::{
@@ -678,7 +679,7 @@ where
continue;
}
// Add public repositories to inventory.
- if repo.doc.visibility.is_public() {
+ if repo.doc.is_public() {
inventory.insert(rid);
} else {
private.insert(rid);
@@ -1168,7 +1169,7 @@ where
// Announce our new inventory if this fetch was a full clone.
// Only update and announce inventory for public repositories.
- if clone && doc.visibility.is_public() {
+ if clone && doc.is_public() {
debug!(target: "service", "Updating and announcing inventory for cloned repository {rid}..");
if let Err(e) = self.add_inventory(rid) {
@@ -2128,7 +2129,7 @@ where
}
/// Announce our own refs for the given repo.
- fn announce_own_refs(&mut self, rid: RepoId, doc: Doc<Verified>) -> Result<Vec<RefsAt>, Error> {
+ fn announce_own_refs(&mut self, rid: RepoId, doc: Doc) -> Result<Vec<RefsAt>, Error> {
let (refs, timestamp) = self.announce_refs(rid, doc, [self.node_id()])?;
// Update refs database with our signed refs branches.
@@ -2161,7 +2162,7 @@ where
fn announce_refs(
&mut self,
rid: RepoId,
- doc: Doc<Verified>,
+ doc: Doc,
remotes: impl IntoIterator<Item = NodeId>,
) -> Result<(Vec<RefsAt>, Timestamp), Error> {
let (ann, refs) = self.refs_announcement_for(rid, remotes)?;
@@ -2192,7 +2193,7 @@ where
ann,
peers.filter(|p| {
// Only announce to peers who are allowed to view this repo.
- doc.is_visible_to(&p.id)
+ doc.is_visible_to(&p.id.into())
}),
self.db.gossip_mut(),
);
@@ -2341,7 +2342,7 @@ where
.get(rid)
.ok()
.flatten()
- .map(|doc| doc.is_visible_to(id))
+ .map(|doc| doc.is_visible_to(&(*id).into()))
.unwrap_or(false)
} else {
// Announcement doesn't concern a specific repository, let it through.
@@ -2634,7 +2635,7 @@ pub trait ServiceState {
/// Get event emitter.
fn emitter(&self) -> &Emitter<Event>;
/// Get a repository from storage.
- fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError>;
+ fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError>;
/// Get the clock.
fn clock(&self) -> &LocalTime;
/// Get the clock mutably.
@@ -2675,7 +2676,7 @@ where
&self.emitter
}
- fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
+ fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
self.storage.get(rid)
}
@@ -2751,7 +2752,7 @@ impl fmt::Display for DisconnectReason {
#[derive(Debug)]
pub struct Lookup {
/// Whether the project was found locally or not.
- pub local: Option<Doc<Verified>>,
+ pub local: Option<Doc>,
/// A list of remote peers on which the project is known to exist.
pub remote: Vec<NodeId>,
}
diff --git a/radicle-node/src/tests.rs b/radicle-node/src/tests.rs
index 3e4cb3209..5af183797 100644
--- a/radicle-node/src/tests.rs
+++ b/radicle-node/src/tests.rs
@@ -712,7 +712,13 @@ fn test_refs_announcement_relay_public() {
// Pretend Alice cloned Bob's repos.
let repos = gen::<[MockRepository; 3]>(1);
for (i, mut repo) in repos.into_iter().enumerate() {
- repo.doc.doc.visibility = Visibility::Public; // Public repos are always gossiped.
+ repo.doc.doc = repo
+ .doc
+ .doc
+ .with_edits(|doc| {
+ doc.visibility = Visibility::Public; // Public repos are always gossiped.
+ })
+ .unwrap();
alice.storage_mut().repos.insert(bob_inv[i], repo);
}
assert_matches!(
@@ -784,14 +790,32 @@ fn test_refs_announcement_relay_private() {
alice.receive(eve.id(), Message::Subscribe(Subscribe::all()));
// The first repo is not visible to Eve.
- let mut repo1 = gen::<MockRepository>(1);
- repo1.doc.doc.visibility = Visibility::Private { allow: [].into() };
+ let repo1 = {
+ let mut repo = gen::<MockRepository>(1);
+ repo.doc.doc = repo
+ .doc
+ .doc
+ .with_edits(|doc| {
+ doc.visibility = Visibility::Private { allow: [].into() };
+ })
+ .unwrap();
+ repo
+ };
alice.storage_mut().repos.insert(bob_inv[0], repo1);
// The second repo is visible to Eve.
- let mut repo2 = gen::<MockRepository>(1);
- repo2.doc.doc.visibility = Visibility::Private {
- allow: [eve.id.into()].into(),
+ let repo2 = {
+ let mut repo = gen::<MockRepository>(1);
+ repo.doc.doc = repo
+ .doc
+ .doc
+ .with_edits(|doc| {
+ doc.visibility = Visibility::Private {
+ allow: [eve.id.into()].into(),
+ };
+ })
+ .unwrap();
+ repo
};
alice.storage_mut().repos.insert(bob_inv[1], repo2);
alice.elapse(service::GOSSIP_INTERVAL);
diff --git a/radicle-node/src/worker.rs b/radicle-node/src/worker.rs
index f84e02e5d..bc5277f3a 100644
--- a/radicle-node/src/worker.rs
+++ b/radicle-node/src/worker.rs
@@ -287,7 +287,8 @@ impl Worker {
}
let repo = self.storage.repository(rid)?;
let doc = repo.identity_doc()?;
- if !doc.is_visible_to(&remote) {
+
+ if !doc.is_visible_to(&remote.into()) {
Err(UploadError::Unauthorized(remote, rid))
} else {
Ok(())
diff --git a/radicle-remote-helper/src/push.rs b/radicle-remote-helper/src/push.rs
index 6bc65723b..4bfd23557 100644
--- a/radicle-remote-helper/src/push.rs
+++ b/radicle-remote-helper/src/push.rs
@@ -266,8 +266,12 @@ pub fn run(
if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
let head = working.find_reference(src.as_str())?;
let head = head.peel_to_commit()?.id();
- let mut canonical =
- Canonical::default_branch(stored, &project, &identity.delegates)?;
+
+ let mut canonical = Canonical::default_branch(
+ stored,
+ &project,
+ identity.delegates().as_ref(),
+ )?;
let converges = canonical::converges(
canonical
.tips()
@@ -279,7 +283,7 @@ pub fn run(
canonical.modify_vote(me, head.into());
}
- match canonical.quorum(identity.threshold, &working) {
+ match canonical.quorum(identity.threshold(), &working) {
Ok(canonical_oid) => {
// Canonical head is an ancestor of head.
let is_ff = head == *canonical_oid
diff --git a/radicle/src/cob/identity.rs b/radicle/src/cob/identity.rs
index 9a166df07..ec44f282a 100644
--- a/radicle/src/cob/identity.rs
+++ b/radicle/src/cob/identity.rs
@@ -4,12 +4,13 @@ use std::{fmt, ops::Deref, str::FromStr};
use crypto::{PublicKey, Signature};
use once_cell::sync::Lazy;
use radicle_cob::{Embed, ObjectId, TypeName};
-use radicle_crypto::{Signer, Verified};
+use radicle_crypto::Signer;
use radicle_git_ext as git_ext;
use radicle_git_ext::Oid;
use serde::{Deserialize, Serialize};
use thiserror::Error;
+use crate::identity::doc::Doc;
use crate::{
cob,
cob::{
@@ -18,7 +19,7 @@ use crate::{
ActorId, Timestamp, Uri,
},
identity::{
- doc::{Doc, DocError, RepoId},
+ doc::{DocError, RepoId},
Did,
},
storage::{ReadRepository, RepositoryError, WriteRepository},
@@ -171,7 +172,7 @@ impl Identity {
root,
current: root,
heads: revision
- .delegates
+ .delegates()
.iter()
.copied()
.map(|did| (did, root))
@@ -182,7 +183,7 @@ impl Identity {
}
pub fn initialize<'a, R: WriteRepository + cob::Store, G: Signer>(
- doc: &Doc<Verified>,
+ doc: &Doc,
store: &'a R,
signer: &G,
) -> Result<IdentityMut<'a, R>, cob::store::Error> {
@@ -249,7 +250,7 @@ impl Identity {
}
/// The current document.
- pub fn doc(&self) -> &Doc<Verified> {
+ pub fn doc(&self) -> &Doc {
&self.current().doc
}
@@ -320,7 +321,7 @@ impl store::Cob for Identity {
"the first operation must contain only one action",
));
}
- let root = Doc::<Verified>::load_at(op.id, repo)?;
+ let root = Doc::load_at(op.id, repo)?;
if root.blob != blob {
return Err(ApplyError::Init("invalid object id specified in revision"));
}
@@ -331,7 +332,7 @@ impl store::Cob for Identity {
}
assert_eq!(root.commit, op.id);
- let founder = root.delegates.first();
+ let founder = root.delegates().first();
if founder.as_key() != &op.author {
return Err(ApplyError::Init("delegate does not match committer"));
}
@@ -411,7 +412,7 @@ impl Identity {
) -> Result<(), ApplyError> {
let current = self.current().clone();
- if !current.is_delegate(&author) {
+ if !current.is_delegate(&author.into()) {
return Err(ApplyError::UnexpectedState);
}
match action {
@@ -674,7 +675,7 @@ pub struct Revision {
/// Author of this proposed revision.
pub author: Author,
/// New [`Doc`] that will replace `previous`' document.
- pub doc: Doc<Verified>,
+ pub doc: Doc,
/// Physical timestamp of this proposal revision.
pub timestamp: Timestamp,
/// Parent revision.
@@ -685,7 +686,7 @@ pub struct Revision {
}
impl std::ops::Deref for Revision {
- type Target = Doc<Verified>;
+ type Target = Doc;
fn deref(&self) -> &Self::Target {
&self.doc
@@ -736,7 +737,7 @@ impl Revision {
description: String,
author: Author,
blob: Oid,
- doc: Doc<Verified>,
+ doc: Doc,
state: State,
signature: Signature,
parent: Option<RevisionId>,
@@ -788,7 +789,7 @@ impl Revision {
// Mark as rejected if it's impossible for this revision to be accepted
// with the current delegate set. Note that if the delegate set changes,
// this proposal will be marked as `stale` anyway.
- if self.is_active() && self.rejected().count() > self.delegates.len() - self.majority() {
+ if self.is_active() && self.rejected().count() > self.delegates().len() - self.majority() {
self.state = State::Rejected;
}
Ok(())
@@ -834,7 +835,7 @@ impl<R: WriteRepository> store::Transaction<Identity, R> {
&mut self,
title: impl ToString,
description: impl ToString,
- doc: &Doc<Verified>,
+ doc: &Doc,
parent: Option<RevisionId>,
repo: &R,
signer: &G,
@@ -914,7 +915,7 @@ where
&mut self,
title: impl ToString,
description: impl ToString,
- doc: &Doc<Verified>,
+ doc: &Doc,
signer: &G,
) -> Result<RevisionId, Error> {
let parent = self.current;
@@ -1045,7 +1046,7 @@ mod test {
let bob = MockSigner::default();
let signer = &node.signer;
let mut identity = Identity::load_mut(&*repo).unwrap();
- let mut doc = identity.doc().clone();
+ let mut doc = identity.doc().clone().edit();
let title = "Identity update";
let description = "";
let r0 = identity.current;
@@ -1054,33 +1055,38 @@ mod test {
assert!(identity.current().is_accepted());
// Using an identical document to the current one fails.
identity
- .update(title, description, &doc, signer)
+ .update(title, description, &doc.clone().verified().unwrap(), signer)
.unwrap_err();
assert_eq!(identity.current, r0);
// Change threshold to `2`, even though there's only one delegate. This should
// fail as it makes the master branch immutable.
doc.threshold = 2;
- identity
- .update(title, description, &doc, signer)
- .unwrap_err();
- assert_eq!(identity.current, r0);
+ assert!(doc.clone().verified().is_err());
+
// Let's add another delegate.
- doc.delegate(bob.public_key());
+ doc.delegate(bob.public_key().into());
// The update should go through now.
- let r1 = identity.update(title, description, &doc, signer).unwrap();
+ let r1 = identity
+ .update(title, description, &doc.clone().verified().unwrap(), signer)
+ .unwrap();
assert!(identity.revision(&r1).unwrap().is_accepted());
assert_eq!(identity.current, r1);
// With two delegates now, we need two signatures for any update to go through.
// So this next update shouldn't be accepted as canonical until the second delegate
// signs it.
doc.visibility = Visibility::private([]);
- let r2 = identity.update(title, description, &doc, signer).unwrap();
+ let r2 = identity
+ .update(title, description, &doc.clone().verified().unwrap(), signer)
+ .unwrap();
// R1 is still the head.
assert_eq!(identity.current, r1);
assert_eq!(identity.revision(&r2).unwrap().state, State::Active);
assert_eq!(repo.canonical_identity_head().unwrap(), r1);
- assert_eq!(repo.identity_doc().unwrap().visibility, Visibility::Public);
+ assert_eq!(
+ repo.identity_doc().unwrap().visibility(),
+ &Visibility::Public
+ );
// Now let's add a signature on R2 from Bob.
identity.accept(&r2, &bob).unwrap();
@@ -1089,8 +1095,8 @@ mod test {
assert_eq!(identity.revision(&r2).unwrap().state, State::Accepted);
assert_eq!(repo.canonical_identity_head().unwrap(), r2);
assert_eq!(
- repo.canonical_identity_doc().unwrap().visibility,
- Visibility::private([])
+ repo.canonical_identity_doc().unwrap().visibility(),
+ &Visibility::private([])
);
}
@@ -1101,18 +1107,25 @@ mod test {
let eve = MockSigner::default();
let signer = &node.signer;
let mut identity = Identity::load_mut(&*repo).unwrap();
- let mut doc = identity.doc().clone();
+ let mut doc = identity.doc().clone().edit();
let title = "Identity update";
let description = "";
// Let's add another delegate.
- doc.delegate(bob.public_key());
- let r1 = identity.update(title, description, &doc, signer).unwrap();
+ doc.delegate(bob.public_key().into());
+ let r1 = identity
+ .update(title, description, &doc.clone().verified().unwrap(), signer)
+ .unwrap();
assert_eq!(identity.current, r1);
doc.visibility = Visibility::private([]);
let r2 = identity
- .update("Make private", description, &doc, &node.signer)
+ .update(
+ "Make private",
+ description,
+ &doc.clone().verified().unwrap(),
+ &node.signer,
+ )
.unwrap();
// 1/2 rejected means that we can never reach the required 2/2 votes.
@@ -1121,16 +1134,26 @@ mod test {
assert_eq!(r2.state, State::Rejected);
// Now let's add another delegate.
- doc.delegate(eve.public_key());
+ doc.delegate(eve.public_key().into());
let r3 = identity
- .update("Add Eve", description, &doc, &node.signer)
+ .update(
+ "Add Eve",
+ description,
+ &doc.clone().verified().unwrap(),
+ &node.signer,
+ )
.unwrap();
let _ = identity.accept(&r3, &bob).unwrap();
assert_eq!(identity.current, r3);
doc.visibility = Visibility::Public;
let r3 = identity
- .update("Make public", description, &doc, &node.signer)
+ .update(
+ "Make public",
+ description,
+ &doc.verified().unwrap(),
+ &node.signer,
+ )
.unwrap();
// 1/3 rejected means that we can still reach the 2/3 required votes.
@@ -1151,27 +1174,42 @@ mod test {
let bob = &network.bob;
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
- let mut alice_doc = alice_identity.doc().clone();
+ let mut alice_doc = alice_identity.doc().clone().edit();
- alice_doc.delegate(bob.signer.public_key());
+ alice_doc.delegate(bob.signer.public_key().into());
let a1 = alice_identity
- .update("Add Bob", "", &alice_doc, &alice.signer)
+ .update(
+ "Add Bob",
+ "",
+ &alice_doc.clone().verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
bob.repo.fetch(alice);
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
let bob_doc = bob_identity.doc().clone();
- assert!(bob_doc.is_delegate(bob.signer.public_key()));
+ assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
// Alice changes the document without making Bob aware.
alice_doc.visibility = Visibility::private([]);
let a2 = alice_identity
- .update("Change visibility", "", &alice_doc, &alice.signer)
+ .update(
+ "Change visibility",
+ "",
+ &alice_doc.clone().clone().verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
// Bob makes the same change without knowing Alice already did.
let b1 = bob_identity
- .update("Make private", "", &alice_doc, &bob.signer)
+ .update(
+ "Make private",
+ "",
+ &alice_doc.verified().unwrap(),
+ &bob.signer,
+ )
.unwrap();
// Bob gets Alice's data.
@@ -1203,17 +1241,27 @@ mod test {
let eve = &network.eve;
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
- let mut alice_doc = alice_identity.doc().clone();
+ let mut alice_doc = alice_identity.doc().clone().edit();
- alice_doc.delegate(bob.signer.public_key());
+ alice_doc.delegate(bob.signer.public_key().into());
let a0 = alice_identity.root;
let a1 = alice_identity
- .update("Add Bob", "Eh.", &alice_doc, &alice.signer)
+ .update(
+ "Add Bob",
+ "Eh.",
+ &alice_doc.clone().clone().verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
alice_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
let a2 = alice_identity
- .update("Change visibility", "Eh.", &alice_doc, &alice.signer)
+ .update(
+ "Change visibility",
+ "Eh.",
+ &alice_doc.verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
bob.repo.fetch(alice);
@@ -1242,18 +1290,28 @@ mod test {
let eve = &network.eve;
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
- let mut alice_doc = alice_identity.doc().clone();
+ let mut alice_doc = alice_identity.doc().clone().edit();
- alice_doc.delegate(bob.signer.public_key());
- alice_doc.delegate(eve.signer.public_key());
+ alice_doc.delegate(bob.signer.public_key().into());
+ alice_doc.delegate(eve.signer.public_key().into());
let a0 = alice_identity.root;
let a1 = alice_identity // Change description to change traversal order.
- .update("Add Bob and Eve", "Eh#!", &alice_doc, &alice.signer)
+ .update(
+ "Add Bob and Eve",
+ "Eh#!",
+ &alice_doc.clone().verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
- alice_doc.rescind(eve.signer.public_key()).unwrap();
+ alice_doc.rescind(&eve.signer.public_key().into()).unwrap();
let a2 = alice_identity
- .update("Remove Eve", "", &alice_doc, &alice.signer)
+ .update(
+ "Remove Eve",
+ "",
+ &alice_doc.verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
bob.repo.fetch(eve);
@@ -1265,10 +1323,15 @@ mod test {
assert_eq!(bob_identity.current, a2);
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
- let mut eve_doc = eve_identity.doc().clone();
+ let mut eve_doc = eve_identity.doc().clone().edit();
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
let e1 = eve_identity
- .update("Change visibility", "", &eve_doc, &eve.signer)
+ .update(
+ "Change visibility",
+ "",
+ &eve_doc.verified().unwrap(),
+ &eve.signer,
+ )
.unwrap();
// Eve's revision is active.
assert!(eve_identity.revision(&e1).unwrap().is_active());
@@ -1288,7 +1351,7 @@ mod test {
// her revision is no longer valid.
assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1]);
assert_eq!(eve_identity.revision(&e1), None);
- assert!(!eve_identity.is_delegate(eve.signer.public_key()));
+ assert!(!eve_identity.is_delegate(&eve.signer.public_key().into()));
}
#[test]
@@ -1299,18 +1362,28 @@ mod test {
let eve = &network.eve;
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
- let mut alice_doc = alice_identity.doc().clone();
+ let mut alice_doc = alice_identity.doc().clone().edit();
- alice_doc.delegate(bob.signer.public_key());
- alice_doc.delegate(eve.signer.public_key());
+ alice_doc.delegate(bob.signer.public_key().into());
+ alice_doc.delegate(eve.signer.public_key().into());
let a0 = alice_identity.root;
let a1 = alice_identity
- .update("Add Bob and Eve", "Eh!#", &alice_doc, &alice.signer)
+ .update(
+ "Add Bob and Eve",
+ "Eh!#",
+ &alice_doc.clone().verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
alice_doc.visibility = Visibility::private([]);
let a2 = alice_identity
- .update("Change visibility", "", &alice_doc, &alice.signer)
+ .update(
+ "Change visibility",
+ "",
+ &alice_doc.verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
bob.repo.fetch(eve);
@@ -1327,10 +1400,15 @@ mod test {
assert!(eve_identity.revision(&a2).unwrap().is_active());
// Then she submits a new revision.
- let mut eve_doc = eve_identity.doc().clone();
+ let mut eve_doc = eve_identity.doc().clone().edit();
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
let e2 = eve_identity
- .update("Change visibility", "", &eve_doc, &eve.signer)
+ .update(
+ "Change visibility",
+ "",
+ &eve_doc.verified().unwrap(),
+ &eve.signer,
+ )
.unwrap();
assert!(eve_identity.revision(&e2).unwrap().is_active());
@@ -1368,23 +1446,28 @@ mod test {
let eve = &network.eve;
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
- let mut alice_doc = alice_identity.doc().clone();
+ let mut alice_doc = alice_identity.doc().clone().edit();
alice.repo.fetch(bob);
alice.repo.fetch(eve);
- alice_doc.delegate(bob.signer.public_key());
- alice_doc.delegate(eve.signer.public_key());
+ alice_doc.delegate(bob.signer.public_key().into());
+ alice_doc.delegate(eve.signer.public_key().into());
let a0 = alice_identity.root;
let a1 = alice_identity
- .update("Add Bob and Eve", "", &alice_doc, &alice.signer)
+ .update(
+ "Add Bob and Eve",
+ "",
+ &alice_doc.verified().unwrap(),
+ &alice.signer,
+ )
.unwrap();
bob.repo.fetch(alice);
eve.repo.fetch(alice);
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
- let mut bob_doc = bob_identity.doc().clone();
- assert!(bob_doc.is_delegate(bob.signer.public_key()));
+ let mut bob_doc = bob_identity.doc().clone().edit();
+ assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
// a2 e1
// | /
@@ -1397,17 +1480,27 @@ mod test {
// Bob and Alice change the document visibility. Eve is not aware.
bob_doc.visibility = Visibility::private([]);
let b1 = bob_identity
- .update("Change visibility #1", "", &bob_doc, &bob.signer)
+ .update(
+ "Change visibility #1",
+ "",
+ &bob_doc.verified().unwrap(),
+ &bob.signer,
+ )
.unwrap();
alice.repo.fetch(bob);
eve.repo.fetch(bob);
// In the meantime, Eve does the same thing on her side.
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
- let mut eve_doc = eve_identity.doc().clone();
+ let mut eve_doc = eve_identity.doc().clone().edit();
eve_doc.visibility = Visibility::private([]);
let e1 = eve_identity
- .update("Change visibility #2", "Woops", &eve_doc, &eve.signer)
+ .update(
+ "Change visibility #2",
+ "Woops",
+ &eve_doc.verified().unwrap(),
+ &eve.signer,
+ )
.unwrap();
assert_eq!(eve_identity.revisions().count(), 4);
assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);
@@ -1441,27 +1534,37 @@ mod test {
let repo = storage.repository(id).unwrap();
let mut identity = Identity::load_mut(&repo).unwrap();
- let mut doc = identity.doc().clone();
+ let doc = identity.doc().clone();
let prj = doc.project().unwrap();
+ let mut doc = doc.edit();
// Make a change to the description and sign it.
let desc = prj.description().to_owned() + "!";
let prj = prj.update(None, desc, None).unwrap();
doc.payload.insert(PayloadId::project(), prj.clone().into());
identity
- .update("Update description", "", &doc, &alice)
+ .update(
+ "Update description",
+ "",
+ &doc.clone().verified().unwrap(),
+ &alice,
+ )
.unwrap();
// Add Bob as a delegate, and sign it.
- doc.delegate(bob.public_key());
+ doc.delegate(bob.public_key().into());
doc.threshold = 2;
- identity.update("Add bob", "", &doc, &alice).unwrap();
+ identity
+ .update("Add bob", "", &doc.clone().verified().unwrap(), &alice)
+ .unwrap();
// Add Eve as a delegate.
- doc.delegate(eve.public_key());
+ doc.delegate(eve.public_key().into());
// Update with both Bob and Alice's signature.
- let revision = identity.update("Add eve", "", &doc, &alice).unwrap();
+ let revision = identity
+ .update("Add eve", "", &doc.clone().verified().unwrap(), &alice)
+ .unwrap();
identity.accept(&revision, &bob).unwrap();
// Update description again with signatures by Eve and Bob.
@@ -1470,7 +1573,12 @@ mod test {
doc.payload.insert(PayloadId::project(), prj.into());
let revision = identity
- .update("Update description again", "Bob's repository", &doc, &bob)
+ .update(
+ "Update description again",
+ "Bob's repository",
+ &doc.verified().unwrap(),
+ &bob,
+ )
.unwrap();
identity.accept(&revision, &eve).unwrap();
diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs
index 088cfd3a7..a8ac8250b 100644
--- a/radicle/src/cob/issue.rs
+++ b/radicle/src/cob/issue.rs
@@ -16,8 +16,8 @@ use crate::cob::thread;
use crate::cob::thread::{Comment, CommentId, Thread};
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName};
use crate::crypto::Signer;
-use crate::identity::doc::{Doc, DocError};
-use crate::prelude::{Did, ReadRepository, RepoId, Verified};
+use crate::identity::doc::DocError;
+use crate::prelude::{Did, Doc, ReadRepository, RepoId};
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};
pub use cache::Cache;
@@ -325,9 +325,9 @@ impl Issue {
&self,
action: &Action,
actor: &ActorId,
- doc: &Doc<Verified>,
+ doc: &Doc,
) -> Result<Authorization, Error> {
- if doc.is_delegate(actor) {
+ if doc.is_delegate(&actor.into()) {
// A delegate is authorized to do all actions.
return Ok(Authorization::Allow);
}
@@ -388,7 +388,7 @@ impl Issue {
author: ActorId,
timestamp: Timestamp,
_concurrent: &[&cob::Entry],
- _doc: &Doc<Verified>,
+ _doc: &Doc,
_repo: &R,
) -> Result<(), Error> {
match action {
diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs
index e81fff552..8755dc892 100644
--- a/radicle/src/cob/patch.rs
+++ b/radicle/src/cob/patch.rs
@@ -630,9 +630,9 @@ impl Patch {
&self,
action: &Action,
actor: &ActorId,
- doc: &Doc<Verified>,
+ doc: &Doc,
) -> Result<Authorization, Error> {
- if doc.is_delegate(actor) {
+ if doc.is_delegate(&actor.into()) {
// A delegate is authorized to do all actions.
return Ok(Authorization::Allow);
}
@@ -745,7 +745,7 @@ impl Patch {
author: ActorId,
timestamp: Timestamp,
_concurrent: &[&cob::Entry],
- identity: &Doc<Verified>,
+ identity: &Doc,
repo: &R,
) -> Result<(), Error> {
match action {
@@ -1046,7 +1046,7 @@ impl Patch {
},
);
// Discard revisions that weren't merged by a threshold of delegates.
- merges.retain(|_, count| *count >= identity.threshold);
+ merges.retain(|_, count| *count >= identity.threshold());
match merges.into_keys().collect::<Vec<_>>().as_slice() {
[] => {
@@ -2970,9 +2970,9 @@ mod test {
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
let mut alice = Actor::new(MockSigner::default());
let rid = gen::<RepoId>(1);
- let doc = Doc::new(
+ let doc = RawDoc::new(
gen::<Project>(1),
- nonempty::NonEmpty::new(alice.did()),
+ vec![alice.did()],
1,
identity::Visibility::Public,
)
diff --git a/radicle/src/identity.rs b/radicle/src/identity.rs
index 64f082f07..620f16c7f 100644
--- a/radicle/src/identity.rs
+++ b/radicle/src/identity.rs
@@ -5,7 +5,7 @@ pub mod project;
pub use crypto::PublicKey;
pub use did::Did;
-pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RepoId, Visibility};
+pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RawDoc, RepoId, Visibility};
pub use project::Project;
pub use crate::cob::identity::{Error, Identity, IdentityMut};
diff --git a/radicle/src/identity/doc.rs b/radicle/src/identity/doc.rs
index e0b2e854c..e47702f29 100644
--- a/radicle/src/identity/doc.rs
+++ b/radicle/src/identity/doc.rs
@@ -2,7 +2,7 @@ mod id;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
-use std::marker::PhantomData;
+use std::num::NonZeroUsize;
use std::ops::{Deref, Not};
use std::path::Path;
use std::str::FromStr;
@@ -17,7 +17,7 @@ use thiserror::Error;
use crate::canonical::formatter::CanonicalFormatter;
use crate::cob::identity;
use crate::crypto;
-use crate::crypto::{Signature, Unverified, Verified};
+use crate::crypto::Signature;
use crate::git;
use crate::identity::{project::Project, Did};
use crate::storage;
@@ -37,10 +37,10 @@ pub const MAX_DELEGATES: usize = 255;
pub enum DocError {
#[error("json: {0}")]
Json(#[from] serde_json::Error),
- #[error("invalid delegates: {0}")]
- Delegates(&'static str),
- #[error("invalid threshold `{0}`: {1}")]
- Threshold(usize, &'static str),
+ #[error(transparent)]
+ Delegates(#[from] DelegatesError),
+ #[error(transparent)]
+ Threshold(#[from] ThresholdError),
#[error("git: {0}")]
GitExt(#[from] git::Error),
#[error("git: {0}")]
@@ -49,6 +49,14 @@ pub enum DocError {
Missing,
}
+#[derive(Debug, Error)]
+#[error("invalid delegates: {0}")]
+pub struct DelegatesError(&'static str);
+
+#[derive(Debug, Error)]
+#[error("invalid threshold `{0}`: {1}")]
+pub struct ThresholdError(usize, &'static str);
+
impl DocError {
/// Whether this error is caused by the document not being found.
pub fn is_not_found(&self) -> bool {
@@ -99,7 +107,9 @@ pub enum PayloadError {
NotFound(PayloadId),
}
-/// Payload value.
+/// A `Payload` is a free-form JSON value that can be associated with an
+/// identity's [`Doc`].
+/// The payload is identified in the [`Doc`] by its corresponding [`PayloadId`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Payload {
@@ -137,25 +147,25 @@ pub struct DocAt {
/// The document blob at this commit.
pub blob: Oid,
/// The parsed document.
- pub doc: Doc<Verified>,
+ pub doc: Doc,
}
impl Deref for DocAt {
- type Target = Doc<Verified>;
+ type Target = Doc;
fn deref(&self) -> &Self::Target {
&self.doc
}
}
-impl From<DocAt> for Doc<Verified> {
+impl From<DocAt> for Doc {
fn from(value: DocAt) -> Self {
value.doc
}
}
-impl AsRef<Doc<Verified>> for DocAt {
- fn as_ref(&self) -> &Doc<Verified> {
+impl AsRef<Doc> for DocAt {
+ fn as_ref(&self) -> &Doc {
&self.doc
}
}
@@ -209,43 +219,410 @@ impl Visibility {
}
}
-/// An identity document.
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+/// `RawDoc` is similar to the [`Doc`] type, however, it can be edited and may
+/// not be valid.
+///
+/// It is expected that any changes to a [`Doc`] are made via [`RawDoc`], and
+/// then verified by using [`RawDoc::verified`].
+///
+/// Note that `RawDoc` only implements [`Deserialize`]. This prevents us from
+/// serializing an unverified document, while also making sure that any document
+/// that is deserialized is verified.
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct Doc<V> {
+pub struct RawDoc {
/// The payload section.
pub payload: BTreeMap<PayloadId, Payload>,
/// The delegates section.
- pub delegates: NonEmpty<Did>,
+ pub delegates: Vec<Did>,
/// The signature threshold.
pub threshold: usize,
/// Repository visibility.
- #[serde(default, skip_serializing_if = "Visibility::is_public")]
+ #[serde(default)]
pub visibility: Visibility,
+}
+
+impl TryFrom<RawDoc> for Doc {
+ type Error = DocError;
+
+ fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
+ doc.verified()
+ }
+}
+
+impl RawDoc {
+ /// Construct a new [`RawDoc`] with an initial [`RawDoc::payload`]
+ /// containing the provided [`Project`], and the given `delegates`,
+ /// `threshold`, and `visibility`.
+ pub fn new(
+ project: Project,
+ delegates: Vec<Did>,
+ threshold: usize,
+ visibility: Visibility,
+ ) -> Self {
+ let project =
+ serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
+
+ Self {
+ payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
+ delegates,
+ threshold,
+ visibility,
+ }
+ }
+
+ /// Get the project payload, if it exists and is valid, out of this document.
+ pub fn project(&self) -> Result<Project, PayloadError> {
+ let value = self
+ .payload
+ .get(&PayloadId::project())
+ .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
+ let proj: Project = serde_json::from_value((**value).clone())?;
+
+ Ok(proj)
+ }
+
+ /// Check if the given `did` is in the set of [`RawDoc::delegates`].
+ pub fn is_delegate(&self, did: &Did) -> bool {
+ self.delegates.contains(did)
+ }
+
+ /// Add a new delegate to the document.
+ ///
+ /// Note that if this `Did` is a duplicate, then the resulting set will only
+ /// show it once.
+ pub fn delegate(&mut self, did: Did) {
+ self.delegates.push(did)
+ }
+
+ /// Remove the `did` from the set of delegates. Returns `true` if it was
+ /// removed.
+ pub fn rescind(&mut self, did: &Did) -> Result<bool, DocError> {
+ let (matches, delegates) = self.delegates.iter().partition(|d| *d == did);
+ self.delegates = delegates;
+ Ok(matches.is_empty().not())
+ }
+
+ /// Construct the `RawDoc` from the set of `bytes` that are expected to be
+ /// in JSON format.
+ pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
+ serde_json::from_slice(bytes).map_err(DocError::from)
+ }
+
+ /// Verify the `RawDoc`'s values, converting it into a valid [`Doc`].
+ ///
+ /// The verifications are as follows:
+ ///
+ /// - [`RawDoc::delegates`]: any duplicates are removed, and for the
+ /// remaining set ensure that it is non-empty and does not exceed a
+ /// length of [`MAX_DELEGATES`].
+ /// - [`RawDoc::threshold`]: ensure that it is in the range `[1, delegates.len()]`.
+ pub fn verified(self) -> Result<Doc, DocError> {
+ let RawDoc {
+ payload,
+ delegates,
+ threshold,
+ visibility,
+ } = self;
+ let delegates = Delegates::new(delegates)?;
+ let threshold = Threshold::new(threshold, &delegates)?;
+ Ok(Doc {
+ payload,
+ delegates,
+ threshold,
+ visibility,
+ })
+ }
+}
- #[serde(skip)]
- verified: PhantomData<V>,
+/// A valid set of delegates for the identity [`Doc`].
+///
+/// It can only be constructed via [`Delegates::new`].
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(try_from = "Vec<Did>")]
+pub struct Delegates(NonEmpty<Did>);
+
+impl AsRef<NonEmpty<Did>> for Delegates {
+ fn as_ref(&self) -> &NonEmpty<Did> {
+ &self.0
+ }
+}
+
+impl TryFrom<Vec<Did>> for Delegates {
+ type Error = DelegatesError;
+
+ fn try_from(dids: Vec<Did>) -> Result<Self, Self::Error> {
+ Delegates::new(dids)
+ }
+}
+
+impl IntoIterator for Delegates {
+ type Item = <NonEmpty<Did> as IntoIterator>::Item;
+ type IntoIter = <NonEmpty<Did> as IntoIterator>::IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.0.into_iter()
+ }
+}
+
+impl Delegates {
+ /// Construct the set of `Delegates` by removing any duplicate [`Did`]s,
+ /// ensure that the set is non-empty, and check the length does not exceed
+ /// the [`MAX_DELEGATES`].
+ pub fn new(delegates: impl IntoIterator<Item = Did>) -> Result<Self, DelegatesError> {
+ let delegates = delegates
+ .into_iter()
+ .try_fold(Vec::<Did>::new(), |mut dids, did| {
+ if !dids.contains(&did) {
+ if dids.len() >= MAX_DELEGATES {
+ return Err(DelegatesError("number of delegates cannot exceed 255"));
+ }
+ dids.push(did);
+ }
+ Ok(dids)
+ })?;
+ NonEmpty::from_vec(delegates)
+ .map(Self)
+ .ok_or(DelegatesError("delegate list cannot be empty"))
+ }
+
+ /// Get the first delegate in the set.
+ pub fn first(&self) -> &Did {
+ self.0.first()
+ }
+
+ /// Obtain an iterator over the [`Did`]s.
+ pub fn iter(&self) -> impl Iterator<Item = &Did> {
+ self.0.iter()
+ }
+
+ /// Check if the set contains the given `did`.
+ pub fn contains(&self, did: &Did) -> bool {
+ self.0.contains(did)
+ }
+
+ /// Get the number of delegates in the set.
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ /// Check if the set is empty. Note that this always returns `false`.
+ pub fn is_empty(&self) -> bool {
+ false
+ }
}
-impl<V> Doc<V> {
- /// Check whether this document and the associated repository is visible to the given peer.
- pub fn is_visible_to(&self, peer: &PublicKey) -> bool {
+impl<'a> From<&'a Delegates> for &'a NonEmpty<Did> {
+ fn from(ds: &'a Delegates) -> Self {
+ &ds.0
+ }
+}
+
+impl From<Delegates> for NonEmpty<Did> {
+ fn from(ds: Delegates) -> Self {
+ ds.0
+ }
+}
+
+impl From<Delegates> for Vec<Did> {
+ fn from(Delegates(ds): Delegates) -> Self {
+ ds.into()
+ }
+}
+
+/// A valid threshold for the identity [`Doc`].
+///
+/// It can only be constructed via [`Threshold::new`] or [`Threshold::MIN`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
+#[serde(transparent)]
+pub struct Threshold(NonZeroUsize);
+
+impl From<Threshold> for usize {
+ fn from(Threshold(t): Threshold) -> Self {
+ t.get()
+ }
+}
+
+impl AsRef<NonZeroUsize> for Threshold {
+ fn as_ref(&self) -> &NonZeroUsize {
+ &self.0
+ }
+}
+
+impl fmt::Display for Threshold {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl Threshold {
+ /// A threshold of `1`.
+ pub const MIN: Threshold = Threshold(NonZeroUsize::MIN);
+
+ /// Construct the `Threshold` by checking that `t` is not greater than
+ /// [`MAX_DELEGATES`], that it does not exceed the number of delegates, and
+ /// is non-zero.
+ pub fn new(t: usize, delegates: &Delegates) -> Result<Self, ThresholdError> {
+ if t > MAX_DELEGATES {
+ Err(ThresholdError(t, "threshold cannot exceed 255"))
+ } else if t > delegates.len() {
+ Err(ThresholdError(
+ t,
+ "threshold cannot exceed number of delegates",
+ ))
+ } else {
+ NonZeroUsize::new(t)
+ .map(Self)
+ .ok_or(ThresholdError(t, "threshold cannot be zero"))
+ }
+ }
+}
+
+/// `Doc` is a valid identity document.
+///
+/// To ensure that only valid documents are used, this type is restricted to be
+/// read-only. For mutating the document use [`Doc::edit`].
+///
+/// A valid `Doc` can be constructed in four ways:
+///
+/// 1. [`Doc::initial`]: a safe way to construct the initial document for an identity.
+/// 2. [`RawDoc::verified`]: validates a [`RawDoc`]'s fields and converts it
+/// into a `Doc`
+/// 3. [`Deserialize`]: will deserialize a `Doc` by first deserializing a
+/// [`RawDoc`] and use [`RawDoc::verified`] to construct the `Doc`.
+/// 4. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
+/// its contents.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[serde(try_from = "RawDoc")]
+pub struct Doc {
+ payload: BTreeMap<PayloadId, Payload>,
+ delegates: Delegates,
+ threshold: Threshold,
+ #[serde(default, skip_serializing_if = "Visibility::is_public")]
+ visibility: Visibility,
+}
+
+impl Doc {
+ /// Construct the initial [`Doc`] for an identity.
+ ///
+ /// It will begin with the provided `project` in the [`Doc::payload`], the
+ /// `delegate` as the sole delegate, a threshold of 1, and the given
+ /// `visibility`.
+ pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
+ let project =
+ serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
+
+ Self {
+ payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
+ delegates: Delegates(NonEmpty::new(delegate)),
+ threshold: Threshold(NonZeroUsize::MIN),
+ visibility,
+ }
+ }
+
+ /// Construct a [`Doc`] contained in the provided Git blob.
+ pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
+ RawDoc::from_json(blob.content())?.verified()
+ }
+
+ /// Convert the [`Doc`] into a [`RawDoc`] for changing the field values and
+ /// re-verifying.
+ pub fn edit(self) -> RawDoc {
+ let Doc {
+ payload,
+ delegates,
+ threshold,
+ visibility,
+ } = self;
+ RawDoc {
+ payload,
+ delegates: delegates.into(),
+ threshold: threshold.into(),
+ visibility,
+ }
+ }
+
+ /// Using the current state of the `Doc`, perform any edits on the `RawDoc`
+ /// form and verify the changes.
+ pub fn with_edits<F>(self, f: F) -> Result<Self, DocError>
+ where
+ F: FnOnce(&mut RawDoc),
+ {
+ let mut raw = self.edit();
+ f(&mut raw);
+ raw.verified()
+ }
+
+ /// Return the associated payloads for this [`Doc`].
+ pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
+ &self.payload
+ }
+
+ /// Get the project payload, if it exists and is valid, out of this document.
+ pub fn project(&self) -> Result<Project, PayloadError> {
+ let value = self
+ .payload
+ .get(&PayloadId::project())
+ .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
+ let proj: Project = serde_json::from_value((**value).clone())?;
+
+ Ok(proj)
+ }
+
+ /// Return the associated [`Visibility`] of this document.
+ pub fn visibility(&self) -> &Visibility {
+ &self.visibility
+ }
+
+ /// Check whether the visibility of the document is public.
+ pub fn is_public(&self) -> bool {
+ self.visibility.is_public()
+ }
+
+ /// Check whether the visibility of the document is private.
+ pub fn is_private(&self) -> bool {
+ self.visibility.is_private()
+ }
+
+ /// Return the associated threshold of this document.
+ pub fn threshold(&self) -> usize {
+ self.threshold.into()
+ }
+
+ /// Return the associated threshold of this document in its non-zero format.
+ pub fn threshold_nonzero(&self) -> &NonZeroUsize {
+ &self.threshold.0
+ }
+
+ /// Return the associated delegates of this document.
+ pub fn delegates(&self) -> &Delegates {
+ &self.delegates
+ }
+
+ /// Check if the `did` is part of the [`Doc::delegates`] set.
+ pub fn is_delegate(&self, did: &Did) -> bool {
+ self.delegates.contains(did)
+ }
+
+ /// Check whether this document and the associated repository is visible to
+ /// the given peer.
+ pub fn is_visible_to(&self, did: &Did) -> bool {
match &self.visibility {
Visibility::Public => true,
- Visibility::Private { allow } => {
- allow.contains(&Did::from(*peer)) || self.is_delegate(peer)
- }
+ Visibility::Private { allow } => allow.contains(did) || self.is_delegate(did),
}
}
- /// Validate signature using this document's delegates, against a given document blob.
+ /// Validate `signature` using this document's delegates, against a given
+ /// document blob.
pub fn verify_signature(
&self,
key: &PublicKey,
signature: &Signature,
blob: Oid,
) -> Result<(), PublicKey> {
- if !self.is_delegate(key) {
+ if !self.is_delegate(&key.into()) {
return Err(*key);
}
if key.verify(blob.as_bytes(), signature).is_err() {
@@ -254,25 +631,27 @@ impl<V> Doc<V> {
Ok(())
}
+ /// Check the provided `votes` passes the [`Doc::majority`].
pub fn is_majority(&self, votes: usize) -> bool {
votes >= self.majority()
}
+ /// Return the majority number based on the size of the delegates set.
pub fn majority(&self) -> usize {
self.delegates.len() / 2 + 1
}
- pub fn blob_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
+ /// Helper for getting an `embeds` Git blob.
+ pub(crate) fn blob_at<R: ReadRepository>(
+ commit: Oid,
+ repo: &R,
+ ) -> Result<git2::Blob, DocError> {
let path = Path::new("embeds").join(*PATH);
repo.blob_at(commit, path.as_path()).map_err(DocError::from)
}
- pub fn is_delegate(&self, key: &crypto::PublicKey) -> bool {
- self.delegates.contains(&key.into())
- }
-}
-
-impl Doc<Verified> {
+ /// Encode the [`Doc`] as canonical JSON, returning the set of bytes and its
+ /// corresponding Git [`Oid`].
pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
let mut buf = Vec::new();
let mut serializer =
@@ -284,45 +663,8 @@ impl Doc<Verified> {
Ok((oid.into(), buf))
}
- /// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
- pub fn delegate(&mut self, key: &crypto::PublicKey) -> bool {
- let delegate = Did::from(key);
- if self.delegates.iter().all(|id| id != &delegate) {
- self.delegates.push(delegate);
- return true;
- }
- false
- }
-
- pub fn rescind(&mut self, key: &crypto::PublicKey) -> Result<Option<Did>, DocError> {
- let delegate = Did::from(key);
- let (matches, delegates) = self.delegates.iter().partition(|d| **d == delegate);
- match NonEmpty::from_vec(delegates) {
- Some(delegates) => {
- self.delegates = delegates;
- if self.threshold > self.delegates.len() {
- return Err(DocError::Threshold(
- self.threshold,
- "the thresholds exceeds the new delegate count after removal",
- ));
- }
- Ok(matches.is_empty().not().then_some(delegate))
- }
- None => Err(DocError::Delegates("cannot remove the last delegate")),
- }
- }
-
- /// Get the project payload, if it exists and is valid, out of this document.
- pub fn project(&self) -> Result<Project, PayloadError> {
- let value = self
- .payload
- .get(&PayloadId::project())
- .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
- let proj: Project = serde_json::from_value((**value).clone())?;
-
- Ok(proj)
- }
-
+ /// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
+ /// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
pub fn sign<G: crypto::Signer>(
&self,
signer: &G,
@@ -333,15 +675,18 @@ impl Doc<Verified> {
Ok((oid, bytes, sig))
}
+ /// Similar to [`Doc::sign`], but only returning the [`Signature`].
pub fn signature_of<G: crypto::Signer>(&self, signer: &G) -> Result<Signature, DocError> {
let (_, _, sig) = self.sign(signer)?;
Ok(sig)
}
+ /// Load the [`DocAt`] found at the given `commit`. The [`DocAt`] will
+ /// contain the corresponding [`Doc`].
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
let blob = Self::blob_at(commit, repo)?;
- let doc = Doc::from_blob(&blob)?;
+ let doc = Self::from_blob(&blob)?;
Ok(DocAt {
commit,
@@ -350,10 +695,8 @@ impl Doc<Verified> {
})
}
- pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
- Doc::from_json(blob.content())?.verified()
- }
-
+ /// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
+ /// document.
pub fn init<G: crypto::Signer>(
&self,
repo: &storage::git::Repository,
@@ -378,79 +721,54 @@ impl Doc<Verified> {
}
}
-impl Doc<Unverified> {
- pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
- Self::new(project, NonEmpty::new(delegate), 1, visibility)
- }
-
- pub fn new(
- project: Project,
- delegates: NonEmpty<Did>,
- threshold: usize,
- visibility: Visibility,
- ) -> Self {
- let project =
- serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
-
- Self {
- payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
- delegates,
- threshold,
- visibility,
- verified: PhantomData,
- }
- }
-
- pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
- serde_json::from_slice(bytes).map_err(DocError::from)
- }
-
- pub fn verified(self) -> Result<Doc<Verified>, DocError> {
- if self.delegates.len() > MAX_DELEGATES {
- return Err(DocError::Delegates("number of delegates cannot exceed 255"));
- }
- if self.delegates.is_empty() {
- return Err(DocError::Delegates("delegate list cannot be empty"));
- }
- if self.threshold > self.delegates.len() {
- return Err(DocError::Threshold(
- self.threshold,
- "threshold cannot exceed number of delegates",
- ));
- }
- if self.threshold == 0 {
- return Err(DocError::Threshold(
- self.threshold,
- "threshold cannot be zero",
- ));
- }
-
- Ok(Doc {
- payload: self.payload,
- delegates: self.delegates,
- threshold: self.threshold,
- visibility: self.visibility,
- verified: PhantomData,
- })
- }
-}
-
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use radicle_crypto::test::signer::MockSigner;
use radicle_crypto::Signer as _;
+ use crate::assert_matches;
use crate::rad;
use crate::storage::git::transport;
use crate::storage::git::Storage;
use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
use crate::test::arbitrary;
+ use crate::test::arbitrary::gen;
use crate::test::fixtures;
use super::*;
use qcheck_macros::quickcheck;
+ #[test]
+ fn test_duplicate_dids() {
+ let delegate = MockSigner::from_seed([0xff; 32]);
+ let did = Did::from(delegate.public_key());
+ let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
+ doc.delegate(did);
+ let doc = doc.verified().unwrap();
+ assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
+ assert!(doc.delegates().first() == &did)
+ }
+
+ #[test]
+ fn test_max_delegates() {
+ // Generate more than the max delegates
+ let delegates = (0..MAX_DELEGATES + 1).map(gen).collect::<Vec<Did>>();
+
+ // A document with max delegates will be fine
+ let doc = RawDoc::new(
+ gen::<Project>(1),
+ delegates[0..MAX_DELEGATES].into(),
+ 1,
+ Visibility::Public,
+ );
+ assert_matches!(doc.verified(), Ok(_));
+
+ // A document that exceeds max delegates should fail
+ let doc = RawDoc::new(gen::<Project>(1), delegates, 1, Visibility::Public);
+ assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
+ }
+
#[test]
fn test_canonical_example() {
let tempdir = tempfile::tempdir().unwrap();
@@ -494,7 +812,7 @@ mod test {
let err = repo.identity_head_of(&remote).unwrap_err();
matches!(err, git::ext::Error::NotFound(_));
- let err = Doc::<Verified>::load_at(oid.into(), &repo).unwrap_err();
+ let err = Doc::load_at(oid.into(), &repo).unwrap_err();
assert!(err.is_not_found());
}
@@ -523,9 +841,9 @@ mod test {
}
#[quickcheck]
- fn prop_encode_decode(doc: Doc<Verified>) {
+ fn prop_encode_decode(doc: Doc) {
let (_, bytes) = doc.encode().unwrap();
- assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
+ assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
}
#[test]
diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs
index 2cb900324..1d38b4044 100644
--- a/radicle/src/lib.rs
+++ b/radicle/src/lib.rs
@@ -40,7 +40,7 @@ pub mod prelude {
use super::*;
pub use crypto::{PublicKey, Signer, Verified};
- pub use identity::{project::Project, Did, Doc, RepoId};
+ pub use identity::{project::Project, Did, Doc, RawDoc, RepoId};
pub use node::{Alias, NodeId, Timestamp};
pub use profile::Profile;
pub use storage::{
diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs
index b3fb88bdc..5b08364a5 100644
--- a/radicle/src/rad.rs
+++ b/radicle/src/rad.rs
@@ -52,7 +52,7 @@ pub fn init<G: Signer, S: WriteStorage>(
visibility: Visibility,
signer: &G,
storage: S,
-) -> Result<(RepoId, identity::Doc<Verified>, SignedRefs<Verified>), InitError> {
+) -> Result<(RepoId, identity::Doc, SignedRefs<Verified>), InitError> {
// TODO: Better error when project id already exists in storage, but remote doesn't.
let delegate: identity::Did = signer.public_key().into();
let proj = Project::new(
@@ -68,7 +68,7 @@ pub fn init<G: Signer, S: WriteStorage>(
.join(", "),
)
})?;
- let doc = identity::Doc::initial(proj, delegate, visibility).verified()?;
+ let doc = identity::Doc::initial(proj, delegate, visibility);
let (project, identity) = Repository::init(&doc, &storage, signer)?;
let url = git::Url::from(project.id);
@@ -452,7 +452,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!(doc.delegates.first(), &Did::from(public_key));
+ assert_eq!(doc.delegates().first(), &Did::from(public_key));
}
#[test]
diff --git a/radicle/src/storage.rs b/radicle/src/storage.rs
index 3396f2a8b..17d2fcdeb 100644
--- a/radicle/src/storage.rs
+++ b/radicle/src/storage.rs
@@ -32,13 +32,13 @@ pub type BranchName = git::RefString;
/// Basic repository information.
#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RepositoryInfo<V> {
+pub struct RepositoryInfo {
/// Repository identifier.
pub rid: RepoId,
/// Head of default branch.
pub head: Oid,
/// Identity document.
- pub doc: Doc<V>,
+ pub doc: Doc,
/// Local signed refs, if any.
/// Repositories with this set to `None` are ones that are seeded but not forked.
pub refs: Option<refs::SignedRefsAt>,
@@ -412,11 +412,11 @@ pub trait ReadStorage {
/// Check whether storage contains a repository.
fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError>;
/// Return all repositories (public and private).
- fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error>;
+ fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error>;
/// Open or create a read-only repository.
fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError>;
/// Get a repository's identity if it exists.
- fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
+ fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
match self.repository(rid) {
Ok(repo) => Ok(Some(repo.identity_doc()?.into())),
Err(e) if e.is_not_found() => Ok(None),
@@ -569,9 +569,9 @@ pub trait ReadRepository: Sized + ValidateRepository {
/// Get repository delegates.
fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
- let doc: Doc<_> = self.identity_doc()?.into();
+ let doc = self.identity_doc()?;
- Ok(doc.delegates)
+ Ok(doc.delegates().clone().into())
}
/// Get the repository's identity document.
@@ -683,7 +683,7 @@ where
self.deref().contains(rid)
}
- fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
+ fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
self.deref().get(rid)
}
@@ -691,7 +691,7 @@ where
self.deref().repository(rid)
}
- fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
+ fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
self.deref().repositories()
}
}
diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs
index 41c27f0a6..d47321040 100644
--- a/radicle/src/storage/git.rs
+++ b/radicle/src/storage/git.rs
@@ -11,10 +11,9 @@ use crypto::{Signer, Verified};
use once_cell::sync::Lazy;
use tempfile::TempDir;
-use crate::crypto::Unverified;
use crate::git::canonical::Canonical;
use crate::identity::doc::DocError;
-use crate::identity::{doc::DocAt, Doc, RepoId};
+use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
use crate::node::SyncedAt;
use crate::storage::refs;
@@ -106,7 +105,7 @@ impl ReadStorage for Storage {
Repository::open(paths::repository(self, &rid), rid)
}
- fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
+ fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
let mut repos = Vec::new();
for result in fs::read_dir(&self.path)? {
@@ -238,7 +237,7 @@ impl Storage {
pub fn repositories_by_id<'a>(
&self,
mut rids: impl Iterator<Item = &'a RepoId>,
- ) -> Result<Vec<RepositoryInfo<Verified>>, RepositoryError> {
+ ) -> Result<Vec<RepositoryInfo>, RepositoryError> {
rids.try_fold(Vec::new(), |mut infos, rid| {
let repo = self.repository(*rid)?;
let (_, head) = repo.head()?;
@@ -391,7 +390,7 @@ impl Repository {
let delegates = self
.delegates()?
.into_iter()
- .map(RemoteId::from)
+ .map(|did| *did)
.collect::<BTreeSet<_>>();
let mut deleted = Vec::new();
for id in self.remote_ids()? {
@@ -435,7 +434,7 @@ impl Repository {
/// Create the repository's identity branch.
pub fn init<G: Signer, S: WriteStorage>(
- doc: &Doc<Verified>,
+ doc: &Doc,
storage: &S,
signer: &G,
) -> Result<(Self, git::Oid), RepositoryError> {
@@ -491,7 +490,7 @@ impl Repository {
Ok(proj)
}
- pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, DocError> {
+ pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc, DocError> {
let oid = self.identity_head_of(remote)?;
Doc::load_at(oid, self).map(|d| d.into())
}
@@ -730,7 +729,7 @@ impl ReadRepository for Repository {
}
fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError> {
- Doc::<Verified>::load_at(head, self)
+ Doc::load_at(head, self)
}
fn head(&self) -> Result<(Qualified, Oid), RepositoryError> {
@@ -748,8 +747,8 @@ impl ReadRepository for Repository {
let project = doc.project()?;
let branch_ref = git::refs::branch(project.default_branch());
let raw = self.raw();
- let oid = Canonical::default_branch(self, &project, &doc.delegates)?
- .quorum(doc.threshold, raw)?;
+ let oid = Canonical::default_branch(self, &project, doc.delegates().into())?
+ .quorum(doc.threshold(), raw)?;
Ok((branch_ref, oid))
}
@@ -794,7 +793,7 @@ impl ReadRepository for Repository {
let Ok(root) = self.identity_root_of(&remote) else {
continue;
};
- let blob = Doc::<Unverified>::blob_at(root, self)?;
+ let blob = Doc::blob_at(root, self)?;
// We've got an identity that goes back to the correct root.
if blob.id() == **self.id {
diff --git a/radicle/src/storage/refs.rs b/radicle/src/storage/refs.rs
index df52e17cf..f5abe8c9b 100644
--- a/radicle/src/storage/refs.rs
+++ b/radicle/src/storage/refs.rs
@@ -498,7 +498,7 @@ mod tests {
// Alice creates "paris" repo.
let (paris_repo, paris_head) = fixtures::repository(tmp.path().join("paris"));
- let (paris_rid, mut paris_doc, _) = rad::init(
+ let (paris_rid, paris_doc, _) = rad::init(
&paris_repo,
"paris".try_into().unwrap(),
"Paris repository",
@@ -511,7 +511,7 @@ mod tests {
// Alice creates "london" repo.
let (london_repo, _london_head) = fixtures::repository(tmp.path().join("london"));
- let (london_rid, mut london_doc, _) = rad::init(
+ let (london_rid, london_doc, _) = rad::init(
&london_repo,
"london".try_into().unwrap(),
"London repository",
@@ -532,8 +532,16 @@ mod tests {
// Bob is added to both repos as a delegate, by Alice.
{
- paris_doc.delegates.push(bob.public_key().into());
- london_doc.delegates.push(bob.public_key().into());
+ let paris_doc = paris_doc
+ .with_edits(|doc| {
+ doc.delegates.push(bob.public_key().into());
+ })
+ .unwrap();
+ let london_doc = london_doc
+ .with_edits(|doc| {
+ doc.delegates.push(bob.public_key().into());
+ })
+ .unwrap();
let mut paris_ident = Identity::load_mut(&paris).unwrap();
let mut london_ident = Identity::load_mut(&london).unwrap();
diff --git a/radicle/src/test/arbitrary.rs b/radicle/src/test/arbitrary.rs
index d049a3297..a6dd07708 100644
--- a/radicle/src/test/arbitrary.rs
+++ b/radicle/src/test/arbitrary.rs
@@ -5,17 +5,16 @@ use std::str::FromStr;
use std::{iter, net};
use crypto::test::signer::MockSigner;
-use crypto::{PublicKey, Unverified, Verified};
+use crypto::{PublicKey, Unverified};
use cyphernet::addr::tor::OnionAddrV3;
use cyphernet::EcPk;
-use nonempty::NonEmpty;
use qcheck::Arbitrary;
use crate::collections::RandomMap;
use crate::identity::doc::Visibility;
use crate::identity::project::ProjectName;
use crate::identity::{
- doc::{Doc, DocAt, RepoId},
+ doc::{Doc, DocAt, RawDoc, RepoId},
project::Project,
Did,
};
@@ -139,28 +138,26 @@ impl Arbitrary for Visibility {
}
}
-impl Arbitrary for Doc<Unverified> {
+impl Arbitrary for RawDoc {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
let proj = Project::arbitrary(g);
let delegate = Did::arbitrary(g);
let visibility = Visibility::arbitrary(g);
- Self::initial(proj, delegate, visibility)
+ Self::new(proj, vec![delegate], 1, visibility)
}
}
-impl Arbitrary for Doc<Verified> {
+impl Arbitrary for Doc {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
let mut rng = fastrand::Rng::with_seed(u64::arbitrary(g));
let project = Project::arbitrary(g);
- let delegates: NonEmpty<_> = iter::repeat_with(|| Did::arbitrary(g))
+ let delegates = iter::repeat_with(|| Did::arbitrary(g))
.take(rng.usize(1..6))
- .collect::<Vec<_>>()
- .try_into()
- .unwrap();
+ .collect::<Vec<_>>();
let threshold = delegates.len() / 2 + 1;
let visibility = Visibility::arbitrary(g);
- let doc: Doc<Unverified> = Doc::new(project, delegates, threshold, visibility);
+ let doc = RawDoc::new(project, delegates, threshold, visibility);
doc.verified().unwrap()
}
@@ -168,7 +165,7 @@ impl Arbitrary for Doc<Verified> {
impl Arbitrary for DocAt {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
- let doc = Doc::<Verified>::arbitrary(g);
+ let doc = Doc::arbitrary(g);
DocAt {
commit: self::oid(),
@@ -238,7 +235,7 @@ impl Arbitrary for MockStorage {
impl Arbitrary for MockRepository {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
let rid = RepoId::arbitrary(g);
- let doc = Doc::<Verified>::arbitrary(g);
+ let doc = Doc::arbitrary(g);
Self::new(rid, doc)
}
diff --git a/radicle/src/test/storage.rs b/radicle/src/test/storage.rs
index 96464d154..6a0668168 100644
--- a/radicle/src/test/storage.rs
+++ b/radicle/src/test/storage.rs
@@ -7,7 +7,7 @@ use std::str::FromStr;
use git_ext::ref_format as fmt;
use crate::crypto::{Signer, Verified};
-use crate::identity::doc::{Doc, DocAt, DocError, RepoId};
+use crate::identity::doc::{Doc, DocAt, DocError, RawDoc, RepoId};
use crate::node::NodeId;
pub use crate::storage::*;
@@ -51,9 +51,9 @@ impl MockStorage {
.expect("MockStorage::repo_mut: repository does not exist")
}
- pub fn map(mut self, f: impl Fn(&mut Doc<Verified>)) -> Self {
+ pub fn map(mut self, f: impl Fn(&mut RawDoc)) -> Self {
for repo in self.repos.values_mut() {
- f(&mut repo.doc.doc);
+ repo.doc.doc = repo.doc.doc.clone().with_edits(|doc| f(doc)).unwrap();
}
self
}
@@ -91,7 +91,7 @@ impl ReadStorage for MockStorage {
.cloned()
}
- fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
+ fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
Ok(self
.repos
.iter()
@@ -135,7 +135,7 @@ pub struct MockRepository {
}
impl MockRepository {
- pub fn new(id: RepoId, doc: Doc<Verified>) -> Self {
+ pub fn new(id: RepoId, doc: Doc) -> Self {
let (blob, _) = doc.encode().unwrap();
Self {
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name c82c6374-9a12-4f16-9a0c-ec50fac9244d -v /opt/radcis/ci.rad.levitte.org/cci/state/c82c6374-9a12-4f16-9a0c-ec50fac9244d/s:/c82c6374-9a12-4f16-9a0c-ec50fac9244d/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/c82c6374-9a12-4f16-9a0c-ec50fac9244d/w:/c82c6374-9a12-4f16-9a0c-ec50fac9244d/w -w /c82c6374-9a12-4f16-9a0c-ec50fac9244d/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /c82c6374-9a12-4f16-9a0c-ec50fac9244d/s/script.sh
+ cargo --version
info: syncing channel updates for '1.80-x86_64-unknown-linux-gnu'
info: latest update on 2024-08-08, rust version 1.80.1 (3f5fd8dd4 2024-08-06)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-src'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
info: installing component 'rust-src'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'
cargo 1.80.1 (376290515 2024-07-16)
+ rustc --version
rustc 1.80.1 (3f5fd8dd4 2024-08-06)
+ cargo fmt --check
Diff in /c82c6374-9a12-4f16-9a0c-ec50fac9244d/w/radicle-cli/src/commands/clone.rs at line 236:
node: &mut Node,
signer: &G,
storage: &Storage,
-) -> Result<
- (
- raw::Repository,
- storage::git::Repository,
- Doc,
- Project,
- ),
- CloneError,
-> {
+) -> Result<(raw::Repository, storage::git::Repository, Doc, Project), CloneError> {
let me = *signer.public_key();
// Seed repository.
Diff in /c82c6374-9a12-4f16-9a0c-ec50fac9244d/w/radicle-cli/src/commands/init.rs at line 14:
use radicle::explorer::ExplorerUrl;
use radicle::git::RefString;
use radicle::identity::project::ProjectName;
-use radicle::identity::{RepoId, Doc, Visibility};
+use radicle::identity::{Doc, RepoId, Visibility};
use radicle::node::events::UploadPack;
use radicle::node::policy::Scope;
use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
Diff in /c82c6374-9a12-4f16-9a0c-ec50fac9244d/w/radicle-fetch/src/handle.rs at line 4:
use bstr::BString;
use radicle::crypto::PublicKey;
use radicle::git::Oid;
-use radicle::identity::{DocError, Doc};
+use radicle::identity::{Doc, DocError};
use radicle::storage::git::Repository;
use radicle::storage::ReadRepository;
Diff in /c82c6374-9a12-4f16-9a0c-ec50fac9244d/w/radicle-fetch/src/state.rs at line 4:
use gix_protocol::handshake;
use radicle::crypto::PublicKey;
use radicle::git::{Oid, Qualified};
-use radicle::identity::{Did, DocError, Doc};
+use radicle::identity::{Did, Doc, DocError};
use radicle::prelude::Verified;
use radicle::storage;
Exit code: 1
{
"response": "finished",
"result": "failure"
}