Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src cob identity.rs
use std::collections::BTreeMap;
use std::sync::LazyLock;
use std::{fmt, ops::Deref, str::FromStr};

use crypto::{PublicKey, Signature};
use nonempty::NonEmpty;
use radicle_cob::{Embed, ObjectId, TypeName};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::cob::store::access::WriteAs;
use crate::git;
use crate::git::Oid;
use crate::identity::doc::Doc;
use crate::node::NodeId;
use crate::storage;
use crate::{
    cob,
    cob::{
        ActorId, Timestamp, Uri, op, store,
        store::{Cob, CobAction, Transaction},
    },
    identity::{
        Did,
        doc::{DocError, RepoId},
    },
    storage::{ReadRepository, RepositoryError, WriteRepository},
};

use super::{Author, EntryId};

/// Type name of an identity proposal.
pub static TYPENAME: LazyLock<TypeName> =
    LazyLock::new(|| FromStr::from_str("xyz.radicle.id").expect("type name is valid"));

/// Identity operation.
pub type Op = cob::Op<Action>;

/// Identifier for an identity revision.
pub type RevisionId = EntryId;

pub type IdentityStream<'a> = cob::stream::Stream<'a, Action>;

impl<'a> IdentityStream<'a> {
    pub fn init(identity: ObjectId, store: &'a storage::git::Repository) -> Self {
        let history = cob::stream::CobRange::new(&TYPENAME, &identity);
        Self::new(&store.backend, history, TYPENAME.clone())
    }
}

/// Proposal operation.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Action {
    #[serde(rename = "revision")]
    Revision {
        /// Short summary of changes.
        title: cob::Title,
        /// Longer comment on proposed changes.
        #[serde(default, skip_serializing_if = "String::is_empty")]
        description: String,
        /// Blob identifier of the document included in this action as an embed.
        /// Hence, we do not include it as a parent of this action in [`CobAction`].
        blob: Oid,
        /// Parent revision that this revision replaces.
        parent: Option<RevisionId>,
        /// Signature over the revision blob.
        signature: Signature,
    },
    RevisionEdit {
        /// The revision to edit.
        revision: RevisionId,
        /// Short summary of changes.
        title: cob::Title,
        /// Longer comment on proposed changes.
        #[serde(default, skip_serializing_if = "String::is_empty")]
        description: String,
    },
    #[serde(rename = "revision.accept")]
    RevisionAccept {
        revision: RevisionId,
        /// Signature over the blob.
        signature: Signature,
    },
    #[serde(rename = "revision.reject")]
    RevisionReject { revision: RevisionId },
    #[serde(rename = "revision.redact")]
    RevisionRedact { revision: RevisionId },
}

impl CobAction for Action {
    fn produces_identifier(&self) -> bool {
        matches!(self, Self::Revision { .. })
    }
}

