Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: refactor doc
Fintan Halpenny committed 1 year ago
commit de1958fab04a95f73b7d7dcc62d66775d0854fc1
parent f83c11674287d4f56c43daac74d7461abfc0ddde
28 files changed +810 -361
modified 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())
modified 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,
modified 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(&current.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
modified 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, \
modified 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)?;
modified 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()
            },
modified 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() {
modified 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));
        }
modified 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()
    };
modified 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)
    }

modified 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)),
modified 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()?;
modified 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>,
}
modified 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);
modified 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(())
modified 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
modified 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();

modified 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 {
modified 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,
        )
modified 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};
modified 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,80 +721,55 @@ 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();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).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]
modified 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::{
modified 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]
modified 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()
    }
}
modified 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 {
modified 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();
modified 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)
    }
modified 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 {