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
9pub enum CirclePackageAccess {
12 Exact(coven_protocol::circle_activation::CircleEpochAccess),
13 Historical(String),
14}
15
16enum CircleActivationCommitLookup {
20 Absent,
21 Reclaimed { stream_id: String, sequence: u64 },
22 Retained(StoreBatchCommitRef),
23}
24
25pub(crate) fn circle_activation_commit_ref_on(
29 conn: &Connection,
30 circle_id: coven_protocol::circle::CircleId,
31 control: &coven_protocol::circle::CircleControlCoord,
32) -> Result<Option<StoreBatchCommitRef>, DbError> {
33 match circle_activation_commit_lookup_on(conn, circle_id, control)? {
34 CircleActivationCommitLookup::Absent => Ok(None),
35 CircleActivationCommitLookup::Reclaimed {
36 stream_id,
37 sequence,
38 } => Err(DbError::Message(format!(
39 "Circle {circle_id} activation commit {stream_id}/{sequence} is not retained"
40 ))),
41 CircleActivationCommitLookup::Retained(reference) => Ok(Some(reference)),
42 }
43}
44
45pub(crate) fn retained_circle_activation_commit_ref_on(
48 conn: &Connection,
49 circle_id: coven_protocol::circle::CircleId,
50 control: &coven_protocol::circle::CircleControlCoord,
51) -> Result<Option<StoreBatchCommitRef>, DbError> {
52 Ok(
53 match circle_activation_commit_lookup_on(conn, circle_id, control)? {
54 CircleActivationCommitLookup::Retained(reference) => Some(reference),
55 CircleActivationCommitLookup::Absent
56 | CircleActivationCommitLookup::Reclaimed { .. } => None,
57 },
58 )
59}
60
61fn circle_activation_commit_lookup_on(
62 conn: &Connection,
63 circle_id: coven_protocol::circle::CircleId,
64 control: &coven_protocol::circle::CircleControlCoord,
65) -> Result<CircleActivationCommitLookup, DbError> {
66 let control_coord = serde_json::to_string(control)
67 .map_err(|error| DbError::context("serialize Circle control coordinate", error))?;
68 let stored = conn
69 .query_row(
70 "SELECT stream_id, seq, commit_hash
71 FROM circle_control_activations
72 WHERE circle_id = ?1 AND control_coord = ?2",
73 rusqlite::params![circle_id.to_string(), control_coord],
74 |row| {
75 Ok((
76 row.get::<_, String>(0)?,
77 row.get::<_, i64>(1)?,
78 row.get::<_, String>(2)?,
79 ))
80 },
81 )
82 .optional()
83 .map_err(DbError::from)?;
84 let Some((stream_id, sequence_sql, commit_hash)) = stored else {
85 return Ok(CircleActivationCommitLookup::Absent);
86 };
87 let sequence = Database::sequence_from_sqlite(&stream_id, sequence_sql)?;
88 let stored_ref: Option<String> = conn
89 .query_row(
90 "SELECT commit_ref FROM retained_merge_materializations
91 WHERE device_id = ?1 AND seq = ?2",
92 rusqlite::params![&stream_id, sequence_sql],
93 |row| row.get(0),
94 )
95 .optional()
96 .map_err(DbError::from)?;
97 let Some(stored_ref) = stored_ref else {
98 return Ok(CircleActivationCommitLookup::Reclaimed {
99 stream_id,
100 sequence,
101 });
102 };
103 let reference = crate::store::materialized_commit_index::parse_stored_commit_ref(
104 &stream_id,
105 sequence,
106 &stored_ref,
107 )?;
108 if reference.commit_hash.to_string() != commit_hash {
109 return Err(DbError::Message(format!(
110 "Circle {circle_id} activation index differs from its retained commit"
111 )));
112 }
113 Ok(CircleActivationCommitLookup::Retained(reference))
114}
115
116impl StoreSession<'_> {
117 fn circle_control_covers_strictly(
118 &mut self,
119 root: &coven_protocol::store_commit::StoreRootRef,
120 circle_id: coven_protocol::circle::CircleId,
121 covering: &coven_protocol::circle::CircleControlCoord,
122 covered: &coven_protocol::circle::CircleControlCoord,
123 ) -> Result<bool, DbError> {
124 let Some(covering_reference) = StoreDatabase::verified_circle_activation_on(
125 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
126 self.verified_store_authority,
127 root,
128 circle_id,
129 covering,
130 )?
131 else {
132 return Ok(false);
133 };
134 StoreDatabase::verified_circle_control_covers_on(
135 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
136 self.verified_store_authority,
137 root,
138 circle_id,
139 &covering_reference.control,
140 covered,
141 )
142 }
143
144 fn circle_epoch_access(
145 &mut self,
146 root: &coven_protocol::store_commit::StoreRootRef,
147 circle_id: coven_protocol::circle::CircleId,
148 expected_control: &coven_protocol::circle::CircleControlCoord,
149 ) -> Result<Option<coven_protocol::circle_activation::CircleEpochAccess>, DbError> {
150 self.verified_store_authority.retained_replay_inputs_on(
151 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
152 root,
153 )?;
154 let Some(activation) = self
155 .verified_store_authority
156 .verified_circle_activation_on(
157 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
158 circle_id,
159 expected_control,
160 )?
161 else {
162 return Ok(None);
163 };
164 activation.epoch_access().map_err(DbError::from)
165 }
166
167 fn circle_package_access(
168 &mut self,
169 root: &coven_protocol::store_commit::StoreRootRef,
170 circle_id: coven_protocol::circle::CircleId,
171 expected_control: &coven_protocol::circle::CircleControlCoord,
172 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
173 activations: &[coven_protocol::circle_activation::VerifiedCircleReference],
174 ) -> Result<Option<CirclePackageAccess>, DbError> {
175 let Some(state) =
176 self.circle_current_state_with_activations(root, circle_id, activations)?
177 else {
178 return Ok(None);
179 };
180 if state.is_deleted() {
181 return Ok(None);
182 }
183 let exact = match activations.iter().find(|activation| {
184 activation.circle_id == circle_id && &activation.control.coord == expected_control
185 }) {
186 Some(activation) => activation.epoch_access().map_err(DbError::from)?,
187 None => self.circle_epoch_access(root, circle_id, expected_control)?,
188 };
189 if let Some(access) = exact {
190 return Ok(Some(CirclePackageAccess::Exact(access)));
193 }
194 self.circle_historical_package_keyring(
195 root,
196 state,
197 expected_control,
198 expected_key_fingerprint,
199 activations,
200 )
201 .map(|keyring| keyring.map(CirclePackageAccess::Historical))
202 }
203
204 fn circle_historical_package_keyring(
205 &mut self,
206 root: &coven_protocol::store_commit::StoreRootRef,
207 state: coven_protocol::circle_activation::CircleCurrentState,
208 expected_control: &coven_protocol::circle::CircleControlCoord,
209 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
210 activations: &[coven_protocol::circle_activation::VerifiedCircleReference],
211 ) -> Result<Option<String>, DbError> {
212 let circle_id = state.circle_id();
213 if !state.verify() {
214 return Err(DbError::Message(
215 "invalid projected Circle package state".to_string(),
216 ));
217 }
218 let Some(current) = state
219 .authoring_state()
220 .or_else(|| state.closing_authoring_state())
221 else {
222 return Ok(None);
223 };
224 let Some(historical) = verified_circle_activation_with_prefix_on(
225 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
226 self.verified_store_authority,
227 root,
228 circle_id,
229 expected_control,
230 activations,
231 )?
232 else {
233 return Ok(None);
234 };
235 if !verified_circle_control_covers_with_prefix_on(
236 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
237 self.verified_store_authority,
238 root,
239 circle_id,
240 ¤t.control,
241 expected_control,
242 activations,
243 )? || current.control.value.epoch_id() != historical.control.value.epoch_id()
244 || current.control.value.key_fingerprint() != expected_key_fingerprint
245 || historical.control.value.key_fingerprint() != expected_key_fingerprint
246 {
247 return Ok(None);
248 }
249 let coven_protocol::circle::CircleAccessDisposition::Active { keyring, .. } =
250 ¤t.access.disposition
251 else {
252 return Ok(None);
253 };
254 let parsed =
255 coven_keys::encryption::MasterKeyring::from_serialized(keyring).map_err(|error| {
256 DbError::context(
257 format!("parse Circle {circle_id} historical package keyring"),
258 error,
259 )
260 })?;
261 let encryption = EncryptionService::from(parsed);
262 if encryption
263 .service_for_fingerprint(expected_key_fingerprint.as_bytes())
264 .is_err()
265 {
266 return Ok(None);
267 }
268 Ok(Some(keyring.clone()))
269 }
270
271 fn circle_current_state_with_activations(
272 &mut self,
273 root: &coven_protocol::store_commit::StoreRootRef,
274 circle_id: coven_protocol::circle::CircleId,
275 activations: &[coven_protocol::circle_activation::VerifiedCircleReference],
276 ) -> Result<Option<coven_protocol::circle_activation::CircleCurrentState>, DbError> {
277 use coven_protocol::circle_activation::CircleCurrentState;
278
279 let records = StoreRecords::new(self.conn, self.store_dir);
280 let mut pending = std::collections::BTreeMap::new();
281 for activation in activations
282 .iter()
283 .filter(|activation| activation.circle_id == circle_id)
284 {
285 if activation.control.value.store_root_hash != root.store_root_hash
286 || activation.reference.circle_id() != circle_id
287 || activation.reference.control() != &activation.control.coord
288 {
289 return Err(DbError::Message(
290 "prepared Circle activation differs from its Store or control reference"
291 .to_string(),
292 ));
293 }
294 let next = CircleCurrentState::from_verified_reference(activation)?;
295 let coordinate = activation.control.coord.clone();
296 if let Some((prior, _)) = pending.insert(coordinate.clone(), (activation, next)) {
297 if prior != activation {
298 return Err(DbError::Message(format!(
299 "Circle {circle_id} prepared history has conflicting copies of control {coordinate:?}"
300 )));
301 }
302 }
303 }
304 let dependencies = pending
305 .iter()
306 .map(|(coordinate, (activation, _))| {
307 let dependencies = activation
308 .control
309 .value
310 .access_epoch()
311 .covered_control_heads
312 .iter()
313 .map(|head| &head.coord)
314 .filter(|coordinate| pending.contains_key(*coordinate))
315 .cloned()
316 .collect::<std::collections::BTreeSet<_>>();
317 (coordinate.clone(), dependencies)
318 })
319 .collect::<std::collections::BTreeMap<_, _>>();
320 let mut applied = std::collections::BTreeSet::new();
321 let mut state = super::circle_operations::circle_current_state_on(self.conn, circle_id)?;
322 while !pending.is_empty() {
323 let coordinate = coven_protocol::causal_grants::canonical_ready_checkpoint(
324 pending
325 .keys()
326 .map(|coordinate| (coordinate, &dependencies[coordinate])),
327 &applied,
328 )
329 .ok_or_else(|| {
330 DbError::Message(format!(
331 "Circle {circle_id} prepared controls contain a causal cycle"
332 ))
333 })?;
334 let (activation, next) = pending.remove(&coordinate).ok_or_else(|| {
335 DbError::Message("ready prepared Circle control is absent".to_string())
336 })?;
337 applied.insert(coordinate);
338 let Some(current) = state.take() else {
339 if !activation.control.value.is_founder() {
340 return Err(DbError::Message(format!(
341 "Circle {circle_id} current state is absent for a prepared successor"
342 )));
343 }
344 state = Some(next);
345 continue;
346 };
347 if current
348 .resolved_control()
349 .is_some_and(|head| head.coordinate() == &activation.control.coord)
350 {
351 state = Some(if activation.local_access.is_some() {
354 next
355 } else {
356 current
357 });
358 continue;
359 }
360 let heads = match ¤t {
361 CircleCurrentState::ControlConflict { branches } => branches
362 .iter()
363 .map(|branch| branch.coordinate())
364 .collect::<Vec<_>>(),
365 _ => vec![current
366 .resolved_control()
367 .ok_or_else(|| {
368 DbError::Message("resolved Circle control is absent".to_string())
369 })?
370 .coordinate()],
371 };
372 let mut already_covered = false;
373 for head in heads {
374 let covering = verified_circle_activation_with_prefix_on(
375 records,
376 self.verified_store_authority,
377 root,
378 circle_id,
379 head,
380 activations,
381 )?
382 .ok_or_else(|| {
383 DbError::Message(format!(
384 "Circle {circle_id} current control has no verified activation"
385 ))
386 })?;
387 if verified_circle_control_covers_with_prefix_on(
388 records,
389 self.verified_store_authority,
390 root,
391 circle_id,
392 &covering.control,
393 &activation.control.coord,
394 activations,
395 )? {
396 already_covered = true;
397 break;
398 }
399 }
400 state = Some(if already_covered {
401 current
402 } else {
403 current.advance(next)?
404 });
405 }
406 Ok(state)
407 }
408
409 fn verified_circle_activation_context(
410 &mut self,
411 root: &coven_protocol::store_commit::StoreRootRef,
412 circle_id: coven_protocol::circle::CircleId,
413 control: &coven_protocol::circle::CircleControlCoord,
414 ) -> Result<
415 Option<(
416 coven_protocol::circle_activation::VerifiedCircleReference,
417 StoreBatchCommitRef,
418 )>,
419 DbError,
420 > {
421 let Some(commit) = circle_activation_commit_ref_on(self.conn, circle_id, control)? else {
422 return Ok(None);
423 };
424 let activation = StoreDatabase::verified_circle_activation_on(
425 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
426 self.verified_store_authority,
427 root,
428 circle_id,
429 control,
430 )?
431 .ok_or_else(|| {
432 DbError::Message(format!(
433 "Circle {circle_id} activation context lost control {control:?}"
434 ))
435 })?;
436 Ok(Some((activation, commit)))
437 }
438
439 fn circle_blob_opening_protection(
440 &mut self,
441 root: &coven_protocol::store_commit::StoreRootRef,
442 circle_id: coven_protocol::circle::CircleId,
443 expected_control: &coven_protocol::circle::CircleControlCoord,
444 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
445 ) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
446 circle_blob_opening_protection_on(
447 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
448 self.verified_store_authority,
449 root,
450 circle_id,
451 expected_control,
452 expected_key_fingerprint,
453 )
454 }
455
456 fn verified_circle_activation(
457 &mut self,
458 root: &coven_protocol::store_commit::StoreRootRef,
459 circle_id: coven_protocol::circle::CircleId,
460 control: &coven_protocol::circle::CircleControlCoord,
461 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
462 StoreDatabase::verified_circle_activation_on(
463 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
464 self.verified_store_authority,
465 root,
466 circle_id,
467 control,
468 )
469 }
470
471 fn circle_restore_head(
472 &mut self,
473 root: &coven_protocol::store_commit::StoreRootRef,
474 circle_id: coven_protocol::circle::CircleId,
475 controls: &[coven_protocol::circle::CircleControlCoord],
476 ) -> Result<
477 Option<(
478 coven_protocol::circle::CircleControlCoord,
479 StoreBatchCommitRef,
480 )>,
481 DbError,
482 > {
483 let Some(head) = StoreDatabase::head_circle_control_on(
484 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
485 self.verified_store_authority,
486 root,
487 circle_id,
488 controls,
489 )?
490 else {
491 return Ok(None);
492 };
493 let commit =
494 circle_activation_commit_ref_on(self.conn, circle_id, &head)?.ok_or_else(|| {
495 DbError::Message(format!(
496 "Circle {circle_id} head control has no activating commit"
497 ))
498 })?;
499 Ok(Some((head, commit)))
500 }
501
502 fn retained_circle_activation_commit_ref(
503 &self,
504 circle_id: coven_protocol::circle::CircleId,
505 control: &coven_protocol::circle::CircleControlCoord,
506 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
507 retained_circle_activation_commit_ref_on(self.conn, circle_id, control)
508 }
509
510 fn verified_circle_control_coord_covers(
511 &mut self,
512 root: &coven_protocol::store_commit::StoreRootRef,
513 circle_id: coven_protocol::circle::CircleId,
514 covering: &coven_protocol::circle::CircleControlCoord,
515 covered: &coven_protocol::circle::CircleControlCoord,
516 ) -> Result<bool, DbError> {
517 let Some(reference) = StoreDatabase::verified_circle_activation_on(
518 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
519 self.verified_store_authority,
520 root,
521 circle_id,
522 covering,
523 )?
524 else {
525 return Ok(false);
526 };
527 StoreDatabase::verified_circle_control_covers_on(
528 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
529 self.verified_store_authority,
530 root,
531 circle_id,
532 &reference.control,
533 covered,
534 )
535 }
536
537 fn verified_circle_control_covers(
538 &mut self,
539 root: &coven_protocol::store_commit::StoreRootRef,
540 circle_id: coven_protocol::circle::CircleId,
541 current: &coven_protocol::circle::PreparedCircleControl,
542 prior: &coven_protocol::circle::CircleControlCoord,
543 ) -> Result<bool, DbError> {
544 StoreDatabase::verified_circle_control_covers_on(
545 crate::store::store_session::StoreRecords::new(self.conn, self.store_dir),
546 self.verified_store_authority,
547 root,
548 circle_id,
549 current,
550 prior,
551 )
552 }
553
554 fn verify_circle_bootstrap_blob_authority(
555 &mut self,
556 root: &coven_protocol::store_commit::StoreRootRef,
557 current: &coven_protocol::circle::PreparedCircleControl,
558 blobs: &[coven_protocol::blob::RowBlobRef],
559 activations: &[coven_protocol::circle_activation::VerifiedCircleReference],
560 ) -> Result<(), DbError> {
561 let records = StoreRecords::new(self.conn, self.store_dir);
562 for binding in blobs {
563 let coven_protocol::blob::RowBlobAuthority::Remote(
564 coven_protocol::audience_package::PackageAudience::Circle {
565 circle_id,
566 control,
567 key_fingerprint,
568 },
569 ) = binding.authority()
570 else {
571 return Err(DbError::Message(
572 "Circle bootstrap row blob lacks Circle package authority".to_string(),
573 ));
574 };
575 let activation = verified_circle_activation_with_prefix_on(
576 records,
577 self.verified_store_authority,
578 root,
579 *circle_id,
580 control,
581 activations,
582 )?
583 .ok_or_else(|| {
584 DbError::Message("Circle bootstrap blob authority is not retained".to_string())
585 })?;
586 if *key_fingerprint != activation.control.value.key_fingerprint()
587 || !verified_circle_control_covers_with_prefix_on(
588 records,
589 self.verified_store_authority,
590 root,
591 *circle_id,
592 current,
593 control,
594 activations,
595 )?
596 {
597 return Err(DbError::Message(
598 "Circle bootstrap blob authority is outside its control history".to_string(),
599 ));
600 }
601 }
602 Ok(())
603 }
604}
605
606impl StoreDatabase {
607 pub async fn verify_circle_bootstrap_blob_authority(
610 &self,
611 root: coven_protocol::store_commit::StoreRootRef,
612 current: coven_protocol::circle::PreparedCircleControl,
613 blobs: Vec<coven_protocol::blob::RowBlobRef>,
614 activations: Vec<coven_protocol::circle_activation::VerifiedCircleReference>,
615 ) -> Result<(), DbError> {
616 self.call_store(move |session| {
617 session.verify_circle_bootstrap_blob_authority(&root, ¤t, &blobs, &activations)
618 })
619 .await
620 }
621
622 pub async fn circle_control_covers_strictly(
628 &self,
629 root: coven_protocol::store_commit::StoreRootRef,
630 circle_id: coven_protocol::circle::CircleId,
631 covering: &coven_protocol::circle::CircleControlCoord,
632 covered: &coven_protocol::circle::CircleControlCoord,
633 ) -> Result<bool, DbError> {
634 if covering == covered {
635 return Ok(false);
636 }
637 let covering = covering.clone();
638 let covered = covered.clone();
639 self.call_store(move |session| {
640 session.circle_control_covers_strictly(&root, circle_id, &covering, &covered)
641 })
642 .await
643 }
644
645 pub async fn circle_epoch_access(
646 &self,
647 root: coven_protocol::store_commit::StoreRootRef,
648 circle_id: coven_protocol::circle::CircleId,
649 expected_control: coven_protocol::circle::CircleControlCoord,
650 ) -> Result<Option<coven_protocol::circle_activation::CircleEpochAccess>, DbError> {
651 self.call_store(move |session| {
652 session.circle_epoch_access(&root, circle_id, &expected_control)
653 })
654 .await
655 }
656
657 pub async fn circle_package_access(
658 &self,
659 root: coven_protocol::store_commit::StoreRootRef,
660 circle_id: coven_protocol::circle::CircleId,
661 expected_control: coven_protocol::circle::CircleControlCoord,
662 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
663 activations: Vec<coven_protocol::circle_activation::VerifiedCircleReference>,
664 ) -> Result<Option<CirclePackageAccess>, DbError> {
665 self.call_store(move |session| {
666 session.circle_package_access(
667 &root,
668 circle_id,
669 &expected_control,
670 expected_key_fingerprint,
671 &activations,
672 )
673 })
674 .await
675 }
676
677 pub async fn verified_circle_activation_context(
678 &self,
679 root: coven_protocol::store_commit::StoreRootRef,
680 circle_id: coven_protocol::circle::CircleId,
681 control: coven_protocol::circle::CircleControlCoord,
682 ) -> Result<
683 Option<(
684 coven_protocol::circle_activation::VerifiedCircleReference,
685 StoreBatchCommitRef,
686 )>,
687 DbError,
688 > {
689 self.call_store(move |session| {
690 session.verified_circle_activation_context(&root, circle_id, &control)
691 })
692 .await
693 }
694
695 pub async fn circle_blob_opening_protection(
696 &self,
697 root: coven_protocol::store_commit::StoreRootRef,
698 circle_id: coven_protocol::circle::CircleId,
699 expected_control: coven_protocol::circle::CircleControlCoord,
700 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
701 ) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
702 self.call_store(move |session| {
703 session.circle_blob_opening_protection(
704 &root,
705 circle_id,
706 &expected_control,
707 expected_key_fingerprint,
708 )
709 })
710 .await
711 }
712
713 pub async fn verified_circle_activation(
714 &self,
715 root: coven_protocol::store_commit::StoreRootRef,
716 circle_id: coven_protocol::circle::CircleId,
717 control: coven_protocol::circle::CircleControlCoord,
718 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
719 self.call_store(move |session| {
720 session.verified_circle_activation(&root, circle_id, &control)
721 })
722 .await
723 }
724
725 pub async fn circle_restore_head(
726 &self,
727 root: coven_protocol::store_commit::StoreRootRef,
728 circle_id: coven_protocol::circle::CircleId,
729 controls: Vec<coven_protocol::circle::CircleControlCoord>,
730 ) -> Result<
731 Option<(
732 coven_protocol::circle::CircleControlCoord,
733 StoreBatchCommitRef,
734 )>,
735 DbError,
736 > {
737 self.call_store(move |session| session.circle_restore_head(&root, circle_id, &controls))
738 .await
739 }
740
741 pub async fn retained_circle_activation_commit_ref(
742 &self,
743 circle_id: coven_protocol::circle::CircleId,
744 control: coven_protocol::circle::CircleControlCoord,
745 ) -> Result<Option<StoreBatchCommitRef>, DbError> {
746 self.call_store(move |session| {
747 session.retained_circle_activation_commit_ref(circle_id, &control)
748 })
749 .await
750 }
751
752 pub async fn verified_circle_control_coord_covers(
753 &self,
754 root: coven_protocol::store_commit::StoreRootRef,
755 circle_id: coven_protocol::circle::CircleId,
756 covering: coven_protocol::circle::CircleControlCoord,
757 covered: coven_protocol::circle::CircleControlCoord,
758 ) -> Result<bool, DbError> {
759 self.call_store(move |session| {
760 session.verified_circle_control_coord_covers(&root, circle_id, &covering, &covered)
761 })
762 .await
763 }
764
765 pub(super) fn head_circle_control_on(
772 records: StoreRecords<'_>,
773 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
774 root: &coven_protocol::store_commit::StoreRootRef,
775 circle_id: coven_protocol::circle::CircleId,
776 controls: &[coven_protocol::circle::CircleControlCoord],
777 ) -> Result<Option<coven_protocol::circle::CircleControlCoord>, DbError> {
778 let mut retained: Vec<(
781 coven_protocol::circle::CircleControlCoord,
782 coven_protocol::circle::PreparedCircleControl,
783 )> = Vec::new();
784 for coord in controls {
785 let Some(activation_commit) =
786 records.retained_circle_activation_commit_ref(circle_id, coord)?
787 else {
788 continue;
789 };
790 let materialization =
791 authority.retained_materialization_by_ref_on(records, &activation_commit)?;
792 if materialization.root() != root {
793 return Err(DbError::Message(
794 "Circle activation belongs to another Store root".to_string(),
795 ));
796 }
797 let reference = materialization.circle_activation(circle_id, coord)?;
798 retained.push((coord.clone(), reference.control));
799 }
800 let mut head: Option<coven_protocol::circle::CircleControlCoord> = None;
801 for (index, (candidate, _)) in retained.iter().enumerate() {
802 let mut covered = false;
803 for (other_index, (_, other_control)) in retained.iter().enumerate() {
804 if other_index == index {
805 continue;
806 }
807 if Self::verified_circle_control_covers_on(
808 records,
809 authority,
810 root,
811 circle_id,
812 other_control,
813 candidate,
814 )? {
815 covered = true;
816 break;
817 }
818 }
819 if !covered {
820 if head.is_some() {
821 return Err(DbError::Message(format!(
822 "Circle {circle_id} has multiple head controls"
823 )));
824 }
825 head = Some(candidate.clone());
826 }
827 }
828 Ok(head)
829 }
830
831 pub(super) fn verified_circle_activation_on(
832 records: StoreRecords<'_>,
833 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
834 root: &coven_protocol::store_commit::StoreRootRef,
835 circle_id: coven_protocol::circle::CircleId,
836 control: &coven_protocol::circle::CircleControlCoord,
837 ) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
838 let Some(activation_commit) = records.circle_activation_commit_ref(circle_id, control)?
839 else {
840 return Ok(None);
841 };
842 let retained = authority.retained_materialization_by_ref_on(records, &activation_commit)?;
843 if retained.root() != root {
844 return Err(DbError::Message(
845 "Circle activation belongs to another Store root".to_string(),
846 ));
847 }
848 retained.circle_activation(circle_id, control).map(Some)
849 }
850
851 pub async fn verified_circle_control_covers(
852 &self,
853 root: coven_protocol::store_commit::StoreRootRef,
854 circle_id: coven_protocol::circle::CircleId,
855 current: coven_protocol::circle::PreparedCircleControl,
856 prior: coven_protocol::circle::CircleControlCoord,
857 ) -> Result<bool, DbError> {
858 self.call_store(move |session| {
859 session.verified_circle_control_covers(&root, circle_id, ¤t, &prior)
860 })
861 .await
862 }
863
864 pub(super) fn verified_circle_control_covers_on(
865 records: StoreRecords<'_>,
866 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
867 root: &coven_protocol::store_commit::StoreRootRef,
868 circle_id: coven_protocol::circle::CircleId,
869 current: &coven_protocol::circle::PreparedCircleControl,
870 prior: &coven_protocol::circle::CircleControlCoord,
871 ) -> Result<bool, DbError> {
872 verified_circle_control_covers_with_prefix_on(
873 records,
874 authority,
875 root,
876 circle_id,
877 current,
878 prior,
879 &[],
880 )
881 }
882}
883
884fn verified_circle_activation_with_prefix_on(
885 records: StoreRecords<'_>,
886 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
887 root: &coven_protocol::store_commit::StoreRootRef,
888 circle_id: coven_protocol::circle::CircleId,
889 coordinate: &coven_protocol::circle::CircleControlCoord,
890 activations: &[coven_protocol::circle_activation::VerifiedCircleReference],
891) -> Result<Option<coven_protocol::circle_activation::VerifiedCircleReference>, DbError> {
892 let mut matching = activations.iter().filter(|activation| {
893 activation.circle_id == circle_id && &activation.control.coord == coordinate
894 });
895 if let Some(activation) = matching.next() {
896 if activation.control.value.store_root_hash != root.store_root_hash
897 || activation.reference.circle_id() != circle_id
898 || activation.reference.control() != coordinate
899 || !activation.control.verify()
900 {
901 return Err(DbError::Message(
902 "prepared Circle lineage differs from its Store or control reference".to_string(),
903 ));
904 }
905 if matching.any(|other| other != activation) {
906 return Err(DbError::Message(format!(
907 "Circle {circle_id} prepared history has conflicting copies of control {coordinate:?}"
908 )));
909 }
910 return Ok(Some(activation.clone()));
911 }
912 StoreDatabase::verified_circle_activation_on(records, authority, root, circle_id, coordinate)
913}
914
915fn verified_circle_control_covers_with_prefix_on(
916 records: StoreRecords<'_>,
917 authority: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
918 root: &coven_protocol::store_commit::StoreRootRef,
919 circle_id: coven_protocol::circle::CircleId,
920 current: &coven_protocol::circle::PreparedCircleControl,
921 prior: &coven_protocol::circle::CircleControlCoord,
922 activations: &[coven_protocol::circle_activation::VerifiedCircleReference],
923) -> Result<bool, DbError> {
924 if current.value.circle_id != circle_id {
925 return Err(DbError::Message(
926 "Circle control lineage starts outside its Circle".to_string(),
927 ));
928 }
929 if current.coord == *prior {
930 return Ok(true);
931 }
932 let mut pending = current
933 .value
934 .access_epoch()
935 .covered_control_heads
936 .iter()
937 .map(|head| (current.clone(), head.coord.clone()))
938 .collect::<Vec<_>>();
939 let mut visited = std::collections::BTreeSet::new();
940 while let Some((successor, coordinate)) = pending.pop() {
941 if !visited.insert(coordinate.clone()) {
942 continue;
943 }
944 let predecessor = verified_circle_activation_with_prefix_on(
945 records,
946 authority,
947 root,
948 circle_id,
949 &coordinate,
950 activations,
951 )?
952 .ok_or_else(|| {
953 DbError::Message(format!(
954 "Circle {circle_id} control lineage omits retained control {coordinate:?}"
955 ))
956 })?;
957 if !successor.value.causally_covers(&predecessor.control.value) {
958 return Err(DbError::Message(format!(
959 "Circle {circle_id} control lineage contains a non-causal edge"
960 )));
961 }
962 if predecessor.control.coord == *prior {
963 return Ok(true);
964 }
965 pending.extend(
966 predecessor
967 .control
968 .value
969 .access_epoch()
970 .covered_control_heads
971 .iter()
972 .map(|head| (predecessor.control.clone(), head.coord.clone())),
973 );
974 }
975 Ok(false)
976}
977
978pub(crate) fn circle_blob_opening_protection_on(
979 records: StoreRecords<'_>,
980 verified_store: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
981 root: &coven_protocol::store_commit::StoreRootRef,
982 circle_id: coven_protocol::circle::CircleId,
983 expected_control: &coven_protocol::circle::CircleControlCoord,
984 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
985) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
986 let Some(authority) = StoreDatabase::verified_circle_activation_on(
987 records,
988 verified_store,
989 root,
990 circle_id,
991 expected_control,
992 )?
993 else {
994 return Err(DbError::Message(format!(
995 "Circle {circle_id} has no retained authority for control {expected_control:?}"
996 )));
997 };
998 if authority.control.value.key_fingerprint() != expected_key_fingerprint {
999 return Err(DbError::Message(format!(
1000 "Circle {circle_id} blob key {expected_key_fingerprint} differs from \
1001 exact control {expected_control:?}"
1002 )));
1003 }
1004
1005 let controls = records.circle_controls(circle_id)?;
1006
1007 let mut retained_key = None;
1008 for control in controls {
1009 let activation = StoreDatabase::verified_circle_activation_on(
1010 records,
1011 verified_store,
1012 root,
1013 circle_id,
1014 &control,
1015 )?
1016 .ok_or_else(|| {
1017 DbError::Message(format!(
1018 "Circle {circle_id} activation index lost control {control:?}"
1019 ))
1020 })?;
1021 let Some((generation, key)) = activation
1022 .retained_key_entry(expected_key_fingerprint)
1023 .map_err(DbError::from)?
1024 else {
1025 continue;
1026 };
1027 let candidate = EncryptionService::from_key_at_generation(generation, key);
1028 if retained_key
1029 .as_ref()
1030 .is_some_and(|existing: &EncryptionService| {
1031 existing.current_generation() != generation || existing.key_bytes() != key
1032 })
1033 {
1034 return Err(DbError::Message(format!(
1035 "Circle {circle_id} retains inconsistent key material for fingerprint \
1036 {expected_key_fingerprint}"
1037 )));
1038 }
1039 retained_key = Some(candidate);
1040 }
1041 retained_key
1042 .map(coven_protocol::objects::BlobSpoolProtection::Opaque)
1043 .ok_or_else(|| {
1044 DbError::Message(format!(
1045 "Circle {circle_id} retains no local key for fingerprint \
1046 {expected_key_fingerprint}"
1047 ))
1048 })
1049}
1050
1051impl crate::store::store_session::StoreTransaction<'_, '_> {
1052 pub(super) fn circle_blob_opening_protection(
1053 self,
1054 verified_store: &mut dyn super::verified_store_authority::VerifiedStoreLookup,
1055 root: &coven_protocol::store_commit::StoreRootRef,
1056 circle_id: coven_protocol::circle::CircleId,
1057 expected_control: &coven_protocol::circle::CircleControlCoord,
1058 expected_key_fingerprint: coven_keys::encryption::KeyFingerprint,
1059 ) -> Result<coven_protocol::objects::BlobSpoolProtection, DbError> {
1060 circle_blob_opening_protection_on(
1061 crate::store::store_session::StoreRecords::new(self.transaction, self.store_dir),
1062 verified_store,
1063 root,
1064 circle_id,
1065 expected_control,
1066 expected_key_fingerprint,
1067 )
1068 }
1069}