/// Error applying an operation onto a state.
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ApplyError {
    /// Causal dependency missing.
    ///
    /// This error indicates that the operations are not being applied
    /// in causal order, which is a requirement for this CRDT.
    ///
    /// For example, this can occur if an operation references another operation
    /// that hasn't happened yet.
    #[error("causal dependency {0:?} missing")]
    Missing(EntryId),
    /// General error initializing an identity.
    #[error("initialization failed: {0}")]
    Init(&'static str),
    /// Invalid signature over document blob.
    #[error("invalid signature from {0} for blob {1}")]
    InvalidSignature(PublicKey, Oid),
    /// Unauthorized action.
    #[error("not authorized to perform this action")]
    NotAuthorized,
    #[error("parent id is missing from revision")]
    MissingParent,
    #[error("verdict for this revision has already been applied")]
    DuplicateVerdict,
    #[error("revision is in an unexpected state")]
    UnexpectedState,
    #[error("revision has been redacted")]
    Redacted,
    #[error("document does not contain any changes to current identity")]
    DocUnchanged,
    #[error("git: {0}")]
    Git(#[from] git::raw::Error),
    #[error("identity document error: {0}")]
    Doc(#[from] DocError),
    #[error("{author} is not a delegate, and only delegates are allowed to {action}")]
    NonDelegateUnauthorized { author: Did, action: String },
}

impl ApplyError {
    fn non_delegate_unauthorized(author: Did, action: &Action) -> Self {
        let action = match action {
            Action::Revision { .. } => "create a revision",
            Action::RevisionEdit { .. } => "edit a revision",
            Action::RevisionAccept { .. } => "accept a revision",
            Action::RevisionReject { .. } => "reject a revision",
            Action::RevisionRedact { .. } => "redact a revision",
        };
        Self::NonDelegateUnauthorized {
            author,
            action: action.to_string(),
        }
    }
}

/// Error updating or creating proposals.
#[derive(Error, Debug)]
pub enum Error {
    #[error("apply failed: {0}")]
    Apply(#[from] ApplyError),
    #[error("store: {0}")]
    Store(#[from] store::Error),
    #[error("op decoding failed: {0}")]
    Op(#[from] op::OpEncodingError),
    #[error(transparent)]
    Doc(#[from] DocError),
    #[error("revision {0} was not found")]
    NotFound(RevisionId),
}

/// An evolving identity document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Identity {
    /// The canonical identifier for this identity.
    /// This is the object id of the initial document blob.
    pub id: RepoId,
    /// The current revision of the document.
    /// Equal to the head of the identity branch.
    pub current: RevisionId,
    /// The initial revision of the document.
    pub root: RevisionId,
    /// The latest revision that each delegate has accepted.
    /// Delegates can only accept one revision at a time.
    pub heads: BTreeMap<Did, RevisionId>,

    /// Revisions.
    revisions: BTreeMap<RevisionId, Option<Revision>>,
    /// Timeline of events.
    timeline: Vec<EntryId>,
}

impl cob::store::CobWithType for Identity {
    fn type_name() -> &'static TypeName {
        &TYPENAME
    }
}

impl std::ops::Deref for Identity {
    type Target = Revision;

    fn deref(&self) -> &Self::Target {
        self.current()
    }
}

impl Identity {
    pub fn new(revision: Revision) -> Self {
        let root = revision.id;

        Self {
            id: revision.blob.into(),
            root,
            current: root,
            heads: revision
                .delegates()
                .iter()
                .copied()
                .map(|did| (did, root))
                .collect(),
            revisions: BTreeMap::from_iter([(root, Some(revision))]),
            timeline: vec![root],
        }
    }

    pub fn initialize<'a, 'b, Repo, Signer>(
        doc: &Doc,
        store: &'a Repo,
        signer: &'b Signer,
    ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, cob::store::Error>
    where
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
        Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
        Signer: crypto::signature::Signer<crypto::Signature>,
        Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
        Signer: crypto::signature::Verifier<crypto::Signature>,
    {
        let mut store = cob::store::Store::open(store, WriteAs::new(signer))?;

        #[allow(clippy::unwrap_used)]
        let title = cob::Title::new("Initial revision").unwrap();

        #[allow(deprecated)]
        let (actions, embeds) = {
            let repo = store.repo();
            let signer = store.signer();
            Transaction::new_revision(title, "", doc, None, repo, signer)?.into_inner()
        };

        let actions = NonEmpty::from_vec(actions)
            .expect("Transaction::initial: transaction must contain at least one action");

        let (id, identity) = store.create("Initialize identity", actions, embeds)?;

        Ok(IdentityMut {
            id,
            identity,
            store,
        })
    }

    pub fn get<Repo>(object: &ObjectId, repo: &Repo) -> Result<Identity, store::Error>
    where
        Repo: ReadRepository + cob::Store,
    {
        use cob::store::CobWithType;

        cob::get::<Self, _>(repo, Self::type_name(), object)
            .map(|r| r.map(|cob| cob.object))?
            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
    }

    /// Get a proposal mutably.
    pub fn get_mut<'a, 'b, Repo, Signer>(
        id: &ObjectId,
        repo: &'a Repo,
        signer: &'b Signer,
    ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, store::Error>
    where
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
        Signer: crypto::signature::Signer<crypto::Signature>,
    {
        let obj = Self::get(id, repo)?;
        let store = cob::store::Store::open(repo, WriteAs::new(signer))?;

        Ok(IdentityMut {
            id: *id,
            identity: obj,
            store,
        })
    }

    pub fn load<R: ReadRepository + cob::Store>(repo: &R) -> Result<Identity, RepositoryError> {
        let oid = repo.identity_root()?;
        let oid = ObjectId::from(oid);

        Self::get(&oid, repo).map_err(RepositoryError::from)
    }

    pub fn load_mut<'a, 'b, Repo, Signer>(
        repo: &'a Repo,
        signer: &'b Signer,
    ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, RepositoryError>
    where
        Repo: WriteRepository + cob::Store<Namespace = NodeId>,
        Signer: crypto::signature::Signer<crypto::Signature>,
    {
        let oid = repo.identity_root()?;
        let oid = ObjectId::from(oid);

        Self::get_mut(&oid, repo, signer).map_err(RepositoryError::from)
    }
}

impl Identity {
    /// The repository identifier.
    pub fn id(&self) -> RepoId {
        self.id
    }

    /// The current document.
    pub fn doc(&self) -> &Doc {
        &self.current().doc
    }

    /// The current revision.
    pub fn current(&self) -> &Revision {
        self.revision(&self.current)
            .expect("Identity::current: the current revision must always exist")
    }

    /// The initial revision of this identity.
    pub fn root(&self) -> &Revision {
        self.revision(&self.root)
            .expect("Identity::root: the root revision must always exist")
    }

    /// The head of the identity branch. This points to a commit that
    /// contains the current document blob.
    pub fn head(&self) -> Oid {
        self.current
    }

    /// A specific [`Revision`], that may be redacted.
    pub fn revision(&self, revision: &RevisionId) -> Option<&Revision> {
        self.revisions.get(revision).and_then(|r| r.as_ref())
    }

    /// All the [`Revision`]s that have not been redacted.
    pub fn revisions(&self) -> impl DoubleEndedIterator<Item = &Revision> {
        self.timeline
            .iter()
            .filter_map(|id| self.revisions.get(id).and_then(|o| o.as_ref()))
    }

    pub fn latest_by(&self, who: &Did) -> Option<&Revision> {
        self.revisions().rev().find(|r| r.author.id() == who)
    }
}

impl store::Cob for Identity {
    type Action = Action;
    type Error = ApplyError;

    fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
        let mut actions = op.actions.into_iter();
        let Some(Action::Revision {
            title,
            description,
            blob,
            signature,
            parent,
        }) = actions.next()
        else {
            return Err(ApplyError::Init(
                "the first action must be of type `revision`",
            ));
        };
        if parent.is_some() {
            return Err(ApplyError::Init(
                "the initial revision must not have a parent",
            ));
        }
        if actions.next().is_some() {
            return Err(ApplyError::Init(
                "the first operation must contain only one action",
            ));
        }
        let root = Doc::load_at(op.id, repo)?;
        if root.blob != blob {
            return Err(ApplyError::Init("invalid object id specified in revision"));
        }
        if root.blob != *repo.id() {
            return Err(ApplyError::Init(
                "repository root does not match identifier",
            ));
        }
        assert_eq!(root.commit, op.id);

        let founder = root.delegates().first();
        if founder.as_key() != &op.author {
            return Err(ApplyError::Init("delegate does not match committer"));
        }
        // Verify signature against root document. Since there is no previous document,
        // we verify it against itself.
        if root
            .verify_signature(founder, &signature, root.blob)
            .is_err()
        {
            return Err(ApplyError::InvalidSignature(**founder, root.blob));
        }
        let revision = Revision::new(
            root.commit,
            title,
            description,
            op.author.into(),
            root.blob,
            root.doc,
            State::Accepted,
            signature,
            parent,
            op.timestamp,
        );
        Ok(Identity::new(revision))
    }

    fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
        &mut self,
        op: Op,
        concurrent: I,
        repo: &R,
    ) -> Result<(), ApplyError> {
        let id = op.id;
        let concurrent = concurrent.into_iter().collect::<Vec<_>>();

        for action in op.actions {
            match self.action(action, id, op.author, op.timestamp, &concurrent, repo) {
                Ok(()) => {}
                // This particular error is returned when there is a mismatch between the expected
                // and the actual state of a revision, which can happen concurrently. Therefore
                // if there are other concurrent ops, it is not fatal and we simply ignore it.
                Err(ApplyError::UnexpectedState) => {
                    if concurrent.is_empty() {
                        return Err(ApplyError::UnexpectedState);
                    }
                }
                // It's not a user error if the revision happens to be redacted by
                // the time this action is processed.
                Err(ApplyError::Redacted) => {}
                Err(other) => return Err(other),
            }
            debug_assert!(!self.timeline.contains(&id));
            self.timeline.push(id);
        }
        Ok(())
    }
}

impl Identity {
    /// Apply a single action to the identity document.
    ///
    /// This function ensures a few things:
    /// * Only delegates can interact with the state.
    /// * There is only ever one accepted revision; this is the "current" revision.
    /// * There can be zero or more active revisions, up to the number of delegates.
    /// * An active revision is one that can be "voted" on.
    /// * An active revision always has the current revision as parent.
    /// * Only the active revision can be accepted, rejected or edited.
    fn action<R: ReadRepository>(
        &mut self,
        action: Action,
        entry: EntryId,
        author: ActorId,
        timestamp: Timestamp,
        _concurrent: &[&cob::Entry],
        repo: &R,
    ) -> Result<(), ApplyError> {
        let current = self.current().clone();

        let did = author.into();
        if !current.is_delegate(&did) {
            return Err(ApplyError::non_delegate_unauthorized(did, &action));
        }
        match action {
            Action::RevisionAccept {
                revision,
                signature,
            } => {
                let id = revision;
                let Some(revision) = lookup::revision_mut(&mut self.revisions, &id)? else {
                    return Err(ApplyError::Redacted);
                };
                if !revision.is_active() {
                    // You can't vote on an inactive revision.
                    return Err(ApplyError::UnexpectedState);
                }
                assert_eq!(revision.parent, Some(current.id));

                self.heads.insert(author.into(), id);
                revision.accept(author, signature, &current)?;

                self.adopt(id);
            }
            Action::RevisionReject { revision } => {
                let Some(revision) = lookup::revision_mut(&mut self.revisions, &revision)? else {
                    return Err(ApplyError::Redacted);
                };
                if !revision.is_active() {
                    // You can't vote on an inactive revision.
                    return Err(ApplyError::UnexpectedState);
                }
                assert_eq!(revision.parent, Some(current.id));

                revision.reject(author)?;
            }
            Action::RevisionEdit {
                title,
                description,
                revision,
            } => {
                if revision == self.current {
                    return Err(ApplyError::NotAuthorized);
                }
                let Some(revision) = lookup::revision_mut(&mut self.revisions, &revision)? else {
                    return Err(ApplyError::Redacted);
                };
                if !revision.is_active() {
                    // You can't edit an inactive revision.
                    return Err(ApplyError::UnexpectedState);
                }
                if revision.author.public_key() != &author {
                    // Can't edit someone else's revision.
                    // Since the author never changes, we can safely mark this as invalid.
                    return Err(ApplyError::NotAuthorized);
                }
                assert_eq!(revision.parent, Some(current.id));

                revision.title = title;
                revision.description = description;
            }
            Action::RevisionRedact { revision } => {
                if revision == self.current {
                    // Can't redact the current revision.
                    return Err(ApplyError::UnexpectedState);
                }
                if let Some(revision) = self.revisions.get_mut(&revision) {
                    if let Some(r) = revision {
                        if r.is_accepted() {
                            // You can't redact an accepted revision.
                            return Err(ApplyError::UnexpectedState);
                        }
                        if r.author.public_key() != &author {
                            // Can't redact someone else's revision.
                            // Since the author never changes, we can safely mark this as invalid.
                            return Err(ApplyError::NotAuthorized);
                        }
                        *revision = None;
                    }
                } else {
                    return Err(ApplyError::Missing(revision));
                }
            }
            Action::Revision {
                title,
                description,
                blob,
                signature,
                parent,
            } => {
                debug_assert!(!self.revisions.contains_key(&entry));

                let doc = repo.blob(blob)?;
                let doc = Doc::from_blob(&doc)?;
                // All revisions but the first one must have a parent.
                let Some(parent) = parent else {
                    return Err(ApplyError::MissingParent);
                };
                let Some(parent) = lookup::revision(&self.revisions, &parent)? else {
                    return Err(ApplyError::Redacted);
                };
                // If the parent of this revision is no longer the current document, this
                // revision can be marked as outdated.
                let state = if parent.id == current.id {
                    // If the revision is not outdated, we expect it to make a change to the
                    // current version.
                    if doc == parent.doc {
                        return Err(ApplyError::DocUnchanged);
                    }
                    State::Active
                } else {
                    State::Stale
                };

                // Verify signature over new blob, using trusted delegates.
                if parent.verify_signature(&author, &signature, blob).is_err() {
                    return Err(ApplyError::InvalidSignature(author, blob));
                }
                let revision = Revision::new(
                    entry,
                    title,
                    description,
                    author.into(),
                    blob,
                    doc,
                    state,
                    signature,
                    Some(parent.id),
                    timestamp,
                );
                let id = revision.id;

                self.heads.insert(author.into(), id);
                self.revisions.insert(id, Some(revision));

                if state == State::Active {
                    self.adopt(id);
                }
            }
        }
        Ok(())
    }

    /// Try to adopt a revision as the current one.
    fn adopt(&mut self, id: RevisionId) {
        if self.current == id {
            return;
        }
        let votes = self
            .heads
            .values()
            .filter(|revision| **revision == id)
            .count();
        if self.is_majority(votes) {
            self.current = id;
            self.current_mut().state = State::Accepted;

            // Void all other active revisions.
            for r in self
                .revisions
                .iter_mut()
                .filter_map(|(_, r)| r.as_mut())
                .filter(|r| r.state == State::Active)
            {
                r.state = State::Stale;
            }
        }
    }

    /// A specific [`Revision`], mutably.
    fn revision_mut(&mut self, revision: &RevisionId) -> Option<&mut Revision> {
        self.revisions.get_mut(revision).and_then(|r| r.as_mut())
    }

    /// The current revision, mutably.
    fn current_mut(&mut self) -> &mut Revision {
        let current = self.current;
        self.revision_mut(&current)
            .expect("Identity::current_mut: the current revision must always exist")
    }
}

impl<R: ReadRepository> cob::Evaluate<R> for Identity {
    type Error = Error;

    fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
        let op = Op::try_from(entry)?;
        let object = Identity::from_root(op, repo)?;

        Ok(object)
    }

    fn apply<'a, I: Iterator<Item = (&'a EntryId, &'a cob::Entry)>>(
        &mut self,
        entry: &cob::Entry,
        concurrent: I,
        repo: &R,
    ) -> Result<(), Self::Error> {
        let op = Op::try_from(entry)?;

        self.op(op, concurrent.map(|(_, e)| e), repo)
            .map_err(Error::Apply)
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum Verdict {
    /// An accepting verdict must supply the [`Signature`] over the
    /// new proposed [`Doc`].
    Accept(Signature),
    /// Rejecting the proposed [`Doc`].
    Reject,
}

/// State of a revision.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum State {
    /// The revision is actively being voted on. From here, it can go into any of the
    /// other states.
    Active,
    /// The revision has been accepted by a majority of delegates. Once accepted,
    /// a revision doesn't change state.
    Accepted,
    /// The revision was rejected by a majority of delegates. Once rejected,
    /// a revision doesn't change state.
    Rejected,
    /// The revision was active, but has been replaced by another revision,
    /// and is now outdated. Once stale, a revision doesn't change state.
    Stale,
}

impl std::fmt::Display for State {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Active => write!(f, "active"),
            Self::Accepted => write!(f, "accepted"),
            Self::Rejected => write!(f, "rejected"),
            Self::Stale => write!(f, "stale"),
        }
    }
}

/// A new [`Doc`] for an [`Identity`]. The revision can be
/// reviewed by gathering [`Signature`]s for accepting the changes, or
/// rejecting them.
///
/// Once a revision has reached the quorum threshold of the previous
/// [`Identity`] it is then adopted as the current identity.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Revision {
    /// The id of this revision. Points to a commit.
    pub id: RevisionId,
    /// Identity document blob at this revision.
    pub blob: Oid,
    /// Title of the proposal.
    pub title: cob::Title,
    /// State of the revision.
    pub state: State,
    /// Description of the proposal.
    pub description: String,
    /// Author of this proposed revision.
    pub author: Author,
    /// New [`Doc`] that will replace `previous`' document.
    pub doc: Doc,
    /// Physical timestamp of this proposal revision.
    pub timestamp: Timestamp,
    /// Parent revision.
    pub parent: Option<RevisionId>,

    /// Signatures and rejections given by the delegates.
    verdicts: BTreeMap<PublicKey, Verdict>,
}

impl std::ops::Deref for Revision {
    type Target = Doc;

    fn deref(&self) -> &Self::Target {
        &self.doc
    }
}

impl Revision {
    pub fn signatures(&self) -> impl Iterator<Item = (&PublicKey, Signature)> {
        self.verdicts().filter_map(|(key, verdict)| match verdict {
            Verdict::Accept(sig) => Some((key, *sig)),
            Verdict::Reject => None,
        })
    }

    pub fn is_accepted(&self) -> bool {
        matches!(self.state, State::Accepted)
    }

    pub fn is_active(&self) -> bool {
        matches!(self.state, State::Active)
    }

    pub fn verdicts(&self) -> impl Iterator<Item = (&PublicKey, &Verdict)> {
        self.verdicts.iter()
    }

    pub fn accepted(&self) -> impl Iterator<Item = Did> + '_ {
        self.signatures().map(|(key, _)| key.into())
    }

    pub fn rejected(&self) -> impl Iterator<Item = Did> + '_ {
        self.verdicts().filter_map(|(key, v)| match v {
            Verdict::Accept(_) => None,
            Verdict::Reject => Some(key.into()),
        })
    }

    pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
        &self,
        signer: &G,
    ) -> Result<Signature, DocError> {
        self.doc.signature_of(signer)
    }
}

