radicle/cob/
identity.rs

1use std::collections::BTreeMap;
2use std::sync::LazyLock;
3use std::{fmt, ops::Deref, str::FromStr};
4
5use crypto::{PublicKey, Signature};
6use radicle_cob::{Embed, ObjectId, TypeName};
7use radicle_git_ext as git_ext;
8use radicle_git_ext::Oid;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::identity::doc::Doc;
13use crate::node::device::Device;
14use crate::node::NodeId;
15use crate::storage;
16use crate::{
17    cob,
18    cob::{
19        op, store,
20        store::{Cob, CobAction, Transaction},
21        ActorId, Timestamp, Uri,
22    },
23    identity::{
24        doc::{DocError, RepoId},
25        Did,
26    },
27    storage::{ReadRepository, RepositoryError, WriteRepository},
28};
29
30use super::{Author, EntryId};
31
32/// Type name of an identity proposal.
33pub static TYPENAME: LazyLock<TypeName> =
34    LazyLock::new(|| FromStr::from_str("xyz.radicle.id").expect("type name is valid"));
35
36/// Identity operation.
37pub type Op = cob::Op<Action>;
38
39/// Identifier for an identity revision.
40pub type RevisionId = EntryId;
41
42pub type IdentityStream<'a> = cob::stream::Stream<'a, Action>;
43
44impl<'a> IdentityStream<'a> {
45    pub fn init(identity: ObjectId, store: &'a storage::git::Repository) -> Self {
46        let history = cob::stream::CobRange::new(&TYPENAME, &identity);
47        Self::new(&store.backend, history, TYPENAME.clone())
48    }
49}
50
51/// Proposal operation.
52#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
53#[serde(tag = "type")]
54pub enum Action {
55    #[serde(rename = "revision")]
56    Revision {
57        /// Short summary of changes.
58        title: cob::Title,
59        /// Longer comment on proposed changes.
60        #[serde(default, skip_serializing_if = "String::is_empty")]
61        description: String,
62        /// Blob identifier of the document included in this action as an embed.
63        /// Hence, we do not include it as a parent of this action in [`CobAction`].
64        blob: Oid,
65        /// Parent revision that this revision replaces.
66        parent: Option<RevisionId>,
67        /// Signature over the revision blob.
68        signature: Signature,
69    },
70    RevisionEdit {
71        /// The revision to edit.
72        revision: RevisionId,
73        /// Short summary of changes.
74        title: cob::Title,
75        /// Longer comment on proposed changes.
76        #[serde(default, skip_serializing_if = "String::is_empty")]
77        description: String,
78    },
79    #[serde(rename = "revision.accept")]
80    RevisionAccept {
81        revision: RevisionId,
82        /// Signature over the blob.
83        signature: Signature,
84    },
85    #[serde(rename = "revision.reject")]
86    RevisionReject { revision: RevisionId },
87    #[serde(rename = "revision.redact")]
88    RevisionRedact { revision: RevisionId },
89}
90
91impl CobAction for Action {
92    fn produces_identifier(&self) -> bool {
93        matches!(self, Self::Revision { .. })
94    }
95}
96
97/// Error applying an operation onto a state.
98#[derive(Error, Debug)]
99pub enum ApplyError {
100    /// Causal dependency missing.
101    ///
102    /// This error indicates that the operations are not being applied
103    /// in causal order, which is a requirement for this CRDT.
104    ///
105    /// For example, this can occur if an operation references another operation
106    /// that hasn't happened yet.
107    #[error("causal dependency {0:?} missing")]
108    Missing(EntryId),
109    /// General error initializing an identity.
110    #[error("initialization failed: {0}")]
111    Init(&'static str),
112    /// Invalid signature over document blob.
113    #[error("invalid signature from {0} for blob {1}")]
114    InvalidSignature(PublicKey, Oid),
115    /// Unauthorized action.
116    #[error("not authorized to perform this action")]
117    NotAuthorized,
118    #[error("parent id is missing from revision")]
119    MissingParent,
120    #[error("verdict for this revision has already been applied")]
121    DuplicateVerdict,
122    #[error("revision is in an unexpected state")]
123    UnexpectedState,
124    #[error("revision has been redacted")]
125    Redacted,
126    #[error("document does not contain any changes to current identity")]
127    DocUnchanged,
128    #[error("git: {0}")]
129    Git(#[from] git2::Error),
130    #[error("git: {0}")]
131    GitExt(#[from] git_ext::Error),
132    #[error("identity document error: {0}")]
133    Doc(#[from] DocError),
134}
135
136/// Error updating or creating proposals.
137#[derive(Error, Debug)]
138pub enum Error {
139    #[error("apply failed: {0}")]
140    Apply(#[from] ApplyError),
141    #[error("store: {0}")]
142    Store(#[from] store::Error),
143    #[error("op decoding failed: {0}")]
144    Op(#[from] op::OpEncodingError),
145    #[error(transparent)]
146    Doc(#[from] DocError),
147    #[error("revision {0} was not found")]
148    NotFound(RevisionId),
149}
150
151/// An evolving identity document.
152#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct Identity {
155    /// The canonical identifier for this identity.
156    /// This is the object id of the initial document blob.
157    pub id: RepoId,
158    /// The current revision of the document.
159    /// Equal to the head of the identity branch.
160    pub current: RevisionId,
161    /// The initial revision of the document.
162    pub root: RevisionId,
163    /// The latest revision that each delegate has accepted.
164    /// Delegates can only accept one revision at a time.
165    pub heads: BTreeMap<Did, RevisionId>,
166
167    /// Revisions.
168    revisions: BTreeMap<RevisionId, Option<Revision>>,
169    /// Timeline of events.
170    timeline: Vec<EntryId>,
171}
172
173impl cob::store::CobWithType for Identity {
174    fn type_name() -> &'static TypeName {
175        &TYPENAME
176    }
177}
178
179impl std::ops::Deref for Identity {
180    type Target = Revision;
181
182    fn deref(&self) -> &Self::Target {
183        self.current()
184    }
185}
186
187impl Identity {
188    pub fn new(revision: Revision) -> Self {
189        let root = revision.id;
190
191        Self {
192            id: revision.blob.into(),
193            root,
194            current: root,
195            heads: revision
196                .delegates()
197                .iter()
198                .copied()
199                .map(|did| (did, root))
200                .collect(),
201            revisions: BTreeMap::from_iter([(root, Some(revision))]),
202            timeline: vec![root],
203        }
204    }
205
206    pub fn initialize<'a, R, G>(
207        doc: &Doc,
208        store: &'a R,
209        signer: &Device<G>,
210    ) -> Result<IdentityMut<'a, R>, cob::store::Error>
211    where
212        G: crypto::signature::Signer<crypto::Signature>,
213        R: WriteRepository + cob::Store<Namespace = NodeId>,
214    {
215        let mut store = cob::store::Store::open(store)?;
216        let (id, identity) = Transaction::<Identity, _>::initial(
217            "Initialize identity",
218            &mut store,
219            signer,
220            |tx, repo| {
221                tx.revision(
222                    // SAFETY: "Initial revision" is a valid title
223                    #[allow(clippy::unwrap_used)]
224                    cob::Title::new("Initial revision").unwrap(),
225                    "",
226                    doc,
227                    None,
228                    repo,
229                    signer,
230                )
231            },
232        )?;
233
234        Ok(IdentityMut {
235            id,
236            identity,
237            store,
238        })
239    }
240
241    pub fn get<R: ReadRepository + cob::Store>(
242        object: &ObjectId,
243        repo: &R,
244    ) -> Result<Identity, store::Error> {
245        use cob::store::CobWithType;
246
247        cob::get::<Self, _>(repo, Self::type_name(), object)
248            .map(|r| r.map(|cob| cob.object))?
249            .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
250    }
251
252    /// Get a proposal mutably.
253    pub fn get_mut<'a, R: WriteRepository + cob::Store<Namespace = NodeId>>(
254        id: &ObjectId,
255        repo: &'a R,
256    ) -> Result<IdentityMut<'a, R>, store::Error> {
257        let obj = Self::get(id, repo)?;
258        let store = cob::store::Store::open(repo)?;
259
260        Ok(IdentityMut {
261            id: *id,
262            identity: obj,
263            store,
264        })
265    }
266
267    pub fn load<R: ReadRepository + cob::Store>(repo: &R) -> Result<Identity, RepositoryError> {
268        let oid = repo.identity_root()?;
269        let oid = ObjectId::from(oid);
270
271        Self::get(&oid, repo).map_err(RepositoryError::from)
272    }
273
274    pub fn load_mut<R: WriteRepository + cob::Store<Namespace = NodeId>>(
275        repo: &R,
276    ) -> Result<IdentityMut<R>, RepositoryError> {
277        let oid = repo.identity_root()?;
278        let oid = ObjectId::from(oid);
279
280        Self::get_mut(&oid, repo).map_err(RepositoryError::from)
281    }
282}
283
284impl Identity {
285    /// The repository identifier.
286    pub fn id(&self) -> RepoId {
287        self.id
288    }
289
290    /// The current document.
291    pub fn doc(&self) -> &Doc {
292        &self.current().doc
293    }
294
295    /// The current revision.
296    pub fn current(&self) -> &Revision {
297        self.revision(&self.current)
298            .expect("Identity::current: the current revision must always exist")
299    }
300
301    /// The initial revision of this identity.
302    pub fn root(&self) -> &Revision {
303        self.revision(&self.root)
304            .expect("Identity::root: the root revision must always exist")
305    }
306
307    /// The head of the identity branch. This points to a commit that
308    /// contains the current document blob.
309    pub fn head(&self) -> Oid {
310        self.current
311    }
312
313    /// A specific [`Revision`], that may be redacted.
314    pub fn revision(&self, revision: &RevisionId) -> Option<&Revision> {
315        self.revisions.get(revision).and_then(|r| r.as_ref())
316    }
317
318    /// All the [`Revision`]s that have not been redacted.
319    pub fn revisions(&self) -> impl DoubleEndedIterator<Item = &Revision> {
320        self.timeline
321            .iter()
322            .filter_map(|id| self.revisions.get(id).and_then(|o| o.as_ref()))
323    }
324
325    pub fn latest_by(&self, who: &Did) -> Option<&Revision> {
326        self.revisions().rev().find(|r| r.author.id() == who)
327    }
328}
329
330impl store::Cob for Identity {
331    type Action = Action;
332    type Error = ApplyError;
333
334    fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
335        let mut actions = op.actions.into_iter();
336        let Some(Action::Revision {
337            title,
338            description,
339            blob,
340            signature,
341            parent,
342        }) = actions.next()
343        else {
344            return Err(ApplyError::Init(
345                "the first action must be of type `revision`",
346            ));
347        };
348        if parent.is_some() {
349            return Err(ApplyError::Init(
350                "the initial revision must not have a parent",
351            ));
352        }
353        if actions.next().is_some() {
354            return Err(ApplyError::Init(
355                "the first operation must contain only one action",
356            ));
357        }
358        let root = Doc::load_at(op.id, repo)?;
359        if root.blob != blob {
360            return Err(ApplyError::Init("invalid object id specified in revision"));
361        }
362        if root.blob != *repo.id() {
363            return Err(ApplyError::Init(
364                "repository root does not match identifier",
365            ));
366        }
367        assert_eq!(root.commit, op.id);
368
369        let founder = root.delegates().first();
370        if founder.as_key() != &op.author {
371            return Err(ApplyError::Init("delegate does not match committer"));
372        }
373        // Verify signature against root document. Since there is no previous document,
374        // we verify it against itself.
375        if root
376            .verify_signature(founder, &signature, root.blob)
377            .is_err()
378        {
379            return Err(ApplyError::InvalidSignature(**founder, root.blob));
380        }
381        let revision = Revision::new(
382            root.commit,
383            title,
384            description,
385            op.author.into(),
386            root.blob,
387            root.doc,
388            State::Accepted,
389            signature,
390            parent,
391            op.timestamp,
392        );
393        Ok(Identity::new(revision))
394    }
395
396    fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
397        &mut self,
398        op: Op,
399        concurrent: I,
400        repo: &R,
401    ) -> Result<(), ApplyError> {
402        let id = op.id;
403        let concurrent = concurrent.into_iter().collect::<Vec<_>>();
404
405        for action in op.actions {
406            match self.action(action, id, op.author, op.timestamp, &concurrent, repo) {
407                Ok(()) => {}
408                // This particular error is returned when there is a mismatch between the expected
409                // and the actual state of a revision, which can happen concurrently. Therefore
410                // if there are other concurrent ops, it is not fatal and we simply ignore it.
411                Err(ApplyError::UnexpectedState) => {
412                    if concurrent.is_empty() {
413                        return Err(ApplyError::UnexpectedState);
414                    }
415                }
416                // It's not a user error if the revision happens to be redacted by
417                // the time this action is processed.
418                Err(ApplyError::Redacted) => {}
419                Err(other) => return Err(other),
420            }
421            debug_assert!(!self.timeline.contains(&id));
422            self.timeline.push(id);
423        }
424        Ok(())
425    }
426}
427
428impl Identity {
429    /// Apply a single action to the identity document.
430    ///
431    /// This function ensures a few things:
432    /// * Only delegates can interact with the state.
433    /// * There is only ever one accepted revision; this is the "current" revision.
434    /// * There can be zero or more active revisions, up to the number of delegates.
435    /// * An active revision is one that can be "voted" on.
436    /// * An active revision always has the current revision as parent.
437    /// * Only the active revision can be accepted, rejected or edited.
438    fn action<R: ReadRepository>(
439        &mut self,
440        action: Action,
441        entry: EntryId,
442        author: ActorId,
443        timestamp: Timestamp,
444        _concurrent: &[&cob::Entry],
445        repo: &R,
446    ) -> Result<(), ApplyError> {
447        let current = self.current().clone();
448
449        if !current.is_delegate(&author.into()) {
450            return Err(ApplyError::UnexpectedState);
451        }
452        match action {
453            Action::RevisionAccept {
454                revision,
455                signature,
456            } => {
457                let id = revision;
458                let Some(revision) = lookup::revision_mut(&mut self.revisions, &id)? else {
459                    return Err(ApplyError::Redacted);
460                };
461                if !revision.is_active() {
462                    // You can't vote on an inactive revision.
463                    return Err(ApplyError::UnexpectedState);
464                }
465                assert_eq!(revision.parent, Some(current.id));
466
467                self.heads.insert(author.into(), id);
468                revision.accept(author, signature, &current)?;
469
470                self.adopt(id);
471            }
472            Action::RevisionReject { revision } => {
473                let Some(revision) = lookup::revision_mut(&mut self.revisions, &revision)? else {
474                    return Err(ApplyError::Redacted);
475                };
476                if !revision.is_active() {
477                    // You can't vote on an inactive revision.
478                    return Err(ApplyError::UnexpectedState);
479                }
480                assert_eq!(revision.parent, Some(current.id));
481
482                revision.reject(author)?;
483            }
484            Action::RevisionEdit {
485                title,
486                description,
487                revision,
488            } => {
489                if revision == self.current {
490                    return Err(ApplyError::NotAuthorized);
491                }
492                let Some(revision) = lookup::revision_mut(&mut self.revisions, &revision)? else {
493                    return Err(ApplyError::Redacted);
494                };
495                if !revision.is_active() {
496                    // You can't edit an inactive revision.
497                    return Err(ApplyError::UnexpectedState);
498                }
499                if revision.author.public_key() != &author {
500                    // Can't edit someone else's revision.
501                    // Since the author never changes, we can safely mark this as invalid.
502                    return Err(ApplyError::NotAuthorized);
503                }
504                assert_eq!(revision.parent, Some(current.id));
505
506                revision.title = title;
507                revision.description = description;
508            }
509            Action::RevisionRedact { revision } => {
510                if revision == self.current {
511                    // Can't redact the current revision.
512                    return Err(ApplyError::UnexpectedState);
513                }
514                if let Some(revision) = self.revisions.get_mut(&revision) {
515                    if let Some(r) = revision {
516                        if r.is_accepted() {
517                            // You can't redact an accepted revision.
518                            return Err(ApplyError::UnexpectedState);
519                        }
520                        if r.author.public_key() != &author {
521                            // Can't redact someone else's revision.
522                            // Since the author never changes, we can safely mark this as invalid.
523                            return Err(ApplyError::NotAuthorized);
524                        }
525                        *revision = None;
526                    }
527                } else {
528                    return Err(ApplyError::Missing(revision));
529                }
530            }
531            Action::Revision {
532                title,
533                description,
534                blob,
535                signature,
536                parent,
537            } => {
538                debug_assert!(!self.revisions.contains_key(&entry));
539
540                let doc = repo.blob(blob)?;
541                let doc = Doc::from_blob(&doc)?;
542                // All revisions but the first one must have a parent.
543                let Some(parent) = parent else {
544                    return Err(ApplyError::MissingParent);
545                };
546                let Some(parent) = lookup::revision(&self.revisions, &parent)? else {
547                    return Err(ApplyError::Redacted);
548                };
549                // If the parent of this revision is no longer the current document, this
550                // revision can be marked as outdated.
551                let state = if parent.id == current.id {
552                    // If the revision is not outdated, we expect it to make a change to the
553                    // current version.
554                    if doc == parent.doc {
555                        return Err(ApplyError::DocUnchanged);
556                    }
557                    State::Active
558                } else {
559                    State::Stale
560                };
561
562                // Verify signature over new blob, using trusted delegates.
563                if parent.verify_signature(&author, &signature, blob).is_err() {
564                    return Err(ApplyError::InvalidSignature(author, blob));
565                }
566                let revision = Revision::new(
567                    entry,
568                    title,
569                    description,
570                    author.into(),
571                    blob,
572                    doc,
573                    state,
574                    signature,
575                    Some(parent.id),
576                    timestamp,
577                );
578                let id = revision.id;
579
580                self.heads.insert(author.into(), id);
581                self.revisions.insert(id, Some(revision));
582
583                if state == State::Active {
584                    self.adopt(id);
585                }
586            }
587        }
588        Ok(())
589    }
590
591    /// Try to adopt a revision as the current one.
592    fn adopt(&mut self, id: RevisionId) {
593        if self.current == id {
594            return;
595        }
596        let votes = self
597            .heads
598            .values()
599            .filter(|revision| **revision == id)
600            .count();
601        if self.is_majority(votes) {
602            self.current = id;
603            self.current_mut().state = State::Accepted;
604
605            // Void all other active revisions.
606            for r in self
607                .revisions
608                .iter_mut()
609                .filter_map(|(_, r)| r.as_mut())
610                .filter(|r| r.state == State::Active)
611            {
612                r.state = State::Stale;
613            }
614        }
615    }
616
617    /// A specific [`Revision`], mutably.
618    fn revision_mut(&mut self, revision: &RevisionId) -> Option<&mut Revision> {
619        self.revisions.get_mut(revision).and_then(|r| r.as_mut())
620    }
621
622    /// The current revision, mutably.
623    fn current_mut(&mut self) -> &mut Revision {
624        let current = self.current;
625        self.revision_mut(&current)
626            .expect("Identity::current_mut: the current revision must always exist")
627    }
628}
629
630impl<R: ReadRepository> cob::Evaluate<R> for Identity {
631    type Error = Error;
632
633    fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
634        let op = Op::try_from(entry)?;
635        let object = Identity::from_root(op, repo)?;
636
637        Ok(object)
638    }
639
640    fn apply<'a, I: Iterator<Item = (&'a EntryId, &'a cob::Entry)>>(
641        &mut self,
642        entry: &cob::Entry,
643        concurrent: I,
644        repo: &R,
645    ) -> Result<(), Self::Error> {
646        let op = Op::try_from(entry)?;
647
648        self.op(op, concurrent.map(|(_, e)| e), repo)
649            .map_err(Error::Apply)
650    }
651}
652
653#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
654pub enum Verdict {
655    /// An accepting verdict must supply the [`Signature`] over the
656    /// new proposed [`Doc`].
657    Accept(Signature),
658    /// Rejecting the proposed [`Doc`].
659    Reject,
660}
661
662/// State of a revision.
663#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub enum State {
666    /// The revision is actively being voted on. From here, it can go into any of the
667    /// other states.
668    Active,
669    /// The revision has been accepted by a majority of delegates. Once accepted,
670    /// a revision doesn't change state.
671    Accepted,
672    /// The revision was rejected by a majority of delegates. Once rejected,
673    /// a revision doesn't change state.
674    Rejected,
675    /// The revision was active, but has been replaced by another revision,
676    /// and is now outdated. Once stale, a revision doesn't change state.
677    Stale,
678}
679
680impl std::fmt::Display for State {
681    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682        match self {
683            Self::Active => write!(f, "active"),
684            Self::Accepted => write!(f, "accepted"),
685            Self::Rejected => write!(f, "rejected"),
686            Self::Stale => write!(f, "stale"),
687        }
688    }
689}
690
691/// A new [`Doc`] for an [`Identity`]. The revision can be
692/// reviewed by gathering [`Signature`]s for accepting the changes, or
693/// rejecting them.
694///
695/// Once a revision has reached the quorum threshold of the previous
696/// [`Identity`] it is then adopted as the current identity.
697#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
698pub struct Revision {
699    /// The id of this revision. Points to a commit.
700    pub id: RevisionId,
701    /// Identity document blob at this revision.
702    pub blob: Oid,
703    /// Title of the proposal.
704    pub title: cob::Title,
705    /// State of the revision.
706    pub state: State,
707    /// Description of the proposal.
708    pub description: String,
709    /// Author of this proposed revision.
710    pub author: Author,
711    /// New [`Doc`] that will replace `previous`' document.
712    pub doc: Doc,
713    /// Physical timestamp of this proposal revision.
714    pub timestamp: Timestamp,
715    /// Parent revision.
716    pub parent: Option<RevisionId>,
717
718    /// Signatures and rejections given by the delegates.
719    verdicts: BTreeMap<PublicKey, Verdict>,
720}
721
722impl std::ops::Deref for Revision {
723    type Target = Doc;
724
725    fn deref(&self) -> &Self::Target {
726        &self.doc
727    }
728}
729
730impl Revision {
731    pub fn signatures(&self) -> impl Iterator<Item = (&PublicKey, Signature)> {
732        self.verdicts().filter_map(|(key, verdict)| match verdict {
733            Verdict::Accept(sig) => Some((key, *sig)),
734            Verdict::Reject => None,
735        })
736    }
737
738    pub fn is_accepted(&self) -> bool {
739        matches!(self.state, State::Accepted)
740    }
741
742    pub fn is_active(&self) -> bool {
743        matches!(self.state, State::Active)
744    }
745
746    pub fn verdicts(&self) -> impl Iterator<Item = (&PublicKey, &Verdict)> {
747        self.verdicts.iter()
748    }
749
750    pub fn accepted(&self) -> impl Iterator<Item = Did> + '_ {
751        self.signatures().map(|(key, _)| key.into())
752    }
753
754    pub fn rejected(&self) -> impl Iterator<Item = Did> + '_ {
755        self.verdicts().filter_map(|(key, v)| match v {
756            Verdict::Accept(_) => None,
757            Verdict::Reject => Some(key.into()),
758        })
759    }
760
761    pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
762        &self,
763        signer: &G,
764    ) -> Result<Signature, DocError> {
765        self.doc.signature_of(signer)
766    }
767}
768
769// Private functions that may not do all the verification. Use with caution.
770impl Revision {
771    fn new(
772        id: RevisionId,
773        title: cob::Title,
774        description: String,
775        author: Author,
776        blob: Oid,
777        doc: Doc,
778        state: State,
779        signature: Signature,
780        parent: Option<RevisionId>,
781        timestamp: Timestamp,
782    ) -> Self {
783        let verdicts = BTreeMap::from_iter([(*author.public_key(), Verdict::Accept(signature))]);
784
785        Self {
786            id,
787            title,
788            description,
789            author,
790            blob,
791            doc,
792            state,
793            verdicts,
794            parent,
795            timestamp,
796        }
797    }
798
799    fn accept(
800        &mut self,
801        author: PublicKey,
802        signature: Signature,
803        current: &Revision,
804    ) -> Result<(), ApplyError> {
805        // Check that this is a valid signature over the new document blob id.
806        if current
807            .verify_signature(&author, &signature, self.blob)
808            .is_err()
809        {
810            return Err(ApplyError::InvalidSignature(author, self.blob));
811        }
812        if self
813            .verdicts
814            .insert(author, Verdict::Accept(signature))
815            .is_some()
816        {
817            return Err(ApplyError::DuplicateVerdict);
818        }
819        Ok(())
820    }
821
822    fn reject(&mut self, key: PublicKey) -> Result<(), ApplyError> {
823        if self.verdicts.insert(key, Verdict::Reject).is_some() {
824            return Err(ApplyError::DuplicateVerdict);
825        }
826        // Mark as rejected if it's impossible for this revision to be accepted
827        // with the current delegate set. Note that if the delegate set changes,
828        // this proposal will be marked as `stale` anyway.
829        if self.is_active() && self.rejected().count() > self.delegates().len() - self.majority() {
830            self.state = State::Rejected;
831        }
832        Ok(())
833    }
834}
835
836impl<R: ReadRepository> store::Transaction<Identity, R> {
837    pub fn accept(
838        &mut self,
839        revision: RevisionId,
840        signature: Signature,
841    ) -> Result<(), store::Error> {
842        self.push(Action::RevisionAccept {
843            revision,
844            signature,
845        })
846    }
847
848    pub fn reject(&mut self, revision: RevisionId) -> Result<(), store::Error> {
849        self.push(Action::RevisionReject { revision })
850    }
851
852    pub fn edit(
853        &mut self,
854        revision: RevisionId,
855        title: cob::Title,
856        description: impl ToString,
857    ) -> Result<(), store::Error> {
858        self.push(Action::RevisionEdit {
859            revision,
860            title,
861            description: description.to_string(),
862        })
863    }
864
865    pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
866        self.push(Action::RevisionRedact { revision })
867    }
868}
869
870impl<R: WriteRepository> store::Transaction<Identity, R> {
871    pub fn revision<G: crypto::signature::Signer<crypto::Signature>>(
872        &mut self,
873        title: cob::Title,
874        description: impl ToString,
875        doc: &Doc,
876        parent: Option<RevisionId>,
877        repo: &R,
878        signer: &Device<G>,
879    ) -> Result<(), store::Error> {
880        let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
881        // Store document blob in repository.
882        let embed =
883            Embed::<Uri>::store("radicle.json", &bytes, repo.raw()).map_err(store::Error::Git)?;
884        debug_assert_eq!(embed.content, Uri::from(blob)); // Make sure we pre-computed the correct OID for the blob.
885
886        // Identity document.
887        self.embed([embed])?;
888
889        // Revision metadata.
890        self.push(Action::Revision {
891            title,
892            description: description.to_string(),
893            blob,
894            parent,
895            signature,
896        })
897    }
898}
899
900pub struct IdentityMut<'a, R> {
901    pub id: ObjectId,
902
903    identity: Identity,
904    store: store::Store<'a, Identity, R>,
905}
906
907impl<R> fmt::Debug for IdentityMut<'_, R> {
908    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909        f.debug_struct("IdentityMut")
910            .field("id", &self.id)
911            .field("identity", &self.identity)
912            .finish()
913    }
914}
915
916impl<R> IdentityMut<'_, R>
917where
918    R: WriteRepository + cob::Store<Namespace = NodeId>,
919{
920    /// Reload the identity data from storage.
921    pub fn reload(&mut self) -> Result<(), store::Error> {
922        self.identity = self
923            .store
924            .get(&self.id)?
925            .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
926
927        Ok(())
928    }
929
930    pub fn transaction<G, F>(
931        &mut self,
932        message: &str,
933        signer: &Device<G>,
934        operations: F,
935    ) -> Result<EntryId, Error>
936    where
937        G: crypto::signature::Signer<crypto::Signature>,
938        F: FnOnce(&mut Transaction<Identity, R>, &R) -> Result<(), store::Error>,
939    {
940        let mut tx = Transaction::default();
941        operations(&mut tx, self.store.as_ref())?;
942
943        let (doc, commit) = tx.commit(message, self.id, &mut self.store, signer)?;
944        self.identity = doc;
945
946        Ok(commit)
947    }
948
949    /// Update the identity by proposing a new revision.
950    /// If the signer is the only delegate, the revision is accepted automatically.
951    pub fn update<G>(
952        &mut self,
953        title: cob::Title,
954        description: impl ToString,
955        doc: &Doc,
956        signer: &Device<G>,
957    ) -> Result<RevisionId, Error>
958    where
959        G: crypto::signature::Signer<crypto::Signature>,
960    {
961        let parent = self.current;
962        let id = self.transaction("Propose revision", signer, |tx, repo| {
963            tx.revision(title, description, doc, Some(parent), repo, signer)
964        })?;
965
966        Ok(id)
967    }
968
969    /// Accept an active revision.
970    pub fn accept<G>(&mut self, revision: &RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
971    where
972        G: crypto::signature::Signer<crypto::Signature>,
973    {
974        let id = *revision;
975        let revision = self.revision(revision).ok_or(Error::NotFound(id))?;
976        let signature = revision.sign(signer)?;
977
978        self.transaction("Accept revision", signer, |tx, _| tx.accept(id, signature))
979    }
980
981    /// Reject an active revision.
982    pub fn reject<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
983    where
984        G: crypto::signature::Signer<crypto::Signature>,
985    {
986        self.transaction("Reject revision", signer, |tx, _| tx.reject(revision))
987    }
988
989    /// Redact a revision.
990    pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
991    where
992        G: crypto::signature::Signer<crypto::Signature>,
993    {
994        self.transaction("Redact revision", signer, |tx, _| tx.redact(revision))
995    }
996
997    /// Edit an active revision's title or description.
998    pub fn edit<G>(
999        &mut self,
1000        revision: RevisionId,
1001        title: cob::Title,
1002        description: String,
1003        signer: &Device<G>,
1004    ) -> Result<EntryId, Error>
1005    where
1006        G: crypto::signature::Signer<crypto::Signature>,
1007    {
1008        self.transaction("Edit revision", signer, |tx, _| {
1009            tx.edit(revision, title, description)
1010        })
1011    }
1012}
1013
1014impl<R> Deref for IdentityMut<'_, R> {
1015    type Target = Identity;
1016
1017    fn deref(&self) -> &Self::Target {
1018        &self.identity
1019    }
1020}
1021
1022mod lookup {
1023    use super::*;
1024
1025    pub fn revision_mut<'a>(
1026        revisions: &'a mut BTreeMap<RevisionId, Option<Revision>>,
1027        revision: &RevisionId,
1028    ) -> Result<Option<&'a mut Revision>, ApplyError> {
1029        match revisions.get_mut(revision) {
1030            Some(Some(revision)) => Ok(Some(revision)),
1031            // Redacted.
1032            Some(None) => Ok(None),
1033            // Missing. Causal error.
1034            None => Err(ApplyError::Missing(*revision)),
1035        }
1036    }
1037
1038    pub fn revision<'a>(
1039        revisions: &'a BTreeMap<RevisionId, Option<Revision>>,
1040        revision: &RevisionId,
1041    ) -> Result<Option<&'a Revision>, ApplyError> {
1042        match revisions.get(revision) {
1043            Some(Some(revision)) => Ok(Some(revision)),
1044            // Redacted.
1045            Some(None) => Ok(None),
1046            // Missing. Causal error.
1047            None => Err(ApplyError::Missing(*revision)),
1048        }
1049    }
1050}
1051
1052#[cfg(test)]
1053#[allow(clippy::unwrap_used)]
1054mod test {
1055    use qcheck_macros::quickcheck;
1056
1057    use crate::cob::{self, Title};
1058    use crate::crypto::PublicKey;
1059    use crate::identity::did::Did;
1060    use crate::identity::doc::PayloadId;
1061    use crate::identity::Visibility;
1062    use crate::rad;
1063    use crate::storage::git::Storage;
1064    use crate::storage::ReadStorage;
1065    use crate::test::fixtures;
1066    use crate::test::setup::{Network, NodeWithRepo};
1067
1068    use super::*;
1069
1070    #[quickcheck]
1071    fn prop_json_eq_str(pk: PublicKey, proj: RepoId, did: Did) {
1072        let json = serde_json::to_string(&pk).unwrap();
1073        assert_eq!(format!("\"{pk}\""), json);
1074
1075        let json = serde_json::to_string(&proj).unwrap();
1076        assert_eq!(format!("\"{}\"", proj.urn()), json);
1077
1078        let json = serde_json::to_string(&did).unwrap();
1079        assert_eq!(format!("\"{did}\""), json);
1080    }
1081
1082    #[test]
1083    fn test_identity_updates() {
1084        let NodeWithRepo { node, repo } = NodeWithRepo::default();
1085        let bob = Device::mock();
1086        let signer = &node.signer;
1087        let mut identity = Identity::load_mut(&*repo).unwrap();
1088        let mut doc = identity.doc().clone().edit();
1089        let title = Title::new("Identity update").unwrap();
1090        let description = "";
1091        let r0 = identity.current;
1092
1093        // The initial state is accepted.
1094        assert!(identity.current().is_accepted());
1095        // Using an identical document to the current one fails.
1096        identity
1097            .update(
1098                title.clone(),
1099                description,
1100                &doc.clone().verified().unwrap(),
1101                signer,
1102            )
1103            .unwrap_err();
1104        assert_eq!(identity.current, r0);
1105
1106        // Change threshold to `2`, even though there's only one delegate. This should
1107        // fail as it makes the master branch immutable.
1108        doc.threshold = 2;
1109        assert!(doc.clone().verified().is_err());
1110
1111        // Let's add another delegate.
1112        doc.delegate(bob.public_key().into());
1113        // The update should go through now.
1114        let r1 = identity
1115            .update(
1116                title.clone(),
1117                description,
1118                &doc.clone().verified().unwrap(),
1119                signer,
1120            )
1121            .unwrap();
1122        assert!(identity.revision(&r1).unwrap().is_accepted());
1123        assert_eq!(identity.current, r1);
1124        // With two delegates now, we need two signatures for any update to go through.
1125        // So this next update shouldn't be accepted as canonical until the second delegate
1126        // signs it.
1127        doc.visibility = Visibility::private([]);
1128        let r2 = identity
1129            .update(
1130                title.clone(),
1131                description,
1132                &doc.clone().verified().unwrap(),
1133                signer,
1134            )
1135            .unwrap();
1136        // R1 is still the head.
1137        assert_eq!(identity.current, r1);
1138        assert_eq!(identity.revision(&r2).unwrap().state, State::Active);
1139        assert_eq!(repo.canonical_identity_head().unwrap(), r1);
1140        assert_eq!(
1141            repo.identity_doc().unwrap().visibility(),
1142            &Visibility::Public
1143        );
1144        // Now let's add a signature on R2 from Bob.
1145        identity.accept(&r2, &bob).unwrap();
1146
1147        // R2 is now the head.
1148        assert_eq!(identity.current, r2);
1149        assert_eq!(identity.revision(&r2).unwrap().state, State::Accepted);
1150        assert_eq!(repo.canonical_identity_head().unwrap(), r2);
1151        assert_eq!(
1152            repo.canonical_identity_doc().unwrap().visibility(),
1153            &Visibility::private([])
1154        );
1155    }
1156
1157    #[test]
1158    fn test_identity_update_rejected() {
1159        let NodeWithRepo { node, repo } = NodeWithRepo::default();
1160        let bob = Device::mock();
1161        let eve = Device::mock();
1162        let signer = &node.signer;
1163        let mut identity = Identity::load_mut(&*repo).unwrap();
1164        let mut doc = identity.doc().clone().edit();
1165        let description = "";
1166
1167        // Let's add another delegate.
1168        doc.delegate(bob.public_key().into());
1169        let r1 = identity
1170            .update(
1171                cob::Title::new("Identity update").unwrap(),
1172                description,
1173                &doc.clone().verified().unwrap(),
1174                signer,
1175            )
1176            .unwrap();
1177        assert_eq!(identity.current, r1);
1178
1179        doc.visibility = Visibility::private([]);
1180        let r2 = identity
1181            .update(
1182                cob::Title::new("Make private").unwrap(),
1183                description,
1184                &doc.clone().verified().unwrap(),
1185                &node.signer,
1186            )
1187            .unwrap();
1188
1189        // 1/2 rejected means that we can never reach the required 2/2 votes.
1190        identity.reject(r2, &bob).unwrap();
1191        let r2 = identity.revision(&r2).unwrap();
1192        assert_eq!(r2.state, State::Rejected);
1193
1194        // Now let's add another delegate.
1195        doc.delegate(eve.public_key().into());
1196        let r3 = identity
1197            .update(
1198                cob::Title::new("Add Eve").unwrap(),
1199                description,
1200                &doc.clone().verified().unwrap(),
1201                &node.signer,
1202            )
1203            .unwrap();
1204        let _ = identity.accept(&r3, &bob).unwrap();
1205        assert_eq!(identity.current, r3);
1206
1207        doc.visibility = Visibility::Public;
1208        let r3 = identity
1209            .update(
1210                cob::Title::new("Make public").unwrap(),
1211                description,
1212                &doc.verified().unwrap(),
1213                &node.signer,
1214            )
1215            .unwrap();
1216
1217        // 1/3 rejected means that we can still reach the 2/3 required votes.
1218        identity.reject(r3, &bob).unwrap();
1219        let r3 = identity.revision(&r3).unwrap().clone();
1220        assert_eq!(r3.state, State::Active); // Still active.
1221
1222        // 2/3 rejected means that we can no longer reach the 2/3 required votes.
1223        identity.reject(r3.id, &eve).unwrap();
1224        let r3 = identity.revision(&r3.id).unwrap();
1225        assert_eq!(r3.state, State::Rejected);
1226    }
1227
1228    #[test]
1229    fn test_identity_updates_concurrent() {
1230        let network = Network::default();
1231        let alice = &network.alice;
1232        let bob = &network.bob;
1233
1234        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1235        let mut alice_doc = alice_identity.doc().clone().edit();
1236
1237        alice_doc.delegate(bob.signer.public_key().into());
1238        let a1 = alice_identity
1239            .update(
1240                cob::Title::new("Add Bob").unwrap(),
1241                "",
1242                &alice_doc.clone().verified().unwrap(),
1243                &alice.signer,
1244            )
1245            .unwrap();
1246
1247        bob.repo.fetch(alice);
1248
1249        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1250        let bob_doc = bob_identity.doc().clone();
1251        assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
1252
1253        // Alice changes the document without making Bob aware.
1254        alice_doc.visibility = Visibility::private([]);
1255        let a2 = alice_identity
1256            .update(
1257                cob::Title::new("Change visibility").unwrap(),
1258                "",
1259                &alice_doc.clone().clone().verified().unwrap(),
1260                &alice.signer,
1261            )
1262            .unwrap();
1263        // Bob makes the same change without knowing Alice already did.
1264        let b1 = bob_identity
1265            .update(
1266                cob::Title::new("Make private").unwrap(),
1267                "",
1268                &alice_doc.verified().unwrap(),
1269                &bob.signer,
1270            )
1271            .unwrap();
1272
1273        // Bob gets Alice's data.
1274        bob.repo.fetch(alice);
1275        bob_identity.reload().unwrap();
1276        assert_eq!(bob_identity.current, a1);
1277
1278        // Alice gets Bob's data.
1279        // There's not enough votes for either of these proposals to pass.
1280        alice.repo.fetch(bob);
1281        alice_identity.reload().unwrap();
1282        assert_eq!(alice_identity.current, a1);
1283        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Active);
1284        assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Active);
1285
1286        // Now Bob accepts Alice's proposal. This voids his own.
1287        bob_identity.accept(&a2, &bob.signer).unwrap();
1288        assert_eq!(bob_identity.current, a2);
1289        assert_eq!(bob_identity.revision(&a1).unwrap().state, State::Accepted);
1290        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
1291        assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Stale);
1292    }
1293
1294    #[test]
1295    fn test_identity_redact_revision() {
1296        let network = Network::default();
1297        let alice = &network.alice;
1298        let bob = &network.bob;
1299        let eve = &network.eve;
1300
1301        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1302        let mut alice_doc = alice_identity.doc().clone().edit();
1303
1304        alice_doc.delegate(bob.signer.public_key().into());
1305        let a0 = alice_identity.root;
1306        let a1 = alice_identity
1307            .update(
1308                cob::Title::new("Add Bob").unwrap(),
1309                "Eh.",
1310                &alice_doc.clone().clone().verified().unwrap(),
1311                &alice.signer,
1312            )
1313            .unwrap();
1314
1315        alice_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
1316        let a2 = alice_identity
1317            .update(
1318                cob::Title::new("Change visibility").unwrap(),
1319                "Eh.",
1320                &alice_doc.verified().unwrap(),
1321                &alice.signer,
1322            )
1323            .unwrap();
1324
1325        bob.repo.fetch(alice);
1326        let a3 = alice_identity.redact(a2, &alice.signer).unwrap();
1327        assert!(alice_identity.revision(&a1).is_some());
1328        assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);
1329
1330        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1331        let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();
1332
1333        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
1334        assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
1335        bob.repo.fetch(alice);
1336        bob_identity.reload().unwrap();
1337
1338        assert_eq!(bob_identity.timeline, vec![a0, a1, a2, a3, b1]);
1339        assert_eq!(bob_identity.revision(&a2), None);
1340        assert_eq!(bob_identity.current, a1);
1341    }
1342
1343    #[test]
1344    fn test_identity_remove_delegate_concurrent() {
1345        let network = Network::default();
1346        let alice = &network.alice;
1347        let bob = &network.bob;
1348        let eve = &network.eve;
1349
1350        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1351        let mut alice_doc = alice_identity.doc().clone().edit();
1352
1353        alice_doc.delegate(bob.signer.public_key().into());
1354        alice_doc.delegate(eve.signer.public_key().into());
1355        let a0 = alice_identity.root;
1356        let a1 = alice_identity // Change description to change traversal order.
1357            .update(
1358                cob::Title::new("Add Bob and Eve").unwrap(),
1359                "Eh#!",
1360                &alice_doc.clone().verified().unwrap(),
1361                &alice.signer,
1362            )
1363            .unwrap();
1364
1365        alice_doc.rescind(&eve.signer.public_key().into()).unwrap();
1366        let a2 = alice_identity
1367            .update(
1368                cob::Title::new("Remove Eve").unwrap(),
1369                "",
1370                &alice_doc.verified().unwrap(),
1371                &alice.signer,
1372            )
1373            .unwrap();
1374
1375        bob.repo.fetch(eve);
1376        bob.repo.fetch(alice);
1377        eve.repo.fetch(bob);
1378
1379        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1380        let b1 = cob::git::stable::with_advanced_timestamp(|| {
1381            bob_identity.accept(&a2, &bob.signer).unwrap()
1382        });
1383        assert_eq!(bob_identity.current, a2);
1384
1385        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
1386        let mut eve_doc = eve_identity.doc().clone().edit();
1387        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
1388        let e1 = cob::git::stable::with_advanced_timestamp(|| {
1389            eve_identity
1390                .update(
1391                    cob::Title::new("Change visibility").unwrap(),
1392                    "",
1393                    &eve_doc.verified().unwrap(),
1394                    &eve.signer,
1395                )
1396                .unwrap()
1397        });
1398        // Eve's revision is active.
1399        assert!(eve_identity.revision(&e1).unwrap().is_active());
1400
1401        //  b1      (Accept "Remove Eve") 2/2
1402        //  |  e1   (Change visibility)
1403        //  | /
1404        //  a2      (Propose "Remove Eve") 1/2
1405        //  |
1406        //  a1      (Add Bob and Eve)
1407        //  |
1408        //  a0
1409
1410        eve.repo.fetch(bob);
1411        eve_identity.reload().unwrap();
1412        // Now that Eve reloaded, since Bob's vote to remove Eve went through first (b1 < e1),
1413        // her revision is no longer valid.
1414        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1]);
1415        assert_eq!(eve_identity.revision(&e1), None);
1416        assert!(!eve_identity.is_delegate(&eve.signer.public_key().into()));
1417    }
1418
1419    #[test]
1420    fn test_identity_reject_concurrent() {
1421        let network = Network::default();
1422        let alice = &network.alice;
1423        let bob = &network.bob;
1424        let eve = &network.eve;
1425
1426        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1427        let mut alice_doc = alice_identity.doc().clone().edit();
1428
1429        alice_doc.delegate(bob.signer.public_key().into());
1430        alice_doc.delegate(eve.signer.public_key().into());
1431        let a0 = alice_identity.root;
1432        let a1 = alice_identity
1433            .update(
1434                cob::Title::new("Add Bob and Eve").unwrap(),
1435                "Eh!#",
1436                &alice_doc.clone().verified().unwrap(),
1437                &alice.signer,
1438            )
1439            .unwrap();
1440
1441        alice_doc.visibility = Visibility::private([]);
1442        let a2 = alice_identity
1443            .update(
1444                cob::Title::new("Change visibility").unwrap(),
1445                "",
1446                &alice_doc.verified().unwrap(),
1447                &alice.signer,
1448            )
1449            .unwrap();
1450
1451        bob.repo.fetch(eve);
1452        bob.repo.fetch(alice);
1453        eve.repo.fetch(bob);
1454
1455        // Bob accepts alice's revision.
1456        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1457        let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();
1458
1459        // Eve rejects the revision, not knowing.
1460        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
1461        let e1 = eve_identity.reject(a2, &eve.signer).unwrap();
1462        assert!(eve_identity.revision(&a2).unwrap().is_active());
1463
1464        // Then she submits a new revision.
1465        let mut eve_doc = eve_identity.doc().clone().edit();
1466        eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
1467        let e2 = eve_identity
1468            .update(
1469                cob::Title::new("Change visibility").unwrap(),
1470                "",
1471                &eve_doc.verified().unwrap(),
1472                &eve.signer,
1473            )
1474            .unwrap();
1475        assert!(eve_identity.revision(&e2).unwrap().is_active());
1476
1477        //     e2   (Propose "Change visibility") 1/2
1478        //     |
1479        //     e1   (Reject "Change visibility")  1/2
1480        //  b1 |    (Accept "Change visibility")  2/2
1481        //  | /
1482        //  a2      (Propose "Change visibility") 1/2
1483        //  |
1484        //  a1      (Add Bob and Eve)
1485        //  |
1486        //  a0
1487
1488        // Though the rules are that you cannot reject an already accepted revision,
1489        // since this update was done concurrently there was no way of knowing. Therefore,
1490        // an error shouldn't be returned. We simply ignore the rejection.
1491
1492        eve.repo.fetch(bob);
1493        eve_identity.reload().unwrap();
1494        assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1, e2]);
1495
1496        // Her revision is there, although stale, since another revision was accepted since.
1497        // However, it wasn't pruned, even though rejecting an accepted revision is an error.
1498        let e2 = eve_identity.revision(&e2).unwrap();
1499        assert_eq!(e2.state, State::Stale);
1500        assert!(eve_identity.revision(&a2).unwrap().is_accepted());
1501    }
1502
1503    #[test]
1504    fn test_identity_updates_concurrent_outdated() {
1505        let network = Network::default();
1506        let alice = &network.alice;
1507        let bob = &network.bob;
1508        let eve = &network.eve;
1509
1510        let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1511        let mut alice_doc = alice_identity.doc().clone().edit();
1512
1513        alice.repo.fetch(bob);
1514        alice.repo.fetch(eve);
1515        alice_doc.delegate(bob.signer.public_key().into());
1516        alice_doc.delegate(eve.signer.public_key().into());
1517        let a0 = alice_identity.root;
1518        let a1 = alice_identity
1519            .update(
1520                cob::Title::new("Add Bob and Eve").unwrap(),
1521                "",
1522                &alice_doc.verified().unwrap(),
1523                &alice.signer,
1524            )
1525            .unwrap();
1526
1527        bob.repo.fetch(alice);
1528        eve.repo.fetch(alice);
1529
1530        let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1531        let mut bob_doc = bob_identity.doc().clone().edit();
1532        assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
1533
1534        //  a2 e1
1535        //  | /
1536        //  b1
1537        //  |
1538        //  a1
1539        //  |
1540        //  a0
1541
1542        // Bob and Alice change the document visibility. Eve is not aware.
1543        bob_doc.visibility = Visibility::private([]);
1544        let b1 = bob_identity
1545            .update(
1546                cob::Title::new("Change visibility #1").unwrap(),
1547                "",
1548                &bob_doc.verified().unwrap(),
1549                &bob.signer,
1550            )
1551            .unwrap();
1552        alice.repo.fetch(bob);
1553        eve.repo.fetch(bob);
1554
1555        // In the meantime, Eve does the same thing on her side.
1556        let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
1557        let mut eve_doc = eve_identity.doc().clone().edit();
1558        eve_doc.visibility = Visibility::private([]);
1559        let e1 = eve_identity
1560            .update(
1561                cob::Title::new("Change visibility #2").unwrap(),
1562                "Woops",
1563                &eve_doc.verified().unwrap(),
1564                &eve.signer,
1565            )
1566            .unwrap();
1567        assert_eq!(eve_identity.revisions().count(), 4);
1568        assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);
1569
1570        alice_identity.reload().unwrap();
1571        let a2 = cob::git::stable::with_advanced_timestamp(|| {
1572            alice_identity.accept(&b1, &alice.signer).unwrap()
1573        });
1574
1575        eve.repo.fetch(alice);
1576        eve_identity.reload().unwrap();
1577
1578        assert_eq!(eve_identity.timeline, vec![a0, a1, b1, e1, a2]);
1579        assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Stale);
1580    }
1581
1582    #[test]
1583    fn test_valid_identity() {
1584        let tempdir = tempfile::tempdir().unwrap();
1585        let mut rng = fastrand::Rng::new();
1586
1587        let alice = Device::mock_rng(&mut rng);
1588        let bob = Device::mock_rng(&mut rng);
1589        let eve = Device::mock_rng(&mut rng);
1590
1591        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1592        let (id, _, _, _) =
1593            fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
1594
1595        // Bob and Eve fork the project from Alice.
1596        rad::fork_remote(id, alice.public_key(), &bob, &storage).unwrap();
1597        rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap();
1598
1599        let repo = storage.repository(id).unwrap();
1600        let mut identity = Identity::load_mut(&repo).unwrap();
1601        let doc = identity.doc().clone();
1602        let prj = doc.project().unwrap();
1603        let mut doc = doc.edit();
1604
1605        // Make a change to the description and sign it.
1606        let desc = prj.description().to_owned() + "!";
1607        let prj = prj.update(None, desc, None).unwrap();
1608        doc.payload.insert(PayloadId::project(), prj.clone().into());
1609        identity
1610            .update(
1611                cob::Title::new("Update description").unwrap(),
1612                "",
1613                &doc.clone().verified().unwrap(),
1614                &alice,
1615            )
1616            .unwrap();
1617
1618        // Add Bob as a delegate, and sign it.
1619        doc.delegate(bob.public_key().into());
1620        doc.threshold = 2;
1621        identity
1622            .update(
1623                cob::Title::new("Add bob").unwrap(),
1624                "",
1625                &doc.clone().verified().unwrap(),
1626                &alice,
1627            )
1628            .unwrap();
1629
1630        // Add Eve as a delegate.
1631        doc.delegate(eve.public_key().into());
1632
1633        // Update with both Bob and Alice's signature.
1634        let revision = identity
1635            .update(
1636                cob::Title::new("Add eve").unwrap(),
1637                "",
1638                &doc.clone().verified().unwrap(),
1639                &alice,
1640            )
1641            .unwrap();
1642        identity.accept(&revision, &bob).unwrap();
1643
1644        // Update description again with signatures by Eve and Bob.
1645        let desc = prj.description().to_owned() + "?";
1646        let prj = prj.update(None, desc, None).unwrap();
1647        doc.payload.insert(PayloadId::project(), prj.into());
1648
1649        let revision = identity
1650            .update(
1651                cob::Title::new("Update description again").unwrap(),
1652                "Bob's repository",
1653                &doc.verified().unwrap(),
1654                &bob,
1655            )
1656            .unwrap();
1657        identity.accept(&revision, &eve).unwrap();
1658
1659        let identity: Identity = Identity::load(&repo).unwrap();
1660        let root = repo.identity_root().unwrap();
1661        let doc = repo.identity_doc_at(revision).unwrap();
1662
1663        assert_eq!(identity.signatures().count(), 2);
1664        assert_eq!(identity.revisions().count(), 5);
1665        assert_eq!(identity.id(), id);
1666        assert_eq!(identity.root().id, root);
1667        assert_eq!(identity.current().blob, doc.blob);
1668        assert_eq!(identity.current().description.as_str(), "Bob's repository");
1669        assert_eq!(identity.head(), revision);
1670        assert_eq!(identity.doc(), &*doc);
1671        assert_eq!(
1672            identity.doc().project().unwrap().description(),
1673            "Acme's repository!?"
1674        );
1675
1676        assert_eq!(doc.project().unwrap().description(), "Acme's repository!?");
1677    }
1678}