Skip to main content

lodestone_types/
mutation_lifecycle.rs

1//! Canonical interpretation of one exact mutation across authority layers.
2//!
3//! This is a transport-neutral derived model, not a serialized receipt or a
4//! second completion authority. Each durable owner retains its existing
5//! receipt/state and maps an exact read-back observation into this vocabulary.
6
7use std::error::Error;
8use std::fmt::{self, Display, Formatter};
9
10/// Identity that must remain byte-exact from prepare through visibility.
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub struct MutationReplayIdentity {
13    authority_binding: [u8; 32],
14    operation_identity: [u8; 32],
15    semantic_digest: [u8; 32],
16}
17
18impl MutationReplayIdentity {
19    pub fn new(
20        authority_binding: [u8; 32],
21        operation_identity: [u8; 32],
22        semantic_digest: [u8; 32],
23    ) -> Result<Self, MutationLifecycleError> {
24        if authority_binding == [0; 32] {
25            return Err(MutationLifecycleError::ZeroAuthorityBinding);
26        }
27        if operation_identity == [0; 32] {
28            return Err(MutationLifecycleError::ZeroOperationIdentity);
29        }
30        if semantic_digest == [0; 32] {
31            return Err(MutationLifecycleError::ZeroSemanticDigest);
32        }
33        Ok(Self {
34            authority_binding,
35            operation_identity,
36            semantic_digest,
37        })
38    }
39
40    #[must_use]
41    pub const fn authority_binding(self) -> [u8; 32] {
42        self.authority_binding
43    }
44
45    #[must_use]
46    pub const fn operation_identity(self) -> [u8; 32] {
47        self.operation_identity
48    }
49
50    #[must_use]
51    pub const fn semantic_digest(self) -> [u8; 32] {
52        self.semantic_digest
53    }
54}
55
56/// The four monotonic lifecycle stages shared by mutation authorities.
57#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
58pub enum MutationLifecycleStage {
59    NotSubmitted,
60    Prepared,
61    Committed,
62    Visible,
63}
64
65/// Exact meaning of a prepared observation.
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum PreparedMutationDisposition {
68    /// The durable prepare proves that replaying the same identity is safe.
69    SafelyReplayable,
70    /// Submission is in progress; callers may observe but must not resubmit.
71    StillPending,
72}
73
74/// Closed result of resolving a possibly submitted mutation.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum SubmittedIndeterminateResolution {
77    Committed { visible: bool },
78    SafelyReplayable,
79    StillPending,
80    ProvenNotSubmitted,
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84enum MutationLifecycleState {
85    NotSubmitted,
86    Prepared(PreparedMutationDisposition),
87    Committed,
88    Visible,
89}
90
91/// One exact authority observation in the canonical lifecycle.
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub struct MutationLifecycleObservation {
94    replay_identity: MutationReplayIdentity,
95    state: MutationLifecycleState,
96}
97
98impl MutationLifecycleObservation {
99    #[must_use]
100    pub const fn proven_not_submitted(replay_identity: MutationReplayIdentity) -> Self {
101        Self {
102            replay_identity,
103            state: MutationLifecycleState::NotSubmitted,
104        }
105    }
106
107    #[must_use]
108    pub const fn prepared(replay_identity: MutationReplayIdentity, disposition: PreparedMutationDisposition) -> Self {
109        Self {
110            replay_identity,
111            state: MutationLifecycleState::Prepared(disposition),
112        }
113    }
114
115    #[must_use]
116    pub const fn committed(replay_identity: MutationReplayIdentity) -> Self {
117        Self {
118            replay_identity,
119            state: MutationLifecycleState::Committed,
120        }
121    }
122
123    #[must_use]
124    pub const fn visible(replay_identity: MutationReplayIdentity) -> Self {
125        Self {
126            replay_identity,
127            state: MutationLifecycleState::Visible,
128        }
129    }
130
131    #[must_use]
132    pub const fn replay_identity(self) -> MutationReplayIdentity {
133        self.replay_identity
134    }
135
136    #[must_use]
137    pub const fn stage(self) -> MutationLifecycleStage {
138        match self.state {
139            MutationLifecycleState::NotSubmitted => MutationLifecycleStage::NotSubmitted,
140            MutationLifecycleState::Prepared(_) => MutationLifecycleStage::Prepared,
141            MutationLifecycleState::Committed => MutationLifecycleStage::Committed,
142            MutationLifecycleState::Visible => MutationLifecycleStage::Visible,
143        }
144    }
145
146    #[must_use]
147    pub const fn prepared_disposition(self) -> Option<PreparedMutationDisposition> {
148        match self.state {
149            MutationLifecycleState::Prepared(disposition) => Some(disposition),
150            MutationLifecycleState::NotSubmitted
151            | MutationLifecycleState::Committed
152            | MutationLifecycleState::Visible => None,
153        }
154    }
155
156    #[must_use]
157    pub const fn resolve_submitted_indeterminate(self) -> SubmittedIndeterminateResolution {
158        match self.state {
159            MutationLifecycleState::NotSubmitted => SubmittedIndeterminateResolution::ProvenNotSubmitted,
160            MutationLifecycleState::Prepared(PreparedMutationDisposition::SafelyReplayable) => {
161                SubmittedIndeterminateResolution::SafelyReplayable
162            }
163            MutationLifecycleState::Prepared(PreparedMutationDisposition::StillPending) => {
164                SubmittedIndeterminateResolution::StillPending
165            }
166            MutationLifecycleState::Committed => SubmittedIndeterminateResolution::Committed { visible: false },
167            MutationLifecycleState::Visible => SubmittedIndeterminateResolution::Committed { visible: true },
168        }
169    }
170
171    pub fn require_monotonic_successor(self, successor: Self) -> Result<(), MutationLifecycleError> {
172        if self.replay_identity != successor.replay_identity {
173            return Err(MutationLifecycleError::ReplayIdentityChanged);
174        }
175        let stage = self.stage();
176        let successor_stage = successor.stage();
177        if successor_stage < stage {
178            return Err(MutationLifecycleError::StageRegressed {
179                from: stage,
180                to: successor_stage,
181            });
182        }
183        Ok(())
184    }
185}
186
187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
188pub enum MutationLifecycleError {
189    ZeroAuthorityBinding,
190    ZeroOperationIdentity,
191    ZeroSemanticDigest,
192    ReplayIdentityChanged,
193    StageRegressed {
194        from: MutationLifecycleStage,
195        to: MutationLifecycleStage,
196    },
197    OutcomeEvidenceRetired,
198}
199
200impl Display for MutationLifecycleError {
201    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
202        match self {
203            Self::ZeroAuthorityBinding => formatter.write_str("mutation authority binding cannot be zero"),
204            Self::ZeroOperationIdentity => formatter.write_str("mutation operation identity cannot be zero"),
205            Self::ZeroSemanticDigest => formatter.write_str("mutation semantic digest cannot be zero"),
206            Self::ReplayIdentityChanged => {
207                formatter.write_str("mutation replay identity changed across lifecycle observations")
208            }
209            Self::StageRegressed { from, to } => {
210                write!(formatter, "mutation lifecycle regressed from {from:?} to {to:?}")
211            }
212            Self::OutcomeEvidenceRetired => {
213                formatter.write_str("mutation outcome evidence retired before deterministic resolution")
214            }
215        }
216    }
217}
218
219impl Error for MutationLifecycleError {}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    fn replay_identity(value: u8) -> Result<MutationReplayIdentity, MutationLifecycleError> {
226        MutationReplayIdentity::new(
227            [value; 32],
228            [value.saturating_add(1); 32],
229            [value.saturating_add(2); 32],
230        )
231    }
232
233    #[test]
234    fn submitted_indeterminate_has_exactly_four_resolution_classes() -> Result<(), Box<dyn Error>> {
235        let identity = replay_identity(1)?;
236        assert_eq!(
237            MutationLifecycleObservation::proven_not_submitted(identity).resolve_submitted_indeterminate(),
238            SubmittedIndeterminateResolution::ProvenNotSubmitted
239        );
240        assert_eq!(
241            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::SafelyReplayable)
242                .resolve_submitted_indeterminate(),
243            SubmittedIndeterminateResolution::SafelyReplayable
244        );
245        assert_eq!(
246            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::StillPending)
247                .resolve_submitted_indeterminate(),
248            SubmittedIndeterminateResolution::StillPending
249        );
250        assert_eq!(
251            MutationLifecycleObservation::committed(identity).resolve_submitted_indeterminate(),
252            SubmittedIndeterminateResolution::Committed { visible: false }
253        );
254        assert_eq!(
255            MutationLifecycleObservation::visible(identity).resolve_submitted_indeterminate(),
256            SubmittedIndeterminateResolution::Committed { visible: true }
257        );
258        Ok(())
259    }
260
261    #[test]
262    fn lifecycle_is_monotonic_and_identity_exact() -> Result<(), Box<dyn Error>> {
263        let identity = replay_identity(7)?;
264        let not_submitted = MutationLifecycleObservation::proven_not_submitted(identity);
265        let replayable =
266            MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::SafelyReplayable);
267        let pending = MutationLifecycleObservation::prepared(identity, PreparedMutationDisposition::StillPending);
268        let committed = MutationLifecycleObservation::committed(identity);
269        let visible = MutationLifecycleObservation::visible(identity);
270        not_submitted.require_monotonic_successor(replayable)?;
271        replayable.require_monotonic_successor(pending)?;
272        pending.require_monotonic_successor(replayable)?;
273        pending.require_monotonic_successor(committed)?;
274        committed.require_monotonic_successor(visible)?;
275        assert!(matches!(
276            visible.require_monotonic_successor(committed),
277            Err(MutationLifecycleError::StageRegressed { .. })
278        ));
279        assert_eq!(
280            committed.require_monotonic_successor(MutationLifecycleObservation::visible(replay_identity(8)?,)),
281            Err(MutationLifecycleError::ReplayIdentityChanged)
282        );
283        Ok(())
284    }
285
286    #[test]
287    fn replay_identity_refuses_ambiguous_components() {
288        assert_eq!(
289            MutationReplayIdentity::new([0; 32], [1; 32], [2; 32]),
290            Err(MutationLifecycleError::ZeroAuthorityBinding)
291        );
292        assert_eq!(
293            MutationReplayIdentity::new([1; 32], [0; 32], [2; 32]),
294            Err(MutationLifecycleError::ZeroOperationIdentity)
295        );
296        assert_eq!(
297            MutationReplayIdentity::new([1; 32], [2; 32], [0; 32]),
298            Err(MutationLifecycleError::ZeroSemanticDigest)
299        );
300    }
301}