// Private functions that may not do all the verification. Use with caution.
impl Revision {
    fn new(
        id: RevisionId,
        title: cob::Title,
        description: String,
        author: Author,
        blob: Oid,
        doc: Doc,
        state: State,
        signature: Signature,
        parent: Option<RevisionId>,
        timestamp: Timestamp,
    ) -> Self {
        let verdicts = BTreeMap::from_iter([(*author.public_key(), Verdict::Accept(signature))]);

        Self {
            id,
            title,
            description,
            author,
            blob,
            doc,
            state,
            verdicts,
            parent,
            timestamp,
        }
    }

    fn accept(
        &mut self,
        author: PublicKey,
        signature: Signature,
        current: &Revision,
    ) -> Result<(), ApplyError> {
        // Check that this is a valid signature over the new document blob id.
        if current
            .verify_signature(&author, &signature, self.blob)
            .is_err()
        {
            return Err(ApplyError::InvalidSignature(author, self.blob));
        }
        if self
            .verdicts
            .insert(author, Verdict::Accept(signature))
            .is_some()
        {
            return Err(ApplyError::DuplicateVerdict);
        }
        Ok(())
    }

    fn reject(&mut self, key: PublicKey) -> Result<(), ApplyError> {
        if self.verdicts.insert(key, Verdict::Reject).is_some() {
            return Err(ApplyError::DuplicateVerdict);
        }
        // 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() {
            self.state = State::Rejected;
        }
        Ok(())
    }
}

