1use crate::store::store_session::StoreRecords;
2use crate::*;
3use coven_keys::encryption::EncryptionService;
4use coven_protocol::store_commit::StoreBatchCommitRef;
5use rusqlite::{Connection, OptionalExtension};
6
7use super::*;
8
9enum CircleActivationCommitLookup {
13 Absent,
14 Reclaimed { stream_id: String, sequence: u64 },
15 Retained(StoreBatchCommitRef),
16}
17
18pub(crate) fn circle_activation_commit_ref_on(
22 conn: &Connection,
23 circle_id: coven_protocol::circle::CircleId,
24 control: &coven_protocol::circle::CircleControlCoord,
25) -> Result<Option<StoreBatchCommitRef>, DbError> {
26 match circle_activation_commit_lookup_on(conn, circle_id, control)? {
27 CircleActivationCommitLookup::Absent => Ok(None),
28 CircleActivationCommitLookup::Reclaimed {
29 stream_id,
30 sequence,
31 } => Err(DbError::Message(format!(
32 "Circle {circle_id} activation commit {stream_id}/{sequence} is not retained"
33 ))),
34 CircleActivationCommitLookup::Retained(reference) => Ok(Some(reference)),
35 }
36}
37
38pub(crate) fn retained_circle_activation_commit_ref_on(
41 conn: &Connection,
42 circle_id: coven_protocol::circle::CircleId,
43 control: &coven_protocol::circle::CircleControlCoord,
44) -> Result<Option<StoreBatchCommitRef>, DbError> {
45 Ok(
46 match circle_activation_commit_lookup_on(conn, circle_id, control)? {
47 CircleActivationCommitLookup::Retained(reference) => Some(reference),
48 CircleActivationCommitLookup::Absent
49 | CircleActivationCommitLookup::Reclaimed { .. } => None,
50 },
51 )
52}
53
54fn circle_activation_commit_lookup_on(
55 conn: &Connection,
56 circle_id: coven_protocol::circle::CircleId,
57 control: &coven_protocol::circle::CircleControlCoord,
58) -> Result<CircleActivationCommitLookup, DbError> {
59 let control_coord = serde_json::to_string(control)
60 .map_err(|error| DbError::context("serialize Circle control coordinate", error))?;
61 let stored = conn
62 .query_row(
63 "SELECT stream_id, seq, commit_hash
64 FROM circle_control_activations
65 WHERE circle_id = ?1 AND control_coord = ?2",
66 rusqlite::params![circle_id.to_string(), control_coord],
67 |row| {
68 Ok((
69 row.get::<_, String>(0)?,
70 row.get::<_, i64>(1)?,
71 row.get::<_, String>(2)?,
72 ))
73 },
74 )
75 .optional()
76 .map_err(DbError::from)?;
77 let Some((stream_id, sequence_sql, commit_hash)) = stored else {
78 return Ok(CircleActivationCommitLookup::Absent);
79 };
80 let sequence = Database::sequence_from_sqlite(&stream_id, sequence_sql)?;
81 let stored_ref: Option<String> = conn
82 .query_row(
83 "SELECT commit_ref FROM retained_merge_materializations
84 WHERE device_id = ?1 AND seq = ?2",
85 rusqlite::params![&stream_id, sequence_sql],
86 |row| row.get(0),
87 )
88 .optional()
89 .map_err(DbError::from)?;
90 let Some(stored_ref) = stored_ref else {
91 return Ok(CircleActivationCommitLookup::Reclaimed {
92 stream_id,
93 sequence,
94 });
95 };
96 let reference = crate::store::materialized_commit_index::parse_stored_commit_ref(
97 &stream_id,
98 sequence,
99 &stored_ref,
100 )?;
101 if reference.commit_hash.to_string() != commit_hash {
102 return Err(DbError::Message(format!(
103 "Circle {circle_id} activation index differs from its retained commit"
104 )));
105 }
106 Ok(CircleActivationCommitLookup::Retained(reference))
107}
108
109impl StoreSession<'_> {
110 fn circle_control_covers_strictly(
111 &mut self,
112 root: &coven_protocol::store_commit::StoreRootRef,
113 circle_id: coven_protocol::circle::CircleId,
114 covering: &coven_protocol::circle::CircleControlCoord,
115 covered: &coven_protocol::circle::CircleControlCoord,
116 ) -> Result<bool, DbError> {
117 let Some(covering_reference) = StoreDatabase::verified_circle_activation_on(
118 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
119 self.verified_store_authority,
120 root,
121 circle_id,
122 covering,
123 )?
124 else {
125 return Ok(false);
126 };
127 StoreDatabase::verified_circle_control_covers_on(
128 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
129 self.verified_store_authority,
130 root,
131 circle_id,
132 &covering_reference.control,
133 covered,
134 )
135 }
136
137 fn circle_epoch_access(
138 &mut self,
139 root: &coven_protocol::store_commit::StoreRootRef,
140 circle_id: coven_protocol::circle::CircleId,
141 expected_control: &coven_protocol::circle::CircleControlCoord,
142 ) -> Result<Option<coven_protocol::circle_activation::CircleEpochAccess>, DbError> {
143 self.verified_store_authority.retained_replay_inputs_on(
144 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
145 root,
146 )?;
147 let Some(activation) = self
148 .verified_store_authority
149 .verified_circle_activation_on(
150 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
151 circle_id,
152 expected_control,
153 )?
154 else {
155 return Ok(None);
156 };
157 activation.epoch_access().map_err(DbError::from)
158 }
159
160 fn circle_historical_package_keyring(
161 &mut self,
162 root: &coven_protocol::store_commit::StoreRootRef,
163 circle_id: coven_protocol::circle::CircleId,
164 expected_control: &coven_protocol::circle::CircleControlCoord,
165 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
166 ) -> Result<Option<String>, DbError> {
167 let Some(state) = super::circle_operations::circle_current_state_on(self.conn, circle_id)?
168 else {
169 return Ok(None);
170 };
171 let Some(current) = state
172 .authoring_state()
173 .or_else(|| state.closing_authoring_state())
174 else {
175 return Ok(None);
176 };
177 let Some(historical) = StoreDatabase::verified_circle_activation_on(
178 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
179 self.verified_store_authority,
180 root,
181 circle_id,
182 expected_control,
183 )?
184 else {
185 return Ok(None);
186 };
187 if !StoreDatabase::verified_circle_control_covers_on(
188 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
189 self.verified_store_authority,
190 root,
191 circle_id,
192 ¤t.control,
193 expected_control,
194 )? || current.control.value.epoch_id() != historical.control.value.epoch_id()
195 || current.control.value.key_fingerprint() != expected_key_fingerprint
196 || historical.control.value.key_fingerprint() != expected_key_fingerprint
197 {
198 return Ok(None);
199 }
200 let coven_protocol::circle::CircleAccessDisposition::Active { keyring, .. } =
201 ¤t.access.disposition
202 else {
203 return Ok(None);
204 };
205 let parsed =
206 coven_keys::encryption::MasterKeyring::from_serialized(keyring).map_err(|error| {
207 DbError::context(
208 format!("parse Circle {circle_id} historical package keyring"),
209 error,
210 )
211 })?;
212 let encryption = EncryptionService::from(parsed);
213 if encryption
214 .service_for_fingerprint(expected_key_fingerprint.as_bytes())
215 .is_err()
216 {
217 return Ok(None);
218 }
219 Ok(Some(keyring.clone()))
220 }
221
222 fn verified_circle_activation_context(
223 &mut self,
224 root: &coven_protocol::store_commit::StoreRootRef,
225 circle_id: coven_protocol::circle::CircleId,
226 control: &coven_protocol::circle::CircleControlCoord,
227 ) -> Result<
228 Option<(
229 coven_protocol::circle_activation::VerifiedCircleReference,
230 StoreBatchCommitRef,
231 )>,
232 DbError,
233 > {
234 let Some(commit) = circle_activation_commit_ref_on(self.conn, circle_id, control)? else {
235 return Ok(None);
236 };
237 let activation = StoreDatabase::verified_circle_activation_on(
238 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
239 self.verified_store_authority,
240 root,
241 circle_id,
242 control,
243 )?
244 .ok_or_else(|| {
245 DbError::Message(format!(
246 "Circle {circle_id} activation context lost control {control:?}"
247 ))
248 })?;
249 Ok(Some((activation, commit)))
250 }
251
252 fn circle_blob_opening_protection(
253 &mut self,
254 root: &coven_protocol::store_commit::StoreRootRef,
255 circle_id: coven_protocol::circle::CircleId,
256 expected_control: &coven_protocol::circle::CircleControlCoord,
257 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
258 ) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
259 circle_blob_opening_protection_on(
260 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
261 self.verified_store_authority,
262 root,
263 circle_id,
264 expected_control,
265 expected_key_fingerprint,
266 )
267 }
268
269 fn verified_circle_activation(
270 &mut self,
271 root: &coven_protocol::store_commit::StoreRootRef,
272 circle_id: coven_protocol::circle::CircleId,
273 control: &coven_protocol::circle::CircleControlCoord,
274 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
275 StoreDatabase::verified_circle_activation_on(
276 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
277 self.verified_store_authority,
278 root,
279 circle_id,
280 control,
281 )
282 }
283
284 fn circle_restore_head(
285 &mut self,
286 root: &coven_protocol::store_commit::StoreRootRef,
287 circle_id: coven_protocol::circle::CircleId,
288 controls: &[coven_protocol::circle::CircleControlCoord],
289 ) -> Result<
290 Option<(
291 coven_protocol::circle::CircleControlCoord,
292 StoreBatchCommitRef,
293 )>,
294 DbError,
295 > {
296 let Some(head) = StoreDatabase::head_circle_control_on(
297 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
298 self.verified_store_authority,
299 root,
300 circle_id,
301 controls,
302 )?
303 else {
304 return Ok(None);
305 };
306 let commit =
307 circle_activation_commit_ref_on(self.conn, circle_id, &head)?.ok_or_else(|| {
308 DbError::Message(format!(
309 "Circle {circle_id} head control has no activating commit"
310 ))
311 })?;
312 Ok(Some((head, commit)))
313 }
314
315 fn retained_circle_activation_commit_ref(
316 &self,
317 circle_id: coven_protocol::circle::CircleId,
318 control: &coven_protocol::circle::CircleControlCoord,
319 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
320 retained_circle_activation_commit_ref_on(self.conn, circle_id, control)
321 }
322
323 fn verified_circle_control_coord_covers(
324 &mut self,
325 root: &coven_protocol::store_commit::StoreRootRef,
326 circle_id: coven_protocol::circle::CircleId,
327 covering: &coven_protocol::circle::CircleControlCoord,
328 covered: &coven_protocol::circle::CircleControlCoord,
329 ) -> Result<bool, DbError> {
330 let Some(reference) = StoreDatabase::verified_circle_activation_on(
331 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
332 self.verified_store_authority,
333 root,
334 circle_id,
335 covering,
336 )?
337 else {
338 return Ok(false);
339 };
340 StoreDatabase::verified_circle_control_covers_on(
341 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
342 self.verified_store_authority,
343 root,
344 circle_id,
345 &reference.control,
346 covered,
347 )
348 }
349
350 fn verified_circle_control_covers(
351 &mut self,
352 root: &coven_protocol::store_commit::StoreRootRef,
353 circle_id: coven_protocol::circle::CircleId,
354 current: &coven_protocol::circle::PreparedCircleControl,
355 prior: &coven_protocol::circle::CircleControlCoord,
356 ) -> Result<bool, DbError> {
357 StoreDatabase::verified_circle_control_covers_on(
358 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
359 self.verified_store_authority,
360 root,
361 circle_id,
362 current,
363 prior,
364 )
365 }
366}
367
368impl StoreDatabase {
369 pub async fn circle_control_covers_strictly(
375 &self,
376 root: coven_protocol::store_commit::StoreRootRef,
377 circle_id: coven_protocol::circle::CircleId,
378 covering: &coven_protocol::circle::CircleControlCoord,
379 covered: &coven_protocol::circle::CircleControlCoord,
380 ) -> Result<bool, DbError> {
381 if covering == covered {
382 return Ok(false);
383 }
384 let covering = covering.clone();
385 let covered = covered.clone();
386 self.call_store(move |session| {
387 session.circle_control_covers_strictly(&root, circle_id, &covering, &covered)
388 })
389 .await
390 }
391
392 pub async fn circle_epoch_access(
393 &self,
394 root: coven_protocol::store_commit::StoreRootRef,
395 circle_id: coven_protocol::circle::CircleId,
396 expected_control: coven_protocol::circle::CircleControlCoord,
397 ) -> Result<Option<coven_protocol::circle_activation::CircleEpochAccess>, DbError> {
398 self.call_store(move |session| {
399 session.circle_epoch_access(&root, circle_id, &expected_control)
400 })
401 .await
402 }
403
404 pub async fn circle_historical_package_keyring(
405 &self,
406 root: coven_protocol::store_commit::StoreRootRef,
407 circle_id: coven_protocol::circle::CircleId,
408 expected_control: coven_protocol::circle::CircleControlCoord,
409 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
410 ) -> Result<Option<String>, DbError> {
411 self.call_store(move |session| {
412 session.circle_historical_package_keyring(
413 &root,
414 circle_id,
415 &expected_control,
416 expected_key_fingerprint,
417 )
418 })
419 .await
420 }
421
422 pub async fn verified_circle_activation_context(
423 &self,
424 root: coven_protocol::store_commit::StoreRootRef,
425 circle_id: coven_protocol::circle::CircleId,
426 control: coven_protocol::circle::CircleControlCoord,
427 ) -> Result<
428 Option<(
429 coven_protocol::circle_activation::VerifiedCircleReference,
430 StoreBatchCommitRef,
431 )>,
432 DbError,
433 > {
434 self.call_store(move |session| {
435 session.verified_circle_activation_context(&root, circle_id, &control)
436 })
437 .await
438 }
439
440 pub async fn circle_blob_opening_protection(
441 &self,
442 root: coven_protocol::store_commit::StoreRootRef,
443 circle_id: coven_protocol::circle::CircleId,
444 expected_control: coven_protocol::circle::CircleControlCoord,
445 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
446 ) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
447 self.call_store(move |session| {
448 session.circle_blob_opening_protection(
449 &root,
450 circle_id,
451 &expected_control,
452 expected_key_fingerprint,
453 )
454 })
455 .await
456 }
457
458 pub async fn verified_circle_activation(
459 &self,
460 root: coven_protocol::store_commit::StoreRootRef,
461 circle_id: coven_protocol::circle::CircleId,
462 control: coven_protocol::circle::CircleControlCoord,
463 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
464 self.call_store(move |session| {
465 session.verified_circle_activation(&root, circle_id, &control)
466 })
467 .await
468 }
469
470 pub async fn circle_restore_head(
471 &self,
472 root: coven_protocol::store_commit::StoreRootRef,
473 circle_id: coven_protocol::circle::CircleId,
474 controls: Vec<coven_protocol::circle::CircleControlCoord>,
475 ) -> Result<
476 Option<(
477 coven_protocol::circle::CircleControlCoord,
478 StoreBatchCommitRef,
479 )>,
480 DbError,
481 > {
482 self.call_store(move |session| session.circle_restore_head(&root, circle_id, &controls))
483 .await
484 }
485
486 pub async fn retained_circle_activation_commit_ref(
487 &self,
488 circle_id: coven_protocol::circle::CircleId,
489 control: coven_protocol::circle::CircleControlCoord,
490 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
491 self.call_store(move |session| {
492 session.retained_circle_activation_commit_ref(circle_id, &control)
493 })
494 .await
495 }
496
497 pub async fn verified_circle_control_coord_covers(
498 &self,
499 root: coven_protocol::store_commit::StoreRootRef,
500 circle_id: coven_protocol::circle::CircleId,
501 covering: coven_protocol::circle::CircleControlCoord,
502 covered: coven_protocol::circle::CircleControlCoord,
503 ) -> Result<bool, DbError> {
504 self.call_store(move |session| {
505 session.verified_circle_control_coord_covers(&root, circle_id, &covering, &covered)
506 })
507 .await
508 }
509
510 pub(super) fn head_circle_control_on(
517 records: StoreRecords<'_>,
518 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
519 root: &coven_protocol::store_commit::StoreRootRef,
520 circle_id: coven_protocol::circle::CircleId,
521 controls: &[coven_protocol::circle::CircleControlCoord],
522 ) -> Result<Option<coven_protocol::circle::CircleControlCoord>, DbError> {
523 let mut retained: Vec<(
526 coven_protocol::circle::CircleControlCoord,
527 coven_protocol::circle::PreparedCircleControl,
528 )> = Vec::new();
529 for coord in controls {
530 let Some(activation_commit) =
531 records.retained_circle_activation_commit_ref(circle_id, coord)?
532 else {
533 continue;
534 };
535 let materialization = Self::load_retained_merge_materialization_by_ref_on(
536 records,
537 root,
538 authority,
539 &activation_commit,
540 )?;
541 let reference = materialization.circle_activation(circle_id, coord)?;
542 retained.push((coord.clone(), reference.control));
543 }
544 let mut head: Option<coven_protocol::circle::CircleControlCoord> = None;
545 for (index, (candidate, _)) in retained.iter().enumerate() {
546 let mut covered = false;
547 for (other_index, (_, other_control)) in retained.iter().enumerate() {
548 if other_index == index {
549 continue;
550 }
551 if Self::verified_circle_control_covers_on(
552 records,
553 authority,
554 root,
555 circle_id,
556 other_control,
557 candidate,
558 )? {
559 covered = true;
560 break;
561 }
562 }
563 if !covered {
564 if head.is_some() {
565 return Err(DbError::Message(format!(
566 "Circle {circle_id} has multiple head controls"
567 )));
568 }
569 head = Some(candidate.clone());
570 }
571 }
572 Ok(head)
573 }
574
575 pub(super) fn verified_circle_activation_on(
576 records: StoreRecords<'_>,
577 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
578 root: &coven_protocol::store_commit::StoreRootRef,
579 circle_id: coven_protocol::circle::CircleId,
580 control: &coven_protocol::circle::CircleControlCoord,
581 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
582 let Some(activation_commit) = records.circle_activation_commit_ref(circle_id, control)?
583 else {
584 return Ok(None);
585 };
586 let retained = Self::load_retained_merge_materialization_by_ref_on(
587 records,
588 root,
589 authority,
590 &activation_commit,
591 )?;
592 retained.circle_activation(circle_id, control).map(Some)
593 }
594
595 pub async fn verified_circle_control_covers(
596 &self,
597 root: coven_protocol::store_commit::StoreRootRef,
598 circle_id: coven_protocol::circle::CircleId,
599 current: coven_protocol::circle::PreparedCircleControl,
600 prior: coven_protocol::circle::CircleControlCoord,
601 ) -> Result<bool, DbError> {
602 self.call_store(move |session| {
603 session.verified_circle_control_covers(&root, circle_id, ¤t, &prior)
604 })
605 .await
606 }
607
608 pub(super) fn verified_circle_control_covers_on(
609 records: StoreRecords<'_>,
610 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
611 root: &coven_protocol::store_commit::StoreRootRef,
612 circle_id: coven_protocol::circle::CircleId,
613 current: &coven_protocol::circle::PreparedCircleControl,
614 prior: &coven_protocol::circle::CircleControlCoord,
615 ) -> Result<bool, DbError> {
616 if current.value.circle_id != circle_id {
617 return Err(DbError::Message(
618 "Circle control lineage starts outside its Circle".to_string(),
619 ));
620 }
621 if current.coord == *prior {
622 return Ok(true);
623 }
624 let mut pending = current
625 .value
626 .access_epoch()
627 .covered_control_heads
628 .iter()
629 .map(|head| (current.clone(), head.coord.clone()))
630 .collect::<Vec<_>>();
631 let mut visited = std::collections::BTreeSet::new();
632 while let Some((successor, coordinate)) = pending.pop() {
633 if !visited.insert(coordinate.clone()) {
634 continue;
635 }
636 let predecessor = Self::verified_circle_activation_on(
637 records,
638 authority,
639 root,
640 circle_id,
641 &coordinate,
642 )?
643 .ok_or_else(|| {
644 DbError::Message(format!(
645 "Circle {circle_id} control lineage omits retained control {coordinate:?}"
646 ))
647 })?;
648 if !successor.value.causally_covers(&predecessor.control.value) {
649 return Err(DbError::Message(format!(
650 "Circle {circle_id} control lineage contains a non-causal edge"
651 )));
652 }
653 if predecessor.control.coord == *prior {
654 return Ok(true);
655 }
656 pending.extend(
657 predecessor
658 .control
659 .value
660 .access_epoch()
661 .covered_control_heads
662 .iter()
663 .map(|head| (predecessor.control.clone(), head.coord.clone())),
664 );
665 }
666 Ok(false)
667 }
668}
669
670pub(crate) fn circle_blob_opening_protection_on(
671 records: StoreRecords<'_>,
672 verified_store: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
673 root: &coven_protocol::store_commit::StoreRootRef,
674 circle_id: coven_protocol::circle::CircleId,
675 expected_control: &coven_protocol::circle::CircleControlCoord,
676 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
677) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
678 let Some(authority) = StoreDatabase::verified_circle_activation_on(
679 records,
680 verified_store,
681 root,
682 circle_id,
683 expected_control,
684 )?
685 else {
686 return Err(DbError::Message(format!(
687 "Circle {circle_id} has no retained authority for control {expected_control:?}"
688 )));
689 };
690 if authority.control.value.key_fingerprint() != expected_key_fingerprint {
691 return Err(DbError::Message(format!(
692 "Circle {circle_id} blob key {expected_key_fingerprint} differs from \
693 exact control {expected_control:?}"
694 )));
695 }
696
697 let controls = records.circle_controls(circle_id)?;
698
699 let mut retained_key = None;
700 for control in controls {
701 let activation = StoreDatabase::verified_circle_activation_on(
702 records,
703 verified_store,
704 root,
705 circle_id,
706 &control,
707 )?
708 .ok_or_else(|| {
709 DbError::Message(format!(
710 "Circle {circle_id} activation index lost control {control:?}"
711 ))
712 })?;
713 let Some((generation, key)) = activation
714 .retained_key_entry(expected_key_fingerprint)
715 .map_err(DbError::from)?
716 else {
717 continue;
718 };
719 let candidate = EncryptionService::from_key_at_generation(generation, key);
720 if retained_key
721 .as_ref()
722 .is_some_and(|existing: &EncryptionService| {
723 existing.current_generation() != generation || existing.key_bytes() != key
724 })
725 {
726 return Err(DbError::Message(format!(
727 "Circle {circle_id} retains inconsistent key material for fingerprint \
728 {expected_key_fingerprint}"
729 )));
730 }
731 retained_key = Some(candidate);
732 }
733 retained_key
734 .map(coven_protocol::objects::BlobSpoolProtection::Opaque)
735 .ok_or_else(|| {
736 DbError::Message(format!(
737 "Circle {circle_id} retains no local key for fingerprint \
738 {expected_key_fingerprint}"
739 ))
740 })
741}
742
743impl crate::store::store_session::StoreTransaction<'_, '_> {
744 pub(super) fn circle_blob_opening_protection(
745 self,
746 verified_store: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
747 root: &coven_protocol::store_commit::StoreRootRef,
748 circle_id: coven_protocol::circle::CircleId,
749 expected_control: &coven_protocol::circle::CircleControlCoord,
750 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
751 ) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
752 circle_blob_opening_protection_on(
753 crate::store::store_session::StoreRecords::new(self.transaction, self.store_dir),
754 verified_store,
755 root,
756 circle_id,
757 expected_control,
758 expected_key_fingerprint,
759 )
760 }
761}