Skip to main content

rings_core/dht/entry/
crdt.rs

1//! CRDT carriers for DHT entries.
2//!
3//! State variables:
4//! - `register` is an optional LWW reset floor for overwrite.
5//! - `values` is an LWW element set keyed by encoded payload.
6//! - `removes` is a two-phase tombstone set for observed data dots.
7//!
8//! Semilattice laws:
9//! - `DataTopicBuffer` join is idempotent, commutative, and associative over
10//!   normalized LWW element sets.
11//! - `RelayMessageSet` join is idempotent, commutative, and associative over
12//!   normalized two-phase sets.
13//!
14//! Constructor postconditions:
15//! - `DataTopicBuffer::new` preserves only values whose dot is at or after the
16//!   reset floor when a reset floor exists.
17//! - Tombstone-aware constructors preserve only adds whose dot has not been
18//!   tombstoned.
19
20use std::collections::BTreeMap;
21use std::collections::BTreeSet;
22
23use serde::Deserialize;
24use serde::Serialize;
25
26use crate::algebra::JoinSemilattice;
27use crate::dht::Did;
28use crate::error::Error;
29use crate::error::Result;
30use crate::message::Encoded;
31
32/// Hybrid logical version for LWW entry registers and element dots.
33///
34/// `logical_time_ms` starts from the wall-clock millisecond observed at the
35/// storage-operation boundary, then advances beyond any local floor that would
36/// otherwise dominate it. `actor` and `operation` make concurrent writes from
37/// the same millisecond totally ordered without claiming wall-clock recency.
38#[derive(
39    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
40)]
41pub struct EntryVersion {
42    /// Hybrid logical time in milliseconds.
43    #[serde(alias = "epoch_ms")]
44    pub logical_time_ms: u128,
45    /// Storage node that first stamped the operation.
46    pub actor: Did,
47    /// Deterministic digest of the stamped operation payload.
48    #[serde(default)]
49    pub operation: Did,
50}
51
52impl EntryVersion {
53    /// Construct a version from an explicit hybrid logical time and actor.
54    pub fn new(logical_time_ms: u128, actor: Did, operation: Did) -> Self {
55        Self {
56            logical_time_ms,
57            actor,
58            operation,
59        }
60    }
61
62    /// Construct a version at the current operation boundary.
63    pub fn issued_by(actor: Did, operation: Did) -> Self {
64        Self::new(crate::utils::get_epoch_ms(), actor, operation)
65    }
66
67    pub(super) fn after(self, floor: Option<Self>) -> Self {
68        let Some(floor) = floor else {
69            return self;
70        };
71        if self > floor {
72            return self;
73        }
74        Self {
75            logical_time_ms: floor.logical_time_ms.saturating_add(1),
76            actor: self.actor,
77            operation: self.operation,
78        }
79    }
80}
81
82/// Unique add witness for one visible entry payload element.
83#[derive(
84    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
85)]
86pub struct EntryDot {
87    /// LWW version that issued this element.
88    pub version: EntryVersion,
89    /// Element position inside the issuing operation.
90    pub index: u32,
91}
92
93impl EntryDot {
94    pub(super) fn for_index(version: EntryVersion, index: usize) -> Result<Self> {
95        let index = u32::try_from(index).map_err(|_| Error::EntryDotIndexOutOfBounds { index })?;
96        Ok(Self { version, index })
97    }
98}
99
100/// CRDT metadata carried beside the legacy entry payload.
101///
102/// `register` is the LWW reset floor used by overwrite. `dots` are per-element
103/// add witnesses used by data/topic and relay element sets. `tombstones` is the
104/// remove set for observed add dots.
105#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
106pub struct EntryCrdt {
107    /// Optional LWW reset floor for the entry payload.
108    pub register: Option<EntryVersion>,
109    /// Per-element add dots. When absent, legacy entries synthesize dots from
110    /// their payload order and value digest.
111    pub dots: Vec<EntryDot>,
112    /// Remove dots for two-phase sets.
113    pub tombstones: Vec<EntryDot>,
114}
115
116impl EntryCrdt {
117    pub(super) fn has_write_witness(&self) -> bool {
118        self.register.is_some() || !self.dots.is_empty()
119    }
120
121    /// Return the bottom floor used only to lift legacy payloads without dots.
122    pub(super) fn legacy_floor(&self) -> EntryVersion {
123        self.register.unwrap_or_default()
124    }
125}
126
127/// Bounded LWW element set used by data topic buffers.
128#[derive(Clone, Debug, Default, PartialEq, Eq)]
129pub struct DataTopicBuffer {
130    pub(super) register: Option<EntryVersion>,
131    pub(super) values: BTreeMap<Encoded, EntryDot>,
132    pub(super) removes: BTreeSet<EntryDot>,
133}
134
135impl DataTopicBuffer {
136    pub(super) fn new(
137        register: Option<EntryVersion>,
138        mut values: BTreeMap<Encoded, EntryDot>,
139        mut removes: BTreeSet<EntryDot>,
140    ) -> Self {
141        if let Some(floor) = register {
142            values.retain(|_, dot| dot.version >= floor);
143            removes.retain(|dot| dot.version >= floor);
144        }
145        values.retain(|_, dot| !removes.contains(dot));
146        Self {
147            register,
148            values,
149            removes,
150        }
151    }
152}
153
154impl JoinSemilattice for DataTopicBuffer {
155    fn join(mut self, other: Self) -> Self {
156        self.register = self.register.max(other.register);
157        self.removes.extend(other.removes);
158        for (value, dot) in other.values {
159            self.values
160                .entry(value)
161                .and_modify(|current| *current = (*current).max(dot))
162                .or_insert(dot);
163        }
164        Self::new(self.register, self.values, self.removes)
165    }
166}
167
168/// Two-phase set used by relay-message storage.
169#[derive(Clone, Debug, Default, PartialEq, Eq)]
170pub struct RelayMessageSet {
171    pub(super) adds: DataTopicBuffer,
172    pub(super) removes: BTreeSet<EntryDot>,
173}
174
175impl RelayMessageSet {
176    pub(super) fn new(mut adds: DataTopicBuffer, removes: BTreeSet<EntryDot>) -> Self {
177        let mut removes = removes;
178        removes.extend(adds.removes.iter().copied());
179        adds = DataTopicBuffer::new(adds.register, adds.values, removes.clone());
180        Self { adds, removes }
181    }
182}
183
184impl JoinSemilattice for RelayMessageSet {
185    fn join(mut self, other: Self) -> Self {
186        self.adds = self.adds.join(other.adds);
187        self.removes.extend(other.removes);
188        Self::new(self.adds, self.removes)
189    }
190}