1use std::error::Error;
8use std::fmt::{self, Display, Formatter};
9
10#[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#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
58pub enum MutationLifecycleStage {
59 NotSubmitted,
60 Prepared,
61 Committed,
62 Visible,
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum PreparedMutationDisposition {
68 SafelyReplayable,
70 StillPending,
72}
73
74#[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#[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}