1use serde::{Deserialize, Serialize};
6
7use crate::objects::{PreparedExactObject, ProtocolObjectContext, ProtocolObjectDomain};
8use crate::prepared_commit::PreparedStoreOperationCommit;
9use crate::remote_object::{
10 CandidateNonactivation, CandidateNonactivationProof, RemoteObjectRecord,
11 RemoteObjectRecordError,
12};
13use crate::store_commit::{
14 ObjectHash, StoreDeviceExclusionOutcome, StoreDeviceExclusionOutcomeRef,
15 StoreDeviceExclusionProposal, StoreDeviceExclusionProposalRef,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case", deny_unknown_fields)]
20pub enum DurableStoreDeviceExclusionObject {
21 Proposal {
22 reference: StoreDeviceExclusionProposalRef,
23 value: StoreDeviceExclusionProposal,
24 prepared: PreparedExactObject,
25 },
26 Outcome {
27 reference: StoreDeviceExclusionOutcomeRef,
28 value: StoreDeviceExclusionOutcome,
29 prepared: PreparedExactObject,
30 },
31}
32
33impl DurableStoreDeviceExclusionObject {
34 fn store_root_hash(&self) -> ObjectHash {
35 match self {
36 Self::Proposal { value, .. } => value.store_root_hash,
37 Self::Outcome { value, .. } => match value {
38 StoreDeviceExclusionOutcome::Excluded(value) => value.store_root_hash,
39 StoreDeviceExclusionOutcome::Cancelled(value) => value.store_root_hash,
40 },
41 }
42 }
43
44 pub fn context(&self) -> ProtocolObjectContext {
45 let domain = match self {
46 Self::Proposal { .. } => ProtocolObjectDomain::StoreDeviceExclusionProposal,
47 Self::Outcome { .. } => ProtocolObjectDomain::StoreDeviceExclusionOutcome,
48 };
49 ProtocolObjectContext::signed_plaintext(self.store_root_hash(), domain)
50 }
51
52 pub fn semantic_prefix(&self) -> Result<&str, StoreDeviceExclusionJournalError> {
53 self.object()
54 .slot()
55 .logical_key()
56 .strip_suffix(".json")
57 .ok_or_else(|| {
58 StoreDeviceExclusionJournalError::Invalid(
59 "exclusion exact object does not use its JSON semantic path".to_string(),
60 )
61 })
62 }
63
64 pub fn operation_id(&self) -> ObjectHash {
65 match self {
66 Self::Proposal { reference, .. } => reference.proposal_hash,
67 Self::Outcome { reference, .. } => reference.outcome_hash(),
68 }
69 }
70
71 pub fn object(&self) -> &crate::objects::ExactObjectRef {
72 match self {
73 Self::Proposal { reference, .. } => &reference.object,
74 Self::Outcome { reference, .. } => reference.object(),
75 }
76 }
77
78 pub fn prepared(&self) -> &PreparedExactObject {
79 match self {
80 Self::Proposal { prepared, .. } | Self::Outcome { prepared, .. } => prepared,
81 }
82 }
83
84 pub fn semantic_bytes(&self) -> Vec<u8> {
85 match self {
86 Self::Proposal { value, .. } => value.to_bytes(),
87 Self::Outcome { value, .. } => value.to_bytes(),
88 }
89 }
90
91 fn commit_names_exact_object(&self, candidate: &PreparedStoreOperationCommit) -> bool {
92 match self {
93 Self::Proposal { reference, .. } => {
94 candidate.commit.device_exclusion_proposals() == [reference.clone()]
95 && candidate.commit.device_exclusion_outcomes().is_empty()
96 }
97 Self::Outcome { reference, .. } => {
98 candidate.commit.device_exclusion_proposals().is_empty()
99 && candidate.commit.device_exclusion_outcomes() == [reference.clone()]
100 }
101 }
102 }
103
104 pub(crate) fn remote_record(
105 &self,
106 candidate: &PreparedStoreOperationCommit,
107 ) -> Result<crate::remote_object::ClosedRemoteObject, StoreDeviceExclusionJournalError> {
108 let bytes = self.semantic_bytes();
109 let stored = self.prepared().stored_bytes();
110 match self {
111 Self::Proposal { reference, .. } => {
112 RemoteObjectRecord::candidate_activated_device_exclusion_proposal(
113 reference.clone(),
114 &bytes,
115 stored,
116 candidate.reference.clone(),
117 )
118 }
119 Self::Outcome { reference, .. } => {
120 RemoteObjectRecord::candidate_activated_device_exclusion_outcome(
121 reference.clone(),
122 &bytes,
123 stored,
124 candidate.reference.clone(),
125 )
126 }
127 }
128 .map_err(StoreDeviceExclusionJournalError::RemoteObject)
129 }
130
131 fn validate(&self) -> Result<(), StoreDeviceExclusionJournalError> {
132 if self.prepared().reference() != self.object() {
133 return Err(StoreDeviceExclusionJournalError::Invalid(
134 "prepared exclusion object differs from its exact reference".to_string(),
135 ));
136 }
137 match self {
138 Self::Proposal {
139 reference, value, ..
140 } => reference.verify_proposal(value)?,
141 Self::Outcome {
142 reference, value, ..
143 } => {
144 if reference.proposal() != value.proposal()
145 || reference.outcome_hash() != value.outcome_hash()
146 {
147 return Err(StoreDeviceExclusionJournalError::Invalid(
148 "exclusion outcome differs from its exact reference".to_string(),
149 ));
150 }
151 }
152 }
153 Ok(())
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case", deny_unknown_fields)]
159pub enum StoreDeviceExclusionCompletion {
160 Activated {
161 object: DurableStoreDeviceExclusionObject,
162 candidate: PreparedStoreOperationCommit,
163 },
164 OutcomeSlotOccupied {
165 intended: DurableStoreDeviceExclusionObject,
166 winner: DurableStoreDeviceExclusionObject,
167 },
168 CandidateNonactivated {
169 object: DurableStoreDeviceExclusionObject,
170 candidate: PreparedStoreOperationCommit,
171 proof: CandidateNonactivationProof,
172 },
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "snake_case", deny_unknown_fields)]
177pub enum DurableStoreDeviceExclusionOperation {
178 CandidatePrepared {
179 object: DurableStoreDeviceExclusionObject,
180 candidate: PreparedStoreOperationCommit,
181 },
182 CandidateNonactivating {
183 object: DurableStoreDeviceExclusionObject,
184 candidate: PreparedStoreOperationCommit,
185 proof: CandidateNonactivationProof,
186 },
187 ReplacingCandidate {
188 object: DurableStoreDeviceExclusionObject,
189 candidate: PreparedStoreOperationCommit,
190 losing: StoreDeviceExclusionCandidateLoss,
191 },
192 Completed(StoreDeviceExclusionCompletion),
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(deny_unknown_fields)]
197pub struct StoreDeviceExclusionCandidateLoss {
198 pub candidate: PreparedStoreOperationCommit,
199 pub proof: CandidateNonactivationProof,
200}
201
202impl DurableStoreDeviceExclusionOperation {
203 pub fn prepared(
204 object: DurableStoreDeviceExclusionObject,
205 candidate: PreparedStoreOperationCommit,
206 ) -> Result<Self, StoreDeviceExclusionJournalError> {
207 let operation = Self::CandidatePrepared { object, candidate };
208 operation.validate()?;
209 Ok(operation)
210 }
211
212 pub fn operation_id(&self) -> ObjectHash {
213 self.object().operation_id()
214 }
215
216 pub fn is_completed(&self) -> bool {
217 matches!(self, Self::Completed(_))
218 }
219
220 pub fn allows_transition_to(&self, next: &Self) -> bool {
221 let current_id = self.operation_id();
222 let next_id = next.operation_id();
223 if current_id != next_id {
224 return false;
225 }
226 match (self, next) {
227 (
228 Self::CandidatePrepared { object, candidate },
229 Self::CandidatePrepared {
230 object: next_object,
231 candidate: next_candidate,
232 },
233 ) => {
234 object == next_object
235 && candidate.reference == next_candidate.reference
236 && candidate.commit.to_bytes() == next_candidate.commit.to_bytes()
237 }
238 (Self::CandidatePrepared { .. }, Self::CandidateNonactivating { .. }) => true,
239 (Self::CandidatePrepared { .. }, Self::ReplacingCandidate { .. }) => true,
240 (
241 Self::CandidatePrepared { .. },
242 Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied { .. }),
243 ) => true,
244 (
245 Self::CandidatePrepared { object, candidate },
246 Self::Completed(StoreDeviceExclusionCompletion::Activated {
247 object: next_object,
248 candidate: next_candidate,
249 }),
250 ) => object == next_object && candidate.has_same_durable_activation_as(next_candidate),
251 (
252 Self::ReplacingCandidate {
253 object, candidate, ..
254 },
255 Self::CandidatePrepared {
256 object: next_object,
257 candidate: next_candidate,
258 },
259 ) => object == next_object && candidate.has_same_durable_activation_as(next_candidate),
260 (
261 Self::CandidateNonactivating {
262 object,
263 candidate,
264 proof,
265 },
266 Self::Completed(StoreDeviceExclusionCompletion::CandidateNonactivated {
267 object: next_object,
268 candidate: next_candidate,
269 proof: next_proof,
270 }),
271 ) => {
272 object == next_object
273 && candidate.has_same_durable_activation_as(next_candidate)
274 && proof == next_proof
275 }
276 _ => false,
277 }
278 }
279
280 pub fn object(&self) -> &DurableStoreDeviceExclusionObject {
281 match self {
282 Self::CandidatePrepared { object, .. }
283 | Self::CandidateNonactivating { object, .. }
284 | Self::ReplacingCandidate { object, .. } => object,
285 Self::Completed(StoreDeviceExclusionCompletion::Activated { object, .. }) => object,
286 Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied {
287 intended,
288 ..
289 }) => intended,
290 Self::Completed(StoreDeviceExclusionCompletion::CandidateNonactivated {
291 object,
292 ..
293 }) => object,
294 }
295 }
296
297 pub fn candidate(&self) -> Option<&PreparedStoreOperationCommit> {
298 match self {
299 Self::CandidatePrepared { candidate, .. }
300 | Self::CandidateNonactivating { candidate, .. }
301 | Self::ReplacingCandidate { candidate, .. } => Some(candidate),
302 Self::Completed(StoreDeviceExclusionCompletion::Activated { candidate, .. }) => {
303 Some(candidate)
304 }
305 Self::Completed(StoreDeviceExclusionCompletion::CandidateNonactivated {
306 candidate,
307 ..
308 }) => Some(candidate),
309 Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied { .. }) => None,
310 }
311 }
312
313 pub fn remote_objects(
314 &self,
315 ) -> Result<Vec<crate::remote_object::ClosedRemoteObject>, StoreDeviceExclusionJournalError>
316 {
317 let candidate = self.candidate().ok_or_else(|| {
318 StoreDeviceExclusionJournalError::Invalid(
319 "Store-device exclusion has no prepared activation candidate".to_string(),
320 )
321 })?;
322 let authority = self.object().remote_record(candidate)?;
323 candidate
324 .retained_authority_remote_objects(vec![authority])
325 .map_err(StoreDeviceExclusionJournalError::Outbound)
326 }
327
328 pub fn authority_remote_object(
329 &self,
330 ) -> Result<crate::remote_object::ClosedRemoteObject, StoreDeviceExclusionJournalError> {
331 let candidate = self.candidate().ok_or_else(|| {
332 StoreDeviceExclusionJournalError::Invalid(
333 "Store-device exclusion has no authority owner candidate".to_string(),
334 )
335 })?;
336 self.object().remote_record(candidate)
337 }
338
339 pub fn begin_nonactivation(
340 &self,
341 nonactivation: CandidateNonactivation,
342 ) -> Result<(Self, CandidateNonactivation), StoreDeviceExclusionJournalError> {
343 let Self::CandidatePrepared { object, candidate } = self else {
344 return Err(StoreDeviceExclusionJournalError::Invalid(
345 "only a prepared exclusion candidate can become nonactivating".to_string(),
346 ));
347 };
348 if nonactivation
349 .reference()
350 .map_err(StoreDeviceExclusionJournalError::RemoteObject)?
351 != candidate.reference
352 {
353 return Err(StoreDeviceExclusionJournalError::Invalid(
354 "exclusion nonactivation names another candidate".to_string(),
355 ));
356 }
357 let proof = nonactivation.proof().clone();
358 let operation = Self::CandidateNonactivating {
359 object: object.clone(),
360 candidate: candidate.clone(),
361 proof,
362 };
363 operation.validate()?;
364 Ok((operation, nonactivation))
365 }
366
367 pub fn begin_replacement(
368 &self,
369 replacement: PreparedStoreOperationCommit,
370 nonactivation: CandidateNonactivation,
371 ) -> Result<(Self, CandidateNonactivation), StoreDeviceExclusionJournalError> {
372 let Self::CandidatePrepared { object, candidate } = self else {
373 return Err(StoreDeviceExclusionJournalError::Invalid(
374 "only a prepared exclusion candidate can be replaced".to_string(),
375 ));
376 };
377 if !matches!(object, DurableStoreDeviceExclusionObject::Outcome { .. }) {
378 return Err(StoreDeviceExclusionJournalError::Invalid(
379 "an exclusion proposal cannot move to another predecessor".to_string(),
380 ));
381 }
382 if nonactivation
383 .reference()
384 .map_err(StoreDeviceExclusionJournalError::RemoteObject)?
385 != candidate.reference
386 {
387 return Err(StoreDeviceExclusionJournalError::Invalid(
388 "replacement exclusion nonactivation names another candidate".to_string(),
389 ));
390 }
391 let proof = nonactivation.proof().clone();
392 let operation = Self::ReplacingCandidate {
393 object: object.clone(),
394 candidate: replacement,
395 losing: StoreDeviceExclusionCandidateLoss {
396 candidate: candidate.clone(),
397 proof,
398 },
399 };
400 operation.validate()?;
401 Ok((operation, nonactivation))
402 }
403
404 pub fn validate(&self) -> Result<(), StoreDeviceExclusionJournalError> {
405 self.object().validate()?;
406 let Some(candidate) = self.candidate() else {
407 if let Self::Completed(StoreDeviceExclusionCompletion::OutcomeSlotOccupied {
408 intended,
409 winner,
410 }) = self
411 {
412 winner.validate()?;
413 if !matches!(intended, DurableStoreDeviceExclusionObject::Outcome { .. })
414 || !matches!(winner, DurableStoreDeviceExclusionObject::Outcome { .. })
415 || intended.object().slot() != winner.object().slot()
416 || intended.object() == winner.object()
417 {
418 return Err(StoreDeviceExclusionJournalError::Invalid(
419 "occupied exclusion outcome slot lacks a distinct exact winner".to_string(),
420 ));
421 }
422 }
423 return Ok(());
424 };
425 candidate.reference.verify_commit(&candidate.commit)?;
426 if !self.object().commit_names_exact_object(candidate)
427 || candidate.commit.acknowledgement().is_some()
428 {
429 return Err(StoreDeviceExclusionJournalError::Invalid(
430 "exclusion journal candidate does not activate its one exact object".to_string(),
431 ));
432 }
433 if let Self::CandidateNonactivating {
434 candidate, proof, ..
435 }
436 | Self::Completed(StoreDeviceExclusionCompletion::CandidateNonactivated {
437 candidate,
438 proof,
439 ..
440 }) = self
441 {
442 CandidateNonactivation::validate_durable_shape(
443 &candidate.reference,
444 &candidate.commit,
445 proof.clone(),
446 )
447 .map_err(StoreDeviceExclusionJournalError::RemoteObject)?;
448 }
449 if let Self::ReplacingCandidate {
450 object,
451 candidate,
452 losing,
453 } = self
454 {
455 if !matches!(object, DurableStoreDeviceExclusionObject::Outcome { .. })
456 || candidate.reference == losing.candidate.reference
457 || !object.commit_names_exact_object(candidate)
458 || !object.commit_names_exact_object(&losing.candidate)
459 {
460 return Err(StoreDeviceExclusionJournalError::Invalid(
461 "replacement exclusion candidate changes its exact outcome".to_string(),
462 ));
463 }
464 CandidateNonactivation::validate_durable_shape(
465 &losing.candidate.reference,
466 &losing.candidate.commit,
467 losing.proof.clone(),
468 )
469 .map_err(StoreDeviceExclusionJournalError::RemoteObject)?;
470 }
471 Ok(())
472 }
473}
474
475#[derive(Debug, thiserror::Error)]
476pub enum StoreDeviceExclusionJournalError {
477 #[error("invalid durable Store-device exclusion: {0}")]
478 Invalid(String),
479 #[error("Store-device exclusion protocol: {0}")]
480 Protocol(#[from] crate::store_commit::StoreProtocolError),
481 #[error("Store-device exclusion remote ownership: {0}")]
482 RemoteObject(#[from] RemoteObjectRecordError),
483 #[error("Store-device exclusion activation: {0}")]
484 Outbound(#[from] crate::prepared_commit::PreparedCommitError),
485 #[error("Store-device exclusion storage: {0}")]
486 Storage(#[from] crate::objects::StorageError),
487}