1use std::collections::BTreeSet;
2
3use rusqlite::OptionalExtension;
4
5use super::*;
6use crate::{
7 begin_remote_candidate_nonactivation_on, insert_store_reclaim_operation_on,
8 load_remote_object_on, load_store_reclaim_operation_on, parse_store_reclaim_operation,
9 persist_exact_remote_object_on, record_reclaimed_store_package_on,
10 replace_prepared_merge_head_remote_on, store_reclaim_journal_error, update_remote_object_on,
11 update_store_reclaim_operation_on,
12};
13use coven_protocol::remote_object::{remote_object_id, RemoteObjectRecord, RetainedReplayOwner};
14use coven_protocol::store_commit::{ObjectHash, StoreBatchCommitRef, StorePackageRef};
15
16pub mod journal;
17
18impl StoreSession<'_> {
19 fn begin_store_reclaim_operation(
20 &mut self,
21 operation: DurableStoreReclaimOperation,
22 remotes: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
23 ) -> Result<DurableStoreReclaimOperation, DbError> {
24 let conn = self.conn;
25 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
26 let operation_id = operation.operation_id();
27 if let Some(existing) = load_store_reclaim_operation_on(&tx, operation_id)? {
28 if existing != operation {
29 return Err(DbError::Message(format!(
30 "Store reclaim operation {operation_id} already has different durable state"
31 )));
32 }
33 return Ok(existing);
34 }
35 for remote in &remotes {
36 persist_exact_remote_object_on(
37 &tx,
38 self.store_dir,
39 remote,
40 "Store reclaim candidate object",
41 )?;
42 }
43 insert_store_reclaim_operation_on(&tx, &operation)?;
44 tx.commit().map_err(DbError::from)?;
45 Ok(operation)
46 }
47
48 fn advance_snapshot_replay_baseline(
51 &mut self,
52 root: &coven_protocol::store_commit::StoreRootRef,
53 proof: coven_protocol::store_commit::ReplayBaselineRetirementProof,
54 routing_encryption: Option<&coven_keys::encryption::EncryptionService>,
55 ) -> Result<Option<crate::AdvancedReplayBaseline>, DbError> {
56 let snapshot_authority = proof.authority.clone();
57 let cut = snapshot_authority.metadata.coverage.clone();
58 if !crate::store::store_session::replay_baseline_advances_on(
63 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
64 &cut,
65 )? {
66 return Ok(None);
67 }
68 let snapshot_hash = snapshot_authority.snapshot.snapshot_hash;
69 let current_cut = proof.current_cut.frontier();
70 let (image, folded) = self.capture_replay_baseline_at_cut(
71 root,
72 &cut,
73 ¤t_cut,
74 snapshot_hash,
75 routing_encryption,
76 )?;
77 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
78 let schema_version = self.schema_version;
79 let routing_hash = self.sync_routing_hash;
80 let store_dir = self.store_dir;
81 let advanced = crate::store::store_session::StoreTransaction::new(&tx, store_dir)
82 .advance_snapshot_replay_baseline(
83 self.verified_store_authority,
84 root,
85 schema_version,
86 routing_hash,
87 proof,
88 image,
89 &folded,
90 self.blob_decls,
91 )?;
92 tx.commit().map_err(DbError::from)?;
93 if advanced.is_some() {
94 self.verified_store_authority
95 .forget_superseded_replay_baseline();
96 }
97 Ok(advanced)
98 }
99
100 fn store_package_is_retained_for_replay(
101 &mut self,
102 root: &coven_protocol::store_commit::StoreRootRef,
103 target: &StorePackageRef,
104 activation: &StoreBatchCommitRef,
105 ) -> Result<bool, DbError> {
106 let object_id = remote_object_id(&target.object);
107 let exists: bool = self
108 .conn
109 .query_row(
110 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
111 [object_id.to_string()],
112 |row| row.get(0),
113 )
114 .map_err(DbError::from)?;
115 if !exists {
116 return Ok(false);
117 }
118 let remote = load_remote_object_on(self.conn, object_id)?;
119 let retained = remote
120 .store_package_is_retained_for_replay(target, activation)
121 .map_err(|error| {
122 DbError::context(
123 format!("validate Store package {object_id} replay ownership"),
124 error,
125 )
126 })?;
127 if !retained {
128 return Ok(false);
129 }
130 for owner in remote.retained_replay_owners() {
131 let RetainedReplayOwner::Commit { commit, input_hash } = owner;
132 let retained = self
133 .verified_store_authority
134 .validate_retained_materialization_by_ref_on(
135 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
136 commit,
137 )?;
138 if retained.root() != root || retained.input_hash() != *input_hash {
139 return Err(DbError::Message(
140 "Store package replay owner differs from retained materialization".to_string(),
141 ));
142 }
143 }
144 Ok(true)
145 }
146
147 fn circle_package_is_retained_for_replay(
148 &mut self,
149 root: &coven_protocol::store_commit::StoreRootRef,
150 target: &coven_protocol::store_commit::CirclePackageRef,
151 activation: &StoreBatchCommitRef,
152 ) -> Result<bool, DbError> {
153 let object_id = remote_object_id(&target.package.object);
154 let exists: bool = self
155 .conn
156 .query_row(
157 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
158 [object_id.to_string()],
159 |row| row.get(0),
160 )
161 .map_err(DbError::from)?;
162 if !exists {
163 return Ok(false);
164 }
165 let remote = load_remote_object_on(self.conn, object_id)?;
166 let retained = remote
167 .circle_package_is_retained_for_replay(target, activation)
168 .map_err(|error| {
169 DbError::context(
170 format!("validate Circle package {object_id} replay ownership"),
171 error,
172 )
173 })?;
174 if !retained {
175 return Ok(false);
176 }
177 for owner in remote.retained_replay_owners() {
178 let RetainedReplayOwner::Commit { commit, input_hash } = owner;
179 let retained = self
180 .verified_store_authority
181 .validate_retained_materialization_by_ref_on(
182 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
183 commit,
184 )?;
185 if retained.root() != root || retained.input_hash() != *input_hash {
186 return Err(DbError::Message(
187 "Circle package replay owner differs from retained materialization".to_string(),
188 ));
189 }
190 }
191 Ok(true)
192 }
193
194 fn circle_image_is_retained_for_replay(
195 &self,
196 circle_id: coven_protocol::circle::CircleId,
197 image: &coven_protocol::store_commit::SnapshotImageRef,
198 ) -> Result<bool, DbError> {
199 let row: Option<Vec<u8>> = self
200 .conn
201 .query_row(
202 "SELECT bootstrap_ref FROM circle_bootstrap_coverage WHERE circle_id = ?1",
203 [circle_id.to_string()],
204 |row| row.get(0),
205 )
206 .optional()
207 .map_err(DbError::from)?;
208 let Some(bootstrap_ref) = row else {
209 return Ok(false);
210 };
211 let bootstrap: coven_protocol::circle::CircleBootstrapRef =
212 serde_json::from_slice(&bootstrap_ref).map_err(|error| {
213 DbError::context("parse retained Circle bootstrap reference", error)
214 })?;
215 Ok(bootstrap.image == *image)
216 }
217
218 fn stored_blob_reclaim_candidates(
219 &self,
220 ) -> Result<
221 Vec<(
222 coven_protocol::blob::locator::StoredBlobRef,
223 Vec<StoreBatchCommitRef>,
224 )>,
225 DbError,
226 > {
227 let conn = self.conn;
228 let mut statement = conn
229 .prepare("SELECT remote_object_id FROM blob_locators ORDER BY remote_object_id")
230 .map_err(DbError::from)?;
231 let object_ids = statement
232 .query_map([], |row| row.get::<_, String>(0))
233 .map_err(DbError::from)?
234 .collect::<Result<Vec<_>, _>>()
235 .map_err(DbError::from)?;
236 drop(statement);
237 let mut candidates = Vec::new();
238 for object_id in object_ids {
239 let parsed = object_id.parse().map_err(|error| {
240 DbError::context(format!("stored blob object id {object_id:?}"), error)
241 })?;
242 let remote = load_remote_object_on(conn, parsed)?;
243 if !remote.is_activated_stored_blob() {
244 continue;
245 }
246 let Some(locator_bytes) = remote.payloads().carried_locator_bytes() else {
247 return Err(DbError::Message(format!(
248 "stored blob {object_id} carries no locator"
249 )));
250 };
251 let locator = coven_protocol::blob::locator::BlobLocator::parse(locator_bytes)
252 .map_err(|error| {
253 DbError::context(format!("stored blob {object_id} locator"), error)
254 })?;
255 let stored =
256 coven_protocol::blob::locator::StoredBlobRef::new(locator, remote.object().clone())
257 .map_err(|error| {
258 DbError::context(format!("stored blob {object_id} reference"), error)
259 })?;
260 candidates.push((stored, remote.stored_blob_commit_owners()));
261 }
262 Ok(candidates)
263 }
264
265 fn stored_blob_is_row_orphaned(
266 &self,
267 stored: &coven_protocol::blob::locator::StoredBlobRef,
268 ) -> Result<bool, DbError> {
269 match crate::Database::stored_blob_reference_state_on(
270 self.conn,
271 self.gates,
272 self.synced_tables,
273 stored,
274 )? {
275 crate::StoredBlobReferenceState::NotLiveRemote => Ok(true),
276 crate::StoredBlobReferenceState::LiveRemote => Ok(false),
277 crate::StoredBlobReferenceState::Unresolved => Err(DbError::Message(format!(
278 "stored blob {} has a live reference whose locality is unresolved",
279 remote_object_id(stored.object())
280 ))),
281 }
282 }
283
284 fn audience_blob_is_retained_for_replay(
285 &self,
286 stored: &coven_protocol::blob::locator::StoredBlobRef,
287 ) -> Result<bool, DbError> {
288 let conn = self.conn;
289 let object_id = remote_object_id(stored.object());
290 let exists: bool = conn
291 .query_row(
292 "SELECT EXISTS(SELECT 1 FROM remote_objects WHERE object_id = ?1)",
293 [object_id.to_string()],
294 |row| row.get(0),
295 )
296 .map_err(DbError::from)?;
297 if exists {
298 let remote = load_remote_object_on(conn, object_id)?;
299 if remote.snapshot_owners().next().is_some()
300 || remote.retained_replay_owners().next().is_some()
301 {
302 return Ok(true);
303 }
304 }
305 let mut statement = conn
306 .prepare("SELECT bootstrap_ref FROM circle_bootstrap_coverage")
307 .map_err(DbError::from)?;
308 let coverages = statement
309 .query_map([], |row| row.get::<_, Vec<u8>>(0))
310 .map_err(DbError::from)?
311 .collect::<Result<Vec<_>, _>>()
312 .map_err(DbError::from)?;
313 drop(statement);
314 for bytes in coverages {
315 let bootstrap: coven_protocol::circle::CircleBootstrapRef =
316 serde_json::from_slice(&bytes).map_err(|error| {
317 DbError::context("parse retained Circle bootstrap reference", error)
318 })?;
319 if bootstrap
320 .blobs
321 .iter()
322 .any(|blob| blob.stored() == Some(stored))
323 {
324 return Ok(true);
325 }
326 }
327 Ok(false)
328 }
329
330 fn package_is_retained_by_pending_blob_reclaim(
340 &self,
341 package: &coven_protocol::objects::ExactObjectRef,
342 ) -> Result<bool, DbError> {
343 let package_id = remote_object_id(package);
344 Ok(self.store_reclaim_operations()?.iter().any(|operation| {
345 if matches!(operation, DurableStoreReclaimOperation::Completed { .. }) {
346 return false;
347 }
348 match operation.authorization().target() {
349 coven_protocol::reclaim::ReclaimTarget::AudienceBlob(target) => {
350 remote_object_id(target.package.object()) == package_id
351 }
352 _ => false,
353 }
354 }))
355 }
356
357 fn store_reclaim_journal(
362 &self,
363 ) -> Result<Vec<(DurableStoreReclaimOperation, Option<String>)>, DbError> {
364 let mut statement = self
365 .conn
366 .prepare(
367 "SELECT authorization_hash, state, stuck_error FROM store_reclaim_operations
368 ORDER BY authorization_hash",
369 )
370 .map_err(DbError::from)?;
371 let rows = statement
372 .query_map([], |row| {
373 Ok((
374 row.get::<_, String>(0)?,
375 row.get::<_, String>(1)?,
376 row.get::<_, Option<String>>(2)?,
377 ))
378 })
379 .map_err(DbError::from)?
380 .collect::<Result<Vec<_>, _>>()
381 .map_err(DbError::from)?;
382 rows.into_iter()
383 .map(|(raw_id, raw, stuck_error)| {
384 let id = raw_id
385 .parse()
386 .map_err(|error| DbError::context("Store reclaim operation id", error))?;
387 Ok((parse_store_reclaim_operation(id, &raw)?, stuck_error))
388 })
389 .collect()
390 }
391
392 fn store_reclaim_operations(&self) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
393 Ok(self
394 .store_reclaim_journal()?
395 .into_iter()
396 .map(|(operation, _)| operation)
397 .collect())
398 }
399
400 fn runnable_store_reclaim_operations(
401 &self,
402 ) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
403 Ok(self
404 .store_reclaim_journal()?
405 .into_iter()
406 .filter_map(|(operation, stuck_error)| stuck_error.is_none().then_some(operation))
407 .collect())
408 }
409
410 fn stuck_reclaim_operations(&self) -> Result<Vec<StuckReclaimOperation>, DbError> {
411 Ok(self
412 .store_reclaim_journal()?
413 .into_iter()
414 .filter_map(|(operation, stuck_error)| {
415 stuck_error.map(|error| StuckReclaimOperation {
416 operation_id: operation.operation_id(),
417 target: operation.authorization().target().clone(),
418 error,
419 })
420 })
421 .collect())
422 }
423
424 fn mark_store_reclaim_operation_stuck(
425 &mut self,
426 operation_id: ObjectHash,
427 error: String,
428 ) -> Result<(), DbError> {
429 crate::mark_store_reclaim_operation_stuck_on(self.conn, operation_id, &error)
430 }
431
432 fn retry_stuck_reclaim_operation(&mut self, operation_id: ObjectHash) -> Result<(), DbError> {
433 crate::clear_store_reclaim_operation_stuck_on(self.conn, operation_id)
434 }
435
436 fn begin_store_reclaim_receipt(
437 &mut self,
438 expected: DurableStoreReclaimOperation,
439 next: DurableStoreReclaimOperation,
440 remotes: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
441 ) -> Result<DurableStoreReclaimOperation, DbError> {
442 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
443 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
444 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
445 if current != expected {
446 return Err(DbError::Message(
447 "Store reclaim operation changed before receipt preparation".to_string(),
448 ));
449 }
450 for remote in &remotes {
451 persist_exact_remote_object_on(
452 &tx,
453 self.store_dir,
454 remote,
455 "Store reclaim receipt candidate",
456 )?;
457 }
458 update_store_reclaim_operation_on(&tx, &expected, &next)?;
459 tx.commit().map_err(DbError::from)?;
460 Ok(next)
461 }
462
463 fn mark_store_reclaim_target_absent(
464 &mut self,
465 expected: DurableStoreReclaimOperation,
466 next: DurableStoreReclaimOperation,
467 reclaimed: ReclaimedStorePackage,
468 ) -> Result<DurableStoreReclaimOperation, DbError> {
469 let root = self.required_root_authority()?;
470 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
471 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
472 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
473 if current != expected {
474 return Err(DbError::Message(
475 "Store reclaim operation changed before absence recording".to_string(),
476 ));
477 }
478 record_reclaimed_store_package_on(&tx, Some(root.store_root_hash), &reclaimed)?;
479 update_store_reclaim_operation_on(&tx, &expected, &next)?;
480 tx.commit().map_err(DbError::from)?;
481 Ok(next)
482 }
483
484 fn replace_store_reclaim_candidate(
485 &mut self,
486 expected: DurableStoreReclaimOperation,
487 current_candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
488 next: DurableStoreReclaimOperation,
489 ) -> Result<DurableStoreReclaimOperation, DbError> {
490 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
491 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
492 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
493 if current != expected {
494 return Err(DbError::Message(
495 "Store reclaim operation changed before candidate replacement".to_string(),
496 ));
497 }
498 let next_candidate = next.candidate().expect("constructed candidate state");
499 match (current_candidate.head_ref(), next_candidate.head_ref()) {
500 (current, replacement) if current != replacement => {
501 let (winner, prepared) = next_candidate.publication();
502 replace_prepared_merge_head_remote_on(
503 &tx,
504 self.store_dir,
505 ¤t.object,
506 winner,
507 prepared,
508 ¤t_candidate.reference,
509 )?;
510 }
511 _ => {}
512 }
513 update_store_reclaim_operation_on(&tx, &expected, &next)?;
514 tx.commit().map_err(DbError::from)?;
515 Ok(next)
516 }
517
518 fn begin_store_reclaim_candidate_replacement(
519 &mut self,
520 expected: DurableStoreReclaimOperation,
521 next: DurableStoreReclaimOperation,
522 replacement_remotes: Vec<coven_protocol::remote_object::ClosedRemoteObject>,
523 nonactivation: coven_protocol::remote_object::CandidateNonactivation,
524 losing_candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
525 ) -> Result<DurableStoreReclaimOperation, DbError> {
526 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
527 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
528 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
529 if current != expected {
530 return Err(DbError::Message(
531 "Store reclaim operation changed before candidate replacement".to_string(),
532 ));
533 }
534 let authority_ids = replacement_remotes
535 .iter()
536 .filter(|remote| matches!(
537 remote.record(),
538 RemoteObjectRecord::RetainedAuthority(record)
539 if matches!(
540 record.identity.domain,
541 coven_protocol::remote_object::RetainedAuthorityObjectDomain::ReclaimEvidence { .. }
542 | coven_protocol::remote_object::RetainedAuthorityObjectDomain::ReclaimAuthorization { .. }
543 | coven_protocol::remote_object::RetainedAuthorityObjectDomain::ReclaimReceipt { .. }
544 )
545 ))
546 .map(|remote| remote.object_id())
547 .collect::<BTreeSet<_>>();
548 for remote in replacement_remotes
549 .iter()
550 .filter(|remote| !authority_ids.contains(&remote.object_id()))
551 {
552 persist_exact_remote_object_on(
553 &tx,
554 self.store_dir,
555 remote,
556 "replacement Store reclaim candidate object",
557 )?;
558 }
559 for authority_id in authority_ids {
560 let mut authority = load_remote_object_on(&tx, authority_id)?;
561 authority
562 .add_retained_authority_candidate(
563 next.candidate()
564 .expect("constructed candidate state")
565 .reference
566 .clone(),
567 )
568 .map_err(|error| {
569 DbError::context("attach replacement reclaim authority candidate", error)
570 })?;
571 update_remote_object_on(&tx, authority_id, &authority)?;
572 if begin_remote_candidate_nonactivation_on(&tx, authority_id, nonactivation.clone())?
573 .is_some()
574 {
575 return Err(DbError::Message(
576 "reusable reclaim authority became a deletion target".to_string(),
577 ));
578 }
579 }
580 let head = losing_candidate.head_ref();
581 if begin_remote_candidate_nonactivation_on(
582 &tx,
583 remote_object_id(&head.object),
584 nonactivation.clone(),
585 )?
586 .is_some()
587 {
588 return Err(DbError::Message(
589 "losing reclaim activation head became a deletion target".to_string(),
590 ));
591 }
592 if begin_remote_candidate_nonactivation_on(
593 &tx,
594 remote_object_id(&losing_candidate.reference.object),
595 nonactivation,
596 )?
597 .is_none()
598 {
599 return Err(DbError::Message(
600 "losing reclaim commit has no exact deletion target".to_string(),
601 ));
602 }
603 update_store_reclaim_operation_on(&tx, &expected, &next)?;
604 tx.commit().map_err(DbError::from)?;
605 Ok(next)
606 }
607
608 fn store_reclaim_replacement_cleanup_targets(
609 &self,
610 expected: &DurableStoreReclaimOperation,
611 ) -> Result<Vec<CandidateCleanupObject>, DbError> {
612 let current = load_store_reclaim_operation_on(self.conn, expected.operation_id())?
613 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
614 if ¤t != expected {
615 return Err(DbError::Message(
616 "Store reclaim operation changed before cleanup".to_string(),
617 ));
618 }
619 let losing = current.losing_candidate().ok_or_else(|| {
620 DbError::Message("Store reclaim operation has no losing candidate".to_string())
621 })?;
622 super::candidate_records::candidate_cleanup_targets_on(
623 self.conn,
624 &losing.candidate.reference,
625 std::slice::from_ref(&losing.candidate.reference.object),
626 )
627 }
628
629 fn complete_store_reclaim_candidate_replacement(
630 &mut self,
631 expected: DurableStoreReclaimOperation,
632 losing: StoreReclaimCandidateLoss,
633 next: DurableStoreReclaimOperation,
634 ) -> Result<DurableStoreReclaimOperation, DbError> {
635 let tx = self.conn.unchecked_transaction().map_err(DbError::from)?;
636 let current = load_store_reclaim_operation_on(&tx, expected.operation_id())?
637 .ok_or_else(|| DbError::Message("Store reclaim operation disappeared".to_string()))?;
638 if current != expected {
639 return Err(DbError::Message(
640 "Store reclaim operation changed before replacement completion".to_string(),
641 ));
642 }
643 let object_id = remote_object_id(&losing.candidate.reference.object);
644 if !super::candidate_records::candidate_cleanup_targets_on(
645 &tx,
646 &losing.candidate.reference,
647 std::slice::from_ref(&losing.candidate.reference.object),
648 )?
649 .is_empty()
650 {
651 return Err(DbError::Message(
652 "losing reclaim commit cleanup is incomplete".to_string(),
653 ));
654 }
655 super::candidate_records::delete_remote_objects_on(&tx, [object_id], "losing reclaim")?;
656 update_store_reclaim_operation_on(&tx, &expected, &next)?;
657 tx.commit().map_err(DbError::from)?;
658 Ok(next)
659 }
660
661 #[cfg(any(test, feature = "test-utils"))]
662 fn stored_blob_has_snapshot_owner_for_test(
663 &self,
664 stored: &coven_protocol::blob::locator::StoredBlobRef,
665 ) -> Result<bool, DbError> {
666 let remote = load_remote_object_on(self.conn, remote_object_id(stored.object()))?;
667 let pinned = remote.snapshot_owners().next().is_some();
668 Ok(pinned)
669 }
670}
671
672impl StoreDatabase {
673 pub async fn replay_baseline_would_advance(
676 &self,
677 cut: coven_protocol::store_commit::CommitFrontier,
678 ) -> Result<bool, DbError> {
679 self.call_store(move |session| {
680 crate::store::store_session::replay_baseline_advances_on(
681 crate::store::store_session::StoreRecords::new(session.conn, session.store_dir),
682 &cut,
683 )
684 })
685 .await
686 }
687
688 pub async fn begin_store_reclaim_operation(
689 &self,
690 operation: DurableStoreReclaimOperation,
691 ) -> Result<DurableStoreReclaimOperation, DbError> {
692 operation.validate().map_err(store_reclaim_journal_error)?;
693 let DurableStoreReclaimOperation::AuthorizationCandidate { .. } = &operation else {
694 return Err(DbError::Message(
695 "a new Store reclaim operation must own an activation candidate".to_string(),
696 ));
697 };
698 let remotes = match &operation {
699 DurableStoreReclaimOperation::AuthorizationCandidate { object, candidate } => object
700 .remote_objects(candidate)
701 .map_err(store_reclaim_journal_error)?,
702 _ => unreachable!("matched reclaim candidate"),
703 };
704 self.call_store(move |session| session.begin_store_reclaim_operation(operation, remotes))
705 .await
706 }
707
708 pub async fn advance_snapshot_replay_baseline(
715 &self,
716 root: coven_protocol::store_commit::StoreRootRef,
717 proof: crate::VerifiedReplayBaselineRetirementProof,
718 routing_encryption: Option<coven_keys::encryption::EncryptionService>,
719 ) -> Result<Option<crate::AdvancedReplayBaseline>, DbError> {
720 let proof = proof.into_proof();
721 self.call_store(move |session| {
722 session.advance_snapshot_replay_baseline(&root, proof, routing_encryption.as_ref())
723 })
724 .await
725 }
726
727 pub async fn store_package_is_retained_for_replay(
728 &self,
729 root: coven_protocol::store_commit::StoreRootRef,
730 target: StorePackageRef,
731 activation: StoreBatchCommitRef,
732 ) -> Result<bool, DbError> {
733 self.call_store(move |session| {
734 session.store_package_is_retained_for_replay(&root, &target, &activation)
735 })
736 .await
737 }
738
739 pub async fn circle_package_is_retained_for_replay(
740 &self,
741 root: coven_protocol::store_commit::StoreRootRef,
742 target: coven_protocol::store_commit::CirclePackageRef,
743 activation: StoreBatchCommitRef,
744 ) -> Result<bool, DbError> {
745 self.call_store(move |session| {
746 session.circle_package_is_retained_for_replay(&root, &target, &activation)
747 })
748 .await
749 }
750
751 pub async fn circle_bootstrap_image_is_retained_for_replay(
757 &self,
758 coverage: coven_protocol::circle::CircleBootstrapCoverageRef,
759 ) -> Result<bool, DbError> {
760 self.circle_image_is_retained_for_replay(
761 coverage.circle_id,
762 coverage.bootstrap.image.clone(),
763 )
764 .await
765 }
766
767 pub async fn circle_image_is_retained_for_replay(
773 &self,
774 circle_id: coven_protocol::circle::CircleId,
775 image: coven_protocol::store_commit::SnapshotImageRef,
776 ) -> Result<bool, DbError> {
777 self.call_store(move |session| {
778 session.circle_image_is_retained_for_replay(circle_id, &image)
779 })
780 .await
781 }
782
783 pub async fn stored_blob_reclaim_candidates(
788 &self,
789 ) -> Result<
790 Vec<(
791 coven_protocol::blob::locator::StoredBlobRef,
792 Vec<StoreBatchCommitRef>,
793 )>,
794 DbError,
795 > {
796 self.call_store(|session| session.stored_blob_reclaim_candidates())
797 .await
798 }
799
800 pub async fn stored_blob_is_row_orphaned(
806 &self,
807 stored: coven_protocol::blob::locator::StoredBlobRef,
808 ) -> Result<bool, DbError> {
809 self.call_store(move |session| session.stored_blob_is_row_orphaned(&stored))
810 .await
811 }
812
813 pub async fn audience_blob_is_retained_for_replay(
824 &self,
825 stored: coven_protocol::blob::locator::StoredBlobRef,
826 ) -> Result<bool, DbError> {
827 self.call_store(move |session| session.audience_blob_is_retained_for_replay(&stored))
828 .await
829 }
830
831 pub async fn package_is_retained_by_pending_blob_reclaim(
834 &self,
835 package: coven_protocol::objects::ExactObjectRef,
836 ) -> Result<bool, DbError> {
837 self.call_store(move |session| {
838 session.package_is_retained_by_pending_blob_reclaim(&package)
839 })
840 .await
841 }
842
843 pub async fn store_reclaim_operations(
847 &self,
848 ) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
849 self.call_store(|session| session.store_reclaim_operations())
850 .await
851 }
852
853 pub async fn runnable_store_reclaim_operations(
856 &self,
857 ) -> Result<Vec<DurableStoreReclaimOperation>, DbError> {
858 self.call_store(|session| session.runnable_store_reclaim_operations())
859 .await
860 }
861
862 pub async fn stuck_reclaim_operations(&self) -> Result<Vec<StuckReclaimOperation>, DbError> {
865 self.call_store(|session| session.stuck_reclaim_operations())
866 .await
867 }
868
869 pub async fn mark_store_reclaim_operation_stuck(
872 &self,
873 operation_id: ObjectHash,
874 error: String,
875 ) -> Result<(), DbError> {
876 self.call_store(move |session| {
877 session.mark_store_reclaim_operation_stuck(operation_id, error)
878 })
879 .await
880 }
881
882 pub async fn retry_stuck_reclaim_operation(
885 &self,
886 operation_id: ObjectHash,
887 ) -> Result<(), DbError> {
888 self.call_store(move |session| session.retry_stuck_reclaim_operation(operation_id))
889 .await
890 }
891
892 pub async fn begin_store_reclaim_receipt(
893 &self,
894 expected: DurableStoreReclaimOperation,
895 object: DurableStoreReclaimObject,
896 candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
897 ) -> Result<DurableStoreReclaimOperation, DbError> {
898 let DurableStoreReclaimOperation::AbsentVerified {
899 authorization,
900 authorization_activation,
901 ..
902 } = &expected
903 else {
904 return Err(DbError::Message(
905 "only an authorized reclaim can prepare a receipt".to_string(),
906 ));
907 };
908 let next = DurableStoreReclaimOperation::ReceiptCandidate {
909 authorization: authorization.clone(),
910 authorization_activation: authorization_activation.clone(),
911 object: Box::new(object),
912 candidate: Box::new(candidate),
913 };
914 next.validate().map_err(store_reclaim_journal_error)?;
915 let remotes = match &next {
916 DurableStoreReclaimOperation::ReceiptCandidate {
917 object, candidate, ..
918 } => object
919 .remote_objects(candidate)
920 .map_err(store_reclaim_journal_error)?,
921 _ => unreachable!("constructed receipt candidate"),
922 };
923 self.call_store(move |session| session.begin_store_reclaim_receipt(expected, next, remotes))
924 .await
925 }
926
927 pub async fn mark_store_reclaim_target_absent(
928 &self,
929 expected: DurableStoreReclaimOperation,
930 target: coven_protocol::reclaim::ReclaimTarget,
931 ) -> Result<DurableStoreReclaimOperation, DbError> {
932 let DurableStoreReclaimOperation::Authorized {
933 authorization,
934 activation,
935 } = &expected
936 else {
937 return Err(DbError::Message(
938 "only an authorized reclaim can record target absence".to_string(),
939 ));
940 };
941 if &target != authorization.target() {
942 return Err(DbError::Message(
943 "verified reclaim target differs from its signed exact reference".to_string(),
944 ));
945 }
946 let next = DurableStoreReclaimOperation::AbsentVerified {
947 authorization: authorization.clone(),
948 authorization_activation: activation.clone(),
949 target,
950 };
951 let reclaimed =
952 ReclaimedStorePackage::absent_verified(authorization.clone(), activation.clone())
953 .map_err(store_reclaim_journal_error)?;
954 next.validate().map_err(store_reclaim_journal_error)?;
955 self.call_store(move |session| {
956 session.mark_store_reclaim_target_absent(expected, next, reclaimed)
957 })
958 .await
959 }
960
961 pub async fn replace_store_reclaim_candidate(
962 &self,
963 expected: DurableStoreReclaimOperation,
964 replacement: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
965 ) -> Result<DurableStoreReclaimOperation, DbError> {
966 let current_candidate = expected.candidate().cloned().ok_or_else(|| {
967 DbError::Message("Store reclaim state has no replaceable candidate".to_string())
968 })?;
969 if current_candidate.reference != replacement.reference
970 || current_candidate.commit != replacement.commit
971 {
972 return Err(DbError::Message(
973 "Store reclaim candidate replacement changes its signed commit".to_string(),
974 ));
975 }
976 let next = match &expected {
977 DurableStoreReclaimOperation::AuthorizationCandidate { object, .. } => {
978 DurableStoreReclaimOperation::AuthorizationCandidate {
979 object: object.clone(),
980 candidate: Box::new(replacement),
981 }
982 }
983 DurableStoreReclaimOperation::ReceiptCandidate {
984 authorization,
985 authorization_activation,
986 object,
987 ..
988 } => DurableStoreReclaimOperation::ReceiptCandidate {
989 authorization: authorization.clone(),
990 authorization_activation: authorization_activation.clone(),
991 object: object.clone(),
992 candidate: Box::new(replacement),
993 },
994 _ => {
995 return Err(DbError::Message(
996 "Store reclaim state has no replaceable candidate".to_string(),
997 ));
998 }
999 };
1000 next.validate().map_err(store_reclaim_journal_error)?;
1001 self.call_store(move |session| {
1002 session.replace_store_reclaim_candidate(expected, current_candidate, next)
1003 })
1004 .await
1005 }
1006
1007 pub async fn begin_store_reclaim_candidate_replacement(
1008 &self,
1009 expected: DurableStoreReclaimOperation,
1010 replacement: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
1011 nonactivation: coven_protocol::remote_object::VerifiedCandidateNonactivation,
1012 ) -> Result<DurableStoreReclaimOperation, DbError> {
1013 let object = expected.object().cloned().ok_or_else(|| {
1014 DbError::Message("Store reclaim operation has no replaceable object".to_string())
1015 })?;
1016 let losing_candidate = expected.candidate().cloned().ok_or_else(|| {
1017 DbError::Message("Store reclaim operation has no losing candidate".to_string())
1018 })?;
1019 if nonactivation.candidate_reference().map_err(DbError::from)? != losing_candidate.reference
1020 {
1021 return Err(DbError::Message(
1022 "verified nonactivation names another Store reclaim candidate".to_string(),
1023 ));
1024 }
1025 let nonactivation = nonactivation.into_durable();
1026 let proof = nonactivation.proof().clone();
1027 let loss = StoreReclaimCandidateLoss {
1028 candidate: Box::new(losing_candidate.clone()),
1029 proof: proof.clone(),
1030 };
1031 let next = match &expected {
1032 DurableStoreReclaimOperation::AuthorizationCandidate { .. } => {
1033 DurableStoreReclaimOperation::AuthorizationReplacing {
1034 object: Box::new(object.clone()),
1035 candidate: Box::new(replacement.clone()),
1036 losing: Box::new(loss),
1037 }
1038 }
1039 DurableStoreReclaimOperation::ReceiptCandidate {
1040 authorization,
1041 authorization_activation,
1042 ..
1043 } => DurableStoreReclaimOperation::ReceiptReplacing {
1044 authorization: authorization.clone(),
1045 authorization_activation: authorization_activation.clone(),
1046 object: Box::new(object.clone()),
1047 candidate: Box::new(replacement.clone()),
1048 losing: Box::new(loss),
1049 },
1050 _ => {
1051 return Err(DbError::Message(
1052 "Store reclaim operation is not awaiting candidate publication".to_string(),
1053 ));
1054 }
1055 };
1056 next.validate().map_err(store_reclaim_journal_error)?;
1057 if nonactivation.candidate().canonical_signed_bytes != losing_candidate.commit.to_bytes() {
1058 return Err(DbError::Message(
1059 "verified nonactivation bytes differ from the Store reclaim candidate".to_string(),
1060 ));
1061 }
1062 let replacement_remotes = object
1063 .remote_objects(&replacement)
1064 .map_err(store_reclaim_journal_error)?;
1065 self.call_store(move |session| {
1066 session.begin_store_reclaim_candidate_replacement(
1067 expected,
1068 next,
1069 replacement_remotes,
1070 nonactivation,
1071 losing_candidate,
1072 )
1073 })
1074 .await
1075 }
1076
1077 pub async fn store_reclaim_replacement_cleanup_targets(
1078 &self,
1079 expected: DurableStoreReclaimOperation,
1080 ) -> Result<Vec<CandidateCleanupObject>, DbError> {
1081 self.call_store(move |session| session.store_reclaim_replacement_cleanup_targets(&expected))
1082 .await
1083 }
1084
1085 pub async fn complete_store_reclaim_candidate_replacement(
1086 &self,
1087 expected: DurableStoreReclaimOperation,
1088 ) -> Result<DurableStoreReclaimOperation, DbError> {
1089 let losing = expected.losing_candidate().cloned().ok_or_else(|| {
1090 DbError::Message("Store reclaim operation has no replacement cleanup".to_string())
1091 })?;
1092 let next = match &expected {
1093 DurableStoreReclaimOperation::AuthorizationReplacing {
1094 object, candidate, ..
1095 } => DurableStoreReclaimOperation::AuthorizationCandidate {
1096 object: object.clone(),
1097 candidate: candidate.clone(),
1098 },
1099 DurableStoreReclaimOperation::ReceiptReplacing {
1100 authorization,
1101 authorization_activation,
1102 object,
1103 candidate,
1104 ..
1105 } => DurableStoreReclaimOperation::ReceiptCandidate {
1106 authorization: authorization.clone(),
1107 authorization_activation: authorization_activation.clone(),
1108 object: object.clone(),
1109 candidate: candidate.clone(),
1110 },
1111 _ => {
1112 return Err(DbError::Message(
1113 "Store reclaim operation has no replacement cleanup".to_string(),
1114 ));
1115 }
1116 };
1117 next.validate().map_err(store_reclaim_journal_error)?;
1118 self.call_store(move |session| {
1119 session.complete_store_reclaim_candidate_replacement(expected, losing, next)
1120 })
1121 .await
1122 }
1123
1124 #[cfg(any(test, feature = "test-utils"))]
1127 pub async fn stored_blob_has_snapshot_owner_for_test(
1128 &self,
1129 stored: coven_protocol::blob::locator::StoredBlobRef,
1130 ) -> Result<bool, DbError> {
1131 self.call_store(move |session| session.stored_blob_has_snapshot_owner_for_test(&stored))
1132 .await
1133 }
1134
1135 #[cfg(any(test, feature = "test-utils"))]
1136 pub async fn stored_blob_reclaim_candidates_for_test(
1137 &self,
1138 ) -> Result<
1139 Vec<(
1140 coven_protocol::blob::locator::StoredBlobRef,
1141 Vec<StoreBatchCommitRef>,
1142 )>,
1143 DbError,
1144 > {
1145 self.stored_blob_reclaim_candidates().await
1146 }
1147}