impl<R: ReadRepository> store::Transaction<Identity, R> {
    pub fn accept(
        &mut self,
        revision: RevisionId,
        signature: Signature,
    ) -> Result<(), store::Error> {
        self.push(Action::RevisionAccept {
            revision,
            signature,
        })
    }

    pub fn reject(&mut self, revision: RevisionId) -> Result<(), store::Error> {
        self.push(Action::RevisionReject { revision })
    }

    pub fn edit(
        &mut self,
        revision: RevisionId,
        title: cob::Title,
        description: impl ToString,
    ) -> Result<(), store::Error> {
        self.push(Action::RevisionEdit {
            revision,
            title,
            description: description.to_string(),
        })
    }

    pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
        self.push(Action::RevisionRedact { revision })
    }
}

impl<R: WriteRepository> store::Transaction<Identity, R> {
    pub fn new_revision<G: crypto::signature::Signer<crypto::Signature>>(
        title: cob::Title,
        description: impl ToString,
        doc: &Doc,
        parent: Option<RevisionId>,
        repo: &R,
        signer: &G,
    ) -> Result<Self, store::Error> {
        let mut tx = Transaction::default();

        let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
        // Store document blob in repository.
        let embed =
            Embed::<Uri>::store("radicle.json", &bytes, repo.raw()).map_err(store::Error::Git)?;

        debug_assert_eq!(embed.content, Uri::from(blob)); // Make sure we pre-computed the correct OID for the blob.

        // Identity document.
        tx.embed([embed])?;

        // Revision metadata.
        tx.push(Action::Revision {
            title,
            description: description.to_string(),
            blob,
            parent,
            signature,
        })?;

        Ok(tx)
    }
}

