1use serde::{Deserialize, Serialize};
6
7use crate::objects::{PreparedExactObject, ProtocolObjectContext, ProtocolObjectDomain};
8use crate::prepared_commit::PreparedStoreOperationCommit;
9use crate::remote_object::{RemoteObjectRecord, RemoteObjectRecordError};
10use crate::store_commit::{
11 ObjectHash, StoreDeviceExclusionOutcome, StoreDeviceExclusionOutcomeRef,
12 StoreDeviceExclusionProposal, StoreDeviceExclusionProposalRef,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case", deny_unknown_fields)]
17pub enum DurableStoreDeviceExclusionObject {
18 Proposal {
19 reference: StoreDeviceExclusionProposalRef,
20 value: StoreDeviceExclusionProposal,
21 prepared: PreparedExactObject,
22 },
23 Outcome {
24 reference: StoreDeviceExclusionOutcomeRef,
25 value: StoreDeviceExclusionOutcome,
26 prepared: PreparedExactObject,
27 },
28}
29
30impl DurableStoreDeviceExclusionObject {
31 fn store_root_hash(&self) -> ObjectHash {
32 match self {
33 Self::Proposal { value, .. } => value.store_root_hash,
34 Self::Outcome { value, .. } => match value {
35 StoreDeviceExclusionOutcome::Excluded(value) => value.store_root_hash,
36 StoreDeviceExclusionOutcome::Cancelled(value) => value.store_root_hash,
37 },
38 }
39 }
40
41 pub fn context(&self) -> ProtocolObjectContext {
42 let domain = match self {
43 Self::Proposal { .. } => ProtocolObjectDomain::StoreDeviceExclusionProposal,
44 Self::Outcome { .. } => ProtocolObjectDomain::StoreDeviceExclusionOutcome,
45 };
46 ProtocolObjectContext::signed_plaintext(self.store_root_hash(), domain)
47 }
48
49 pub fn semantic_prefix(&self) -> Result<&str, StoreDeviceExclusionJournalError> {
50 self.object()
51 .slot()
52 .logical_key()
53 .strip_suffix(".json")
54 .ok_or_else(|| {
55 StoreDeviceExclusionJournalError::Invalid(
56 "exclusion exact object does not use its JSON semantic path".to_string(),
57 )
58 })
59 }
60
61 pub fn operation_id(&self) -> ObjectHash {
62 match self {
63 Self::Proposal { reference, .. } => reference.proposal_hash,
64 Self::Outcome { reference, .. } => reference.outcome_hash(),
65 }
66 }
67
68 pub fn object(&self) -> &crate::objects::ExactObjectRef {
69 match self {
70 Self::Proposal { reference, .. } => &reference.object,
71 Self::Outcome { reference, .. } => reference.object(),
72 }
73 }
74
75 pub fn prepared(&self) -> &PreparedExactObject {
76 match self {
77 Self::Proposal { prepared, .. } | Self::Outcome { prepared, .. } => prepared,
78 }
79 }
80
81 pub fn semantic_bytes(&self) -> Vec<u8> {
82 match self {
83 Self::Proposal { value, .. } => value.to_bytes(),
84 Self::Outcome { value, .. } => value.to_bytes(),
85 }
86 }
87
88 fn commit_names_exact_object(&self, candidate: &PreparedStoreOperationCommit) -> bool {
89 match self {
90 Self::Proposal { reference, .. } => {
91 candidate.commit.device_exclusion_proposals() == [reference.clone()]
92 && candidate.commit.device_exclusion_outcomes().is_empty()
93 }
94 Self::Outcome { reference, .. } => {
95 candidate.commit.device_exclusion_proposals().is_empty()
96 && candidate.commit.device_exclusion_outcomes() == [reference.clone()]
97 }
98 }
99 }
100
101 pub(crate) fn remote_record(
102 &self,
103 candidate: &PreparedStoreOperationCommit,
104 ) -> Result<crate::remote_object::ClosedRemoteObject, StoreDeviceExclusionJournalError> {
105 let bytes = self.semantic_bytes();
106 let stored = self.prepared().stored_bytes();
107 match self {
108 Self::Proposal { reference, .. } => {
109 RemoteObjectRecord::candidate_activated_device_exclusion_proposal(
110 reference.clone(),
111 &bytes,
112 stored,
113 candidate.reference.clone(),
114 )
115 }
116 Self::Outcome { reference, .. } => {
117 RemoteObjectRecord::candidate_activated_device_exclusion_outcome(
118 reference.clone(),
119 &bytes,
120 stored,
121 candidate.reference.clone(),
122 )
123 }
124 }
125 .map_err(StoreDeviceExclusionJournalError::RemoteObject)
126 }
127
128 fn validate(&self) -> Result<(), StoreDeviceExclusionJournalError> {
129 if self.prepared().reference() != self.object() {
130 return Err(StoreDeviceExclusionJournalError::Invalid(
131 "prepared exclusion object differs from its exact reference".to_string(),
132 ));
133 }
134 match self {
135 Self::Proposal {
136 reference, value, ..
137 } => reference.verify_proposal(value)?,
138 Self::Outcome {
139 reference, value, ..
140 } => {
141 if reference.proposal() != value.proposal()
142 || reference.outcome_hash() != value.outcome_hash()
143 {
144 return Err(StoreDeviceExclusionJournalError::Invalid(
145 "exclusion outcome differs from its exact reference".to_string(),
146 ));
147 }
148 }
149 }
150 Ok(())
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case", deny_unknown_fields)]
156pub enum StoreDeviceExclusionCompletion {
157 Activated {
158 object: DurableStoreDeviceExclusionObject,
159 candidate: PreparedStoreOperationCommit,
160 },
161 OutcomeSlotOccupied {
162 intended: DurableStoreDeviceExclusionObject,
163 winner: DurableStoreDeviceExclusionObject,
164 },
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case", deny_unknown_fields)]
169pub enum DurableStoreDeviceExclusionOperation {
170 CandidatePrepared {
171 object: DurableStoreDeviceExclusionObject,
172 candidate: PreparedStoreOperationCommit,
173 },
174 Completed(StoreDeviceExclusionCompletion),
175}
176
177impl DurableStoreDeviceExclusionOperation {
178 pub fn prepared(
179 object: DurableStoreDeviceExclusionObject,
180 candidate: PreparedStoreOperationCommit,
181 ) -> Result<Self, StoreDeviceExclusionJournalError> {
182 let operation = Self::CandidatePrepared { object, candidate };
183 operation.validate()?;
184 Ok(operation)
185 }
186
187 pub fn activated(&self) -> Result<Self, StoreDeviceExclusionJournalError> {
188 self.validate()?;
189 let Self::CandidatePrepared { object, candidate } = self else {
190 return Err(StoreDeviceExclusionJournalError::Invalid(
191 "Store-device exclusion has no pending activation candidate".into(),
192 ));
193 };
194 Ok(Self::Completed(StoreDeviceExclusionCompletion::Activated {
195 object: object.clone(),
196 candidate: candidate.clone(),
197 }))
198 }
199
200 pub fn operation_id(&self) -> ObjectHash {
201 self.object().operation_id()
202 }
203
204 pub fn is_completed(&self) -> bool {
205 matches!(self, Self::Completed(_))
206 }
207
208 pub fn allows_transition_to(&self, next: &Self) -> bool {
209 let current_id = self.operation_id();
210 let next_id = next.operation_id();
211 if current_id != next_id {
212 return false;
213 }
214 match (self, next) {
215 (
216 Self::CandidatePrepared { object, candidate },
217 Self::CandidatePrepared {
218 object: next_object,
219 candidate: next_candidate,
220 },
221 ) => {
222 object == next_object
223 && candidate.reference == next_candidate.reference
224 && candidate.commit.to_bytes() == next_candidate.commit.to_bytes()
225 }
226 (
227 Self::CandidatePrepared { .. },
228 Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied { .. }),
229 ) => true,
230 (
231 Self::CandidatePrepared { object, candidate },
232 Self::Completed(StoreDeviceExclusionCompletion::Activated {
233 object: next_object,
234 candidate: next_candidate,
235 }),
236 ) => object == next_object && candidate.has_same_durable_activation_as(next_candidate),
237 _ => false,
238 }
239 }
240
241 pub fn object(&self) -> &DurableStoreDeviceExclusionObject {
242 match self {
243 Self::CandidatePrepared { object, .. } => object,
244 Self::Completed(StoreDeviceExclusionCompletion::Activated { object, .. }) => object,
245 Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied {
246 intended,
247 ..
248 }) => intended,
249 }
250 }
251
252 pub fn candidate(&self) -> Option<&PreparedStoreOperationCommit> {
253 match self {
254 Self::CandidatePrepared { candidate, .. } => Some(candidate),
255 Self::Completed(StoreDeviceExclusionCompletion::Activated { candidate, .. }) => {
256 Some(candidate)
257 }
258 Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied { .. }) => None,
259 }
260 }
261
262 pub fn remote_objects(
263 &self,
264 ) -> Result<Vec<crate::remote_object::ClosedRemoteObject>, StoreDeviceExclusionJournalError>
265 {
266 let candidate = self.candidate().ok_or_else(|| {
267 StoreDeviceExclusionJournalError::Invalid(
268 "Store-device exclusion has no prepared activation candidate".to_string(),
269 )
270 })?;
271 let authority = self.object().remote_record(candidate)?;
272 candidate
273 .retained_control_remote_objects(vec![authority])
274 .map_err(StoreDeviceExclusionJournalError::Outbound)
275 }
276
277 pub fn authority_remote_object(
278 &self,
279 ) -> Result<crate::remote_object::ClosedRemoteObject, StoreDeviceExclusionJournalError> {
280 let candidate = self.candidate().ok_or_else(|| {
281 StoreDeviceExclusionJournalError::Invalid(
282 "Store-device exclusion has no authority owner candidate".to_string(),
283 )
284 })?;
285 self.object().remote_record(candidate)
286 }
287
288 pub fn validate(&self) -> Result<(), StoreDeviceExclusionJournalError> {
289 self.object().validate()?;
290 let Some(candidate) = self.candidate() else {
291 if let Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied {
292 intended,
293 winner,
294 }) = self
295 {
296 winner.validate()?;
297 if !matches!(intended, DurableStoreDeviceExclusionObject::Outcome { .. })
298 || !matches!(winner, DurableStoreDeviceExclusionObject::Outcome { .. })
299 || intended.object().slot() != winner.object().slot()
300 || intended.object() == winner.object()
301 {
302 return Err(StoreDeviceExclusionJournalError::Invalid(
303 "occupied exclusion outcome slot lacks a distinct exact winner".to_string(),
304 ));
305 }
306 }
307 return Ok(());
308 };
309 candidate.reference.verify_commit(&candidate.commit)?;
310 let publication = candidate.prepared_membership_publication()?;
311 let change_matches = match (self.object(), &publication.entry.change) {
312 (
313 DurableStoreDeviceExclusionObject::Proposal { reference, .. },
314 crate::membership::StoreAuthorityChange::DeviceExclusionProposal { proposal },
315 ) => reference == proposal,
316 (
317 DurableStoreDeviceExclusionObject::Outcome { reference, .. },
318 crate::membership::StoreAuthorityChange::DeviceExclusionOutcome { outcome },
319 ) => reference == outcome,
320 _ => false,
321 };
322 if !change_matches
323 || !self.object().commit_names_exact_object(candidate)
324 || candidate.commit.acknowledgement().is_some()
325 {
326 return Err(StoreDeviceExclusionJournalError::Invalid(
327 "exclusion journal candidate does not activate its one exact object".to_string(),
328 ));
329 }
330 Ok(())
331 }
332}
333
334#[derive(Debug, thiserror::Error)]
335pub enum StoreDeviceExclusionJournalError {
336 #[error("invalid durable Store-device exclusion: {0}")]
337 Invalid(String),
338 #[error("Store-device exclusion protocol: {0}")]
339 Protocol(#[from] crate::store_commit::StoreProtocolError),
340 #[error("Store-device exclusion remote ownership: {0}")]
341 RemoteObject(#[from] RemoteObjectRecordError),
342 #[error("Store-device exclusion activation: {0}")]
343 Outbound(#[from] crate::prepared_commit::PreparedCommitError),
344 #[error("Store-device exclusion storage: {0}")]
345 Storage(#[from] crate::objects::StorageError),
346}