144 lines
5.6 KiB
Rust
144 lines
5.6 KiB
Rust
//! [`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<Value>`] 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<JsonValue>,
|
|
/// List of causal dependencies
|
|
#[serde_as(as = "Vec<Bytes>")]
|
|
pub depends_on: Vec<SignedDigest>,
|
|
}
|
|
|
|
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::<Vec<_>>()
|
|
.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<T: CrdtNode>(
|
|
value: Op<T>,
|
|
keypair: &Ed25519KeyPair,
|
|
depends_on: Vec<SignedDigest>,
|
|
) -> 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
|
|
}
|
|
}
|