```rust use std::collections::{BTreeSet, HashMap}; use std::ops::Deref; use radicle::crypto::Signer; use radicle::hash; use radicle::prelude::PublicKey; use radicle::{cob::Timestamp, crypto::Signature}; use serde::{Deserialize, Serialize}; pub type ChangeId = radicle::hash::Digest; pub type Author = PublicKey; /// The `Change` is the unit of replication. /// Everything that can be done in the system is represented by a `Change` object. /// Changes are applied to an accumulator to yield a final state. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Change { /// The action carried out by this change. action: Action, /// The author of the change. author: Author, /// The time at which this change was authored. timestamp: Timestamp, /// Other observed changes. (UNUSED) _observed: BTreeSet<ChangeId>, } impl Change { /// Get the change id. pub fn id(&self) -> ChangeId { hash::Digest::new(self.encode()) } /// Serialize the change into a byte string. pub fn encode(&self) -> Vec<u8> { let mut buf = Vec::new(); let mut serializer = serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new()); self.serialize(&mut serializer).unwrap(); buf } } /// Change envelope. Carries signed changes. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Envelope { /// Changes included in this envelope, serialized as JSON. pub changes: Vec<u8>, /// Signature over the change, by the change author. pub signature: Signature, } /// An object that can be either present or removed. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum Redactable<T> { /// When the object is present. Present(T), /// When the object has been removed. #[default] Redacted, } /// A comment on a discussion thread. #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Comment { /// The comment body. body: String, /// Thread or comment this is a reaply to. reply_to: ChangeId, } impl Comment { /// Create a new comment. pub fn new(body: String, reply_to: ChangeId) -> Self { Self { body, reply_to } } } /// A discussion thread. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Thread { /// The id of the thread. id: ChangeId, /// The thread title. title: String, /// The thread author. author: Author, /// The thread timestamp. timestamp: Timestamp, /// The comments under the thread. comments: HashMap<ChangeId, Redactable<Comment>>, } impl Deref for Thread { type Target = HashMap<ChangeId, Redactable<Comment>>; fn deref(&self) -> &Self::Target { &self.comments } } /// An action that can be carried out in a change. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub enum Action { /// Initialize a new thread. Thread { title: String }, /// Comment on a thread. Comment { comment: Comment }, /// Redact an action. Not all actions can be redacted. Redact { id: ChangeId }, } impl Thread { pub fn new(root: Change) -> Self { let id = root.id(); let Action::Thread { title } = root.action else { panic!("Threads need to be initialized with a `Thread` message"); }; Self { id, title, author: root.author, timestamp: root.timestamp, comments: HashMap::default(), } } pub fn clear(&mut self) { self.comments.clear(); } pub fn apply(&mut self, msgs: impl IntoIterator<Item = Change>) { for msg in msgs.into_iter() { let id = msg.id(); match msg.action { Action::Comment { comment } => { match self.comments.get(&id) { Some(Redactable::Present(_)) => { // Do nothing, the action was already processed. } Some(Redactable::Redacted) => { // Do nothing, the action was redacted. } None => { self.comments.insert(id, Redactable::Present(comment)); } } } Action::Redact { id } => { self.comments.insert(id, Redactable::Redacted); } Action::Thread { .. } => { // Ignored } } } } } /// An object that can be used to create and sign changes. #[derive(Default)] pub struct Actor<G> { signer: G, } impl<G: Signer> Actor<G> { /// Create a new thread. pub fn thread(&self, title: &str, timestamp: Timestamp) -> Change { self.change( Action::Thread { title: title.to_owned(), }, timestamp, ) } /// Create a new comment. pub fn comment(&self, body: &str, timestamp: Timestamp, parent: ChangeId) -> Change { self.change( Action::Comment { comment: Comment::new(String::from(body), parent), }, timestamp, ) } /// Create a new redaction. pub fn redact(&self, id: ChangeId, timestamp: Timestamp) -> Change { self.change(Action::Redact { id }, timestamp) } /// Create a new change. pub fn change(&self, action: Action, timestamp: Timestamp) -> Change { let author = *self.signer.public_key(); let _observed = BTreeSet::new(); Change { action, author, timestamp, _observed, } } pub fn sign(&self, changes: impl IntoIterator<Item = Change>) -> Envelope { let changes = changes.into_iter().collect::<Vec<_>>(); let json = serde_json::to_value(changes).unwrap(); let mut buffer = Vec::new(); let mut serializer = serde_json::Serializer::with_formatter( &mut buffer, olpc_cjson::CanonicalFormatter::new(), ); json.serialize(&mut serializer).unwrap(); let signature = self.signer.sign(&buffer); Envelope { changes: buffer, signature, } } } ``` Tests ```rust #[cfg(test)] mod tests { use itertools::Itertools; use radicle::crypto::test::signer::MockSigner; use super::*; #[test] fn test_invariants() { let alice = Actor::<MockSigner>::default(); let bob = Actor::<MockSigner>::default(); let time = Timestamp::now(); let b0 = bob.thread("Dinner Ingredients", time); let a0 = alice.comment("Ham", time, b0.id()); let a1 = alice.comment("Rye", time, a0.id()); let a2 = alice.comment("Dough", time, a1.id()); let a3 = alice.redact(a1.id(), time); let t = Thread::new(b0); assert_order_invariance(&t, [&a0, &a1, &a2, &a3]); assert_idempotence(&t, [&a0, &a1, &a2, &a3]); } fn assert_order_invariance<'a>(t: &Thread, msgs: impl IntoIterator<Item = &'a Change>) { let msgs = msgs.into_iter().cloned().collect::<Vec<_>>(); let count = msgs.len(); let mut actual = t.clone(); let mut expected = t.clone(); expected.clear(); expected.apply(msgs.clone()); for permutation in msgs.into_iter().permutations(count) { actual.clear(); actual.apply(permutation); assert_eq!(actual, expected); } } fn assert_idempotence<'a>(t: &Thread, msgs: impl IntoIterator<Item = &'a Change>) { let msgs = msgs.into_iter().cloned().collect::<Vec<_>>(); let mut actual = t.clone(); let mut expected = t.clone(); expected.clear(); expected.apply(msgs.clone()); actual.clear(); actual.apply(msgs.clone()); actual.apply(msgs.clone()); actual.apply(msgs); assert_eq!(actual, expected); } } ```