//! [`SignedOp`], [`OpState`], and the causal queue capacity constant. use crate::keypair::{Ed25519KeyPair, Ed25519PublicKey, Ed25519Signature}; use ed25519_dalek::Verifier as _; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Bytes}; use crate::keypair::{sha256, sign, AuthorId, SignedDigest}; use crate::op::{print_hex, print_path, Op, OpId}; use super::{CrdtNode, JsonValue}; /// Enum representing possible outcomes of applying an operation to a CRDT #[derive(Debug, PartialEq)] pub enum OpState { /// Operation applied successfully Ok, /// Tried to apply an operation to a non-CRDT primitive (i.e. f64, bool, etc.) /// If you would like a mutable primitive, wrap it in a [`LWWRegisterCRDT`] ErrApplyOnPrimitive, /// Tried to apply an operation to a static struct CRDT /// If you would like a mutable object, use a [`Value`] ErrApplyOnStruct, /// Tried to apply an operation that contains content of the wrong type. /// In other words, the content cannot be coerced to the CRDT at the path specified. ErrMismatchedType, /// The signed digest of the message did not match the claimed author of the message. /// This can happen if the message was tampered with during delivery ErrDigestMismatch, /// The hash of the message did not match the contents of the message. /// This can happen if the author tried to perform an equivocation attack by creating an /// operation and modifying it has already been created ErrHashMismatch, /// Tried to apply an operation to a non-existent path. The author may have forgotten to attach /// a causal dependency ErrPathMismatch, /// Trying to modify/delete the sentinel (zero-th) node element that is used for book-keeping ErrListApplyToEmpty, /// We have not received all of the causal dependencies of this operation. It has been queued /// up and will be executed when its causal dependencies have been delivered MissingCausalDependencies, /// This op has already been applied (identified by its `signed_digest`). /// The CRDT state is unchanged — this is a no-op (idempotent self-loop guard). AlreadySeen, } /// Maximum total number of ops that may sit in the causal-order hold queue at any /// one time, summed across all pending dependency buckets. /// /// **Overflow policy: drop oldest.** /// When the limit is reached, the oldest pending op in the largest dependency bucket /// is silently evicted before the new op is queued. Rationale: a misbehaving or /// heavily-partitioned peer can send ops whose causal ancestors never arrive, causing /// unbounded memory growth. Dropping the oldest entry preserves the most recent /// information and caps memory use. The peer can reconnect and receive a fresh bulk /// state dump to recover any dropped ops. pub const CAUSAL_QUEUE_MAX: usize = 256; /// An [`Op`] with a few bits of extra metadata #[serde_as] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct SignedOp { // Note that this can be different from the author of the inner op as the inner op could have been created // by a different person author: AuthorId, /// Signed hash using priv key of author. Effectively [`OpID`] Use this as the ID to figure out what has been delivered already #[serde_as(as = "Bytes")] pub signed_digest: SignedDigest, pub inner: Op, /// List of causal dependencies #[serde_as(as = "Vec")] pub depends_on: Vec, } impl SignedOp { pub fn id(&self) -> OpId { self.inner.id } pub fn author(&self) -> AuthorId { self.author } /// Creates a digest of the following fields. Any changes in the fields will change the signed digest /// - id (hash of the following) /// - origin /// - author /// - seq /// - is_deleted /// - path /// - dependencies fn digest(&self) -> [u8; 32] { let path_string = print_path(self.inner.path.clone()); let dependency_string = self .depends_on .iter() .map(print_hex) .collect::>() .join(""); let fmt_str = format!("{:?},{path_string},{dependency_string}", self.id()); sha256(fmt_str) } /// Sign this digest with the given keypair. Shouldn't need to be called manually, /// just use [`SignedOp::from_op`] instead fn sign_digest(&mut self, keypair: &Ed25519KeyPair) { self.signed_digest = sign(keypair, &self.digest()).to_bytes() } /// Ensure digest was actually signed by the author it claims to be signed by pub fn is_valid_digest(&self) -> bool { let digest = Ed25519Signature::from_bytes(&self.signed_digest); match Ed25519PublicKey::from_bytes(&self.author()) { Ok(pubkey) => pubkey.verify(&self.digest(), &digest).is_ok(), Err(_) => false, } } /// Sign a normal op and add all the needed metadata pub fn from_op( value: Op, keypair: &Ed25519KeyPair, depends_on: Vec, ) -> Self { let author = keypair.verifying_key().to_bytes(); let mut new = Self { inner: Op { content: value.content.map(|c| c.view()), origin: value.origin, author: value.author, seq: value.seq, path: value.path, is_deleted: value.is_deleted, id: value.id, }, author, signed_digest: [0u8; 64], depends_on, }; new.sign_digest(keypair); new } }