pub struct IdentityMut<'a, 'b, Repo, Signer> {
    pub id: ObjectId,

    identity: Identity,
    store: store::Store<'a, Identity, Repo, WriteAs<'b, Signer>>,
}

impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IdentityMut")
            .field("id", &self.id)
            .field("identity", &self.identity)
            .finish()
    }
}

impl<Repo, Signer> IdentityMut<'_, '_, Repo, Signer>
where
    Repo: WriteRepository + cob::Store<Namespace = NodeId>,
    Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>,
    Signer: crypto::signature::Signer<crypto::Signature>,
    Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>,
    Signer: crypto::signature::Verifier<crypto::Signature>,
{
    /// Reload the identity data from storage.
    #[cfg(test)]
    pub fn reload(&mut self) -> Result<(), store::Error> {
        self.identity = self
            .store
            .get(&self.id)?
            .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;

        Ok(())
    }

    pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error>
    where
        F: FnOnce(&mut Transaction<Identity, Repo>, &Repo) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
        operations(&mut tx, self.store.as_ref())?;

        let (doc, commit) = tx.commit(message, self.id, &mut self.store)?;
        self.identity = doc;

        Ok(commit)
    }

    /// Update the identity by proposing a new revision.
    /// If the signer is the only delegate, the revision is accepted automatically.
    pub fn update(
        &mut self,
        title: cob::Title,
        description: impl ToString,
        doc: &Doc,
    ) -> Result<RevisionId, Error> {
        let parent = Some(self.current);

        #[allow(deprecated)]
        let tx = {
            let signer = self.store.signer();
            let repo = self.store.repo();
            Transaction::new_revision(title, description, doc, parent, repo, signer)?
        };
        let (doc, commit) = tx.commit("Propose revision", self.id, &mut self.store)?;
        self.identity = doc;

        Ok(commit)
    }

    /// Accept an active revision.
    pub fn accept(&mut self, revision: &RevisionId) -> Result<EntryId, Error> {
        let id = *revision;
        let revision = self.revision(revision).ok_or(Error::NotFound(id))?;

        #[allow(deprecated)]
        let signature = revision.sign(self.store.signer())?;

        self.transaction("Accept revision", |tx, _| tx.accept(id, signature))
    }

    /// Reject an active revision.
    pub fn reject(&mut self, revision: RevisionId) -> Result<EntryId, Error> {
        self.transaction("Reject revision", |tx, _| tx.reject(revision))
    }

    /// Redact a revision.
    pub fn redact(&mut self, revision: RevisionId) -> Result<EntryId, Error> {
        self.transaction("Redact revision", |tx, _| tx.redact(revision))
    }

    /// Edit an active revision's title or description.
    pub fn edit(
        &mut self,
        revision: RevisionId,
        title: cob::Title,
        description: String,
    ) -> Result<EntryId, Error> {
        self.transaction("Edit revision", |tx, _| {
            tx.edit(revision, title, description)
        })
    }
}

impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> {
    type Target = Identity;

    fn deref(&self) -> &Self::Target {
        &self.identity
    }
}

mod lookup {
    use super::*;

    pub fn revision_mut<'a>(
        revisions: &'a mut BTreeMap<RevisionId, Option<Revision>>,
        revision: &RevisionId,
    ) -> Result<Option<&'a mut Revision>, ApplyError> {
        match revisions.get_mut(revision) {
            Some(Some(revision)) => Ok(Some(revision)),
            // Redacted.
            Some(None) => Ok(None),
            // Missing. Causal error.
            None => Err(ApplyError::Missing(*revision)),
        }
    }

    pub fn revision<'a>(
        revisions: &'a BTreeMap<RevisionId, Option<Revision>>,
        revision: &RevisionId,
    ) -> Result<Option<&'a Revision>, ApplyError> {
        match revisions.get(revision) {
            Some(Some(revision)) => Ok(Some(revision)),
            // Redacted.
            Some(None) => Ok(None),
            // Missing. Causal error.
            None => Err(ApplyError::Missing(*revision)),
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    use qcheck_macros::quickcheck;

    use crate::cob::{self, Title};
    use crate::crypto::PublicKey;
    use crate::identity::Visibility;
    use crate::identity::did::Did;
    use crate::identity::doc::PayloadId;
    use crate::node::device::Device;
    use crate::rad;
    use crate::storage::ReadStorage;
    use crate::storage::git::Storage;
    use crate::test::fixtures;
    use crate::test::setup::{Network, NodeWithRepo};

    use super::*;

    #[quickcheck]
    fn prop_json_eq_str(pk: PublicKey, proj: RepoId, did: Did) {
        let json = serde_json::to_string(&pk).unwrap();
        assert_eq!(format!("\"{pk}\""), json);

        let json = serde_json::to_string(&proj).unwrap();
        assert_eq!(format!("\"{}\"", proj.urn()), json);

        let json = serde_json::to_string(&did).unwrap();
        assert_eq!(format!("\"{did}\""), json);
    }

    #[test]
    fn test_identity_updates() {
        let NodeWithRepo { node, repo } = NodeWithRepo::default();
        let bob = Device::mock();
        let signer = &node.signer;
        let mut identity = Identity::load_mut(&*repo, signer).unwrap();
        let mut doc = identity.doc().clone().edit();
        let title = Title::new("Identity update").unwrap();
        let description = "";
        let r0 = identity.current;

        // The initial state is accepted.
        assert!(identity.current().is_accepted());
        // Using an identical document to the current one fails.
        identity
            .update(title.clone(), description, &doc.clone().verified().unwrap())
            .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;
        assert!(doc.clone().verified().is_err());

        // Let's add another delegate.
        doc.delegate(bob.public_key().into());
        // The update should go through now.
        let r1 = identity
            .update(title.clone(), description, &doc.clone().verified().unwrap())
            .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.clone(), description, &doc.clone().verified().unwrap())
            .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
        );
        // Now let's add a signature on R2 from Bob.
        let mut bob_identity = Identity::load_mut(&*repo, &bob).unwrap();
        bob_identity.accept(&r2).unwrap();

        identity.reload().unwrap();

        // R2 is now the head.
        assert_eq!(identity.current, r2);
        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([])
        );
    }

    #[test]
    fn test_identity_update_rejected() {
        let NodeWithRepo { node, repo } = NodeWithRepo::default();
        let bob = Device::mock();
        let eve = Device::mock();
        let signer = &node.signer;

        let mut identity = Identity::load_mut(&*repo, signer).unwrap();
        let mut doc = identity.doc().clone().edit();
        let description = "";

        // Let's add another delegate.
        doc.delegate(bob.public_key().into());
        let r1 = identity
            .update(
                cob::Title::new("Identity update").unwrap(),
                description,
                &doc.clone().verified().unwrap(),
            )
            .unwrap();
        assert_eq!(identity.current, r1);

        doc.visibility = Visibility::private([]);
        let r2 = identity
            .update(
                cob::Title::new("Make private").unwrap(),
                description,
                &doc.clone().verified().unwrap(),
            )
            .unwrap();

        let mut bob_identity = Identity::load_mut(&*repo, &bob).unwrap();

        // 1/2 rejected means that we can never reach the required 2/2 votes.
        bob_identity.reject(r2).unwrap();
        let r2 = bob_identity.revision(&r2).unwrap();
        assert_eq!(r2.state, State::Rejected);

        // Now let's add another delegate.
        doc.delegate(eve.public_key().into());
        let r3 = identity
            .update(
                cob::Title::new("Add Eve").unwrap(),
                description,
                &doc.clone().verified().unwrap(),
            )
            .unwrap();

        bob_identity.reload().unwrap();
        let _ = bob_identity.accept(&r3).unwrap();

        identity.reload().unwrap();
        assert_eq!(identity.current, r3);

        doc.visibility = Visibility::Public;
        let r3 = identity
            .update(
                cob::Title::new("Make public").unwrap(),
                description,
                &doc.verified().unwrap(),
            )
            .unwrap();

        // 1/3 rejected means that we can still reach the 2/3 required votes.
        bob_identity.reject(r3).unwrap();
        let r3 = identity.revision(&r3).unwrap().clone();
        assert_eq!(r3.state, State::Active); // Still active.

        let mut eve_identity = Identity::load_mut(&*repo, &eve).unwrap();

        // 2/3 rejected means that we can no longer reach the 2/3 required votes.
        eve_identity.reject(r3.id).unwrap();
        let r3 = eve_identity.revision(&r3.id).unwrap();
        assert_eq!(r3.state, State::Rejected);
    }

    #[test]
    fn test_identity_updates_concurrent() {
        let network = Network::default();
        let alice = &network.alice;
        let bob = &network.bob;

        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice_doc.delegate(bob.signer.public_key().into());
        let a1 = alice_identity
            .update(
                cob::Title::new("Add Bob").unwrap(),
                "",
                &alice_doc.clone().verified().unwrap(),
            )
            .unwrap();

        bob.repo.fetch(alice);

        let bob_identity = Identity::load(&*bob.repo).unwrap();
        let bob_doc = bob_identity.doc().clone();
        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(
                cob::Title::new("Change visibility").unwrap(),
                "",
                &alice_doc.clone().clone().verified().unwrap(),
            )
            .unwrap();

        let bob_identity_mut = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
        assert_eq!(*bob_identity_mut, bob_identity);
        let mut bob_identity = bob_identity_mut;

        // Bob makes the same change without knowing Alice already did.
        let b1 = bob_identity
            .update(
                cob::Title::new("Make private").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
            )
            .unwrap();

        // Bob gets Alice's data.
        bob.repo.fetch(alice);
        bob_identity.reload().unwrap();
        assert_eq!(bob_identity.current, a1);

        // Alice gets Bob's data.
        // There's not enough votes for either of these proposals to pass.
        alice.repo.fetch(bob);
        alice_identity.reload().unwrap();
        assert_eq!(alice_identity.current, a1);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Active);
        assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Active);

        // Now Bob accepts Alice's proposal. This voids his own.
        bob_identity.accept(&a2).unwrap();
        assert_eq!(bob_identity.current, a2);
        assert_eq!(bob_identity.revision(&a1).unwrap().state, State::Accepted);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
        assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Stale);
    }

    #[test]
    fn test_identity_redact_revision() {
        let network = Network::default();
        let alice = &network.alice;
        let bob = &network.bob;
        let eve = &network.eve;

        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice_doc.delegate(bob.signer.public_key().into());
        let a0 = alice_identity.root;
        let a1 = alice_identity
            .update(
                cob::Title::new("Add Bob").unwrap(),
                "Eh.",
                &alice_doc.clone().clone().verified().unwrap(),
            )
            .unwrap();

        alice_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
        let a2 = alice_identity
            .update(
                cob::Title::new("Change visibility").unwrap(),
                "Eh.",
                &alice_doc.verified().unwrap(),
            )
            .unwrap();

        bob.repo.fetch(alice);
        let a3 = cob::stable::with_advanced_timestamp(|| alice_identity.redact(a2).unwrap());
        assert!(alice_identity.revision(&a1).is_some());
        assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);

        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
        let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap());

        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
        bob.repo.fetch(alice);
        bob_identity.reload().unwrap();

        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, a3, b1]);
        assert_eq!(bob_identity.revision(&a2), None);
        assert_eq!(bob_identity.current, a1);
    }

    #[test]
    fn test_identity_remove_delegate_concurrent() {
        let network = Network::default();
        let alice = &network.alice;
        let bob = &network.bob;
        let eve = &network.eve;

        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        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(
                cob::Title::new("Add Bob and Eve").unwrap(),
                "Eh#!",
                &alice_doc.clone().verified().unwrap(),
            )
            .unwrap();

        alice_doc.rescind(&eve.signer.public_key().into()).unwrap();
        let a2 = alice_identity
            .update(
                cob::Title::new("Remove Eve").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
            )
            .unwrap();

        bob.repo.fetch(eve);
        bob.repo.fetch(alice);
        eve.repo.fetch(bob);

        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
        let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap());
        assert_eq!(bob_identity.current, a2);

        let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap();
        let mut eve_doc = eve_identity.doc().clone().edit();
        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
        let e1 = cob::stable::with_advanced_timestamp(|| {
            eve_identity
                .update(
                    cob::Title::new("Change visibility").unwrap(),
                    "",
                    &eve_doc.verified().unwrap(),
                )
                .unwrap()
        });
        // Eve's revision is active.
        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, e1]);
        assert!(eve_identity.revision(&e1).unwrap().is_active());

        //  b1      (Accept "Remove Eve") 2/2
        //  |  e1   (Change visibility)
        //  | /
        //  a2      (Propose "Remove Eve") 1/2
        //  |
        //  a1      (Add Bob and Eve)
        //  |
        //  a0

        eve.repo.fetch(bob);
        eve_identity.reload().unwrap();
        // Now that Eve reloaded, since Bob's vote to remove Eve went through first (b1 < e1),
        // her revision is no longer valid.
        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1]);
        assert_eq!(eve_identity.revision(&e1), None);
        assert!(!eve_identity.is_delegate(&eve.signer.public_key().into()));
    }

    #[test]
    fn test_identity_reject_concurrent() {
        let network = Network::default();
        let alice = &network.alice;
        let bob = &network.bob;
        let eve = &network.eve;

        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        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(
                cob::Title::new("Add Bob and Eve").unwrap(),
                "Eh!#",
                &alice_doc.clone().verified().unwrap(),
            )
            .unwrap();

        alice_doc.visibility = Visibility::private([]);
        let a2 = alice_identity
            .update(
                cob::Title::new("Change visibility").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
            )
            .unwrap();

        bob.repo.fetch(eve);
        bob.repo.fetch(alice);
        eve.repo.fetch(bob);

        // Bob accepts alice's revision.
        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
        let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap());

        // Eve rejects the revision, not knowing.
        let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap();
        let e1 = cob::stable::with_advanced_timestamp(|| eve_identity.reject(a2).unwrap());
        assert!(eve_identity.revision(&a2).unwrap().is_active());

        // Then she submits a new revision.
        let mut eve_doc = eve_identity.doc().clone().edit();
        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
        let e2 = eve_identity
            .update(
                cob::Title::new("Change visibility").unwrap(),
                "",
                &eve_doc.verified().unwrap(),
            )
            .unwrap();
        assert!(eve_identity.revision(&e2).unwrap().is_active());

        //     e2   (Propose "Change visibility") 1/2
        //     |
        //     e1   (Reject "Change visibility")  1/2
        //  b1 |    (Accept "Change visibility")  2/2
        //  | /
        //  a2      (Propose "Change visibility") 1/2
        //  |
        //  a1      (Add Bob and Eve)
        //  |
        //  a0

        // Though the rules are that you cannot reject an already accepted revision,
        // since this update was done concurrently there was no way of knowing. Therefore,
        // an error shouldn't be returned. We simply ignore the rejection.

        eve.repo.fetch(bob);
        eve_identity.reload().unwrap();
        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1, e2]);

        // Her revision is there, although stale, since another revision was accepted since.
        // However, it wasn't pruned, even though rejecting an accepted revision is an error.
        let e2 = eve_identity.revision(&e2).unwrap();
        assert_eq!(e2.state, State::Stale);
        assert!(eve_identity.revision(&a2).unwrap().is_accepted());
    }

    #[test]
    fn test_identity_updates_concurrent_outdated() {
        let network = Network::default();
        let alice = &network.alice;
        let bob = &network.bob;
        let eve = &network.eve;

        let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
        let mut alice_doc = alice_identity.doc().clone().edit();

        alice.repo.fetch(bob);
        alice.repo.fetch(eve);
        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(
                cob::Title::new("Add Bob and Eve").unwrap(),
                "",
                &alice_doc.verified().unwrap(),
            )
            .unwrap();

        bob.repo.fetch(alice);
        eve.repo.fetch(alice);

        let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap();
        let mut bob_doc = bob_identity.doc().clone().edit();
        assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));

        //  a2 e1
        //  | /
        //  b1
        //  |
        //  a1
        //  |
        //  a0

        // Bob and Alice change the document visibility. Eve is not aware.
        bob_doc.visibility = Visibility::private([]);
        let b1 = bob_identity
            .update(
                cob::Title::new("Change visibility #1").unwrap(),
                "",
                &bob_doc.verified().unwrap(),
            )
            .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, &eve.signer).unwrap();
        let mut eve_doc = eve_identity.doc().clone().edit();
        eve_doc.visibility = Visibility::private([]);
        let e1 = eve_identity
            .update(
                cob::Title::new("Change visibility #2").unwrap(),
                "Woops",
                &eve_doc.verified().unwrap(),
            )
            .unwrap();
        assert_eq!(eve_identity.revisions().count(), 4);
        assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);

        alice_identity.reload().unwrap();
        let a2 = cob::stable::with_advanced_timestamp(|| alice_identity.accept(&b1).unwrap());

        eve.repo.fetch(alice);

        eve_identity.reload().unwrap();

        assert_eq!(eve_identity.timeline, vec![a0, a1, b1, e1, a2]);
        assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Stale);
    }

    #[test]
    fn test_valid_identity() {
        let tempdir = tempfile::tempdir().unwrap();
        let mut rng = fastrand::Rng::new();

        let alice = Device::mock_rng(&mut rng);
        let bob = Device::mock_rng(&mut rng);
        let eve = Device::mock_rng(&mut rng);

        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
        let (id, _, _, _) =
            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();

        // Bob and Eve fork the project from Alice.
        rad::fork_remote(id, alice.public_key(), &bob, &storage).unwrap();
        rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap();

        let repo = storage.repository(id).unwrap();
        let mut identity = Identity::load_mut(&repo, &alice).unwrap();
        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(
                cob::Title::new("Update description").unwrap(),
                "",
                &doc.clone().verified().unwrap(),
            )
            .unwrap();

        // Add Bob as a delegate, and sign it.
        doc.delegate(bob.public_key().into());
        doc.threshold = 2;
        identity
            .update(
                cob::Title::new("Add bob").unwrap(),
                "",
                &doc.clone().verified().unwrap(),
            )
            .unwrap();

        // Add Eve as a delegate.
        doc.delegate(eve.public_key().into());

        // Update with both Bob and Alice's signature.
        let revision = identity
            .update(
                cob::Title::new("Add eve").unwrap(),
                "",
                &doc.clone().verified().unwrap(),
            )
            .unwrap();

        let mut bob_identity = Identity::load_mut(&repo, &bob).unwrap();
        bob_identity.accept(&revision).unwrap();

        // Update description again with signatures by Eve and Bob.
        let desc = prj.description().to_owned() + "?";
        let prj = prj.update(None, desc, None).unwrap();
        doc.payload.insert(PayloadId::project(), prj.into());
        let revision = bob_identity
            .update(
                cob::Title::new("Update description again").unwrap(),
                "Bob's repository",
                &doc.verified().unwrap(),
            )
            .unwrap();

        let mut eve_identity = Identity::load_mut(&repo, &eve).unwrap();
        eve_identity.accept(&revision).unwrap();

        let identity: Identity = Identity::load(&repo).unwrap();
        let root = repo.identity_root().unwrap();
        let doc = repo.identity_doc_at(revision).unwrap();

        assert_eq!(identity.signatures().count(), 2);
        assert_eq!(identity.revisions().count(), 5);
        assert_eq!(identity.id(), id);
        assert_eq!(identity.root().id, root);
        assert_eq!(identity.current().blob, doc.blob);
        assert_eq!(identity.current().description.as_str(), "Bob's repository");
        assert_eq!(identity.head(), revision);
        assert_eq!(identity.doc(), &*doc);
        assert_eq!(
            identity.doc().project().unwrap().description(),
            "Acme's repository!?"
        );

        assert_eq!(doc.project().unwrap().description(), "Acme's repository!?");
    }
}