1use std::future::Future;
2use std::pin::Pin;
3
4use rusqlite::{Connection, OptionalExtension};
5
6use super::*;
7use crate::store::StoreSession;
8use crate::{mark_remote_object_uploaded_on, update_remote_object_on};
9use coven_protocol::device_exclusion_journal::{
10 DurableStoreDeviceExclusionObject, DurableStoreDeviceExclusionOperation,
11 StoreDeviceExclusionCompletion, StoreDeviceExclusionJournalError,
12};
13use coven_protocol::remote_object::{
14 remote_object_id, ClosedRemoteObject, RemoteObjectRecord, RetainedAuthorityObjectState,
15};
16use coven_protocol::store_commit::ObjectHash;
17
18pub(crate) fn store_device_exclusion_journal_error(
19 error: StoreDeviceExclusionJournalError,
20) -> DbError {
21 DbError::from(error)
22}
23
24pub(crate) fn parse_store_device_exclusion_operation(
25 operation_id: ObjectHash,
26 raw: &str,
27) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
28 let operation: DurableStoreDeviceExclusionOperation =
29 serde_json::from_str(raw).map_err(|error| {
30 DbError::context(
31 format!(
32 "Store-device exclusion operation {operation_id} has invalid durable state"
33 ),
34 error,
35 )
36 })?;
37 operation
38 .validate()
39 .map_err(store_device_exclusion_journal_error)?;
40 if operation.operation_id() != operation_id {
41 return Err(DbError::Message(format!(
42 "Store-device exclusion operation key {operation_id} differs from its signed object {}",
43 operation.operation_id()
44 )));
45 }
46 Ok(operation)
47}
48
49pub(crate) fn load_store_device_exclusion_on(
50 conn: &Connection,
51 operation_id: ObjectHash,
52) -> Result<Option<DurableStoreDeviceExclusionOperation>, DbError> {
53 conn.query_row(
54 "SELECT state FROM outbound_store_device_exclusion WHERE operation_id = ?1",
55 [operation_id.to_string()],
56 |row| row.get::<_, String>(0),
57 )
58 .optional()
59 .map_err(DbError::from)?
60 .map(|raw| parse_store_device_exclusion_operation(operation_id, &raw))
61 .transpose()
62}
63
64pub(crate) fn load_active_store_device_exclusion_on(
65 conn: &Connection,
66) -> Result<Option<DurableStoreDeviceExclusionOperation>, DbError> {
67 conn.query_row(
68 "SELECT operation_id, state FROM outbound_store_device_exclusion WHERE active_key = 1",
69 [],
70 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
71 )
72 .optional()
73 .map_err(DbError::from)?
74 .map(|(raw_id, raw)| {
75 let operation_id = raw_id
76 .parse::<ObjectHash>()
77 .map_err(|error| DbError::context("Store-device exclusion operation id", error))?;
78 let operation = parse_store_device_exclusion_operation(operation_id, &raw)?;
79 if operation.is_completed() {
80 return Err(DbError::Message(
81 "completed Store-device exclusion remains active".to_string(),
82 ));
83 }
84 Ok(operation)
85 })
86 .transpose()
87}
88
89pub(crate) fn insert_store_device_exclusion_on(
90 conn: &Connection,
91 operation: &DurableStoreDeviceExclusionOperation,
92 active: bool,
93) -> Result<(), DbError> {
94 operation
95 .validate()
96 .map_err(store_device_exclusion_journal_error)?;
97 if active == operation.is_completed() {
98 return Err(DbError::Message(
99 "Store-device exclusion active marker differs from its closed state".to_string(),
100 ));
101 }
102 let encoded = serde_json::to_string(operation)
103 .map_err(|error| DbError::context("serialize Store-device exclusion operation", error))?;
104 conn.execute(
105 "INSERT INTO outbound_store_device_exclusion (operation_id, active_key, state)
106 VALUES (?1, ?2, ?3)",
107 rusqlite::params![
108 operation.operation_id().to_string(),
109 active.then_some(1_i64),
110 encoded,
111 ],
112 )
113 .map(|_| ())
114 .map_err(DbError::from)
115}
116
117pub(crate) fn require_store_device_exclusion_transition_on(
118 conn: &Connection,
119 expected: &DurableStoreDeviceExclusionOperation,
120 next: &DurableStoreDeviceExclusionOperation,
121) -> Result<(), DbError> {
122 if !expected.allows_transition_to(next) {
123 return Err(DbError::Message(
124 "invalid Store-device exclusion journal transition".to_string(),
125 ));
126 }
127 let expected_state = serde_json::to_string(expected)
128 .map_err(|error| DbError::context("serialize expected Store-device exclusion", error))?;
129 let current = conn
130 .query_row(
131 "SELECT state FROM outbound_store_device_exclusion WHERE operation_id = ?1",
132 [expected.operation_id().to_string()],
133 |row| row.get::<_, String>(0),
134 )
135 .optional()
136 .map_err(DbError::from)?
137 .ok_or_else(|| {
138 DbError::Message("Store-device exclusion journal disappeared".to_string())
139 })?;
140 if current != expected_state {
141 return Err(DbError::Message(
142 "Store-device exclusion journal changed during transition".to_string(),
143 ));
144 }
145 Ok(())
146}
147
148pub(crate) fn update_store_device_exclusion_on(
149 conn: &Connection,
150 expected: &DurableStoreDeviceExclusionOperation,
151 next: &DurableStoreDeviceExclusionOperation,
152 active: bool,
153) -> Result<(), DbError> {
154 require_store_device_exclusion_transition_on(conn, expected, next)?;
155 if active == next.is_completed() {
156 return Err(DbError::Message(
157 "Store-device exclusion active marker differs from its next state".to_string(),
158 ));
159 }
160 let expected_state = serde_json::to_string(expected)
161 .map_err(|error| DbError::context("serialize expected Store-device exclusion", error))?;
162 let next_state = serde_json::to_string(next)
163 .map_err(|error| DbError::context("serialize next Store-device exclusion", error))?;
164 let updated = conn
165 .execute(
166 "UPDATE outbound_store_device_exclusion
167 SET active_key = ?3, state = ?4
168 WHERE operation_id = ?1 AND state = ?2",
169 rusqlite::params![
170 expected.operation_id().to_string(),
171 expected_state,
172 active.then_some(1_i64),
173 next_state,
174 ],
175 )
176 .map_err(DbError::from)?;
177 if updated != 1 {
178 return Err(DbError::Message(
179 "Store-device exclusion journal disappeared during transition".to_string(),
180 ));
181 }
182 Ok(())
183}
184
185impl StoreSession<'_> {
186 fn begin_outbound_store_device_exclusion(
187 &mut self,
188 operation: DurableStoreDeviceExclusionOperation,
189 remotes: Vec<ClosedRemoteObject>,
190 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
191 let conn = self.conn;
192 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
193 if let Some(active) = load_active_store_device_exclusion_on(&tx)? {
194 if active.operation_id() != operation.operation_id() {
195 return Err(DbError::Message(format!(
196 "Store-device exclusion operation {} remains active",
197 active.operation_id()
198 )));
199 }
200 return Ok(active);
201 }
202 let operation_id = operation.operation_id();
203 if let Some(existing) = load_store_device_exclusion_on(&tx, operation_id)? {
204 if existing != operation || !existing.is_completed() {
205 return Err(DbError::Message(format!(
206 "Store-device exclusion operation {operation_id} already has different durable state"
207 )));
208 }
209 return Ok(existing);
210 }
211 for remote in &remotes {
212 persist_exact_remote_object_on(
213 &tx,
214 self.store_dir,
215 remote,
216 "Store-device exclusion candidate object",
217 )?;
218 }
219 insert_store_device_exclusion_on(&tx, &operation, true)?;
220 tx.commit().map_err(DbError::from)?;
221 Ok(operation)
222 }
223
224 fn active_outbound_store_device_exclusion(
225 &mut self,
226 ) -> Result<Option<DurableStoreDeviceExclusionOperation>, DbError> {
227 load_active_store_device_exclusion_on(self.conn)
228 }
229
230 fn replace_outbound_store_device_exclusion_candidate(
231 &mut self,
232 expected: DurableStoreDeviceExclusionOperation,
233 next: DurableStoreDeviceExclusionOperation,
234 candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
235 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
236 let conn = self.conn;
237 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
238 require_store_device_exclusion_transition_on(&tx, &expected, &next)?;
239 let next_candidate = next.candidate().expect("validated candidate state");
240 match (candidate.head_ref(), next_candidate.head_ref()) {
241 (current, replacement) if current != replacement => {
242 let (winner, prepared) = next_candidate.publication();
243 replace_prepared_merge_head_remote_on(
244 &tx,
245 self.store_dir,
246 ¤t.object,
247 winner,
248 prepared,
249 &candidate.reference,
250 )?;
251 }
252 _ => {}
253 }
254 update_store_device_exclusion_on(&tx, &expected, &next, true)?;
255 tx.commit().map_err(DbError::from)?;
256 Ok(next)
257 }
258
259 fn complete_outbound_store_device_exclusion_activation(
260 &mut self,
261 expected: Box<DurableStoreDeviceExclusionOperation>,
262 next: Box<DurableStoreDeviceExclusionOperation>,
263 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
264 let conn = self.conn;
265 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
266 require_store_device_exclusion_transition_on(&tx, expected.as_ref(), next.as_ref())?;
267 let candidate = expected
268 .candidate()
269 .expect("candidate-prepared exclusion has a candidate");
270 let stream = candidate.reference.coord.stream_id.to_string();
271 if crate::store::materialized_commit_index::materialized_commit_ref_on(
272 &tx,
273 &stream,
274 candidate.reference.coord.sequence(),
275 )? != Some(candidate.reference.clone())
276 {
277 return Err(DbError::Message(
278 "Store-device exclusion completion is not materialized at its exact position"
279 .to_string(),
280 ));
281 }
282 update_store_device_exclusion_on(&tx, expected.as_ref(), next.as_ref(), false)?;
283 tx.commit().map_err(DbError::from)?;
284 Ok(*next)
285 }
286
287 fn complete_outbound_store_device_exclusion_slot_loss(
288 &mut self,
289 expected: DurableStoreDeviceExclusionOperation,
290 next: DurableStoreDeviceExclusionOperation,
291 remotes: Vec<ClosedRemoteObject>,
292 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
293 let conn = self.conn;
294 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
295 require_store_device_exclusion_transition_on(&tx, &expected, &next)?;
296 for remote in &remotes {
297 let object_id = remote.object_id();
298 let current = load_remote_object_on(&tx, object_id)?;
299 let unuploaded = matches!(
300 ¤t,
301 RemoteObjectRecord::CandidateCommit(record)
302 if matches!(record.state, coven_protocol::remote_object::CandidateCommitState::Prepared)
303 ) || matches!(
304 ¤t,
305 RemoteObjectRecord::RetainedAuthority(record)
306 if matches!(
307 record.state,
308 coven_protocol::remote_object::RetainedAuthorityObjectState::Prepared { .. }
309 )
310 );
311 if current != **remote || !unuploaded {
312 return Err(DbError::Message(format!(
313 "outcome-slot loss cannot discard uploaded exclusion object {object_id}"
314 )));
315 }
316 if !crate::remote_object_records::delete_remote_object_on(&tx, object_id)? {
317 return Err(DbError::Message(format!(
318 "unuploaded exclusion object {object_id} disappeared during slot resolution"
319 )));
320 }
321 }
322 update_store_device_exclusion_on(&tx, &expected, &next, false)?;
323 tx.commit().map_err(DbError::from)?;
324 Ok(next)
325 }
326
327 fn begin_outbound_store_device_exclusion_nonactivation(
328 &mut self,
329 expected: DurableStoreDeviceExclusionOperation,
330 next: DurableStoreDeviceExclusionOperation,
331 candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
332 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
333 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
334 let conn = self.conn;
335 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
336 require_store_device_exclusion_transition_on(&tx, &expected, &next)?;
337 let authority_id = remote_object_id(expected.object().object());
338 if begin_remote_candidate_nonactivation_on(&tx, authority_id, nonactivation.clone())?
339 .is_some()
340 {
341 return Err(DbError::Message(
342 "uploaded exclusion authority became a deletion target".to_string(),
343 ));
344 }
345 let head = candidate.head_ref();
346 if begin_remote_candidate_nonactivation_on(
347 &tx,
348 remote_object_id(&head.object),
349 nonactivation.clone(),
350 )?
351 .is_some()
352 {
353 return Err(DbError::Message(
354 "Store-device exclusion activation head became a deletion target".to_string(),
355 ));
356 }
357 if begin_remote_candidate_nonactivation_on(
358 &tx,
359 remote_object_id(&candidate.reference.object),
360 nonactivation,
361 )?
362 .is_none()
363 {
364 return Err(DbError::Message(
365 "losing Store-device exclusion commit has no deletion target".to_string(),
366 ));
367 }
368 update_store_device_exclusion_on(&tx, &expected, &next, true)?;
369 tx.commit().map_err(DbError::from)?;
370 Ok(next)
371 }
372
373 fn begin_outbound_store_device_exclusion_replacement(
374 &mut self,
375 expected: DurableStoreDeviceExclusionOperation,
376 next: DurableStoreDeviceExclusionOperation,
377 replacement_candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
378 losing_candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
379 authority_id: ObjectHash,
380 replacement_remotes: Vec<ClosedRemoteObject>,
381 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
382 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
383 let conn = self.conn;
384 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
385 require_store_device_exclusion_transition_on(&tx, &expected, &next)?;
386 for remote in replacement_remotes
387 .iter()
388 .filter(|remote| remote.object_id() != authority_id)
389 {
390 persist_exact_remote_object_on(
391 &tx,
392 self.store_dir,
393 remote,
394 "replacement Store-device exclusion candidate object",
395 )?;
396 }
397 let mut authority = load_remote_object_on(&tx, authority_id)?;
398 authority
399 .add_retained_authority_candidate(replacement_candidate.reference.clone())
400 .map_err(|error| {
401 DbError::context("attach replacement exclusion candidate authority", error)
402 })?;
403 update_remote_object_on(&tx, authority_id, &authority)?;
404 if begin_remote_candidate_nonactivation_on(&tx, authority_id, nonactivation.clone())?
405 .is_some()
406 {
407 return Err(DbError::Message(
408 "reusable exclusion outcome became a deletion target".to_string(),
409 ));
410 }
411 let head = losing_candidate.head_ref();
412 if begin_remote_candidate_nonactivation_on(
413 &tx,
414 remote_object_id(&head.object),
415 nonactivation.clone(),
416 )?
417 .is_some()
418 {
419 return Err(DbError::Message(
420 "losing exclusion activation head became a deletion target".to_string(),
421 ));
422 }
423 if begin_remote_candidate_nonactivation_on(
424 &tx,
425 remote_object_id(&losing_candidate.reference.object),
426 nonactivation,
427 )?
428 .is_none()
429 {
430 return Err(DbError::Message(
431 "losing exclusion candidate has no exact deletion target".to_string(),
432 ));
433 }
434 update_store_device_exclusion_on(&tx, &expected, &next, true)?;
435 tx.commit().map_err(DbError::from)?;
436 Ok(next)
437 }
438
439 fn nonactivating_store_device_exclusion_cleanup_targets(
440 &mut self,
441 expected: &DurableStoreDeviceExclusionOperation,
442 ) -> Result<Vec<CandidateCleanupObject>, DbError> {
443 let conn = self.conn;
444 let current =
445 load_store_device_exclusion_on(conn, expected.operation_id())?.ok_or_else(|| {
446 DbError::Message("Store-device exclusion journal is absent".to_string())
447 })?;
448 if current != *expected {
449 return Err(DbError::Message(
450 "Store-device exclusion is not awaiting candidate cleanup".to_string(),
451 ));
452 }
453 let candidate = match ¤t {
454 DurableStoreDeviceExclusionOperation::CandidateNonactivating { candidate, .. } => {
455 candidate
456 }
457 DurableStoreDeviceExclusionOperation::ReplacingCandidate { losing, .. } => {
458 &losing.candidate
459 }
460 _ => {
461 return Err(DbError::Message(
462 "Store-device exclusion is not awaiting candidate cleanup".to_string(),
463 ));
464 }
465 };
466 super::candidate_records::candidate_cleanup_targets_on(
467 conn,
468 &candidate.reference,
469 std::slice::from_ref(&candidate.reference.object),
470 )
471 }
472
473 fn complete_store_device_exclusion_replacement_cleanup(
474 &mut self,
475 expected: DurableStoreDeviceExclusionOperation,
476 next: DurableStoreDeviceExclusionOperation,
477 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
478 let DurableStoreDeviceExclusionOperation::ReplacingCandidate { object, losing, .. } =
479 &expected
480 else {
481 unreachable!("validated replacement state")
482 };
483 let conn = self.conn;
484 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
485 require_store_device_exclusion_transition_on(&tx, &expected, &next)?;
486 let commit_id = remote_object_id(&losing.candidate.reference.object);
487 let head = losing.candidate.head_ref();
488 let head_id = remote_object_id(&head.object);
489 super::candidate_records::require_candidate_cleanup_complete_on(
490 &tx,
491 &losing.candidate.reference,
492 &[
493 losing.candidate.reference.object.clone(),
494 object.object().clone(),
495 head.object.clone(),
496 ],
497 "replaced exclusion cleanup is incomplete",
498 )?;
499 let commit = load_remote_object_on(&tx, commit_id)?;
500 if commit
501 .candidate_nonactivation_proof(&losing.candidate.reference)
502 .map_err(DbError::from)?
503 != Some(&losing.proof)
504 {
505 return Err(DbError::Message(
506 "replaced exclusion commit lacks complete nonactivation evidence".to_string(),
507 ));
508 }
509 let authority = load_remote_object_on(&tx, remote_object_id(object.object()))?;
510 if authority
511 .candidate_nonactivation_proof(&losing.candidate.reference)
512 .map_err(DbError::from)?
513 != Some(&losing.proof)
514 {
515 return Err(DbError::Message(
516 "reusable exclusion outcome lacks its former candidate proof".to_string(),
517 ));
518 }
519 let mut removable = vec![commit_id];
520 let remote = load_remote_object_on(&tx, head_id)?;
521 if remote
522 .candidate_nonactivation_proof(&losing.candidate.reference)
523 .map_err(DbError::from)?
524 != Some(&losing.proof)
525 {
526 return Err(DbError::Message(
527 "replaced exclusion head lacks complete nonactivation evidence".to_string(),
528 ));
529 }
530 removable.push(head_id);
531 super::candidate_records::delete_remote_objects_on(&tx, removable, "replaced exclusion")?;
532 update_store_device_exclusion_on(&tx, &expected, &next, true)?;
533 tx.commit().map_err(DbError::from)?;
534 Ok(next)
535 }
536
537 fn complete_nonactivating_store_device_exclusion(
538 &mut self,
539 expected: DurableStoreDeviceExclusionOperation,
540 next: DurableStoreDeviceExclusionOperation,
541 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
542 let DurableStoreDeviceExclusionOperation::CandidateNonactivating {
543 object,
544 candidate,
545 proof,
546 } = &expected
547 else {
548 unreachable!("validated nonactivating state")
549 };
550 let conn = self.conn;
551 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
552 require_store_device_exclusion_transition_on(&tx, &expected, &next)?;
553 let commit_id = remote_object_id(&candidate.reference.object);
554 let head = candidate.head_ref();
555 let head_id = remote_object_id(&head.object);
556 super::candidate_records::require_candidate_cleanup_complete_on(
557 &tx,
558 &candidate.reference,
559 &[candidate.reference.object.clone(), head.object.clone()],
560 "nonactivating exclusion cleanup is incomplete",
561 )?;
562 let commit = load_remote_object_on(&tx, commit_id)?;
563 if commit
564 .candidate_nonactivation_proof(&candidate.reference)
565 .map_err(DbError::from)?
566 != Some(proof)
567 {
568 return Err(DbError::Message(
569 "losing exclusion commit lacks complete exact nonactivation evidence".to_string(),
570 ));
571 }
572 let inert = load_protocol_inert_object_on(&tx, remote_object_id(object.object()))?;
573 if inert
574 .candidate_nonactivation_proof(&candidate.reference)
575 .map_err(DbError::from)?
576 != Some(proof)
577 {
578 return Err(DbError::Message(
579 "protocol-inert exclusion object lacks its candidate proof".to_string(),
580 ));
581 }
582 let mut removable = vec![commit_id];
583 let head_remote = load_remote_object_on(&tx, head_id)?;
584 if head_remote
585 .candidate_nonactivation_proof(&candidate.reference)
586 .map_err(DbError::from)?
587 != Some(proof)
588 {
589 return Err(DbError::Message(
590 "losing exclusion head lacks complete nonactivation evidence".to_string(),
591 ));
592 }
593 removable.push(head_id);
594 super::candidate_records::delete_remote_objects_on(
595 &tx,
596 removable,
597 "nonactivating exclusion",
598 )?;
599 update_store_device_exclusion_on(&tx, &expected, &next, false)?;
600 tx.commit().map_err(DbError::from)?;
601 Ok(next)
602 }
603
604 fn mark_store_device_exclusion_authority_uploaded(
605 &mut self,
606 expected: ClosedRemoteObject,
607 candidate: StoreBatchCommitRef,
608 ) -> Result<(), DbError> {
609 let conn = self.conn;
610 let object_id = expected.object_id();
611 let current = load_remote_object_on(conn, object_id)?;
612 let (
613 RemoteObjectRecord::RetainedAuthority(expected_record),
614 RemoteObjectRecord::RetainedAuthority(current_record),
615 ) = (expected.record(), ¤t)
616 else {
617 return Err(DbError::Message(
618 "Store-device exclusion authority is not retained authority".to_string(),
619 ));
620 };
621 if expected_record.identity != current_record.identity
622 || expected_record.payloads != current_record.payloads
623 {
624 return Err(DbError::Message(
625 "Store-device exclusion authority changed before upload completion".to_string(),
626 ));
627 }
628 match ¤t_record.state {
629 RetainedAuthorityObjectState::Prepared { ownership }
630 if ownership.pending.contains(&candidate) =>
631 {
632 mark_remote_object_uploaded_on(conn, current)?;
633 }
634 RetainedAuthorityObjectState::UploadedVerified { ownership }
635 if ownership.pending.contains(&candidate) => {}
636 _ => {
637 return Err(DbError::Message(
638 "Store-device exclusion authority does not belong to its current candidate"
639 .to_string(),
640 ));
641 }
642 }
643 Ok(())
644 }
645
646 #[cfg(any(test, feature = "test-utils"))]
647 fn outbound_store_device_exclusion_operations(
648 &mut self,
649 ) -> Result<Vec<DurableStoreDeviceExclusionOperation>, DbError> {
650 let conn = self.conn;
651 let mut statement = conn
652 .prepare(
653 "SELECT operation_id, state
654 FROM outbound_store_device_exclusion
655 ORDER BY operation_id",
656 )
657 .map_err(DbError::from)?;
658 let operations = statement
659 .query_map([], |row| {
660 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
661 })
662 .map_err(DbError::from)?
663 .map(|row| {
664 let (raw_id, raw) = row.map_err(DbError::from)?;
665 let operation_id = raw_id.parse::<ObjectHash>().map_err(|error| {
666 DbError::context("Store-device exclusion operation id", error)
667 })?;
668 parse_store_device_exclusion_operation(operation_id, &raw)
669 })
670 .collect();
671 operations
672 }
673}
674
675impl StoreDatabase {
676 pub async fn begin_outbound_store_device_exclusion(
677 &self,
678 operation: DurableStoreDeviceExclusionOperation,
679 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
680 operation
681 .validate()
682 .map_err(store_device_exclusion_journal_error)?;
683 if !matches!(
684 operation,
685 DurableStoreDeviceExclusionOperation::CandidatePrepared { .. }
686 ) {
687 return Err(DbError::Message(
688 "a new Store-device exclusion journal must own its exact activation candidate"
689 .to_string(),
690 ));
691 }
692 let remotes = operation
693 .remote_objects()
694 .map_err(store_device_exclusion_journal_error)?;
695 Box::pin(self.call_store(move |session| {
696 session.begin_outbound_store_device_exclusion(operation, remotes)
697 }))
698 .await
699 }
700
701 pub async fn active_outbound_store_device_exclusion(
702 &self,
703 ) -> Result<Option<DurableStoreDeviceExclusionOperation>, DbError> {
704 Box::pin(self.call_store(|session| session.active_outbound_store_device_exclusion())).await
705 }
706
707 pub async fn replace_outbound_store_device_exclusion_candidate(
708 &self,
709 expected: DurableStoreDeviceExclusionOperation,
710 replacement: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
711 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
712 let DurableStoreDeviceExclusionOperation::CandidatePrepared { object, candidate } =
713 expected.clone()
714 else {
715 return Err(DbError::Message(
716 "Store-device exclusion has no replaceable activation candidate".to_string(),
717 ));
718 };
719 let next = DurableStoreDeviceExclusionOperation::CandidatePrepared {
720 object,
721 candidate: replacement,
722 };
723 next.validate()
724 .map_err(store_device_exclusion_journal_error)?;
725 if !expected.allows_transition_to(&next) {
726 return Err(DbError::Message(
727 "replacement Store-device exclusion candidate changes its signed commit"
728 .to_string(),
729 ));
730 }
731 Box::pin(self.call_store(move |session| {
732 session.replace_outbound_store_device_exclusion_candidate(expected, next, candidate)
733 }))
734 .await
735 }
736
737 pub fn complete_outbound_store_device_exclusion_activation<'a>(
738 &'a self,
739 expected: DurableStoreDeviceExclusionOperation,
740 ) -> Pin<
741 Box<dyn Future<Output = Result<DurableStoreDeviceExclusionOperation, DbError>> + Send + 'a>,
742 > {
743 Box::pin(async move {
744 let next = match &expected {
745 DurableStoreDeviceExclusionOperation::CandidatePrepared { object, candidate } => {
746 DurableStoreDeviceExclusionOperation::Completed(
747 StoreDeviceExclusionCompletion::Activated {
748 object: object.clone(),
749 candidate: candidate.clone(),
750 },
751 )
752 }
753 _ => {
754 return Err(DbError::Message(
755 "Store-device exclusion has no activated candidate".to_string(),
756 ));
757 }
758 };
759 let expected = Box::new(expected);
760 let next = Box::new(next);
761 Box::pin(self.call_store(move |session| {
762 session.complete_outbound_store_device_exclusion_activation(expected, next)
763 }))
764 .await
765 })
766 }
767
768 pub async fn complete_outbound_store_device_exclusion_slot_loss(
769 &self,
770 expected: DurableStoreDeviceExclusionOperation,
771 winner: DurableStoreDeviceExclusionObject,
772 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
773 let next = DurableStoreDeviceExclusionOperation::Completed(
774 StoreDeviceExclusionCompletion::OutcomeSlotOccupied {
775 intended: expected.object().clone(),
776 winner,
777 },
778 );
779 next.validate()
780 .map_err(store_device_exclusion_journal_error)?;
781 let remotes = expected
782 .remote_objects()
783 .map_err(store_device_exclusion_journal_error)?;
784 Box::pin(self.call_store(move |session| {
785 session.complete_outbound_store_device_exclusion_slot_loss(expected, next, remotes)
786 }))
787 .await
788 }
789
790 pub async fn begin_outbound_store_device_exclusion_nonactivation(
791 &self,
792 expected: DurableStoreDeviceExclusionOperation,
793 nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
794 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
795 let candidate = expected.candidate().cloned().ok_or_else(|| {
796 DbError::Message("Store-device exclusion has no losing candidate".to_string())
797 })?;
798 if nonactivation.candidate_reference().map_err(DbError::from)? != candidate.reference {
799 return Err(DbError::Message(
800 "verified nonactivation names another Store-device exclusion candidate".to_string(),
801 ));
802 }
803 let nonactivation = nonactivation.into_durable();
804 let (next, nonactivation) = expected
805 .begin_nonactivation(nonactivation)
806 .map_err(store_device_exclusion_journal_error)?;
807 if nonactivation.candidate().canonical_signed_bytes != candidate.commit.to_bytes() {
808 return Err(DbError::Message(
809 "verified nonactivation bytes differ from the Store-device exclusion candidate"
810 .to_string(),
811 ));
812 }
813 Box::pin(self.call_store(move |session| {
814 session.begin_outbound_store_device_exclusion_nonactivation(
815 expected,
816 next,
817 candidate,
818 nonactivation,
819 )
820 }))
821 .await
822 }
823
824 pub async fn begin_outbound_store_device_exclusion_replacement(
825 &self,
826 expected: DurableStoreDeviceExclusionOperation,
827 replacement: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
828 nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
829 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
830 let expected_candidate = expected.candidate().cloned().ok_or_else(|| {
831 DbError::Message("Store-device exclusion has no losing candidate".to_string())
832 })?;
833 if nonactivation.candidate_reference().map_err(DbError::from)?
834 != expected_candidate.reference
835 {
836 return Err(DbError::Message(
837 "verified nonactivation names another Store-device exclusion candidate".to_string(),
838 ));
839 }
840 let nonactivation = nonactivation.into_durable();
841 let (next, nonactivation) = expected
842 .begin_replacement(replacement, nonactivation)
843 .map_err(store_device_exclusion_journal_error)?;
844 if nonactivation.candidate().canonical_signed_bytes != expected_candidate.commit.to_bytes()
845 {
846 return Err(DbError::Message(
847 "verified nonactivation bytes differ from the Store-device exclusion candidate"
848 .to_string(),
849 ));
850 }
851 let DurableStoreDeviceExclusionOperation::ReplacingCandidate {
852 candidate, losing, ..
853 } = &next
854 else {
855 unreachable!("begin_replacement returns replacement state")
856 };
857 let replacement_candidate = candidate.clone();
858 let losing_candidate = losing.candidate.clone();
859 let authority_id = remote_object_id(expected.object().object());
860 let replacement_remotes = DurableStoreDeviceExclusionOperation::CandidatePrepared {
861 object: expected.object().clone(),
862 candidate: replacement_candidate.clone(),
863 }
864 .remote_objects()
865 .map_err(store_device_exclusion_journal_error)?;
866 Box::pin(self.call_store(move |session| {
867 session.begin_outbound_store_device_exclusion_replacement(
868 expected,
869 next,
870 replacement_candidate,
871 losing_candidate,
872 authority_id,
873 replacement_remotes,
874 nonactivation,
875 )
876 }))
877 .await
878 }
879
880 pub async fn nonactivating_store_device_exclusion_cleanup_targets(
881 &self,
882 expected: DurableStoreDeviceExclusionOperation,
883 ) -> Result<Vec<CandidateCleanupObject>, DbError> {
884 Box::pin(self.call_store(move |session| {
885 session.nonactivating_store_device_exclusion_cleanup_targets(&expected)
886 }))
887 .await
888 }
889
890 pub async fn complete_store_device_exclusion_replacement_cleanup(
891 &self,
892 expected: DurableStoreDeviceExclusionOperation,
893 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
894 let DurableStoreDeviceExclusionOperation::ReplacingCandidate {
895 object,
896 candidate,
897 losing: _,
898 } = expected.clone()
899 else {
900 return Err(DbError::Message(
901 "Store-device exclusion has no replacement cleanup".to_string(),
902 ));
903 };
904 let next = DurableStoreDeviceExclusionOperation::CandidatePrepared {
905 object: object.clone(),
906 candidate,
907 };
908 next.validate()
909 .map_err(store_device_exclusion_journal_error)?;
910 Box::pin(self.call_store(move |session| {
911 session.complete_store_device_exclusion_replacement_cleanup(expected, next)
912 }))
913 .await
914 }
915
916 pub async fn complete_nonactivating_store_device_exclusion(
917 &self,
918 expected: DurableStoreDeviceExclusionOperation,
919 ) -> Result<DurableStoreDeviceExclusionOperation, DbError> {
920 let DurableStoreDeviceExclusionOperation::CandidateNonactivating {
921 object,
922 candidate,
923 proof,
924 } = expected.clone()
925 else {
926 return Err(DbError::Message(
927 "Store-device exclusion is not nonactivating".to_string(),
928 ));
929 };
930 let next = DurableStoreDeviceExclusionOperation::Completed(
931 StoreDeviceExclusionCompletion::CandidateNonactivated {
932 object: object.clone(),
933 candidate: candidate.clone(),
934 proof: proof.clone(),
935 },
936 );
937 next.validate()
938 .map_err(store_device_exclusion_journal_error)?;
939 Box::pin(self.call_store(move |session| {
940 session.complete_nonactivating_store_device_exclusion(expected, next)
941 }))
942 .await
943 }
944
945 pub async fn mark_store_device_exclusion_authority_uploaded(
946 &self,
947 operation: DurableStoreDeviceExclusionOperation,
948 ) -> Result<(), DbError> {
949 let expected = operation
950 .authority_remote_object()
951 .map_err(store_device_exclusion_journal_error)?;
952 let candidate = operation
953 .candidate()
954 .ok_or_else(|| {
955 DbError::Message(
956 "Store-device exclusion authority has no current candidate".to_string(),
957 )
958 })?
959 .reference
960 .clone();
961 self.call_store(move |session| {
962 session.mark_store_device_exclusion_authority_uploaded(expected, candidate)
963 })
964 .await
965 }
966
967 #[cfg(any(test, feature = "test-utils"))]
968 pub async fn outbound_store_device_exclusion_operations(
969 &self,
970 ) -> Result<Vec<DurableStoreDeviceExclusionOperation>, DbError> {
971 Box::pin(self.call_store(|session| session.outbound_store_device_exclusion_operations()))
972 .await
973 }
974}